diff --git a/src/apps/schema/src/appRevamp/components/AddFieldModal/FieldFormInput.tsx b/src/apps/schema/src/appRevamp/components/AddFieldModal/FieldFormInput.tsx index a898cd6b83..12a798c7c6 100644 --- a/src/apps/schema/src/appRevamp/components/AddFieldModal/FieldFormInput.tsx +++ b/src/apps/schema/src/appRevamp/components/AddFieldModal/FieldFormInput.tsx @@ -9,6 +9,8 @@ import { InputBase, } from "@mui/material"; +import { FormValue } from "./views/FieldForm"; + type FieldType = "input" | "checkbox" | "dropdown"; export interface InputField { name: string; @@ -22,18 +24,14 @@ export interface InputField { interface Props { fieldConfig: InputField; errorMsg?: string; - onDataChange: ({ - name, - value, - }: { - name: string; - value: string | boolean; - }) => void; + onDataChange: ({ name, value }: { name: string; value: FormValue }) => void; + prefillData?: FormValue; } export const FieldFormInput = ({ fieldConfig, errorMsg, onDataChange, + prefillData, }: Props) => { return ( @@ -78,6 +76,7 @@ export const FieldFormInput = ({ value: e.target.value, }); }} + value={prefillData} /> {errorMsg && ( @@ -100,6 +99,7 @@ export const FieldFormInput = ({ value: e.target.checked, }); }} + checked={Boolean(prefillData)} /> } label={ diff --git a/src/apps/schema/src/appRevamp/components/AddFieldModal/index.tsx b/src/apps/schema/src/appRevamp/components/AddFieldModal/index.tsx index ada7d92fd7..ad0067446b 100644 --- a/src/apps/schema/src/appRevamp/components/AddFieldModal/index.tsx +++ b/src/apps/schema/src/appRevamp/components/AddFieldModal/index.tsx @@ -1,21 +1,33 @@ -import { Dispatch, SetStateAction, useState } from "react"; +import { Dispatch, SetStateAction, useState, useMemo } from "react"; +import { useParams } from "react-router"; import { Dialog } from "@mui/material"; import { FieldSelection } from "./views/FieldSelection"; import { FieldForm } from "./views/FieldForm"; -import { ContentModelField } from "../../../../../../shell/services/types"; +import { useGetContentModelFieldsQuery } from "../../../../../../shell/services/instance"; +type Params = { + id: string; + fieldId: string; +}; export type ViewMode = "fields_list" | "new_field" | "update_field"; interface Props { onModalClose: Dispatch>; - fields: ContentModelField[]; + mode: ViewMode; } -export const AddFieldModal = ({ onModalClose, fields }: Props) => { - const [viewMode, setViewMode] = useState("fields_list"); +export const AddFieldModal = ({ onModalClose, mode }: Props) => { + const [viewMode, setViewMode] = useState(mode); const [selectedField, setSelectedField] = useState({ fieldType: "", fieldName: "", }); + const params = useParams(); + const { id, fieldId } = params; + const { data: fields } = useGetContentModelFieldsQuery(id); + + const fieldData = useMemo(() => { + return fields?.find((field) => field.ZUID === fieldId); + }, [fieldId, fields]); const handleFieldClick = (fieldType: string, fieldName: string) => { setViewMode("new_field"); @@ -57,6 +69,16 @@ export const AddFieldModal = ({ onModalClose, fields }: Props) => { onFieldCreationSuccesssful={() => onModalClose(false)} /> )} + {viewMode === "update_field" && ( + onModalClose(false)} + onFieldCreationSuccesssful={() => onModalClose(false)} + fieldData={fieldData} + /> + )} ); }; diff --git a/src/apps/schema/src/appRevamp/components/AddFieldModal/views/FieldForm.tsx b/src/apps/schema/src/appRevamp/components/AddFieldModal/views/FieldForm.tsx index 50bf537195..0c41934884 100644 --- a/src/apps/schema/src/appRevamp/components/AddFieldModal/views/FieldForm.tsx +++ b/src/apps/schema/src/appRevamp/components/AddFieldModal/views/FieldForm.tsx @@ -10,8 +10,10 @@ import { Tabs, Tab, Button, + CircularProgress, } from "@mui/material"; -import { snakeCase } from "lodash"; +import LoadingButton from "@mui/lab/LoadingButton"; +import { snakeCase, isEmpty } from "lodash"; import CloseIcon from "@mui/icons-material/Close"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; @@ -20,8 +22,15 @@ import AddRoundedIcon from "@mui/icons-material/AddRounded"; import { FieldIcon } from "../../Field/FieldIcon"; import { stringStartsWithVowel } from "../../utils"; import { InputField, FieldFormInput } from "../FieldFormInput"; -import { useCreateContentModelFieldMutation } from "../../../../../../../shell/services/instance"; -import { ContentModelField } from "../../../../../../../shell/services/types"; +import { + useCreateContentModelFieldMutation, + useUpdateContentModelFieldMutation, +} from "../../../../../../../shell/services/instance"; +import { + ContentModelField, + FieldSettings, + ContentModelFieldValue, +} from "../../../../../../../shell/services/types"; const commonFields: InputField[] = [ { @@ -88,8 +97,9 @@ type ActiveTab = "details" | "rules"; type Params = { id: string; }; +export type FormValue = Exclude; interface FormData { - [key: string]: string | boolean; + [key: string]: FormValue; } interface Errors { [key: string]: string; @@ -98,9 +108,10 @@ interface Props { type: string; name: string; onModalClose: () => void; - onBackClick: () => void; + onBackClick?: () => void; fields: ContentModelField[]; onFieldCreationSuccesssful: () => void; + fieldData?: ContentModelField; } export const FieldForm = ({ type, @@ -109,6 +120,7 @@ export const FieldForm = ({ onBackClick, fields, onFieldCreationSuccesssful, + fieldData, }: Props) => { const [activeTab, setActiveTab] = useState("details"); const [isSubmitClicked, setIsSubmitClicked] = useState(false); @@ -116,31 +128,54 @@ export const FieldForm = ({ const [formData, setFormData] = useState({}); const params = useParams(); const { id } = params; - const [createContentModelField, { isLoading, isSuccess }] = - useCreateContentModelFieldMutation(); + const [ + createContentModelField, + { isLoading: isCreatingField, isSuccess: isFieldCreated }, + ] = useCreateContentModelFieldMutation(); + const [ + updateContentModelField, + { isLoading: isUpdatingField, isSuccess: isFieldUpdated }, + ] = useUpdateContentModelFieldMutation(); + const isUpdateField = !isEmpty(fieldData); useEffect(() => { - let formFields: { [key: string]: string | boolean } = {}; + let formFields: { [key: string]: FormValue } = {}; let errors: { [key: string]: string } = {}; - formConfig[type].forEach((field) => { - formFields[field.name] = field.type === "checkbox" ? false : ""; + formConfig[type]?.forEach((field) => { + if (isUpdateField) { + if (field.name === "list") { + formFields[field.name] = fieldData.settings[field.name]; + } else { + formFields[field.name] = fieldData[field.name] as FormValue; + } - if (field.required) { - errors[field.name] = "This field is required"; + // Pre-fill error messages based on content + if (field.required) { + errors[field.name] = isEmpty(fieldData[field.name]) + ? "This field is required" + : ""; + } + } else { + formFields[field.name] = field.type === "checkbox" ? false : ""; + + // Pre-fill required fields error msgs + if (field.required) { + errors[field.name] = "This field is required"; + } } }); setFormData(formFields); setErrors(errors); - }, [type]); + }, [type, fieldData]); useEffect(() => { // TODO: Field creation flow is not yet completed, closing modal on success for now - if (isSuccess) { + if (isFieldCreated || isFieldUpdated) { onFieldCreationSuccesssful(); } - }, [isSuccess]); + }, [isFieldCreated, isFieldUpdated]); const handleSubmitForm = () => { setIsSubmitClicked(true); @@ -163,10 +198,23 @@ export const FieldForm = ({ settings: { list: formData.list as boolean, }, - sort: fields?.length, // Just use the length since sort starts at 0 + sort: isUpdateField ? fieldData.sort : fields?.length, // Just use the length since sort starts at 0 }; - createContentModelField({ modelZUID: id, body }); + if (isUpdateField) { + const updateBody: ContentModelField = { + ...fieldData, + ...body, + }; + + updateContentModelField({ + modelZUID: id, + fieldZUID: fieldData.ZUID, + body: updateBody, + }); + } else { + createContentModelField({ modelZUID: id, body }); + } }; const handleFieldDataChange = ({ @@ -185,9 +233,17 @@ export const FieldForm = ({ let errorMsg = value ? "" : "This field is required"; if (value && name === "name") { - errorMsg = currFieldNames.includes(value as string) - ? "Field name already exists" - : ""; + if (isUpdateField) { + // Re-using its original name is fine when updating a field + errorMsg = + currFieldNames.includes(value as string) && value !== fieldData.name + ? "Field name already exists" + : ""; + } else { + errorMsg = currFieldNames.includes(value as string) + ? "Field name already exists" + : ""; + } } if (name in errors) { @@ -217,9 +273,11 @@ export const FieldForm = ({ pb={0.5} > - - - + {!isUpdateField && ( + + + + )} - {headerText} + {isUpdateField ? fieldData.label : headerText} @@ -251,13 +309,14 @@ export const FieldForm = ({ > {activeTab === "details" && ( <> - {formConfig[type].map((fieldConfig, index) => { + {formConfig[type]?.map((fieldConfig, index) => { return ( ); })} @@ -290,13 +349,13 @@ export const FieldForm = ({ > Add another field - + diff --git a/src/apps/schema/src/appRevamp/components/Field/index.tsx b/src/apps/schema/src/appRevamp/components/Field/index.tsx index 66863e96c4..7a561d937e 100644 --- a/src/apps/schema/src/appRevamp/components/Field/index.tsx +++ b/src/apps/schema/src/appRevamp/components/Field/index.tsx @@ -1,9 +1,25 @@ import React, { useRef, useState, useEffect } from "react"; -import { Box, IconButton, Typography, Button, Tooltip } from "@mui/material"; +import { useLocation, useHistory } from "react-router"; +import { + Box, + IconButton, + Typography, + Button, + Tooltip, + Menu, + MenuList, + MenuItem, + ListItemIcon, + ListItemText, +} from "@mui/material"; import { ContentModelField } from "../../../../../../shell/services/types"; import DragIndicatorRoundedIcon from "@mui/icons-material/DragIndicatorRounded"; import MoreHorizRoundedIcon from "@mui/icons-material/MoreHorizRounded"; import CheckIcon from "@mui/icons-material/Check"; +import ContentCopyRoundedIcon from "@mui/icons-material/ContentCopyRounded"; +import DriveFileRenameOutlineRoundedIcon from "@mui/icons-material/DriveFileRenameOutlineRounded"; +import WidgetsRoundedIcon from "@mui/icons-material/WidgetsRounded"; +import HighlightOffRoundedIcon from "@mui/icons-material/HighlightOffRounded"; import { FieldIcon } from "./FieldIcon"; @@ -53,6 +69,10 @@ export const Field = ({ const [isDragging, setIsDragging] = useState(false); const [isDraggable, setIsDraggable] = useState(false); const [isFieldNameCopied, setIsFieldNameCopied] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const isMenuOpen = Boolean(anchorEl); + const location = useLocation(); + const history = useHistory(); useEffect(() => { let timeoutId: NodeJS.Timeout; @@ -116,6 +136,10 @@ export const Field = ({ } }; + const handleMenuClick = (e: React.MouseEvent) => { + setAnchorEl(e.currentTarget); + }; + const style = { opacity: isDragging ? 0.01 : 1, }; @@ -194,10 +218,49 @@ export const Field = ({ {isFieldNameCopied ? "Copied" : field.name} - {/* TODO: More button click action handler, still pending confirmation from zosh on what will happen */} - + + setAnchorEl(null)} + anchorEl={anchorEl} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + transformOrigin={{ + vertical: "top", + horizontal: "right", + }} + > + + + + + Duplicate Field + + history.push(`${location.pathname}/${field.ZUID}`)} + > + + + + Edit Field + + + + + + Copy ZUID + + + + + + De-activate Field + + ); diff --git a/src/apps/schema/src/appRevamp/components/ModelHeader/index.tsx b/src/apps/schema/src/appRevamp/components/ModelHeader/index.tsx index ec06027221..34c8ac5723 100644 --- a/src/apps/schema/src/appRevamp/components/ModelHeader/index.tsx +++ b/src/apps/schema/src/appRevamp/components/ModelHeader/index.tsx @@ -30,8 +30,7 @@ export const ModelHeader = () => { const { data: models } = useGetContentModelsQuery(); const location = useLocation(); const history = useHistory(); - const { data: fields, isSuccess: isFieldsLoaded } = - useGetContentModelFieldsQuery(id); + const { isSuccess: isFieldsLoaded } = useGetContentModelFieldsQuery(id); const model = models?.find((model) => model.ZUID === id); @@ -84,7 +83,7 @@ export const ModelHeader = () => { }, }, }} - value={location.pathname.split("/").pop()} + value={location.pathname.split("/")[3]} onChange={(event, value) => history.push(`/schema/${model?.ZUID}/${value}`) } @@ -105,7 +104,7 @@ export const ModelHeader = () => { {isAddFieldModalOpen && ( - + )} ); diff --git a/src/apps/schema/src/appRevamp/components/utils.ts b/src/apps/schema/src/appRevamp/components/utils.ts index 656edf8039..30bfea1be1 100644 --- a/src/apps/schema/src/appRevamp/components/utils.ts +++ b/src/apps/schema/src/appRevamp/components/utils.ts @@ -1,4 +1,6 @@ export const stringStartsWithVowel = (string: string): boolean => { + if (!string) return; + const firstLetter = string[0]; return ["a", "e", "i", "o", "u"].includes(firstLetter.toLowerCase()); diff --git a/src/apps/schema/src/appRevamp/views/Model.tsx b/src/apps/schema/src/appRevamp/views/Model.tsx index 64e0b43f98..59127eb553 100644 --- a/src/apps/schema/src/appRevamp/views/Model.tsx +++ b/src/apps/schema/src/appRevamp/views/Model.tsx @@ -1,15 +1,33 @@ import { Box } from "@mui/material"; -import { Redirect, Route, Switch, useParams } from "react-router"; +import { Redirect, Route, Switch, useParams, useHistory } from "react-router"; import { useGetContentModelsQuery } from "../../../../../shell/services/instance"; import { FieldList } from "../components/FieldList"; import { ModelHeader } from "../components/ModelHeader"; +import { AddFieldModal } from "../components/AddFieldModal"; +type Params = { + id: string; +}; export const Model = () => { + const history = useHistory(); + const params = useParams(); + const { id } = params; + return ( } /> + ( + history.push(`/schema/${id}/fields`)} + /> + )} + /> ({ getItemPublishings: builder.query< @@ -246,4 +247,5 @@ export const { useBulkUpdateContentModelFieldMutation, useUpdateContentModelMutation, useCreateContentModelFieldMutation, + useUpdateContentModelFieldMutation, } = instanceApi; diff --git a/src/shell/services/types.ts b/src/shell/services/types.ts index f8ad19981e..b7d9d5d78b 100644 --- a/src/shell/services/types.ts +++ b/src/shell/services/types.ts @@ -147,6 +147,8 @@ export interface FieldSettings { tooltip?: string; } +export type ContentModelFieldValue = string | number | boolean | FieldSettings; + export interface ContentModelField { ZUID: string; contentModelZUID: string; @@ -165,4 +167,5 @@ export interface ContentModelField { relatedFieldZUID?: any; createdAt: string; updatedAt: string; + [key: string]: ContentModelFieldValue; }