diff --git a/editor-packages/editor-figma-file/repository.ts b/editor-packages/editor-figma-file/repository.ts index f20212a1c..5f61a7198 100644 --- a/editor-packages/editor-figma-file/repository.ts +++ b/editor-packages/editor-figma-file/repository.ts @@ -39,7 +39,11 @@ export class FigmaDesignRepository { if (existing) { // everytime the file is consumed consider it as used, we upsert the file so that the lastUsed can be updated. metastore.upsert(existing.key, existing); - yield { ...existing, __initial: false } as TFetchFileForApp; + yield { + ...existing, + __initial: false, + __type: "file-fetched-for-app", + } as TFetchFileForApp; } const _iter = fetch.fetchFile({ file: filekey, auth: this.auth }); diff --git a/editor/components/home/home-side-bar-tree.tsx b/editor/components/home/home-side-bar-tree.tsx index 2c8ee037c..006f73cbb 100644 --- a/editor/components/home/home-side-bar-tree.tsx +++ b/editor/components/home/home-side-bar-tree.tsx @@ -89,6 +89,12 @@ const preset_pages: PresetPage[] = [ }, ], }, + { + id: "/community/files", + name: "Community Files", + path: "/community/files", + depth: 0, + }, ]; export function HomeSidebarTree() { diff --git a/editor/components/prompt-banner-signin-to-continue/index.tsx b/editor/components/prompt-banner-signin-to-continue/index.tsx index 2a0e57286..928e47eb1 100644 --- a/editor/components/prompt-banner-signin-to-continue/index.tsx +++ b/editor/components/prompt-banner-signin-to-continue/index.tsx @@ -1,9 +1,10 @@ import styled from "@emotion/styled"; -import React, { useEffect } from "react"; +import React from "react"; import { useAuthState } from "hooks"; import { useRouter } from "next/router"; - -const __is_dev = process.env.NODE_ENV == "development"; +import { ArrowRightIcon } from "@radix-ui/react-icons"; +const __is_prod = process.env.NODE_ENV == "production"; +const __overide_show_if_dev = true; export function SigninToContinuePrmoptProvider({ children, @@ -17,7 +18,9 @@ export function SigninToContinuePrmoptProvider({ return ( <> {children} - {!__is_dev && shouldshow && } + {((__is_prod && shouldshow) || (!__is_prod && __overide_show_if_dev)) && ( + + )} ); } @@ -31,34 +34,30 @@ export function SigninToContinueBannerPrmopt() { return ( - - Ready to build your apps with Grida? - Next - + +
Ready to build your Apps with Grida?
+ + Sign Up + + +
); } const Positioner = styled.div` - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; - align-items: flex-end; - position: fixed; bottom: 0; - left: 0px; - right: 0px; + left: 0; + right: 0; + padding: 40px 40px; - background-color: #fff; z-index: 998; - + display: flex; justify-content: center; flex-direction: column; - align-items: end; + align-items: center; box-sizing: border-box; - padding: 16px 20px; a { margin: 0px 2px; @@ -66,56 +65,62 @@ const Positioner = styled.div` } `; -const Contents = styled.div` +const Container = styled.div` + width: 100%; + max-width: 600px; + min-width: 400px; + background-color: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(21px); + color: white; display: flex; - justify-content: flex-end; + justify-content: space-between; flex-direction: row; align-items: center; flex: none; + padding: 16px 24px; + border-radius: 48px; + border: 1px solid rgba(255, 255, 255, 0.2); gap: 48px; - width: 439px; - height: 59px; box-sizing: border-box; -`; + box-shadow: 0px 4px 32px rgba(0, 0, 0, 0.24); -const Desc = styled.span` - color: rgba(0, 0, 0, 1); - text-overflow: ellipsis; - font-size: 16px; - font-family: "Helvetica Neue", sans-serif; - font-weight: 500; - text-align: center; + h5 { + color: white; + margin: 0; + text-overflow: ellipsis; + font-weight: 500; + } `; -const NextButton = styled.button` +const CTAButton = styled.button` + cursor: pointer; display: flex; justify-content: center; flex-direction: row; align-items: center; flex: none; - gap: 10px; - border-radius: 4px; - width: 116px; - height: 59px; - background-color: rgba(45, 66, 255, 1); + gap: 4px; + border-radius: 24px; + background-color: transparent; + border: 1px solid rgba(255, 255, 255, 0.4); box-sizing: border-box; - padding: 10px 10px; + padding: 8px 16px; outline: none; - border: none; - color: rgba(255, 255, 255, 1); + color: white; text-overflow: ellipsis; - font-size: 21px; - font-family: "Helvetica Neue", sans-serif; - font-weight: 400; + font-weight: 500; text-align: left; :hover { opacity: 0.9; + scale: 1.02; } :focus { opacity: 0.9; } + + transition: all 0.1s ease-in-out; `; diff --git a/editor/core/reducers/editor-reducer.ts b/editor/core/reducers/editor-reducer.ts index 4471d7ac8..62e11e408 100644 --- a/editor/core/reducers/editor-reducer.ts +++ b/editor/core/reducers/editor-reducer.ts @@ -21,7 +21,7 @@ import type { EnterIsolatedInspectionAction, ExitIsolatedInspectionAction, } from "core/actions"; -import { EditorState } from "core/states"; +import { EditorState, EssentialWorkspaceInfo } from "core/states"; import { NextRouter, useRouter } from "next/router"; import { CanvasStateStore } from "@code-editor/canvas/stores"; import q from "@design-sdk/query"; @@ -31,8 +31,6 @@ import { nanoid } from "nanoid"; import { last_page_by_mode } from "core/stores"; import { track } from "@code-editor/analytics"; -const _editor_path_name = "/files/[key]/"; - const _DEV_CLEAR_LOG = false; const clearlog = (by: string) => { @@ -42,7 +40,10 @@ const clearlog = (by: string) => { } }; -export function editorReducer(state: EditorState, action: Action): EditorState { +export function editorReducer( + state: EditorState & EssentialWorkspaceInfo, + action: Action +): EditorState { const router = useRouter(); const filekey = state.design.key; @@ -395,10 +396,8 @@ function update_route( // remove undefined fields Object.keys(q).forEach((k) => q[k] === undefined && delete q[k]); - router.push( { - pathname: _editor_path_name, query: { ...router.query, ...q }, }, undefined, diff --git a/editor/core/states/workspace-initial-state.ts b/editor/core/states/workspace-initial-state.ts index b51a93f21..83566a973 100644 --- a/editor/core/states/workspace-initial-state.ts +++ b/editor/core/states/workspace-initial-state.ts @@ -1,5 +1,5 @@ import { EditorSnapshot } from "./editor-state"; -import { WorkspaceState } from "./workspace-state"; +import { WorkspaceState, EssentialWorkspaceInfo } from "./workspace-state"; import { createInitialHistoryState, createPendingHistoryState, @@ -23,7 +23,7 @@ export function merge_initial_workspace_state_with_editor_snapshot( }; } -export function create_initial_pending_workspace_state(): WorkspaceState { +export function create_initial_pending_workspace_state({}: EssentialWorkspaceInfo): WorkspaceState { return { taskQueue: { isBusy: false, diff --git a/editor/core/states/workspace-state.ts b/editor/core/states/workspace-state.ts index 0ea4caf0c..640fe3aba 100644 --- a/editor/core/states/workspace-state.ts +++ b/editor/core/states/workspace-state.ts @@ -1,7 +1,11 @@ import { config } from "@grida/builder-config"; import { HistoryState } from "core/states/history-state"; -export interface WorkspaceState { +export interface EssentialWorkspaceInfo { + // Add workspace seed data here, which cannot be automatically filled on initial state. +} + +export interface WorkspaceState extends EssentialWorkspaceInfo { history: HistoryState; /** * hovered layer; single or none. diff --git a/editor/hooks/index.ts b/editor/hooks/index.ts index 8c1b01f77..f45eb3794 100644 --- a/editor/hooks/index.ts +++ b/editor/hooks/index.ts @@ -1,4 +1,4 @@ -export * from "./use-design"; +export * from "./use-figma"; export * from "./use-async-effect"; export * from "./use-auth-state"; export * from "./use-target-node"; diff --git a/editor/hooks/use-figma/index.ts b/editor/hooks/use-figma/index.ts new file mode 100644 index 000000000..78d5ffa18 --- /dev/null +++ b/editor/hooks/use-figma/index.ts @@ -0,0 +1,2 @@ +export * from "./use-figma"; +export * from "./use-figma-community"; diff --git a/editor/hooks/use-figma/types.ts b/editor/hooks/use-figma/types.ts new file mode 100644 index 000000000..1a876bd42 --- /dev/null +++ b/editor/hooks/use-figma/types.ts @@ -0,0 +1,37 @@ +import type { NextRouter } from "next/router"; +import type { TFetchFileForApp } from "@editor/figma-file"; + +export type UseFigmaInput = + | (UseFigmaFromRouter & UseFigmaOptions) + | (UseFimgaFromUrl & UseFigmaOptions) + | (UseFigmaFromFileNodeKey & UseFigmaOptions); + +export interface UseFigmaOptions { + use_session_cache?: boolean; +} + +export interface UseFigmaFromRouter { + type: "use-router"; + router?: NextRouter; +} + +export interface UseFimgaFromUrl { + type: "use-url"; + url: string; +} + +export interface UseFigmaFromFileNodeKey { + type: "use-file-node-id"; + file: string; + node: string; +} + +export type TUseDesignFile = + | TFetchFileForApp + | { + __type: "error"; + reason: "no-auth" | "unauthorized"; + cached?: TFetchFileForApp; + } + | { __type: "error"; reason: "no-file" } + | { __type: "loading" }; diff --git a/editor/hooks/use-figma/use-figma-community.ts b/editor/hooks/use-figma/use-figma-community.ts new file mode 100644 index 000000000..ee66e0beb --- /dev/null +++ b/editor/hooks/use-figma/use-figma-community.ts @@ -0,0 +1,40 @@ +import { useState, useEffect } from "react"; +import type { TUseDesignFile, UseFigmaInput, UseFimgaFromUrl } from "./types"; +import { Client } from "@figma-api/community"; +import { TargetNodeConfig } from "query/target-node"; + +const client = Client(); + +/** + * Figma Community File Retrieval Hook + * This does not use... + * 1. local store since the api response is static and cached by browser. + * 2. procedual loading since whole file is archived at the server. + * @returns + */ +export function useFigmaCommunityFile({ id }: { id: string }) { + const [file, setFile] = useState({ + __type: "loading", + }); + + useEffect(() => { + // load with community client + client.file(id).then(({ data }) => { + setFile({ + ...data, + key: id, + __initial: true, // ? + __type: "file-fetched-for-app", + }); + }); + }, [id]); + + // + return file; +} + +export function useFigmaCommunityNode() { + const [design, setDesign] = useState(null); + throw new Error("not implemented"); + // +} diff --git a/editor/hooks/use-design.ts b/editor/hooks/use-figma/use-figma.ts similarity index 77% rename from editor/hooks/use-design.ts rename to editor/hooks/use-figma/use-figma.ts index f2dbd4efb..5c222420d 100644 --- a/editor/hooks/use-design.ts +++ b/editor/hooks/use-figma/use-figma.ts @@ -1,4 +1,4 @@ -import { NextRouter, useRouter } from "next/router"; +import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { DesignProvider, analyzeDesignUrl } from "@design-sdk/url-analysis"; import { @@ -8,17 +8,18 @@ import { import { fetch } from "@design-sdk/figma-remote"; import { personal } from "@design-sdk/figma-auth-store"; import { configure_auth_credentials } from "@design-sdk/figma-remote"; -import { TargetNodeConfig } from "../query/target-node"; +import { TargetNodeConfig } from "query/target-node"; import { FigmaRemoteErrors, UnauthorizedError, NotfoundError, } from "@design-sdk/figma-remote"; -import { RemoteDesignSessionCacheStore } from "../store"; +import { RemoteDesignSessionCacheStore } from "store"; import { convert } from "@design-sdk/figma-node-conversion"; import { mapper } from "@design-sdk/figma-remote"; import { useFigmaAuth } from "scaffolds/workspace/figma-auth"; import { FigmaDesignRepository, TFetchFileForApp } from "@editor/figma-file"; +import type { TUseDesignFile, UseFigmaInput, UseFimgaFromUrl } from "./types"; // globally configure auth credentials for interacting with `@design-sdk/figma-remote` configure_auth_credentials({ @@ -30,36 +31,11 @@ configure_auth_credentials({ */ export const P_DESIGN = "design"; -type UseDesignProp = - | (UseDesignFromRouter & UseDesingOptions) - | (UseDesingFromUrl & UseDesingOptions) - | (UseDesignFromFileAndNode & UseDesingOptions); - -interface UseDesingOptions { - use_session_cache?: boolean; -} - -interface UseDesignFromRouter { - type: "use-router"; - router?: NextRouter; -} - -interface UseDesingFromUrl { - type: "use-url"; - url: string; -} - -interface UseDesignFromFileAndNode { - type: "use-file-node-id"; - file: string; - node: string; -} - -export function useDesign({ +export function useFigmaNode({ use_session_cache = false, type, ...props -}: UseDesignProp) { +}: UseFigmaInput) { const [design, setDesign] = useState(null); const fat = useFigmaAuth(); const router = (type === "use-router" && props["router"]) ?? useRouter(); @@ -79,7 +55,7 @@ export function useDesign({ } case "use-router": { const designparam: string = router.query[P_DESIGN] as string; - const _r = designparam && analyze(designparam); + const _r = designparam && analyzeRouterQuery(designparam); switch (_r) { case "figma": { targetnodeconfig = parseFileAndNodeId(designparam); @@ -96,7 +72,7 @@ export function useDesign({ break; } case "use-url": { - targetnodeconfig = parseFileAndNodeId((props as UseDesingFromUrl).url); + targetnodeconfig = parseFileAndNodeId((props as UseFimgaFromUrl).url); break; } } @@ -176,36 +152,28 @@ export function useDesign({ return design; } -export type TUseDesignFile = - | TFetchFileForApp - | { - __type: "error"; - reason: "no-auth" | "unauthorized"; - cached?: TFetchFileForApp; - } - | { __type: "error"; reason: "no-file" } - | { __type: "loading" }; - -export function useDesignFile({ file }: { file: string }) { +export function useFigmaFile({ file }: { file: string }) { const [designfile, setDesignFile] = useState({ __type: "loading", }); const fat = useFigmaAuth(); + + async function iterator() { + const repo = new FigmaDesignRepository({ + personalAccessToken: fat.personalAccessToken, + accessToken: fat.accessToken.token, + }); + const iterator = repo.fetchFile(file); + let next: IteratorResult; + while ((next = await iterator.next()).done === false) { + setDesignFile(next.value); + } + } + useEffect(() => { if (file) { if (fat.personalAccessToken || fat.accessToken.token) { - async function handle() { - const repo = new FigmaDesignRepository({ - personalAccessToken: fat.personalAccessToken, - accessToken: fat.accessToken.token, - }); - const iterator = repo.fetchFile(file); - let next: IteratorResult; - while ((next = await iterator.next()).done === false) { - setDesignFile(next.value); - } - } - handle(); + iterator(); } else { if (fat.accessToken.loading) { setDesignFile({ @@ -240,7 +208,7 @@ export function useDesignFile({ file }: { file: string }) { return designfile; } -const analyze = (query: string): "id" | DesignProvider => { +const analyzeRouterQuery = (query: string): "id" | DesignProvider => { const _r = analyzeDesignUrl(query); if (_r == "unknown") { return "id"; diff --git a/editor/package.json b/editor/package.json index f45835539..5fe0a1f97 100644 --- a/editor/package.json +++ b/editor/package.json @@ -23,6 +23,7 @@ "@emotion/react": "^11.11.0", "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.0", + "@figma-api/community": "^0.0.5", "@floating-ui/core": "^1.0.1", "@floating-ui/react-dom": "^1.0.0", "@floating-ui/react-dom-interactions": "^0.10.2", diff --git a/editor/pages/community/file/[id]/index.tsx b/editor/pages/community/file/[key]/index.tsx similarity index 74% rename from editor/pages/community/file/[id]/index.tsx rename to editor/pages/community/file/[key]/index.tsx index 89ce97a39..5609483f1 100644 --- a/editor/pages/community/file/[id]/index.tsx +++ b/editor/pages/community/file/[key]/index.tsx @@ -1,7 +1,11 @@ import React, { useEffect, useState } from "react"; import Head from "next/head"; import { SigninToContinuePrmoptProvider } from "components/prompt-banner-signin-to-continue"; -import { Editor, SetupEditor } from "scaffolds/editor"; +import { + Editor, + EditorDefaultProviders, + SetupFigmaCommunityFileEditor, +} from "scaffolds/editor"; import { Workspace, useWorkspaceInitializerContext } from "scaffolds/workspace"; import { useRouter } from "next/router"; import { InferGetServerSidePropsType } from "next"; @@ -58,45 +62,26 @@ export default function FigmaCommunityFileEditorPage( - - + + - - + + ); } -interface CommunityFileSetupProps { - /** - * The file id of the community file. - */ - id: string; -} - -function CommunityFileSetup({ - children, - id, -}: React.PropsWithChildren) { - const { provideEditorSnapshot: initialize } = - useWorkspaceInitializerContext(); - - // TODO: - return <>{children}; -} - export async function getServerSideProps(context) { return { props: new FigmaCommunityArchiveMetaRepository().getProps( - context.params.id + context.params.key ), }; } diff --git a/editor/pages/community/file/[id]/notes.md b/editor/pages/community/file/[key]/notes.md similarity index 100% rename from editor/pages/community/file/[id]/notes.md rename to editor/pages/community/file/[key]/notes.md diff --git a/editor/pages/figma/inspect-component.tsx b/editor/pages/figma/inspect-component.tsx index ca578b8ca..bdd999aa3 100644 --- a/editor/pages/figma/inspect-component.tsx +++ b/editor/pages/figma/inspect-component.tsx @@ -17,12 +17,12 @@ import { WorkspaceContentPanelGridLayout, } from "layouts/panel"; import { WorkspaceBottomPanelDockLayout } from "layouts/panel/workspace-bottom-panel-dock-layout"; -import { useDesign } from "hooks"; +import { useFigmaNode } from "hooks"; import { make_instance_component_meta } from "@code-features/component"; export default function InspectComponent() { // - const design = useDesign({ type: "use-router" }); + const design = useFigmaNode({ type: "use-router" }); if (!design) { return ; } diff --git a/editor/pages/figma/inspect-frame.tsx b/editor/pages/figma/inspect-frame.tsx index 7222de65f..aac50aaa1 100644 --- a/editor/pages/figma/inspect-frame.tsx +++ b/editor/pages/figma/inspect-frame.tsx @@ -1,7 +1,7 @@ import React from "react"; import { MonacoEditor } from "components/code-editor"; import { SceneNode } from "@design-sdk/figma-types"; -import { useDesign } from "hooks"; +import { useFigmaNode } from "hooks"; import LoadingLayout from "layouts/loading-overlay"; /** @@ -10,7 +10,7 @@ import LoadingLayout from "layouts/loading-overlay"; */ export default function InspectAutolayout() { // - const design = useDesign({ type: "use-router" }); + const design = useFigmaNode({ type: "use-router" }); if (!design) { return ; } diff --git a/editor/pages/figma/inspect-raw.tsx b/editor/pages/figma/inspect-raw.tsx index ece227dc1..d596aba9d 100644 --- a/editor/pages/figma/inspect-raw.tsx +++ b/editor/pages/figma/inspect-raw.tsx @@ -1,6 +1,6 @@ import React from "react"; import { MonacoEditor } from "components/code-editor"; -import { useDesign } from "hooks"; +import { useFigmaNode } from "hooks"; import LoadingLayout from "layouts/loading-overlay"; /** @@ -9,7 +9,7 @@ import LoadingLayout from "layouts/loading-overlay"; */ export default function InspectRaw() { // - const design = useDesign({ type: "use-router" }); + const design = useFigmaNode({ type: "use-router" }); if (!design) { return ; } diff --git a/editor/pages/figma/to-token.tsx b/editor/pages/figma/to-token.tsx index 1d7ad6ed9..1ee22ed1a 100644 --- a/editor/pages/figma/to-token.tsx +++ b/editor/pages/figma/to-token.tsx @@ -10,7 +10,7 @@ import { LayerHierarchy } from "components/editor-hierarchy"; import { WorkspaceContentPanelGridLayout } from "layouts/panel/workspace-content-panel-grid-layout"; import { WorkspaceContentPanel } from "layouts/panel"; import { WorkspaceBottomPanelDockLayout } from "layouts/panel/workspace-bottom-panel-dock-layout"; -import { useDesign } from "hooks"; +import { useFigmaNode } from "hooks"; import { ImageRepository, MainImageRepository, @@ -19,7 +19,7 @@ import { RemoteImageRepositories } from "@design-sdk/figma-remote/asset-reposito import LoadingLayout from "layouts/loading-overlay"; export default function FigmaToReflectWidgetTokenPage() { - const design = useDesign({ type: "use-router" }); + const design = useFigmaNode({ type: "use-router" }); if (!design) { return ; diff --git a/editor/pages/files/[key]/index.tsx b/editor/pages/files/[key]/index.tsx index 5975ccfc4..c64ae4808 100644 --- a/editor/pages/files/[key]/index.tsx +++ b/editor/pages/files/[key]/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { SigninToContinuePrmoptProvider } from "components/prompt-banner-signin-to-continue"; -import { Editor, SetupEditor } from "scaffolds/editor"; +import { Editor, SetupFigmaFileEditor } from "scaffolds/editor"; import { Workspace } from "scaffolds/workspace/workspace"; import { EditorDefaultProviders } from "scaffolds/editor"; import { EditorBrowserMetaHead } from "components/editor"; @@ -16,7 +16,7 @@ export default function FileEntryEditor() { return ( - - + ); diff --git a/editor/pages/live/index.tsx b/editor/pages/live/index.tsx index a773c56e6..df0d6b34d 100644 --- a/editor/pages/live/index.tsx +++ b/editor/pages/live/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import Pusher from "pusher-js"; import LoadingLayout from "layouts/loading-overlay"; -import { useDesign } from "hooks"; +import { useFigmaNode } from "hooks"; import { designToCode, Result } from "@designto/code"; import { TargetNodeConfig } from "../../query/target-node"; import { @@ -71,7 +71,7 @@ export default function LiveSessionPage() { } function DesignProxyPage({ file, node }: { file: string; node: string }) { - const design = useDesign({ + const design = useFigmaNode({ type: "use-file-node-id", file: file, node: node, diff --git a/editor/scaffolds/editor/_providers.tsx b/editor/scaffolds/editor/_providers.tsx index 6d785902a..20bbd2ef8 100644 --- a/editor/scaffolds/editor/_providers.tsx +++ b/editor/scaffolds/editor/_providers.tsx @@ -4,7 +4,6 @@ import { EditorImageRepositoryProvider } from "./editor-image-repository-provide import { EditorPreviewDataProvider } from "./editor-preview-provider"; import { EditorCodeWebworkerProvider } from "scaffolds/editor/editor-code-webworker-provider"; import { EditorToastProvider } from "./editor-toast-provider"; -import { FigmaImageServiceProvider } from "./editor-figma-image-service-provider"; import { FigmaImageServiceProviderForCanvasRenderer } from "./editor-figma-image-service-for-canvas-provider"; import { DashboardStateProvider } from "@code-editor/dashboard"; import { EditorState, useEditorState } from "core/states"; @@ -34,13 +33,11 @@ export function EditorDefaultProviders(props: { children: React.ReactNode }) { - - - - {props.children} - - - + + + {props.children} + + diff --git a/editor/scaffolds/editor/editor-figma-image-service-provider.tsx b/editor/scaffolds/editor/editor-figma-image-service-provider.tsx index 49f0d8bcf..1397c3d41 100644 --- a/editor/scaffolds/editor/editor-figma-image-service-provider.tsx +++ b/editor/scaffolds/editor/editor-figma-image-service-provider.tsx @@ -1,25 +1,42 @@ -import { useWorkspace, useWorkspaceState } from "core/states"; -import React, { useCallback, useEffect, useMemo } from "react"; -import { FigmaImageService } from "services"; +import { WorkspaceState, useWorkspace, useWorkspaceState } from "core/states"; +import React, { useEffect, useMemo } from "react"; +import { FigmaImageService, ImageClientInterface } from "services"; type Fetcher = { fetch: FigmaImageService["fetch"] }; type FetcherParams = Parameters; +type ApiClientResolver = ({ + filekey, + authentication, +}: { + filekey: string; + authentication?: WorkspaceState["figmaAuthentication"]; +}) => ImageClientInterface | undefined | "reject"; export const FigmaImageServiceContext = React.createContext(null); export function FigmaImageServiceProvider({ filekey, children, + resolveApiClient, }: React.PropsWithChildren<{ + resolveApiClient: ApiClientResolver; filekey: string; }>) { const wssate = useWorkspaceState(); const { pushTask, popTask } = useWorkspace(); const service = useMemo(() => { - if (!filekey || !wssate.figmaAuthentication) return; + const client = resolveApiClient({ + filekey: filekey, + authentication: wssate.figmaAuthentication, + }); + + if (client === "reject" || !client) { + // do not create service without valid client. + return; + } - return new FigmaImageService(filekey, wssate.figmaAuthentication, null, 24); + return new FigmaImageService(filekey, client, null, 24); }, [filekey, wssate.figmaAuthentication]); const fetcher = useMemo(() => { diff --git a/editor/scaffolds/editor/index.ts b/editor/scaffolds/editor/index.ts index 59b735bfd..f73d6257a 100644 --- a/editor/scaffolds/editor/index.ts +++ b/editor/scaffolds/editor/index.ts @@ -1,4 +1,8 @@ -export { SetupEditor, useEditorSetupContext } from "./setup"; +export { + SetupFigmaFileEditor, + SetupFigmaCommunityFileEditor, + useEditorSetupContext, +} from "./setup"; export { Editor } from "./editor"; export { EditorDefaultProviders } from "./_providers"; export { useFigmaImageService } from "./editor-figma-image-service-provider"; diff --git a/editor/scaffolds/editor/setup.tsx b/editor/scaffolds/editor/setup.tsx index 801498dc9..bba84f056 100644 --- a/editor/scaffolds/editor/setup.tsx +++ b/editor/scaffolds/editor/setup.tsx @@ -1,11 +1,14 @@ import React, { useEffect, useCallback, useState } from "react"; import { NextRouter } from "next/router"; import { EditorPage, EditorSnapshot, useEditorState } from "core/states"; -import { useDesignFile } from "hooks"; +import { useFigmaCommunityFile, useFigmaFile } from "hooks"; import { warmup } from "scaffolds/editor"; import type { FileResponse } from "@design-sdk/figma-remote-types"; import { useWorkspaceInitializerContext } from "scaffolds/workspace"; import { useDispatch } from "@code-editor/preferences"; +import { FigmaImageServiceProvider } from "./editor-figma-image-service-provider"; +import { Client as FigmaImageClient } from "@design-sdk/figma-remote-api"; +import { Client as FigmaCommunityImageClient } from "@figma-api/community"; const action_fetchfile_id = "fetchfile" as const; @@ -22,22 +25,28 @@ export function useEditorSetupContext() { return React.useContext(EditorSetupContext); } -export function SetupEditor({ +interface EssentialEditorSetupProps { + nodeid: string; + filekey: string; + router: NextRouter; +} + +function FigmaEditorBaseSetup({ + file, filekey, nodeid, router, children, -}: React.PropsWithChildren<{ - nodeid: string; - filekey: string; - router: NextRouter; -}>) { + loaded, +}: React.PropsWithChildren< + EssentialEditorSetupProps & { + file: FileResponse; + loaded?: boolean; + } +>) { const { provideEditorSnapshot: initialize } = useWorkspaceInitializerContext(); - // background whole file fetching - const file = useDesignFile({ file: filekey }); - // todo background file fetching to task queue // useEffect(() => { // const task = @@ -51,16 +60,7 @@ export function SetupEditor({ // }; // }, [file]); - const [loading, setLoading] = useState(true); const [state] = useEditorState(); - const prefDispatch = useDispatch(); - - const openFpatConfigurationPreference = useCallback(() => { - prefDispatch({ - type: "open", - route: "/figma/personal-access-token", - }); - }, [prefDispatch]); const initialCanvasMode = q_map_canvas_mode_from_query( router.query.mode as string @@ -136,22 +136,103 @@ export function SetupEditor({ ); useEffect(() => { - if (!loading) { + if (file) { + initWith(file); + } + }, [file]); + + return ( + + {children} + + ); +} + +export function SetupFigmaCommunityFileEditor({ + filekey, + nodeid, + router, + children, +}: React.PropsWithChildren) { + const fig = useFigmaCommunityFile({ id: filekey }); + const [file, setFile] = useState(null); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + switch (fig.__type) { + case "error": { + // TODO: set error here + break; + } + case "loading": { + // Do nothing. the community file won't be having loading state though. + break; + } + case "file-fetched-for-app": { + // ready. + setLoaded(true); + setFile(fig); + break; + } + } + }, [fig]); + + return ( + + FigmaCommunityImageClient()} + > + {children} + + + ); +} + +export function SetupFigmaFileEditor({ + filekey, + nodeid, + router, + children, +}: React.PropsWithChildren) { + const [file, setFile] = useState(null); + const [loaded, setLoaded] = useState(false); + + // background whole file fetching + const fig = useFigmaFile({ file: filekey }); + + const prefDispatch = useDispatch(); + + const openFpatConfigurationPreference = useCallback(() => { + prefDispatch({ + type: "open", + route: "/figma/personal-access-token", + }); + }, [prefDispatch]); + + useEffect(() => { + if (loaded) { return; } - if (file.__type === "loading") { + if (fig.__type === "loading") { return; } - if (file.__type === "error") { + if (fig.__type === "error") { // handle error by reason - switch (file.reason) { + switch (fig.reason) { case "unauthorized": case "no-auth": { - if (file.cached) { - initWith(file.cached); - setLoading(false); + if (fig.cached) { + setFile(fig.cached); + setLoaded(true); alert( "You will now see the cached version of this file. To view the latest version, setup your personall access token." ); @@ -170,22 +251,38 @@ export function SetupEditor({ return; } - if (!file.__initial) { + if (!fig.__initial) { // when full file is loaded, allow editor with user interaction. - setLoading(false); + setLoaded(true); } - initWith(file); + setFile(fig); }, [ filekey, - file, - file.__type == "file-fetched-for-app" ? file.document?.children : null, + fig, + fig.__type == "file-fetched-for-app" ? fig.document?.children : null, ]); return ( - - {children} - + + { + if (!filekey || !authentication) return "reject"; + return FigmaImageClient({ + ...authentication, + }); + }} + > + {children} + + ); } diff --git a/editor/scaffolds/workspace/warmup.ts b/editor/scaffolds/workspace/warmup.ts index e4efaa5b1..66722ee95 100644 --- a/editor/scaffolds/workspace/warmup.ts +++ b/editor/scaffolds/workspace/warmup.ts @@ -3,23 +3,29 @@ import { EditorSnapshot, WorkspaceState, } from "core/states"; -import { merge_initial_workspace_state_with_editor_snapshot } from "core/states"; +import { + merge_initial_workspace_state_with_editor_snapshot, + EssentialWorkspaceInfo, +} from "core/states"; import { workspaceWarmupReducer, workspaceReducer } from "core/reducers"; import { PendingState, PendingState_Pending } from "core/utility-types"; import { WorkspaceAction, WorkspaceWarmupAction } from "core/actions"; -const initial_pending_workspace_state = - create_initial_pending_workspace_state(); // export type InitializationAction = | { type: "warmup"; value: WorkspaceWarmupAction } | { type: "setup-with-editor-snapshot"; value: EditorSnapshot } | { type: "update"; value: WorkspaceAction }; +type TState = PendingState & EssentialWorkspaceInfo; + export function initialReducer( - state: PendingState, + state: TState, action: InitializationAction ): PendingState { + const initial_pending_workspace_state = + create_initial_pending_workspace_state({}); + switch (action.type) { case "setup-with-editor-snapshot": return { @@ -63,13 +69,16 @@ export function initialReducer( } } -export function safestate(initialState) { +export function safestate(initialState: TState): WorkspaceState { + const initial_pending_workspace_state = + create_initial_pending_workspace_state({}); + switch (initialState.type) { case "success": return initialState.value; case "pending": { if (initialState.value) { - return initialState.value; + return initialState.value as WorkspaceState; } else { return initial_pending_workspace_state; } diff --git a/editor/scaffolds/workspace/workspace.tsx b/editor/scaffolds/workspace/workspace.tsx index eab049cfa..83e7f61c7 100644 --- a/editor/scaffolds/workspace/workspace.tsx +++ b/editor/scaffolds/workspace/workspace.tsx @@ -5,7 +5,7 @@ import React, { createContext, } from "react"; import { useRouter } from "next/router"; -import { StateProvider } from "core/states"; +import { EssentialWorkspaceInfo, StateProvider } from "core/states"; import { SetupWorkspace } from "./setup"; import { WorkspaceDefaultProviders } from "./_providers"; import * as warmup from "./warmup"; @@ -21,7 +21,10 @@ export function useWorkspaceInitializerContext() { return useContext(WorkspaceInitializerContext); } -export function Workspace({ children }: React.PropsWithChildren<{}>) { +export function Workspace({ + children, + ...seed +}: React.PropsWithChildren) { const router = useRouter(); const handleDispatch = useCallback((action: WorkspaceAction) => { @@ -46,7 +49,10 @@ export function Workspace({ children }: React.PropsWithChildren<{}>) { type: "pending", }); - const safe_value = warmup.safestate(initialState); + const safe_value = warmup.safestate({ + ...initialState, + ...seed, + }); return ( <> diff --git a/editor/services/figma-image-service/index.ts b/editor/services/figma-image-service/index.ts index 48af60119..344c36f33 100644 --- a/editor/services/figma-image-service/index.ts +++ b/editor/services/figma-image-service/index.ts @@ -1,21 +1,25 @@ -import type { TargetImage, FigmaImageType, ImageHashMap } from "./types"; +import type { TargetImage, ImageHashMap } from "./types"; import { FigmaNodeImageStore, ImageHashmapCache } from "./figma-image-store"; import { Client } from "@design-sdk/figma-remote-api"; -import { fetchNodeAsImage } from "@design-sdk/figma-remote"; + +type TClientInterface = ReturnType; +export type ImageClientInterface = { + fileImages: TClientInterface["fileImages"]; + fileImageFills: TClientInterface["fileImageFills"]; +}; /** * if the new request is made within 0.1 second (100ms), merge the requests. */ const DEBOUNCE = 100; +const ACTION_UPDATE_IMAGE_HASH_MAP = "update-image-hash-map"; + /** * Top level Figma image service to fetch images in a safe, promised and request efficient way. */ export class FigmaImageService { private store = new FigmaNodeImageStore(this.filekey); - private api = Client({ - ...this.authentication, - }); private retries: number; /** @@ -25,10 +29,11 @@ export class FigmaImageService { constructor( readonly filekey: string, - private readonly authentication: { - personalAccessToken?: string; - accessToken?: string; - }, + readonly api: ImageClientInterface, + // private readonly authentication: { + // personalAccessToken?: string; + // accessToken?: string; + // }, readonly version?: string | null, readonly maxQueue = 100 ) {} @@ -78,11 +83,11 @@ export class FigmaImageService { // #endregion private async updateImageHashMap() { - if (this.hasTask("update-image-hash-map")) { - return this.tasks.get("update-image-hash-map"); + if (this.hasTask(ACTION_UPDATE_IMAGE_HASH_MAP)) { + return this.tasks.get(ACTION_UPDATE_IMAGE_HASH_MAP); } else { const { data } = await this.pushTask( - "update-image-hash-map", + ACTION_UPDATE_IMAGE_HASH_MAP, this.api.fileImageFills(this.filekey) ); @@ -133,25 +138,25 @@ export class FigmaImageService { } const tasktargetsmap: { - bakes: string[]; + exports: string[]; images: string[]; } = tasktargets.reduce( (acc, id) => { if (is_node_id(id)) { - acc["bakes"].push(id); + acc["exports"].push(id); } else { acc["images"].push(id); } return acc; }, { - bakes: [], + exports: [], images: [], } ); // - const { bakes, images } = tasktargetsmap; + const { exports, images } = tasktargetsmap; const tasks: { [key: string]: Promise } = {}; // @@ -174,11 +179,11 @@ export class FigmaImageService { }); } } - // fetch bakes (handle debounce) - if (bakes.length > 0) { + // fetch exports (handle debounce) + if (exports.length > 0) { if (do_debounce) { // fetch with merged queues - this.queue = new Set([...this.queue, ...bakes]); + this.queue = new Set([...this.queue, ...exports]); const handle_queue = async () => { const fetchtargets = async (...targets) => { @@ -220,7 +225,7 @@ export class FigmaImageService { this.timeout = setTimeout(handle_queue, DEBOUNCE); } - const promises = bakes.map((b) => { + const promises = exports.map((b) => { const key = b; const promise = new Promise((resolve) => { this.queuedResolvers.set(key, resolve); @@ -236,21 +241,25 @@ export class FigmaImageService { ).reduce((acc, data: string, i) => { return { ...acc, - [bakes[i]]: data, + [exports[i]]: data, }; }, {}); resolve({ ...datas, ...results }); }); } else { - // this does not support format, scale, ... (todo) - const request = fetchNodeAsImage( - this.filekey, - this.authentication, - ...bakes - ); - - for (const t of bakes) { + // fetcher promise with data un-wrapping + const request = new Promise(async (resolve) => { + this.api + .fileImages(this.filekey, { + ids: exports, + }) + .then(({ data }) => { + resolve(data.images); + }); + }); + + for (const t of exports) { tasks[t] = this.pushTask( t, new Promise((resolve) => { diff --git a/editor/services/index.ts b/editor/services/index.ts index 9de40073f..46897c9fc 100644 --- a/editor/services/index.ts +++ b/editor/services/index.ts @@ -1,2 +1,3 @@ export { FigmaImageService } from "./figma-image-service"; +export type { ImageClientInterface } from "./figma-image-service"; export { FigmaCommentsStore, useFigmaComments } from "./figma-comments-service"; diff --git a/testing/report/package.json b/testing/report/package.json index e0c7f7f5b..6aae49ae6 100644 --- a/testing/report/package.json +++ b/testing/report/package.json @@ -18,7 +18,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "@figma-api/community": "^0.0.3", + "@figma-api/community": "^0.0.5", "axios-cache-interceptor": "^1.1.1", "minimist": "^1.2.8", "ora": "^5.4.0" diff --git a/yarn.lock b/yarn.lock index e8d0da874..e5206ab95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2374,10 +2374,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== -"@figma-api/community@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@figma-api/community/-/community-0.0.3.tgz#f0556beb72722a2051e70901e4b36661575e4028" - integrity sha512-F3SV7wTrVGQ+hBH/Wk8nmy96ZZ3rFqGhycEO8LCZZKf0LHQL84eH/DHe/P+mgZCBEIHQXUtWu3K5TwSiF+ycVw== +"@figma-api/community@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@figma-api/community/-/community-0.0.5.tgz#bb747f8881e199ab9d28f53cfcd14af676a1d02a" + integrity sha512-cjr13Ku0kZypbCmcBIRMZAHTnMjQ+vfodoDDxAOreGNjaRA4jAoMILZImzVy8EmbcJccch+bHwsVrK/ibWacqg== dependencies: mime-types "^2.1.35"