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)}`