feat(Storybook): read Figma links directly from pages

This commit is contained in:
Michał Cieślak 2023-10-03 11:48:40 +02:00 committed by Michał
parent 158bb87b4a
commit 853641fb89
15 changed files with 71 additions and 878 deletions

View File

@ -45,11 +45,9 @@ set(PROJECT_LIB "${PROJECT_NAME}Lib")
add_library(${PROJECT_LIB} add_library(${PROJECT_LIB}
cachecleaner.cpp cachecleaner.h cachecleaner.cpp cachecleaner.h
directorieswatcher.cpp directorieswatcher.h directorieswatcher.cpp directorieswatcher.h
figmadecoratormodel.cpp figmadecoratormodel.h
figmaio.cpp figmaio.h figmaio.cpp figmaio.h
figmalinks.cpp figmalinks.h figmalinks.cpp figmalinks.h
figmalinksmodel.cpp figmalinksmodel.h figmalinksmodel.cpp figmalinksmodel.h
figmalinkssource.cpp figmalinkssource.h
modelutils.cpp modelutils.h modelutils.cpp modelutils.h
pagesmodel.h pagesmodel.cpp pagesmodel.h pagesmodel.cpp
sectionsdecoratormodel.cpp sectionsdecoratormodel.h sectionsdecoratormodel.cpp sectionsdecoratormodel.h
@ -99,10 +97,6 @@ add_executable(SectionsDecoratorModelTest tests/tst_SectionsDecoratorModel.cpp)
target_link_libraries(SectionsDecoratorModelTest PRIVATE Qt5::Test ${PROJECT_LIB}) target_link_libraries(SectionsDecoratorModelTest PRIVATE Qt5::Test ${PROJECT_LIB})
add_test(NAME SectionsDecoratorModelTest COMMAND SectionsDecoratorModelTest) add_test(NAME SectionsDecoratorModelTest COMMAND SectionsDecoratorModelTest)
add_executable(FigmaDecoratorModelTest tests/tst_FigmaDecoratorModel.cpp)
target_link_libraries(FigmaDecoratorModelTest PRIVATE Qt5::Test ${PROJECT_LIB})
add_test(NAME FigmaModelTest COMMAND FigmaModelTest)
add_executable(QmlTests add_executable(QmlTests
qmlTests/main.cpp qmlTests/main.cpp
qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h

View File

@ -1,94 +0,0 @@
#include "figmadecoratormodel.h"
#include "figmalinks.h"
#include "figmalinksmodel.h"
#include "modelutils.h"
FigmaDecoratorModel::FigmaDecoratorModel(QObject *parent)
: QIdentityProxyModel{parent}
{
}
QHash<int, QByteArray> FigmaDecoratorModel::roleNames() const
{
auto roles = QIdentityProxyModel::roleNames();
roles.insert(FigmaRole, QByteArrayLiteral("figma"));
return roles;
}
QVariant FigmaDecoratorModel::data(const QModelIndex &proxyIndex, int role) const
{
if (!checkIndex(proxyIndex, CheckIndexOption::IndexIsValid))
return {};
if (role == FigmaRole) {
static FigmaLinksModel empty({});
if (!m_titleRole)
return QVariant::fromValue(&empty);
const auto title = data(proxyIndex, m_titleRole.value()).toString();
auto it = m_submodels.find(title);
if (it == m_submodels.end()) {
QStringList links;
if (m_figmaLinks)
links = m_figmaLinks->getLinksMap().value(title, {});
auto linksModel = new FigmaLinksModel(
links, const_cast<FigmaDecoratorModel*>(this));
it = m_submodels.insert(title, linksModel);
}
return QVariant::fromValue(it.value());
}
return QIdentityProxyModel::data(proxyIndex, role);
}
FigmaLinks* FigmaDecoratorModel::getFigmaLinks() const
{
return m_figmaLinks;
}
void FigmaDecoratorModel::setFigmaLinks(FigmaLinks *figmaLinks)
{
if (figmaLinks == m_figmaLinks)
return;
m_figmaLinks = figmaLinks;
const auto& linksMap = m_figmaLinks
? m_figmaLinks->getLinksMap()
: QMap<QString, QStringList>{};
auto linksIt = linksMap.constBegin();
while (linksIt != linksMap.constEnd()) {
if (m_submodels.contains(linksIt.key()))
m_submodels.value(linksIt.key())->setContent(linksIt.value());
++linksIt;
}
auto submodelsIt = m_submodels.constBegin();
while (submodelsIt != m_submodels.constEnd()) {
if (!linksMap.contains(submodelsIt.key()))
submodelsIt.value()->setContent({});
++submodelsIt;
}
emit figmaLinksChanged();
}
void FigmaDecoratorModel::setSourceModel(QAbstractItemModel *sourceModel)
{
qDeleteAll(m_submodels);
m_submodels.clear();
m_titleRole = ModelUtils::findRole(QByteArrayLiteral("title"), sourceModel);
if(!m_titleRole)
qWarning("The source model is missing title role!");
QIdentityProxyModel::setSourceModel(sourceModel);
}

View File

@ -1,34 +0,0 @@
#pragma once
#include <QIdentityProxyModel>
#include <optional>
class FigmaLinks;
class FigmaLinksModel;
class FigmaDecoratorModel : public QIdentityProxyModel
{
Q_OBJECT
Q_PROPERTY(FigmaLinks* figmaLinks READ getFigmaLinks
WRITE setFigmaLinks NOTIFY figmaLinksChanged)
public:
static constexpr auto FigmaRole = Qt::UserRole + 100;
explicit FigmaDecoratorModel(QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &proxyIndex, int role) const override;
FigmaLinks* getFigmaLinks() const;
void setFigmaLinks(FigmaLinks *figmaLinks);
void setSourceModel(QAbstractItemModel *sourceModel) override;
signals:
void figmaLinksChanged();
private:
std::optional<int> m_titleRole;
FigmaLinks* m_figmaLinks = nullptr;
mutable QMap<QString, FigmaLinksModel*> m_submodels;
};

View File

@ -1,139 +0,0 @@
#include "figmalinkssource.h"
#include <QQmlEngine>
#include "figmaio.h"
#include "figmalinks.h"
FigmaLinksSource::FigmaLinksSource(QObject *parent)
: QObject{parent}
{
connect(&m_watcher, &QFileSystemWatcher::fileChanged,
this, [this](const QString &path) {
this->readFile();
if (!this->m_watcher.files().contains(path))
this->m_watcher.addPath(path);
});
}
const QUrl& FigmaLinksSource::getFilePath() const
{
return m_filePath;
}
void FigmaLinksSource::setFilePath(const QUrl& path)
{
if (path == m_filePath)
return;
m_filePath = path;
readFile();
setupWatcher();
emit filePathChanged();
}
FigmaLinks* FigmaLinksSource::getFigmaLinks() const
{
return m_figmaLinks;
}
void FigmaLinksSource::remove(const QString &key, const QList<int> &indexes)
{
if (m_filePath.isEmpty()) {
qWarning("FigmaLinksSource::remove - file path is not set!");
return;
}
QMap<QString, QStringList> linksMap;
if (m_figmaLinks)
linksMap = m_figmaLinks->getLinksMap();
auto it = linksMap.find(key);
if (it == linksMap.end()) {
qWarning("FigmaLinksSource::remove - provided key doesn't exist!");
return;
}
if (indexes.isEmpty())
return;
auto indexesSorted = indexes;
std::sort(indexesSorted.begin(), indexesSorted.end());
if (std::adjacent_find(indexesSorted.cbegin(), indexesSorted.cend())
!= indexesSorted.cend()) {
qWarning("FigmaLinksSource::remove - provided indexes list contains duplicates!");
return;
}
auto& linksList = it.value();
if (indexesSorted.first() < 0 || indexesSorted.last() >= linksList.size()) {
qWarning("FigmaLinksSource::remove - at least one provided index is out of range!");
return;
}
if (linksList.size() == indexesSorted.size()) {
linksMap.erase(it);
} else {
std::for_each(std::crbegin(indexesSorted), std::crend(indexesSorted),
[&linksList](int idx) {
linksList.removeAt(idx);
});
}
FigmaIO::write(m_filePath.path(), linksMap);
}
void FigmaLinksSource::append(const QString &key, const QList<QString> &links)
{
QMap<QString, QStringList> linksMap;
if (m_filePath.isEmpty()) {
qWarning("FigmaLinksSource::append - file path is not set!");
return;
}
if (m_figmaLinks)
linksMap = m_figmaLinks->getLinksMap();
linksMap[key].append(links);
FigmaIO::write(m_filePath.path(), linksMap);
}
void FigmaLinksSource::updateFigmaLinks(const QMap<QString, QStringList>& map)
{
FigmaLinks *mapping = new FigmaLinks(map, this);
if (m_figmaLinks && qjsEngine(m_figmaLinks)) {
m_figmaLinks->setParent(nullptr);
QQmlEngine::setObjectOwnership(m_figmaLinks, QQmlEngine::JavaScriptOwnership);
}
m_figmaLinks = mapping;
emit figmaLinksChanged();
}
void FigmaLinksSource::readFile()
{
QMap<QString, QStringList> figmaLinks = FigmaIO::read(m_filePath.path());
updateFigmaLinks(figmaLinks);
}
void FigmaLinksSource::setupWatcher()
{
auto currentlyWatched = m_watcher.files();
if (!currentlyWatched.isEmpty())
m_watcher.removePaths(currentlyWatched);
if (m_filePath.isEmpty())
return;
m_watcher.addPath(m_filePath.path());
}

View File

@ -1,37 +0,0 @@
#pragma once
#include <QObject>
#include <QFileSystemWatcher>
#include <QUrl>
class FigmaLinks;
class FigmaLinksSource : public QObject
{
Q_OBJECT
Q_PROPERTY(QUrl filePath READ getFilePath WRITE setFilePath NOTIFY filePathChanged)
Q_PROPERTY(FigmaLinks* figmaLinks READ getFigmaLinks NOTIFY figmaLinksChanged)
public:
explicit FigmaLinksSource(QObject *parent = nullptr);
const QUrl& getFilePath() const;
void setFilePath(const QUrl& path);
FigmaLinks* getFigmaLinks() const;
Q_INVOKABLE void remove(const QString &key, const QList<int> &indexes);
Q_INVOKABLE void append(const QString &key, const QList<QString> &links);
signals:
void filePathChanged();
void figmaLinksChanged();
private:
void updateFigmaLinks(const QMap<QString, QStringList>& map);
void readFile();
void setupWatcher();
FigmaLinks *m_figmaLinks = nullptr;
QUrl m_filePath;
QFileSystemWatcher m_watcher;
};

View File

@ -4,9 +4,7 @@
#include "cachecleaner.h" #include "cachecleaner.h"
#include "directorieswatcher.h" #include "directorieswatcher.h"
#include "figmadecoratormodel.h"
#include "figmalinks.h" #include "figmalinks.h"
#include "figmalinkssource.h"
#include "pagesmodel.h" #include "pagesmodel.h"
#include "sectionsdecoratormodel.h" #include "sectionsdecoratormodel.h"
@ -46,8 +44,6 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty( engine.rootContext()->setContextProperty(
"pagesFolder", QML_IMPORT_ROOT + QStringLiteral("/pages")); "pagesFolder", QML_IMPORT_ROOT + QStringLiteral("/pages"));
qmlRegisterType<FigmaDecoratorModel>("Storybook", 1, 0, "FigmaDecoratorModel");
qmlRegisterType<FigmaLinksSource>("Storybook", 1, 0, "FigmaLinksSource");
qmlRegisterType<PagesModelInitialized>("Storybook", 1, 0, "PagesModel"); qmlRegisterType<PagesModelInitialized>("Storybook", 1, 0, "PagesModel");
qmlRegisterType<SectionsDecoratorModel>("Storybook", 1, 0, "SectionsDecoratorModel"); qmlRegisterType<SectionsDecoratorModel>("Storybook", 1, 0, "SectionsDecoratorModel");
qmlRegisterUncreatableType<FigmaLinks>("Storybook", 1, 0, "FigmaLinks", {}); qmlRegisterUncreatableType<FigmaLinks>("Storybook", 1, 0, "FigmaLinks", {});

View File

@ -1,6 +1,6 @@
import QtQuick 2.14 import QtQuick 2.15
import QtQuick.Controls 2.14 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.15
import Qt.labs.settings 1.0 import Qt.labs.settings 1.0
@ -78,19 +78,6 @@ ApplicationWindow {
id: pagesModel id: pagesModel
} }
FigmaLinksSource {
id: figmaLinksSource
filePath: "figma.json"
}
FigmaDecoratorModel {
id: figmaModel
sourceModel: pagesModel
figmaLinks: figmaLinksSource.figmaLinks
}
HotReloader { HotReloader {
id: reloader id: reloader
@ -212,7 +199,7 @@ ApplicationWindow {
id: currentPageModelItem id: currentPageModelItem
model: SingleItemProxyModel { model: SingleItemProxyModel {
sourceModel: figmaModel sourceModel: pagesModel
roleName: "title" roleName: "title"
value: root.currentPage value: root.currentPage
} }
@ -320,9 +307,6 @@ Tips:
figmaLinksCache: figmaImageLinksCache figmaLinksCache: figmaImageLinksCache
} }
onRemoveLinksRequested: figmaLinksSource.remove(pageTitle, indexes)
onAppendLinksRequested: figmaLinksSource.append(pageTitle, links)
onClosing: Qt.callLater(destroy) onClosing: Qt.callLater(destroy)
} }
} }

