@samitouri / QOSami-HFS / commits / e03f0e1e

plugins: support async for html-producing frontend events

Massimo Melina committed Mar 22, 2025 at 19:41 UTC e03f0e1ec657cbf8ae48eaaa55d88758773ab01d
2 files changed +30 -17
dev-plugins.md
+1
@@ -400,6 +400,7 @@ Some frontend events can return HTML, which can be expressed in several ways:
400 - as a ReactElement
401 - as an array of ReactNode
402 - `null`, `undefined`, `false`, and empty strings will be discarded
403 +- as a Promise for any of the above
404
405 These events will receive a `def` property (in addition event's specific properties),
406 with the default content that will be displayed if no callback return a valid output.
frontend/src/components.ts
+29 -17
@@ -1,12 +1,12 @@
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 {
4 - Callback, getHFS, hfsEvent, hIcon, Html, isPrimitive, onlyTruthy, prefix, noAriaTitle, formatBytes
4 + Callback, getHFS, hfsEvent, hIcon, Html, isPrimitive, onlyTruthy, prefix, noAriaTitle, formatBytes, useStateMounted
5 } from './misc'
6 import {
7 ButtonHTMLAttributes, ChangeEvent, createElement as h, CSSProperties, forwardRef, Fragment,
8 HTMLAttributes, InputHTMLAttributes, isValidElement, MouseEventHandler, ReactNode, SelectHTMLAttributes,
9 - useMemo, useState, ComponentPropsWithoutRef, LabelHTMLAttributes, useRef, ReactElement
9 + useEffect, useMemo, useState, ComponentPropsWithoutRef, LabelHTMLAttributes, useRef, ReactElement
10 } from 'react'
11 import _ from 'lodash'
12 import i18n from './i18n'
@@ -83,24 +83,36 @@ export function CustomCode({ name, children, render, ...props }: {
83 render?: Callback<ReactNode, ReactNode>
84 [k :string]: any,
85 }) {
86 - const result = useMemo(() => {
87 - props.def = children // not using 'default' because user can have unexpected error destructuring object
88 - let keep = true
89 - const ret = onlyTruthy(hfsEvent(name, props)
90 - .map((x, key) => isValidElement(x) ? h(Fragment, { key }, x)
86 + const raw = useMemo(() => hfsEvent(name, Object.assign(props, { def: children })), // not using 'default' as key, because user can have unexpected error destructuring object
87 + [name, children, ...props ? Object.values(props) : []])
88 + const [out, setOut] = useStateMounted<null | ReactNode[]>([])
89 + useEffect(() => {
90 + if (raw.some(x => x === null)) // null means skip this
91 + return setOut(null)
92 + const worked: ReactNode[] = raw.map(toElement)
93 + setOut(onlyTruthy(worked))
94 + raw.forEach((x, i) => {
95 + if (typeof x?.then === 'function') // thenable
96 + x.then((resolved: any) => {
97 + worked[i] = toElement(resolved, i)
98 + setOut(onlyTruthy(worked))
99 + }, () => {})
100 + })
101 + const html = getHFS().customHtml?.[name]
102 + if (html?.trim?.()) {
103 + worked.push(toElement(html, -1))
104 + setOut(onlyTruthy(worked))
105 + }
106 +
107 + function toElement(x: unknown, key: number) {
108 + return isValidElement(x) ? h(Fragment, { key }, x) // wrap to avoid console warnings
109 : x === 0 || x && isPrimitive(x) ? h(Html, { key }, String(x))
110 : _.isArray(x) ? h(Fragment, { key }, ...x)
93 - : x === null ? keep = false
94 - : null))
95 - if (!keep)
96 - return null
97 - const html = getHFS().customHtml?.[name]
98 - if (html?.trim?.())
99 - ret.push(h(Html, { key: 'x' }, html))
100 - return ret
101 - }, [name, children, ...props ? Object.values(props) : []])
111 + : null
112 + }
113 + }, [raw])
114 render ??= _.identity
103 - return h(Fragment, {}, render(result && (result.length || !children ? result : children)) )
115 + return h(Fragment, {}, render(out && (out?.length || !children ? out : children)) )
116 }
117
118 interface IconBtnOptions extends ButtonHTMLAttributes<any> { style?: any, title?: string }