Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Comment thread
benlife5 marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/visual-editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ export * from "./types/index.ts";
export * from "./editor/index.ts";
export * from "./components/index.ts";
export { LocalEditorShell } from "./local-editor/LocalEditorShell.tsx";
export {
buildLocalEditorDocumentRequestPath,
readDocumentLayoutData,
readLocalEditorPreviewLayoutData,
} from "./local-editor/selection.ts";
export * from "./fields/index.ts";
50 changes: 46 additions & 4 deletions packages/visual-editor/src/local-editor/LocalEditorShell.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from "react";
import { ExternalLink } from "lucide-react";
import { Editor } from "../editor/Editor.tsx";
import { VisualEditorProvider } from "../utils/VisualEditorProvider.tsx";
import { LocalEditorControls } from "./LocalEditorControls.tsx";
import { LocalEditorNotice } from "./LocalEditorNotice.tsx";
import {
buildEditorLocalDevOptions,
buildLocalEditorDocumentRequestPath,
buildLocalEditorPreviewUrl,
buildLocalEditorSelection,
syncSelectionToUrl,
updateSearchParam,
Expand Down Expand Up @@ -120,6 +122,8 @@ export const LocalEditorShell = ({
selectedTemplateDefaults,
selectedTemplateId,
]);
const canOpenPreview =
!isDocumentLoading && !!documentResponse?.document && !!selectedTemplateId;

return (
<div
Expand All @@ -145,16 +149,54 @@ export const LocalEditorShell = ({
Switch templates, entities, and locales against local snapshot data.
</p>
</div>
<code
<button
type="button"
disabled={!canOpenPreview}
onClick={() => {
if (
typeof window === "undefined" ||
!canOpenPreview ||
!selectedTemplateId ||
!selectedLocale
) {
return;
}

const previewUrl = buildLocalEditorPreviewUrl({
origin: window.location.origin,
templateId: selectedTemplateId,
entityId: selectedEntity?.entityId,
locale: selectedLocale,
});

window.open(previewUrl, "_blank", "noopener,noreferrer");
}}
style={{
appearance: "none",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
background: "#111",
border: "1px solid #111",
color: "#fff",
padding: "6px 10px",
cursor: canOpenPreview ? "pointer" : "not-allowed",
font: "inherit",
fontWeight: 600,
opacity: canOpenPreview ? 1 : 0.55,
padding: "8px 14px",
borderRadius: "999px",
lineHeight: 1,
}}
title={routePath}
>
{routePath}
</code>
Open Preview
<ExternalLink
aria-hidden="true"
size={15}
style={{ flexShrink: 0 }}
/>
</button>
</div>

<LocalEditorControls
Expand Down
115 changes: 115 additions & 0 deletions packages/visual-editor/src/local-editor/selection.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as lzstring from "lz-string";
import type { Config } from "@puckeditor/core";
import type { LocalDevOptions } from "../editor/types.ts";
import type {
Expand All @@ -6,6 +7,7 @@ import type {
LocalEditorManifestResponse,
BuildEditorLocalDevOptionsArgs,
} from "./types.ts";
import type { StreamDocument } from "../utils/types/StreamDocument.ts";

type SelectionResult = {
supportedTemplateIds: string[];
Expand All @@ -17,6 +19,8 @@ type SelectionResult = {
selectedMode: LocalEditorMode;
};

export const LOCAL_EDITOR_API_BASE_PATH = "/__yext_visual_editor/local-editor";

export const updateSearchParam = (key: string, value: string) => {
if (typeof window === "undefined") {
return;
Expand Down Expand Up @@ -144,6 +148,74 @@ export const buildLocalEditorDocumentRequestPath = ({
return `${apiBasePath}/document?${documentParams.toString()}`;
};

export const buildLocalEditorPreviewUrl = ({
origin,
templateId,
entityId,
locale,
}: {
origin: string;
templateId: string;
entityId?: string;
locale: string;
}): string => {
const previewStorageKey = buildLocalEditorLayoutStorageKey(
templateId,
locale
);
const previewUrl = new URL(`/edit/${encodeURIComponent(templateId)}`, origin);
Comment thread
benlife5 marked this conversation as resolved.

previewUrl.searchParams.set("preview", "1");
previewUrl.searchParams.set("templateId", templateId);
previewUrl.searchParams.set("locale", locale);
previewUrl.searchParams.set("previewStorageKey", previewStorageKey);

if (entityId) {
previewUrl.searchParams.set("entityId", entityId);
}

return previewUrl.toString();
};

export const readLocalEditorPreviewLayoutData = ({
previewStorageKey,
document,
}: {
previewStorageKey: string | null;
document: Record<string, unknown>;
}): Record<string, unknown> => {
const publishedLayoutData = readDocumentLayoutData(document);
if (typeof window === "undefined" || !previewStorageKey) {
return publishedLayoutData;
}

const compressedHistories = window.localStorage.getItem(previewStorageKey);
if (!compressedHistories) {
return publishedLayoutData;
}

const decompressedHistories = lzstring.decompress(compressedHistories);
if (!decompressedHistories) {
return publishedLayoutData;
}

try {
const histories = JSON.parse(decompressedHistories) as Array<{
state?: { data?: Record<string, unknown> };
}>;
const latestHistory = histories
.slice()
.reverse()
.find((history) => {
return !!history?.state?.data;
});

return latestHistory?.state?.data ?? publishedLayoutData;
} catch {
return publishedLayoutData;
}
};

export const buildEditorLocalDevOptions = ({
selectedTemplateId,
selectedEntity,
Expand Down Expand Up @@ -209,3 +281,46 @@ const pickPreferredEntity = (

return entities[0];
};

const buildLocalEditorLayoutStorageKey = (
templateId: string,
locale: string
): string => {
const layoutScopeKey = `${templateId}:${locale}`;
return `devTEMPLATE_${templateId}LAYOUT_${hashCode(layoutScopeKey)}`;
};

export const readDocumentLayoutData = (
streamDocument: StreamDocument
): Record<string, unknown> => {
const layoutJson = streamDocument?.__?.layout;
if (typeof layoutJson !== "string" || !layoutJson.length) {
return {
root: {},
content: [],
zones: {},
};
}

try {
return JSON.parse(layoutJson) as Record<string, unknown>;
} catch {
return {
root: {},
content: [],
zones: {},
};
}
};

const hashCode = (value: string): number => {
let hash = 0;

for (let index = 0; index < value.length; index += 1) {
const characterCode = value.charCodeAt(index);
hash = (hash << 5) - hash + characterCode;
hash |= 0;
}

return Math.abs(hash);
};
Loading
Loading