View File

@ -1,6 +1,6 @@
import QtQuick 2.14 import QtQuick 2.15
import QtQuick.Controls 2.14 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.15
import Storybook 1.0 import Storybook 1.0
@ -50,64 +50,14 @@ Pane {
} }
} }
ColumnLayout {
anchors.fill: parent
RowLayout {
Layout.fillWidth: true
CheckBox {
id: selectableCheckBox
Layout.alignment: Qt.AlignVCenter
text: "selectable"
}
ToolSeparator {
Layout.alignment: Qt.AlignVCenter
}
Label {
id: selectionText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
property string selectionAsString: ""
text: `selected indexes: [${selectionAsString}]`
Connections {
target: grid.selection
function onSelectionChanged() {
const indexes = grid.selection.selectedIndexes
const rows = indexes.map(idx => idx.row)
selectionText.selectionAsString = rows.join(", ")
}
}
}
Button {
text: "Clear selection"
onClicked: grid.selection.clear()
}
}
ImagesGridView { ImagesGridView {
id: grid id: grid
Layout.fillWidth: true anchors.fill: parent
Layout.fillHeight: true
selectable: selectableCheckBox.checked
clip: true clip: true
model: imagesModel model: imagesModel
} }
}
} }
// category: Components // category: Components

View File

@ -4,6 +4,8 @@
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QRegularExpression> #include <QRegularExpression>
#include <unordered_map>
namespace { namespace {
const auto categoryUncategorized QStringLiteral("Uncategorized"); const auto categoryUncategorized QStringLiteral("Uncategorized");
} }
@ -15,6 +17,10 @@ PagesModel::PagesModel(const QString &path, QObject *parent)
m_items = load(); m_items = load();
readMetadata(m_items); readMetadata(m_items);
for (const auto& item : qAsConst(m_items)) {
setFigmaLinks(item.title, item.figmaLinks);
}
fsWatcher->addPath(path); fsWatcher->addPath(path);
connect(fsWatcher, &QFileSystemWatcher::directoryChanged, connect(fsWatcher, &QFileSystemWatcher::directoryChanged,
@ -62,6 +68,21 @@ void PagesModel::readMetadata(PagesModelItem& item) {
? categoryMatch.captured(2).trimmed() : categoryUncategorized; ? categoryMatch.captured(2).trimmed() : categoryUncategorized;
item.category = category; item.category = category;
static QRegularExpression figmaRegex(
"^(\\/\\/\\s*)((?:https:\\/\\/)?(?:www\\.)?figma\\.com\\/.*)$",
QRegularExpression::MultilineOption);
QRegularExpressionMatchIterator i = figmaRegex.globalMatch(content);
QStringList links;
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
QString link = match.captured(2);
links << link;
}
item.figmaLinks = links;
} }
void PagesModel::readMetadata(QList<PagesModelItem> &items) { void PagesModel::readMetadata(QList<PagesModelItem> &items) {
@ -129,6 +150,7 @@ void PagesModel::reload() {
auto index = std::distance(m_items.begin(), it); auto index = std::distance(m_items.begin(), it);
const auto& previous = *it; const auto& previous = *it;
readMetadata(item); readMetadata(item);
setFigmaLinks(item.title, item.figmaLinks);
if (previous.category != item.category) { if (previous.category != item.category) {
// For simplicity category change is handled by removing and // For simplicity category change is handled by removing and
@ -149,7 +171,8 @@ QHash<int, QByteArray> PagesModel::roleNames() const
{ {
static const QHash<int,QByteArray> roles { static const QHash<int,QByteArray> roles {
{ TitleRole, QByteArrayLiteral("title") }, { TitleRole, QByteArrayLiteral("title") },
{ CategoryRole, QByteArrayLiteral("category") } { CategoryRole, QByteArrayLiteral("category") },
{ FigmaRole, QByteArrayLiteral("figma") }
}; };
return roles; return roles;
@ -167,8 +190,28 @@ QVariant PagesModel::data(const QModelIndex &index, int role) const
if (role == TitleRole) if (role == TitleRole)
return m_items.at(index.row()).title; return m_items.at(index.row()).title;
if (role == CategoryRole) if (role == CategoryRole)
return m_items.at(index.row()).category; return m_items.at(index.row()).category;
if (role == FigmaRole) {
auto title = m_items.at(index.row()).title;
auto it = m_figmaSubmodels.find(title);
assert(it != m_figmaSubmodels.end());
return QVariant::fromValue(it.value());
}
return {}; return {};
} }
void PagesModel::setFigmaLinks(const QString& title, const QStringList& links)
{
auto it = m_figmaSubmodels.find(title);
if (it == m_figmaSubmodels.end()) {
m_figmaSubmodels.insert(title, new FigmaLinksModel(links, this));
} else {
it.value()->setContent(links);
}
}

View File

@ -3,6 +3,8 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QDateTime> #include <QDateTime>
#include "figmalinksmodel.h"
class QFileSystemWatcher; class QFileSystemWatcher;
struct PagesModelItem { struct PagesModelItem {
@ -10,6 +12,7 @@ struct PagesModelItem {
QDateTime lastModified; QDateTime lastModified;
QString title; QString title;
QString category; QString category;
QStringList figmaLinks;
}; };
class PagesModel : public QAbstractListModel class PagesModel : public QAbstractListModel
@ -20,7 +23,8 @@ public:
enum Roles { enum Roles {
TitleRole = Qt::UserRole + 1, TitleRole = Qt::UserRole + 1,
CategoryRole CategoryRole,
FigmaRole
}; };
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
@ -34,7 +38,10 @@ private:
static void readMetadata(PagesModelItem &item); static void readMetadata(PagesModelItem &item);
static void readMetadata(QList<PagesModelItem> &items); static void readMetadata(QList<PagesModelItem> &items);
void setFigmaLinks(const QString& title, const QStringList& links);
QString m_path; QString m_path;
QList<PagesModelItem> m_items; QList<PagesModelItem> m_items;
QMap<QString, FigmaLinksModel*> m_figmaSubmodels;
QFileSystemWatcher* fsWatcher; QFileSystemWatcher* fsWatcher;
}; };

View File

@ -1,9 +1,9 @@
import QtQuick 2.14 import QtQuick 2.15
ListModel { ListModel {
id: root id: root
/* required */ property FigmaLinksCache figmaLinksCache required property FigmaLinksCache figmaLinksCache
property alias sourceModel: d.model property alias sourceModel: d.model
readonly property Instantiator _d: Instantiator { readonly property Instantiator _d: Instantiator {
@ -29,7 +29,7 @@ ListModel {
d.idCounter++ d.idCounter++
figmaLinksCache.getImageUrl(model.link, link => { figmaLinksCache.getImageUrl(model.link, link => {
if (delegate) if (delegate && link !== null)
root.setProperty(model.index, "imageLink", link) root.setProperty(model.index, "imageLink", link)
}) })
} }

View File

@ -1,4 +1,4 @@
import QtQml 2.14 import QtQml 2.15
QtObject { QtObject {
id: root id: root

View File

@ -14,10 +14,6 @@ ApplicationWindow {
signal removeLinksRequested(var indexes) signal removeLinksRequested(var indexes)
signal appendLinksRequested(var links) signal appendLinksRequested(var links)
readonly property var urlRegex:
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/
SwipeView { SwipeView {
id: topSwipeView id: topSwipeView
@ -40,37 +36,6 @@ ApplicationWindow {
topSwipeView.incrementCurrentIndex() topSwipeView.incrementCurrentIndex()
} }
} }
footer: ToolBar {
RowLayout {
anchors.fill: parent
Button {
id: removeButton
readonly property int selectionCount:
grid.selection.selectedIndexes.length
text: "Remove selected"
+ (enabled ? ` (${selectionCount})` : "")
enabled: grid.selection.hasSelection
onClicked: removeConfirmDialog.open()
}
ToolSeparator {}
Button {
text: "Add new links"
onClicked: addNewLinksDialog.open()
}
Item {
Layout.fillWidth: true
}
}
}
} }
Item { Item {
@ -106,81 +71,4 @@ ApplicationWindow {
} }
} }
} }
Dialog {
id: removeConfirmDialog
readonly property var selected: grid.selection.selectedIndexes
anchors.centerIn: Overlay.overlay
title: "Links removal"
standardButtons: Dialog.Ok | Dialog.Cancel
Label {
text: "Are you sure that you want to remove "
+ removeButton.selectionCount + " link(s)?"
}
onAccepted: root.removeLinksRequested(selected.map(idx => idx.row))
onSelectedChanged: close()
}
Dialog {
id: addNewLinksDialog
anchors.centerIn: Overlay.overlay
title: "Add new Figma links"
standardButtons: Dialog.Save | Dialog.Cancel
width: parent.width * 0.8
height: parent.height * 0.4
GroupBox {
anchors.fill: parent
title: "Figma links, 1 per line"
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
contentHeight: linksTextEdit.implicitHeight
contentWidth: linksTextEdit.implicitWidth
TextEdit {
id: linksTextEdit
property var links: []
width: scrollView.width
height: scrollView.height
font.pixelSize: 13
selectByMouse: true
onTextChanged: {
const allLines = text.split("\n")
const nonEmptyLines = allLines.filter(
line => line.trim().length > 0)
const trimmed = nonEmptyLines.map(line => line.trim())
links = trimmed.every(line => root.urlRegex.test(line))
? trimmed : []
}
}
}
}
onClosed: Qt.callLater(linksTextEdit.clear)
onAccepted: root.appendLinksRequested(linksTextEdit.links)
Component.onCompleted: {
standardButton(Dialog.Save).enabled
= Qt.binding(() => linksTextEdit.links.length > 0)
}
}
} }

View File

@ -1,6 +1,6 @@
import QtQuick 2.14 import QtQuick 2.15
import QtQuick.Controls 2.14 import QtQuick.Controls 2.15
import QtQml.Models 2.14 import QtQml.Models 2.15
GridView { GridView {
id: root id: root
@ -10,15 +10,6 @@ GridView {
signal clicked(int index) signal clicked(int index)
property bool selectable: true
readonly property alias selection: selectionModel
ItemSelectionModel {
id: selectionModel
model: root.model
}
delegate: Item { delegate: Item {
width: root.cellWidth width: root.cellWidth
height: root.cellHeight height: root.cellHeight
@ -60,21 +51,6 @@ GridView {
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: model.rawLink ToolTip.text: model.rawLink
} }
CheckBox {
visible: root.selectable
anchors.top: parent.top
anchors.right: parent.right
checked: {
selectionModel.selection
return selectionModel.isSelected(root.model.index(index, 0))
}
onToggled: selectionModel.select(root.model.index(index, 0),
ItemSelectionModel.Toggle)
}
} }
} }
} }

