Storybook: added ability to mark pages with their status

Closes: #16606
This commit is contained in:
Michał Cieślak 2024-10-30 12:19:53 +01:00 committed by Michał
parent 7854271c2e
commit 0c86fbf7b6
10 changed files with 275 additions and 56 deletions

View File

@ -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
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.currentPage+')\n\
For popups set closePolicy to "Popup.NoAutoClose"\n\
'
pageName: root.currentPage
}
StatusStatisticsDialog {
id: statusStatsDialog
pagesModel: pagesModel
}
Component {

View File

@ -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

View File

@ -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<PagesModelItem> 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
// 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<int, QByteArray> PagesModel::roleNames() const
static const QHash<int,QByteArray> 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);

View File

@ -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<int, QByteArray> 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<PagesModelItem> &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;

View File

@ -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()
}
}

View File

@ -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\""
}
}

View File

@ -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\
'
}
}

View File

@ -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

View File

@ -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 <a href="${root.contributingMdLink}">CONTRIBUTING.md</a>`
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 }
}
}
}

View File

@ -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