Skip to content

feat: add rtf functionality #560

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -13335,4 +13335,4 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-----------

This file was generated with the generate-license-file npm package!
https://www.npmjs.com/package/generate-license-file
https://www.npmjs.com/package/generate-license-file
4 changes: 3 additions & 1 deletion packages/visual-editor/src/components/atoms/maybeRTF.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const MaybeRTF = ({
const background = useBackground();

if (!data) {
return undefined;
return <></>;
}

if (typeof data === "string") {
Expand All @@ -41,4 +41,6 @@ export const MaybeRTF = ({
return <LexicalRichText serializedAST={JSON.stringify(data.json)} />;
}
}

return <></>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import {
resolveYextStructField,
ComponentFields,
EntityField,
resolveTranslatableString,
resolveTranslatableRichText,
msg,
pt,
HeadingLevel,
ThemeOptions,
resolveTranslatableString,
resolveTranslatableRichText,
} from "@yext/visual-editor";
import { AnalyticsScopeProvider } from "@yext/pages-components";

Expand Down
55 changes: 42 additions & 13 deletions packages/visual-editor/src/editor/TranslatableRichTextField.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TranslatableRichText } from "../types/types.ts";
import { TranslatableRichText, RichText } from "../types/types.ts";
import { MsgString, pt } from "../utils/i18nPlatform.ts";
import { CustomField, FieldLabel } from "@measured/puck";
import { getDisplayValue } from "../utils/resolveTranslatableString.tsx";
import { resolveTranslatableRichText } from "../utils/resolveTranslatableString.tsx";
import React from "react";
import {
TARGET_ORIGINS,
Expand All @@ -22,51 +22,80 @@ export function TranslatableRichTextField<
render: ({ onChange, value }) => {
const { i18n } = useTranslation();
const locale = i18n.language;
const resolvedValue = getDisplayValue(value, locale);
const fieldLabel = (label && pt(label)) + ` (${locale})`;
const resolvedValue = resolveTranslatableRichText(value, locale);
const fieldLabel = label ? `${pt(label)} (${locale})` : "";

const [pendingMessageId, setPendingMessageId] = React.useState<
string | undefined
>();

const { sendToParent: openConstantValueEditor } = useSendMessageToParent(
"constantValueEditorOpened",
TARGET_ORIGINS
);

const [pendingMessageId, setPendingMessageId] = React.useState<
string | undefined
>();
useReceiveMessage(
"constantValueEditorClosed",
TARGET_ORIGINS,
(_, payload) => {
if (pendingMessageId && pendingMessageId === payload?.id) {
handleNewValue(payload.value);
// Handle the new Storm payload structure with locale, rtfJson, and rtfHtml
if (payload.locale && payload.rtfJson) {
handleNewValue(payload.rtfJson, payload.locale, payload.rtfHtml);
} else {
// Fallback for backward compatibility
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once Storm is deployed it will always send the new payload right? If so then you shouldn't need any fallback.

handleNewValue(payload.value || "", locale);
}
}
}
);

const handleClick = () => {
const messageId = `RichText-${Date.now()}`;
setPendingMessageId(messageId);
const valueForCurrentLocale =
typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, any>)[locale]
: undefined;

const initialValue = React.isValidElement(resolvedValue)
? valueForCurrentLocale?.json
: valueForCurrentLocale;

openConstantValueEditor({
payload: {
type: "RichText",
value: resolvedValue,
value: initialValue,
id: messageId,
fieldName: fieldLabel,
locale: locale,
},
});

// localDev
// for local development testing
if (
window.location.href.includes("http://localhost:5173/dev-location")
) {
handleNewValue(prompt("Enter text:") ?? "");
const userInput = prompt("Enter Rich Text (HTML):");
handleNewValue("", locale, userInput ?? "");
}
};

const handleNewValue = (newValue: string) => {
const handleNewValue = (
newValue: string,
targetLocale?: string,
rtfHtml?: string
) => {
const localeToUpdate = targetLocale || locale;

// Create a RichText object if we have both JSON and HTML
const richTextValue = rtfHtml
? ({ json: newValue, html: rtfHtml } as RichText)
: newValue;

onChange({
...(typeof value === "object" && !Array.isArray(value) ? value : {}),
[locale]: newValue,
[localeToUpdate]: richTextValue,
hasLocalizedValue: "true",
} as TranslatableRichText as T);
};
Expand Down
8 changes: 5 additions & 3 deletions packages/visual-editor/src/editor/TranslatableStringField.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TranslatableString } from "../types/types.ts";
import { MsgString, pt } from "../utils/i18nPlatform.ts";
import { AutoField, CustomField, FieldLabel } from "@measured/puck";
import { getDisplayValue } from "../utils/resolveTranslatableString.tsx";
import { resolveTranslatableString } from "../utils/resolveTranslatableString.tsx";
import React from "react";
import { useTranslation } from "react-i18next";

Expand All @@ -18,10 +18,12 @@ export function TranslatableStringField<
render: ({ onChange, value }) => {
const { i18n } = useTranslation();
const locale = i18n.language;
const resolvedValue = resolveTranslatableString(value, locale);

const autoField = (
<AutoField
field={{ type: fieldType ?? "text" }}
value={getDisplayValue(value, locale)}
value={resolvedValue}
onChange={(val) => {
return onChange({
...(typeof value === "object" && !Array.isArray(value)
Expand All @@ -39,7 +41,7 @@ export function TranslatableStringField<
}

return (
<FieldLabel label={pt(label) + ` (${locale})`}>{autoField}</FieldLabel>
<FieldLabel label={`${pt(label)} (${locale})`}>{autoField}</FieldLabel>
);
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ const SubfieldsInput = ({
constantValueEnabled={value?.constantValueOverride?.[field]}
toggleConstantValueEnabled={toggleConstantValueEnabled}
label={label}
showLocale={type === "type.string" && !disallowTranslation}
showLocale={
(type === "type.string" || type === "type.rich_text_v2") &&
!disallowTranslation
}
/>
</div>
{value?.constantValueOverride?.[field] && (
Expand Down
2 changes: 1 addition & 1 deletion packages/visual-editor/src/internal/puck/Subfields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const PROMO_SECTION_SUBFIELD: SubFieldProps = [
{ field: "title", type: "type.string", label: msg("fields.title", "Title") },
{
field: "description",
type: "type.string",
type: "type.rich_text_v2",
label: msg("fields.description", "Description"),
},
{ field: "cta", type: "type.cta", label: msg("fields.cta", "CTA") },
Expand Down
61 changes: 38 additions & 23 deletions packages/visual-editor/src/utils/resolveTranslatableString.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,48 @@ export const resolveTranslatableString = (
};

/**
* Converts a type TranslatableRichText to a type that can be viewed on the page
* Converts a type TranslatableRichText to string or RTF Element
* @param translatableRichText
* @param locale
*/
export const resolveTranslatableRichText = (
translatableRichText?: TranslatableRichText,
locale: string = "en"
): string | React.ReactElement => {
if (!translatableRichText) return "";
try {
if (!translatableRichText) return "";

if (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's this if condition, the one below, and then the logic in isRichText which also checks if the type is an object like on line 55. Can this be simplified to just check if translatableRichText is a map, grab the locale, and then always call toStringOrElement which happens in both of these conditions?

typeof translatableRichText === "string" ||
isRichText(translatableRichText)
) {
return toStringOrElement(translatableRichText);
}

if (
typeof translatableRichText === "string" ||
isRichText(translatableRichText)
) {
return toStringOrElement(translatableRichText);
}
if (typeof translatableRichText === "object") {
const localizedValue = translatableRichText[locale];
if (localizedValue) {
return toStringOrElement(localizedValue);
}
}

const localizedValue = translatableRichText[locale];
if (localizedValue) {
return toStringOrElement(localizedValue);
return "";
} catch (error) {
console.warn("Error in resolveTranslatableRichText:", error);
return "";
}

return "";
};

function isRichText(value: unknown): value is RichText {
return (
typeof value === "object" &&
value !== null &&
("html" in value || "json" in value)
);
}

/**
* Takes a TranslatableString and a locale and returns the value to be displayed in the editor input
* Takes a TranslatableString and a locale and returns the value as a string
* @param translatableString a TranslatableString
* @param locale "en" or other locale value
* @return string to be displayed in the editor input
Expand All @@ -75,6 +90,7 @@ export function getDisplayValue(
if (!locale) {
locale = "en";
}

if (typeof translatableString === "string") {
return translatableString;
}
Expand All @@ -100,14 +116,6 @@ function richTextToString(rtf: RichText): string {
return rtf.html || rtf.json || "";
}

function isRichText(value: unknown): value is RichText {
return (
typeof value === "object" &&
value !== null &&
("html" in value || "json" in value)
);
}

/**
* Converts a "string | RichText" type to "string | React.ReactElement" which can be viewed on the page
* @param value
Expand All @@ -118,5 +126,12 @@ function toStringOrElement(
if (isRichText(value)) {
return <MaybeRTF data={value} />;
}
return value ?? "";

// Ensure we only return strings
if (typeof value === "string") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, we can't migrate the strings to rtf because we don't know the formatting and that happens in Storm, right? But now the rtf fields from Storm that we're going to render are always the html version so can't we just always pretend it's a string we need to render? Or is this to support the json version from the document because Spruce hasn't switch it over yet?

Basically I'm trying to see if we can simplify a lot of the crazy logic in here. Is that possible? If not now, will it be possible at some point, and if so, what is needed to do that?

return value;
}

// If value is anything else (object, number, etc.), return empty string
return "";
}