View File

@ -1,341 +0,0 @@
#include <QSignalSpy>
#include <QTest>
#include <QTemporaryFile>
#include <QStringListModel>
#include "figmadecoratormodel.h"
#include "figmaio.h"
#include "figmalinks.h"
#include "figmalinksmodel.h"
#include "figmalinkssource.h"
namespace {
auto constexpr sampleJson1 = R"(
{
"Component_1": [
"link_1", "link_2"
],
"Component_2": [
"link_3", "link_4"
]
}
)";
auto constexpr sampleJson2 = R"(
{
"Component_1": [
"link_1"
],
"Component_2": [
"link_3", "link_5"
]
}
)";
class TestSourceModel : public QAbstractListModel {
public:
static constexpr auto TitleRole = 0;
TestSourceModel(int count = 1) : m_count(count) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return m_count;
}
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid())
return {};
return QString("title_%1").arg(index.row());
}
QHash<int, QByteArray> roleNames() const override {
QHash<int, QByteArray> roles;
roles.insert(TitleRole, QByteArrayLiteral("title"));
return roles;
}
int m_count;
};
} // unnamed namespace
class FigmaDecoratorModelTest: public QObject
{
Q_OBJECT
private slots:
void figmaIOTest() {
QTemporaryFile file;
QVERIFY(file.open());
file.close();
const auto readEmpty = FigmaIO::read(file.fileName());
QCOMPARE(readEmpty, {});
const QMap<QString, QStringList> content = {
{ "k_1", { "l_1", "l_2"}},
{ "k_2", { "l_3", "l_4"}}
};
FigmaIO::write(file.fileName(), content);
const auto readContent = FigmaIO::read(file.fileName());
QCOMPARE(readContent, content);
}
void readingFigmaFileTest() {
FigmaLinksSource figmaLinksSource;
QSignalSpy spy(&figmaLinksSource, &FigmaLinksSource::figmaLinksChanged);
QCOMPARE(figmaLinksSource.getFigmaLinks(), nullptr);
QTemporaryFile file;
if (file.open()) {
QTextStream stream(&file);
stream << sampleJson1;
}
figmaLinksSource.setFilePath(file.fileName());
QVERIFY(figmaLinksSource.getFigmaLinks() != nullptr);
const FigmaLinks *links = figmaLinksSource.getFigmaLinks();
QCOMPARE(links->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1", "link_2"}},
{{"Component_2"}, {"link_3", "link_4"}}}));
QCOMPARE(spy.count(), 1);
QTemporaryFile file2;
if (file2.open()) {
QTextStream stream(&file2);
stream << sampleJson2;
}
figmaLinksSource.setFilePath(file2.fileName());
QVERIFY(figmaLinksSource.getFigmaLinks() != nullptr);
const FigmaLinks *links2 = figmaLinksSource.getFigmaLinks();
QCOMPARE(links2->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1"}},
{{"Component_2"}, {"link_3", "link_5"}}}));
QCOMPARE(spy.count(), 2);
}
void readingAfterFigmaFileChangedTest() {
FigmaLinksSource figmaLinksSource;
QSignalSpy spy(&figmaLinksSource, &FigmaLinksSource::figmaLinksChanged);
QCOMPARE(figmaLinksSource.getFigmaLinks(), nullptr);
QTemporaryFile file;
if (file.open()) {
QTextStream stream(&file);
stream << sampleJson1;
}
figmaLinksSource.setFilePath(file.fileName());
QVERIFY(figmaLinksSource.getFigmaLinks() != nullptr);
const FigmaLinks *links = figmaLinksSource.getFigmaLinks();
QCOMPARE(links->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1", "link_2"}},
{{"Component_2"}, {"link_3", "link_4"}}}));
QCOMPARE(spy.count(), 1);
if (QFile f(file.fileName());
f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QTextStream stream(&f);
stream << sampleJson2;
}
QVERIFY(spy.wait());
QCOMPARE(spy.count(), 2);
const FigmaLinks *links2 = figmaLinksSource.getFigmaLinks();
QCOMPARE(links2->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1"}},
{{"Component_2"}, {"link_3", "link_5"}}}));
if (QFile f(file.fileName());
f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QTextStream stream(&f);
stream << sampleJson1;
}
QVERIFY(spy.wait());
QCOMPARE(spy.count(), 3);
const FigmaLinks *links3 = figmaLinksSource.getFigmaLinks();
QCOMPARE(links3->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1", "link_2"}},
{{"Component_2"}, {"link_3", "link_4"}}}));
}
void emptyFigmaModelTest() {
FigmaDecoratorModel model;
QCOMPARE(model.rowCount(), 0);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
}
void figmaModelWithoutSourceModel() {
FigmaLinks links({
{{"Component_1"}, {"link_1", "link_2"}},
{{"Component_2"}, {"link_3", "link_4"}}
});
FigmaDecoratorModel model;
model.setFigmaLinks(&links);
QCOMPARE(model.rowCount(), 0);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
}
void figmaModelWithIncompatibleSourceModel() {
QStringListModel stringsModel({"s1", "s2"});
FigmaDecoratorModel model;
QTest::ignoreMessage(QtWarningMsg,
"The source model is missing title role!");
model.setSourceModel(&stringsModel);
QCOMPARE(model.rowCount(), 2);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
QCOMPARE(model.data(
model.index(0, 0),
FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>()->rowCount(), 0);
}
void figmaModelWithoutLinksTest() {
TestSourceModel sourceModel;
FigmaDecoratorModel model;
model.setSourceModel(&sourceModel);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.roleNames().count(), 2);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
QCOMPARE(model.data(
model.index(0, 0),
FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>()->rowCount(), 0);
}
void figmaModelTest() {
TestSourceModel sourceModel{2};
FigmaLinks links({
{{"title_0"}, {"link_1", "link_2"}},
{{"title_x"}, {"link_3", "link_4"}}
});
FigmaDecoratorModel model;
QSignalSpy spy(&model, &FigmaDecoratorModel::dataChanged);
model.setSourceModel(&sourceModel);
QCOMPARE(spy.size(), 0);
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.roleNames().count(), 2);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QVERIFY(model.roleNames().contains(TestSourceModel::TitleRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
QCOMPARE(model.roleNames().value(
TestSourceModel::TitleRole), QStringLiteral("title"));
QCOMPARE(model.data(model.index(0, 0),
TestSourceModel::TitleRole).toString(), "title_0");
QCOMPARE(model.data(model.index(1, 0),
TestSourceModel::TitleRole).toString(), "title_1");
auto figmaLinksModel1 = model.data(model.index(0, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>();
QVERIFY(figmaLinksModel1 != nullptr);
QCOMPARE(figmaLinksModel1->rowCount(), 0);
auto figmaLinksModel2 = model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>();
QVERIFY(figmaLinksModel2 != nullptr);
QCOMPARE(figmaLinksModel2->rowCount(), 0);
QSignalSpy linksModelspy1(figmaLinksModel1,
&QAbstractItemModel::modelReset);
QSignalSpy linksModelspy2(figmaLinksModel2,
&QAbstractItemModel::modelReset);
model.setFigmaLinks(&links);
QCOMPARE(spy.size(), 0);
QCOMPARE(linksModelspy1.size(), 1);
QCOMPARE(linksModelspy2.size(), 0);
QCOMPARE(model.data(model.index(0, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>(), figmaLinksModel1);
QCOMPARE(model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>(), figmaLinksModel2);
QCOMPARE(figmaLinksModel1->rowCount(), 2);
QCOMPARE(figmaLinksModel2->rowCount(), 0);
QCOMPARE(model.data(model.index(0, 0),
TestSourceModel::TitleRole).toString(), "title_0");
QCOMPARE(model.data(model.index(1, 0),
TestSourceModel::TitleRole).toString(), "title_1");
QCOMPARE(figmaLinksModel1->roleNames().size(), 1);
QCOMPARE(figmaLinksModel2->roleNames().size(), 1);
QCOMPARE(figmaLinksModel1->data(figmaLinksModel1->index(0, 0),
FigmaLinksModel::LinkRole).toString(), "link_1");
QCOMPARE(figmaLinksModel1->data(figmaLinksModel1->index(1, 0),
FigmaLinksModel::LinkRole).toString(), "link_2");
model.setFigmaLinks(nullptr);
QCOMPARE(spy.size(), 0);
QCOMPARE(linksModelspy1.size(), 2);
QCOMPARE(linksModelspy2.size(), 0);
QCOMPARE(model.data(model.index(0, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>(), figmaLinksModel1);
QCOMPARE(model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>(), figmaLinksModel2);
QCOMPARE(figmaLinksModel1->rowCount(), 0);
QCOMPARE(figmaLinksModel2->rowCount(), 0);
}
};
QTEST_MAIN(FigmaDecoratorModelTest)
#include "tst_FigmaDecoratorModel.moc"