From 3757c583cb010950b866b653264a099d5fef5830 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Wed, 20 Nov 2024 18:52:18 +0100 Subject: [PATCH 1/3] fix(api-headless-cms): use null as default value on multi-value object --- .../src/fieldConverters/CmsModelObjectFieldConverterPlugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api-headless-cms/src/fieldConverters/CmsModelObjectFieldConverterPlugin.ts b/packages/api-headless-cms/src/fieldConverters/CmsModelObjectFieldConverterPlugin.ts index e26525878ca..ba62c9302fa 100644 --- a/packages/api-headless-cms/src/fieldConverters/CmsModelObjectFieldConverterPlugin.ts +++ b/packages/api-headless-cms/src/fieldConverters/CmsModelObjectFieldConverterPlugin.ts @@ -39,7 +39,7 @@ export class CmsModelObjectFieldConverterPlugin extends CmsModelFieldConverterPl if (field.multipleValues) { if (Array.isArray(value) === false) { return { - [field.storageId]: [] + [field.storageId]: null }; } return { @@ -163,7 +163,7 @@ export class CmsModelObjectFieldConverterPlugin extends CmsModelFieldConverterPl if (field.multipleValues) { if (Array.isArray(value) === false) { return { - [field.fieldId]: [] + [field.fieldId]: null }; } return { From bcd88328c0c5794f50221ad4cbb747fbbf9b3545 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Wed, 20 Nov 2024 18:52:42 +0100 Subject: [PATCH 2/3] fix(app-headless-cms): improve multi-value renderers --- .../ContentEntryFormProvider.tsx | 1 + .../components/ContentEntryForm/useBind.tsx | 2 +- .../plugins/fieldRenderers/DynamicSection.tsx | 90 ++++++++---------- .../plugins/fieldRenderers/dateTime/Input.tsx | 2 +- .../dateTime/dateTimeFields.tsx | 11 +-- .../lexicalText/lexicalTextInputs.tsx | 12 +-- .../fieldRenderers/longText/longTexts.tsx | 11 +-- .../fieldRenderers/number/numberInputs.tsx | 11 +-- .../fieldRenderers/object/multipleObjects.tsx | 19 +--- .../object/multipleObjectsAccordion.tsx | 95 +++++++++++-------- .../richText/richTextInputs.tsx | 1 - .../fieldRenderers/text/textInputs.tsx | 11 +-- 12 files changed, 120 insertions(+), 146 deletions(-) diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryFormProvider.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryFormProvider.tsx index ba486fd0571..4fe183aad6b 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryFormProvider.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryFormProvider.tsx @@ -136,6 +136,7 @@ export const ContentEntryFormProvider = ({ onSubmit={onFormSubmit} data={entry} ref={ref} + validateOnFirstSubmit invalidFields={invalidFields} onInvalid={invalidFields => { setInvalidFields(formValidationToMap(invalidFields)); diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/useBind.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/useBind.tsx index 6a44f533707..03440fcae5b 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/useBind.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/useBind.tsx @@ -99,7 +99,7 @@ export function useBind({ Bind, field }: UseBindProps) { let value = bind.value; value = [...value.slice(0, index), ...value.slice(index + 1)]; - bind.onChange(value); + bind.onChange(value.length === 0 ? null : value); // To make sure the field is still valid, we must trigger validation. form.validateInput(field.fieldId); diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx index b4070af96b3..bb0caf415ba 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx @@ -1,27 +1,24 @@ import React from "react"; import classSet from "classnames"; import { css } from "emotion"; +import styled from "@emotion/styled"; import { i18n } from "@webiny/app/i18n"; import { Cell, Grid } from "@webiny/ui/Grid"; +import { Typography } from "@webiny/ui/Typography"; import { ButtonDefault, ButtonIcon } from "@webiny/ui/Button"; -import { BindComponent, BindComponentRenderProp, CmsModelField } from "~/types"; import { FormElementMessage } from "@webiny/ui/FormElementMessage"; import { ReactComponent as AddIcon } from "@webiny/app-admin/assets/icons/add-18px.svg"; import { GetBindCallable } from "~/admin/components/ContentEntryForm/useBind"; import { ParentFieldProvider } from "~/admin/hooks"; import { ParentValueIndexProvider } from "~/admin/components/ModelFieldProvider"; +import { BindComponent, BindComponentRenderProp, CmsModelField } from "~/types"; const t = i18n.ns("app-headless-cms/admin/fields/text"); const style = { gridContainer: css` padding: 0 !important; - `, - addButton: css({ - width: "100%", - borderTop: "1px solid var(--mdc-theme-background)", - paddingTop: 8 - }) + ` }; export interface DynamicSectionPropsChildrenParams { @@ -38,25 +35,34 @@ export interface DynamicSectionProps { field: CmsModelField; getBind: GetBindCallable; showLabel?: boolean; - Label: React.ComponentType; children: (params: DynamicSectionPropsChildrenParams) => JSX.Element; emptyValue?: any; - renderTitle?: (value: any[]) => React.ReactElement; gridClassName?: string; } +const FieldLabel = styled.div` + font-size: 24px; + font-weight: normal; + border-bottom: 1px solid var(--mdc-theme-background); + margin-bottom: 20px; + padding-bottom: 5px; +`; + +const AddButtonCell = styled(Cell)<{ items: number }>` + width: 100%; + padding-top: ${({ items }) => (items > 0 ? "8px" : "0")}; + border-top: ${({ items }) => (items > 0 ? "1px solid var(--mdc-theme-background)" : "none")}; +`; + const DynamicSection = ({ field, getBind, - Label, children, showLabel = true, emptyValue = "", - renderTitle, gridClassName }: DynamicSectionProps) => { const Bind = getBind(); - const FirstFieldBind = getBind(0); return ( /* First we mount the top level field, for example: "items" */ @@ -69,44 +75,29 @@ const DynamicSection = ({ const { value, appendValue } = bindField; const bindFieldValue: string[] = value || []; + return ( + {showLabel ? ( + + + {`${field.label} ${ + bindFieldValue.length ? `(${bindFieldValue.length})` : "" + }`} + + {field.helpText && ( + {field.helpText} + )} + + ) : null} - {typeof renderTitle === "function" && renderTitle(bindFieldValue)} - - {/* We always render the first item, for better UX */} - {showLabel && field.label && } - - - {bindProps => ( - - {/* We bind it to index "0", so when you start typing, that index in parent array will be populated */} - {children({ - Bind: FirstFieldBind, - field, - // "index" contains Bind props for this particular item in the array - // "field" contains Bind props for the main (parent) field. - bind: { - index: bindProps, - field: bindField - }, - index: 0 // Binds to "items.0" in the
. - })} - - )} - - - - {/* Now we skip the first item, because we already rendered it above, and proceed with all other items. */} - {bindFieldValue.slice(1).map((_, index) => { - /* We simply increase index, and as you type, the appropriate indexes in the parent array will be updated. */ - const realIndex = index + 1; - const BindField = getBind(realIndex); + {bindFieldValue.map((_, index) => { + const BindField = getBind(index); return ( - + {bindProps => ( - + {children({ Bind: BindField, field, @@ -114,7 +105,7 @@ const DynamicSection = ({ index: bindProps, field: bindField }, - index: realIndex + index })} )} @@ -130,15 +121,12 @@ const DynamicSection = ({ )} - - appendValue(emptyValue)} - > + + appendValue(emptyValue)}> } /> {t`Add value`} - + ); diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/Input.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/Input.tsx index feea7439f99..f70b86dd2b8 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/Input.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/Input.tsx @@ -29,7 +29,7 @@ export const Input = ({ bind, ...props }: InputProps) => { }} label={props.field.label} placeholder={props.field.placeholderText} - description={props.field.helpText} + description={props.field.multipleValues ? undefined : props.field.helpText} type={props.type} trailingIcon={props.trailingIcon} data-testid={`fr.input.${props.field.label}`} diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/dateTimeFields.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/dateTimeFields.tsx index 0610acc1b97..d9c94ef087c 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/dateTimeFields.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/dateTimeFields.tsx @@ -33,13 +33,10 @@ const plugin: CmsModelFieldRendererPlugin = { return ( {({ bind, index }) => { - let trailingIcon = undefined; - if (index > 0) { - trailingIcon = { - icon: , - onClick: () => bind.field.removeValue(index) - }; - } + const trailingIcon = { + icon: , + onClick: () => bind.field.removeValue(index) + }; if (fieldSettingsType === "dateTimeWithoutTimezone") { return ( diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/lexicalText/lexicalTextInputs.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/lexicalText/lexicalTextInputs.tsx index b47e1d904df..b0f521e53d5 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/lexicalText/lexicalTextInputs.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/lexicalText/lexicalTextInputs.tsx @@ -66,13 +66,13 @@ const plugin: CmsModelFieldRendererPlugin = { /> )} - {field.helpText} - {index > 0 && ( - } - onClick={() => bind.field.removeValue(index)} - /> + {field.multipleValues ? null : ( + {field.helpText} )} + } + onClick={() => bind.field.removeValue(index)} + /> )} diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/longText/longTexts.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/longText/longTexts.tsx index 15505b62167..cf4ca523e1b 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/longText/longTexts.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/longText/longTexts.tsx @@ -37,14 +37,11 @@ const plugin: CmsModelFieldRendererPlugin = { rows={5} label={t`Value {number}`({ number: index + 1 })} placeholder={props.field.placeholderText} - description={props.field.helpText} data-testid={`fr.input.longTexts.${props.field.label}.${index + 1}`} - trailingIcon={ - index > 0 && { - icon: , - onClick: () => bind.field.removeValue(index) - } - } + trailingIcon={{ + icon: , + onClick: () => bind.field.removeValue(index) + }} /> )} diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/number/numberInputs.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/number/numberInputs.tsx index b96fe337cc7..590825aff63 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/number/numberInputs.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/number/numberInputs.tsx @@ -37,15 +37,12 @@ const plugin: CmsModelFieldRendererPlugin = { onEnter={() => bind.field.appendValue("")} label={t`Value {number}`({ number: index + 1 })} placeholder={props.field.placeholderText} - description={props.field.helpText} data-testid={`fr.input.numbers.${props.field.label}.${index + 1}`} type="number" - trailingIcon={ - index > 0 && { - icon: , - onClick: () => bind.field.removeValue(index) - } - } + trailingIcon={{ + icon: , + onClick: () => bind.field.removeValue(index) + }} /> )} diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx index 59e5b9e6401..15b716a32fa 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx @@ -64,13 +64,13 @@ const Actions = ({ setHighlightIndex, bind, index }: ActionsProps) => { [moveValueUp, index] ); - return index > 0 ? ( + return ( <> } onClick={onDown} /> } onClick={onUp} /> } onClick={() => bind.field.removeValue(index)} /> - ) : null; + ); }; const ObjectsRenderer = (props: CmsModelFieldRendererProps) => { @@ -87,20 +87,7 @@ const ObjectsRenderer = (props: CmsModelFieldRendererProps) => { const settings = fieldSettings.getSettings(); return ( - ( - - - {`${field.label} ${value.length ? `(${value.length})` : ""}`} - - {field.helpText && {field.helpText}} - - )} - gridClassName={dynamicSectionGridStyle} - > + {({ Bind, bind, index }) => ( {highlightMap[index] ? : null} diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjectsAccordion.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjectsAccordion.tsx index 5160757fdc5..bdcc6fe438b 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjectsAccordion.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjectsAccordion.tsx @@ -63,18 +63,18 @@ const Actions = ({ setHighlightIndex, bind, index }: ActionsProps) => { [moveValueUp, index] ); - return index > 0 ? ( + return ( <> } onClick={onDown} /> } onClick={onUp} /> } onClick={() => bind.field.removeValue(index)} /> - ) : null; + ); }; const ObjectsRenderer = (props: CmsModelFieldRendererProps) => { const [highlightMap, setHighlightIndex] = useState<{ [key: number]: string }>({}); - const { field, contentModel } = props; + const { field, contentModel, getBind } = props; const fieldSettings = FieldSettings.createFrom(field); @@ -86,47 +86,58 @@ const ObjectsRenderer = (props: CmsModelFieldRendererProps) => { const settings = fieldSettings.getSettings(); const { open } = getAccordionRenderSettings(field); + const Bind = getBind(); + return ( - - - - {({ Bind, bind, index }) => ( - - {highlightMap[index] ? ( - - ) : null} - - } - // Open first Accordion by default - defaultValue={index === 0} + + {({ value }) => { + const values = value || []; + const label = `${field.label} ${values.length ? `(${values.length})` : ""}`; + + return ( + + + - - - - - - )} - - - + {({ Bind, bind, index }) => ( + + {highlightMap[index] ? ( + + ) : null} + + } + // Open first Accordion by default + defaultValue={index === 0} + > + + + + + + )} + + + + ); + }} + ); }; diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/richText/richTextInputs.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/richText/richTextInputs.tsx index e61ee8ba4aa..2aa962dddc7 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/richText/richTextInputs.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/richText/richTextInputs.tsx @@ -98,7 +98,6 @@ const plugin: CmsModelFieldRendererPlugin = { {...rteProps} label={`Value ${index + 1}`} placeholder={field.placeholderText} - description={field.helpText} /> diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/text/textInputs.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/text/textInputs.tsx index 0f6aba40f2a..e34894cd155 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/text/textInputs.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/text/textInputs.tsx @@ -37,14 +37,11 @@ const plugin: CmsModelFieldRendererPlugin = { onEnter={() => bind.field.appendValue("")} label={t`Value {number}`({ number: index + 1 })} placeholder={props.field.placeholderText} - description={props.field.helpText} data-testid={`fr.input.texts.${props.field.label}.${index + 1}`} - trailingIcon={ - index > 0 && { - icon: , - onClick: () => bind.field.removeValue(index) - } - } + trailingIcon={{ + icon: , + onClick: () => bind.field.removeValue(index) + }} /> )} From 7aba6310fe8e9b0347e5d9664bf55f2326f023e3 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Wed, 20 Nov 2024 23:45:42 +0100 Subject: [PATCH 3/3] fix(app-headless-cms): remove unused imports --- .../admin/plugins/fieldRenderers/object/multipleObjects.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx index 15b716a32fa..8d09900a8da 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx @@ -2,8 +2,6 @@ import React, { Dispatch, SetStateAction, useState, useCallback } from "react"; import { i18n } from "@webiny/app/i18n"; import { IconButton } from "@webiny/ui/Button"; import { Cell } from "@webiny/ui/Grid"; -import { FormElementMessage } from "@webiny/ui/FormElementMessage"; -import { Typography } from "@webiny/ui/Typography"; import { BindComponentRenderProp, CmsModelFieldRendererPlugin, @@ -17,7 +15,6 @@ import { ReactComponent as ArrowDown } from "./arrow_drop_down.svg"; import Accordion from "~/admin/plugins/fieldRenderers/Accordion"; import { fieldsWrapperStyle, - dynamicSectionTitleStyle, dynamicSectionGridStyle, fieldsGridStyle, ItemHighLight,