plugins: support array.fields as callback
Massimo Melina committed
Mar 21, 2025 at 21:46 UTC
7d1eb5a73ce2062baf2084820075fb58f9cd93bd
2 files changed
+23
-18
admin/src/ArrayField.ts
+19
-15
@@ -1,7 +1,7 @@
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 } from 'react'
4
-import { callable, Dict, Functionable, isOrderedEqual, setHidden, swap } from './misc'
4
+import { callable, DialogOptions, Dict, Functionable, isOrderedEqual, setHidden, swap } from './misc'
5
import { Add, Edit, Delete, ArrowUpward, ArrowDownward, Undo, Check } from '@mui/icons-material'
6
import { formDialog } from './dialog'
7
import { GridActionsCellItem, GridAlignment, GridColDef } from '@mui/x-data-grid'
@@ -12,15 +12,15 @@ import _ from 'lodash'
12
import { Center, Flex, IconBtn, useBreakpoint } from './mui'
13
import { DataTable } from './DataTable'
14
15
-type ArrayFieldProps<T> = FieldProps<T[]> & { fields: Functionable<FieldDescriptor[]>, height?: number, reorder?: boolean, prepend?: boolean, autoRowHeight?: boolean }
16
-export function ArrayField<T extends object>({ label, helperText, fields, value, onChange, onError, setApi, reorder, prepend, noRows, valuesForAdd, autoRowHeight, ...rest }: ArrayFieldProps<T>) {
15
+type ArrayFieldProps<T> = FieldProps<T[]> & { fields: Functionable<FieldDescriptor[]>, height?: number, reorder?: boolean, prepend?: boolean, autoRowHeight?: boolean, dialog?: Omit<DialogOptions, 'Content'> }
16
+export function ArrayField<T extends object>({ label, helperText, fields, value, onChange, onError, setApi, reorder, prepend, noRows, valuesForAdd, autoRowHeight, dialog, ...rest }: ArrayFieldProps<T>) {
17
if (!Array.isArray(value)) // avoid crash if non-array values are passed, especially developing plugins
18
value = []
19
const rows = useMemo(() => value!.map((x,$idx) =>
20
setHidden({ ...x } as any, x.hasOwnProperty('id') ? { $idx } : { id: $idx })),
21
[JSON.stringify(value)]) //eslint-disable-line
22
const form = (values: any) => ({
23
- fields: callable(fields, values).map(({ $width, $column, $type, $hideUnder, showIf, ...rest }) => (!showIf || showIf(values)) && _.defaults(rest, byType[$type]?.field))
23
+ fields: callable(fields, values).map(({ $width, $column, $type, $hideUnder, showIf, $render, $mergeRender, ...rest }) => (!showIf || showIf(values)) && _.defaults(rest, byType[$type]?.field))
24
})
25
setApi?.({ isEqual: isOrderedEqual }) // don't rely on stringify, as it wouldn't work with non-json values
26
const [undo, setUndo] = useState<typeof value>()
@@ -52,18 +52,19 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
52
columns: [
53
...callable(fields, false).map(f => {
54
const def = byType[f.$type]?.column
55
- return {
55
+ return _.defaults({
56
field: f.k,
57
headerName: f.headerName ?? (typeof f.label === 'string' ? f.label : labelFromKey(f.k)),
58
disableColumnMenu: true,
59
valueGetter({ value }: any) {
60
return (f.toField || _.identity)(value)
61
},
62
- ...def,
62
...f.$width ? { [f.$width >= 8 ? 'width' : 'flex']: f.$width } : (!def?.width && !def?.flex && { flex: 1 }),
63
+ renderCell: f.$render,
64
+ mergeRender: f.$mergeRender,
65
hideUnder: f.$hideUnder,
66
...f.$column,
66
- }
67
+ }, def)
68
}),
69
{
70
field: '',
@@ -78,7 +79,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
79
title,
80
size: 'small',
81
onClick: ev =>
81
- formDialog<T>({ form, title, values: valuesForAdd }).then(x => {
82
+ formDialog<T>({ form, title, values: valuesForAdd, dialogProps: dialog }).then(x => {
83
if (!x) return
84
const newValue = value?.slice() || []
85
if (prepend) newValue.unshift(x)
@@ -102,12 +103,16 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
103
icon: h(Edit),
104
label: title,
105
title,
105
- onClick(ev: MouseEvent) {
106
+ async onClick(ev: MouseEvent) {
107
ev.stopPropagation()
107
- formDialog<T>({ values: row as any, form, title: h(Fragment, {}, title, label && ' - ', label) }).then(x => {
108
- if (x)
109
- set(value!.map((oldRec, i) => i === $idx ? x : oldRec), ev)
108
+ const res = await formDialog<T>({
109
+ values: row as any,
110
+ form,
111
+ title: h(Fragment, {}, title, label && ' - ', label),
112
+ dialogProps: dialog
113
})
114
+ if (res)
115
+ set(value!.map((oldRec, i) => i === $idx ? res : oldRec), ev)
116
}
117
}),
118
h(GridActionsCellItem as any, {
@@ -160,9 +165,8 @@ const byType: Dict<{ field?: Partial<FieldDescriptor>, column?: Partial<GridColD
165
dateTime: {
166
field: { comp: DateTimeField },
167
column: {
163
- type: 'dateTime', minWidth: 96, flex: 0.5,
164
- valueGetter: ({ value }) => value && new Date(value),
165
- renderCell: ({ value }) => value && h(Box, {}, value.toLocaleDateString(), h('br'), value.toLocaleTimeString())
168
+ minWidth: 96, flex: 0.5,
169
+ renderCell: ({ value }) => value && new Date(value).toLocaleString(),
170
}
171
}
172
}
\ No newline at end of file
admin/src/InstalledPlugins.ts
+4
-3
@@ -258,10 +258,11 @@ function makeFields(config: any, values: any) {
258
if (values === false && type === 'date_time')
259
rest.$type = 'dateTime'
260
if (comp === ArrayField) {
261
- const {fields} = rest
261
+ let {fields} = rest
262
rest.valuesForAdd = newObj(callable(fields, false), x => x.defaultValue)
263
- rest.fields = !_.isFunction(fields) ? makeFields(fields, false)
264
- : (values: unknown) => _.map(fields(values), (v,k) => v && ({ k, ...v, defaultValue: undefined })).filter(Boolean)
263
+ if (typeof fields === 'string')
264
+ fields = eval(fields)
265
+ rest.fields = (values: unknown) => _.map(makeFields(callable(fields, values), values), (v,k) => v && ({ k, ...v, defaultValue: undefined })).filter(Boolean)
266
}
267
if (defaultValue !== undefined && type === 'boolean')
268
rest.placeholder = `Default value is ${JSON.stringify(defaultValue)}`