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
+}