diff --git a/packages/studio/src/components/AddPageButton/BasicPageDataCollector.tsx b/packages/studio/src/components/AddPageButton/BasicPageDataCollector.tsx index 705700e82..b5d51c870 100644 --- a/packages/studio/src/components/AddPageButton/BasicPageDataCollector.tsx +++ b/packages/studio/src/components/AddPageButton/BasicPageDataCollector.tsx @@ -5,6 +5,7 @@ import useStudioStore from "../../store/useStudioStore"; import AddPageContext from "./AddPageContext"; import TemplateExpressionFormatter from "../../utils/TemplateExpressionFormatter"; import { GetPathVal, PropValueKind } from "@yext/studio-plugin"; +import PageDataValidator from "../../utils/PageDataValidator"; type BasicPageData = { pageName: string; @@ -22,6 +23,10 @@ export default function BasicPageDataCollector({ ); const { state } = useContext(AddPageContext); const isEntityPage = isPagesJSRepo && !state.isStatic; + const pageDataValidator = useMemo( + () => new PageDataValidator(isEntityPage), + [isEntityPage] + ); const formData: FormData = useMemo( () => ({ @@ -37,10 +42,18 @@ export default function BasicPageDataCollector({ const onConfirm = useCallback( async (data: BasicPageData) => { + const getPathValue = data.url + ? createGetPathVal(data.url, isEntityPage) + : undefined; + const validationResult = pageDataValidator.validate({ + ...data, + url: getPathValue?.value, + }); + if (!validationResult.valid) { + setErrorMessage(validationResult.errorMessages.join("\r\n")); + return false; + } try { - const getPathValue = data.url - ? createGetPathVal(data.url, isEntityPage) - : undefined; await handleConfirm(data.pageName, getPathValue); return true; } catch (err: unknown) { @@ -52,7 +65,7 @@ export default function BasicPageDataCollector({ } } }, - [handleConfirm, isEntityPage] + [handleConfirm, isEntityPage, pageDataValidator] ); const transformOnChangeValue = useCallback( diff --git a/packages/studio/src/components/PageSettingsButton/EntityPageModal.tsx b/packages/studio/src/components/PageSettingsButton/EntityPageModal.tsx index c0563e70b..97e23ede2 100644 --- a/packages/studio/src/components/PageSettingsButton/EntityPageModal.tsx +++ b/packages/studio/src/components/PageSettingsButton/EntityPageModal.tsx @@ -1,5 +1,5 @@ import useStudioStore from "../../store/useStudioStore"; -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import FormModal, { FormData } from "../common/FormModal"; import { GetPathVal, PropValueKind, ResponseType } from "@yext/studio-plugin"; import TemplateExpressionFormatter from "../../utils/TemplateExpressionFormatter"; @@ -10,6 +10,7 @@ import StreamScopeParser, { import { PageSettingsModalProps } from "./PageSettingsButton"; import { StaticPageSettings } from "./StaticPageModal"; import { streamScopeFormData } from "../AddPageButton/StreamScopeCollector"; +import PageDataValidator from "../../utils/PageDataValidator"; import { toast } from "react-toastify"; import { isEqual } from "lodash"; @@ -38,28 +39,34 @@ export default function EntityPageModal({ store.pages.updateStreamScope, store.actions.generateTestData, ]); - const isPathUndefined = !currGetPathValue; + const [errorMessage, setErrorMessage] = useState(""); + const pageDataValidator = useMemo(() => new PageDataValidator(true), []); + const isURLEditable = useMemo( + () => pageDataValidator.checkIsURLEditable(currGetPathValue?.value), + [currGetPathValue?.value, pageDataValidator] + ); const initialFormValue: EntityPageSettings = useMemo( () => ({ - url: getUrlDisplayValue(currGetPathValue), + url: isURLEditable ? getUrlDisplayValue(currGetPathValue) : "", ...StreamScopeParser.convertStreamScopeToForm(streamScope), }), - [currGetPathValue, streamScope] + [currGetPathValue, streamScope, isURLEditable] ); const entityFormData: FormData = useMemo( () => ({ url: { description: "URL Slug", - optional: isPathUndefined, - placeholder: isPathUndefined - ? "" - : "", + optional: !isURLEditable, + placeholder: isURLEditable + ? "" + : "", + disabled: !isURLEditable, }, ...streamScopeFormData, }), - [isPathUndefined] + [isURLEditable] ); const handleModalSave = useCallback( @@ -68,6 +75,13 @@ export default function EntityPageModal({ kind: PropValueKind.Expression, value: TemplateExpressionFormatter.getRawValue(form.url), }; + const validationResult = pageDataValidator.validate({ + url: getPathValue.value, + }); + if (!validationResult.valid) { + setErrorMessage(validationResult.errorMessages.join("\r\n")); + return false; + } if (form.url || currGetPathValue) { updateGetPathValue(pageName, getPathValue); } @@ -91,6 +105,7 @@ export default function EntityPageModal({ updateStreamScope, currGetPathValue, pageName, + pageDataValidator, generateTestData, streamScope, ] @@ -103,6 +118,7 @@ export default function EntityPageModal({ instructions="Use the optional fields below to specify which entities this page can access. Values should be separated by commas. Changing the scope of the stream (entity IDs, entity type IDs, and saved filter IDs) may result in entity data references being invalid or out of date." formData={entityFormData} initialFormValue={initialFormValue} + errorMessage={errorMessage} requireChangesToSubmit={true} handleClose={handleClose} handleConfirm={handleModalSave} diff --git a/packages/studio/src/components/PageSettingsButton/StaticPageModal.tsx b/packages/studio/src/components/PageSettingsButton/StaticPageModal.tsx index fd06a99c1..71078d6cd 100644 --- a/packages/studio/src/components/PageSettingsButton/StaticPageModal.tsx +++ b/packages/studio/src/components/PageSettingsButton/StaticPageModal.tsx @@ -1,8 +1,9 @@ import useStudioStore from "../../store/useStudioStore"; -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import FormModal, { FormData } from "../common/FormModal"; import { GetPathVal, PropValueKind } from "@yext/studio-plugin"; import { PageSettingsModalProps } from "./PageSettingsButton"; +import PageDataValidator from "../../utils/PageDataValidator"; export type StaticPageSettings = { url: string; @@ -21,7 +22,12 @@ export default function StaticPageModal({ store.pages.pages[pageName].pagesJS?.getPathValue, store.pages.updateGetPathValue, ]); - const isPathUndefined = !currGetPathValue; + const [errorMessage, setErrorMessage] = useState(""); + const pageDataValidator = useMemo(() => new PageDataValidator(), []); + const isURLEditable = useMemo( + () => pageDataValidator.checkIsURLEditable(currGetPathValue?.value), + [currGetPathValue?.value, pageDataValidator] + ); const initialFormValue: StaticPageSettings = useMemo( () => ({ url: currGetPathValue?.value ?? "" }), @@ -32,13 +38,14 @@ export default function StaticPageModal({ () => ({ url: { description: "URL Slug", - optional: isPathUndefined, - placeholder: isPathUndefined - ? "" - : "", + optional: !isURLEditable, + placeholder: isURLEditable + ? "" + : "", + disabled: !isURLEditable, }, }), - [isPathUndefined] + [isURLEditable] ); const handleModalSave = useCallback( @@ -47,10 +54,17 @@ export default function StaticPageModal({ kind: PropValueKind.Literal, value: form.url, }; + const validationResult = pageDataValidator.validate({ + url: getPathValue.value, + }); + if (!validationResult.valid) { + setErrorMessage(validationResult.errorMessages.join("\r\n")); + return false; + } updateGetPathValue(pageName, getPathValue); return true; }, - [updateGetPathValue, pageName] + [updateGetPathValue, pageName, pageDataValidator] ); return ( @@ -59,6 +73,7 @@ export default function StaticPageModal({ title="Page Settings" formData={staticFormData} initialFormValue={initialFormValue} + errorMessage={errorMessage} requireChangesToSubmit={true} handleClose={handleClose} handleConfirm={handleModalSave} diff --git a/packages/studio/src/components/common/FormModal.tsx b/packages/studio/src/components/common/FormModal.tsx index 78e2d273e..ca9cfd411 100644 --- a/packages/studio/src/components/common/FormModal.tsx +++ b/packages/studio/src/components/common/FormModal.tsx @@ -12,6 +12,7 @@ export type FormData = { optional?: boolean; placeholder?: string; tooltip?: string; + disabled?: boolean; }; }; @@ -149,6 +150,7 @@ function FormField({ description, placeholder, tooltip, + disabled, }: { field: string; value: string; @@ -157,6 +159,7 @@ function FormField({ description: string; placeholder?: string; tooltip?: string; + disabled?: boolean; }): JSX.Element { const handleChange = useCallback( (e: ChangeEvent) => { @@ -182,6 +185,7 @@ function FormField({ placeholder={placeholder} value={value} onChange={handleChange} + disabled={disabled} /> ); diff --git a/packages/studio/src/components/common/Modal.tsx b/packages/studio/src/components/common/Modal.tsx index 4bd3fa51e..3ba2e1478 100644 --- a/packages/studio/src/components/common/Modal.tsx +++ b/packages/studio/src/components/common/Modal.tsx @@ -71,7 +71,9 @@ export default function Modal({ {body}
{errorMessage && ( -
{errorMessage}
+
+ {errorMessage} +
)}