plugins: support showIf in config.array.fields

Massimo Melina committed Mar 17, 2025 at 14:47 UTC 81a54e551c1064a0b103bd093baa0fcb1c77ed4b
4 files changed +15 -19
admin/src/ArrayField.ts
+7 -7
@@ -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 { Dict, isOrderedEqual, setHidden, swap } from './misc'
4 +import { callable, 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,16 +12,16 @@ import _ from 'lodash'
12 import { Center, Flex, IconBtn, useBreakpoint } from './mui'
13 import { DataTable } from './DataTable'
14
15 -type ArrayFieldProps<T> = FieldProps<T[]> & { fields: FieldDescriptor[], height?: number, reorder?: boolean, prepend?: boolean, autoRowHeight?: boolean }
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>) {
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 = {
23 - fields: fields.map(({ $width, $column, $type, $hideUnder, ...rest }) => _.defaults(rest, byType[$type]?.field))
24 - }
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))
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>()
27 return h(Fragment, {},
@@ -50,7 +50,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
50 }
51 },
52 columns: [
53 - ...fields.map(f => {
53 + ...callable(fields, false).map(f => {
54 const def = byType[f.$type]?.column
55 return {
56 field: f.k,
@@ -116,7 +116,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
116 showInMenu: reorder,
117 onClick: ev => {
118 ev.stopPropagation()
119 - set(value!.filter((rec, i) => i !== $idx), ev)
119 + set(value!.filter((_rec, i) => i !== $idx), ev)
120 },
121 }),
122 reorder && $idx && h(GridActionsCellItem as any, {
admin/src/InstalledPlugins.ts
+6 -7
@@ -240,24 +240,23 @@ export function renderName({ row, value }: any) {
240
241 function makeFields(config: any, values: any) {
242 return Object.entries(config).map(([k,o]: [string,any]) => {
243 - let { type, defaultValue, fields, frontend, helperText, showIf, ...rest } = o
243 + let { type, defaultValue, frontend, showIf, ...rest } = o
244 try {
245 if (typeof showIf === 'string') // compile once
246 - o.showIf = showIf = eval(showIf) // eval is normally considered a threat, but this code is coming from a plugin that's already running on your server, so you already decided to trust it. Here it will run in your browser, and inside the page that administrating the same server.
246 + rest.showIf = showIf = eval(showIf) // eval is normally considered a threat, but this code is coming from a plugin that's already running on your server, so you already decided to trust it. Here it will run in your browser, and inside the page that administrating the same server.
247 if (showIf && !showIf(values))
248 return
249 }
250 catch {}
251 - if (helperText)
252 - helperText = md(helperText, { html: false })
251 + rest.helperText &&= md(rest.helperText, { html: false })
252 const comp = (type2comp as any)[type] as Field<any> | undefined
253 if (comp === ArrayField) {
255 - rest.valuesForAdd = newObj(fields, x => x.defaultValue)
256 - fields = makeFields(fields, values)
254 + rest.valuesForAdd = newObj(rest.fields, x => x.defaultValue)
255 + rest.fields = makeFields(rest.fields, false)
256 }
257 if (defaultValue !== undefined && type === 'boolean')
258 rest.placeholder = `Default value is ${JSON.stringify(defaultValue)}`
260 - return { k, comp, fields, helperText, ...rest }
259 + return { k, comp, ...rest }
260 })
261 }
262
src/api.plugins.ts
+1 -4
@@ -15,7 +15,6 @@ import {
15 } from './github'
16 import { HTTP_FAILED_DEPENDENCY, HTTP_NOT_FOUND, HTTP_SERVER_ERROR } from './const'
17 import { SendListReadable } from './SendList'
18 -import produce from 'immer'
18
19 const apis: ApiHandlers = {
20
@@ -192,9 +191,7 @@ function serialize(p: Readonly<Plugin> | AvailablePlugin) {
191 if (typeof o.repo === 'object') // custom repo
192 o.repo = o.repo.web
193 o.log = 'log' in p && p.log?.length > 0
195 - o = produce(o, (o: any) => {
196 - _.each(o.config, x => x.showIf &&= String(x.showIf))
197 - })
194 + o.config &&= JSON.stringify(o.config, (_k, v) => _.isFunction(v) ? String(v) : v) // allow simple functions
195 return _.defaults(o, { started: null, badApi: null }) // nulls should be used to be sure to overwrite previous values,
196 }
197
src/cross.ts
+1 -1
@@ -38,7 +38,7 @@ export type Falsy = false | null | undefined | '' | 0
38 type Truthy<T> = T extends false | '' | 0 | null | undefined | void ? never : T
39 export type Callback<IN=void, OUT=void> = (x:IN) => OUT
40 export type Promisable<T> = T | Promise<T>
41 -export type Functionable<T> = T | ((...args: any[]) => T)
41 +export type Functionable<T, Args extends any[] = any[]> = T | ((...args: Args) => T)
42 export type Timeout = ReturnType<typeof setTimeout>
43 export interface VfsPerms {
44 can_see?: Who