feat(Storybook): add basic Figma integration

After setting Figma private token, Figma desings related
to a given Storybook page can be browsed directly via the
Storybook app.

Closes: #8188
This commit is contained in:
Michał Cieślak 2022-11-21 11:55:16 +01:00 committed by Michał
parent ff9c678a1e
commit ec38dca735
12 changed files with 577 additions and 34 deletions

View File

@ -31,10 +31,40 @@ ApplicationWindow {
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 {
@ -55,6 +85,15 @@ ApplicationWindow {
}
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 {
@ -87,48 +126,54 @@ ApplicationWindow {
SplitView {
anchors.fill: parent
ColumnLayout {
SplitView.preferredWidth: 240
Pane {
SplitView.preferredWidth: 270
CheckBox {
id: loadAsyncCheckBox
ColumnLayout {
width: parent.width
height: parent.height
Layout.fillWidth: true
Button {
Layout.fillWidth: true
text: "Load asynchronously"
}
text: "Settings"
CheckBox {
id: darkModeCheckBox
Layout.fillWidth: true
text: "Dark mode"
StatusLightTheme { id: lightTheme }
StatusDarkTheme { id: darkTheme }
Binding {
target: Theme
property: "palette"
value: darkModeCheckBox.checked ? darkTheme : lightTheme
onClicked: settingsPopup.open()
}
}
HotReloaderControls {
id: hotReloaderControls
CheckBox {
id: darkModeCheckBox
Layout.fillWidth: true
Layout.fillWidth: true
onForceReloadClicked: reloader.forceReload()
}
text: "Dark mode"
Pane {
Layout.fillWidth: true
Layout.fillHeight: true
StatusLightTheme { id: lightTheme }
StatusDarkTheme { id: darkTheme }
Binding {
target: Theme
property: "palette"
value: darkModeCheckBox.checked ? darkTheme : lightTheme
}
}
HotReloaderControls {
id: hotReloaderControls
Layout.fillWidth: true
onForceReloadClicked: reloader.forceReload()
}
MenuSeparator {
Layout.fillWidth: true
}
FilteredPagesList {
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
currentPage: root.currentPage
model: pagesModel
@ -137,7 +182,7 @@ ApplicationWindow {
}
}
Item {
Page {
SplitView.fillWidth: true
Loader {
@ -147,7 +192,7 @@ ApplicationWindow {
clip: true
source: `pages/${root.currentPage}Page.qml`
asynchronous: loadAsyncCheckBox.checked
asynchronous: settingsLayout.loadAsynchronously
visible: status === Loader.Ready
// force reload when `asynchronous` changes
@ -167,13 +212,107 @@ ApplicationWindow {
visible: viewLoader.status === Loader.Error
text: "Loading page failed"
}
footer: PageToolBar {
id: pageToolBar
title: `pages/${root.currentPage}Page.qml`
figmaPagesCount: currentPageModelItem.object
? currentPageModelItem.object.figmaCount : 0
Instantiator {
id: currentPageModelItem
model: SingleItemProxyModel {
sourceModel: pagesModel
roleName: "title"
value: root.currentPage
}
delegate: QtObject {
readonly property string title: model.title
readonly property var figma: model.figma
readonly property int figmaCount: figma ? figma.count : 0
}
}
onFigmaPreviewClicked: {
if (!settingsLayout.figmaToken) {
noFigmaTokenDialog.open()
return
}
const window = figmaWindow.createObject(root, {
figmaModel: currentPageModelItem.object.figma,
title: currentPageModelItem.object.title + " - Figma"
})
}
}
}
}
Dialog {
id: settingsPopup
anchors.centerIn: Overlay.overlay
width: 420
modal: true
header: Pane {
background: null
Label {
text: "Settings"
}
}
SettingsLayout {
id: settingsLayout
width: parent.width
}
}
Dialog {
id: noFigmaTokenDialog
anchors.centerIn: Overlay.overlay
title: "Figma token not set"
standardButtons: Dialog.Ok
Label {
text: "Please set Figma personal token in \"Settings\""
}
}
FigmaLinksCache {
id: figmaImageLinksCache
figmaToken: settingsLayout.figmaToken
}
Component {
id: figmaWindow
FigmaPreviewWindow {
property alias figmaModel: figmaImagesProxyModel.sourceModel
model: FigmaImagesProxyModel {
id: figmaImagesProxyModel
figmaLinksCache: figmaImageLinksCache
}
onClosing: Qt.callLater(destroy)
}
}
Settings {
property alias currentPage: root.currentPage
property alias loadAsynchronously: loadAsyncCheckBox.checked
property alias loadAsynchronously: settingsLayout.loadAsynchronously
property alias darkMode: darkModeCheckBox.checked
property alias hotReloading: hotReloaderControls.enabled
property alias figmaToken: settingsLayout.figmaToken
}
}

View File

@ -0,0 +1,30 @@
import QtQuick 2.14
ListModel {
/* required */ property FigmaLinksCache figmaLinksCache
property alias sourceModel: d.model
readonly property Instantiator _d: Instantiator {
id: d
model: 0
delegate: QtObject {
id: delegate
Component.onCompleted: {
append({
rawLink: model.link,
imageLink: ""
})
figmaLinksCache.getImageUrl(model.link, link => {
if (delegate)
setProperty(model.index, "imageLink", link)
})
}
}
onObjectRemoved: console.warn("FigmaImagesProxyModel: removing items from the source model is not supported!")
}
}

View File

@ -0,0 +1,37 @@
import QtQml 2.14
QtObject {
id: root
property string figmaToken
readonly property QtObject _d: QtObject {
id: d
readonly property var linksMap: new Map()
function createKey(file, nodeId) {
return file + "/" + nodeId
}
}
function getImageUrl(figmaLink, cb) {
const { file, nodeId } = FigmaUtils.decomposeLink(figmaLink)
const key = d.createKey(file, nodeId);
if (d.linksMap.has(key)) {
cb(d.linksMap.get(key))
} else {
FigmaUtils.getLinks(root.figmaToken, file, [nodeId],
(err, result) => {
if (err)
return cb(null)
for (const value of Object.values(result)) {
d.linksMap.set(key, value)
return cb(value)
}
})
}
}
}

View File

@ -0,0 +1,64 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
ApplicationWindow {
id: root
width: 1024
height: 768
visible: true
property var model
SwipeView {
id: topSwipeView
anchors.fill: parent
orientation: Qt.Vertical
interactive: false
ImagesGridView {
clip: true
model: root.model
onClicked: {
imagesSwipeView.setCurrentIndex(index)
topSwipeView.incrementCurrentIndex()
}
}
Item {
SwipeView {
id: imagesSwipeView
anchors.fill: parent
currentIndex: imageNavigationLayout.currentIndex
Repeater {
id: repeater
model: root.model
FlickableImage {
source: model.imageLink
}
}
}
ImagesNavigationLayout {
id: imageNavigationLayout
anchors.bottom: imagesSwipeView.bottom
anchors.horizontalCenter: parent.horizontalCenter
count: imagesSwipeView.count
currentIndex: imagesSwipeView.currentIndex
onUp: topSwipeView.decrementCurrentIndex()
onLeft: imagesSwipeView.decrementCurrentIndex()
onRight: imagesSwipeView.incrementCurrentIndex()
}
}
}
}

View File

@ -0,0 +1,42 @@
pragma Singleton
import QtQml 2.14
QtObject {
function decomposeLink(link) {
const fileRegex = /www\.figma\.com\/file\/([a-zA-Z0-9]+)/
const fileMatch = link.match(fileRegex)
const nodeIdRegex = /node-id=([0-9A-Za-z%]+)/
const nodeIdMatch = link.match(nodeIdRegex)
return {
file: fileMatch[1],
nodeId: nodeIdMatch[1]
}
}
function getLinks(token, file, nodeIds, cb) {
console.assert(nodeIds.length > 0)
const ids = nodeIds.join()
const url = `https://api.figma.com/v1/images/${file}?ids=${ids}`
const http = new XMLHttpRequest()
http.open("GET", url, true)
http.setRequestHeader("X-FIGMA-TOKEN", token)
http.onreadystatechange = () => {
if (http.readyState !== 4)
return
if (http.status === 200)
cb(null, JSON.parse(http.response).images)
else
cb(`Failed to fetch figma image links, status: ${http.status}`)
}
http.send()
}
}

View File

@ -0,0 +1,32 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
Item {
property alias source: image.source
Rectangle {
anchors.fill: parent
color: "lightgray"
}
Flickable {
anchors.fill: parent
contentWidth: image.implicitWidth
contentHeight: image.implicitHeight
clip: true
ScrollIndicator.vertical: ScrollIndicator {}
ScrollIndicator.horizontal: ScrollIndicator {}
Image {
id: image
}
}
BusyIndicator {
anchors.centerIn: parent
running: image.status !== Image.Ready
}
}

View File

@ -0,0 +1,55 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
GridView {
id: root
cellWidth: 400
cellHeight: 300
signal clicked(int index)
delegate: Item {
width: root.cellWidth
height: root.cellHeight
Frame {
anchors.fill: parent
anchors.margins: padding / 2
Image {
id: image
anchors.fill: parent
mipmap: true
source: model.imageLink
fillMode: Image.PreserveAspectFit
}
BusyIndicator {
anchors.centerIn: parent
running: image.status !== Image.Ready
}
MouseArea {
anchors.fill: parent
onClicked: root.clicked(model.index)
}
RoundButton {
anchors.bottom: parent.bottom
anchors.right: parent.right
text: "🔗"
onClicked: Qt.openUrlExternally(model.rawLink)
ToolTip.delay: 1500
ToolTip.visible: hovered
ToolTip.text: model.rawLink
}
}
}
}

View File

@ -0,0 +1,41 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
ColumnLayout {
id: root
property alias count: indicator.count
property alias currentIndex: indicator.currentIndex
signal up
signal left
signal right
RowLayout {
Layout.alignment: Qt.AlignHCenter
RoundButton {
text: "⬅"
enabled: root.currentIndex !== 0
onClicked: root.left()
}
RoundButton {
text: "⬆"
onClicked: root.up()
}
RoundButton {
text: "➡"
enabled: root.currentIndex !== root.count - 1
onClicked: root.right()
}
}
PageIndicator {
id: indicator
Layout.alignment: Qt.AlignHCenter
interactive: true
}
}

View File

@ -0,0 +1,39 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
ToolBar {
id: root
property string title
property int figmaPagesCount: 0
signal figmaPreviewClicked
RowLayout {
anchors.fill: parent
TextField {
Layout.fillWidth: true
text: root.title
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
selectByMouse: true
readOnly: true
background: null
}
ToolSeparator {}
ToolButton {
id: openFigmaButton
enabled: root.figmaPagesCount
text: `Figma designs (${root.figmaPagesCount})`
onClicked: root.figmaPreviewClicked()
}
}
}

View File

@ -0,0 +1,44 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
ColumnLayout {
property alias loadAsynchronously: loadAsyncCheckBox.checked
property alias figmaToken: figmaTokenTextInput.text
CheckBox {
id: loadAsyncCheckBox
Layout.fillWidth: true
text: "Load pages asynchronously"
}
GroupBox {
Layout.fillWidth: true
title: "Figma token"
ColumnLayout {
anchors.fill: parent
Label {
Layout.fillWidth: true
text: `Figma token can be obtained <a href=\"https://www.figma.com/developers/api#access-tokens\">here</a>
by clicking \"Get personal access token\". It's necessary to fetch figma data via Figma API.`
onLinkActivated: Qt.openUrlExternally(link)
wrapMode: Text.Wrap
}
TextField {
id: figmaTokenTextInput
Layout.fillWidth: true
placeholderText: "Figma personal access token"
}
}
}
}

View File

@ -0,0 +1,10 @@
import SortFilterProxyModel 0.2
SortFilterProxyModel {
property alias roleName: valueFilter.roleName
property alias value: valueFilter.value
filters: ValueFilter {
id: valueFilter
}
}

View File

@ -1,14 +1,24 @@
CompilationErrorsBox 1.0 CompilationErrorsBox.qml
FigmaImagesProxyModel 1.0 FigmaImagesProxyModel.qml
FigmaLinksCache 1.0 FigmaLinksCache.qml
FigmaPreviewWindow 1.0 FigmaPreviewWindow.qml
FilteredPagesList 1.0 FilteredPagesList.qml
FlickableImage 1.0 FlickableImage.qml
HotComponentFromSource 1.0 HotComponentFromSource.qml
HotLoader 1.0 HotLoader.qml
HotReloader 1.0 HotReloader.qml
HotReloaderControls 1.0 HotReloaderControls.qml
ImageSelectPopup 1.0 ImageSelectPopup.qml
ImagesGridView 1.0 ImagesGridView.qml
ImagesNavigationLayout 1.0 ImagesNavigationLayout.qml
Logs 1.0 Logs.qml
LogsAndControlsPanel 1.0 LogsAndControlsPanel.qml
LogsView 1.0 LogsView.qml
PageToolBar 1.0 PageToolBar.qml
PagesList 1.0 PagesList.qml
PopupBackground 1.0 PopupBackground.qml
SettingsLayout 1.0 SettingsLayout.qml
SingleItemProxyModel 1.0 SingleItemProxyModel.qml
SourceCodeBox 1.0 SourceCodeBox.qml
singleton FigmaUtils 1.0 FigmaUtils.qml
singleton StorybookUtils 1.0 StorybookUtils.qml