diff --git a/apps/example/.env.sample b/apps/example/.env.sample deleted file mode 100644 index 8b0fbc03..00000000 --- a/apps/example/.env.sample +++ /dev/null @@ -1,2 +0,0 @@ -NEXT_PUBLIC_EASYBLOCKS_ACCESS_TOKEN= -NEXT_PUBLIC_EASYBLOCKS_PROJECT_ID= \ No newline at end of file diff --git a/apps/example/src/app/(system)/entries/[id]/page.tsx b/apps/example/src/app/(system)/entries/[id]/page.tsx new file mode 100644 index 00000000..b7c59eda --- /dev/null +++ b/apps/example/src/app/(system)/entries/[id]/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { Fragment, useEffect } from "react"; +import { useMockService } from "@/data/MockData/useMockService"; +import { DocumentWidget } from "@/app/document-widget"; +import Link from "next/link"; +import { notFound } from "next/navigation"; + +export type EntryPageParams = { + id: string; +}; +export default function EntryPage({ + params: { id }, +}: { + params: EntryPageParams; +}) { + const mockService = useMockService(); + + if (!mockService) { + return null; + } + + const entry = mockService.getEntryById(id); + + if (!entry) { + notFound(); + } + + return ( + +
+ + ← All Entries + +

{entry.name}

+ +
+
ID
+
{entry.id}
+
+ +
+
Name
+
{entry.name}
+
+ +
+
Description
+
{entry.description}
+
+ +
+
Last modified
+
{entry.updatedAt.toString()}
+
+ +
+
Created at
+
{entry.createdAt.toString()}
+
+ +
+
Page
+
+ { + mockService.updateEntry({ + ...entry, + page: document, + }); + }} + /> +
+
+
+
+ ); +} + +export const revalidate = 0; diff --git a/apps/example/src/app/(system)/entries/page.tsx b/apps/example/src/app/(system)/entries/page.tsx new file mode 100644 index 00000000..1637e910 --- /dev/null +++ b/apps/example/src/app/(system)/entries/page.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { Fragment, useEffect } from "react"; +import { useMockService } from "@/data/MockData/useMockService"; +import Link from "next/link"; + +export default function EntriesPage() { + const service = useMockService(); + + if (!service) { + return null; + } + + const entries = service.getEntries(); + + const cellClasses = "p-2 text-left border border-gray-200 leading-none"; + + return ( + +
+

System entries

+ + + + + + + + + + + {entries.map((entry) => ( + + + + + + ))} + +
NameIdLast modified
+ + {entry.name} + + {entry.id} + {entry.updatedAt.toDateString()} +
+
+
+ ); +} + +export const revalidate = 0; diff --git a/apps/example/src/app/(system)/layout.tsx b/apps/example/src/app/(system)/layout.tsx new file mode 100644 index 00000000..826fa119 --- /dev/null +++ b/apps/example/src/app/(system)/layout.tsx @@ -0,0 +1,38 @@ +import "../globals.css"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "ACME Software", + description: "ACME Software using Easyblocks", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +
+
+
+
+ + ACME Company Software + +
+
+ John Doe - john.doe@acme.com +
+
+
+
+
{children}
+ + + ); +} diff --git a/apps/example/src/app/(system)/not-found.tsx b/apps/example/src/app/(system)/not-found.tsx new file mode 100644 index 00000000..36b07446 --- /dev/null +++ b/apps/example/src/app/(system)/not-found.tsx @@ -0,0 +1,9 @@ +import Link from "next/link"; + +export default function NotFound() { + return ( +
+

Could not find requested resource

+
+ ); +} diff --git a/apps/example/src/app/(system)/page.tsx b/apps/example/src/app/(system)/page.tsx new file mode 100644 index 00000000..bd3d1f75 --- /dev/null +++ b/apps/example/src/app/(system)/page.tsx @@ -0,0 +1,16 @@ +import Link from "next/link"; + +export default function MainPage() { + return ( +
+

Hello

+ + + System entries + +
+ ); +} diff --git a/apps/example/src/app/document-preview.tsx b/apps/example/src/app/document-preview.tsx deleted file mode 100644 index 08cd836b..00000000 --- a/apps/example/src/app/document-preview.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import { ContextParams } from "@easyblocks/core"; - -function DocumentPreview({ - configId, - accessToken, - version, -}: { - configId: string; - accessToken: string; - version?: number; -}) { - return ( - - ); -} - -export { DocumentPreview }; - -function getComponentConfigPreviewImageURL({ - configId, - contextParams, - accessToken, - version, -}: { - configId: string; - version?: number; - accessToken: string; - contextParams: ContextParams; -}): string | undefined { - // we generate screenshot only if template config is remote - const url = getConfigPreviewUrl({ - configId, - version, - accessToken, - contextParams: { - ...contextParams, - locale: "en-US", - }, - }); - - const searchParams = new URLSearchParams(); - searchParams.set("access_key", "yeKG1SmUryGzuw"); - searchParams.set("url", url); - searchParams.set("cache", "true"); - searchParams.set("format", "jpg"); - searchParams.set("block_cookie_banners", "true"); - searchParams.set("block_trackers", "true"); - searchParams.set("device_scale_factor", "1"); - searchParams.set("selector", "#__shopstory-container"); - - const screenshotPreviewUrl = `https://api.screenshotone.com/take?${searchParams.toString()}`; - - if (isLocalhost()) { - console.log(`(localhost) Preview URL: ${url}`); - return; - } - - return screenshotPreviewUrl; -} - -function getConfigPreviewUrl({ - configId, - accessToken, - version, - contextParams, -}: { - configId: string; - version?: number; - accessToken: string; - contextParams: ContextParams; -}): string { - // 1. Get current URL - const currentUrl = window.location.href; - - // 2. Remove query params - const urlWithoutQueryParams = currentUrl.split("?")[0]; - - // 3. Add new query param - const searchParams = new URLSearchParams(); - searchParams.set("configId", configId); - if (version) { - searchParams.set("version", version.toString()); - } - searchParams.set("accessToken", accessToken); - searchParams.set( - "contextParams", - encodeURIComponent(JSON.stringify(contextParams)) - ); - searchParams.set("mode", "app"); - - // 4. Generate new URL string - const newUrl = `${urlWithoutQueryParams}shopstory-canvas?${searchParams.toString()}`; - return newUrl; -} - -function isLocalhost() { - return ( - location.hostname === "localhost" || - location.hostname === "127.0.0.1" || - location.hostname === "" - ); -} diff --git a/apps/example/src/app/document-widget.tsx b/apps/example/src/app/document-widget.tsx new file mode 100644 index 00000000..189d3bc4 --- /dev/null +++ b/apps/example/src/app/document-widget.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogOverlay, + DialogPortal, + DialogTrigger, +} from "@radix-ui/react-dialog"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { generatePreviewUrl } from "@/utils/generatePreviewUrl"; + +const DocumentWidget: React.FC<{ + document: any; + onSave: (document: any) => void; +}> = ({ document, onSave }) => { + const router = useRouter(); + + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const [editorIframeNode, setEditorIframeNode] = + useState(null); + + useEffect(() => { + function handleMessage(event: MessageEvent) { + if (event.data.type === "@easyblocks/closed") { + window.removeEventListener("message", handleMessage); + setIsDialogOpen(false); + router.refresh(); + } + + if (event.data.type === "@easyblocks/content-saved") { + onSave(event.data.document); + } + } + + editorIframeNode?.contentWindow?.addEventListener("message", handleMessage); + + return () => { + editorIframeNode?.contentWindow?.removeEventListener( + "message", + handleMessage + ); + }; + }, [editorIframeNode, router]); + + let canvasUrl = `${ + window.location.origin + }/shopstory-canvas?rootContainer=content&mode=app&source=sales-app&contextParams=${JSON.stringify( + { locale: "en-US" } + )}`; + + if (document) { + canvasUrl += `&documentId=${document.id}`; + } + + return ( +
+ {document && ( +
+ +
+ )} + + { + setIsDialogOpen(isOpen); + }} + > + + + + + + + + + + + +
+ ); +}; + +export { DocumentWidget }; diff --git a/apps/example/src/app/edit-document-button.tsx b/apps/example/src/app/edit-document-button.tsx deleted file mode 100644 index 2772c777..00000000 --- a/apps/example/src/app/edit-document-button.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; - -import { - Dialog, - DialogContent, - DialogOverlay, - DialogPortal, - DialogTrigger, -} from "@radix-ui/react-dialog"; -import { useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; -import { accessToken } from "./lib/apiClient"; - -function EditDocumentButton({ documentId }: { documentId: string }) { - const router = useRouter(); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [editorIframeNode, setEditorIframeNode] = - useState(null); - - useEffect(() => { - function handleMessage(event: MessageEvent) { - if (event.data.type === "@shopstory-editor/closed") { - window.removeEventListener("message", handleMessage); - setIsDialogOpen(false); - router.refresh(); - } - } - - editorIframeNode?.contentWindow?.addEventListener("message", handleMessage); - - return () => { - editorIframeNode?.contentWindow?.removeEventListener( - "message", - handleMessage - ); - }; - }, [editorIframeNode, router]); - - return ( - { - setIsDialogOpen(isOpen); - }} - > - - - - - - - - - - - ); -} - -export { EditDocumentButton }; diff --git a/apps/example/src/app/layout.tsx b/apps/example/src/app/layout.tsx deleted file mode 100644 index 57322998..00000000 --- a/apps/example/src/app/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import "./globals.css"; -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - {children} - - ); -} diff --git a/apps/example/src/app/new-document-button.tsx b/apps/example/src/app/new-document-button.tsx deleted file mode 100644 index 21d54f03..00000000 --- a/apps/example/src/app/new-document-button.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import { - Dialog, - DialogContent, - DialogOverlay, - DialogPortal, - DialogTrigger, -} from "@radix-ui/react-dialog"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; -import { accessToken } from "./lib/apiClient"; - -function NewDocument() { - const router = useRouter(); - const [isDialogOpen, setIsDialogOpen] = useState(false); - - const [editorIframeNode, setEditorIframeNode] = - useState(null); - - useEffect(() => { - function handleMessage(event: MessageEvent) { - if (event.data.type === "@shopstory-editor/closed") { - window.removeEventListener("message", handleMessage); - setIsDialogOpen(false); - router.refresh(); - } - } - - editorIframeNode?.contentWindow?.addEventListener("message", handleMessage); - - return () => { - editorIframeNode?.contentWindow?.removeEventListener( - "message", - handleMessage - ); - }; - }, [editorIframeNode, router]); - - const canvasUrl = `/shopstory-canvas?rootContainer=content&mode=app&source=sales-app&contextParams=${JSON.stringify( - { locale: "en-US" } - )}`; - - return ( - { - setIsDialogOpen(isOpen); - }} - > - - - - - - - - - - - ); -} - -export { NewDocument }; diff --git a/apps/example/src/app/page.tsx b/apps/example/src/app/page.tsx deleted file mode 100644 index 500144f7..00000000 --- a/apps/example/src/app/page.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import formatRelative from "date-fns/formatRelative"; -import { Fragment } from "react"; -import { DocumentPreview } from "./document-preview"; -import { EditDocumentButton } from "./edit-document-button"; -import { accessToken, createApiClient } from "./lib/apiClient"; -import { NewDocument } from "./new-document-button"; - -export default async function Home() { - const apiClient = createApiClient(); - - if (!process.env.NEXT_PUBLIC_EASYBLOCKS_PROJECT_ID) { - throw new Error("Missing NEXT_PUBLIC_EASYBLOCKS_PROJECT_ID"); - } - - const documents = await apiClient.documents.getDocuments({ - projectId: process.env.NEXT_PUBLIC_EASYBLOCKS_PROJECT_ID, - }); - - return ( - -
- ACME Company Software -
-
John Doe
-
john.doe@acme.com
-
-
-
- {documents.length >= 0 && ( -
- -
- )} - -
- {[...documents] - .sort( - (a, b) => - new Date(b.updated_at).getTime() - - new Date(a.updated_at).getTime() - ) - .map((document) => { - return ( -
- -
-
-

{document.title}

-
- {formatRelative( - new Date(document.updated_at), - new Date(), - { - weekStartsOn: 1, - } - )} -
-
-
- -
-
-
- ); - })} - - {documents.length === 0 && ( -

-
No documents
-

- )} -
-
-
- ); -} - -export const revalidate = 0; diff --git a/apps/example/src/app/shopstory-canvas/layout.tsx b/apps/example/src/app/shopstory-canvas/layout.tsx new file mode 100644 index 00000000..30f09b07 --- /dev/null +++ b/apps/example/src/app/shopstory-canvas/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/example/src/app/shopstory-canvas/page.tsx b/apps/example/src/app/shopstory-canvas/page.tsx index 0bbc0faa..1623309c 100644 --- a/apps/example/src/app/shopstory-canvas/page.tsx +++ b/apps/example/src/app/shopstory-canvas/page.tsx @@ -1,39 +1,12 @@ "use client"; -import { - builtinEditableComponents, - builtinEditableComponentsDefinitions, -} from "@easyblocks/editable-components"; -import { Canvas, ShopstoryProvider } from "@easyblocks/react"; -import { accessToken } from "../lib/apiClient"; - +import { Canvas } from "@easyblocks/react"; +import { shopstoryConfig } from "@/app/shopstory/shopstory.config"; +import { AcmeShopstoryProvider } from "@/app/shopstory/AcmeShopstoryProvider"; export default function ShopstoryCanvas() { return ( - - - + + + ); } diff --git a/apps/example/src/app/shopstory/AcmeShopstoryProvider.tsx b/apps/example/src/app/shopstory/AcmeShopstoryProvider.tsx new file mode 100644 index 00000000..7088aac9 --- /dev/null +++ b/apps/example/src/app/shopstory/AcmeShopstoryProvider.tsx @@ -0,0 +1,12 @@ +import { builtinEditableComponents } from "@easyblocks/editable-components"; +import { ShopstoryProvider } from "@easyblocks/react"; + +export const AcmeShopstoryProvider: React.FC<{ + children?: React.ReactNode; +}> = ({ children }) => { + return ( + + {children} + + ); +}; diff --git a/apps/example/src/app/shopstory/shopstory.config.ts b/apps/example/src/app/shopstory/shopstory.config.ts new file mode 100644 index 00000000..607521b1 --- /dev/null +++ b/apps/example/src/app/shopstory/shopstory.config.ts @@ -0,0 +1,25 @@ +import { Config } from "@easyblocks/core"; +import { builtinEditableComponentsDefinitions } from "@easyblocks/editable-components"; + +export const shopstoryConfig: Config = { + accessToken: process.env.NEXT_PUBLIC_EASYBLOCKS_ACCESS_TOKEN, + locales: [ + { + code: "en-US", + isDefault: true, + }, + { + code: "de-DE", + fallback: "en-US", + }, + ], + rootContainers: { + content: { + defaultConfig: { + _template: "$RootSections", + data: [], + }, + }, + }, + components: builtinEditableComponentsDefinitions, +}; diff --git a/apps/example/src/data/MockData/MockDataService.ts b/apps/example/src/data/MockData/MockDataService.ts new file mode 100644 index 00000000..52668b28 --- /dev/null +++ b/apps/example/src/data/MockData/MockDataService.ts @@ -0,0 +1,85 @@ +export interface Entry { + id: string; + name: string; + description: string; + createdAt: Date; + updatedAt: Date; + page?: any; +} + +const TEN_DAYS_IN_SECONDS = 10 * 24 * 60 * 60 * 1000; + +export class MockDataService { + private readonly storageKey = "entries"; + private readonly mockEntries: Entry[] = [ + { + id: "84b180c9-4a47-4ecf-bc59-21c2069060e3", + name: "Mock Entry 1", + description: "This is a mock entry", + createdAt: new Date(new Date().getTime() - 2 * TEN_DAYS_IN_SECONDS), + updatedAt: new Date(new Date().getTime() - 2 * TEN_DAYS_IN_SECONDS), + }, + { + id: "d9b657e3-2b17-47ae-8450-3d7f1ce47ce3", + name: "Mock Entry 2", + description: "This is another mock entry", + createdAt: new Date(new Date().getTime() - TEN_DAYS_IN_SECONDS), + updatedAt: new Date(new Date().getTime() - TEN_DAYS_IN_SECONDS), + }, + { + id: "b85bb2dc-9769-44b7-ac53-93e0ca9134eb", + name: "Mock Entry 3", + description: "This is yet another mock entry", + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + constructor() { + this.initStorage(); + } + + private initStorage() { + if (!localStorage.getItem(this.storageKey)) { + localStorage.setItem(this.storageKey, JSON.stringify(this.mockEntries)); + } + } + + public getEntries(): Entry[] { + const entriesString = localStorage.getItem(this.storageKey); + if (entriesString === null) { + throw new Error("unreachable"); + } + return JSON.parse(entriesString).map((entry: any) => ({ + ...entry, + createdAt: new Date(entry.createdAt), + updatedAt: new Date(entry.updatedAt), + })); + } + + public getEntryById(id: string): Entry | undefined { + const entries = this.getEntries(); + return entries.find((entry) => entry.id === id); + } + + public addEntry(entry: Entry) { + const currentEntries = this.getEntries(); + if (currentEntries) { + currentEntries.push(entry); + localStorage.setItem(this.storageKey, JSON.stringify(currentEntries)); + } + } + + public updateEntry(entry: Entry) { + let entries = this.getEntries(); + if (entries) { + entries = entries.map((e) => { + if (e.id === entry.id) { + return entry; + } + return e; + }); + localStorage.setItem(this.storageKey, JSON.stringify(entries)); + } + } +} diff --git a/apps/example/src/data/MockData/useMockService.ts b/apps/example/src/data/MockData/useMockService.ts new file mode 100644 index 00000000..ef80e753 --- /dev/null +++ b/apps/example/src/data/MockData/useMockService.ts @@ -0,0 +1,12 @@ +import { useEffect, useState } from "react"; +import { MockDataService } from "@/data/MockData/MockDataService"; + +export function useMockService() { + const [service, setService] = useState(null); + + useEffect(() => { + setService(new MockDataService()); + }, []); + + return service; +} diff --git a/apps/example/src/pages/documents/[id]/index.tsx b/apps/example/src/pages/documents/[id]/index.tsx index a48b5341..30ae8def 100644 --- a/apps/example/src/pages/documents/[id]/index.tsx +++ b/apps/example/src/pages/documents/[id]/index.tsx @@ -1,81 +1,3 @@ -import { Metadata, RenderableContent, ShopstoryClient } from "@easyblocks/core"; -import { - builtinClientOnlyEditableComponents, - builtinEditableComponentsDefinitions, -} from "@easyblocks/editable-components"; -import { - Shopstory, - ShopstoryMetadataProvider, - ShopstoryProvider, -} from "@easyblocks/react"; -import { GetServerSideProps, InferGetServerSidePropsType } from "next"; -import { createApiClient } from "../../../app/lib/apiClient"; - -function DocumentPage({ - content, - meta, -}: InferGetServerSidePropsType) { - return ( - - - - - - ); +export default function DocumentsPage() { + return
Nothing
; } - -export default DocumentPage; - -export const getServerSideProps: GetServerSideProps< - { content: RenderableContent; meta: Metadata }, - { id: string } -> = async (context) => { - const { id } = context.params ?? {}; - - const apiClient = createApiClient(); - - if (!id) { - return { - notFound: true, - }; - } - - const document = await apiClient.documents.getDocumentById({ - projectId: "89ed48c6-dc0b-4936-9a97-4eb791396853", - documentId: id, - }); - - if (document === null) { - return { - notFound: true, - }; - } - - const shopstoryClient = new ShopstoryClient( - { - components: builtinEditableComponentsDefinitions, - rootContainers: { - content: { - defaultConfig: { - _template: "$RootSections", - data: [], - }, - }, - }, - accessToken: - process.env.NEXT_PUBLIC_ACCESS_TOKEN ?? - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NjE5NTM2NzMsImV4cCI6MTY5MzQ4OTY3MywiYXVkIjoiYXV0aGVudGljYXRlZCIsInN1YiI6IjEyMyIsImVtYWlsIjoibWljaGFsQHNob3BzdG9yeS5hcHAiLCJwcm9qZWN0X2lkIjoiODllZDQ4YzYtZGMwYi00OTM2LTlhOTctNGViNzkxMzk2ODUzIn0.3I9QMjhPkYHtzW19g-uIjattobPBiXEK0Fz4AwxEfQg", - }, - { locale: "en-US" } - ); - - const content = shopstoryClient.add(document.config.config); - const meta = await shopstoryClient.build(); - - return { - props: { - content, - meta, - }, - }; -}; diff --git a/apps/example/src/utils/generatePreviewUrl.ts b/apps/example/src/utils/generatePreviewUrl.ts new file mode 100644 index 00000000..6545ff7d --- /dev/null +++ b/apps/example/src/utils/generatePreviewUrl.ts @@ -0,0 +1,39 @@ +export function generatePreviewUrl(params: { + id: string; + projectId: string; + version: number; + contextParams: Record; + accessToken: string; + canvasUrl: string; +}): string { + const previewUrl = `${params.canvasUrl}?documentId=${params.id}&projectId=${ + params.projectId + }&accessToken=${params.accessToken}&contextParams=${JSON.stringify( + params.contextParams + )}&preview=true&version=${params.version}`; + + if (isLocalhost()) { + return "https://placehold.co/600x400"; + } + + const searchParams = new URLSearchParams(); + searchParams.set("access_key", "yeKG1SmUryGzuw"); + searchParams.set("url", previewUrl); + searchParams.set("cache", "true"); + searchParams.set("format", "jpg"); + searchParams.set("block_cookie_banners", "true"); + searchParams.set("block_trackers", "true"); + searchParams.set("device_scale_factor", "1"); + searchParams.set("selector", "#__shopstory-container"); + + const screenshotPreviewUrl = `https://api.screenshotone.com/take?${searchParams.toString()}`; + return screenshotPreviewUrl; +} + +function isLocalhost() { + return ( + location.hostname === "localhost" || + location.hostname === "127.0.0.1" || + location.hostname === "" + ); +} diff --git a/packages/core/src/buildPreview.ts b/packages/core/src/buildPreview.ts index 267f31c9..433c3026 100644 --- a/packages/core/src/buildPreview.ts +++ b/packages/core/src/buildPreview.ts @@ -3,20 +3,24 @@ import { ShopstoryClient } from "./ShopstoryClient"; import { Config, ContextParams } from "./types"; export async function buildPreview( - configId: string, + documentId: string, + projectId: string, width: number | undefined, widthAuto: boolean | undefined, accessToken: string, config: Config, contextParams: ContextParams ) { - const response = await fetch(`${getAppUrlRoot()}/api/configs/${configId}`, { - method: "GET", - headers: { - Accept: "application/json", - "x-shopstory-access-token": accessToken, - }, - }); + const response = await fetch( + `${getAppUrlRoot()}/api/projects/${projectId}/documents/${documentId}`, + { + method: "GET", + headers: { + Accept: "application/json", + "x-shopstory-access-token": accessToken, + }, + } + ); const data = await response.json(); @@ -26,7 +30,7 @@ export async function buildPreview( _template: "$ComponentContainer", widthAuto: widthAuto ?? false, width: width ?? 5000, - Component: [data.config], + Component: [data.config.config], }); const meta = await client.build(); diff --git a/packages/editor/src/Editor.tsx b/packages/editor/src/Editor.tsx index c19d32fb..286e7e67 100644 --- a/packages/editor/src/Editor.tsx +++ b/packages/editor/src/Editor.tsx @@ -937,18 +937,11 @@ const EditorContent = ({ handleSetBreakpoint(newBreakpointIndex); }, actions, - save: async (localisedDocument, externals) => { - if (props.save) { - await props.save(localisedDocument, externals); - } else { - window.postMessage({ - type: "@shopstory-editor/content-saved", - payload: { - localisedDocument, - references: externals, - }, - }); - } + save: async (documentData) => { + window.postMessage({ + type: "@easyblocks/content-saved", + document: documentData, + }); }, text: undefined, locales: props.locales, @@ -1138,12 +1131,15 @@ const EditorContent = ({ saveNow().finally(() => { setDataSaverOverlayOpen(false); + window.postMessage( + { + type: "@easyblocks/closed", + }, + "*" + ); + if (props.onClose) { props.onClose(); - } else { - window.postMessage({ - type: "@shopstory-editor/closed", - }); } if (isDemoProject) { diff --git a/packages/editor/src/EditorContext.ts b/packages/editor/src/EditorContext.ts index 8680136b..922e190b 100644 --- a/packages/editor/src/EditorContext.ts +++ b/packages/editor/src/EditorContext.ts @@ -8,8 +8,6 @@ import { CompilationCache } from "@easyblocks/compiler"; import { CompiledComponentConfig, ConfigComponent, - ExternalReference, - LocalisedDocument, LocalizedText, Resource, ResourceDefinition, @@ -27,10 +25,12 @@ export type EditorContextType = BaseEditorContextType & { isEditing?: boolean; actions: ActionsType; text?: ResourceDefinition & TextSyncers; - save: ( - localisedDocument: LocalisedDocument, - externals: ExternalReference[] - ) => Promise; + save: (document: { + id: string; + version: number; + updatedAt: number; + projectId: string; + }) => Promise; compiledComponentConfig?: CompiledComponentConfig; configAfterAuto?: ConfigComponent; variantsManager?: VariantsManager; diff --git a/packages/editor/src/launchEditor.tsx b/packages/editor/src/launchEditor.tsx index b1d328e7..64b27386 100644 --- a/packages/editor/src/launchEditor.tsx +++ b/packages/editor/src/launchEditor.tsx @@ -64,6 +64,7 @@ export function launchEditor(props: EditorLauncherProps) { editorSearchParams.contextParams ?? props.contextParams ?? raiseError(`Missing "contextParams" value.`); + const rootContainer = editorSearchParams.rootContainer ?? props.rootContainer ?? props.mode; const mode = editorSearchParams.mode ?? "playground"; diff --git a/packages/editor/src/useDataSaver.ts b/packages/editor/src/useDataSaver.ts index dbdf84e7..ed646910 100644 --- a/packages/editor/src/useDataSaver.ts +++ b/packages/editor/src/useDataSaver.ts @@ -388,31 +388,45 @@ export function useDataSaver( } } - const configAfterSplit = splitConfigIntoSingleLocaleConfigs( - configToSaveWithLocalisedFlag, - editorContext.locales - ); - const localisedDocument: LocalisedDocument = {}; - const previewData = getPreviewData( - configToSaveWithLocalisedFlag, - editorContext.rootContainer - ); - - editorContext.locales.forEach((locale) => { - localisedDocument[locale.code] = { - documentId: editorContext.isPlayground - ? "playground-document" - : remoteDocument.current!.id, - config: configAfterSplit[locale.code], - preview: previewData, - projectId: editorContext.project - ? editorContext.project.id - : "playground", - rootContainer: editorContext.rootContainer, - }; - }); - - await editorContext.save(localisedDocument, externalReferences); + const documentData = { + id: editorContext.isPlayground + ? "playground-document" + : remoteDocument.current!.id, + version: editorContext.isPlayground + ? 0 + : remoteDocument.current!.version, + updatedAt: new Date().getTime(), + projectId: editorContext.project + ? editorContext.project.id + : "playground", + }; + + // const configAfterSplit = splitConfigIntoSingleLocaleConfigs( + // configToSaveWithLocalisedFlag, + // editorContext.locales + // ); + // + // const localisedDocument: LocalisedDocument = {}; + // const previewData = getPreviewData( + // configToSaveWithLocalisedFlag, + // editorContext.rootContainer + // ); + // + // editorContext.locales.forEach((locale) => { + // localisedDocument[locale.code] = { + // documentId: editorContext.isPlayground + // ? "playground-document" + // : remoteDocument.current!.id, + // config: configAfterSplit[locale.code], + // preview: previewData, + // projectId: editorContext.project + // ? editorContext.project.id + // : "playground", + // rootContainer: editorContext.rootContainer, + // }; + // }); + + await editorContext.save(documentData); if (Object.keys(externalsMap).length > 0) { const newFormValues = updateConfigExternals( diff --git a/packages/react/src/Canvas.tsx b/packages/react/src/Canvas.tsx index e76d1243..9a888efa 100644 --- a/packages/react/src/Canvas.tsx +++ b/packages/react/src/Canvas.tsx @@ -20,7 +20,7 @@ export function Canvas(props: CanvasProps) { }; useEffect(() => { - if (parseQueryParams().configId) { + if (parseQueryParams().preview) { setSelectedWindow("preview"); return; } diff --git a/packages/react/src/PreviewRenderer.tsx b/packages/react/src/PreviewRenderer.tsx index 559e2c2d..6287c023 100644 --- a/packages/react/src/PreviewRenderer.tsx +++ b/packages/react/src/PreviewRenderer.tsx @@ -16,10 +16,16 @@ export const PreviewRenderer: React.FC = ({ useEffect(() => { import("@easyblocks/core").then(({ buildPreview }) => { - const { configId, accessToken, width, widthAuto, contextParams } = - parseQueryParams(); + const { + documentId, + projectId, + accessToken, + width, + widthAuto, + contextParams, + } = parseQueryParams(); - if (!configId) { + if (!documentId || !projectId) { throw new Error("unreachable"); } @@ -32,7 +38,8 @@ export const PreviewRenderer: React.FC = ({ } buildPreview( - configId, + documentId, + projectId, width, widthAuto, accessToken, diff --git a/packages/react/src/parseQueryParams.ts b/packages/react/src/parseQueryParams.ts index 22f691d0..693fbf5f 100644 --- a/packages/react/src/parseQueryParams.ts +++ b/packages/react/src/parseQueryParams.ts @@ -1,19 +1,22 @@ import type { ContextParams } from "@easyblocks/core"; export function parseQueryParams(): { - configId: undefined | string; + documentId: undefined | string; + projectId: undefined | string; accessToken: undefined | string; mode: "playground" | "app"; widthAuto: boolean; width: number | undefined; contextParams: ContextParams | undefined; + preview: boolean; } { const urlSearchParams = new URLSearchParams(window.location.search); const queryParams = Object.fromEntries(urlSearchParams.entries()); const contextParams = parseContextParams(queryParams.contextParams); return { - configId: queryParams.configId, + documentId: queryParams.documentId, + projectId: queryParams.projectId, /** * We need shopstoryAccessToken for non-cloud versions - they rely on this query param. * It can be removed only when all clients move to cloud based version and 1.0.0 SDK. @@ -23,6 +26,7 @@ export function parseQueryParams(): { widthAuto: queryParams.widthAuto === "true" ? true : false, width: queryParams.width ? parseInt(queryParams.width) : undefined, contextParams, + preview: queryParams.preview === "true", }; }