From df30dfd376354cdac74a11aab43c303161566383 Mon Sep 17 00:00:00 2001 From: Jordi Sala Morales Date: Tue, 4 Jun 2024 15:33:55 +0200 Subject: [PATCH] Refactor UI code so it does not fail when exporting (#137) --- package-lock.json | 8 +- package.json | 2 +- ui-src/App.tsx | 29 +++--- ui-src/components/ExportForm.tsx | 32 ++++++ ui-src/components/ExporterProgress.tsx | 32 +----- ui-src/components/MissingFontsSection.tsx | 14 +-- ui-src/components/PenpotExporter.tsx | 111 ++------------------- ui-src/components/PluginReload.tsx | 25 +++++ ui-src/context/FigmaContext.tsx | 113 ++++++++++++++++++++++ ui-src/context/createGenericContext.ts | 19 ++++ ui-src/context/index.ts | 2 + 11 files changed, 231 insertions(+), 156 deletions(-) create mode 100644 ui-src/components/ExportForm.tsx create mode 100644 ui-src/components/PluginReload.tsx create mode 100644 ui-src/context/FigmaContext.tsx create mode 100644 ui-src/context/createGenericContext.ts create mode 100644 ui-src/context/index.ts diff --git a/package-lock.json b/package-lock.json index 1d30ce3..f892844 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@create-figma-plugin/ui": "^3.2", "base64-js": "^1.5", "classnames": "^2.5", - "preact": "^10.21", + "preact": "^10.22", "react-hook-form": "^7.51", "romans": "^2.0", "slugify": "^1.6", @@ -6722,9 +6722,9 @@ "dev": true }, "node_modules/preact": { - "version": "10.21.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.21.0.tgz", - "integrity": "sha512-aQAIxtzWEwH8ou+OovWVSVNlFImL7xUCwJX3YMqA3U8iKCNC34999fFOnWjYNsylgfPgMexpbk7WYOLtKr/mxg==", + "version": "10.22.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.22.0.tgz", + "integrity": "sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" diff --git a/package.json b/package.json index c455105..2d9c357 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@create-figma-plugin/ui": "^3.2", "base64-js": "^1.5", "classnames": "^2.5", - "preact": "^10.21", + "preact": "^10.22", "react-hook-form": "^7.51", "romans": "^2.0", "slugify": "^1.6", diff --git a/ui-src/App.tsx b/ui-src/App.tsx index f4bc015..306ad64 100644 --- a/ui-src/App.tsx +++ b/ui-src/App.tsx @@ -5,6 +5,7 @@ import Penpot from '@ui/assets/penpot.svg?react'; import { PenpotExporter } from '@ui/components/PenpotExporter'; import { Stack } from '@ui/components/Stack'; import { Wrapper } from '@ui/components/Wrapper'; +import { FigmaProvider } from '@ui/context/FigmaContext'; // Safe default value to avoid overflowing from the screen const MAX_HEIGHT = 800; @@ -21,18 +22,20 @@ export const App = () => { }, [height]); return ( - MAX_HEIGHT}> - - - - - + + MAX_HEIGHT}> + + + + + + ); }; diff --git a/ui-src/components/ExportForm.tsx b/ui-src/components/ExportForm.tsx new file mode 100644 index 0000000..5c963d3 --- /dev/null +++ b/ui-src/components/ExportForm.tsx @@ -0,0 +1,32 @@ +import { Button } from '@create-figma-plugin/ui'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { Stack } from '@ui/components/Stack'; +import { useFigma } from '@ui/context'; + +import { MissingFontsSection } from './MissingFontsSection'; + +export type FormValues = Record; + +export const ExportForm = () => { + const { cancel, exportPenpot } = useFigma(); + const methods = useForm(); + + return ( + +
+ + + + + + + +
+
+ ); +}; diff --git a/ui-src/components/ExporterProgress.tsx b/ui-src/components/ExporterProgress.tsx index 549b99d..ae3fded 100644 --- a/ui-src/components/ExporterProgress.tsx +++ b/ui-src/components/ExporterProgress.tsx @@ -1,35 +1,11 @@ import { LoadingIndicator } from '@create-figma-plugin/ui'; -import { useEffect, useState } from 'react'; + +import { useFigma } from '@ui/context'; import { Stack } from './Stack'; -type ExporterProgressProps = { - downloading: boolean; -}; - -export const ExporterProgress = ({ downloading }: ExporterProgressProps) => { - const [currentNode, setCurrentNode] = useState(); - const [totalPages, setTotalPages] = useState(); - const [processedPages, setProcessedPages] = useState(); - - const onMessage = (event: MessageEvent<{ pluginMessage: { type: string; data: unknown } }>) => { - if (event.data.pluginMessage?.type === 'PROGRESS_NODE') { - setCurrentNode(event.data.pluginMessage.data as string); - } else if (event.data.pluginMessage?.type === 'PROGRESS_TOTAL_PAGES') { - setTotalPages(event.data.pluginMessage.data as number); - setProcessedPages(0); - } else if (event.data.pluginMessage?.type === 'PROGRESS_PROCESSED_PAGES') { - setProcessedPages(event.data.pluginMessage.data as number); - } - }; - - useEffect(() => { - window.addEventListener('message', onMessage); - - return () => { - window.removeEventListener('message', onMessage); - }; - }, []); +export const ExporterProgress = () => { + const { currentNode, totalPages, processedPages, downloading } = useFigma(); const truncateText = (text: string, maxChars: number) => { if (text.length <= maxChars) { diff --git a/ui-src/components/MissingFontsSection.tsx b/ui-src/components/MissingFontsSection.tsx index f4a4c0e..5c9863c 100644 --- a/ui-src/components/MissingFontsSection.tsx +++ b/ui-src/components/MissingFontsSection.tsx @@ -1,20 +1,20 @@ import { Banner, IconInfo32, Link, Textbox } from '@create-figma-plugin/ui'; import { Controller, useFormContext } from 'react-hook-form'; +import { useFigma } from '@ui/context'; + import { Stack } from './Stack'; -type MissingFontsSectionProps = { - fonts?: string[]; -}; +export const MissingFontsSection = () => { + const { missingFonts } = useFigma(); -export const MissingFontsSection = ({ fonts }: MissingFontsSectionProps) => { - if (!fonts || !fonts.length) return null; + if (!missingFonts || !missingFonts.length) return null; return ( }> - {fonts.length} custom font{fonts.length > 1 ? 's' : ''} detected + {missingFonts.length} custom font{missingFonts.length > 1 ? 's' : ''} detected To export your file with custom fonts, please follow these steps: @@ -43,7 +43,7 @@ export const MissingFontsSection = ({ fonts }: MissingFontsSectionProps) => {
  • Return here and paste the font IDs in the section below
  • - {fonts.map(font => ( + {missingFonts.map(font => ( {font} diff --git a/ui-src/components/PenpotExporter.tsx b/ui-src/components/PenpotExporter.tsx index 4061d34..49b52eb 100644 --- a/ui-src/components/PenpotExporter.tsx +++ b/ui-src/components/PenpotExporter.tsx @@ -1,114 +1,19 @@ -import { Banner, Button, IconInfo32, LoadingIndicator } from '@create-figma-plugin/ui'; -import { useEffect, useState } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { LoadingIndicator } from '@create-figma-plugin/ui'; -import { Stack } from '@ui/components/Stack'; -import { parse } from '@ui/parser'; -import { PenpotDocument } from '@ui/types'; +import { useFigma } from '@ui/context'; +import { ExportForm } from './ExportForm'; import { ExporterProgress } from './ExporterProgress'; -import { MissingFontsSection } from './MissingFontsSection'; - -type FormValues = Record; +import { PluginReload } from './PluginReload'; export const PenpotExporter = () => { - const [missingFonts, setMissingFonts] = useState(); - const [needsReload, setNeedsReload] = useState(false); - const [loading, setLoading] = useState(true); - const [exporting, setExporting] = useState(false); - const [downloading, setDownloading] = useState(false); - const methods = useForm(); - - methods.getValues(); - - const onMessage = (event: MessageEvent<{ pluginMessage: { type: string; data: unknown } }>) => { - if (event.data.pluginMessage?.type == 'PENPOT_DOCUMENT') { - setDownloading(true); - - const document = event.data.pluginMessage.data as PenpotDocument; - const file = parse(document); - - file.export(); - } else if (event.data.pluginMessage?.type == 'CUSTOM_FONTS') { - setMissingFonts(event.data.pluginMessage.data as string[]); - setLoading(false); - setNeedsReload(false); - } else if (event.data.pluginMessage?.type == 'CHANGES_DETECTED') { - setNeedsReload(true); - } - }; - - const exportPenpot = (data: FormValues) => { - setExporting(true); - - parent.postMessage( - { - pluginMessage: { - type: 'export', - data - } - }, - '*' - ); - }; - - const cancel = () => { - parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*'); - }; - - const reload = () => { - setLoading(true); - parent.postMessage({ pluginMessage: { type: 'reload' } }, '*'); - }; - - useEffect(() => { - window.addEventListener('message', onMessage); - - parent.postMessage({ pluginMessage: { type: 'ready' } }, '*'); - - return () => { - window.removeEventListener('message', onMessage); - }; - }, []); + const { loading, needsReload, exporting } = useFigma(); if (loading) return ; - if (exporting) return ; + if (exporting) return ; - if (needsReload) { - return ( - - }> - Changes detected. Please reload the plug-in to ensure all modifications are included in - the exported file. - - - - - - - ); - } + if (needsReload) return ; - return ( - -
    - - - - - - - -
    -
    - ); + return ; }; diff --git a/ui-src/components/PluginReload.tsx b/ui-src/components/PluginReload.tsx new file mode 100644 index 0000000..d20ee84 --- /dev/null +++ b/ui-src/components/PluginReload.tsx @@ -0,0 +1,25 @@ +import { Banner, Button, IconInfo32 } from '@create-figma-plugin/ui'; + +import { Stack } from '@ui/components/Stack'; +import { useFigma } from '@ui/context'; + +export const PluginReload = () => { + const { reload, cancel } = useFigma(); + + return ( + + }> + Changes detected. Please reload the plug-in to ensure all modifications are included in the + exported file. + + + + + + + ); +}; diff --git a/ui-src/context/FigmaContext.tsx b/ui-src/context/FigmaContext.tsx new file mode 100644 index 0000000..61c3350 --- /dev/null +++ b/ui-src/context/FigmaContext.tsx @@ -0,0 +1,113 @@ +import { JSX, PropsWithChildren, useEffect, useState } from 'react'; + +import { FormValues } from '@ui/components/ExportForm'; +import { parse } from '@ui/parser'; +import { PenpotDocument } from '@ui/types'; + +import { createGenericContext } from './createGenericContext'; + +type Context = { + missingFonts: string[] | undefined; + needsReload: boolean; + loading: boolean; + exporting: boolean; + downloading: boolean; + currentNode: string | undefined; + totalPages: number | undefined; + processedPages: number | undefined; + reload: () => void; + cancel: () => void; + exportPenpot: (data: FormValues) => void; +}; + +const [useFigma, StateContextProvider] = createGenericContext(); + +const FigmaProvider = ({ children }: PropsWithChildren): JSX.Element => { + const [missingFonts, setMissingFonts] = useState(); + const [needsReload, setNeedsReload] = useState(false); + const [loading, setLoading] = useState(true); + const [exporting, setExporting] = useState(false); + const [downloading, setDownloading] = useState(false); + const [currentNode, setCurrentNode] = useState(); + const [totalPages, setTotalPages] = useState(); + const [processedPages, setProcessedPages] = useState(); + + const onMessage = (event: MessageEvent<{ pluginMessage: { type: string; data: unknown } }>) => { + if (event.data.pluginMessage?.type == 'PENPOT_DOCUMENT') { + setDownloading(true); + + const document = event.data.pluginMessage.data as PenpotDocument; + const file = parse(document); + + file.export(); + } else if (event.data.pluginMessage?.type == 'CUSTOM_FONTS') { + setMissingFonts(event.data.pluginMessage.data as string[]); + setLoading(false); + setNeedsReload(false); + } else if (event.data.pluginMessage?.type == 'CHANGES_DETECTED') { + setNeedsReload(true); + } else if (event.data.pluginMessage?.type === 'PROGRESS_NODE') { + setCurrentNode(event.data.pluginMessage.data as string); + } else if (event.data.pluginMessage?.type === 'PROGRESS_TOTAL_PAGES') { + setTotalPages(event.data.pluginMessage.data as number); + setProcessedPages(0); + } else if (event.data.pluginMessage?.type === 'PROGRESS_PROCESSED_PAGES') { + setProcessedPages(event.data.pluginMessage.data as number); + } + }; + + const reload = () => { + setLoading(true); + parent.postMessage({ pluginMessage: { type: 'reload' } }, '*'); + }; + + const cancel = () => { + parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*'); + }; + + const exportPenpot = (data: FormValues) => { + setExporting(true); + + parent.postMessage( + { + pluginMessage: { + type: 'export', + data + } + }, + '*' + ); + }; + + useEffect(() => { + window.addEventListener('message', onMessage); + + parent.postMessage({ pluginMessage: { type: 'ready' } }, '*'); + + return () => { + window.removeEventListener('message', onMessage); + }; + }, []); + + return ( + + {children} + + ); +}; + +export { FigmaProvider, useFigma }; diff --git a/ui-src/context/createGenericContext.ts b/ui-src/context/createGenericContext.ts new file mode 100644 index 0000000..366b041 --- /dev/null +++ b/ui-src/context/createGenericContext.ts @@ -0,0 +1,19 @@ +import { Provider } from 'preact'; +// @TODO: Try to use react +import { createContext, useContext } from 'react'; + +export const createGenericContext = (): [() => K, Provider] => { + const genericContext = createContext(undefined); + + const useGenericContext = (): K => { + const context = useContext(genericContext); + + if (!context) { + throw new Error('useGenericContext must be used within a Provider'); + } + + return context as K; + }; + + return [useGenericContext, genericContext.Provider]; +}; diff --git a/ui-src/context/index.ts b/ui-src/context/index.ts new file mode 100644 index 0000000..5c18f31 --- /dev/null +++ b/ui-src/context/index.ts @@ -0,0 +1,2 @@ +export * from './createGenericContext'; +export * from './FigmaContext';