dx: support objects for ArrayField.value

Massimo Melina committed Jun 26, 2025 at 00:30 UTC 227a150e2d9c524b37ffeb53ce9359f0d524a7a9
1 file changed +21 -22
admin/src/ArrayField.ts
+21 -22
@@ -1,18 +1,15 @@
1 -// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2 -
3 -import { createElement as h, Fragment, useMemo, useState, isValidElement } from 'react'
4 -import { callable, DialogOptions, Dict, Functionable, isOrderedEqual, setHidden, swap } from './misc'
1 +import { createElement as h, Fragment, isValidElement, useMemo, useState } from 'react'
2 +import { callable, Dict, Functionable, isOrderedEqual, setHidden, swap } from './misc'
3 import { Add, Edit, Delete, ArrowUpward, ArrowDownward, Undo, Check } from '@mui/icons-material'
6 -import { FormDialog, formDialog } from './dialog'
4 +import { DialogOptions, FormDialog, formDialog } from './dialog'
5 import { GridActionsCellItem, GridAlignment, GridColDef } from '@mui/x-data-grid'
6 import { BoolField, FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-grid-form'
7 import { Box, FormHelperText, FormLabel } from '@mui/material'
10 -import { DateTimeField } from './DateTimeField'
8 import _ from 'lodash'
9 import { Center, Flex, IconBtn, useBreakpoint } from './mui'
10 import { DataTable, DataTableColumn } from './DataTable'
11
15 -type ArrayFieldProps<T> = FieldProps<T[]> & {
12 +type ArrayFieldProps<T> = FieldProps<T[] | Dict<T>> & {
13 fields: Functionable<FieldDescriptor[] & {
14 $width?: number,
15 $column?: Partial<DataTableColumn>,
@@ -29,20 +26,22 @@ type ArrayFieldProps<T> = FieldProps<T[]> & {
26 form?: Partial<FormDialog<any>>,
27 dialog?: Partial<DialogOptions>,
28 details?: boolean
29 + objectK?: string
30 }
33 -export function ArrayField<T extends object>({ label, helperText, fields, value, onChange, onError, setApi, reorder, prepend, noRows, valuesForAdd, autoRowHeight, dialog, form, details, ...rest }: ArrayFieldProps<T>) {
34 - if (!Array.isArray(value)) // avoid crash if non-array values are passed, especially developing plugins
35 - value = []
36 - const rows = useMemo(() => value!.map((x,$idx) =>
31 +export function ArrayField<T extends object>({ label, helperText, fields, value, onChange, onError, setApi, reorder, prepend, noRows, valuesForAdd, autoRowHeight, dialog, form, details, objectK, ...rest }: ArrayFieldProps<T>) {
32 + const valueA = Array.isArray(value) ? value
33 + : !objectK || !value ? [] // avoid crash if non-array values are passed, especially developing plugins
34 + : Object.entries(value).map(([k, v]) => ({ [objectK]: k, ...v }))
35 + const rows = useMemo(() => valueA!.map((x,$idx) =>
36 setHidden({ ...x } as any, x.hasOwnProperty('id') ? { $idx } : { id: $idx })),
38 - [JSON.stringify(value)]) //eslint-disable-line
37 + [JSON.stringify(valueA)]) //eslint-disable-line
38 const formProp = (values: any) => ({
39 fields: callable(fields, values).map(({ $width, $column, $type, $hideUnder, showIf, $render, $mergeRender, ...rest }) =>
40 (!showIf || showIf(values)) && _.defaults(rest, byType[$type]?.field)),
41 ...form,
42 })
43 setApi?.({ isEqual: isOrderedEqual }) // don't rely on stringify, as it wouldn't work with non-json values
45 - const [undo, setUndo] = useState<typeof value>()
44 + const [undo, setUndo] = useState<typeof valueA>()
45 return h(Fragment, {},
46 h(Flex, { rowGap: 0, flexWrap: 'wrap' },
47 label && h(FormLabel, { sx: { ml: .5 } }, label),
@@ -102,7 +101,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
101 onClick: ev =>
102 formDialog<T>({ form: formProp, title, values: valuesForAdd, dialogProps: dialog }).then(x => {
103 if (!x) return
105 - const newValue = value?.slice() || []
104 + const newValue = valueA.slice() || []
105 if (prepend) newValue.unshift(x)
106 else newValue.push(x)
107 set(newValue, ev)
@@ -133,7 +132,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
132 dialogProps: dialog
133 })
134 if (res)
136 - set(value!.map((oldRec, i) => i === $idx ? res : oldRec), ev)
135 + set(valueA.map((oldRec, i) => i === $idx ? res : oldRec), ev)
136 }
137 }),
138 h(GridActionsCellItem as any, {
@@ -142,7 +141,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
141 showInMenu: reorder,
142 onClick: ev => {
143 ev.stopPropagation()
145 - set(value!.filter((_rec, i) => i !== $idx), ev)
144 + set(valueA.filter((_rec, i) => i !== $idx), ev)
145 },
146 }),
147 reorder && $idx && h(GridActionsCellItem as any, {
@@ -151,7 +150,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
150 showInMenu: true,
151 onClick: ev => {
152 ev.stopPropagation()
154 - set(swap(value!.slice(), $idx, $idx - 1), ev)
153 + set(swap(valueA.slice(), $idx, $idx - 1), ev)
154 },
155 }),
156 reorder && $idx < rows.length - 1 && h(GridActionsCellItem as any, {
@@ -160,7 +159,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
159 showInMenu: true,
160 onClick: ev => {
161 ev.stopPropagation()
163 - set(swap(value!.slice(), $idx, $idx + 1), ev)
162 + set(swap(valueA.slice(), $idx, $idx + 1), ev)
163 },
164 }),
165 ].filter(Boolean)
@@ -171,9 +170,10 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
170 )
171 )
172
174 - function set(newValue: NonNullable<typeof value>, event?: any) {
175 - onChange(newValue, { was: value, event })
176 - setUndo(value)
173 + function set(newValue: typeof valueA, event?: any) {
174 + onChange(!objectK ? newValue : Object.fromEntries((newValue as any).map(({ [objectK]: k, ...v }) => [k, v])),
175 + { was: value, event })
176 + setUndo(valueA)
177 }
178
179 }
@@ -184,7 +184,6 @@ const byType: Dict<{ field?: Partial<FieldDescriptor>, column?: Partial<GridColD
184 column: { renderCell: ({ value }) => value && h(Check) },
185 },
186 dateTime: {
187 - field: { comp: DateTimeField },
187 column: {
188 minWidth: 96, flex: 0.5,
189 renderCell: ({ value }) => value && new Date(value).toLocaleString(),