import { EditorConfig, SharedSliceEditor } from "@prismicio/editor-fields"; import { DefaultErrorMessage } from "@prismicio/editor-ui"; import { renderSliceMock } from "@prismicio/mocks"; import { SharedSliceContent } from "@prismicio/types-internal/lib/content"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { FC, Suspense, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { useSelector } from "react-redux"; import { toast } from "react-toastify"; import { BaseStyles, Box, Flex, Spinner } from "theme-ui"; import { saveSliceMock, telemetry } from "@/apiClient"; import { DefaultErrorBoundary } from "@/features/errorBoundaries"; import useThrottle from "@/hooks/useThrottle"; import ScreenshotPreviewModal from "@/legacy/components/ScreenshotPreviewModal"; import { ComponentUI } from "@/legacy/lib/models/common/ComponentUI"; import { ScreenDimensions } from "@/legacy/lib/models/common/Screenshots"; import { Slices, VariationSM } from "@/legacy/lib/models/common/Slice"; import { selectEndpoints, selectSimulatorUrl } from "@/modules/environment"; import useSliceMachineActions from "@/modules/useSliceMachineActions"; import { SliceMachineStoreType } from "@/redux/type"; import { defaultSharedSliceContent } from "@/utils/editor"; import FailedConnect from "./components/FailedConnect"; import FullPage from "./components/FullPage"; import Header from "./components/Header"; import IframeRenderer from "./components/IframeRenderer"; import { Toolbar } from "./components/Toolbar"; import { ScreenSizeOptions, ScreenSizes, } from "./components/Toolbar/ScreensizeInput"; type SimulatorProps = { slice: ComponentUI; variation: VariationSM; }; const IFRAME_CONNECTION_TIMEOUT = 20000; const queryClient = new QueryClient(); const Simulator: FC = ({ slice, variation }) => { const { updateSliceMockSuccess } = useSliceMachineActions(); const { simulatorUrl, endpoints } = useSelector( (state: SliceMachineStoreType) => ({ simulatorUrl: selectSimulatorUrl(state), endpoints: selectEndpoints(state), }), ); const editorConfig: EditorConfig = useMemo( () => ({ embedApiEndpoint: new URL(endpoints.PrismicEmbed), authStrategy: "cookie", unsplashApiBaseUrl: new URL(endpoints.PrismicUnsplash), baseUrl: new URL("builder/", endpoints.PrismicWroom), }), [endpoints.PrismicEmbed, endpoints.PrismicUnsplash, endpoints.PrismicWroom], ); useEffect(() => { void telemetry.track({ event: "slice-simulator:open" }); }, []); const startedNewEditorSessionRef = useRef(false); useEffect(() => { startedNewEditorSessionRef.current = true; }, []); const trackWidgetUsed = (sliceId: string) => { if (!startedNewEditorSessionRef.current) return; startedNewEditorSessionRef.current = false; void telemetry.track({ event: "editor:widget-used", sliceId }); }; const [iframeConnectionStatus, setIframeConnectionStatus] = useState< "waiting" | "successful" | "failed" >("waiting"); useEffect(() => { if (iframeConnectionStatus === "waiting") { const timer = setTimeout(() => { setIframeConnectionStatus("failed"); }, IFRAME_CONNECTION_TIMEOUT); return () => clearTimeout(timer); } }, [iframeConnectionStatus]); const handleSimulatorConnectionResult = (result: "successful" | "failed") => { if (iframeConnectionStatus !== "waiting") { return; } if (result === "failed") { void telemetry.track({ event: "slice-simulator:is-not-running" }); } else { toggleIsDisplayEditor(true); } setIframeConnectionStatus(result); }; const onRetrigger = () => { setIframeConnectionStatus("waiting"); }; const [screenDimensions, setScreenDimensions] = useState( ScreenSizes[ScreenSizeOptions.DESKTOP], ); const sharedSlice = useMemo(() => Slices.fromSM(slice.model), [slice.model]); // state used only to store updates coming from the editor const [editorState, setEditorState] = useState( null, ); // computed state that takes the editorState if any change // for the current variation or directly the mocks const editorContent = useMemo(() => { if (editorState?.variation === variation.id) return editorState; return ( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing slice.mocks?.find((m) => m.variation === variation.id) || defaultSharedSliceContent(variation.id) ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [editorState, variation.id]); // this is temporary, it allows to retain the slice ID generated //at first render so we don't trigger too many re-render at each content change const renderedSliceId = useMemo( () => ( renderSliceMock(sharedSlice, editorContent) as { id: string; [k: string]: unknown; } ).id, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); // When the content change, we re-render the content but overwrite the newly // generated slice ID by the initial one for render optim const renderSliceMockCb = useCallback( () => () => ({ // cast as object because type is unknown ...(renderSliceMock(sharedSlice, editorContent) as object), id: renderedSliceId, }), // eslint-disable-next-line react-hooks/exhaustive-deps [sharedSlice, editorContent], ); const apiContent = useThrottle(renderSliceMockCb, 800, [ sharedSlice, editorContent, ]); const [isDisplayEditor, toggleIsDisplayEditor] = useState(false); const [isSavingMock, setIsSavingMock] = useState(false); const saveMock = async () => { if (editorState) { setIsSavingMock(true); try { const payload = { libraryID: slice.from, sliceID: slice.model.id, mocks: (slice.mocks ?? []) .filter((mock) => mock.variation !== editorState.variation) .concat(editorState), }; const { errors } = await saveSliceMock(payload); if (errors.length > 0) { throw errors; } updateSliceMockSuccess(payload); toast.success("Saved"); } catch (error) { console.error("Error while saving mock", error); toast.error("Error saving content"); } setIsSavingMock(false); } }; return (
toggleIsDisplayEditor(!isDisplayEditor)} onSaveMock={() => void saveMock()} isSavingMock={isSavingMock} /> {iframeConnectionStatus === "failed" ? ( ) : null} {iframeConnectionStatus === "successful" ? ( ) : ( <> {iframeConnectionStatus === "waiting" ? ( ) : null} )} {iframeConnectionStatus === "successful" && isDisplayEditor ? ( ( )} > { setEditorState(c); trackWidgetUsed(slice.model.id); }} sharedSlice={sharedSlice} /> ) : null} {/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions */} {!!slice.screenshots[variation.id]?.url && ( )} ); }; export default Simulator;