plugins: support config as callback

Massimo Melina committed Mar 17, 2025 at 15:22 UTC a483aaf57e9e5587a98f2af36b33bfa10e5b0832
4 files changed +16 -8
admin/src/InstalledPlugins.ts
+11 -5
@@ -8,7 +8,8 @@ import {
8 Clear, Delete, Error as ErrorIcon, FormatPaint as ThemeIcon, ListAlt, PlayCircle, Settings, StopCircle, Upgrade
9 } from '@mui/icons-material'
10 import {
11 - CFG, Html, HTTP_FAILED_DEPENDENCY, md, newObj, prefix, with_, xlate, formatTime, formatDate, replaceStringToReact
11 + CFG, Html, HTTP_FAILED_DEPENDENCY, md, newObj, prefix, with_, xlate, formatTime, formatDate, replaceStringToReact,
12 + callable, tryJson
13 } from './misc'
14 import { alertDialog, confirmDialog, formDialog, toast } from './dialog'
15 import _ from 'lodash'
@@ -22,7 +23,9 @@ import VfsPathField from './VfsPathField'
23
24 // updates=true will show the "check updates" version of the page
25 export default function InstalledPlugins({ updates }: { updates?: true }) {
25 - const { list, error, setList, initializing } = useApiList(updates ? 'get_plugin_updates' : 'get_plugins')
26 + const { list, error, setList, initializing } = useApiList(updates ? 'get_plugin_updates' : 'get_plugins', {}, {
27 + map(x: any) { x.config &&= tryJson(x.config, s => eval('()=>('+s+')')()) }
28 + })
29 useEffect(() => {
30 setList(list =>
31 _.sortBy(list, x => (x.error ? 0 : x.started ? 1 : 2) + treatPluginName(x.id)))
@@ -136,7 +139,7 @@ export default function InstalledPlugins({ updates }: { updates?: true }) {
139 title: showOptions ? `Options for ${id}` : `Log for ${id}`,
140 form: values => ({
141 before: row.description && h(Box, { mx: 2, mb: 2 }, row.description),
139 - fields: makeFields(row.config || {}, values),
142 + fields: makeFields(callable(row.config, values) || {}, values),
143 save: showOptions ? { children: "Save and close" } : false,
144 barSx: { gap: 1 },
145 addToBar: [h(Btn, { variant: 'outlined', onClick: () => save(values) }, "Save")],
@@ -240,6 +243,7 @@ export function renderName({ row, value }: any) {
243
244 function makeFields(config: any, values: any) {
245 return Object.entries(config).map(([k,o]: [string,any]) => {
246 + if (!o) return
247 let { type, defaultValue, frontend, showIf, ...rest } = o
248 try {
249 if (typeof showIf === 'string') // compile once
@@ -251,8 +255,10 @@ function makeFields(config: any, values: any) {
255 rest.helperText &&= md(rest.helperText, { html: false })
256 const comp = (type2comp as any)[type] as Field<any> | undefined
257 if (comp === ArrayField) {
254 - rest.valuesForAdd = newObj(rest.fields, x => x.defaultValue)
255 - rest.fields = makeFields(rest.fields, false)
258 + const {fields} = rest
259 + rest.valuesForAdd = newObj(callable(fields, false), x => x.defaultValue)
260 + rest.fields = !_.isFunction(fields) ? makeFields(fields, false)
261 + : (values: unknown) => _.map(fields(values), (v,k) => v && ({ k, ...v, defaultValue: undefined })).filter(Boolean)
262 }
263 if (defaultValue !== undefined && type === 'boolean')
264 rest.placeholder = `Default value is ${JSON.stringify(defaultValue)}`
admin/src/api.ts
+1 -1
@@ -119,7 +119,7 @@ export function useApiList<T=any, S=T>(cmd:string|Falsy, params: Dict={}, { map,
119 return
120 }
121 if (op === LIST.update) {
122 - const change = msg[2]
122 + const change = map?.(msg[2]) ?? msg[2]
123 const found = _.find(bufferAdd, par)
124 if (found)
125 return Object.assign(found, change)
dev-plugins.md
+2 -1
@@ -126,7 +126,7 @@ used must be strictly JSON (thus, no single quotes, only double quotes for strin
126 - `onDirEntry: ({ entry: DirEntry, listUri: string, ctx, node: VfsNode }) => Promisable<void | false>`
127 by providing this callback you can manipulate the record that is sent to the frontend (`entry`),
128 or you can return false to exclude this entry from the results. Refer to source `frontend/src/state.ts`.
129 -- `config: { [key]: FieldDescriptor }` declare a set of admin-configurable values owned by the plugin
129 +- `config: { [key]: FieldDescriptor } | function` declare a set of admin-configurable values owned by the plugin
130 that will be displayed inside Admin-panel for change. Each property is identified by its key,
131 and the descriptor is another object with options about the field.
132
@@ -144,6 +144,7 @@ used must be strictly JSON (thus, no single quotes, only double quotes for strin
144
145 When necessary your plugin will read its value using `api.getConfig('message')` in the backend,
146 or `HFS.getPluginConfig('message')` in the frontend, but the latter must be enabled using the `frontend` flag in the config.
147 + To handle more complex cases, you can pass a function to `config` instead of an object. The function will receive a parameter `values`.
148
149 - `configDialog: DialogOptions` object to override dialog options. Please refer to sources for details.
150 - `onFrontendConfig: (config: object) => void | object` manipulate config values exposed to frontend.
src/api.plugins.ts
+2 -1
@@ -191,7 +191,8 @@ 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
194 - o.config &&= JSON.stringify(o.config, (_k, v) => _.isFunction(v) ? String(v) : v) // allow simple functions
194 + o.config &&= _.isFunction(o.config) ? String(o.config)
195 + : JSON.stringify(o.config, (_k, v) => _.isFunction(v) ? String(v) : v) // allow simple functions
196 return _.defaults(o, { started: null, badApi: null }) // nulls should be used to be sure to overwrite previous values,
197 }
198