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

Add bulk editing support by passing array down to the fields #67584

Open
wants to merge 14 commits into
base: trunk
Choose a base branch
from
Open
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
18 changes: 11 additions & 7 deletions packages/dataviews/src/components/dataform/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ToggleControl } from '@wordpress/components';
* Internal dependencies
*/
import DataForm from '../index';
import type { Field, Form } from '../../../types';
import type { DataFormControlProps, Field, Form } from '../../../types';

type SamplePost = {
title: string;
Expand All @@ -19,6 +19,7 @@ type SamplePost = {
date: string;
birthdate: string;
password?: string;
sticky?: boolean;
};

const meta = {
Expand Down Expand Up @@ -107,16 +108,19 @@ const fields = [
id: 'sticky',
label: 'Sticky',
type: 'integer',
Edit: ( { field, onChange, data, hideLabelFromVision } ) => {
const { id, getValue } = field;
Edit: ( {
field,
onChange,
value,
hideLabelFromVision,
}: DataFormControlProps< SamplePost > ) => {
const { id } = field;
return (
<ToggleControl
__nextHasNoMarginBottom
label={ hideLabelFromVision ? '' : field.label }
checked={ getValue( { item: data } ) }
onChange={ () =>
onChange( { [ id ]: ! getValue( { item: data } ) } )
}
checked={ value }
onChange={ () => onChange( { [ id ]: ! value } ) }
/>
);
},
Expand Down
3 changes: 3 additions & 0 deletions packages/dataviews/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ export const sortIcons = {
export const LAYOUT_TABLE = 'table';
export const LAYOUT_GRID = 'grid';
export const LAYOUT_LIST = 'list';

// Dataform mixed value.
export const MIXED_VALUE = Symbol.for( 'DATAFORM_MIXED_VALUE' );
7 changes: 3 additions & 4 deletions packages/dataviews/src/dataform-controls/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import { useCallback } from '@wordpress/element';
import type { DataFormControlProps } from '../types';

export default function DateTime< Item >( {
data,
field,
onChange,
hideLabelFromVision,
}: DataFormControlProps< Item > ) {
value,
}: DataFormControlProps< Item, string > ) {
const { id, label } = field;
const value = field.getValue( { item: data } );

const onChangeControl = useCallback(
( newValue: string | null ) => onChange( { [ id ]: newValue } ),
Expand All @@ -34,7 +33,7 @@ export default function DateTime< Item >( {
<VisuallyHidden as="legend">{ label }</VisuallyHidden>
) }
<TimePicker
currentTime={ value }
currentTime={ typeof value === 'symbol' ? '' : value }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the check about symbol in other controls like integer, why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will update it, the TimePicker component it-self seemed to handle it gracefully, but its not expected.

onChange={ onChangeControl }
hideLabelFromVision
/>
Expand Down
7 changes: 3 additions & 4 deletions packages/dataviews/src/dataform-controls/integer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import { useCallback } from '@wordpress/element';
import type { DataFormControlProps } from '../types';

export default function Integer< Item >( {
data,
field,
onChange,
hideLabelFromVision,
}: DataFormControlProps< Item > ) {
value,
}: DataFormControlProps< Item, number > ) {
const { id, label, description } = field;
const value = field.getValue( { item: data } ) ?? '';
const onChangeControl = useCallback(
( newValue: string | undefined ) =>
onChange( {
Expand All @@ -29,7 +28,7 @@ export default function Integer< Item >( {
<NumberControl
label={ label }
help={ description }
value={ value }
value={ typeof value === 'symbol' ? '' : value }
onChange={ onChangeControl }
__next40pxDefaultSize
hideLabelFromVision={ hideLabelFromVision }
Expand Down
7 changes: 3 additions & 4 deletions packages/dataviews/src/dataform-controls/radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import { useCallback } from '@wordpress/element';
import type { DataFormControlProps } from '../types';

export default function Radio< Item >( {
data,
field,
onChange,
hideLabelFromVision,
}: DataFormControlProps< Item > ) {
value,
}: DataFormControlProps< Item, string > ) {
const { id, label } = field;
const value = field.getValue( { item: data } );

const onChangeControl = useCallback(
( newValue: string ) =>
Expand All @@ -32,7 +31,7 @@ export default function Radio< Item >( {
label={ label }
onChange={ onChangeControl }
options={ field.elements }
selected={ value }
selected={ typeof value === 'symbol' ? '' : value }
hideLabelFromVision={ hideLabelFromVision }
/>
);
Expand Down
7 changes: 3 additions & 4 deletions packages/dataviews/src/dataform-controls/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ import { __ } from '@wordpress/i18n';
import type { DataFormControlProps } from '../types';

export default function Select< Item >( {
data,
field,
onChange,
hideLabelFromVision,
}: DataFormControlProps< Item > ) {
value,
}: DataFormControlProps< Item, string | number > ) {
const { id, label } = field;
const value = field.getValue( { item: data } ) ?? '';
const onChangeControl = useCallback(
( newValue: any ) =>
onChange( {
Expand All @@ -41,7 +40,7 @@ export default function Select< Item >( {
return (
<SelectControl
label={ label }
value={ value }
value={ typeof value === 'symbol' ? '' : value }
options={ elements }
onChange={ onChangeControl }
__next40pxDefaultSize
Expand Down
7 changes: 3 additions & 4 deletions packages/dataviews/src/dataform-controls/text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import { useCallback } from '@wordpress/element';
import type { DataFormControlProps } from '../types';

export default function Text< Item >( {
data,
field,
onChange,
hideLabelFromVision,
}: DataFormControlProps< Item > ) {
value,
}: DataFormControlProps< Item, string > ) {
const { id, label, placeholder } = field;
const value = field.getValue( { item: data } );

const onChangeControl = useCallback(
( newValue: string ) =>
Expand All @@ -30,7 +29,7 @@ export default function Text< Item >( {
<TextControl
label={ label }
placeholder={ placeholder }
value={ value ?? '' }
value={ typeof value === 'symbol' ? '' : value ?? '' }
onChange={ onChangeControl }
__next40pxDefaultSize
__nextHasNoMarginBottom
Expand Down
37 changes: 34 additions & 3 deletions packages/dataviews/src/dataforms-layouts/data-form-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,43 @@ import { useContext, useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
import type { Form, FormField, SimpleFormField } from '../types';
import type {
CombinedFormField,
Form,
FormField,
NormalizedField,
SimpleFormField,
} from '../types';
import { getFormFieldLayout } from './index';
import DataFormContext from '../components/dataform-context';
import { isCombinedField } from './is-combined-field';
import normalizeFormFields from '../normalize-form-fields';

function doesCombinedFieldSupportBulkEdits< Item >(
combinedField: CombinedFormField,
fieldDefinitions: NormalizedField< Item >[]
): boolean {
return combinedField.children.some( ( child ) => {
const fieldId = typeof child === 'string' ? child : child.id;

return ! fieldDefinitions.find(
( fieldDefinition ) => fieldDefinition.id === fieldId
)?.unique;
} );
}

export function DataFormLayout< Item >( {
data,
form,
onChange,
children,
}: {
data: Item;
data: Item | Item[];
form: Form;
onChange: ( value: any ) => void;
children?: (
FieldLayout: ( props: {
data: Item;
data: Item | Item[];
field: FormField;
onChange: ( value: any ) => void;
hideLabelFromVision?: boolean;
Expand Down Expand Up @@ -69,6 +88,18 @@ export function DataFormLayout< Item >( {
return null;
}

if (
Array.isArray( data ) &&
( ( isCombinedField( formField ) &&
! doesCombinedFieldSupportBulkEdits(
formField,
fieldDefinitions
) ) ||
( fieldDefinition && fieldDefinition.unique ) )
) {
return null;
}

if ( children ) {
return children( FieldLayout, formField );
}
Expand Down
16 changes: 14 additions & 2 deletions packages/dataviews/src/dataforms-layouts/panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import type {
import DataFormContext from '../../components/dataform-context';
import { DataFormLayout } from '../data-form-layout';
import { isCombinedField } from '../is-combined-field';
import { MIXED_VALUE } from '../../constants';
import useFieldValue from '../use-field-value';

function DropdownHeader( {
title,
Expand Down Expand Up @@ -70,7 +72,7 @@ function PanelDropdown< Item >( {
fieldDefinition: NormalizedField< Item >;
popoverAnchor: HTMLElement | null;
labelPosition: 'side' | 'top' | 'none';
data: Item;
data: Item | Item[];
onChange: ( value: any ) => void;
field: FormField;
} ) {
Expand Down Expand Up @@ -98,6 +100,8 @@ function PanelDropdown< Item >( {
};
}, [ field ] );

const fieldValue = useFieldValue( data, field.id );

// Memoize popoverProps to avoid returning a new object every time.
const popoverProps = useMemo(
() => ( {
Expand All @@ -111,6 +115,8 @@ function PanelDropdown< Item >( {
[ popoverAnchor ]
);

const showMixedValue = Array.isArray( data ) && fieldValue === MIXED_VALUE;

return (
<Dropdown
contentClassName="dataforms-layouts-panel__field-dropdown"
Expand Down Expand Up @@ -138,7 +144,13 @@ function PanelDropdown< Item >( {
) }
onClick={ onToggle }
>
<fieldDefinition.render item={ data } />
{ showMixedValue ? (
__( 'Mixed' )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In one of the issues, @jasmussen mentions the possibility to render a different "mixed" label/component based on the "type" of the control. So potentially we could defer this to the control/field itself at some point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context is loosely discussed here.

) : (
<fieldDefinition.render
item={ Array.isArray( data ) ? data[ 0 ] : data }
/>
) }
</Button>
) }
renderContent={ ( { onClose } ) => (
Expand Down
5 changes: 5 additions & 0 deletions packages/dataviews/src/dataforms-layouts/regular/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { Form, FieldLayoutProps } from '../../types';
import DataFormContext from '../../components/dataform-context';
import { DataFormLayout } from '../data-form-layout';
import { isCombinedField } from '../is-combined-field';
import useFieldValue from '../use-field-value';

function Header( { title }: { title: string } ) {
return (
Expand Down Expand Up @@ -59,6 +60,8 @@ export default function FormRegularField< Item >( {
};
}, [ field ] );

const fieldValue = useFieldValue( data, field.id );

if ( isCombinedField( field ) ) {
return (
<>
Expand Down Expand Up @@ -95,6 +98,7 @@ export default function FormRegularField< Item >( {
field={ fieldDefinition }
onChange={ onChange }
hideLabelFromVision
value={ fieldValue }
/>
</div>
</HStack>
Expand All @@ -105,6 +109,7 @@ export default function FormRegularField< Item >( {
<div className="dataforms-layouts-regular__field">
<fieldDefinition.Edit
data={ data }
value={ fieldValue }
field={ fieldDefinition }
onChange={ onChange }
hideLabelFromVision={
Expand Down
38 changes: 38 additions & 0 deletions packages/dataviews/src/dataforms-layouts/use-field-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* WordPress dependencies
*/
import { useContext, useMemo } from '@wordpress/element';

/**
* Internal dependencies
*/
import DataFormContext from '../components/dataform-context';
import { MIXED_VALUE } from '../constants';

export default function useFieldValue< Item >(
data: Item | Item[],
fieldId: string
) {
const { fields } = useContext( DataFormContext );
return useMemo( () => {
const fieldDefinition = fields.find(
( fieldDef ) => fieldDef.id === fieldId
);
if ( ! fieldDefinition ) {
return undefined;
}
if ( Array.isArray( data ) ) {
const [ firstRecord, ...remainingRecords ] = data;
const firstValue = fieldDefinition.getValue( {
item: firstRecord,
} );
const intersects = remainingRecords.every( ( item ) => {
return fieldDefinition.getValue( { item } ) === firstValue;
} );
return intersects ? firstValue : MIXED_VALUE;
}
return fieldDefinition.getValue( {
item: data,
} );
}, [ data, fields, fieldId ] );
}
1 change: 1 addition & 0 deletions packages/dataviews/src/normalize-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export function normalizeFields< Item >(
sort,
isValid,
Edit,
unique: field.unique ?? false,
enableHiding: field.enableHiding ?? true,
enableSorting: field.enableSorting ?? true,
};
Expand Down
Loading
Loading