-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TELESTION-462 Make widgets configurable
Uses a context to make widgets configurable. While currently, configuration is only possible in the edit dashboard page, this context can, in the future, be used to also allow editing the configuration in other places, such as within the widget itself. For convenience, components are provided for basic confiugration fields such as textfields and checkboxes. This makes configurability as easy as this: ```tsx { ... configElement: ( <WidgetConfigWrapper> <WidgetConfigCheckboxField label={'Bool value'} name={'bool'} /> <WidgetConfigTextField label={'Test Text'} name={'text'} /> </WidgetConfigWrapper> ) } ``` It is also possible to create custom configuration fields (using `useConfigureWidgetField(name, validator)`) or even fully custom configuration UIs (using `useConfigureWidget()`). Both of these hooks return both the current configuration and a function that works the same way a `useState()`-setter works. Note that any congiuration passed into or out of the confiuration controls automatically, controlled by the context, get validated using the widget's `createConfig` function. Example of using the `useConfiugreWidgetField()` hook: ```tsx function WidgetConfigTextField(props: { label: string; name: string }) { const [value, setValue] = useConfigureWidgetField(props.name, s => z.string().parse(s) ); return ( <FormGroup className={'mb-3'}> <FormLabel>{props.label}</FormLabel> <FormControl data-name={props.name} value={value} onChange={e => setValue(e.target.value)} /> </FormGroup> ); } ``` Everything related to widget configuration can be imported from `@wuespace/telestion/widget`. Note that this also adjusts the user data to use a `Record<string, jsonSchema>` instead of a `Record<string, unknown>` as the widget instance configuration type. The `jsonSchema` implementation is taken from the zod documentation (`README.md`) wiwhere https://github.com/ggoodman is credited; thank you for this great implementation!
- Loading branch information
Showing
12 changed files
with
228 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,4 @@ | |
*/ | ||
export * from './model.ts'; | ||
export * from './state.ts'; | ||
export { jsonSchema } from './json-schema.ts'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { z } from 'zod'; | ||
|
||
// Source: zod's `README.md`, crediting https://github.com/ggoodman | ||
|
||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); | ||
type Literal = z.infer<typeof literalSchema>; | ||
type Json = Literal | { [key: string]: Json } | Json[]; | ||
export const jsonSchema: z.ZodType<Json> = z.lazy(() => | ||
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
frontend-react/src/lib/widget/configuration/configuration-context.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { createContext, SetStateAction, useContext } from 'react'; | ||
import { | ||
BaseWidgetConfiguration, | ||
WidgetConfigurationContextValue | ||
} from '@wuespace/telestion/widget/configuration/model.tsx'; | ||
import { Widget } from '@wuespace/telestion'; | ||
|
||
const WidgetConfigurationContext = | ||
createContext<WidgetConfigurationContextValue>({ | ||
get configuration(): never { | ||
throw new Error( | ||
'Widget configuration controls can only be accessed inside a widget configuration context.' | ||
); | ||
}, | ||
setConfiguration: (): never => { | ||
throw new Error( | ||
'Widget configuration controls can only be set inside a widget configuration context.' | ||
); | ||
} | ||
}); | ||
|
||
export function useConfigureWidget() { | ||
const { configuration, setConfiguration } = useContext( | ||
WidgetConfigurationContext | ||
); | ||
|
||
return [configuration, setConfiguration] as const; | ||
} | ||
|
||
export function WidgetConfigurationContextProvider(props: { | ||
configuration: BaseWidgetConfiguration; | ||
setConfiguration: (s: BaseWidgetConfiguration) => void; | ||
createConfig: Widget['createConfig']; | ||
children: React.ReactNode; | ||
}) { | ||
const onSetConfiguration = ( | ||
newConfig: SetStateAction<BaseWidgetConfiguration> | ||
) => { | ||
newConfig = | ||
typeof newConfig === 'function' | ||
? newConfig(props.configuration) | ||
: newConfig; | ||
newConfig = props.createConfig(newConfig); | ||
props.setConfiguration(newConfig); | ||
}; | ||
|
||
return ( | ||
<WidgetConfigurationContext.Provider | ||
value={{ | ||
configuration: props.createConfig(props.configuration), | ||
setConfiguration: onSetConfiguration | ||
}} | ||
> | ||
{props.children} | ||
</WidgetConfigurationContext.Provider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { BaseWidgetConfiguration } from './model.tsx'; | ||
import { useConfigureWidget } from './configuration-context.tsx'; | ||
import { SetStateAction, useMemo } from 'react'; | ||
|
||
export function useConfigureWidgetField< | ||
T extends BaseWidgetConfiguration[string] | ||
>(name: string, validator: (v: unknown) => T) { | ||
const [widgetConfiguration, setValue] = useConfigureWidget(); | ||
return useMemo(() => { | ||
const onSetValue = (newValue: SetStateAction<T>) => | ||
setValue(oldWidgetConfiguration => { | ||
try { | ||
if (typeof newValue === 'function') | ||
newValue = newValue(validator(oldWidgetConfiguration[name])); | ||
newValue = validator(newValue); | ||
return { ...oldWidgetConfiguration, [name]: newValue }; | ||
} catch (e) { | ||
if (e instanceof Error) | ||
throw new Error( | ||
`Type error while trying to set widget configuration field "${name}". Details: ${e.message}` | ||
); | ||
else throw e; | ||
} | ||
}); | ||
|
||
try { | ||
const validatedField = validator(widgetConfiguration[name]); | ||
return [validatedField, onSetValue] as const; | ||
} catch (e) { | ||
if (e instanceof Error) | ||
throw new Error( | ||
`Widget configuration does not contain a property named "${name}". Please adjust your createConfig function. Details: ${e.message}` | ||
); | ||
else throw e; | ||
} | ||
}, [name, validator, widgetConfiguration, setValue]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { z } from 'zod'; | ||
import { FormCheck, FormControl, FormGroup, FormLabel } from 'react-bootstrap'; | ||
import { useConfigureWidgetField } from './hooks.tsx'; | ||
import { ReactNode } from 'react'; | ||
|
||
export * from './hooks.tsx'; | ||
export { useConfigureWidget } from './configuration-context.tsx'; | ||
|
||
// Helper components | ||
export function WidgetConfigWrapper({ children }: { children: ReactNode }) { | ||
return <section className={'px-3'}>{children}</section>; | ||
} | ||
|
||
export function WidgetConfigCheckboxField(props: { | ||
label: string; | ||
name: string; | ||
}) { | ||
const [checked, setChecked] = useConfigureWidgetField(props.name, b => | ||
z.boolean().parse(b) | ||
); | ||
|
||
return ( | ||
<FormGroup className={'mb-3'}> | ||
<FormCheck | ||
data-name={props.name} | ||
label={props.label} | ||
checked={checked} | ||
onChange={e => setChecked(e.target.checked)} | ||
/> | ||
</FormGroup> | ||
); | ||
} | ||
|
||
export function WidgetConfigTextField(props: { label: string; name: string }) { | ||
const [value, setValue] = useConfigureWidgetField(props.name, s => | ||
z.string().parse(s) | ||
); | ||
|
||
return ( | ||
<FormGroup className={'mb-3'}> | ||
<FormLabel>{props.label}</FormLabel> | ||
<FormControl | ||
data-name={props.name} | ||
value={value} | ||
onChange={e => setValue(e.target.value)} | ||
/> | ||
</FormGroup> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { z } from 'zod'; | ||
import { SetStateAction } from 'react'; | ||
import { widgetInstanceSchema } from '@wuespace/telestion/user-data'; | ||
|
||
export type BaseWidgetConfiguration = z.infer< | ||
typeof widgetInstanceSchema.shape.configuration | ||
>; | ||
|
||
export interface WidgetConfigurationContextValue< | ||
T extends BaseWidgetConfiguration = BaseWidgetConfiguration | ||
> { | ||
configuration: T; | ||
setConfiguration: (s: SetStateAction<T>) => void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters