From 337ac57c5e232c1b58732693858f5eec4321553d Mon Sep 17 00:00:00 2001 From: Nar -- <28705606+finnar-bin@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:42:07 +0800 Subject: [PATCH] [Content | Schema] Block Selector (#3090) Resolves #3052 ### Preview [screen-recorder-fri-dec-06-2024-12-47-33.webm](https://github.com/user-attachments/assets/68c028ef-2671-477b-93a1-7a710110a9b6) --------- Co-authored-by: Andres --- cypress/e2e/content/content.spec.js | 21 ++ cypress/e2e/schema/field.spec.js | 37 ++- .../src/app/components/Editor/Editor.js | 7 + .../src/app/components/Editor/Field/Field.tsx | 13 + .../components/Editor/Field/FieldShell.tsx | 5 + .../src/app/components/Editor/FieldError.tsx | 4 + .../src/app/views/ItemCreate/ItemCreate.tsx | 17 +- .../src/app/views/ItemEdit/ItemEdit.js | 9 + .../AddFieldModal/views/FieldSelection.tsx | 38 ++- .../components/AddFieldModal/views/Rules.tsx | 2 +- .../src/app/components/Field/FieldIcon.tsx | 7 +- src/apps/schema/src/app/components/configs.ts | 19 +- src/apps/schema/src/app/utils/index.ts | 1 + .../FieldTypeBlockSelector/NoVariant.tsx | 55 ++++ .../VariantSelector.tsx | 207 +++++++++++++ .../FieldTypeBlockSelector/index.tsx | 290 ++++++++++++++++++ .../components/NoSearchResults/index.tsx | 4 +- src/shell/services/instance.ts | 1 + src/shell/store/content.js | 30 +- 19 files changed, 747 insertions(+), 20 deletions(-) create mode 100644 src/shell/components/FieldTypeBlockSelector/NoVariant.tsx create mode 100644 src/shell/components/FieldTypeBlockSelector/VariantSelector.tsx create mode 100644 src/shell/components/FieldTypeBlockSelector/index.tsx diff --git a/cypress/e2e/content/content.spec.js b/cypress/e2e/content/content.spec.js index 2decaabd56..c30c6032dc 100644 --- a/cypress/e2e/content/content.spec.js +++ b/cypress/e2e/content/content.spec.js @@ -411,4 +411,25 @@ describe("Content Specs", () => { .should("have.value", "12:00 pm"); }); }); + + describe("Block Selector Field", () => { + before(() => { + cy.waitOn("/v1/content/models*", () => { + cy.visit("/content/6-556370-8sh47g/7-b939a4-457q19"); + }); + }); + + it("Sets a block variant", () => { + cy.getBySelector("BlockSelectorModelField", { timeout: 10000 }) + .find("input") + .click(); + cy.get(".MuiAutocomplete-popper .MuiAutocomplete-option") + .contains("Test Block Do Not Delete") + .click(); + + cy.getBySelector("BlockSelectorVariantField", { timeout: 10000 }).click(); + cy.getBySelector("Variant_0").click(); + cy.getBySelector("BlockFieldVariantPreview").should("exist"); + }); + }); }); diff --git a/cypress/e2e/schema/field.spec.js b/cypress/e2e/schema/field.spec.js index b4a6cf4521..8cd78c3121 100644 --- a/cypress/e2e/schema/field.spec.js +++ b/cypress/e2e/schema/field.spec.js @@ -18,6 +18,7 @@ const SELECTORS = { FIELD_SELECT_BOOLEAN: "FieldItem_yes_no", FIELD_SELECT_ONE_TO_ONE: "FieldItem_one_to_one", FIELD_SELECT_CURRENCY: "FieldItem_currency", + FIELD_SELECT_BLOCK_SELECTOR: "FieldItem_block_selector", MEDIA_CHECKBOX_LIMIT: "MediaCheckbox_limit", MEDIA_CHECKBOX_LOCK: "MediaCheckbox_group_id", DROPDOWN_ADD_OPTION: "DropdownAddOption", @@ -322,7 +323,7 @@ describe("Schema: Fields", () => { // Select a related model cy.getBySelector(SELECTORS.AUTOCOMPLETE_MODEL_ZUID) .should("exist") - .type("cypress"); + .type("group with visible"); cy.get("[role=listbox] [role=option]").first().click(); cy.wait("@getFields"); @@ -397,6 +398,40 @@ describe("Schema: Fields", () => { cy.getBySelector(`Field_${fieldName}`).should("exist"); }); + it("Creates a Block field", () => { + cy.intercept("**/fields?showDeleted=true").as("getFields"); + + const fieldLabel = `Block Selector ${timestamp}`; + const fieldName = `block_selector_${timestamp}`; + + // Open the add field modal + cy.getBySelector(SELECTORS.ADD_FIELD_BTN).should("exist").click(); + cy.getBySelector(SELECTORS.ADD_FIELD_MODAL).should("exist"); + + // Select one-to-one relationship field + cy.getBySelector(SELECTORS.FIELD_SELECT_BLOCK_SELECTOR) + .should("exist") + .click(); + + // Fill up fields + cy.getBySelector(SELECTORS.INPUT_LABEL).should("exist").type(fieldLabel); + cy.get("input[name='label']") + .should("exist") + .should("have.value", fieldLabel); + cy.get("input[name='name']") + .should("exist") + .should("have.value", fieldName); + + // Click done + cy.getBySelector(SELECTORS.SAVE_FIELD_BUTTON).should("exist").click(); + cy.getBySelector(SELECTORS.ADD_FIELD_MODAL).should("not.exist"); + + cy.wait("@getFields"); + + // Check if field exists + cy.getBySelector(`Field_${fieldName}`).should("exist"); + }); + it("Creates a field via add another field button", () => { cy.intercept("**/fields?showDeleted=true").as("getFields"); diff --git a/src/apps/content-editor/src/app/components/Editor/Editor.js b/src/apps/content-editor/src/app/components/Editor/Editor.js index 32a15ea7c4..e172e1fbb3 100644 --- a/src/apps/content-editor/src/app/components/Editor/Editor.js +++ b/src/apps/content-editor/src/app/components/Editor/Editor.js @@ -202,6 +202,13 @@ export default memo(function Editor({ } } + if (field.datatype === "block_selector") { + errors[name] = { + ...(errors[name] ?? []), + INVALID_BLOCK_VARIANT: false, + }; + } + onUpdateFieldErrors(errors); // Always dispatch the data update diff --git a/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx b/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx index b99289a636..108535b753 100644 --- a/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx +++ b/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx @@ -53,6 +53,7 @@ import { FieldTypeDate } from "../../../../../../../shell/components/FieldTypeDa import { FieldTypeDateTime } from "../../../../../../../shell/components/FieldTypeDateTime"; import { FieldTypeSort } from "../../../../../../../shell/components/FieldTypeSort"; import { FieldTypeNumber } from "../../../../../../../shell/components/FieldTypeNumber"; +import { FieldTypeBlockSelector } from "../../../../../../../shell/components/FieldTypeBlockSelector"; import styles from "./Field.less"; import { MemoryRouter } from "react-router"; @@ -961,6 +962,18 @@ export const Field = ({ ); + case "block_selector": + return ( + + onChange(value, name, datatype)} + requiredError={errors?.MISSING_REQUIRED} + missingVariantError={errors?.INVALID_BLOCK_VARIANT} + /> + + ); + default: return ( diff --git a/src/apps/content-editor/src/app/components/Editor/Field/FieldShell.tsx b/src/apps/content-editor/src/app/components/Editor/Field/FieldShell.tsx index 7e1032e3e4..4d329c3fce 100644 --- a/src/apps/content-editor/src/app/components/Editor/Field/FieldShell.tsx +++ b/src/apps/content-editor/src/app/components/Editor/Field/FieldShell.tsx @@ -37,6 +37,7 @@ export type Error = { REGEX_PATTERN_MISMATCH?: string; REGEX_RESTRICT_PATTERN_MATCH?: string; INVALID_RANGE?: string; + INVALID_BLOCK_VARIANT?: boolean; }; type FieldShellProps = { @@ -112,6 +113,10 @@ export const FieldShell = ({ errorMessages.push(errors.CUSTOM_ERROR); } + if (errors?.INVALID_BLOCK_VARIANT) { + errorMessages.push("Please select a block variant."); + } + if (errorMessages.length === 0) { return ""; } diff --git a/src/apps/content-editor/src/app/components/Editor/FieldError.tsx b/src/apps/content-editor/src/app/components/Editor/FieldError.tsx index dfb8a3b700..456af7c996 100644 --- a/src/apps/content-editor/src/app/components/Editor/FieldError.tsx +++ b/src/apps/content-editor/src/app/components/Editor/FieldError.tsx @@ -67,6 +67,10 @@ const getErrorMessage = (errors: Error) => { errorMessages.push(errors.CUSTOM_ERROR); } + if (errors?.INVALID_BLOCK_VARIANT) { + errorMessages.push("Please select a block variant."); + } + return errorMessages; }; diff --git a/src/apps/content-editor/src/app/views/ItemCreate/ItemCreate.tsx b/src/apps/content-editor/src/app/views/ItemCreate/ItemCreate.tsx index 73c5994f16..ff5819d989 100644 --- a/src/apps/content-editor/src/app/views/ItemCreate/ItemCreate.tsx +++ b/src/apps/content-editor/src/app/views/ItemCreate/ItemCreate.tsx @@ -201,7 +201,11 @@ export const ItemCreate = () => { }) ); if (res.err || res.error) { - if (res.missingRequired || res.lackingCharLength) { + if ( + res.missingRequired || + res.lackingCharLength || + res.invalidBlockVariantValue + ) { const missingRequiredFieldNames: string[] = res.missingRequired?.reduce( (acc: string[], curr: ContentModelField) => { @@ -269,6 +273,17 @@ export const ItemCreate = () => { }); } + if (res.invalidBlockVariantValue?.length) { + res.invalidBlockVariantValue?.forEach( + (field: ContentModelField) => { + errors[field.name] = { + ...(errors[field.name] ?? {}), + INVALID_BLOCK_VARIANT: true, + }; + } + ); + } + setFieldErrors(errors); // scroll to required field diff --git a/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js b/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js index e30cc89af7..3be92ddd25 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js +++ b/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js @@ -373,6 +373,15 @@ export default function ItemEdit() { }); } + if (res.invalidBlockVariantValue?.length) { + res.invalidBlockVariantValue?.forEach((field) => { + errors[field.name] = { + ...(errors[field.name] ?? {}), + INVALID_BLOCK_VARIANT: true, + }; + }); + } + setFieldErrors(errors); throw new Error(errors); } diff --git a/src/apps/schema/src/app/components/AddFieldModal/views/FieldSelection.tsx b/src/apps/schema/src/app/components/AddFieldModal/views/FieldSelection.tsx index 175c15a0cd..c86619cd02 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/views/FieldSelection.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/views/FieldSelection.tsx @@ -11,15 +11,20 @@ import { } from "@mui/material"; import SearchIcon from "@mui/icons-material/Search"; import CloseIcon from "@mui/icons-material/Close"; +import { useSelector } from "react-redux"; import { FieldItem } from "../FieldItem"; import { FieldListData, FIELD_COPY_CONFIG } from "../../configs"; +import { isZestyEmail } from "../../../../../../../utility/isZestyEmail"; +import { AppState } from "../../../../../../../shell/store/types"; +import { User } from "../../../../../../../shell/services/types"; interface Props { onFieldClick: (fieldType: string, fieldName: string) => void; onModalClose: () => void; } export const FieldSelection = ({ onFieldClick, onModalClose }: Props) => { + const user: User = useSelector((state: AppState) => state.user); const [fieldTypes, setFieldTypes] = useState(FIELD_COPY_CONFIG); const handleFilterFields = (e: React.ChangeEvent) => { @@ -140,18 +145,27 @@ export const FieldSelection = ({ onFieldClick, onModalClose }: Props) => { rowGap={1.5} columnGap={2} > - {fieldTypes[fieldKey].map((field: FieldListData, index) => ( - onFieldClick(field.type, field.name)} - /> - ))} + {fieldTypes[fieldKey].map((field: FieldListData, index) => { + if ( + !isZestyEmail(user.email) && + field?.type === "block_selector" + ) { + return <>; + } + + return ( + onFieldClick(field.type, field.name)} + /> + ); + })} ))} diff --git a/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx b/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx index a92b5caf27..7a4c95a7b1 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx @@ -42,7 +42,7 @@ export const Rules = ({ formData?.minCharLimit !== null && formData?.maxCharLimit !== null ); - if (type === "uuid") { + if (type === "uuid" || type === "block_selector") { return ; } diff --git a/src/apps/schema/src/app/components/Field/FieldIcon.tsx b/src/apps/schema/src/app/components/Field/FieldIcon.tsx index 8567f38449..94c40879f5 100644 --- a/src/apps/schema/src/app/components/Field/FieldIcon.tsx +++ b/src/apps/schema/src/app/components/Field/FieldIcon.tsx @@ -15,7 +15,7 @@ import ToggleOnRounded from "@mui/icons-material/ToggleOnRounded"; import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; import ColorLensRounded from "@mui/icons-material/ColorLensRounded"; import FormatListNumberedRounded from "@mui/icons-material/FormatListNumberedRounded"; -import { Markdown, OneToOne } from "@zesty-io/material"; +import { Markdown, OneToOne, Block } from "@zesty-io/material"; import { Box } from "@mui/system"; import { SvgIcon } from "@mui/material"; @@ -67,6 +67,11 @@ const icons: Icons = { backgroundColor: "pink.50", borderColor: "pink.600", }, + block_selector: { + icon: Block as SvgIconComponent, + backgroundColor: "pink.50", + borderColor: "pink.600", + }, markdown: { icon: Markdown as SvgIconComponent, backgroundColor: "green.50", diff --git a/src/apps/schema/src/app/components/configs.ts b/src/apps/schema/src/app/components/configs.ts index ec94b50998..359f9989a0 100644 --- a/src/apps/schema/src/app/components/configs.ts +++ b/src/apps/schema/src/app/components/configs.ts @@ -23,7 +23,8 @@ export type FieldType = | "files" | "fontawesome" | "wysiwyg_advanced" - | "article_writer"; + | "article_writer" + | "block_selector"; // TODO: Will need to confirm if this type is already supported by the api interface FieldListData { type: FieldType; name: string; @@ -185,6 +186,17 @@ const FIELD_COPY_CONFIG: { [key: string]: FieldListData[] } = { "You can use External URL fields if you want to link to external websites.", subHeaderText: "Use this field to link to an internal content item", }, + { + type: "block_selector", + name: "Block Selector", + shortDescription: "Link to a variant of a block model", + description: + "The Block Selector field allows a user to select a unique variant of any block model they would like to see rendered on their page.", + commonUses: ["Footer Section", "Block at the end of article", "Forms"], + proTip: + "These are great to use when you want to use different end blocks at the end of different pages of the same model", + subHeaderText: "Link to a variant of a block model", + }, ], numeric: [ { @@ -351,6 +363,7 @@ const TYPE_TEXT: Record = { wysiwyg_advanced: "WYSIWYG (Advanced)", wysiwyg_basic: "WYSIWYG", yes_no: "Boolean", + block_selector: "Block Selector", }; const COMMON_FIELDS: InputField[] = [ @@ -700,6 +713,10 @@ const FORM_CONFIG: Record = { ], rules: [...COMMON_RULES], }, + block_selector: { + details: [...COMMON_FIELDS], + rules: [], + }, }; const SYSTEM_FIELDS: readonly SystemField[] = [ diff --git a/src/apps/schema/src/app/utils/index.ts b/src/apps/schema/src/app/utils/index.ts index 496eda7f4c..7cbfdefca6 100644 --- a/src/apps/schema/src/app/utils/index.ts +++ b/src/apps/schema/src/app/utils/index.ts @@ -139,6 +139,7 @@ export const getCategory = (type: string) => { case "one_to_many": case "link": case "internal_link": + case "block_selector": category = "relationship"; break; diff --git a/src/shell/components/FieldTypeBlockSelector/NoVariant.tsx b/src/shell/components/FieldTypeBlockSelector/NoVariant.tsx new file mode 100644 index 0000000000..b75c63e7ee --- /dev/null +++ b/src/shell/components/FieldTypeBlockSelector/NoVariant.tsx @@ -0,0 +1,55 @@ +import { useMemo } from "react"; +import { Stack, Typography, Button, Box } from "@mui/material"; +import { Block } from "@zesty-io/material"; +import { AddRounded } from "@mui/icons-material"; + +import { useHistory } from "react-router-dom"; + +type NoVariantProps = { + blockModelZUID: string; + blockModelName: string; +}; +export const NoVariant = ({ + blockModelZUID, + blockModelName, +}: NoVariantProps) => { + const history = useHistory(); + + return ( + + + + No variants have been created for the {blockModelName} Model + + + To create a variant, please go to the Blocks App and select this model + and click on the create variant button + + + + + + + + ); +}; diff --git a/src/shell/components/FieldTypeBlockSelector/VariantSelector.tsx b/src/shell/components/FieldTypeBlockSelector/VariantSelector.tsx new file mode 100644 index 0000000000..c7b5ab877f --- /dev/null +++ b/src/shell/components/FieldTypeBlockSelector/VariantSelector.tsx @@ -0,0 +1,207 @@ +import { useState, useMemo, useRef } from "react"; +import { + MenuList, + TextField, + MenuItem, + Typography, + Popover, + Box, + Stack, + ListSubheader, + Tooltip, +} from "@mui/material"; +import { Search } from "@mui/icons-material"; +import moment from "moment"; + +import { ContentItem } from "../../services/types"; +import { useGetUsersQuery } from "../../services/accounts"; +import { NoSearchResults } from "../NoSearchResults"; +import { NoVariant } from "./NoVariant"; +import blockPlaceholder from "../../../../public/images/blockPlaceholder.png"; + +type VariantSelectorProps = { + anchorEl: Element; + onClose: () => void; + variants: ContentItem[]; + blockModelZUID: string; + blockModelName: string; + onVariantSelected: (ZUID: string) => void; +}; +export const VariantSelector = ({ + anchorEl, + onClose, + variants, + blockModelZUID, + blockModelName, + onVariantSelected, +}: VariantSelectorProps) => { + const { data: users } = useGetUsersQuery(); + const [filterKeyword, setFilterKeyword] = useState(""); + const filterTextField = useRef(null); + + const filteredVariants = useMemo(() => { + if (!filterKeyword) return variants; + + return variants?.filter((variant) => + variant?.web?.metaTitle + ?.toLowerCase() + ?.includes(filterKeyword?.toLowerCase()?.trim()) + ); + }, [variants, filterKeyword]); + + const getUserName = (ZUID: string) => { + const user = users?.find((user) => user.ZUID === ZUID); + + if (!!user) { + return `${user.firstName} ${user.lastName}`; + } + + return ""; + }; + + return ( + + + + setFilterKeyword(evt.currentTarget.value)} + InputProps={{ + startAdornment: , + }} + onKeyDown={(e: React.KeyboardEvent) => { + const allowedKeys = ["ArrowUp", "ArrowDown", "Escape"]; + + if (!allowedKeys.includes(e.key)) { + e.stopPropagation(); + } + }} + /> + + {!variants?.length ? ( + + ) : filteredVariants?.length ? ( + filteredVariants?.map((variant, index) => ( + onVariantSelected(variant?.meta?.ZUID)} + sx={{ + display: "flex", + px: 2, + py: 1.75, + gap: 1.5, + borderColor: "border", + }} + > + + } + components={{ Tooltip: Box }} + slotProps={{ + popper: { + sx: { + maxWidth: "none", + }, + }, + tooltip: { + sx: { + mr: 1, + }, + }, + }} + > + + + + + {variant?.web?.metaTitle} + + + Updated on {moment(variant.web?.updatedAt).format("MMMM D")}{" "} + by {getUserName(variant?.web?.createdByUserZUID)} + + + + )) + ) : ( + + { + setFilterKeyword(""); + filterTextField.current?.querySelector("input").focus(); + }} + /> + + )} + + + ); +}; diff --git a/src/shell/components/FieldTypeBlockSelector/index.tsx b/src/shell/components/FieldTypeBlockSelector/index.tsx new file mode 100644 index 0000000000..57f62cbcdc --- /dev/null +++ b/src/shell/components/FieldTypeBlockSelector/index.tsx @@ -0,0 +1,290 @@ +import { useEffect, useMemo, useState, useRef, useReducer } from "react"; +import { + Typography, + Autocomplete, + TextField, + Stack, + Box, + IconButton, +} from "@mui/material"; +import { + KeyboardArrowDownRounded, + ModeEditRounded, + LinkRounded, + OpenInNewRounded, + CheckRounded, +} from "@mui/icons-material"; +import { useHistory } from "react-router"; +import { useSelector } from "react-redux"; + +import { + useGetContentModelsQuery, + useLazyGetContentModelItemsQuery, +} from "../../services/instance"; +import { VariantSelector } from "./VariantSelector"; +import { AppState } from "../../store/types"; +import blockPlaceholder from "../../../../public/images/blockPlaceholder.png"; + +type BlockValue = { + model: { + label: string; + value: string; + } | null; + variant: string; +}; +type FieldTypeBlockSelectorProps = { + value: string; + onChange: (value: string) => void; + requiredError: boolean; + missingVariantError: boolean; +}; +export const FieldTypeBlockSelector = ({ + value, + onChange, + requiredError, + missingVariantError, +}: FieldTypeBlockSelectorProps) => { + const history = useHistory(); + const instance = useSelector((state: AppState) => state.instance); + const previewLock = useSelector((state: AppState) => + state.settings?.instance?.find( + (setting: any) => setting.key === "preview_lock_password" && setting.value + ) + ); + const { data: models, isLoading: isLoadingModels } = + useGetContentModelsQuery(); + const [getContentModelItems, { data: variants }] = + useLazyGetContentModelItemsQuery(); + const [isVariantSelectorOpen, setIsVariantSelectorOpen] = useState(false); + const [isLinkCopied, setIsLinkCopied] = useState(false); + const variantSelectorRef = useRef(null); + + const [blockValue, updateBlockValue] = useReducer( + (state: BlockValue, action: Partial) => { + return { + ...state, + ...action, + }; + }, + { model: null, variant: null } + ); + const blockModelData = models?.find( + (model) => model.name === blockValue.model?.value + ); + const selectedVariantData = variants?.find( + (variant) => variant.meta?.ZUID === blockValue.variant + ); + + const blockModelOptions = useMemo(() => { + if (!models?.length) return []; + + return models + .filter((model) => model.type === "block") + .map((model) => ({ + label: model.label, + value: model.name, + })); + }, [models]); + + const url = useMemo(() => { + if (!blockValue || !variants?.length || !instance || !selectedVariantData) + return ""; + + // @ts-expect-error config not typed + const domain = `${CONFIG.URL_PREVIEW_PROTOCOL}${instance?.randomHashID}${CONFIG.URL_PREVIEW}`; + let path = `/-/block/${blockValue.model?.value}.html?variant=${selectedVariantData?.meta?.ZUID}&_bypassError=true`; + + if (previewLock) { + path = `${path}&zpw=${previewLock.value}`; + } + + return `${domain}${path}`; + }, [blockValue, variants, instance, selectedVariantData]); + + useEffect(() => { + if (!blockModelData) return; + + getContentModelItems({ modelZUID: blockModelData.ZUID }); + }, [blockValue.model, blockModelData]); + + useEffect(() => { + if (!value) { + updateBlockValue({ + model: null, + variant: null, + }); + } else { + const blockModelName = value.split("/")?.[3]?.split(".")?.[0]; + const blockVariantZUID = value.split("variant=")?.[1]; + + updateBlockValue({ + model: { + label: + models?.find((model) => model.name === blockModelName)?.label || "", + value: blockModelName || "", + }, + variant: blockVariantZUID, + }); + } + }, [value, models]); + + const handleCopyLinkClick = (data: string) => { + navigator?.clipboard + ?.writeText(data) + .then(() => { + setIsLinkCopied(true); + setTimeout(() => { + setIsLinkCopied(false); + }, 3000); + }) + .catch((err) => { + console.error(err); + }); + }; + + return ( + <> + + ( + + )} + options={blockModelOptions} + value={blockValue?.model} + onChange={(_, value) => { + if (!value) { + onChange(null); + } else { + onChange(`/-/block/${value?.value}.html?variant=`); + } + }} + /> + + { + if (!blockValue?.model || !blockValue?.model?.value) return; + + setIsVariantSelectorOpen(true); + }} + > + + {!!blockValue?.variant + ? variants?.find( + (variant) => variant?.meta?.ZUID === blockValue.variant + )?.web?.metaTitle + : "Variant"} + + + + {!!isVariantSelectorOpen && ( + setIsVariantSelectorOpen(false)} + variants={blockValue?.model?.value ? variants : []} + blockModelName={blockValue?.model?.label} + blockModelZUID={blockModelData?.ZUID} + onVariantSelected={(ZUID) => { + onChange( + `/-/block/${blockValue?.model?.value}.html?variant=${ZUID}` + ); + setIsVariantSelectorOpen(false); + }} + /> + )} + + {!!blockValue?.model && !!blockValue?.variant && ( + + + + {variants?.find( + (variant) => variant?.meta?.ZUID === blockValue.variant + )?.web?.metaTitle || ""} + + + + history.push( + `/blocks/${blockModelData?.ZUID}/${blockValue?.variant}` + ) + } + > + + + handleCopyLinkClick(url)}> + {isLinkCopied ? ( + + ) : ( + + )} + + + window.open(url, "_blank", "noopener=true,noreferrer=true") + } + > + + + + + + + )} + + ); +}; diff --git a/src/shell/components/NoSearchResults/index.tsx b/src/shell/components/NoSearchResults/index.tsx index 1db992ae1e..3ef5ca94f0 100644 --- a/src/shell/components/NoSearchResults/index.tsx +++ b/src/shell/components/NoSearchResults/index.tsx @@ -17,6 +17,7 @@ type Props = { ignoreFilters?: boolean; hideBackButton?: boolean; onSearchAgain?: () => void; + imageHeight?: number; }; export const NoSearchResults: FC = ({ @@ -24,6 +25,7 @@ export const NoSearchResults: FC = ({ onSearchAgain, ignoreFilters, hideBackButton, + imageHeight = 200, }) => { const history = useHistory(); const [params, setParams] = useParams(); @@ -55,7 +57,7 @@ export const NoSearchResults: FC = ({ className="NoResultsState" > - + field.settings?.maxValue) ); + // Validates that block_selector fields contain the correct format 6-xxxx-xxxx.html?variant=7-xxxx-xxxx + const invalidBlockVariantValue = fields?.filter((field) => { + if (field.datatype === "block_selector" && !!item.data[field.name]) { + if (!item.data[field.name]?.split("variant=")?.[1]) return true; + + return false; + } + + return false; + }); + // When skipContentItemValidation is true, this means that only the // SEO meta tags were changed, so we skip validating the content item if ( @@ -458,7 +469,8 @@ export function saveItem({ lackingCharLength?.length || regexPatternMismatch?.length || regexRestrictPatternMatch?.length || - invalidRange?.length) + invalidRange?.length || + invalidBlockVariantValue?.length) ) { return Promise.resolve({ err: "VALIDATION_ERROR", @@ -469,6 +481,7 @@ export function saveItem({ regexRestrictPatternMatch, }), ...(!!invalidRange?.length && { invalidRange }), + ...(!!invalidBlockVariantValue && { invalidBlockVariantValue }), }); } @@ -637,13 +650,25 @@ export function createItem({ modelZUID, itemZUID, skipPathPartValidation }) { item.data[field.name] > field.settings?.maxValue) ); + // Validates that block_selector fields contain the correct format 6-xxxx-xxxx.html?variant=7-xxxx-xxxx + const invalidBlockVariantValue = fields?.filter((field) => { + if (field.datatype === "block_selector" && !!item.data[field.name]) { + if (!item.data[field.name]?.split("variant=")?.[1]) return true; + + return false; + } + + return false; + }); + if ( missingRequired?.length || lackingCharLength?.length || regexPatternMismatch?.length || regexRestrictPatternMatch?.length || invalidRange?.length || - hasMissingRequiredSEOFields + hasMissingRequiredSEOFields || + invalidBlockVariantValue?.length ) { return Promise.resolve({ err: "VALIDATION_ERROR", @@ -654,6 +679,7 @@ export function createItem({ modelZUID, itemZUID, skipPathPartValidation }) { regexRestrictPatternMatch, }), ...(!!invalidRange?.length && { invalidRange }), + ...(!!invalidBlockVariantValue && { invalidBlockVariantValue }), }); }