Skip to content

Commit

Permalink
fix(app-headless-cms): improve multi-value renderers
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel910 committed Nov 21, 2024
1 parent 4473264 commit 8d55fa9
Show file tree
Hide file tree
Showing 12 changed files with 120 additions and 146 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export const ContentEntryFormProvider = ({
onSubmit={onFormSubmit}
data={entry}
ref={ref}
validateOnFirstSubmit
invalidFields={invalidFields}
onInvalid={invalidFields => {
setInvalidFields(formValidationToMap(invalidFields));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -38,25 +35,34 @@ export interface DynamicSectionProps {
field: CmsModelField;
getBind: GetBindCallable;
showLabel?: boolean;
Label: React.ComponentType<any>;
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" */
Expand All @@ -69,52 +75,37 @@ const DynamicSection = ({
const { value, appendValue } = bindField;

const bindFieldValue: string[] = value || [];

return (
<ParentFieldProvider value={value} path={Bind.parentName}>
{showLabel ? (
<FieldLabel>
<Typography use={"headline5"}>
{`${field.label} ${
bindFieldValue.length ? `(${bindFieldValue.length})` : ""
}`}
</Typography>
{field.helpText && (
<FormElementMessage>{field.helpText}</FormElementMessage>
)}
</FieldLabel>
) : null}
<Grid className={classSet(gridClassName, style.gridContainer)}>
{typeof renderTitle === "function" && renderTitle(bindFieldValue)}
<Cell span={12}>
{/* We always render the first item, for better UX */}
{showLabel && field.label && <Label>{field.label}</Label>}

<FirstFieldBind>
{bindProps => (
<ParentValueIndexProvider index={0}>
{/* 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 <Form>.
})}
</ParentValueIndexProvider>
)}
</FirstFieldBind>
</Cell>

{/* 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 (
<Cell span={12} key={realIndex}>
<Cell span={12} key={index}>
<BindField>
{bindProps => (
<ParentValueIndexProvider index={realIndex}>
<ParentValueIndexProvider index={index}>
{children({
Bind: BindField,
field,
bind: {
index: bindProps,
field: bindField
},
index: realIndex
index
})}
</ParentValueIndexProvider>
)}
Expand All @@ -130,15 +121,12 @@ const DynamicSection = ({
</FormElementMessage>
</Cell>
)}
<Cell span={12} className={style.addButton}>
<ButtonDefault
disabled={bindFieldValue[0] === undefined}
onClick={() => appendValue(emptyValue)}
>
<AddButtonCell span={12} items={bindFieldValue.length}>
<ButtonDefault onClick={() => appendValue(emptyValue)}>
<ButtonIcon icon={<AddIcon />} />
{t`Add value`}
</ButtonDefault>
</Cell>
</AddButtonCell>
</Grid>
</ParentFieldProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,10 @@ const plugin: CmsModelFieldRendererPlugin = {
return (
<DynamicSection {...props}>
{({ bind, index }) => {
let trailingIcon = undefined;
if (index > 0) {
trailingIcon = {
icon: <DeleteIcon />,
onClick: () => bind.field.removeValue(index)
};
}
const trailingIcon = {
icon: <DeleteIcon />,
onClick: () => bind.field.removeValue(index)
};

if (fieldSettingsType === "dateTimeWithoutTimezone") {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ const plugin: CmsModelFieldRendererPlugin = {
/>
)}
</DelayedOnChange>
<FormElementMessage>{field.helpText}</FormElementMessage>
{index > 0 && (
<IconButton
icon={<DeleteIcon />}
onClick={() => bind.field.removeValue(index)}
/>
{field.multipleValues ? null : (
<FormElementMessage>{field.helpText}</FormElementMessage>
)}
<IconButton
icon={<DeleteIcon />}
onClick={() => bind.field.removeValue(index)}
/>
</EditorWrapper>
)}
</DynamicSection>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: <DeleteIcon />,
onClick: () => bind.field.removeValue(index)
}
}
trailingIcon={{
icon: <DeleteIcon />,
onClick: () => bind.field.removeValue(index)
}}
/>
</DelayedOnChange>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: <DeleteIcon />,
onClick: () => bind.field.removeValue(index)
}
}
trailingIcon={{
icon: <DeleteIcon />,
onClick: () => bind.field.removeValue(index)
}}
/>
</DelayedOnChange>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ const Actions = ({ setHighlightIndex, bind, index }: ActionsProps) => {
[moveValueUp, index]
);

return index > 0 ? (
return (
<>
<IconButton icon={<ArrowDown />} onClick={onDown} />
<IconButton icon={<ArrowUp />} onClick={onUp} />
<IconButton icon={<DeleteIcon />} onClick={() => bind.field.removeValue(index)} />
</>
) : null;
);
};

const ObjectsRenderer = (props: CmsModelFieldRendererProps) => {
Expand All @@ -87,20 +87,7 @@ const ObjectsRenderer = (props: CmsModelFieldRendererProps) => {
const settings = fieldSettings.getSettings();

return (
<DynamicSection
{...props}
emptyValue={{}}
showLabel={false}
renderTitle={value => (
<Cell span={12} className={dynamicSectionTitleStyle}>
<Typography use={"headline5"}>
{`${field.label} ${value.length ? `(${value.length})` : ""}`}
</Typography>
{field.helpText && <FormElementMessage>{field.helpText}</FormElementMessage>}
</Cell>
)}
gridClassName={dynamicSectionGridStyle}
>
<DynamicSection {...props} emptyValue={{}} gridClassName={dynamicSectionGridStyle}>
{({ Bind, bind, index }) => (
<ObjectItem>
{highlightMap[index] ? <ItemHighLight key={highlightMap[index]} /> : null}
Expand Down
Loading

0 comments on commit 8d55fa9

Please sign in to comment.