From 8e39d761dc89d75ed0d34abacb5d169e8f943505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Fri, 25 Nov 2022 16:41:50 +0100 Subject: [PATCH] feat(Storybook): Figma links loaded from json file Closes: #8187 --- storybook/CMakeLists.txt | 15 +- storybook/PagesModel.qml | 138 -------- storybook/figma.json | 59 ++++ storybook/figmadecoratormodel.cpp | 94 +++++ storybook/figmadecoratormodel.h | 33 ++ storybook/figmaio.cpp | 49 +++ storybook/figmaio.h | 9 + storybook/figmalinks.cpp | 11 + storybook/figmalinks.h | 16 + storybook/figmalinksmodel.cpp | 42 +++ storybook/figmalinksmodel.h | 27 ++ storybook/figmalinkssource.cpp | 71 ++++ storybook/figmalinkssource.h | 34 ++ storybook/main.cpp | 6 + storybook/main.qml | 26 +- storybook/modelutils.cpp | 20 ++ storybook/modelutils.h | 13 + storybook/sectionsdecoratormodel.cpp | 19 +- storybook/sectionsdecoratormodel.h | 1 - .../src/Storybook/FigmaImagesProxyModel.qml | 23 +- .../src/Storybook/ImagesNavigationLayout.qml | 2 +- storybook/tests/tst_FigmaDecoratorModel.cpp | 321 ++++++++++++++++++ 22 files changed, 863 insertions(+), 166 deletions(-) create mode 100644 storybook/figma.json create mode 100644 storybook/figmadecoratormodel.cpp create mode 100644 storybook/figmadecoratormodel.h create mode 100644 storybook/figmaio.cpp create mode 100644 storybook/figmaio.h create mode 100644 storybook/figmalinks.cpp create mode 100644 storybook/figmalinks.h create mode 100644 storybook/figmalinksmodel.cpp create mode 100644 storybook/figmalinksmodel.h create mode 100644 storybook/figmalinkssource.cpp create mode 100644 storybook/figmalinkssource.h create mode 100644 storybook/modelutils.cpp create mode 100644 storybook/modelutils.h create mode 100644 storybook/tests/tst_FigmaDecoratorModel.cpp diff --git a/storybook/CMakeLists.txt b/storybook/CMakeLists.txt index 3c6787286d..0bfec467f6 100644 --- a/storybook/CMakeLists.txt +++ b/storybook/CMakeLists.txt @@ -28,21 +28,34 @@ add_executable( main.cpp cachecleaner.cpp cachecleaner.h directorieswatcher.cpp directorieswatcher.h + figmadecoratormodel.cpp figmadecoratormodel.h + figmaio.cpp figmaio.h + figmalinks.cpp figmalinks.h + figmalinksmodel.cpp figmalinksmodel.h + figmalinkssource.cpp figmalinkssource.h + modelutils.cpp modelutils.h sectionsdecoratormodel.cpp sectionsdecoratormodel.h ${QML_FILES} main.qml PagesModel.qml ${JS_FILES} + figma.json ) target_compile_definitions(${PROJECT_NAME} PRIVATE QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}") + target_link_libraries( ${PROJECT_NAME} PRIVATE Qt5::Core Qt5::Quick Qt5::QuickControls2 SortFilterProxyModel) enable_testing() -add_executable(SectionsDecoratorModelTest tests/tst_SectionsDecoratorModel.cpp sectionsdecoratormodel.cpp) +add_executable(SectionsDecoratorModelTest tests/tst_SectionsDecoratorModel.cpp sectionsdecoratormodel.cpp modelutils.cpp) add_test(NAME SectionsDecoratorModelTest COMMAND SectionsDecoratorModelTest) target_link_libraries(SectionsDecoratorModelTest PRIVATE Qt5::Test) +add_executable(FigmaDecoratorModelTest tests/tst_FigmaDecoratorModel.cpp figmadecoratormodel.cpp figmalinkssource.cpp + figmalinks.cpp figmaio.cpp modelutils.cpp figmalinksmodel.cpp) +add_test(NAME FigmaModelTest COMMAND FigmaModelTest) +target_link_libraries(FigmaDecoratorModelTest PRIVATE Qt5::Test Qt5::Qml) + list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/StatusQ/src") list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/app") list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/imports") diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml index e3b268aa3c..c3131d2ace 100644 --- a/storybook/PagesModel.qml +++ b/storybook/PagesModel.qml @@ -3,39 +3,10 @@ import QtQuick 2.14 ListModel { ListElement { title: "ProfileDialogView" - - figma: [ - ListElement { - link: "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=733%3A12552" - }, - ListElement { - link: "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=682%3A15078" - }, - ListElement { - link: "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=682%3A17655" - }, - ListElement { - link: "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=682%3A17087" - }, - ListElement { - link: "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=4%3A23525" - }, - ListElement { - link: "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=4%3A23932" - } - ] section: "Views" } ListElement { title: "CommunitiesPortalLayout" - figma: [ - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A415655" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A415935" - } - ] section: "Views" } ListElement { @@ -44,67 +15,22 @@ ListModel { } ListElement { title: "LoginView" - - figma: [ - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=1080%3A313192" - } - ] section: "Views" } ListElement { title: "AboutView" - - figma: [ - ListElement { - link: "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1159%3A114479" - }, - ListElement { - link: "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1684%3A127762" - } - ] section: "Views" } ListElement { title: "StatusCommunityCard" - - figma: [ - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A416159" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A416160" - } - ] section: "Panels" } ListElement { title: "CommunityProfilePopupInviteFriendsPanel" - - figma: [ - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2927%3A343592" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2990%3A353179" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2927%3A344073" - } - ] section: "Panels" } ListElement { title: "CommunityProfilePopupInviteMessagePanel" - - figma: [ - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=4291%3A385536" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=4295%3A385958" - } - ] section: "Panels" } ListElement { @@ -113,86 +39,22 @@ ListModel { } ListElement { title: "InviteFriendsToCommunityPopup" - - figma: [ - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2927%3A343592" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2990%3A353179" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2927%3A344073" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=4291%3A385536" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=4295%3A385958" - } - ] section: "Popups" } ListElement { title: "CreateChannelPopup" - - figma: [ - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A488608" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A488256" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2903%3A348301" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A488848" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A489237" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A489607" - }, - ListElement { - link: "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A492910" - } - ] section: "Popups" } - ListElement { title: "MembersSelector" section: "Components" } ListElement { title: "BrowserSettings" - - figma: [ - ListElement { - link: "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=448%3A36296" - }, - ListElement { - link: "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1573%3A296338" - } - ] section: "Settings" } ListElement { title: "LanguageCurrencySettings" - - figma: [ - ListElement { - link: "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=701%3A74776" - }, - ListElement { - link: "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1592%3A112840" - }, - ListElement { - link: "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=701%3A75345" - } - ] section: "Settings" } } diff --git a/storybook/figma.json b/storybook/figma.json new file mode 100644 index 0000000000..66f27c0200 --- /dev/null +++ b/storybook/figma.json @@ -0,0 +1,59 @@ +{ + "ProfileDialogView": [ + "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=733%3A12552", + "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=682%3A15078", + "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=682%3A17655", + "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=682%3A17087", + "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=4%3A23525", + "https://www.figma.com/file/ibJOTPlNtIxESwS96vJb06/%F0%9F%91%A4-Profile-%7C-Desktop?node-id=4%3A23932" + ], + "CommunitiesPortalLayout": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A415655", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A415935" + ], + "LoginView": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=1080%3A313192" + ], + "AboutView": [ + "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1159%3A114479", + "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1684%3A127762" + ], + "StatusCommunityCard": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A416159", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A416160" + ], + "CommunityProfilePopupInviteFriendsPanel": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2927%3A343592", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2990%3A353179", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2927%3A344073" + ], + "CommunityProfilePopupInviteMessagePanel": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=4291%3A385536", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=4295%3A385958" + ], + "InviteFriendsToCommunityPopup": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2927%3A343592", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2990%3A353179", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2927%3A344073", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=4291%3A385536", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=4295%3A385958" + ], + "CreateChannelPopup": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A488608", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A488256", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2903%3A348301", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A488848", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A489237", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A489607", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A492910" + ], + "BrowserSettings": [ + "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=448%3A36296", + "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1573%3A296338" + ], + "LanguageCurrencySettings": [ + "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=701%3A74776", + "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1592%3A112840", + "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=701%3A75345" + ] +} diff --git a/storybook/figmadecoratormodel.cpp b/storybook/figmadecoratormodel.cpp new file mode 100644 index 0000000000..d8e0110afc --- /dev/null +++ b/storybook/figmadecoratormodel.cpp @@ -0,0 +1,94 @@ +#include "figmadecoratormodel.h" + +#include "figmalinks.h" +#include "figmalinksmodel.h" +#include "modelutils.h" + +FigmaDecoratorModel::FigmaDecoratorModel(QObject *parent) + : QIdentityProxyModel{parent} +{ +} + +QHash 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(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{}; + + 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); +} diff --git a/storybook/figmadecoratormodel.h b/storybook/figmadecoratormodel.h new file mode 100644 index 0000000000..c5d8ce1657 --- /dev/null +++ b/storybook/figmadecoratormodel.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +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 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 m_titleRole; + FigmaLinks* m_figmaLinks = nullptr; + mutable QMap m_submodels; +}; diff --git a/storybook/figmaio.cpp b/storybook/figmaio.cpp new file mode 100644 index 0000000000..23f350400d --- /dev/null +++ b/storybook/figmaio.cpp @@ -0,0 +1,49 @@ +#include "figmaio.h" + +#include +#include +#include +#include +#include + +QMap FigmaIO::read(const QString &file) +{ + QFile f(file); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning() << "FigmaIO::read - failed to open file:" << file; + return {}; + } + + QJsonParseError error; + auto jsonDoc = QJsonDocument::fromJson(f.readAll(), &error); + + if (error.error != QJsonParseError::NoError) { + qWarning() << "FigmaIO::read - error parsing json file:" << file + << "->" << error.errorString(); + return {}; + } + + QMap mapping; + + if (jsonDoc.isObject()) { + auto rootObject = jsonDoc.object(); + + auto i = rootObject.constBegin(); + while (i != rootObject.constEnd()) { + QJsonValue val = i.value(); + QJsonArray links = val.toArray(); + + QStringList linksList; + linksList.reserve(links.size()); + + for (const QJsonValue &link : qAsConst(links)) + if (link.isString()) + linksList << link.toString(); + + mapping.insert(i.key(), linksList); + ++i; + } + } + + return mapping; +} diff --git a/storybook/figmaio.h b/storybook/figmaio.h new file mode 100644 index 0000000000..d7e65fed24 --- /dev/null +++ b/storybook/figmaio.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +class FigmaIO +{ +public: + static QMap read(const QString &file); +}; diff --git a/storybook/figmalinks.cpp b/storybook/figmalinks.cpp new file mode 100644 index 0000000000..73d9143bf4 --- /dev/null +++ b/storybook/figmalinks.cpp @@ -0,0 +1,11 @@ +#include "figmalinks.h" + +FigmaLinks::FigmaLinks(const QMap& linksMap, QObject *parent) + : m_linksMap{linksMap}, QObject{parent} +{ +} + +const QMap& FigmaLinks::getLinksMap() const +{ + return m_linksMap; +} diff --git a/storybook/figmalinks.h b/storybook/figmalinks.h new file mode 100644 index 0000000000..aff637c900 --- /dev/null +++ b/storybook/figmalinks.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +class FigmaLinks : public QObject +{ + Q_OBJECT +public: + explicit FigmaLinks(const QMap& mapping, + QObject *parent = nullptr); + const QMap& getLinksMap() const; + +private: + QMap m_linksMap; +}; diff --git a/storybook/figmalinksmodel.cpp b/storybook/figmalinksmodel.cpp new file mode 100644 index 0000000000..ec45f68a39 --- /dev/null +++ b/storybook/figmalinksmodel.cpp @@ -0,0 +1,42 @@ +#include "figmalinksmodel.h" + +FigmaLinksModel::FigmaLinksModel(const QStringList &links, QObject *parent) + : QAbstractListModel{parent}, m_links{links} +{ +} + +int FigmaLinksModel::rowCount(const QModelIndex &parent) const +{ + return m_links.size(); +} + +QVariant FigmaLinksModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid)) + return {}; + + const int row = index.row(); + return m_links.at(row); +} + +QHash FigmaLinksModel::roleNames() const +{ + static QHash roles( + {{LinkRole, QByteArrayLiteral("link")}}); + return roles; +} + +void FigmaLinksModel::setContent(const QStringList &links) +{ + if (m_links == links) + return; + + const auto oldCount = m_links.size(); + + beginResetModel(); + m_links = links; + endResetModel(); + + if (m_links.size() != oldCount) + emit countChanged(); +} diff --git a/storybook/figmalinksmodel.h b/storybook/figmalinksmodel.h new file mode 100644 index 0000000000..0b3932a1af --- /dev/null +++ b/storybook/figmalinksmodel.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +class FigmaLinksModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + +public: + static constexpr auto LinkRole = 0; + + explicit FigmaLinksModel(const QStringList &links, + QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + void setContent(const QStringList &links); + +signals: + void countChanged(); + +private: + QStringList m_links; +}; diff --git a/storybook/figmalinkssource.cpp b/storybook/figmalinkssource.cpp new file mode 100644 index 0000000000..547e790dad --- /dev/null +++ b/storybook/figmalinkssource.cpp @@ -0,0 +1,71 @@ +#include "figmalinkssource.h" + +#include + +#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::updateFigmaLinks(const QMap& 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 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()); +} diff --git a/storybook/figmalinkssource.h b/storybook/figmalinkssource.h new file mode 100644 index 0000000000..cc91e03e54 --- /dev/null +++ b/storybook/figmalinkssource.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +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; + +signals: + void filePathChanged(); + void figmaLinksChanged(); + +private: + void updateFigmaLinks(const QMap& map); + void readFile(); + void setupWatcher(); + + FigmaLinks *m_figmaLinks = nullptr; + QUrl m_filePath; + QFileSystemWatcher m_watcher; +}; diff --git a/storybook/main.cpp b/storybook/main.cpp index 95e4ec7a7d..08e98ccb6c 100644 --- a/storybook/main.cpp +++ b/storybook/main.cpp @@ -3,6 +3,9 @@ #include "cachecleaner.h" #include "directorieswatcher.h" +#include "figmadecoratormodel.h" +#include "figmalinks.h" +#include "figmalinkssource.h" #include "sectionsdecoratormodel.h" int main(int argc, char *argv[]) @@ -35,6 +38,9 @@ int main(int argc, char *argv[]) engine.addImportPath(path); qmlRegisterType("Storybook", 1, 0, "SectionsDecoratorModel"); + qmlRegisterType("Storybook", 1, 0, "FigmaDecoratorModel"); + qmlRegisterType("Storybook", 1, 0, "FigmaLinksSource"); + qmlRegisterUncreatableType("Storybook", 1, 0, "FigmaLinks", ""); auto watcherFactory = [additionalImportPaths](QQmlEngine*, QJSEngine*) { auto watcher = new DirectoriesWatcher(); diff --git a/storybook/main.qml b/storybook/main.qml index b70a3e8fab..72863e377a 100644 --- a/storybook/main.qml +++ b/storybook/main.qml @@ -18,6 +18,23 @@ ApplicationWindow { font.pixelSize: 13 + PagesModel { + id: pagesModel + } + + FigmaLinksSource { + id: figmaLinksSource + + filePath: "figma.json" + } + + FigmaDecoratorModel { + id: figmaModel + + sourceModel: pagesModel + figmaLinks: figmaLinksSource.figmaLinks + } + HotReloader { id: reloader @@ -26,10 +43,6 @@ ApplicationWindow { onReloaded: hotReloaderControls.notifyReload() } - PagesModel { - id: pagesModel - } - SplitView { anchors.fill: parent @@ -125,13 +138,13 @@ ApplicationWindow { title: `pages/${root.currentPage}Page.qml` figmaPagesCount: currentPageModelItem.object - ? currentPageModelItem.object.figmaCount : 0 + ? currentPageModelItem.object.figma.count : 0 Instantiator { id: currentPageModelItem model: SingleItemProxyModel { - sourceModel: pagesModel + sourceModel: figmaModel roleName: "title" value: root.currentPage } @@ -139,7 +152,6 @@ ApplicationWindow { delegate: QtObject { readonly property string title: model.title readonly property var figma: model.figma - readonly property int figmaCount: figma ? figma.count : 0 } } diff --git a/storybook/modelutils.cpp b/storybook/modelutils.cpp new file mode 100644 index 0000000000..3d27943906 --- /dev/null +++ b/storybook/modelutils.cpp @@ -0,0 +1,20 @@ +#include "modelutils.h" + +#include + +std::optional ModelUtils::findRole(const QByteArray &role, + const QAbstractItemModel *model) +{ + if (model == nullptr) + return std::nullopt; + + const auto roleNames = model->roleNames(); + + auto it = std::find_if(roleNames.constKeyValueBegin(), + roleNames.constKeyValueEnd(), [&role](auto entry) { + return entry.second == role; + }); + + return it == roleNames.constKeyValueEnd() + ? std::nullopt : std::make_optional((*it).first); +} diff --git a/storybook/modelutils.h b/storybook/modelutils.h new file mode 100644 index 0000000000..4d07f68a3e --- /dev/null +++ b/storybook/modelutils.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include + +class QAbstractItemModel; + +struct ModelUtils +{ + static std::optional findRole(const QByteArray &role, + const QAbstractItemModel *model); +}; diff --git a/storybook/sectionsdecoratormodel.cpp b/storybook/sectionsdecoratormodel.cpp index 3c1309bce2..cae9b8c881 100644 --- a/storybook/sectionsdecoratormodel.cpp +++ b/storybook/sectionsdecoratormodel.cpp @@ -1,5 +1,7 @@ #include "sectionsdecoratormodel.h" +#include "modelutils.h" + #include SectionsDecoratorModel::SectionsDecoratorModel(QObject *parent) @@ -121,20 +123,6 @@ void SectionsDecoratorModel::calculateOffsets() }); } -std::optional SectionsDecoratorModel::findSectionRole() const -{ - const auto roleNames = m_sourceModel->roleNames(); - auto i = roleNames.constBegin(); - - while (i != roleNames.constEnd()) { - if (i.value() == QStringLiteral("section")) - return i.key(); - ++i; - } - - return std::nullopt; -} - void SectionsDecoratorModel::initialize() { beginResetModel(); @@ -142,7 +130,8 @@ void SectionsDecoratorModel::initialize() m_rowsMetadata.clear(); - const auto sectionRoleOpt = findSectionRole(); + const auto sectionRoleOpt = ModelUtils::findRole( + QByteArrayLiteral("section"), m_sourceModel); if (!sectionRoleOpt) { qWarning("Section role not found!"); diff --git a/storybook/sectionsdecoratormodel.h b/storybook/sectionsdecoratormodel.h index 98e4f69988..218a92a091 100644 --- a/storybook/sectionsdecoratormodel.h +++ b/storybook/sectionsdecoratormodel.h @@ -36,7 +36,6 @@ private: int count = 0; }; - std::optional findSectionRole() const; void initialize(); void calculateOffsets(); diff --git a/storybook/src/Storybook/FigmaImagesProxyModel.qml b/storybook/src/Storybook/FigmaImagesProxyModel.qml index 096246a85d..084c806660 100644 --- a/storybook/src/Storybook/FigmaImagesProxyModel.qml +++ b/storybook/src/Storybook/FigmaImagesProxyModel.qml @@ -1,30 +1,47 @@ import QtQuick 2.14 ListModel { + id: root + /* required */ property FigmaLinksCache figmaLinksCache property alias sourceModel: d.model readonly property Instantiator _d: Instantiator { id: d + property int idCounter: 0 + model: 0 delegate: QtObject { id: delegate + property int uniqueId + Component.onCompleted: { append({ rawLink: model.link, - imageLink: "" + imageLink: "", + uniqueId: d.idCounter }) + uniqueId = d.idCounter + d.idCounter++ + figmaLinksCache.getImageUrl(model.link, link => { if (delegate) - setProperty(model.index, "imageLink", link) + root.setProperty(model.index, "imageLink", link) }) } } - onObjectRemoved: console.warn("FigmaImagesProxyModel: removing items from the source model is not supported!") + onObjectRemoved: { + for (let i = 0; i < root.count; i++) { + if (root.get(i).uniqueId === object.uniqueId) { + root.remove(i) + break + } + } + } } } diff --git a/storybook/src/Storybook/ImagesNavigationLayout.qml b/storybook/src/Storybook/ImagesNavigationLayout.qml index d16eb98ca7..a0bcf26975 100644 --- a/storybook/src/Storybook/ImagesNavigationLayout.qml +++ b/storybook/src/Storybook/ImagesNavigationLayout.qml @@ -17,7 +17,7 @@ ColumnLayout { RoundButton { text: "⬅" - enabled: root.currentIndex !== 0 + enabled: root.currentIndex !== 0 && root.currentIndex !== -1 onClicked: root.left() } RoundButton { diff --git a/storybook/tests/tst_FigmaDecoratorModel.cpp b/storybook/tests/tst_FigmaDecoratorModel.cpp new file mode 100644 index 0000000000..7145bd26e9 --- /dev/null +++ b/storybook/tests/tst_FigmaDecoratorModel.cpp @@ -0,0 +1,321 @@ +#include +#include +#include +#include + +#include "figmadecoratormodel.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 roleNames() const override { + QHash roles; + roles.insert(TitleRole, QByteArrayLiteral("title")); + return roles; + } + + int m_count; +}; + +} // unnamed namespace + +class FigmaDecoratorModelTest: public QObject +{ + Q_OBJECT + +private slots: + 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 { + {{"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 { + {{"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 { + {{"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 { + {{"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 { + {{"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()->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()->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(); + + QVERIFY(figmaLinksModel1 != nullptr); + QCOMPARE(figmaLinksModel1->rowCount(), 0); + + auto figmaLinksModel2 = model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole) + .value(); + + 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(), figmaLinksModel1); + QCOMPARE(model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole) + .value(), 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(), figmaLinksModel1); + QCOMPARE(model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole) + .value(), figmaLinksModel2); + + QCOMPARE(figmaLinksModel1->rowCount(), 0); + QCOMPARE(figmaLinksModel2->rowCount(), 0); + } +}; + +QTEST_MAIN(FigmaDecoratorModelTest) +#include "tst_FigmaDecoratorModel.moc"