From eb70ca2a9b0544578c8e6c5f18d04e9e904abb79 Mon Sep 17 00:00:00 2001 From: Nar -- <28705606+finnar-bin@users.noreply.github.com> Date: Mon, 3 Feb 2025 08:56:52 +0800 Subject: [PATCH 01/10] [Schema] Add visual highlight to currently active sorting order (#3180) ![image](https://github.com/user-attachments/assets/add15837-bf2a-4bee-abbf-1995b8f85e15) Highlights the current active sorting mode in the sidebar (Closes #2815) --- src/apps/schema/src/app/components/Sidebar/ModelList.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apps/schema/src/app/components/Sidebar/ModelList.tsx b/src/apps/schema/src/app/components/Sidebar/ModelList.tsx index f93e68ff6..ee89e2f5b 100644 --- a/src/apps/schema/src/app/components/Sidebar/ModelList.tsx +++ b/src/apps/schema/src/app/components/Sidebar/ModelList.tsx @@ -135,6 +135,7 @@ export const ModelList = ({ title, models, type, app = "schema" }: Props) => { { handleClose(); setSort("asc"); @@ -143,6 +144,7 @@ export const ModelList = ({ title, models, type, app = "schema" }: Props) => { Name (A to Z) { handleClose(); setSort("desc"); @@ -151,6 +153,7 @@ export const ModelList = ({ title, models, type, app = "schema" }: Props) => { Name (Z to A) { handleClose(); setSort(""); @@ -159,6 +162,7 @@ export const ModelList = ({ title, models, type, app = "schema" }: Props) => { Last Created { handleClose(); setSort("modified"); From f01caf2def28ad74607497ad700086fbf3b98766 Mon Sep 17 00:00:00 2001 From: Nar -- <28705606+finnar-bin@users.noreply.github.com> Date: Thu, 6 Feb 2025 07:06:42 +0800 Subject: [PATCH 02/10] [Content] One-to-one/many Revamp Phase 1 (#3184) Resolves #2936 Enhances the UI for both the one-to-one and one-to-many fields --- cypress/e2e/content/content.spec.js | 107 +++ .../src/app/components/Editor/Editor.js | 12 + .../src/app/components/Editor/Field/Field.tsx | 134 ++-- .../ItemEditHeader/ItemEditHeaderActions.tsx | 195 ++++- .../ItemEditHeader/UnpublishedRelatedItem.tsx | 166 +++++ .../components}/ConfirmPublishModal.tsx | 45 +- src/shell/components/Filters/FilterButton.tsx | 18 +- .../ActiveItem/ActiveItemLoading.tsx | 80 ++ .../RelationalFieldBase/ActiveItem/index.tsx | 498 +++++++++++++ .../FieldSelectorDialog/DialogHeader.tsx | 91 +++ .../FieldSelectorFilters.tsx | 486 +++++++++++++ .../FieldSelectorDialog/ImageCell.tsx | 68 ++ .../FieldSelectorDialog/ItemsLoading.tsx | 64 ++ .../FieldSelectorDialog/TitleCell.tsx | 35 + .../FieldSelectorDialog/VersionCell.tsx | 45 ++ .../FieldSelectorDialog/index.tsx | 684 ++++++++++++++++++ .../RelationalFieldBase/VersionChip.tsx | 77 ++ .../components/RelationalFieldBase/index.tsx | 166 +++++ .../components/SchedulePublish/index.tsx | 1 + 19 files changed, 2880 insertions(+), 92 deletions(-) create mode 100644 src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx rename src/{apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader => shell/components}/ConfirmPublishModal.tsx (71%) create mode 100644 src/shell/components/RelationalFieldBase/ActiveItem/ActiveItemLoading.tsx create mode 100644 src/shell/components/RelationalFieldBase/ActiveItem/index.tsx create mode 100644 src/shell/components/RelationalFieldBase/FieldSelectorDialog/DialogHeader.tsx create mode 100644 src/shell/components/RelationalFieldBase/FieldSelectorDialog/FieldSelectorFilters.tsx create mode 100644 src/shell/components/RelationalFieldBase/FieldSelectorDialog/ImageCell.tsx create mode 100644 src/shell/components/RelationalFieldBase/FieldSelectorDialog/ItemsLoading.tsx create mode 100644 src/shell/components/RelationalFieldBase/FieldSelectorDialog/TitleCell.tsx create mode 100644 src/shell/components/RelationalFieldBase/FieldSelectorDialog/VersionCell.tsx create mode 100644 src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx create mode 100644 src/shell/components/RelationalFieldBase/VersionChip.tsx create mode 100644 src/shell/components/RelationalFieldBase/index.tsx diff --git a/cypress/e2e/content/content.spec.js b/cypress/e2e/content/content.spec.js index 27bf6ca53..84a31e224 100644 --- a/cypress/e2e/content/content.spec.js +++ b/cypress/e2e/content/content.spec.js @@ -430,4 +430,111 @@ describe("Content Specs", () => { cy.getBySelector("BlockFieldVariantPreview").should("exist"); }); }); + + context("One to one field", () => { + before(() => { + cy.waitOn("/v1/content/models*", () => { + cy.visit("/content/6-556370-8sh47g/7-b939a4-457q19"); + }); + + cy.intercept({ method: "GET", url: "**/items*" }).as("fetchItems"); + cy.intercept({ method: "GET", url: "**/models*" }).as("fetchModels"); + cy.intercept({ method: "GET", url: "**/fields*" }).as("fetchFields"); + + cy.wait("@fetchFields"); + cy.getBySelector("DuoModeToggle").click(); + }); + + it("can only select/add one item", () => { + cy.get("#12-edee00-6zb866 [data-cy='add-relational-item-button']").click({ + force: true, + }); + + cy.wait("@fetchItems"); + + cy.get(".MuiDataGrid-row").first().find("input").click(); + cy.get(".MuiDataGrid-row").eq(1).find("input").click(); + + cy.getBySelector("selected-count").contains("1 / 1 selected"); + cy.getBySelector("done-selecting-item-button").click(); + cy.get("#12-edee00-6zb866 [data-cy='active-relational-item']").should( + "have.length", + 1 + ); + }); + + it("can publish an item", () => { + cy.get( + "#12-edee00-6zb866 [data-cy='active-relational-item-more-button']" + ).click(); + cy.getBySelector("active-relational-item-publish-now-button").click(); + cy.getBySelector("ConfirmPublishModal").should("exist"); + cy.getBySelector("CancelPublishButton").click(); + }); + + it("can schedule publish an item", () => { + cy.get( + "#12-edee00-6zb866 [data-cy='active-relational-item-more-button']" + ).click(); + cy.getBySelector( + "active-relational-item-schedule-publish-button" + ).click(); + cy.getBySelector("SchedulePublishModal").should("exist"); + cy.getBySelector("CancelSchedulePublishButton").click(); + }); + + it("can remove the selected item", () => { + cy.get( + "#12-edee00-6zb866 [data-cy='active-relational-item-more-button']" + ).click(); + cy.getBySelector("active-relational-item-remove-item-button").click(); + cy.get("#12-edee00-6zb866 [data-cy='active-relational-item']").should( + "not.exist" + ); + }); + }); + + context("One to many field", () => { + before(() => { + cy.waitOn("/v1/content/models*", () => { + cy.visit("/content/6-556370-8sh47g/7-b939a4-457q19"); + }); + + cy.intercept({ method: "GET", url: "**/items*" }).as("fetchItems"); + cy.intercept({ method: "GET", url: "**/models*" }).as("fetchModels"); + cy.intercept({ method: "GET", url: "**/fields*" }).as("fetchFields"); + + cy.wait("@fetchFields"); + cy.getBySelector("DuoModeToggle").click(); + }); + + it("can add multiple items", () => { + cy.get("#12-269a28-1bkm34 [data-cy='add-relational-item-button']").click({ + force: true, + }); + + cy.wait("@fetchItems"); + + [...Array(3)].forEach((_, i) => { + cy.get(".MuiDataGrid-row").eq(i).find("input").click(); + }); + cy.getBySelector("selected-count").contains("3 selected"); + cy.getBySelector("done-selecting-item-button").click(); + cy.get("#12-269a28-1bkm34 [data-cy='active-relational-item']").should( + "have.length", + 3 + ); + }); + + it("can remove the selected item", () => { + cy.get("#12-269a28-1bkm34 [data-cy='active-relational-item-more-button']") + .first() + .click(); + cy.getBySelector("active-relational-item-remove-item-button").click(); + cy.get("#12-269a28-1bkm34 [data-cy='active-relational-item']").should( + "have.length", + 2 + ); + }); + }); }); 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 e172e1fbb..02059a80b 100644 --- a/src/apps/content-editor/src/app/components/Editor/Editor.js +++ b/src/apps/content-editor/src/app/components/Editor/Editor.js @@ -209,6 +209,18 @@ export default memo(function Editor({ }; } + if (field.datatype === "one_to_many") { + // Value is stored as string in DB with max char limit of 255. + // This means users can only add up to 12 item zuids + errors[name] = { + ...(errors[name] ?? []), + CUSTOM_ERROR: + !!value && value?.length > 255 + ? "Cannot save field. Please reduce the total number of items selected." + : "", + }; + } + 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 ae41c1aa8..5d1bfd130 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 @@ -50,6 +50,7 @@ import { FieldTypeOneToOne, OneToOneOptions, } from "../../../../../../../shell/components/FieldTypeOneToOne"; +import { RelationalFieldBase } from "../../../../../../../shell/components/RelationalFieldBase"; import { FieldTypeDate } from "../../../../../../../shell/components/FieldTypeDate"; import { FieldTypeDateTime } from "../../../../../../../shell/components/FieldTypeDateTime"; import { FieldTypeSort } from "../../../../../../../shell/components/FieldTypeSort"; @@ -93,7 +94,7 @@ export const resolveRelatedOptions = ( modelZUID: string, langID: number, value: any -): OneToManyOptions[] | OneToOneOptions[] => { +): OneToManyOptions[] => { // guard against absent data in state const field = fields && fields[fieldZUID]; if (!field || !items) { @@ -781,27 +782,38 @@ export const Field = ({ return ( - options.value === value) || - null - } - onChange={(_, option) => onChange(option.value, name)} - options={oneToOneOptions} - onOpen={onOneToOneOpen} - startAdornment={ - value && ( - - - - ) - } - endAdornment={ - value && {getSelectedLang(allLanguages, langID)} - } - error={errors && Object.values(errors)?.some((error) => !!error)} - /> + <> + + {/** + options.value === value) || + null + } + onChange={(_, option) => onChange(option.value, name)} + options={oneToOneOptions} + onOpen={onOneToOneOpen} + startAdornment={ + value && ( + + + + ) + } + endAdornment={ + value && {getSelectedLang(allLanguages, langID)} + } + error={errors && Object.values(errors)?.some((error) => !!error)} + /> + */} + ); @@ -840,39 +852,51 @@ export const Field = ({ return ( - - oneToManyOptions?.find( - (options) => options.value === value - ) || { value, inputLabel: value, component: value } - )) || - [] - } - onChange={(_, options: OneToManyOptions[]) => { - const selectedOptions = options?.length - ? options.map((option) => option.value).join(",") - : null; - onChange(selectedOptions, name); - }} - options={oneToManyOptions} - onOpen={onOneToManyOpen} - renderTags={(tags, getTagProps) => - tags.map((tag, index) => ( - - )) - } - error={errors && Object.values(errors)?.some((error) => !!error)} - /> + <> + + {/** + + oneToManyOptions?.find( + (options) => options.value === value + ) || { value, inputLabel: value, component: value } + )) || + [] + } + onChange={(_, options: OneToManyOptions[]) => { + const selectedOptions = options?.length + ? options.map((option) => option.value).join(",") + : null; + onChange(selectedOptions, name); + }} + options={oneToManyOptions} + onOpen={onOneToManyOpen} + renderTags={(tags, getTagProps) => + tags.map((tag, index) => ( + + )) + } + error={errors && Object.values(errors)?.some((error) => !!error)} + /> + */} + ); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx index 889814adc..d7a05a1b9 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx +++ b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx @@ -8,15 +8,19 @@ import { Menu, MenuItem, ListItemIcon, + Stack, + List, } from "@mui/material"; import { useCreateItemPublishingMutation, + useCreateItemsPublishingMutation, useDeleteItemPublishingMutation, useGetAuditsQuery, + useGetContentModelFieldsQuery, useGetItemPublishingsQuery, } from "../../../../../../../../shell/services/instance"; import { useHistory, useParams } from "react-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { SaveRounded, @@ -31,18 +35,24 @@ import { import { useDispatch, useSelector } from "react-redux"; import { AppState } from "../../../../../../../../shell/store/types"; import { useMetaKey } from "../../../../../../../../shell/hooks/useMetaKey"; -import { fetchItemPublishing } from "../../../../../../../../shell/store/content"; +import { + fetchItemPublishing, + fetchItemPublishings, +} from "../../../../../../../../shell/store/content"; import { LoadingButton } from "@mui/lab"; import { useGetUsersQuery } from "../../../../../../../../shell/services/accounts"; import { formatDate } from "../../../../../../../../utility/formatDate"; import { UnpublishDialog } from "./UnpublishDialog"; import { usePermission } from "../../../../../../../../shell/hooks/use-permissions"; import { + ContentItem, ContentItemWithDirtyAndPublishing, ContentModel, } from "../../../../../../../../shell/services/types"; -import { ConfirmPublishModal } from "./ConfirmPublishModal"; import { SchedulePublish } from "../../../../../../../../shell/components/SchedulePublish"; +import { ConfirmPublishModal } from "../../../../../../../../shell/components/ConfirmPublishModal"; +import { UnpublishedRelatedItem } from "./UnpublishedRelatedItem"; +import { uniqBy } from "lodash"; const ITEM_STATES = { dirty: "dirty", @@ -78,19 +88,26 @@ export const ItemEditHeaderActions = ({ const [publishAfterUnschedule, setPublishAfterUnschedule] = useState(false); const [isConfirmPublishModalOpen, setIsConfirmPublishModalOpen] = useState(false); + const [relatedItemsToPublish, setRelatedItemsToPublish] = useState< + ContentItem[] + >([]); + const [isPublishing, setIsPublishing] = useState(false); const item = useSelector( (state: AppState) => state.content[itemZUID] as ContentItemWithDirtyAndPublishing ); + const items = useSelector((state: AppState) => state.content); const model = useSelector( (state: AppState) => state.models[modelZUID] ) as ContentModel; + const { data: fields } = useGetContentModelFieldsQuery(modelZUID, { + skip: !modelZUID, + }); const { data: users } = useGetUsersQuery(); const { data: itemAudit } = useGetAuditsQuery({ affectedZUID: itemZUID, }); - const [createPublishing, { isLoading: publishing }] = - useCreateItemPublishingMutation(); + const [createPublishing] = useCreateItemPublishingMutation(); const [deleteItemPublishing, { isLoading: unpublishing }] = useDeleteItemPublishingMutation(); const lastItemUpdateAudit = itemAudit?.find( @@ -121,6 +138,79 @@ export const ItemEditHeaderActions = ({ } }); + const unpublishedRelatedItems = useMemo(() => { + if (!fields || !item.data || !items) return []; + + const relatedFieldZUIDs = Object.entries(item.data)?.reduce( + ( + acc: { + itemZUIDs: string[]; + relatedFieldZUID: string; + relatedModelZUID: string; + }[], + [key, value] + ) => { + const field = fields.find((field) => field.name === key); + + if ( + !!value && + (field?.datatype === "one_to_many" || + field?.datatype === "one_to_one") + ) { + acc = [ + ...acc, + { + relatedFieldZUID: field.relatedFieldZUID, + relatedModelZUID: field.relatedModelZUID, + itemZUIDs: (value as string)?.split(","), + }, + ]; + } + + return acc; + }, + [] + ); + + const unpublishedRelatedItems = Object.values(relatedFieldZUIDs) + ?.map(({ relatedFieldZUID, relatedModelZUID, itemZUIDs }) => { + const relatedItems = itemZUIDs?.map((ZUID) => { + const item = Object.values(items)?.find( + (item) => + item.meta.ZUID === ZUID && + item.meta.contentModelZUID === relatedModelZUID + ); + + if (!!item) { + const draftVersion = item?.meta?.version; + const publishedVersion = item?.publishing?.version || 0; + + if ( + draftVersion > publishedVersion && + !item?.scheduling?.isScheduled + ) { + return { + ...item, + relatedFieldZUID, + relatedModelZUID, + }; + } + } + }); + + return relatedItems; + }) + ?.flat() + ?.filter((item) => !!item); + + const uniqueItems = uniqBy(unpublishedRelatedItems, "meta.ZUID"); + + // Make sure that unpublished related items are checked by default + setRelatedItemsToPublish(uniqueItems); + + return uniqueItems; + }, [fields, item, items]); + const itemState = (() => { if (item?.dirty) { return ITEM_STATES.dirty; @@ -134,6 +224,7 @@ export const ItemEditHeaderActions = ({ })(); const handlePublish = async () => { + setIsPublishing(true); // If item is scheduled, delete the scheduled publishing first if (itemState === ITEM_STATES.scheduled) { await deleteItemPublishing({ @@ -142,18 +233,37 @@ export const ItemEditHeaderActions = ({ publishingZUID: item?.scheduling?.ZUID, }); } - createPublishing({ - modelZUID, - itemZUID, - body: { - version: item?.meta.version, - publishAt: "now", - unpublishAt: "never", - }, - }).then(() => { - // Retain non rtk-query fetch of item publishing for legacy code - dispatch(fetchItemPublishing(modelZUID, itemZUID)); - }); + + Promise.allSettled([ + createPublishing({ + modelZUID, + itemZUID, + body: { + version: item?.meta.version, + publishAt: "now", + unpublishAt: "never", + }, + }), + relatedItemsToPublish.map((item) => { + return createPublishing({ + modelZUID: item.meta.contentModelZUID, + itemZUID: item.meta.ZUID, + body: { + version: item.meta.version, + publishAt: "now", + unpublishAt: "never", + }, + }); + }), + ]) + .then(() => { + // Retain non rtk-query fetch of item publishing for legacy code + dispatch(fetchItemPublishings()); + }) + .finally(() => { + setIsPublishing(false); + setIsConfirmPublishModalOpen(false); + }); }; const handleUnpublish = async () => { @@ -320,7 +430,7 @@ export const ItemEditHeaderActions = ({ setIsConfirmPublishModalOpen(true); } }} - loading={publishing || saving || isFetching} + loading={isPublishing || saving || isFetching} color="success" variant="contained" id="PublishButton" @@ -338,7 +448,7 @@ export const ItemEditHeaderActions = ({ onClick={(e) => { setPublishMenu(e.currentTarget); }} - disabled={publishing || saving || isFetching} + disabled={isPublishing || saving || isFetching} data-cy="PublishMenuButton" > @@ -422,7 +532,7 @@ export const ItemEditHeaderActions = ({ onClick={() => { setIsConfirmPublishModalOpen(true); }} - loading={publishing || publishAfterSave || isFetching} + loading={isPublishing || publishAfterSave || isFetching} color="success" variant="contained" id="PublishButton" @@ -440,7 +550,7 @@ export const ItemEditHeaderActions = ({ onClick={(e) => { setPublishMenu(e.currentTarget); }} - disabled={publishing || publishAfterSave || isFetching} + disabled={isPublishing || publishAfterSave || isFetching} data-cy="PublishMenuButton" > @@ -526,12 +636,51 @@ export const ItemEditHeaderActions = ({ setPublishAfterUnschedule(false); }} onConfirm={() => { - setIsConfirmPublishModalOpen(false); + // setIsConfirmPublishModalOpen(false); setPublishAfterUnschedule(false); handlePublish(); }} altText={model?.type === "block" && "Variant"} - /> + relatedItemsToPublishCount={relatedItemsToPublish.length} + isPublishing={isPublishing} + > + {unpublishedRelatedItems?.length > 0 && ( + + + Also publish related items + + + This will publish all items selected in the list below + + + {unpublishedRelatedItems.map((item, index) => ( + index + 1} + selected={relatedItemsToPublish.some( + (i) => i.meta.ZUID === item.meta.ZUID + )} + onChange={({ action, contentItem }) => { + if (action === "add") { + setRelatedItemsToPublish([ + ...relatedItemsToPublish, + contentItem, + ]); + } else { + setRelatedItemsToPublish( + relatedItemsToPublish.filter( + (item) => item.meta.ZUID !== contentItem.meta.ZUID + ) + ); + } + }} + /> + ))} + + + )} + )} ); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx new file mode 100644 index 000000000..e2ed3cf89 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx @@ -0,0 +1,166 @@ +import { useMemo, useState } from "react"; +import { + ListItem, + ListItemButton, + ListItemText, + ListItemIcon, + Box, + Checkbox, + Stack, +} from "@mui/material"; +import { ImageRounded } from "@mui/icons-material"; +import { + ContentItem, + ContentItemWithDirtyAndPublishing, +} from "../../../../../../../../shell/services/types"; +import { useGetContentModelFieldsQuery } from "../../../../../../../../shell/services/instance"; + +export type ContentItemWithRelatedZUIDs = ContentItemWithDirtyAndPublishing & { + relatedModelZUID: string; + relatedFieldZUID: string; +}; +type UnpublishedRelatedItemProps = { + contentItem: ContentItemWithRelatedZUIDs; + onChange: (payload: { + action: "add" | "remove"; + contentItem: ContentItem; + }) => void; + selected: boolean; + divider?: boolean; +}; +export const UnpublishedRelatedItem = ({ + contentItem, + onChange, + selected, + divider, +}: UnpublishedRelatedItemProps) => { + const [imageError, setImageError] = useState(false); + const { data: modelFields } = useGetContentModelFieldsQuery( + contentItem.relatedModelZUID, + { + skip: !contentItem.relatedModelZUID, + } + ); + + const imageFieldName = useMemo(() => { + if (!modelFields?.length) return null; + + const imageFields = modelFields.filter( + (field) => !field.deletedAt && field.datatype === "images" + ); + + return imageFields?.[0]?.name || null; + }, [modelFields]); + + const imageURL = useMemo(() => { + if (!contentItem?.data || !imageFieldName) return null; + + if (!!contentItem.data[imageFieldName]) { + const value = String(contentItem.data[imageFieldName]).split(",")?.[0]; + + if (value.startsWith("3-")) { + return `${ + // @ts-ignore + CONFIG.SERVICE_MEDIA_RESOLVER + }/resolve/${value}/getimage/?w=64&h=64&type=crop`; + } else { + return value; + } + } + + return null; + }, [contentItem, imageFieldName]); + + const fieldValue = + contentItem?.data[ + modelFields?.find((field) => field.ZUID === contentItem.relatedFieldZUID) + ?.name + ]; + + return ( + + + + onChange({ + contentItem, + action: evt.target.checked ? "add" : "remove", + }) + } + /> + + {!!imageFieldName && + (!!imageURL && !imageError ? ( + setImageError(true)} + /> + ) : ( + + + + ))} + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ConfirmPublishModal.tsx b/src/shell/components/ConfirmPublishModal.tsx similarity index 71% rename from src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ConfirmPublishModal.tsx rename to src/shell/components/ConfirmPublishModal.tsx index b9266013d..ef794fcf3 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ConfirmPublishModal.tsx +++ b/src/shell/components/ConfirmPublishModal.tsx @@ -1,23 +1,28 @@ +import { useRef } from "react"; import { + Box, + Button, + ButtonBaseActions, Dialog, - DialogTitle, - DialogContent, DialogActions, - Button, - Typography, - Box, + DialogContent, + DialogTitle, Stack, - ButtonBaseActions, + Typography, } from "@mui/material"; import CloudUploadRoundedIcon from "@mui/icons-material/CloudUploadRounded"; -import { useRef } from "react"; +import { LoadingButton } from "@mui/lab"; +import pluralizeWord from "../../utility/pluralizeWord"; -type ConfirmPublishModal = { +export type ConfirmPublishModal = { contentTitle: string; onCancel: () => void; onConfirm: () => void; contentVersion: number; altText?: string; + isPublishing?: boolean; + children?: JSX.Element; + relatedItemsToPublishCount?: number; }; export const ConfirmPublishModal = ({ contentTitle, @@ -25,6 +30,9 @@ export const ConfirmPublishModal = ({ onConfirm, contentVersion, altText, + isPublishing, + children, + relatedItemsToPublishCount, }: ConfirmPublishModal) => { const actionRef = useRef(null); const onEntered = () => actionRef?.current?.focusVisible(); @@ -61,12 +69,20 @@ export const ConfirmPublishModal = ({ {altText ? altText?.toLowerCase() : "item"} available on all of your platforms. You can always unpublish this item later if needed. + {children} - - + Publish{" "} + {!!altText + ? pluralizeWord(altText, relatedItemsToPublishCount) + : pluralizeWord("Item", relatedItemsToPublishCount)}{" "} + {!!relatedItemsToPublishCount && + `(${relatedItemsToPublishCount + 1})`} + ); diff --git a/src/shell/components/Filters/FilterButton.tsx b/src/shell/components/Filters/FilterButton.tsx index 917d7d5e1..c12305741 100644 --- a/src/shell/components/Filters/FilterButton.tsx +++ b/src/shell/components/Filters/FilterButton.tsx @@ -6,7 +6,7 @@ import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; interface FilterButton { isFilterActive: boolean; - buttonText: string; + buttonText: string | React.ReactNode; onOpenMenu: (e: React.MouseEvent) => void; onRemoveFilter: (e: React.MouseEvent) => void; children?: React.ReactNode; @@ -32,7 +32,21 @@ export const FilterButton: FC = ({ onClick={onOpenMenu} data-cy={`${filterId}_selected`} > - {buttonText} + + {buttonText} + + + + + + + + ); +}; diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/FieldSelectorFilters.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/FieldSelectorFilters.tsx new file mode 100644 index 000000000..c1eb3bf8e --- /dev/null +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/FieldSelectorFilters.tsx @@ -0,0 +1,486 @@ +import { useState, useMemo } from "react"; +import { + Stack, + Menu, + MenuItem, + MenuList, + Typography, + ListItemText, + Box, +} from "@mui/material"; +import { ChevronRightOutlined } from "@mui/icons-material"; + +import { FilterButton, UserFilter } from "../../Filters"; +import { CascadingMenuItem } from "../../CascadingMenuItem"; +import { + useGetContentModelFieldsQuery, + useGetLangsQuery, +} from "../../../services/instance"; +import { useGetUsersQuery } from "../../../services/accounts"; +import { DateFilterValue, DateFilter } from "../../Filters/DateFilter"; +import { FieldFilters } from "./index"; +import { DateRangeFilterValue } from "../../Filters/DateFilter/types"; + +const SORT_ORDER = { + lastSaved: "Last Saved", + lastPublished: "Last Published", + createdOn: "Date Created", + version: "Status", +} as const; + +export const STATUS_FILTER = { + published: "Published", + scheduled: "Scheduled", + notPublished: "Not Published", +} as const; + +const FILTERABLE_DATA_TYPES = [ + "text", + "wysiwyg_basic", + "wysiwyg_advanced", + "article_writer", + "markdown", + "textarea", + "number", + "images", + "date", + "datetime", + "one_to_many", + "one_to_one", + "uuid", + "number", + "currency", + "date", + "datetime", + "link", + "internal_link", + "sort", +] as const; + +const getCountryCode = (langCode: string) => { + if (!langCode) return ""; + const splitTag = langCode.split("-"); + const countryCode = + splitTag.length === 2 ? splitTag[1] : langCode.toUpperCase(); + + return countryCode; +}; + +type FieldSelectorFiltersProps = { + modelZUID: string; + filters: FieldFilters; + onUpdateFilter: (filter: Partial) => void; +}; +export const FieldSelectorFilters = ({ + modelZUID, + filters, + onUpdateFilter, +}: FieldSelectorFiltersProps) => { + const [anchorEl, setAnchorEl] = useState({ + currentTarget: null, + id: "", + }); + const { data: users } = useGetUsersQuery(); + const { data: fields, isLoading: isFieldsLoading } = + useGetContentModelFieldsQuery(modelZUID); + const { data: langs } = useGetLangsQuery({}); + + const userOptions = useMemo(() => { + return users?.map((user) => ({ + firstName: user.firstName, + lastName: user.lastName, + ZUID: user.ZUID, + email: user.email, + })); + }, [users]); + + const selectedLang = useMemo(() => { + return langs?.find((lang) => lang.ID === filters.lang); + }, [langs, filters.lang]); + + const handleUpdateSortOrder = (sortOrder: string) => { + setAnchorEl({ + currentTarget: null, + id: "", + }); + + onUpdateFilter({ sortOrder }); + }; + + const getButtonText = (sortOrder: string) => { + if (!sortOrder) { + return SORT_ORDER.lastSaved; + } + + if (sortOrder === "createdBy") { + return "Created By"; + } + + if (sortOrder === "zuid") { + return "ZUID"; + } + + if (SORT_ORDER.hasOwnProperty(sortOrder)) { + return SORT_ORDER[sortOrder as keyof typeof SORT_ORDER]; + } + + const fieldLabel = fields?.find((field) => field.name === sortOrder)?.label; + return fieldLabel; + }; + + const handleUpdateDateFilter = (dateFilter: DateFilterValue) => { + switch (dateFilter.type) { + case "daterange": { + const value = dateFilter.value as DateRangeFilterValue; + + onUpdateFilter({ + date: { + preset: null, + to: value.to, + from: value.from, + }, + }); + return; + } + + case "on": { + const value = dateFilter.value as string; + + onUpdateFilter({ + date: { + preset: null, + to: value, + from: value, + }, + }); + return; + } + case "before": { + const value = dateFilter.value as string; + + onUpdateFilter({ + date: { + preset: null, + to: value, + from: null, + }, + }); + return; + } + case "after": { + const value = dateFilter.value as string; + + onUpdateFilter({ + date: { + preset: null, + to: null, + from: value, + }, + }); + return; + } + case "preset": { + const value = dateFilter.value as string; + + onUpdateFilter({ + date: { + preset: value, + to: null, + from: null, + }, + }); + return; + } + + default: { + onUpdateFilter({ + date: { + preset: null, + to: null, + from: null, + }, + }); + return; + } + } + }; + + const activeDateFilter: DateFilterValue = useMemo(() => { + const isPreset = !!filters.date.preset; + const isBefore = !!filters.date.to && !!!filters.date.from; + const isAfter = !!filters.date.from && !!!filters.date.to; + const isOn = + !!filters.date.to && + !!filters.date.from && + filters.date.to === filters.date.from; + const isDateRange = + !!filters.date.to && + !!filters.date.from && + filters.date.to !== filters.date.from; + + if (isPreset) { + return { + type: "preset", + value: filters.date.preset, + }; + } + + if (isBefore) { + return { + type: "before", + value: filters.date.to, + }; + } + + if (isAfter) { + return { + type: "after", + value: filters.date.from, + }; + } + + if (isOn) { + return { + type: "on", + value: filters.date.from, + }; + } + + if (isDateRange) { + return { + type: "daterange", + value: { + from: filters.date.from, + to: filters.date.to, + }, + }; + } + + return { + type: "", + value: "", + }; + }, [filters.date]); + + return ( + + ) => { + setAnchorEl({ + currentTarget: event.currentTarget, + id: "sort", + }); + }} + onRemoveFilter={() => {}} + /> + setAnchorEl(null)} + anchorEl={anchorEl?.currentTarget} + transformOrigin={{ + vertical: -8, + horizontal: "left", + }} + // add set width to the menu + PaperProps={{ + sx: { + width: "240px", + maxHeight: "420px", + }, + }} + > + {Object.entries(SORT_ORDER).map(([key, value]) => ( + handleUpdateSortOrder(key)} + selected={ + key === "lastSaved" + ? !filters.sortOrder || filters.sortOrder === "lastSaved" + : filters.sortOrder === key + } + > + {value} + + ))} + + More + + + } + PaperProps={{ + sx: { + width: 240, + }, + }} + > + + handleUpdateSortOrder("createdBy")} + > + Created By + + handleUpdateSortOrder("zuid")} + > + ZUID + + + + `1px solid ${theme.palette.border}`, + }} + > + FIELDS + + {fields + ?.filter((field) => + FILTERABLE_DATA_TYPES.includes(field.datatype as any) + ) + ?.map((field) => ( + handleUpdateSortOrder(field.name)} + selected={filters.sortOrder === field.name} + > + + {field.label} + + + ))} + + ) => { + setAnchorEl({ + currentTarget: event.currentTarget, + id: "statusFilter", + }); + }} + onRemoveFilter={() => { + onUpdateFilter({ status: null }); + }} + /> + setAnchorEl(null)} + anchorEl={anchorEl?.currentTarget} + transformOrigin={{ + vertical: -8, + horizontal: "left", + }} + > + {Object.entries(STATUS_FILTER).map(([key, value]) => ( + { + onUpdateFilter({ status: key as keyof typeof STATUS_FILTER }); + setAnchorEl({ + currentTarget: null, + id: "", + }); + }} + selected={filters.status === key} + > + {value} + + ))} + + onUpdateFilter({ user })} + defaultButtonText="Created By" + options={userOptions} + /> + handleUpdateDateFilter(date)} + value={activeDateFilter} + /> + + + {selectedLang?.code.split("-")?.[0]?.toUpperCase()} ( + {getCountryCode(selectedLang.code)}) + + ) : ( + "" + ) + } + onOpenMenu={(event: React.MouseEvent) => { + setAnchorEl({ + currentTarget: event.currentTarget, + id: "lang", + }); + }} + onRemoveFilter={() => {}} + /> + setAnchorEl(null)} + transformOrigin={{ + vertical: -8, + horizontal: "left", + }} + anchorEl={anchorEl?.currentTarget} + open={!!anchorEl?.currentTarget && anchorEl.id === "lang"} + > + {langs?.map((lang) => ( + { + setAnchorEl({ + currentTarget: null, + id: "", + }); + onUpdateFilter({ lang: lang.ID }); + }} + > + + {lang.code?.split("-")?.[0]?.toUpperCase()} ( + {getCountryCode(lang.code)}) + + ))} + + + ); +}; diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/ImageCell.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/ImageCell.tsx new file mode 100644 index 000000000..a381f3024 --- /dev/null +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/ImageCell.tsx @@ -0,0 +1,68 @@ +import { Box, Stack } from "@mui/material"; +import { useMemo, useState } from "react"; +import { ImageRounded } from "@mui/icons-material"; + +import { useGetContentItemQuery } from "../../../services/instance"; + +type ImageCellProps = { + imageFieldName: string; + itemZUID: string; +}; +export const ImageCell = ({ imageFieldName, itemZUID }: ImageCellProps) => { + const [imageError, setImageError] = useState(false); + const { data: contentItem, isLoading: isLoadingContentItem } = + useGetContentItemQuery(itemZUID, { + skip: !itemZUID, + }); + + const imageURL = useMemo(() => { + if (!contentItem?.data || !imageFieldName) return null; + + if (!!contentItem.data[imageFieldName]) { + const value = String(contentItem.data[imageFieldName]).split(",")?.[0]; + + if (value.startsWith("3-")) { + return `${ + // @ts-ignore + CONFIG.SERVICE_MEDIA_RESOLVER + }/resolve/${value}/getimage/?w=40&h=40&type=crop`; + } else { + return value; + } + } + + return null; + }, [contentItem, imageFieldName]); + + if (!imageURL || imageError) { + return ( + + + + ); + } + + return ( + setImageError(true)} + /> + ); +}; diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/ItemsLoading.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/ItemsLoading.tsx new file mode 100644 index 000000000..8a4414990 --- /dev/null +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/ItemsLoading.tsx @@ -0,0 +1,64 @@ +import { Stack, Skeleton, Checkbox } from "@mui/material"; + +type ItemsLoadingProps = {}; +export const ItemsLoading = ({}: ItemsLoadingProps) => { + return ( + + {Array.from({ length: 10 }).map((_, index) => ( + + + + + + + + + + + + + + + + + + + + + ))} + + ); +}; diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/TitleCell.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/TitleCell.tsx new file mode 100644 index 000000000..02c40c773 --- /dev/null +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/TitleCell.tsx @@ -0,0 +1,35 @@ +import { Box, Typography } from "@mui/material"; + +type TitleCellProps = { + primaryText: string; + secondaryText: string; +}; +export const TitleCell = ({ primaryText, secondaryText }: TitleCellProps) => { + return ( + + + {primaryText} + + {!!secondaryText && ( + + {secondaryText} + + )} + + ); +}; diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/VersionCell.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/VersionCell.tsx new file mode 100644 index 000000000..a7ed8f58d --- /dev/null +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/VersionCell.tsx @@ -0,0 +1,45 @@ +import { Stack } from "@mui/material"; + +import { ContentItem, Publishing } from "../../../services/types"; +import { VersionChip } from "../VersionChip"; + +type VersionCellProps = { + itemData: ContentItem & { createdByName: string }; + publishData: Publishing & { publishedByName: string }; + scheduleData: Publishing & { scheduledByName: string }; +}; +export const VersionCell = ({ + itemData, + publishData, + scheduleData, +}: VersionCellProps) => { + return ( + + {itemData?.meta?.version > (publishData?.version || 0) && ( + + )} + {!!scheduleData ? ( + + ) : publishData ? ( + + ) : ( + <> + )} + + ); +}; diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx new file mode 100644 index 000000000..1678851b9 --- /dev/null +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx @@ -0,0 +1,684 @@ +import { + useEffect, + useMemo, + useState, + useCallback, + useRef, + useReducer, +} from "react"; +import { + Dialog, + DialogContent, + TextField, + InputAdornment, + Box, +} from "@mui/material"; +import { Search } from "@mui/icons-material"; +import { + DataGridPro, + GridColumns, + GridInputSelectionModel, + GridRenderCellParams, +} from "@mui/x-data-grid-pro"; +import { debounce } from "lodash"; +import { useDispatch, useSelector } from "react-redux"; + +import { FieldSelectorFilters, STATUS_FILTER } from "./FieldSelectorFilters"; +import { + useGetLangsQuery, + useGetContentModelFieldsQuery, +} from "../../../services/instance"; +import { ImageCell } from "./ImageCell"; +import { TitleCell } from "./TitleCell"; +import { VersionCell } from "./VersionCell"; +import { ItemsLoading } from "./ItemsLoading"; +import { useGetUsersQuery } from "../../../services/accounts"; +import { NoSearchResults } from "../../NoSearchResults"; +import { DialogHeader } from "./DialogHeader"; +import { fetchItems } from "../../../store/content"; +import { AppState } from "../../../store/types"; +import { ContentItem } from "../../../services/types"; +import moment from "moment"; +import { getDateFilterFnByValues } from "../../Filters/DateFilter/getDateFilter"; + +const selectFilteredItems = ( + state: AppState, + modelZUID: string, + activeLangId: number, + skip = false +) => { + if (skip) { + return []; + } + return Object.values(state.content).filter( + (item: ContentItem) => + item.meta.contentModelZUID === modelZUID && + item.meta.langID === activeLangId + ); +}; + +export type FieldFilters = { + sortOrder: string; + user: string; + date: { + preset: string; + from: string; + to: string; + }; + lang: number; + status: keyof typeof STATUS_FILTER; +}; +type FieldSelectorDialogProps = { + onClose: () => void; + modelZUID: string; + modelName: string; + relatedFieldName: string; + selectedZUIDs: string[]; + onUpdateSelectedZUIDs: (selectedZUIDs: string[]) => void; + multiselect?: boolean; +}; +export const FieldSelectorDialog = ({ + onClose, + modelZUID, + modelName, + relatedFieldName, + selectedZUIDs, + onUpdateSelectedZUIDs, + multiselect, +}: FieldSelectorDialogProps) => { + const dispatch = useDispatch(); + const searchField = useRef(null); + const [filterKeyword, setFilterKeyword] = useState(null); + const [filters, updateFilters] = useReducer( + (state: FieldFilters, newValue: Partial): FieldFilters => { + return { + ...state, + ...newValue, + }; + }, + { + sortOrder: "lastSaved", + user: null, + date: { + preset: null, + from: null, + to: null, + }, + lang: null, + status: null, + } + ); + const [selectionModel, setSelectionModel] = + useState(selectedZUIDs); + const [isFetchingContentItems, setIsFetchingContentItems] = useState(false); + + const { data: langs } = useGetLangsQuery({}); + const langCode = langs?.find((lang) => lang.ID === filters.lang)?.code; + const contentItems = useSelector((state: AppState) => + selectFilteredItems(state, modelZUID, filters.lang, isFetchingContentItems) + ); + const { data: relatedModelFields, isLoading: isLoadingRelatedModel } = + useGetContentModelFieldsQuery(modelZUID, { + skip: !modelZUID, + }); + const { data: users, isLoading: isLoadingUsers } = useGetUsersQuery(); + + useEffect(() => { + if (!!langs?.length) { + updateFilters({ lang: langs.find((lang) => lang.default)?.ID }); + } + }, [langs]); + + useEffect(() => { + if (!!modelZUID) { + setIsFetchingContentItems(true); + dispatch( + fetchItems(modelZUID, { + lang: langCode, + limit: 5000, + }) + // @ts-ignore + ).then(() => { + setIsFetchingContentItems(false); + }); + } + }, [modelZUID, langCode]); + + const imageFieldName = useMemo(() => { + if (!relatedModelFields?.length) return null; + + const imageFields = relatedModelFields.filter( + (field) => !field.deletedAt && field.datatype === "images" + ); + + return imageFields?.[0]?.name || null; + }, [relatedModelFields]); + + const columns = useMemo(() => { + let defaultCols: GridColumns = [ + { + field: "title", + flex: 1, + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + { + field: "version", + width: 60, + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + ]; + + if (imageFieldName) { + defaultCols = [ + { + field: "image", + width: 40, + minWidth: 40, + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + ...defaultCols, + ]; + } + + return defaultCols; + }, [imageFieldName]); + + const resolveUserZUID = (userZUID: string) => { + const user = users?.find((user) => user.ZUID === userZUID); + + if (!!user) { + return `${user?.firstName} ${user.lastName}`; + } + + return userZUID; + }; + + const mappedRows = useMemo(() => { + if (!contentItems?.length || !users?.length) return []; + + let _rows = [...contentItems]; + + return _rows?.map((item) => ({ + id: item.meta?.ZUID, + image: { + imageFieldName, + itemZUID: item.meta?.ZUID, + }, + title: { + primary: + item.data?.[relatedFieldName] || + item.web?.metaTitle || + item.web?.metaLinkText, + secondary: item.web?.metaDescription, + }, + version: { + itemData: { + ...item, + createdByName: resolveUserZUID(item.meta?.createdByUserZUID), + }, + publishData: item?.publishing?.version + ? { + ...item.publishing, + publishedByName: resolveUserZUID( + item.publishing?.publishedByUserZUID + ), + } + : null, + scheduleData: item?.scheduling?.version + ? { + ...item.scheduling, + scheduledByName: resolveUserZUID( + item.scheduling?.publishedByUserZUID + ), + } + : null, + }, + item, + })); + }, [contentItems, users, relatedFieldName, imageFieldName]); + + const rows = useMemo(() => { + if (!mappedRows?.length) return []; + + let _rows = [...mappedRows]; + + // Sorting + _rows?.sort((a: any, b: any) => { + if (filters.sortOrder === "lastSaved") { + const dateA = new Date(a.item?.web?.createdAt).getTime(); + const dateB = new Date(b.item?.web?.createdAt).getTime(); + + if (!a.item?.web?.createdAt) { + return -1; + } else if (!b.item?.web?.createdAt) { + return 1; + } else { + return dateB - dateA; + } + } else if (filters.sortOrder === "lastPublished") { + // Handle undefined publishAt by setting a default far-future date for sorting purposes + + let dateA = + a?.item?.scheduling?.publishAt || a?.item?.publishing?.publishAt; + dateA = dateA ? new Date(dateA).getTime() : Number.NEGATIVE_INFINITY; + + let dateB = + b?.item?.scheduling?.publishAt || b?.item?.publishing?.publishAt; + dateB = dateB ? new Date(dateB).getTime() : Number.NEGATIVE_INFINITY; + + return dateB - dateA; + // return moment(dateB).diff(moment(dateA)); + } else if (filters.sortOrder === "createdOn") { + return moment(b?.item?.meta.createdAt).diff(a?.item?.meta.createdAt); + // new Date(b?.item?.meta.createdAt).getTime() - + // new Date(a?.item?.meta.createdAt).getTime() + } else if (filters.sortOrder === "version") { + const aIsPublished = a?.item?.publishing?.publishAt; + const bIsPublished = b?.item?.publishing?.publishAt; + + const aIsScheduled = a?.item?.scheduling?.publishAt; + const bIsScheduled = b?.item?.scheduling?.publishAt; + + // Check if meta.version exists + const aHasVersion = a?.item?.meta?.version !== null; + const bHasVersion = b?.item?.meta?.version !== null; + + // Place items without meta.version at the bottom + if (!aHasVersion && bHasVersion) { + return 1; + } else if (aHasVersion && !bHasVersion) { + return -1; + } + + // Items with only publish date + if (aIsPublished && !aIsScheduled && bIsPublished && !bIsScheduled) { + return ( + new Date(bIsPublished).getTime() - new Date(aIsPublished).getTime() + ); // Both have only published date, sort by publish date descending + } else if (aIsPublished && !aIsScheduled) { + return -1; // A has only published date, B does not + } else if (bIsPublished && !bIsScheduled) { + return 1; // B has only published date, A does not + } + + // Items with scheduled date (and also publish date) + if (aIsScheduled && bIsScheduled) { + return ( + new Date(aIsScheduled).getTime() - new Date(bIsScheduled).getTime() + ); // Both are scheduled, sort by scheduled date ascending + } else if (aIsScheduled) { + return -1; // A is scheduled, B is not + } else if (bIsScheduled) { + return 1; // B is scheduled, A is not + } + + // Items with neither publish nor schedule dates + if (aIsPublished && bIsPublished) { + return ( + new Date(bIsPublished).getTime() - new Date(aIsPublished).getTime() + ); // Both are published, sort by publish date descending + } else if (aIsPublished) { + return -1; // A is published, B is not + } else if (bIsPublished) { + return 1; // B is published, A is not + } + + return 0; // Neither are published or scheduled + } else if (filters.sortOrder === "createdBy") { + const userA = a?.version?.itemData?.createdByName; + const userB = b?.version?.itemData?.createdByName; + + const startsWithNumber = (str: string) => /^\d/.test(str); + + if (!userA || (startsWithNumber(userA) && !startsWithNumber(userB))) { + return 1; + } else if ( + !userB || + (!startsWithNumber(userA) && startsWithNumber(userB)) + ) { + return -1; + } else { + return userA.localeCompare(userB); + } + } else if (filters.sortOrder === "zuid") { + return a?.item?.meta?.ZUID?.localeCompare(b?.item?.meta?.ZUID); + } else if ( + relatedModelFields?.find((field) => field.name === filters.sortOrder) + ) { + const fieldName = filters.sortOrder; + const dataType = relatedModelFields?.find( + (field) => field.name === filters.sortOrder + )?.datatype; + + if (typeof a?.item?.data[fieldName] === "number") { + if (a?.item?.data[fieldName] == null) return 1; + if (b?.item?.data[fieldName] == null) return -1; + + if (dataType === "sort") { + return b?.item?.data[fieldName] - a?.item?.data[fieldName]; + } + + return b?.item?.data[fieldName] - a?.item?.data[fieldName]; + } + if (dataType === "date" || dataType === "datetime") { + if (!a?.item?.data[fieldName]) { + return 1; + } else if (!b?.item?.data[fieldName]) { + return -1; + } else { + return ( + new Date(b?.item?.data[fieldName]).getTime() - + new Date(a?.item?.data[fieldName]).getTime() + ); + } + } + + if (dataType === "yes_no") { + if (!a?.item?.data[fieldName]) { + return 1; + } else if (!b?.item?.data[fieldName]) { + return -1; + } else { + return b - a; + } + } + + const aValue = + dataType === "images" + ? a?.item?.data[fieldName]?.filename + : a?.item?.data[fieldName]; + const bValue = + dataType === "images" + ? b?.item?.data[fieldName]?.filename + : b?.item?.data[fieldName]; + + return aValue?.trim()?.localeCompare(bValue?.trim()); + } else { + const dateA = new Date(a.item?.web?.createdAt).getTime(); + const dateB = new Date(b.item?.web?.createdAt).getTime(); + + if (!a.item?.web?.createdAt) { + return -1; + } else if (!b.item?.web?.createdAt) { + return 1; + } else { + return dateB - dateA; + } + } + }); + + // Keyword search + if (!!filterKeyword) { + const search = filterKeyword.toLowerCase(); + + _rows = _rows?.filter((row) => { + const matchedUser = users.find( + (user) => user.ZUID === row?.item?.meta?.createdByUserZUID + ); + const creator = matchedUser + ? `${matchedUser.firstName} ${matchedUser.lastName}` + : null; + + return ( + Object.values(row?.item.data).some((value: any) => { + if (!value) return false; + + if (value?.filename || value?.title) { + return ( + value?.filename?.toLowerCase()?.includes(search) || + value?.title?.toLowerCase()?.includes(search) + ); + } + + return value.toString().toLowerCase().includes(search); + }) || + row?.item?.meta?.createdAt?.toLowerCase().includes(search) || + row?.item?.web?.updatedAt?.toLowerCase().includes(search) || + row?.item?.meta?.ZUID?.toLowerCase().includes(search) || + creator?.toLowerCase()?.includes(search) + ); + }); + } + + // Filtering + if (filters.status) { + _rows = _rows?.filter((item) => { + if (filters.status === "published") { + return ( + item.item?.publishing?.publishAt && + !item.item?.scheduling?.publishAt + ); + } else if (filters.status === "scheduled") { + return item.item?.scheduling?.publishAt; + } else if (filters.status === "notPublished") { + return ( + !item.item?.publishing?.publishAt && + !item.item?.scheduling?.publishAt + ); + } + }); + } + + if (filters.user) { + _rows = _rows.filter( + (item) => item.item?.meta?.createdByUserZUID === filters.user + ); + } + + const dateFilterFn = getDateFilterFnByValues(filters.date); + if (dateFilterFn) { + _rows = _rows.filter((item) => { + if (!!item.item?.meta?.updatedAt) { + return dateFilterFn(item.item?.meta?.updatedAt); + } + + return false; + }); + } + return _rows; + }, [mappedRows, filterKeyword, relatedModelFields]); + + const deletedItemZUIDs = useMemo(() => { + if (!contentItems?.length || !selectedZUIDs) return []; + + return ( + selectedZUIDs.filter( + (ZUID) => !contentItems?.find((item) => item.meta?.ZUID === ZUID) + ) || [] + ); + }, [contentItems, selectedZUIDs]); + + const debouncedSetFilterKeyword = useCallback( + debounce((value) => { + setFilterKeyword(value); + }, 300), + [setFilterKeyword] + ); + + const isLoading = + isFetchingContentItems || isLoadingRelatedModel || isLoadingUsers; + const isFilteringResults = + !!filterKeyword || + !!filters.status || + !!filters.user || + !!filters.date.preset || + !!filters.date.from || + !!filters.date.to; + const filteredSelectionModels = (selectionModel as string[])?.filter( + (ZUID) => !deletedItemZUIDs?.includes(ZUID) + ); + + return ( + + setSelectionModel([])} + onDone={() => onUpdateSelectedZUIDs(selectionModel as string[])} + loading={isLoading} + /> + + debouncedSetFilterKeyword(evt.currentTarget.value)} + size="small" + placeholder="Filter Items" + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + {isLoading ? ( + + ) : ( + + {!rows?.length && isFilteringResults ? ( + { + if (!!filterKeyword) { + setFilterKeyword(""); + if (!!searchField.current) { + searchField.current.querySelector("input").value = ""; + searchField.current.querySelector("input").focus(); + } + } + + updateFilters({ + sortOrder: "lastSaved", + user: null, + date: { + preset: null, + from: null, + to: null, + }, + lang: langs.find((lang) => lang.default)?.ID, + status: null, + }); + }} + ignoreFilters + hideBackButton + /> + ) : ( + { + let _newSelectionModel = newSelectionModel as string[]; + + if (!multiselect && _newSelectionModel?.length > 1) { + _newSelectionModel = [_newSelectionModel[0]]; + } + + setSelectionModel([ + ...deletedItemZUIDs, + ..._newSelectionModel, + ]); + }} + sx={{ + bgcolor: "background.paper", + + "& .MuiDataGrid-columnHeaders": { + borderBottom: 0, + }, + + "& .MuiDataGrid-cellCheckbox": { + mx: "3px", + }, + + "& [data-field='image']": { + p: 0, + }, + + "& [data-field='title']": { + pl: !!imageFieldName ? 2 : 0, + pr: 2, + }, + + "& [data-field='version']": { + pl: 0, + pr: 2, + justifyContent: "center", + }, + }} + /> + )} + + )} + + + ); +}; diff --git a/src/shell/components/RelationalFieldBase/VersionChip.tsx b/src/shell/components/RelationalFieldBase/VersionChip.tsx new file mode 100644 index 000000000..cee27e850 --- /dev/null +++ b/src/shell/components/RelationalFieldBase/VersionChip.tsx @@ -0,0 +1,77 @@ +import { Tooltip, Chip } from "@mui/material"; + +const CHIP_CONFIG = { + scheduled: { + text: "scheduled to publish", + color: "warning", + }, + published: { + text: "published", + color: "success", + }, + draft: { + text: "saved", + color: "info", + }, +} as const; + +type VersionChipProps = { + type: "scheduled" | "draft" | "published"; + version: number; + dateTime: string; + publisher: string; +}; +export const VersionChip = ({ + type, + version, + dateTime, + publisher, +}: VersionChipProps) => { + return ( + + v{version} {CHIP_CONFIG[type]?.text} on
+ {new Date(dateTime).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + timeZoneName: "short", + })}{" "} +
by {publisher} + + } + slotProps={{ + popper: { + style: { + width: 160, + }, + }, + }} + > + +
+ ); +}; diff --git a/src/shell/components/RelationalFieldBase/index.tsx b/src/shell/components/RelationalFieldBase/index.tsx new file mode 100644 index 000000000..eccfa7ee2 --- /dev/null +++ b/src/shell/components/RelationalFieldBase/index.tsx @@ -0,0 +1,166 @@ +import { useEffect, useState, useCallback } from "react"; +import { Box, Button, Stack } from "@mui/material"; +import { + LinkRounded, + KeyboardArrowUpRounded, + KeyboardArrowDownRounded, +} from "@mui/icons-material"; +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import { useDispatch } from "react-redux"; + +import { ActiveItem } from "./ActiveItem"; +import { FieldSelectorDialog } from "./FieldSelectorDialog"; +import { + useGetContentModelQuery, + useGetContentModelFieldsQuery, +} from "../../services/instance"; +import { fetchItems } from "../../store/content"; +import { ActiveItemLoading } from "./ActiveItem/ActiveItemLoading"; + +type RelationalFieldBaseProps = { + name: string; + value: string; + relatedModelZUID: string; + relatedFieldZUID: string; + onChange: (value: string, name: string) => void; + multiselect?: boolean; +}; +export const RelationalFieldBase = ({ + name, + value, + relatedModelZUID, + relatedFieldZUID, + onChange, + multiselect, +}: RelationalFieldBaseProps) => { + const dispatch = useDispatch(); + const [itemZUIDs, setItemZUIDs] = useState(value?.split(",") || []); + const [showAll, setShowAll] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + + const { data: modelData, isLoading: isLoadingModelData } = + useGetContentModelQuery(relatedModelZUID, { + skip: !relatedModelZUID, + }); + const { data: modelFields, isLoading: isLoadingModelFields } = + useGetContentModelFieldsQuery(relatedModelZUID, { + skip: !relatedModelZUID, + }); + + useEffect(() => { + if (!!relatedModelZUID) { + dispatch(fetchItems(relatedModelZUID)); + } + }, [relatedModelZUID]); + + const handleMoveCard = useCallback( + (draggedItemZUID: string, dropIndex: number) => { + const draggedIndex = itemZUIDs.indexOf(draggedItemZUID); + const _itemZUIDs = [...itemZUIDs]; + + _itemZUIDs.splice(draggedIndex, 1); + _itemZUIDs.splice(dropIndex, 0, draggedItemZUID); + + setItemZUIDs(_itemZUIDs); + }, + [itemZUIDs] + ); + + const handleReorder = useCallback(() => { + onChange(itemZUIDs?.join(","), name); + }, [itemZUIDs]); + + return ( + + + {isLoadingModelData || isLoadingModelFields ? ( + [...Array(multiselect ? 5 : 1)].map((_, index) => ( + + )) + ) : ( + + {itemZUIDs?.slice(0, showAll ? undefined : 5)?.map((val, index) => ( + field.ZUID === relatedFieldZUID + )} + onMoveCard={handleMoveCard} + onDropCard={handleReorder} + onRemoveCard={(itemZUID) => { + setItemZUIDs((prev) => + prev.filter((zuid) => zuid !== itemZUID) + ); + onChange( + itemZUIDs.filter((zuid) => zuid !== itemZUID).join(","), + name + ); + }} + draggable={multiselect} + /> + ))} + + )} + + {itemZUIDs?.length > 5 && ( + + )} + {(multiselect || (!multiselect && !value)) && ( + + )} + {!!anchorEl && ( + setAnchorEl(null)} + modelZUID={relatedModelZUID} + modelName={modelData?.label} + relatedFieldName={ + modelFields?.find((field) => field.ZUID === relatedFieldZUID)?.name + } + selectedZUIDs={itemZUIDs} + onUpdateSelectedZUIDs={(selectedZUIDs) => { + onChange( + !!selectedZUIDs?.length ? selectedZUIDs.join(",") : null, + name + ); + setItemZUIDs(!!selectedZUIDs?.length ? selectedZUIDs : null); + setAnchorEl(null); + }} + /> + )} + + ); +}; diff --git a/src/shell/components/SchedulePublish/index.tsx b/src/shell/components/SchedulePublish/index.tsx index a8b260b2d..6f68cedad 100644 --- a/src/shell/components/SchedulePublish/index.tsx +++ b/src/shell/components/SchedulePublish/index.tsx @@ -109,6 +109,7 @@ export const SchedulePublish = ({ return ( Date: Tue, 11 Feb 2025 10:56:24 +0800 Subject: [PATCH 03/10] [Content | Schema] VQA updates for the one-to-one/many revamp (#3194) --- cypress/e2e/schema/field.spec.js | 12 +- .../AddFieldModal/DefaultValueInput.tsx | 192 ++---------------- .../AddFieldModal/views/FieldForm.tsx | 18 +- .../FieldSelectorDialog/index.tsx | 1 + .../components/RelationalFieldBase/index.tsx | 2 +- 5 files changed, 39 insertions(+), 186 deletions(-) diff --git a/cypress/e2e/schema/field.spec.js b/cypress/e2e/schema/field.spec.js index 8cd78c312..cfd1c4a8d 100644 --- a/cypress/e2e/schema/field.spec.js +++ b/cypress/e2e/schema/field.spec.js @@ -54,6 +54,9 @@ const SELECTORS = { MAX_CHARACTER_LIMIT_INPUT: "MaxCharacterLimitInput", MIN_CHARACTER_ERROR_MSG: "MinCharacterErrorMsg", MAX_CHARACTER_ERROR_MSG: "MaxCharacterErrorMsg", + ADD_NEW_RELATED_ITEM: "add-relational-item-button", + CONFIRM_NEW_RELATED_ITEM: "done-selecting-item-button", + ACTIVE_RELATED_ITEM: "active-relational-item", }; /** @@ -341,13 +344,12 @@ describe("Schema: Fields", () => { // click on the default value checkbox cy.getBySelector(SELECTORS.DEFAULT_VALUE_CHECKBOX).click(); // enter a default value - cy.getBySelector(SELECTORS.DEFAULT_VALUE_INPUT).click(); + cy.getBySelector(SELECTORS.ADD_NEW_RELATED_ITEM).click(); // Select the option - cy.get("[role=listbox] [role=option]").first().click(); + cy.get(".MuiDataGrid-row").first().find("input").click(); + cy.getBySelector(SELECTORS.CONFIRM_NEW_RELATED_ITEM).click(); // verify that the default value is set - cy.getBySelector(SELECTORS.DEFAULT_VALUE_INPUT) - .find("input") - .should("have.value", "- None -"); + cy.getBySelector(SELECTORS.ACTIVE_RELATED_ITEM).should("have.length", 1); cy.getBySelector(SELECTORS.DEFAULT_VALUE_CHECKBOX).click(); // Click done diff --git a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx index 91c52679d..77f361419 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx @@ -4,7 +4,6 @@ import { Menu, MenuItem, Button, - Chip, ToggleButtonGroup, ToggleButton, Select, @@ -24,23 +23,13 @@ import { } from "../../../../../content-editor/src/app/components/Editor/Field/FieldShell"; import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded"; import { FieldTypeMedia } from "../../../../../content-editor/src/app/components/FieldTypeMedia"; -import { AppLink } from "@zesty-io/core/AppLink"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faEdit } from "@fortawesome/free-solid-svg-icons"; + import { useDispatch, useSelector } from "react-redux"; -import { fetchItems, searchItems } from "../../../../../../shell/store/content"; -import { - FieldTypeOneToMany, - OneToManyOptions, -} from "../../../../../../shell/components/FieldTypeOneToMany"; +import { searchItems } from "../../../../../../shell/store/content"; + import { AppState } from "../../../../../../shell/store/types"; import { FieldSettingsOptions } from "../../../../../../shell/services/types"; -import { FieldTypeOneToOne } from "../../../../../../shell/components/FieldTypeOneToOne"; -import { - resolveRelatedOptions, - sortHTML, -} from "../../../../../content-editor/src/app/components/Editor/Field/Field"; -import { fetchFields } from "../../../../../../shell/store/fields"; +import { sortHTML } from "../../../../../content-editor/src/app/components/Editor/Field/Field"; import { FieldTypeInternalLink } from "../../../../../../shell/components/FieldTypeInternalLink"; import { LinkOption } from "../../../../../content-editor/src/app/components/Editor/Field/LinkOption"; import { FieldTypeNumber } from "../../../../../../shell/components/FieldTypeNumber"; @@ -49,6 +38,7 @@ import { FieldTypeDate } from "../../../../../../shell/components/FieldTypeDate" import { FieldTypeDateTime } from "../../../../../../shell/components/FieldTypeDateTime"; import { FieldTypeColor } from "../../../../../../shell/components/FieldTypeColor"; import { FieldTypeSort } from "../../../../../../shell/components/FieldTypeSort"; +import { RelationalFieldBase } from "../../../../../../shell/components/RelationalFieldBase"; import moment from "moment"; type DefaultValueInputProps = { @@ -311,170 +301,24 @@ export const DefaultValueInput = ({ ); case "one_to_one": - const onOneToOneOpen = () => { - return dispatch( - fetchItems(relatedModelZUID, { - lang: "en-US", - }) - ); - }; - - let oneToOneOptions: OneToManyOptions[] = useMemo(() => { - const filterValidItems = (items: any) => { - // remove items that are only saved in memory - const filteredValidItems = Object.entries(items).filter( - ([, value]) => value.web.version - ); - // Reshape the array back into an object - let options = Object.fromEntries(filteredValidItems); - - return options; - }; - const options = filterValidItems(allItems); - - return [ - { - inputLabel: "- None -", - value: null, - component: "- None -", - }, - ...resolveRelatedOptions( - allFields, - options, - relatedFieldZUID, - relatedModelZUID, - 1, - value - ), - ]; - }, [ - Object.keys(allFields).length, - Object.keys(allItems).length, - relatedModelZUID, - relatedFieldZUID, - value, - ]); - - if (value && !oneToOneOptions.find((opt) => opt.value === value)) { - //the related option is not in the array, we need to insert it - oneToOneOptions.unshift({ - value: value as string, - inputLabel: `Selected item not found: ${value}`, - component: ( - - evt.stopPropagation()}> - - - - -  Selected item not found: {value} - - ), - }); - } - return ( - options.value === value) || null - } - onChange={(_, option) => onChange(option.value)} - options={oneToOneOptions} - // @ts-ignore - onOpen={onOneToOneOpen} - startAdornment={ - value && ( - - - - ) - } - endAdornment={value && en-US} - error={error} + onChange(value)} /> ); case "one_to_many": - const oneToManyOptions: OneToManyOptions[] = useMemo(() => { - const filterValidItems = (items: any) => { - // remove items that are only saved in memory - const filteredValidItems = Object.entries(items).filter( - ([, value]) => value.web.version - ); - // Reshape the array back into an object - let options = Object.fromEntries(filteredValidItems); - - return options; - }; - const options = filterValidItems(allItems); - - return resolveRelatedOptions( - allFields, - options, - relatedFieldZUID, - relatedModelZUID, - 1, - value - ); - }, [ - Object.keys(allFields).length, - Object.keys(allItems).length, - relatedModelZUID, - relatedFieldZUID, - 1, - value, - ]); - const onOneToManyOpen = useCallback(() => { - return Promise.all([ - dispatch(fetchFields(relatedModelZUID)), - dispatch( - fetchItems(relatedModelZUID, { - lang: "en-US", - }) - ), - ]); - }, [relatedModelZUID]); - return ( - - oneToManyOptions?.find( - (options) => options.value === value - ) || { value, inputLabel: value, component: value } - )) || - [] - } - onChange={(_, options: OneToManyOptions[]) => { - const selectedOptions = options?.length - ? options.map((option) => option.value).join(",") - : null; - onChange(selectedOptions); - }} - options={oneToManyOptions} - onOpen={onOneToManyOpen} - renderTags={(tags, getTagProps) => - tags.map((tag, index) => ( - - )) - } - error={error} + onChange(value)} /> ); case "link": diff --git a/src/apps/schema/src/app/components/AddFieldModal/views/FieldForm.tsx b/src/apps/schema/src/app/components/AddFieldModal/views/FieldForm.tsx index 32c999569..1357f409c 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/views/FieldForm.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/views/FieldForm.tsx @@ -338,12 +338,18 @@ export const FieldForm = ({ let newErrorsObj: Errors = {}; Object.keys(formData).map((inputName) => { - if ( - inputName === "defaultValue" && - isDefaultValueEnabled && - (formData.defaultValue === "" || formData.defaultValue === null) - ) { - newErrorsObj[inputName] = "Required Field. Please enter a value."; + if (inputName === "defaultValue" && isDefaultValueEnabled) { + if (formData.defaultValue === "" || formData.defaultValue === null) { + newErrorsObj[inputName] = "Required Field. Please enter a value."; + } else if ( + type === "one_to_many" && + (formData.defaultValue as string)?.length > 255 + ) { + // Value is stored as string in DB with max char limit of 255. + // This means users can only add up to 12 item zuids + newErrorsObj[inputName] = + "Cannot save field. Please reduce the total number of items selected."; + } } if (type === "text" || type === "textarea") { diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx index 1678851b9..b751486d5 100644 --- a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx @@ -532,6 +532,7 @@ export const FieldSelectorDialog = ({ sx: { width: 800, maxWidth: 800, + minHeight: 680, maxHeight: "min(1240px, calc(100% - 64px))", }, }} diff --git a/src/shell/components/RelationalFieldBase/index.tsx b/src/shell/components/RelationalFieldBase/index.tsx index eccfa7ee2..4f29c23c5 100644 --- a/src/shell/components/RelationalFieldBase/index.tsx +++ b/src/shell/components/RelationalFieldBase/index.tsx @@ -136,7 +136,7 @@ export const RelationalFieldBase = ({ sx={{ mt: 1, }} - disabled={isLoadingModelData || isLoadingModelFields} + disabled={isLoadingModelData || isLoadingModelFields || !modelData} > Add Existing {modelData?.label} From 1b4a85f87b237ff8dd9838e463900f1879827f17 Mon Sep 17 00:00:00 2001 From: Andres Galindo Date: Tue, 11 Feb 2025 12:11:19 -0800 Subject: [PATCH 04/10] Use Local State for Immediate UI Updates with Deferred Global Store Sync (#3193) closes https://github.com/zesty-io/manager-ui/issues/3178 This approach mimics the [useDeferredValue](https://react.dev/reference/react/useDeferredValue ) hook value method that is only available on react 19 - Introduces a local state to handle immediate UI updates for text based controlled inputs. - Ensures smoother user experience by eliminating lag caused by global store updates. - Global state updates are now deferred, preventing excessive store updates on key clicks in order to keep the UI update process free and responsive. - Syncs local state with external value changes to maintain consistency. **This approach ensures real-time user feedback with zero lag** An alternative but much larger scope approach would be to restructure the component tree and optimize the use of selectors to minimize re-renders caused the global store and prop drilling. However it could potentially not reach the zero lag achieved by the deferring approach as it guarantees UI update calls have the highest priority --- .../src/app/components/Editor/Field/Field.tsx | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) 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 5d1bfd130..754624bb7 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 @@ -71,6 +71,7 @@ import { import { ResolvedOption } from "./ResolvedOption"; import { LinkOption } from "./LinkOption"; import { FieldTypeMedia } from "../../FieldTypeMedia"; +import { debounce } from "lodash"; const AIFieldShell = withAI(FieldShell); @@ -203,6 +204,22 @@ export const Field = ({ const value = item?.data?.[name]; const version = item?.meta?.version; const fieldData = fields?.find((field) => field.ZUID === ZUID); + const [inputValue, setInputValue] = useState(value || ""); + + const debouncedOnChange = useMemo(() => debounce(onChange, 300), [onChange]); + + const deferredChange = useCallback( + (value, name) => { + setInputValue(value); + debouncedOnChange(value, name); + }, + [debouncedOnChange] + ); + + // Keep local input value in sync with global field value + useEffect(() => { + setInputValue(value || ""); + }, [value]); useEffect(() => { if (datatype !== "date" && datatype !== "datetime") { @@ -288,7 +305,7 @@ export const Field = ({ ZUID={fieldData?.ZUID} name={fieldData?.name || name} label={fieldData?.label || label} - valueLength={(value as string)?.length ?? 0} + valueLength={(inputValue as string)?.length ?? 0} settings={ fieldData || { name: name, @@ -304,11 +321,11 @@ export const Field = ({ minLength={minLength} errors={errors} aiType="text" - value={value} + value={inputValue} > onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth inputProps={{ name: fieldData?.name || name, @@ -322,12 +339,12 @@ export const Field = ({ return ( onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth error={errors && Object.values(errors)?.some((error) => !!error)} /> @@ -338,14 +355,14 @@ export const Field = ({ return ( onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth type="url" error={errors && Object.values(errors)?.some((error) => !!error)} @@ -377,7 +394,7 @@ export const Field = ({ ZUID={fieldData?.ZUID} name={fieldData?.name} label={fieldData?.label} - valueLength={(value as string)?.length ?? 0} + valueLength={(inputValue as string)?.length ?? 0} settings={fieldData} onChange={(evt: ChangeEvent) => onChange(evt.target.value, name) @@ -387,11 +404,11 @@ export const Field = ({ aiType="word" maxLength={maxLength} minLength={minLength} - value={value} + value={inputValue} > onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth multiline rows={6} @@ -424,7 +441,7 @@ export const Field = ({ name={name} value={value} version={version} - onChange={onChange} + onChange={deferredChange} onSave={onSave} onCharacterCountChange={(charCount: number) => setCharacterCount(charCount) @@ -448,7 +465,7 @@ export const Field = ({ ZUID={fieldData?.ZUID} name={fieldData?.name} label={fieldData?.label} - valueLength={(value as string)?.length ?? 0} + valueLength={(inputValue as string)?.length ?? 0} settings={fieldData} onChange={onChange} errors={errors} @@ -456,14 +473,14 @@ export const Field = ({ datatype={fieldData?.datatype} editorType={editorType} onEditorChange={(value: EditorType) => setEditorType(value)} - value={value} + value={inputValue} > { setImageModal(opts); @@ -918,10 +935,10 @@ export const Field = ({ return ( !!error)} /> @@ -937,8 +954,8 @@ export const Field = ({ !!error)} /> From fc5101fde23bead5a1acdbb9ef035d1ab550f6a1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:35:48 -0800 Subject: [PATCH 05/10] Stage Release (#3201) Created by Github action Co-authored-by: Andres Galindo --- .../src/app/components/Editor/Field/Field.tsx | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) 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 5d1bfd130..754624bb7 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 @@ -71,6 +71,7 @@ import { import { ResolvedOption } from "./ResolvedOption"; import { LinkOption } from "./LinkOption"; import { FieldTypeMedia } from "../../FieldTypeMedia"; +import { debounce } from "lodash"; const AIFieldShell = withAI(FieldShell); @@ -203,6 +204,22 @@ export const Field = ({ const value = item?.data?.[name]; const version = item?.meta?.version; const fieldData = fields?.find((field) => field.ZUID === ZUID); + const [inputValue, setInputValue] = useState(value || ""); + + const debouncedOnChange = useMemo(() => debounce(onChange, 300), [onChange]); + + const deferredChange = useCallback( + (value, name) => { + setInputValue(value); + debouncedOnChange(value, name); + }, + [debouncedOnChange] + ); + + // Keep local input value in sync with global field value + useEffect(() => { + setInputValue(value || ""); + }, [value]); useEffect(() => { if (datatype !== "date" && datatype !== "datetime") { @@ -288,7 +305,7 @@ export const Field = ({ ZUID={fieldData?.ZUID} name={fieldData?.name || name} label={fieldData?.label || label} - valueLength={(value as string)?.length ?? 0} + valueLength={(inputValue as string)?.length ?? 0} settings={ fieldData || { name: name, @@ -304,11 +321,11 @@ export const Field = ({ minLength={minLength} errors={errors} aiType="text" - value={value} + value={inputValue} > onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth inputProps={{ name: fieldData?.name || name, @@ -322,12 +339,12 @@ export const Field = ({ return ( onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth error={errors && Object.values(errors)?.some((error) => !!error)} /> @@ -338,14 +355,14 @@ export const Field = ({ return ( onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth type="url" error={errors && Object.values(errors)?.some((error) => !!error)} @@ -377,7 +394,7 @@ export const Field = ({ ZUID={fieldData?.ZUID} name={fieldData?.name} label={fieldData?.label} - valueLength={(value as string)?.length ?? 0} + valueLength={(inputValue as string)?.length ?? 0} settings={fieldData} onChange={(evt: ChangeEvent) => onChange(evt.target.value, name) @@ -387,11 +404,11 @@ export const Field = ({ aiType="word" maxLength={maxLength} minLength={minLength} - value={value} + value={inputValue} > onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth multiline rows={6} @@ -424,7 +441,7 @@ export const Field = ({ name={name} value={value} version={version} - onChange={onChange} + onChange={deferredChange} onSave={onSave} onCharacterCountChange={(charCount: number) => setCharacterCount(charCount) @@ -448,7 +465,7 @@ export const Field = ({ ZUID={fieldData?.ZUID} name={fieldData?.name} label={fieldData?.label} - valueLength={(value as string)?.length ?? 0} + valueLength={(inputValue as string)?.length ?? 0} settings={fieldData} onChange={onChange} errors={errors} @@ -456,14 +473,14 @@ export const Field = ({ datatype={fieldData?.datatype} editorType={editorType} onEditorChange={(value: EditorType) => setEditorType(value)} - value={value} + value={inputValue} > { setImageModal(opts); @@ -918,10 +935,10 @@ export const Field = ({ return ( !!error)} /> @@ -937,8 +954,8 @@ export const Field = ({ !!error)} /> From 78cf4c8c75d860c8e80c688264b3009570c7ed89 Mon Sep 17 00:00:00 2001 From: Nar -- <28705606+finnar-bin@users.noreply.github.com> Date: Wed, 12 Feb 2025 13:47:34 +0800 Subject: [PATCH 06/10] [Content | Schema] VQA Round 2 updates for the one-to-one/many revamp (#3198) --- .../src/app/components/Editor/Field/Field.tsx | 2 + .../src/app/views/ItemEdit/ItemEdit.js | 5 + .../ItemEditHeader/ItemEditHeaderActions.tsx | 94 +++++++++++-------- .../ItemEditHeader/UnpublishedRelatedItem.tsx | 16 +++- .../components/AddFieldModal/DefaultValue.tsx | 3 + .../AddFieldModal/DefaultValueInput.tsx | 4 + .../components/AddFieldModal/views/Rules.tsx | 1 + src/shell/components/ConfirmPublishModal.tsx | 6 +- .../components/NoSearchResults/index.tsx | 4 +- .../ActiveItem/ActiveItemLoading.tsx | 4 +- .../RelationalFieldBase/ActiveItem/index.tsx | 75 ++++++++++----- .../FieldSelectorDialog/DialogHeader.tsx | 6 +- .../FieldSelectorDialog/index.tsx | 24 ++++- .../components/RelationalFieldBase/index.tsx | 14 ++- 14 files changed, 174 insertions(+), 84 deletions(-) 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 754624bb7..603952dd6 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 @@ -806,6 +806,7 @@ export const Field = ({ relatedModelZUID={relatedModelZUID} relatedFieldZUID={relatedFieldZUID} onChange={onChange} + fieldLabel={fieldData?.label} /> {/** {/** state.headTags, @@ -416,6 +420,7 @@ export default function ItemEdit() { } finally { if (isMounted.current) { setSaving(false); + dispatch(fetchItemPublishings()); } } } diff --git a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx index d7a05a1b9..dafbf137f 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx +++ b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx @@ -89,7 +89,7 @@ export const ItemEditHeaderActions = ({ const [isConfirmPublishModalOpen, setIsConfirmPublishModalOpen] = useState(false); const [relatedItemsToPublish, setRelatedItemsToPublish] = useState< - ContentItem[] + ContentItemWithDirtyAndPublishing[] >([]); const [isPublishing, setIsPublishing] = useState(false); const item = useSelector( @@ -184,10 +184,11 @@ export const ItemEditHeaderActions = ({ if (!!item) { const draftVersion = item?.meta?.version; const publishedVersion = item?.publishing?.version || 0; + const scheduledVersion = item?.scheduling?.version || 0; if ( - draftVersion > publishedVersion && - !item?.scheduling?.isScheduled + draftVersion > publishedVersion || + scheduledVersion > publishedVersion ) { return { ...item, @@ -225,45 +226,60 @@ export const ItemEditHeaderActions = ({ const handlePublish = async () => { setIsPublishing(true); - // If item is scheduled, delete the scheduled publishing first - if (itemState === ITEM_STATES.scheduled) { - await deleteItemPublishing({ - modelZUID, - itemZUID, - publishingZUID: item?.scheduling?.ZUID, - }); - } + try { + // Delete scheduled publishings first + const deleteScheduledPromises = [ + // Delete main item's scheduled publishing if it exists + itemState === ITEM_STATES.scheduled && + deleteItemPublishing({ + modelZUID, + itemZUID, + publishingZUID: item?.scheduling?.ZUID, + }), + // Delete related items' scheduled publishings if they exist + ...relatedItemsToPublish + .filter((item) => !!item.scheduling?.ZUID) + .map((item) => + deleteItemPublishing({ + modelZUID: item.meta.contentModelZUID, + itemZUID: item.meta.ZUID, + publishingZUID: item.scheduling.ZUID, + }) + ), + ].filter((item) => !!item); + + await Promise.all(deleteScheduledPromises); - Promise.allSettled([ - createPublishing({ - modelZUID, - itemZUID, - body: { - version: item?.meta.version, - publishAt: "now", - unpublishAt: "never", - }, - }), - relatedItemsToPublish.map((item) => { - return createPublishing({ - modelZUID: item.meta.contentModelZUID, - itemZUID: item.meta.ZUID, + // Proceed with publishing + await Promise.allSettled([ + createPublishing({ + modelZUID, + itemZUID, body: { - version: item.meta.version, + version: item?.meta.version, publishAt: "now", unpublishAt: "never", }, - }); - }), - ]) - .then(() => { - // Retain non rtk-query fetch of item publishing for legacy code - dispatch(fetchItemPublishings()); - }) - .finally(() => { - setIsPublishing(false); - setIsConfirmPublishModalOpen(false); - }); + }), + ...relatedItemsToPublish.map((item) => + createPublishing({ + modelZUID: item.meta.contentModelZUID, + itemZUID: item.meta.ZUID, + body: { + version: item.meta.version, + publishAt: "now", + unpublishAt: "never", + }, + }) + ), + ]); + + // Retain non rtk-query fetch of item publishing for legacy code + dispatch(fetchItemPublishings()); + } finally { + setIsPublishing(false); + setIsConfirmPublishModalOpen(false); + } }; const handleUnpublish = async () => { @@ -649,7 +665,7 @@ export const ItemEditHeaderActions = ({ Also publish related items - + This will publish all items selected in the list below @@ -657,7 +673,7 @@ export const ItemEditHeaderActions = ({ index + 1} + divider selected={relatedItemsToPublish.some( (i) => i.meta.ZUID === item.meta.ZUID )} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx index e2ed3cf89..072f4419a 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx +++ b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx @@ -23,7 +23,7 @@ type UnpublishedRelatedItemProps = { contentItem: ContentItemWithRelatedZUIDs; onChange: (payload: { action: "add" | "remove"; - contentItem: ContentItem; + contentItem: ContentItemWithDirtyAndPublishing; }) => void; selected: boolean; divider?: boolean; @@ -79,15 +79,27 @@ export const UnpublishedRelatedItem = ({ return ( - + onChange({ contentItem, action: evt.target.checked ? "add" : "remove", }) } + sx={{ + pl: 0, + pr: 2, + "&:hover": { + bgcolor: "transparent", + }, + }} /> {!!imageFieldName && diff --git a/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx b/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx index 0a4d2ac97..71786aa39 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx @@ -28,6 +28,7 @@ type DefaultValueProps = { }; options: FieldSettingsOptions[]; currency?: string; + fieldLabel: string; }; export const DefaultValue = ({ @@ -41,6 +42,7 @@ export const DefaultValue = ({ relationshipFields, options, currency, + fieldLabel, }: DefaultValueProps) => { return ( @@ -91,6 +93,7 @@ export const DefaultValue = ({ relationshipFields={relationshipFields} options={options} currency={currency} + fieldLabel={fieldLabel} /> diff --git a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx index 77f361419..fea9fa710 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx @@ -56,6 +56,7 @@ type DefaultValueInputProps = { }; options: FieldSettingsOptions[]; currency?: string; + fieldLabel: string; }; export const DefaultValueInput = ({ @@ -67,6 +68,7 @@ export const DefaultValueInput = ({ relationshipFields: { relatedModelZUID, relatedFieldZUID }, options, currency, + fieldLabel, }: DefaultValueInputProps) => { const [imageModal, setImageModal] = useState(null); const dispatch = useDispatch(); @@ -308,6 +310,7 @@ export const DefaultValueInput = ({ relatedModelZUID={relatedModelZUID} relatedFieldZUID={relatedFieldZUID} onChange={(value) => onChange(value)} + fieldLabel={fieldLabel} /> ); case "one_to_many": @@ -319,6 +322,7 @@ export const DefaultValueInput = ({ relatedModelZUID={relatedModelZUID} relatedFieldZUID={relatedFieldZUID} onChange={(value) => onChange(value)} + fieldLabel={fieldLabel} /> ); case "link": 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 7a4c95a7b..ee388a338 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx @@ -67,6 +67,7 @@ export const Rules = ({ }} options={formData["options"] as FieldSettingsOptions[]} currency={(formData["currency"] as string) || "USD"} + fieldLabel={formData["label"] as string} /> {type === "images" && ( diff --git a/src/shell/components/ConfirmPublishModal.tsx b/src/shell/components/ConfirmPublishModal.tsx index ef794fcf3..5401b7c08 100644 --- a/src/shell/components/ConfirmPublishModal.tsx +++ b/src/shell/components/ConfirmPublishModal.tsx @@ -12,7 +12,6 @@ import { } from "@mui/material"; import CloudUploadRoundedIcon from "@mui/icons-material/CloudUploadRounded"; import { LoadingButton } from "@mui/lab"; -import pluralizeWord from "../../utility/pluralizeWord"; export type ConfirmPublishModal = { contentTitle: string; @@ -90,10 +89,7 @@ export const ConfirmPublishModal = ({ onClick={onConfirm} data-cy="ConfirmPublishButton" > - Publish{" "} - {!!altText - ? pluralizeWord(altText, relatedItemsToPublishCount) - : pluralizeWord("Item", relatedItemsToPublishCount)}{" "} + Publish {altText || !!relatedItemsToPublishCount ? "Items " : "Item "} {!!relatedItemsToPublishCount && `(${relatedItemsToPublishCount + 1})`} diff --git a/src/shell/components/NoSearchResults/index.tsx b/src/shell/components/NoSearchResults/index.tsx index 3ef5ca94f..428ff93a1 100644 --- a/src/shell/components/NoSearchResults/index.tsx +++ b/src/shell/components/NoSearchResults/index.tsx @@ -18,6 +18,7 @@ type Props = { hideBackButton?: boolean; onSearchAgain?: () => void; imageHeight?: number; + isFilter?: boolean; }; export const NoSearchResults: FC = ({ @@ -26,6 +27,7 @@ export const NoSearchResults: FC = ({ ignoreFilters, hideBackButton, imageHeight = 200, + isFilter, }) => { const history = useHistory(); const [params, setParams] = useParams(); @@ -68,7 +70,7 @@ export const NoSearchResults: FC = ({ "No results that matched your filters could be found" ) : ( <> - Your search + Your {isFilter ? "filter" : "search"} {" "} "{query}"{" "} diff --git a/src/shell/components/RelationalFieldBase/ActiveItem/ActiveItemLoading.tsx b/src/shell/components/RelationalFieldBase/ActiveItem/ActiveItemLoading.tsx index d0200f8e7..b8673f32f 100644 --- a/src/shell/components/RelationalFieldBase/ActiveItem/ActiveItemLoading.tsx +++ b/src/shell/components/RelationalFieldBase/ActiveItem/ActiveItemLoading.tsx @@ -10,7 +10,7 @@ export const ActiveItemLoading = ({ draggable }: ActiveItemLoadingProps) => { direction="row" sx={{ bgcolor: "background.paper", - height: 64, + height: 62, width: "100%", border: 1, borderColor: "border", @@ -20,7 +20,7 @@ export const ActiveItemLoading = ({ draggable }: ActiveItemLoadingProps) => { > {draggable && ( - + )} diff --git a/src/shell/components/RelationalFieldBase/ActiveItem/index.tsx b/src/shell/components/RelationalFieldBase/ActiveItem/index.tsx index 88be97e37..c2c3281ec 100644 --- a/src/shell/components/RelationalFieldBase/ActiveItem/index.tsx +++ b/src/shell/components/RelationalFieldBase/ActiveItem/index.tsx @@ -8,6 +8,7 @@ import { ListItemText, ListItemIcon, IconButton, + Tooltip, } from "@mui/material"; import { DragIndicatorRounded, @@ -67,6 +68,7 @@ export const ActiveItem = memo( const [isPublishModalOpen, setIsPublishModalOpen] = useState(false); const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); const [isCopied, setIsCopied] = useState(false); + const [showTooltip, setShowTooltip] = useState(false); const history = useHistory(); const dispatch = useDispatch(); const domain = useDomain(); @@ -242,7 +244,7 @@ export const ActiveItem = memo( direction="row" sx={{ bgcolor: "background.paper", - height: 64, + height: !!imageFieldName ? 62 : 58, width: "100%", border: 1, borderColor: "border", @@ -250,20 +252,31 @@ export const ActiveItem = memo( alignItems: "center", overflow: "hidden", opacity: isDragging ? 0 : 1, + transform: "translate(0, 0)", }} > {draggable && ( - setShowTooltip(true)} + onClose={() => setShowTooltip(false)} > - - + setShowTooltip(false)} + > + + + )} {!!imageFieldName && (!!imageURL && !imageError ? ( @@ -349,22 +362,36 @@ export const ActiveItem = memo( /> )} - - history.push(`/content/${relatedModelData?.ZUID}/${itemZUID}`) - } - disabled={!contentItem} + - - - setAnchorEl(evt.currentTarget)} + + history.push( + `/content/${relatedModelData?.ZUID}/${itemZUID}` + ) + } + disabled={!contentItem} + > + + + + - - + setAnchorEl(evt.currentTarget)} + > + + + diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/DialogHeader.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/DialogHeader.tsx index 278666128..ab0cd76ae 100644 --- a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/DialogHeader.tsx +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/DialogHeader.tsx @@ -9,7 +9,7 @@ import { CheckRounded, CloseRounded } from "@mui/icons-material"; type DialogHeaderProps = { selectedCount: number; - modelName: string; + fieldLabel: string; onClose: () => void; onDeselectAll: () => void; onDone: () => void; @@ -18,7 +18,7 @@ type DialogHeaderProps = { }; export const DialogHeader = ({ selectedCount, - modelName, + fieldLabel, onClose, onDone, onDeselectAll, @@ -39,7 +39,7 @@ export const DialogHeader = ({ }} > - Select {modelName} + Select {fieldLabel} diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx index b751486d5..4515d2b91 100644 --- a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx @@ -71,19 +71,19 @@ export type FieldFilters = { type FieldSelectorDialogProps = { onClose: () => void; modelZUID: string; - modelName: string; relatedFieldName: string; selectedZUIDs: string[]; onUpdateSelectedZUIDs: (selectedZUIDs: string[]) => void; + fieldLabel: string; multiselect?: boolean; }; export const FieldSelectorDialog = ({ onClose, modelZUID, - modelName, relatedFieldName, selectedZUIDs, onUpdateSelectedZUIDs, + fieldLabel, multiselect, }: FieldSelectorDialogProps) => { const dispatch = useDispatch(); @@ -538,7 +538,7 @@ export const FieldSelectorDialog = ({ }} > {!rows?.length && isFilteringResults ? ( { if (!!filterKeyword) { @@ -660,6 +661,23 @@ export const FieldSelectorDialog = ({ mx: "3px", }, + "& .MuiDataGrid-row.Mui-selected": { + borderBottom: (theme) => + `1px solid ${theme.palette.primary.main}`, + + "& .MuiDataGrid-cell": { + borderBottom: 0, + }, + }, + + "& .MuiDataGrid-cell:focus-within": { + outline: "none", + }, + + ".MuiDataGrid-row": { + cursor: "pointer", + }, + "& [data-field='image']": { p: 0, }, diff --git a/src/shell/components/RelationalFieldBase/index.tsx b/src/shell/components/RelationalFieldBase/index.tsx index 4f29c23c5..14de000f3 100644 --- a/src/shell/components/RelationalFieldBase/index.tsx +++ b/src/shell/components/RelationalFieldBase/index.tsx @@ -21,6 +21,7 @@ import { ActiveItemLoading } from "./ActiveItem/ActiveItemLoading"; type RelationalFieldBaseProps = { name: string; value: string; + fieldLabel: string; relatedModelZUID: string; relatedFieldZUID: string; onChange: (value: string, name: string) => void; @@ -29,6 +30,7 @@ type RelationalFieldBaseProps = { export const RelationalFieldBase = ({ name, value, + fieldLabel, relatedModelZUID, relatedFieldZUID, onChange, @@ -71,10 +73,12 @@ export const RelationalFieldBase = ({ onChange(itemZUIDs?.join(","), name); }, [itemZUIDs]); + const isLoading = isLoadingModelData || isLoadingModelFields; + return ( - {isLoadingModelData || isLoadingModelFields ? ( + {isLoading ? ( [...Array(multiselect ? 5 : 1)].map((_, index) => ( )) @@ -134,11 +138,11 @@ export const RelationalFieldBase = ({ fullWidth onClick={(evt) => setAnchorEl(evt.currentTarget)} sx={{ - mt: 1, + mt: !!itemZUIDs?.length || isLoading ? 1 : 0, }} - disabled={isLoadingModelData || isLoadingModelFields || !modelData} + disabled={isLoading || !modelData} > - Add Existing {modelData?.label} + Add Existing {fieldLabel} )} {!!anchorEl && ( @@ -146,7 +150,7 @@ export const RelationalFieldBase = ({ multiselect={multiselect} onClose={() => setAnchorEl(null)} modelZUID={relatedModelZUID} - modelName={modelData?.label} + fieldLabel={fieldLabel} relatedFieldName={ modelFields?.find((field) => field.ZUID === relatedFieldZUID)?.name } From dd17a0f462cfbea94f6b5ecab3f18ab08608ed8d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:08:21 +0800 Subject: [PATCH 07/10] Stage Release (#3202) Created by Github action --------- Co-authored-by: Andres Galindo Co-authored-by: Nar -- <28705606+finnar-bin@users.noreply.github.com> --- .../src/app/components/Editor/Field/Field.tsx | 2 + .../src/app/views/ItemEdit/ItemEdit.js | 5 + .../ItemEditHeader/ItemEditHeaderActions.tsx | 94 +++++++++++-------- .../ItemEditHeader/UnpublishedRelatedItem.tsx | 16 +++- .../components/AddFieldModal/DefaultValue.tsx | 3 + .../AddFieldModal/DefaultValueInput.tsx | 4 + .../components/AddFieldModal/views/Rules.tsx | 1 + src/shell/components/ConfirmPublishModal.tsx | 6 +- .../components/NoSearchResults/index.tsx | 4 +- .../ActiveItem/ActiveItemLoading.tsx | 4 +- .../RelationalFieldBase/ActiveItem/index.tsx | 75 ++++++++++----- .../FieldSelectorDialog/DialogHeader.tsx | 6 +- .../FieldSelectorDialog/index.tsx | 24 ++++- .../components/RelationalFieldBase/index.tsx | 14 ++- 14 files changed, 174 insertions(+), 84 deletions(-) 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 754624bb7..603952dd6 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 @@ -806,6 +806,7 @@ export const Field = ({ relatedModelZUID={relatedModelZUID} relatedFieldZUID={relatedFieldZUID} onChange={onChange} + fieldLabel={fieldData?.label} /> {/** {/** state.headTags, @@ -416,6 +420,7 @@ export default function ItemEdit() { } finally { if (isMounted.current) { setSaving(false); + dispatch(fetchItemPublishings()); } } } diff --git a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx index d7a05a1b9..dafbf137f 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx +++ b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx @@ -89,7 +89,7 @@ export const ItemEditHeaderActions = ({ const [isConfirmPublishModalOpen, setIsConfirmPublishModalOpen] = useState(false); const [relatedItemsToPublish, setRelatedItemsToPublish] = useState< - ContentItem[] + ContentItemWithDirtyAndPublishing[] >([]); const [isPublishing, setIsPublishing] = useState(false); const item = useSelector( @@ -184,10 +184,11 @@ export const ItemEditHeaderActions = ({ if (!!item) { const draftVersion = item?.meta?.version; const publishedVersion = item?.publishing?.version || 0; + const scheduledVersion = item?.scheduling?.version || 0; if ( - draftVersion > publishedVersion && - !item?.scheduling?.isScheduled + draftVersion > publishedVersion || + scheduledVersion > publishedVersion ) { return { ...item, @@ -225,45 +226,60 @@ export const ItemEditHeaderActions = ({ const handlePublish = async () => { setIsPublishing(true); - // If item is scheduled, delete the scheduled publishing first - if (itemState === ITEM_STATES.scheduled) { - await deleteItemPublishing({ - modelZUID, - itemZUID, - publishingZUID: item?.scheduling?.ZUID, - }); - } + try { + // Delete scheduled publishings first + const deleteScheduledPromises = [ + // Delete main item's scheduled publishing if it exists + itemState === ITEM_STATES.scheduled && + deleteItemPublishing({ + modelZUID, + itemZUID, + publishingZUID: item?.scheduling?.ZUID, + }), + // Delete related items' scheduled publishings if they exist + ...relatedItemsToPublish + .filter((item) => !!item.scheduling?.ZUID) + .map((item) => + deleteItemPublishing({ + modelZUID: item.meta.contentModelZUID, + itemZUID: item.meta.ZUID, + publishingZUID: item.scheduling.ZUID, + }) + ), + ].filter((item) => !!item); + + await Promise.all(deleteScheduledPromises); - Promise.allSettled([ - createPublishing({ - modelZUID, - itemZUID, - body: { - version: item?.meta.version, - publishAt: "now", - unpublishAt: "never", - }, - }), - relatedItemsToPublish.map((item) => { - return createPublishing({ - modelZUID: item.meta.contentModelZUID, - itemZUID: item.meta.ZUID, + // Proceed with publishing + await Promise.allSettled([ + createPublishing({ + modelZUID, + itemZUID, body: { - version: item.meta.version, + version: item?.meta.version, publishAt: "now", unpublishAt: "never", }, - }); - }), - ]) - .then(() => { - // Retain non rtk-query fetch of item publishing for legacy code - dispatch(fetchItemPublishings()); - }) - .finally(() => { - setIsPublishing(false); - setIsConfirmPublishModalOpen(false); - }); + }), + ...relatedItemsToPublish.map((item) => + createPublishing({ + modelZUID: item.meta.contentModelZUID, + itemZUID: item.meta.ZUID, + body: { + version: item.meta.version, + publishAt: "now", + unpublishAt: "never", + }, + }) + ), + ]); + + // Retain non rtk-query fetch of item publishing for legacy code + dispatch(fetchItemPublishings()); + } finally { + setIsPublishing(false); + setIsConfirmPublishModalOpen(false); + } }; const handleUnpublish = async () => { @@ -649,7 +665,7 @@ export const ItemEditHeaderActions = ({ Also publish related items - + This will publish all items selected in the list below @@ -657,7 +673,7 @@ export const ItemEditHeaderActions = ({ index + 1} + divider selected={relatedItemsToPublish.some( (i) => i.meta.ZUID === item.meta.ZUID )} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx index e2ed3cf89..072f4419a 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx +++ b/src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/UnpublishedRelatedItem.tsx @@ -23,7 +23,7 @@ type UnpublishedRelatedItemProps = { contentItem: ContentItemWithRelatedZUIDs; onChange: (payload: { action: "add" | "remove"; - contentItem: ContentItem; + contentItem: ContentItemWithDirtyAndPublishing; }) => void; selected: boolean; divider?: boolean; @@ -79,15 +79,27 @@ export const UnpublishedRelatedItem = ({ return ( - + onChange({ contentItem, action: evt.target.checked ? "add" : "remove", }) } + sx={{ + pl: 0, + pr: 2, + "&:hover": { + bgcolor: "transparent", + }, + }} /> {!!imageFieldName && diff --git a/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx b/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx index 0a4d2ac97..71786aa39 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx @@ -28,6 +28,7 @@ type DefaultValueProps = { }; options: FieldSettingsOptions[]; currency?: string; + fieldLabel: string; }; export const DefaultValue = ({ @@ -41,6 +42,7 @@ export const DefaultValue = ({ relationshipFields, options, currency, + fieldLabel, }: DefaultValueProps) => { return ( @@ -91,6 +93,7 @@ export const DefaultValue = ({ relationshipFields={relationshipFields} options={options} currency={currency} + fieldLabel={fieldLabel} /> diff --git a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx index 77f361419..fea9fa710 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx @@ -56,6 +56,7 @@ type DefaultValueInputProps = { }; options: FieldSettingsOptions[]; currency?: string; + fieldLabel: string; }; export const DefaultValueInput = ({ @@ -67,6 +68,7 @@ export const DefaultValueInput = ({ relationshipFields: { relatedModelZUID, relatedFieldZUID }, options, currency, + fieldLabel, }: DefaultValueInputProps) => { const [imageModal, setImageModal] = useState(null); const dispatch = useDispatch(); @@ -308,6 +310,7 @@ export const DefaultValueInput = ({ relatedModelZUID={relatedModelZUID} relatedFieldZUID={relatedFieldZUID} onChange={(value) => onChange(value)} + fieldLabel={fieldLabel} /> ); case "one_to_many": @@ -319,6 +322,7 @@ export const DefaultValueInput = ({ relatedModelZUID={relatedModelZUID} relatedFieldZUID={relatedFieldZUID} onChange={(value) => onChange(value)} + fieldLabel={fieldLabel} /> ); case "link": 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 7a4c95a7b..ee388a338 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx @@ -67,6 +67,7 @@ export const Rules = ({ }} options={formData["options"] as FieldSettingsOptions[]} currency={(formData["currency"] as string) || "USD"} + fieldLabel={formData["label"] as string} /> {type === "images" && ( diff --git a/src/shell/components/ConfirmPublishModal.tsx b/src/shell/components/ConfirmPublishModal.tsx index ef794fcf3..5401b7c08 100644 --- a/src/shell/components/ConfirmPublishModal.tsx +++ b/src/shell/components/ConfirmPublishModal.tsx @@ -12,7 +12,6 @@ import { } from "@mui/material"; import CloudUploadRoundedIcon from "@mui/icons-material/CloudUploadRounded"; import { LoadingButton } from "@mui/lab"; -import pluralizeWord from "../../utility/pluralizeWord"; export type ConfirmPublishModal = { contentTitle: string; @@ -90,10 +89,7 @@ export const ConfirmPublishModal = ({ onClick={onConfirm} data-cy="ConfirmPublishButton" > - Publish{" "} - {!!altText - ? pluralizeWord(altText, relatedItemsToPublishCount) - : pluralizeWord("Item", relatedItemsToPublishCount)}{" "} + Publish {altText || !!relatedItemsToPublishCount ? "Items " : "Item "} {!!relatedItemsToPublishCount && `(${relatedItemsToPublishCount + 1})`} diff --git a/src/shell/components/NoSearchResults/index.tsx b/src/shell/components/NoSearchResults/index.tsx index 3ef5ca94f..428ff93a1 100644 --- a/src/shell/components/NoSearchResults/index.tsx +++ b/src/shell/components/NoSearchResults/index.tsx @@ -18,6 +18,7 @@ type Props = { hideBackButton?: boolean; onSearchAgain?: () => void; imageHeight?: number; + isFilter?: boolean; }; export const NoSearchResults: FC = ({ @@ -26,6 +27,7 @@ export const NoSearchResults: FC = ({ ignoreFilters, hideBackButton, imageHeight = 200, + isFilter, }) => { const history = useHistory(); const [params, setParams] = useParams(); @@ -68,7 +70,7 @@ export const NoSearchResults: FC = ({ "No results that matched your filters could be found" ) : ( <> - Your search + Your {isFilter ? "filter" : "search"} {" "} "{query}"{" "} diff --git a/src/shell/components/RelationalFieldBase/ActiveItem/ActiveItemLoading.tsx b/src/shell/components/RelationalFieldBase/ActiveItem/ActiveItemLoading.tsx index d0200f8e7..b8673f32f 100644 --- a/src/shell/components/RelationalFieldBase/ActiveItem/ActiveItemLoading.tsx +++ b/src/shell/components/RelationalFieldBase/ActiveItem/ActiveItemLoading.tsx @@ -10,7 +10,7 @@ export const ActiveItemLoading = ({ draggable }: ActiveItemLoadingProps) => { direction="row" sx={{ bgcolor: "background.paper", - height: 64, + height: 62, width: "100%", border: 1, borderColor: "border", @@ -20,7 +20,7 @@ export const ActiveItemLoading = ({ draggable }: ActiveItemLoadingProps) => { > {draggable && ( - + )} diff --git a/src/shell/components/RelationalFieldBase/ActiveItem/index.tsx b/src/shell/components/RelationalFieldBase/ActiveItem/index.tsx index 88be97e37..c2c3281ec 100644 --- a/src/shell/components/RelationalFieldBase/ActiveItem/index.tsx +++ b/src/shell/components/RelationalFieldBase/ActiveItem/index.tsx @@ -8,6 +8,7 @@ import { ListItemText, ListItemIcon, IconButton, + Tooltip, } from "@mui/material"; import { DragIndicatorRounded, @@ -67,6 +68,7 @@ export const ActiveItem = memo( const [isPublishModalOpen, setIsPublishModalOpen] = useState(false); const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); const [isCopied, setIsCopied] = useState(false); + const [showTooltip, setShowTooltip] = useState(false); const history = useHistory(); const dispatch = useDispatch(); const domain = useDomain(); @@ -242,7 +244,7 @@ export const ActiveItem = memo( direction="row" sx={{ bgcolor: "background.paper", - height: 64, + height: !!imageFieldName ? 62 : 58, width: "100%", border: 1, borderColor: "border", @@ -250,20 +252,31 @@ export const ActiveItem = memo( alignItems: "center", overflow: "hidden", opacity: isDragging ? 0 : 1, + transform: "translate(0, 0)", }} > {draggable && ( - setShowTooltip(true)} + onClose={() => setShowTooltip(false)} > - - + setShowTooltip(false)} + > + + + )} {!!imageFieldName && (!!imageURL && !imageError ? ( @@ -349,22 +362,36 @@ export const ActiveItem = memo( /> )} - - history.push(`/content/${relatedModelData?.ZUID}/${itemZUID}`) - } - disabled={!contentItem} + - - - setAnchorEl(evt.currentTarget)} + + history.push( + `/content/${relatedModelData?.ZUID}/${itemZUID}` + ) + } + disabled={!contentItem} + > + + + + - - + setAnchorEl(evt.currentTarget)} + > + + + diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/DialogHeader.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/DialogHeader.tsx index 278666128..ab0cd76ae 100644 --- a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/DialogHeader.tsx +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/DialogHeader.tsx @@ -9,7 +9,7 @@ import { CheckRounded, CloseRounded } from "@mui/icons-material"; type DialogHeaderProps = { selectedCount: number; - modelName: string; + fieldLabel: string; onClose: () => void; onDeselectAll: () => void; onDone: () => void; @@ -18,7 +18,7 @@ type DialogHeaderProps = { }; export const DialogHeader = ({ selectedCount, - modelName, + fieldLabel, onClose, onDone, onDeselectAll, @@ -39,7 +39,7 @@ export const DialogHeader = ({ }} > - Select {modelName} + Select {fieldLabel} diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx index b751486d5..4515d2b91 100644 --- a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx @@ -71,19 +71,19 @@ export type FieldFilters = { type FieldSelectorDialogProps = { onClose: () => void; modelZUID: string; - modelName: string; relatedFieldName: string; selectedZUIDs: string[]; onUpdateSelectedZUIDs: (selectedZUIDs: string[]) => void; + fieldLabel: string; multiselect?: boolean; }; export const FieldSelectorDialog = ({ onClose, modelZUID, - modelName, relatedFieldName, selectedZUIDs, onUpdateSelectedZUIDs, + fieldLabel, multiselect, }: FieldSelectorDialogProps) => { const dispatch = useDispatch(); @@ -538,7 +538,7 @@ export const FieldSelectorDialog = ({ }} > {!rows?.length && isFilteringResults ? ( { if (!!filterKeyword) { @@ -660,6 +661,23 @@ export const FieldSelectorDialog = ({ mx: "3px", }, + "& .MuiDataGrid-row.Mui-selected": { + borderBottom: (theme) => + `1px solid ${theme.palette.primary.main}`, + + "& .MuiDataGrid-cell": { + borderBottom: 0, + }, + }, + + "& .MuiDataGrid-cell:focus-within": { + outline: "none", + }, + + ".MuiDataGrid-row": { + cursor: "pointer", + }, + "& [data-field='image']": { p: 0, }, diff --git a/src/shell/components/RelationalFieldBase/index.tsx b/src/shell/components/RelationalFieldBase/index.tsx index 4f29c23c5..14de000f3 100644 --- a/src/shell/components/RelationalFieldBase/index.tsx +++ b/src/shell/components/RelationalFieldBase/index.tsx @@ -21,6 +21,7 @@ import { ActiveItemLoading } from "./ActiveItem/ActiveItemLoading"; type RelationalFieldBaseProps = { name: string; value: string; + fieldLabel: string; relatedModelZUID: string; relatedFieldZUID: string; onChange: (value: string, name: string) => void; @@ -29,6 +30,7 @@ type RelationalFieldBaseProps = { export const RelationalFieldBase = ({ name, value, + fieldLabel, relatedModelZUID, relatedFieldZUID, onChange, @@ -71,10 +73,12 @@ export const RelationalFieldBase = ({ onChange(itemZUIDs?.join(","), name); }, [itemZUIDs]); + const isLoading = isLoadingModelData || isLoadingModelFields; + return ( - {isLoadingModelData || isLoadingModelFields ? ( + {isLoading ? ( [...Array(multiselect ? 5 : 1)].map((_, index) => ( )) @@ -134,11 +138,11 @@ export const RelationalFieldBase = ({ fullWidth onClick={(evt) => setAnchorEl(evt.currentTarget)} sx={{ - mt: 1, + mt: !!itemZUIDs?.length || isLoading ? 1 : 0, }} - disabled={isLoadingModelData || isLoadingModelFields || !modelData} + disabled={isLoading || !modelData} > - Add Existing {modelData?.label} + Add Existing {fieldLabel} )} {!!anchorEl && ( @@ -146,7 +150,7 @@ export const RelationalFieldBase = ({ multiselect={multiselect} onClose={() => setAnchorEl(null)} modelZUID={relatedModelZUID} - modelName={modelData?.label} + fieldLabel={fieldLabel} relatedFieldName={ modelFields?.find((field) => field.ZUID === relatedFieldZUID)?.name } From 0013f0343ebbfe90f1e5bc8e189e8ece9e3cb97c Mon Sep 17 00:00:00 2001 From: Nar -- <28705606+finnar-bin@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:32:57 +0800 Subject: [PATCH 08/10] Handle all failed network request errors (#3214) Add handling for all failed network requests to clean up the sentry log Fixes #3204 --- src/shell/store/content.js | 28 ++++++++++++++++++++++++++++ src/shell/store/fields.js | 8 ++++++++ src/shell/store/headTags.js | 8 ++++++++ src/shell/store/media.js | 34 ++++++++++++++++++++++++++++++++++ src/shell/store/models.js | 8 ++++++++ 5 files changed, 86 insertions(+) diff --git a/src/shell/store/content.js b/src/shell/store/content.js index 33cbfb587..acff43c44 100644 --- a/src/shell/store/content.js +++ b/src/shell/store/content.js @@ -303,6 +303,14 @@ export function searchItems( } return res; }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to search item: ${err?.message || err || ""}`, + }) + ); + }, }); }; } @@ -915,6 +923,16 @@ export function fetchItemPublishing(modelZUID, itemZUID) { ); } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch item publishing: ${ + err?.message || err || "" + }`, + }) + ); + }, }); }; } @@ -941,6 +959,16 @@ export function fetchItemPublishings() { ); } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch item publishings: ${ + err?.message || err || "" + }`, + }) + ); + }, }); }; } diff --git a/src/shell/store/fields.js b/src/shell/store/fields.js index fa6c2977a..0144ecef7 100644 --- a/src/shell/store/fields.js +++ b/src/shell/store/fields.js @@ -114,6 +114,14 @@ export function fetchField(modelZUID, fieldZUID) { }, }); }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch field: ${err?.message || err || ""}`, + }) + ); + }, }); }; } diff --git a/src/shell/store/headTags.js b/src/shell/store/headTags.js index 34904dda9..5b7a93f09 100644 --- a/src/shell/store/headTags.js +++ b/src/shell/store/headTags.js @@ -126,6 +126,14 @@ export const fetchHeadTags = () => { } } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch head tags: ${err?.message || err || ""}`, + }) + ); + }, }); }; }; diff --git a/src/shell/store/media.js b/src/shell/store/media.js index b36840716..d80b99fca 100644 --- a/src/shell/store/media.js +++ b/src/shell/store/media.js @@ -422,6 +422,14 @@ function fetchGroups(binZUID) { throw res; } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch groups: ${err?.message || err || ""}`, + }) + ); + }, }); }; } @@ -521,6 +529,14 @@ export function fetchBinFiles(binZUID) { throw res; } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch bin files: ${err?.message || err || ""}`, + }) + ); + }, }); }; } @@ -543,6 +559,16 @@ export function fetchGroupFiles(groupZUID) { throw res; } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch group files: ${ + err?.message || err || "" + }`, + }) + ); + }, }); }; } @@ -769,6 +795,14 @@ export function searchFiles(term) { throw res; } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to search for file: ${err?.message || err || ""}`, + }) + ); + }, }); }; } diff --git a/src/shell/store/models.js b/src/shell/store/models.js index beef38952..30e6b48c2 100644 --- a/src/shell/store/models.js +++ b/src/shell/store/models.js @@ -119,6 +119,14 @@ export function fetchModel(modelZUID) { } } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch model: ${err?.message || err || ""}`, + }) + ); + }, }); }; } From b13ca61d7f12550d9f04860ba39e65e4ffd63050 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:33:23 +0800 Subject: [PATCH 09/10] Stage Release (#3215) Created by Github action --------- Co-authored-by: Andres Galindo Co-authored-by: Nar -- <28705606+finnar-bin@users.noreply.github.com> --- src/shell/store/content.js | 28 ++++++++++++++++++++++++++++ src/shell/store/fields.js | 8 ++++++++ src/shell/store/headTags.js | 8 ++++++++ src/shell/store/media.js | 34 ++++++++++++++++++++++++++++++++++ src/shell/store/models.js | 8 ++++++++ 5 files changed, 86 insertions(+) diff --git a/src/shell/store/content.js b/src/shell/store/content.js index 33cbfb587..acff43c44 100644 --- a/src/shell/store/content.js +++ b/src/shell/store/content.js @@ -303,6 +303,14 @@ export function searchItems( } return res; }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to search item: ${err?.message || err || ""}`, + }) + ); + }, }); }; } @@ -915,6 +923,16 @@ export function fetchItemPublishing(modelZUID, itemZUID) { ); } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch item publishing: ${ + err?.message || err || "" + }`, + }) + ); + }, }); }; } @@ -941,6 +959,16 @@ export function fetchItemPublishings() { ); } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch item publishings: ${ + err?.message || err || "" + }`, + }) + ); + }, }); }; } diff --git a/src/shell/store/fields.js b/src/shell/store/fields.js index fa6c2977a..0144ecef7 100644 --- a/src/shell/store/fields.js +++ b/src/shell/store/fields.js @@ -114,6 +114,14 @@ export function fetchField(modelZUID, fieldZUID) { }, }); }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch field: ${err?.message || err || ""}`, + }) + ); + }, }); }; } diff --git a/src/shell/store/headTags.js b/src/shell/store/headTags.js index 34904dda9..5b7a93f09 100644 --- a/src/shell/store/headTags.js +++ b/src/shell/store/headTags.js @@ -126,6 +126,14 @@ export const fetchHeadTags = () => { } } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch head tags: ${err?.message || err || ""}`, + }) + ); + }, }); }; }; diff --git a/src/shell/store/media.js b/src/shell/store/media.js index b36840716..d80b99fca 100644 --- a/src/shell/store/media.js +++ b/src/shell/store/media.js @@ -422,6 +422,14 @@ function fetchGroups(binZUID) { throw res; } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch groups: ${err?.message || err || ""}`, + }) + ); + }, }); }; } @@ -521,6 +529,14 @@ export function fetchBinFiles(binZUID) { throw res; } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch bin files: ${err?.message || err || ""}`, + }) + ); + }, }); }; } @@ -543,6 +559,16 @@ export function fetchGroupFiles(groupZUID) { throw res; } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch group files: ${ + err?.message || err || "" + }`, + }) + ); + }, }); }; } @@ -769,6 +795,14 @@ export function searchFiles(term) { throw res; } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to search for file: ${err?.message || err || ""}`, + }) + ); + }, }); }; } diff --git a/src/shell/store/models.js b/src/shell/store/models.js index beef38952..30e6b48c2 100644 --- a/src/shell/store/models.js +++ b/src/shell/store/models.js @@ -119,6 +119,14 @@ export function fetchModel(modelZUID) { } } }, + error: (err) => { + dispatch( + notify({ + kind: "warn", + message: `Failed to fetch model: ${err?.message || err || ""}`, + }) + ); + }, }); }; } From 3d6583c2734275119cf4e2ae090a614c8a75a590 Mon Sep 17 00:00:00 2001 From: Andres Galindo Date: Thu, 13 Feb 2025 16:37:18 -0800 Subject: [PATCH 10/10] Add whitelisted emails (#3218) --- src/utility/isZestyEmail.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/utility/isZestyEmail.ts b/src/utility/isZestyEmail.ts index cf83ad465..85f5b6804 100644 --- a/src/utility/isZestyEmail.ts +++ b/src/utility/isZestyEmail.ts @@ -1,3 +1,12 @@ +import { MD5 } from "./md5"; + export function isZestyEmail(email: string): boolean { - return email.endsWith("@zesty.io"); + const hashedWhitelistedEmails = [ + "f6b705ad0f149b40abe7ab939d6f2cc4", + "91de2eca00c7588dbd5b477751a68e39", + ]; + + return ( + email.endsWith("@zesty.io") || hashedWhitelistedEmails.includes(MD5(email)) + ); }