diff --git a/storybook/main.qml b/storybook/main.qml index 3e1c2286ce..5b2b71f65c 100644 --- a/storybook/main.qml +++ b/storybook/main.qml @@ -8,7 +8,6 @@ import Qt.labs.settings 1.0 import StatusQ.Core.Theme 0.1 import Storybook 1.0 -import utils 1.0 ApplicationWindow { id: root @@ -181,6 +180,7 @@ ApplicationWindow { model: pagesModel onPageSelected: root.currentPage = page + onStatusClicked: statusStatsDialog.open() } } } @@ -287,17 +287,8 @@ ApplicationWindow { } } - Dialog { + NoFigmaTokenDialog { id: noFigmaTokenDialog - - anchors.centerIn: Overlay.overlay - - title: "Figma token not set" - standardButtons: Dialog.Ok - - Label { - text: "Please set Figma personal token in \"Settings\"" - } } FigmaLinksCache { @@ -310,24 +301,16 @@ ApplicationWindow { id: inspectionWindow } - Dialog { + NothingToInspectDialog { id: nothingToInspectDialog - anchors.centerIn: Overlay.overlay - width: contentItem.implicitWidth + leftPadding + rightPadding + pageName: root.currentPage + } - title: "No items to inspect found" - standardButtons: Dialog.Ok - modal: true + StatusStatisticsDialog { + id: statusStatsDialog - contentItem: Label { - text: ' -Tips:\n\ - • For inline components use naming convention of adding\n\ - "Custom" at the begining (like Custom'+root.currentPage+')\n\ - • For popups set closePolicy to "Popup.NoAutoClose"\n\ -' - } + pagesModel: pagesModel } Component { diff --git a/storybook/pages/PlaygroundPage.qml b/storybook/pages/PlaygroundPage.qml index ed67aa486e..fd0167c73d 100644 --- a/storybook/pages/PlaygroundPage.qml +++ b/storybook/pages/PlaygroundPage.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import QtQml 2.15 +import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Utils 0.1 import StatusQ.Controls 0.1 @@ -27,3 +28,4 @@ Item { } // category: _ +// status: good diff --git a/storybook/pagesmodel.cpp b/storybook/pagesmodel.cpp index 1d7fed0672..fc38534ab4 100644 --- a/storybook/pagesmodel.cpp +++ b/storybook/pagesmodel.cpp @@ -5,7 +5,6 @@ #include "directoryfileswatcher.h" - namespace { const auto categoryUncategorized QStringLiteral("Uncategorized"); } @@ -35,29 +34,9 @@ PagesModelItem PagesModel::readMetadata(const QString& path) file.open(QIODevice::ReadOnly); QByteArray content = file.readAll(); - static QRegularExpression categoryRegex( - "^//(\\s)*category:(.+)$", QRegularExpression::MultilineOption); - - QRegularExpressionMatch categoryMatch = categoryRegex.match(content); - QString category = categoryMatch.hasMatch() - ? categoryMatch.captured(2).trimmed() : categoryUncategorized; - - item.category = category.size() ? category : categoryUncategorized; - - 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; + item.category = extractCategory(content); + item.status = extractStatus(content); + item.figmaLinks = extractFigmaLinks(content); return item; } @@ -75,6 +54,55 @@ QList PagesModel::readMetadata(const QStringList &paths) return metadata; } +QString PagesModel::extractCategory(const QByteArray& content) +{ + static QRegularExpression categoryRegex( + "^//(\\s)*category:(.+)$", QRegularExpression::MultilineOption); + + QRegularExpressionMatch categoryMatch = categoryRegex.match(content); + QString category = categoryMatch.hasMatch() + ? categoryMatch.captured(2).trimmed() : categoryUncategorized; + + return category.isEmpty() ? categoryUncategorized : category; +} + +PagesModel::Status PagesModel::extractStatus(const QByteArray& content) +{ + static QRegularExpression statusRegex( + "^//(\\s)*status:(.+)$", QRegularExpression::MultilineOption); + + QRegularExpressionMatch statusMatch = statusRegex.match(content); + QString status = statusMatch.hasMatch() + ? statusMatch.captured(2).trimmed() : ""; + + if (status == QStringLiteral("bad")) + return Bad; + if (status == QStringLiteral("decent")) + return Decent; + if (status == QStringLiteral("good")) + return Good; + + return Uncategorized; +} + +QStringList PagesModel::extractFigmaLinks(const QByteArray& content) +{ + 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; + } + + return links; +} + void PagesModel::onPagesChanged(const QStringList& added, const QStringList& removed, const QStringList& changed) @@ -106,10 +134,11 @@ void PagesModel::onPagesChanged(const QStringList& added, PagesModelItem metadata = readMetadata(path); setFigmaLinks(metadata.title, metadata.figmaLinks); - if (previous.category != metadata.category) { - // For simplicity category change is handled by removing and - // adding item. In the future it can be changed to regular dataChanged - // event and handled properly in upstream models like SectionSDecoratorModel. + // For simplicity category and status change is handled by removing and + // adding item. In the future it can be changed to regular dataChanged + // event and handled properly in upstream models like SectionSDecoratorModel. + if (previous.category != metadata.category + || previous.status != metadata.status) { beginRemoveRows({}, index, index); m_items.removeAt(index); endRemoveRows(); @@ -135,6 +164,7 @@ QHash PagesModel::roleNames() const static const QHash roles { { TitleRole, QByteArrayLiteral("title") }, { CategoryRole, QByteArrayLiteral("category") }, + { StatusRole, QByteArrayLiteral("status") }, { FigmaRole, QByteArrayLiteral("figma") } }; @@ -157,6 +187,9 @@ QVariant PagesModel::data(const QModelIndex &index, int role) const if (role == CategoryRole) return m_items.at(index.row()).category; + if (role == StatusRole) + return m_items.at(index.row()).status; + if (role == FigmaRole) { auto title = m_items.at(index.row()).title; auto it = m_figmaSubmodels.find(title); diff --git a/storybook/pagesmodel.h b/storybook/pagesmodel.h index e34325d9a9..f3b7b0dda5 100644 --- a/storybook/pagesmodel.h +++ b/storybook/pagesmodel.h @@ -12,6 +12,7 @@ struct PagesModelItem { QDateTime lastModified; QString title; QString category; + int status = 0; QStringList figmaLinks; }; @@ -24,14 +25,23 @@ public: enum Roles { TitleRole = Qt::UserRole + 1, CategoryRole, + StatusRole, FigmaRole }; + enum Status : int { + Uncategorized = 0, + Bad, + Decent, + Good + }; + + Q_ENUM(Status) + QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; - private: void onPagesChanged(const QStringList& added, const QStringList& removed, const QStringList& changed); @@ -44,6 +54,10 @@ private: static void readMetadata(PagesModelItem &item); static void readMetadata(QList &items); + static QString extractCategory(const QByteArray& content); + static PagesModel::Status extractStatus(const QByteArray& content); + static QStringList extractFigmaLinks(const QByteArray& content); + void setFigmaLinks(const QString& title, const QStringList& links); QString m_path; diff --git a/storybook/src/Storybook/FilteredPagesList.qml b/storybook/src/Storybook/FilteredPagesList.qml index b6b1addb26..45cdd4c8a6 100644 --- a/storybook/src/Storybook/FilteredPagesList.qml +++ b/storybook/src/Storybook/FilteredPagesList.qml @@ -13,6 +13,7 @@ ColumnLayout { property alias currentPage: pagesList.currentPage signal pageSelected(string page) + signal statusClicked SortFilterProxyModel { id: filteredModel @@ -96,5 +97,6 @@ ColumnLayout { onPageSelected: root.pageSelected(page) onSectionClicked: sectionsModel.flipFolding(index) + onStatusClicked: root.statusClicked() } } diff --git a/storybook/src/Storybook/NoFigmaTokenDialog.qml b/storybook/src/Storybook/NoFigmaTokenDialog.qml new file mode 100644 index 0000000000..40ab379de3 --- /dev/null +++ b/storybook/src/Storybook/NoFigmaTokenDialog.qml @@ -0,0 +1,13 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Dialog { + anchors.centerIn: Overlay.overlay + + title: "Figma token not set" + standardButtons: Dialog.Ok + + Label { + text: "Please set Figma personal token in \"Settings\"" + } +} diff --git a/storybook/src/Storybook/NothingToInspectDialog.qml b/storybook/src/Storybook/NothingToInspectDialog.qml new file mode 100644 index 0000000000..cf0ce5d29f --- /dev/null +++ b/storybook/src/Storybook/NothingToInspectDialog.qml @@ -0,0 +1,24 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Dialog { + id: root + + property string pageName + + anchors.centerIn: Overlay.overlay + width: contentItem.implicitWidth + leftPadding + rightPadding + + title: "No items to inspect found" + standardButtons: Dialog.Ok + modal: true + + contentItem: Label { + text: ' +Tips:\n\ +• For inline components use naming convention of adding\n\ + "Custom" at the begining (like Custom'+root.pageName+')\n\ +• For popups set closePolicy to "Popup.NoAutoClose"\n\ +' + } +} diff --git a/storybook/src/Storybook/PagesList.qml b/storybook/src/Storybook/PagesList.qml index 35cf044109..dc5a16eb80 100644 --- a/storybook/src/Storybook/PagesList.qml +++ b/storybook/src/Storybook/PagesList.qml @@ -1,5 +1,7 @@ -import QtQuick 2.14 -import QtQuick.Controls 2.14 +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import Storybook 1.0 ListView { id: root @@ -10,6 +12,7 @@ ListView { signal pageSelected(string page) signal sectionClicked(int index) + signal statusClicked readonly property string foldedPrefix: "▶ " readonly property string unfoldedPrefix: "▼ " @@ -27,6 +30,35 @@ ListView { "text/uri-list": `file:${pagesFolder}/${model.title}Page.qml` } + indicator: Rectangle { + visible: !model.isSection + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: parent.leftPadding / 2 + + width: 6 + height: 6 + radius: 3 + + color: { + if (model.status === PagesModel.Good) + return "green" + if (model.status === PagesModel.Decent) + return "orange" + if (model.status === PagesModel.Bad) + return "red" + + return "gray" + } + + MouseArea { + anchors.fill: parent + + onClicked: root.statusClicked() + } + } + MouseArea { id: dragArea anchors.fill: parent diff --git a/storybook/src/Storybook/StatusStatisticsDialog.qml b/storybook/src/Storybook/StatusStatisticsDialog.qml new file mode 100644 index 0000000000..3e697ce78f --- /dev/null +++ b/storybook/src/Storybook/StatusStatisticsDialog.qml @@ -0,0 +1,113 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ 0.1 + +import Storybook 1.0 +import utils 1.0 + +Dialog { + id: root + + required property var pagesModel + + readonly property string contributingMdLink: + "https://github.com/status-im/status-desktop/blob/master/" + + "CONTRIBUTING.md#page-classification" + + anchors.centerIn: Overlay.overlay + width: 420 + + title: "Page status statistics" + standardButtons: Dialog.Ok + modal: true + + QtObject { + id: d + + readonly property int total: root.pagesModel.ModelCount.count + + function percent(val) { + return (val / total * 100).toFixed(2) + } + } + + contentItem: ColumnLayout { + Label { + Layout.bottomMargin: 20 + + text: `Total number of pages: ${d.total}` + } + + Repeater { + model: [ + { status: "good", color: "green" }, + { status: "decent", color: "orange" }, + { status: "bad", color: "red" }, + { status: "uncategorized", color: "gray" } + ] + + Row { + spacing: 10 + + Rectangle { + readonly property int size: label.height - 2 + + width: size + height: size + radius: size / 2 + color: modelData.color + } + + Label { + id: label + + readonly property string status: modelData.status + readonly property int count: statusAggregator[status] + + text: `${status}: ${count} (${d.percent(count)}%)` + } + } + } + + Label { + Layout.topMargin: 20 + + text: `For details check CONTRIBUTING.md` + onLinkActivated: Qt.openUrlExternally(link) + } + } + + FunctionAggregator { + id: statusAggregator + + readonly property int bad: value.bad + readonly property int decent: value.decent + readonly property int good: value.good + readonly property int uncategorized: value.uncategorized + + model: root.pagesModel + roleName: "status" + + initialValue: ({ + bad: 0, + decent: 0, + good: 0, + uncategorized: 0 + }) + + aggregateFunction: (aggr, value) => { + let { bad, decent, good, uncategorized } = aggr + + switch (value) { + case PagesModel.Bad: bad++; break + case PagesModel.Decent: decent++; break + case PagesModel.Good: good++; break + default: uncategorized++ + } + + return { bad, decent, good, uncategorized } + } + } +} diff --git a/storybook/src/Storybook/qmldir b/storybook/src/Storybook/qmldir index 4bdfa75983..10120562ef 100644 --- a/storybook/src/Storybook/qmldir +++ b/storybook/src/Storybook/qmldir @@ -21,6 +21,8 @@ LimitProxyModel 1.0 LimitProxyModel.qml Logs 1.0 Logs.qml LogsAndControlsPanel 1.0 LogsAndControlsPanel.qml LogsView 1.0 LogsView.qml +NoFigmaTokenDialog 1.0 NoFigmaTokenDialog.qml +NothingToInspectDialog 1.0 NothingToInspectDialog.qml PageToolBar 1.0 PageToolBar.qml PagesList 1.0 PagesList.qml PopupBackground 1.0 PopupBackground.qml @@ -28,6 +30,7 @@ RadioButtonFlowSelector 1.0 RadioButtonFlowSelector.qml SettingsLayout 1.0 SettingsLayout.qml SingleItemProxyModel 1.0 SingleItemProxyModel.qml SourceCodeBox 1.0 SourceCodeBox.qml +StatusStatisticsDialog 1.0 StatusStatisticsDialog.qml TestRunnerController 1.0 TestRunnerController.qml TestRunnerControls 1.0 TestRunnerControls.qml singleton FigmaUtils 1.0 FigmaUtils.qml