diff --git a/cypress/e2e/content/content.spec.js b/cypress/e2e/content/content.spec.js index 84a31e2241..bbcb00f724 100644 --- a/cypress/e2e/content/content.spec.js +++ b/cypress/e2e/content/content.spec.js @@ -536,5 +536,26 @@ describe("Content Specs", () => { 2 ); }); + + it("can create & add new item", () => { + cy.get( + "#12-269a28-1bkm34 [data-cy='create-new-relational-item-button']" + ).click({ + force: true, + }); + + cy.get("#12-d6e4c1d797-sjv628", { retries: 1 }) + .find("input") + .type(`Test Item ${TIMESTAMP}`); + cy.get("#12-aaa5ce87e3-89whjq") + .find("textarea") + .first() + .type(`Test Item ${TIMESTAMP}`); + cy.getBySelector("CreateItemSaveButton").click(); + + cy.get("#12-269a28-1bkm34 [data-cy='active-relational-item']", { + retries: 1, + }).should("have.length", 3); + }); }); }); diff --git a/src/apps/content-editor/src/app/ContentEditor.js b/src/apps/content-editor/src/app/ContentEditor.js index 3dd51afe76..b8e9b77da0 100644 --- a/src/apps/content-editor/src/app/ContentEditor.js +++ b/src/apps/content-editor/src/app/ContentEditor.js @@ -32,6 +32,7 @@ import { ResizableContainer } from "../../../../shell/components/ResizeableConta import { StagedChangesProvider } from "./views/ItemList/StagedChangesContext"; import { SelectedItemsProvider } from "./views/ItemList/SelectedItemsContext"; import { TableSortProvider } from "./views/ItemList/TableSortProvider"; +import { useParams } from "../../../../shell/hooks/useParams"; // Makes sure that other apps using legacy theme does not get affected with the palette export let customTheme = createTheme(legacyTheme, { @@ -101,6 +102,7 @@ export let customTheme = createTheme(legacyTheme, { export default function ContentEditor() { const navContent = useSelector((state) => state.navContent); const dispatch = useDispatch(); + const [params] = useParams(); const [loading, setLoading] = useState(true); @@ -133,14 +135,17 @@ export default function ContentEditor() { ) : (
- - - + {params.get("isDialog") !== "true" && ( + + + + )} +
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 6f03e8c29a..3c392d1910 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 @@ -1,23 +1,16 @@ -import { useMemo, useCallback, useState, useEffect, ChangeEvent } from "react"; +import { useMemo, useState, useEffect, ChangeEvent, useCallback } from "react"; import ReactDOM from "react-dom"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import moment from "moment-timezone"; import zuid from "zuid"; -import { fetchFields } from "../../../../../../../shell/store/fields"; -import { - fetchItems, - searchItems, -} from "../../../../../../../shell/store/content"; +import { searchItems } from "../../../../../../../shell/store/content"; import { EditorType, FieldShell, Error } from "./FieldShell"; import { ToggleButtonGroup, ToggleButton, Box, - Select, - MenuItem, - Chip, TextField, Dialog, IconButton, @@ -27,41 +20,29 @@ import { import CloseIcon from "@mui/icons-material/Close"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faEdit, - faExclamationTriangle, -} from "@fortawesome/free-solid-svg-icons"; +import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons"; // it would be nice to have a central import for all of these // instead of individually importing import { AppLink } from "@zesty-io/core/AppLink"; import { MediaApp } from "../../../../../../media/src/app"; import { FieldTypeUUID } from "../../../../../../../shell/components/FieldTypeUUID"; import { FieldTypeCurrency } from "../../../../../../../shell/components/FieldTypeCurrency"; -import { FieldTypeInternalLink } from "../../../../../../../shell/components/FieldTypeInternalLink"; -import { FieldTypeImage } from "../../../../../../../shell/components/FieldTypeImage"; import { FieldTypeEditor } from "../../../../../../../shell/components/FieldTypeEditor"; import { FieldTypeTinyMCE } from "../../../../../../../shell/components/FieldTypeTinyMCE"; import { FieldTypeColor } from "../../../../../../../shell/components/FieldTypeColor"; -import { - FieldTypeOneToMany, - OneToManyOptions, -} from "../../../../../../../shell/components/FieldTypeOneToMany"; -import { - FieldTypeOneToOne, - OneToOneOptions, -} from "../../../../../../../shell/components/FieldTypeOneToOne"; +import { OneToManyOptions } from "../../../../../../../shell/components/FieldTypeOneToMany"; import { RelationalFieldBase } from "../../../../../../../shell/components/RelationalFieldBase"; import { FieldTypeDate } from "../../../../../../../shell/components/FieldTypeDate"; import { FieldTypeDateTime } from "../../../../../../../shell/components/FieldTypeDateTime"; import { FieldTypeSort } from "../../../../../../../shell/components/FieldTypeSort"; import { FieldTypeNumber } from "../../../../../../../shell/components/FieldTypeNumber"; import { FieldTypeBlockSelector } from "../../../../../../../shell/components/FieldTypeBlockSelector"; +import { InternalLink } from "./InternalLink"; import styles from "./Field.less"; import { MemoryRouter } from "react-router"; import { withAI } from "../../../../../../../shell/components/withAi"; import { useGetContentModelFieldsQuery } from "../../../../../../../shell/services/instance"; -import { AppState } from "../../../../../../../shell/store/types"; import { ContentItem, ContentModelField, @@ -69,7 +50,6 @@ import { Language, } from "../../../../../../../shell/services/types"; import { ResolvedOption } from "./ResolvedOption"; -import { LinkOption } from "./LinkOption"; import { FieldTypeMedia } from "../../FieldTypeMedia"; import { debounce, parseInt } from "lodash"; @@ -153,9 +133,6 @@ export const resolveRelatedOptions = ( .sort((a, b) => (a.inputLabel > b.inputLabel ? 1 : -1)); }; -const getSelectedLang = (langs: Language[], langID: number) => - langs.find((lang: any) => lang.ID === langID).code; - type FieldProps = { ZUID: string; contentModelZUID: string; @@ -193,9 +170,6 @@ export const Field = ({ minLength, }: FieldProps) => { const dispatch = useDispatch(); - const allItems = useSelector((state: AppState) => state.content); - const allFields = useSelector((state: AppState) => state.fields); - const allLanguages = useSelector((state: AppState) => state.languages); const { data: fields } = useGetContentModelFieldsQuery(contentModelZUID); const [imageModal, setImageModal] = useState(null); @@ -665,257 +639,46 @@ export const Field = ({ ); case "internal_link": - let internalLinkRelatedItem = allItems[value]; - let internalLinkOptions = useMemo(() => { - const options = Object.keys(allItems) - .filter( - (itemZUID) => - !itemZUID.includes("new") && // exclude new items - allItems[itemZUID].meta.ZUID && // ensure the item has a zuid - allItems[itemZUID].web.pathPart && // exclude non-routeable items - allItems[itemZUID].meta.langID === langID // exclude non-relevant langs - ) - .map((itemZUID) => { - let item = allItems[itemZUID]; - let html = ""; - - if (item.web.metaTitle) { - html += `${item.web.metaTitle}`; - } else { - return { - component: ( - - ), - }; - } - - if (item.web.path || item.web.pathPart) { - html += `${ - item.web.path || item.web.pathPart - }`; - } - - return { - value: itemZUID, - html: html, - }; - }) - .sort(sortHTML); - - // if the selected item is not found, insert a placeholder - if (internalLinkRelatedItem && !internalLinkRelatedItem?.meta?.ZUID) { - // insert placeholder - options.unshift({ - value: value as string, - html: `Selected item not found: ${value}`, - }); - } - - return options; - }, [internalLinkRelatedItem, Object.keys(allItems).length]); - - const onInternalLinkSearch = useCallback( - (term) => dispatch(searchItems(term)), - [] - ); - return ( - !!error)} + langID={langID} /> ); case "one_to_one": - const onOneToOneOpen = useCallback(() => { - if (zuid.isValid(relatedModelZUID)) { - return dispatch( - fetchItems(relatedModelZUID, { - lang: getSelectedLang(allLanguages, langID), - }) - ); - } else { - return Promise.reject(new Error("Missing modelZUID")); - } - }, [allLanguages.length, relatedModelZUID, langID]); - - let oneToOneOptions: OneToManyOptions[] = useMemo(() => { - const options = filterValidItems(allItems); - - return [ - { - inputLabel: "- None -", - value: null, - component: "- None -", - }, - ...resolveRelatedOptions( - allFields, - options, - relatedFieldZUID, - relatedModelZUID, - langID, - value - ), - ]; - }, [ - Object.keys(allFields).length, - Object.keys(allItems).length, - relatedModelZUID, - relatedFieldZUID, - langID, - 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, name)} - options={oneToOneOptions} - onOpen={onOneToOneOpen} - startAdornment={ - value && ( - - - - ) - } - endAdornment={ - value && {getSelectedLang(allLanguages, langID)} - } - error={errors && Object.values(errors)?.some((error) => !!error)} - /> - */} - + ); case "one_to_many": - const oneToManyOptions: OneToManyOptions[] = useMemo(() => { - const options = filterValidItems(allItems); - - return resolveRelatedOptions( - allFields, - options, - relatedFieldZUID, - relatedModelZUID, - langID, - value - ); - }, [ - Object.keys(allFields).length, - Object.keys(allItems).length, - relatedModelZUID, - relatedFieldZUID, - langID, - value, - ]); - - // Delay loading options until user opens dropdown - const onOneToManyOpen = useCallback(() => { - return Promise.all([ - dispatch(fetchFields(relatedModelZUID)), - dispatch( - fetchItems(relatedModelZUID, { - lang: getSelectedLang(allLanguages, langID), - }) - ), - ]); - }, [allLanguages.length, relatedModelZUID, langID]); - 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)} - /> - */} - + ); diff --git a/src/apps/content-editor/src/app/components/Editor/Field/InternalLink.tsx b/src/apps/content-editor/src/app/components/Editor/Field/InternalLink.tsx new file mode 100644 index 0000000000..9674ff5219 --- /dev/null +++ b/src/apps/content-editor/src/app/components/Editor/Field/InternalLink.tsx @@ -0,0 +1,94 @@ +import { useDispatch, useSelector } from "react-redux"; +import { FieldTypeInternalLink } from "../../../../../../../shell/components/FieldTypeInternalLink"; +import { AppState } from "../../../../../../../shell/store/types"; +import { useCallback, useMemo } from "react"; +import { searchItems } from "../../../../../../../shell/store/content"; +import { LinkOption } from "./LinkOption"; +import { sortHTML } from "./Field"; + +type InternalLinkProps = { + name: string; + value: string; + onChange: (value: any, name: string, datatype?: string) => void; + error: boolean; + langID: number; +}; +export const InternalLink = ({ + name, + value, + onChange, + error, + langID, +}: InternalLinkProps) => { + const dispatch = useDispatch(); + + const allItems = useSelector((state: AppState) => state.content); + let internalLinkRelatedItem = allItems[value]; + let internalLinkOptions = useMemo(() => { + const options = Object.keys(allItems) + .filter( + (itemZUID) => + !itemZUID.includes("new") && // exclude new items + allItems[itemZUID].meta.ZUID && // ensure the item has a zuid + allItems[itemZUID].web.pathPart && // exclude non-routeable items + allItems[itemZUID].meta.langID === langID // exclude non-relevant langs + ) + .map((itemZUID) => { + let item = allItems[itemZUID]; + let html = ""; + + if (item.web.metaTitle) { + html += `${item.web.metaTitle}`; + } else { + return { + component: ( + + ), + }; + } + + if (item.web.path || item.web.pathPart) { + html += `${ + item.web.path || item.web.pathPart + }`; + } + + return { + value: itemZUID, + html: html, + }; + }) + .sort(sortHTML); + + // if the selected item is not found, insert a placeholder + if (internalLinkRelatedItem && !internalLinkRelatedItem?.meta?.ZUID) { + // insert placeholder + options.unshift({ + value: value as string, + html: `Selected item not found: ${value}`, + }); + } + + return options; + }, [internalLinkRelatedItem, Object.keys(allItems).length]); + + const onInternalLinkSearch = useCallback( + (term) => dispatch(searchItems(term)), + [] + ); + + return ( + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemCreate/Header.tsx b/src/apps/content-editor/src/app/views/ItemCreate/Header.tsx index 92e15be99a..d05816917c 100644 --- a/src/apps/content-editor/src/app/views/ItemCreate/Header.tsx +++ b/src/apps/content-editor/src/app/views/ItemCreate/Header.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useContext } from "react"; import { LoadingButton } from "@mui/lab"; import { Box, @@ -11,18 +11,22 @@ import { MenuItem, ListItemIcon, ListItemText, + IconButton, } from "@mui/material"; import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded"; import AddRoundedIcon from "@mui/icons-material/AddRounded"; import SaveRoundedIcon from "@mui/icons-material/SaveRounded"; import CloudUploadRoundedIcon from "@mui/icons-material/CloudUploadRounded"; import CalendarTodayRoundedIcon from "@mui/icons-material/CalendarTodayRounded"; +import { CloseRounded } from "@mui/icons-material"; import { theme } from "@zesty-io/material"; import { useMetaKey } from "../../../../../../shell/hooks/useMetaKey"; import { ContentModel } from "../../../../../../shell/services/types"; import { ContentBreadcrumbs } from "../../components/ContentBreadcrumbs"; import { ActionAfterSave } from "./ItemCreate"; +import { useParams } from "../../../../../../shell/hooks/useParams"; +import { CreateContentItemDialogContext } from "../../../../../../shell/contexts/CreateContentItemDialogProvider"; type DropdownMenuType = "default" | "addNew"; const DropdownMenu: Record> = { @@ -43,11 +47,14 @@ interface Props { isDirty: boolean; } export const Header = ({ model, onSave, isLoading, isDirty }: Props) => { + const [params] = useParams(); const [dropdownMenuType, setDropdownMenuType] = useState(null); const [anchorEl, setAnchorEl] = useState(null); + const [_, setInitiatorZUID] = useContext(CreateContentItemDialogContext); const metaShortcut = useMetaKey("s", onSave); + const isRenderedAsDialog = params.get("isDialog") === "true"; return ( @@ -84,38 +91,40 @@ export const Header = ({ model, onSave, isLoading, isDirty }: Props) => { - - } - onClick={() => { - onSave("addNew"); - }} - loading={isLoading} + {!isRenderedAsDialog && ( + - Create & Add New - - - + } + onClick={() => { + onSave("addNew"); + }} + loading={isLoading} + variant="outlined" + > + Create & Add New + + + + )} { + {isRenderedAsDialog && ( + { + setInitiatorZUID(null); + }} + > + + + )} { const history = useHistory(); const isMounted = useIsMounted(); const dispatch = useDispatch(); + const [_, __, ___, setNewlyCreatedItemZUID] = useContext( + CreateContentItemDialogContext + ); + const [queryParams] = useQueryParams(); + const isRenderedAsDialog = queryParams.get("isDialog") === "true"; const { modelZUID } = useParams<{ modelZUID: string }>(); const itemZUID = `new:${modelZUID}`; const model = useSelector((state: AppState) => state.models[modelZUID]); @@ -79,7 +86,7 @@ export const ItemCreate = () => { const [active, setActive] = useState(); const [newItemZUID, setNewItemZUID] = useState(); const [isScheduleDialogOpen, setIsScheduleDialogOpen] = useState(false); - const [willRedirect, setWillRedirect] = useState(true); + const [willRedirect, setWillRedirect] = useState(false); const [fieldErrors, setFieldErrors] = useState({}); const [saveClicked, setSaveClicked] = useState(false); // const [hasSEOErrors, setHasSEOErrors] = useState(false); @@ -125,7 +132,7 @@ export const ItemCreate = () => { if (!isPublishing && isPublished) { // console.log("will it redirect?", redirect); if (willRedirect) { - history.push(`/content/${modelZUID}/${publishedItem?.data?.itemZUID}`); + handleRedirect(publishedItem?.data?.itemZUID); } } }, [isPublishing, isPublished, publishedItem, willRedirect]); @@ -303,6 +310,7 @@ export const ItemCreate = () => { await dispatch(fetchItem(modelZUID, res.data.ZUID)); setNewItemZUID(res.data.ZUID); + setFieldErrors({}); switch (action) { case "addNew": @@ -336,11 +344,7 @@ export const ItemCreate = () => { default: // Redirect to new item - history.push( - `/${ - model?.type === "block" ? "blocks" : "content" - }/${modelZUID}/${res.data.ZUID}` - ); + handleRedirect(res.data.ZUID); break; } @@ -384,6 +388,18 @@ export const ItemCreate = () => { }); }; + const handleRedirect = (itemZUID: string) => { + if (isRenderedAsDialog) { + setNewlyCreatedItemZUID(itemZUID); + } else { + history.push( + `/${ + model?.type === "block" ? "blocks" : "content" + }/${modelZUID}/${itemZUID}` + ); + } + }; + if (!loading && !model) { return ; } @@ -422,7 +438,7 @@ export const ItemCreate = () => { )} - {model.type === "block" && ( + {model?.type === "block" && ( { setSEOErrors(errors); @@ -452,7 +468,7 @@ export const ItemCreate = () => { setFieldErrors(errors); }} /> - {model.type !== "block" && ( + {model?.type !== "block" && ( { setSEOErrors(errors); @@ -512,13 +528,19 @@ export const ItemCreate = () => { {isScheduleDialogOpen && !isLoadingNewItem && ( setIsScheduleDialogOpen(false)} + onClose={() => { + setIsScheduleDialogOpen(false); + + if (willRedirect) { + handleRedirect(newItemData?.meta?.ZUID); + } + }} onPublishNow={() => handlePublish(newItemZUID)} onScheduleSuccess={() => { setIsScheduleDialogOpen(false); if (willRedirect) { - history.push(`/content/${modelZUID}/${newItemData?.meta?.ZUID}`); + handleRedirect(newItemData?.meta?.ZUID); } }} /> diff --git a/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js b/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js index 5775129598..7fc1325936 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js +++ b/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js @@ -89,7 +89,6 @@ export default function ItemEdit() { const metaRef = useRef(null); const fieldErrorRef = useRef(null); const item = useSelector((state) => state.content[itemZUID]); - const items = useSelector((state) => state.content); const model = useSelector((state) => state.models[modelZUID]); const tags = useSelector((state) => selectItemHeadTags(state, itemZUID)); const languages = useSelector((state) => state.languages); @@ -608,7 +607,6 @@ export default function ItemEdit() { fields={fields} itemZUID={itemZUID} item={item} - items={items} user={user} onSave={() => save().catch((err) => console.error(err)) diff --git a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx index 264424f3a2..0e11585123 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx @@ -307,6 +307,7 @@ export const DefaultValueInput = ({ return ( void; +}; +export const CreateNewItemDialog = ({ + modelZUID, + onClose, +}: CreateNewItemDialogProps) => { + return createPortal( + + + + + , + document.getElementById("modalMount") + ); +}; diff --git a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx index c5358e7f0f..860386e42d 100644 --- a/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx +++ b/src/shell/components/RelationalFieldBase/FieldSelectorDialog/index.tsx @@ -230,10 +230,46 @@ export const FieldSelectorDialog = ({ let _rows = [...contentItems]; - return _rows?.map((item) => ({ - id: item.meta?.ZUID, - // Column is only used for keyword search purposes - keywordSearch: { + return _rows + ?.filter((item) => !item.meta?.ZUID?.startsWith("new")) + ?.map((item) => ({ + id: item.meta?.ZUID, + // Column is only used for keyword search purposes + keywordSearch: { + 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, + }, + }, + image: { + imageFieldName, + itemZUID: item.meta?.ZUID, + }, title: { primary: item.data?.[relatedFieldName] || @@ -263,42 +299,8 @@ export const FieldSelectorDialog = ({ } : null, }, - }, - 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, - })); + item, + })); }, [contentItems, users, relatedFieldName, imageFieldName]); const rows = useMemo(() => { diff --git a/src/shell/components/RelationalFieldBase/index.tsx b/src/shell/components/RelationalFieldBase/index.tsx index 14de000f35..1ebd804297 100644 --- a/src/shell/components/RelationalFieldBase/index.tsx +++ b/src/shell/components/RelationalFieldBase/index.tsx @@ -1,9 +1,10 @@ -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useContext } from "react"; import { Box, Button, Stack } from "@mui/material"; import { LinkRounded, KeyboardArrowUpRounded, KeyboardArrowDownRounded, + AddRounded, } from "@mui/icons-material"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; @@ -17,11 +18,15 @@ import { } from "../../services/instance"; import { fetchItems } from "../../store/content"; import { ActiveItemLoading } from "./ActiveItem/ActiveItemLoading"; +import { CreateNewItemDialog } from "./CreateNewItemDialog"; +import { useParams } from "../../hooks/useParams"; +import { CreateContentItemDialogContext } from "../../contexts/CreateContentItemDialogProvider"; type RelationalFieldBaseProps = { name: string; value: string; fieldLabel: string; + fieldZUID: string; relatedModelZUID: string; relatedFieldZUID: string; onChange: (value: string, name: string) => void; @@ -31,15 +36,23 @@ export const RelationalFieldBase = ({ name, value, fieldLabel, + fieldZUID, relatedModelZUID, relatedFieldZUID, onChange, multiselect, }: RelationalFieldBaseProps) => { const dispatch = useDispatch(); + const [params] = useParams(); const [itemZUIDs, setItemZUIDs] = useState(value?.split(",") || []); const [showAll, setShowAll] = useState(false); const [anchorEl, setAnchorEl] = useState(null); + const [ + initiatorZUID, + setInitiatorZUID, + newlyCreatedItemZUID, + setNewlyCreatedItemZUID, + ] = useContext(CreateContentItemDialogContext); const { data: modelData, isLoading: isLoadingModelData } = useGetContentModelQuery(relatedModelZUID, { @@ -56,6 +69,17 @@ export const RelationalFieldBase = ({ } }, [relatedModelZUID]); + useEffect(() => { + if (!!newlyCreatedItemZUID && initiatorZUID === fieldZUID) { + const newItemZUIDs = [...itemZUIDs, newlyCreatedItemZUID]; + + onChange(!!newItemZUIDs?.length ? newItemZUIDs.join(",") : null, name); + setItemZUIDs(!!newItemZUIDs?.length ? newItemZUIDs : null); + setInitiatorZUID(null); + setNewlyCreatedItemZUID(null); + } + }, [newlyCreatedItemZUID, itemZUIDs, initiatorZUID]); + const handleMoveCard = useCallback( (draggedItemZUID: string, dropIndex: number) => { const draggedIndex = itemZUIDs.indexOf(draggedItemZUID); @@ -73,6 +97,7 @@ export const RelationalFieldBase = ({ onChange(itemZUIDs?.join(","), name); }, [itemZUIDs]); + const isRenderedAsDialog = params.get("isDialog") === "true"; const isLoading = isLoadingModelData || isLoadingModelFields; return ( @@ -130,20 +155,36 @@ export const RelationalFieldBase = ({ )} {(multiselect || (!multiselect && !value)) && ( - + + {multiselect && !isRenderedAsDialog && ( + + )} + )} {!!anchorEl && ( )} + {initiatorZUID === fieldZUID && ( + setInitiatorZUID(null)} + /> + )} ); }; diff --git a/src/shell/contexts/CreateContentItemDialogProvider.tsx b/src/shell/contexts/CreateContentItemDialogProvider.tsx new file mode 100644 index 0000000000..8b5e980b71 --- /dev/null +++ b/src/shell/contexts/CreateContentItemDialogProvider.tsx @@ -0,0 +1,39 @@ +import React, { createContext, useState, Dispatch } from "react"; + +type CreateContentItemDialogContextType = [ + string, + Dispatch, + string, + Dispatch +]; +export const CreateContentItemDialogContext = + createContext([ + null, + () => {}, + null, + () => {}, + ]); + +type CreateContentItemDialogProviderType = { + children?: React.ReactNode; +}; +export const CreateContentItemDialogProvider = ({ + children, +}: CreateContentItemDialogProviderType) => { + const [initiatorZUID, setInitiatorZUID] = useState(null); + const [newlyCreatedItemZUID, setNewlyCreatedItemZUID] = + useState(null); + + return ( + + {children} + + ); +}; diff --git a/src/shell/index.js b/src/shell/index.js index 9c4422b8de..37eec9f30c 100644 --- a/src/shell/index.js +++ b/src/shell/index.js @@ -34,6 +34,7 @@ import Shell from "./views/Shell"; import { MonacoSetup } from "../apps/code-editor/src/app/components/Editor/components/MemoizedEditor/MonacoSetup"; import { actions } from "shell/store/ui"; import { CommentProvider } from "./contexts/CommentProvider"; +import { CreateContentItemDialogProvider } from "./contexts/CreateContentItemDialogProvider"; // needed for Breadcrumbs in Shell injectReducer(store, "navContent", navContent); @@ -60,11 +61,13 @@ const App = Sentry.withProfiler(() => ( - - - - - + + + + + + +