-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Dataform bulk editing support #67344
base: trunk
Are you sure you want to change the base?
Changes from all commits
0444bed
3e5e62c
7869b30
97ae2b5
489623a
fdc7bf8
4df0165
ac03ed1
c27bd65
366883a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,8 +10,44 @@ import type { DataFormProps } from '../../types'; | |
import { DataFormProvider } from '../dataform-context'; | ||
import { normalizeFields } from '../../normalize-fields'; | ||
import { DataFormLayout } from '../../dataforms-layouts/data-form-layout'; | ||
import { MIXED_VALUE } from '../../constants'; | ||
|
||
export default function DataForm< Item >( { | ||
/** | ||
* Loops through the list of data items and returns an object with the intersecting ( same ) key and values. | ||
* Skips keys that start with an underscore. | ||
* | ||
* @param data list of items. | ||
*/ | ||
function getIntersectingValues< Item extends object >( data: Item[] ): Item { | ||
const intersectingValues = {} as Item; | ||
const keys = Object.keys( data[ 0 ] ) as Array< keyof Item >; | ||
for ( const key of keys ) { | ||
// Skip keys that start with underscore. | ||
if ( key.toString().startsWith( '_' ) ) { | ||
continue; | ||
} | ||
const [ firstRecord, ...remainingRecords ] = data; | ||
|
||
if ( typeof firstRecord[ key ] === 'object' ) { | ||
// Traverse through nested objects. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you think we should traverse nested objects? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some fields may rely on the nested values only, incase we want to support that for bulk editing. Although we don't have direct use case for that in core right now, aside from maybe metadata fields. |
||
intersectingValues[ key ] = getIntersectingValues( | ||
data.map( ( item ) => item[ key ] as object ) | ||
) as Item[ keyof Item ]; | ||
} else { | ||
const intersects = remainingRecords.every( ( item ) => { | ||
return item[ key ] === firstRecord[ key ]; | ||
} ); | ||
if ( intersects ) { | ||
intersectingValues[ key ] = firstRecord[ key ]; | ||
} else { | ||
intersectingValues[ key ] = MIXED_VALUE as Item[ keyof Item ]; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an exact replicate of this comment: #67344 (comment) I am not sure if you meant to do that or if GH messed up? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, forget about this one :P There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actually where I had my initial "Symbol" comment :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aaaha that makes a lot of sense! :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in: c27bd65 |
||
} | ||
return intersectingValues; | ||
} | ||
|
||
export default function DataForm< Item extends object >( { | ||
data, | ||
form, | ||
fields, | ||
|
@@ -22,13 +58,27 @@ export default function DataForm< Item >( { | |
[ fields ] | ||
); | ||
|
||
const flattenedData = useMemo( () => { | ||
if ( Array.isArray( data ) ) { | ||
return getIntersectingValues< Item >( data ); | ||
} | ||
return data; | ||
}, [ data ] ); | ||
|
||
if ( ! form.fields ) { | ||
return null; | ||
} | ||
|
||
const isBulkEditing = Array.isArray( data ); | ||
|
||
return ( | ||
<DataFormProvider fields={ normalizedFields }> | ||
<DataFormLayout data={ data } form={ form } onChange={ onChange } /> | ||
<DataFormLayout | ||
data={ flattenedData } | ||
form={ form } | ||
onChange={ onChange } | ||
isBulkEditing={ isBulkEditing } | ||
/> | ||
</DataFormProvider> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,17 +7,37 @@ 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'; | ||
|
||
export function DataFormLayout< Item >( { | ||
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 | ||
)?.supportsBulkEditing; | ||
} ); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checks if 'any' of the nested fields support bulk, returns 'true' in that case. |
||
|
||
export function DataFormLayout< Item extends object >( { | ||
data, | ||
form, | ||
onChange, | ||
children, | ||
isBulkEditing, | ||
}: { | ||
data: Item; | ||
form: Form; | ||
|
@@ -31,6 +51,7 @@ export function DataFormLayout< Item >( { | |
} ) => React.JSX.Element | null, | ||
field: FormField | ||
) => React.JSX.Element; | ||
isBulkEditing?: boolean; | ||
} ) { | ||
const { fields: fieldDefinitions } = useContext( DataFormContext ); | ||
|
||
|
@@ -69,6 +90,19 @@ export function DataFormLayout< Item >( { | |
return null; | ||
} | ||
|
||
if ( | ||
isBulkEditing && | ||
( ( isCombinedField( formField ) && | ||
! doesCombinedFieldSupportBulkEdits( | ||
formField, | ||
fieldDefinitions | ||
) ) || | ||
( fieldDefinition && | ||
! fieldDefinition.supportsBulkEditing ) ) | ||
) { | ||
return null; | ||
} | ||
|
||
if ( children ) { | ||
return children( FieldLayout, formField ); | ||
} | ||
|
@@ -79,6 +113,7 @@ export function DataFormLayout< Item >( { | |
data={ data } | ||
field={ formField } | ||
onChange={ onChange } | ||
isBulkEditing={ isBulkEditing } | ||
/> | ||
); | ||
} ) } | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed that this function is trying to combine the items into an
Item
object. Unfortunately, I don't think that's possible really because theSymbol
is not a validItem
value and the nesting is not the right thing to do.I think what we should be doing here is calling
field.getValue
for all the fields (maybe the fields that are within the form) and compute the result in a merged object (that is notItem
) who keys are the field keys and values are the result ofgetValue
calls.This is necessary because we can't really assume/know the shape of valid
Item
. this is something we discussed IRL with @oandregal and @gigitux at Rome core days.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah, I can give this approach a try. I agree it makes more sense, but will need quite a bit more changes I believe.
For example, if we do generate this new merged object using
getValue
and receive an array ofdata
do we pass the array of data down to each field? or the merged object? or just the first item in the array ( probably the whole array, but currently a lot of fields pass that straight intogetValue
).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it's not very straightforward. Maybe the current PR can be a good interim solution (although not entirely "correct"). Should we try it in a separate PR to see how they compare?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah a different PR is a good idea, otherwise it may be hard to see the pros/cons of each approach.