Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve multi value field renderers #4404

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class CmsModelObjectFieldConverterPlugin extends CmsModelFieldConverterPl
if (field.multipleValues) {
if (Array.isArray(value) === false) {
return {
[field.storageId]: []
[field.storageId]: null
};
}
return {
Expand Down Expand Up @@ -163,7 +163,7 @@ export class CmsModelObjectFieldConverterPlugin extends CmsModelFieldConverterPl
if (field.multipleValues) {
if (Array.isArray(value) === false) {
return {
[field.fieldId]: []
[field.fieldId]: null
};
}
return {
Expand Down
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 @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -64,13 +61,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 +84,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
Loading