ArrayField

Massimo Melina committed Apr 26, 2022 at 17:03 UTC e3061ac6560a81f56e9136fa3fcb01966f73f821
6 files changed +111 -10
admin/src/ArrayField.ts new
+86
@@ -0,0 +1,86 @@
1 +import { createElement as h, Fragment, useMemo } from 'react'
2 +import { IconBtn, setHidden } from './misc'
3 +import { Add, Edit, Delete } from '@mui/icons-material'
4 +import { confirmDialog, formDialog } from './dialog'
5 +import { DataGrid, GridAlignment } from '@mui/x-data-grid'
6 +import { FieldDescriptor, FieldProps, labelFromKey } from './Form'
7 +import { Box, FormHelperText, FormLabel } from '@mui/material'
8 +
9 +export function ArrayField<T=any>({ label, helperText, fields, value, onChange, ...rest }: FieldProps<T[]> & { fields: FieldDescriptor[], height?: number }) {
10 + const rows = useMemo(() => (value||[]).map((x,$idx) =>
11 + setHidden({ ...x } as any, 'id' in x ? { $idx } : { id: $idx })),
12 + [JSON.stringify(value)]) //eslint-disable-line
13 + const columns = useMemo(() => {
14 + return [
15 + ...fields.map(f => ({
16 + field: f.k,
17 + headerName: f.headerName ?? (typeof f.label === 'string' ? f.label : labelFromKey(f.k)),
18 + disableColumnMenu: true,
19 + flex: .1,
20 + })),
21 + {
22 + field: '',
23 + width: 80,
24 + disableColumnMenu: true,
25 + sortable: false,
26 + align: 'center' as GridAlignment,
27 + headerAlign: 'center' as GridAlignment,
28 + renderHeader(){
29 + return h(IconBtn, {
30 + icon: Add,
31 + title: "Add",
32 + onClick: (event:any) =>
33 + formDialog({ fields }).then(o => // @ts-ignore
34 + o && onChange([...value||[], o], { was: value, event }))
35 + })
36 + },
37 + renderCell({ row }: any) {
38 + const { $idx=row.id } = row
39 + return h('div', {},
40 + h(IconBtn, {
41 + icon: Edit,
42 + title: "Modify",
43 + onClick: (event:any) =>
44 + formDialog({ fields, values: row }).then(newRec => {
45 + if (!newRec) return
46 + const newValue = value!.map((oldRec, i) => i === $idx ? newRec : oldRec)
47 + onChange(newValue, { was: value, event })
48 + }),
49 + }),
50 + h(IconBtn, {
51 + icon: Delete,
52 + title: "Delete",
53 + onClick: (event:any) =>
54 + confirmDialog("Delete?").then(ok => {
55 + if (!ok) return
56 + const newValue = value!.filter((rec, i) => i !== $idx)
57 + onChange(newValue, { was: value, event })
58 + }),
59 + }),
60 + )
61 + }
62 + }
63 + ]
64 + }, [fields, value, onChange])
65 + return h(Fragment, {},
66 + label && h(FormLabel, { sx: { ml: 1 } }, label),
67 + helperText && h(FormHelperText, {}, helperText),
68 + h(Box, rest,
69 + h(DataGrid, {
70 + columns,
71 + rows,
72 + hideFooterSelectedRowCount: true,
73 + hideFooter: true,
74 + pageSize: rows.length,
75 + rowsPerPageOptions: [rows.length],
76 + autoPageSize: true,
77 + componentsProps: {
78 + pagination: {
79 + showFirstButton: true,
80 + showLastButton: true,
81 + }
82 + },
83 + })
84 + )
85 + )
86 +}
admin/src/Form.ts
+6 -3
@@ -28,7 +28,7 @@ import { LoadingButton } from '@mui/lab'
28 import _ from 'lodash'
29 import { SxProps } from '@mui/system'
30
31 -interface FieldDescriptor<T=any> {
31 +export interface FieldDescriptor<T=any> {
32 k: string
33 comp?: any
34 label?: ReactNode
@@ -141,7 +141,7 @@ export function Form<Values extends Dict>({ fields, values, set, defaults, save,
141 field.helperText = field.helperText ? h(Fragment, {}, error, h('br'), field.helperText)
142 : error
143 if (field.label === undefined)
144 - field.label = _.capitalize(k.replace(/_/g, ' '))
144 + field.label = labelFromKey(k)
145 _.defaults(field, defaults?.(field))
146 }
147 const { xs=12, sm, md, lg, xl, comp=StringField,
@@ -171,6 +171,10 @@ export function Form<Values extends Dict>({ fields, values, set, defaults, save,
171 )
172 }
173
174 +export function labelFromKey(k: string) {
175 + return _.capitalize(k.replace(/_/g, ' '))
176 +}
177 +
178 export interface FieldProps<T> {
179 label?: string | ReactElement
180 value?: T
@@ -351,7 +355,6 @@ export function RadioField<T>({ label, options, value, onChange }: FieldProps<T>
355 }
356
357 export function CheckboxesField({ label, options, value, onChange }: FieldProps<string[]> & { options: string[] }) {
354 - console.log({ value })
358 return h(FormControl, {},
359 label && h(FormLabel, {}, label),
360 h(FormGroup, {},
admin/src/PluginsPage.ts
+10 -5
@@ -1,11 +1,12 @@
1 -import { createElement as h, FC, isValidElement } from "react"
1 +import { createElement as h, isValidElement } from "react"
2 import { apiCall, useApiComp, useApiList } from './api'
3 import { DataGrid } from '@mui/x-data-grid'
4 import { Alert, Box } from '@mui/material'
5 import { IconBtn } from './misc'
6 import { PowerSettingsNew, Settings } from '@mui/icons-material'
7 import { alertDialog, formDialog } from './dialog'
8 -import { BoolField, MultiSelectField, NumberField, SelectField, StringField } from './Form'
8 +import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from './Form'
9 +import { ArrayField } from './ArrayField'
10
11 const PLUGINS_CONFIG = 'plugins_config'
12
@@ -79,9 +80,12 @@ export default function PluginsPage() {
80 }
81
82 function makeFields(config: any) {
82 - return Object.entries(config).map(([k,o]) => {
83 - const comp = (type2comp as any)[(o as any)?.type] as FC | undefined
84 - return ({ k, comp, ...(typeof o === 'object' ? o : null) })
83 + return Object.entries(config).map(([k,o]: [string,any]) => {
84 + const comp = (type2comp as any)[o?.type] as Field<any> | undefined
85 + // @ts-ignore
86 + if (comp === ArrayField)
87 + o.fields = makeFields(o.fields)
88 + return { k, comp, ...(typeof o === 'object' ? o : null) }
89 })
90 }
91
@@ -91,4 +95,5 @@ const type2comp = {
95 boolean: BoolField,
96 select: SelectField,
97 multiselect: MultiSelectField,
98 + array: ArrayField,
99 }
server/src/const.ts
+1 -1
@@ -12,7 +12,7 @@ export const VERSION = ''
12 export const SESSION_DURATION = 30*60_000
13 export const DAY = 86_400_000
14
15 -export const API_VERSION = 1
15 +export const API_VERSION = 2
16 export const COMPATIBLE_API_VERSION = 1 // while changes in the api are not breaking, this number stays the same, otherwise is made equal to API_VERSION
17
18 export const SPECIAL_URI = '/~/'
server/src/misc.ts
+1 -1
@@ -34,7 +34,7 @@ export function prefix(pre:string, v:string|number, post:string='') {
34 }
35
36 export function setHidden(dest: object, src:object) {
37 - Object.defineProperties(dest, objSameKeys(src as any, value => ({
37 + return Object.defineProperties(dest, objSameKeys(src as any, value => ({
38 enumerable: false,
39 writable: true,
40 value,
shared/src/index.ts
+7
@@ -59,3 +59,10 @@ export function onlyTruthy<T>(arr: T[]) {
59 return arr.filter(truthy)
60 }
61
62 +export function setHidden(dest: object, src:object) {
63 + return Object.defineProperties(dest, objSameKeys(src as any, value => ({
64 + enumerable: false,
65 + writable: true,
66 + value,
67 + })))
68 +}