Skip to content

Commit 5834ee9

Browse files
authored
Clean up generic widget handling code in the frontend (#3856)
* Clean up WidgetSpan component code to avoid the giant if-chain for choosing a widget * Improve typing support
1 parent 8a75c0c commit 5834ee9

File tree

2 files changed

+193
-146
lines changed

2 files changed

+193
-146
lines changed

frontend/src/components/widgets/WidgetSpan.svelte

Lines changed: 181 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import { getContext } from "svelte";
33
44
import type { Editor } from "@graphite/editor";
5-
import type { LayoutTarget, WidgetInstance, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages";
6-
import { narrowWidgetProps, isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/messages";
5+
import type { LayoutTarget, WidgetInstance, WidgetPropsNames, WidgetPropsSet, WidgetTypes, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages";
6+
import { isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/messages";
77
import { debouncer } from "@graphite/utility-functions/debounce";
88
99
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
@@ -71,134 +71,189 @@
7171
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
7272
}
7373
74-
// TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283
75-
function exclude<T extends object>(props: T, additional?: (keyof T)[]): Omit<T, typeof additional extends Array<infer K> ? K : never> {
76-
const exclusions = ["kind", ...(additional || [])];
74+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
75+
function exclude(props: WidgetPropsSet, additional?: string[]): Record<string, any> {
76+
const exclusions = new Set(["kind", ...(additional || [])]);
77+
return Object.fromEntries(Object.entries(props).filter(([key]) => !exclusions.has(key)));
78+
}
7779
80+
type WidgetConfig = {
7881
// eslint-disable-next-line @typescript-eslint/no-explicit-any
79-
return Object.fromEntries(Object.entries(props).filter((entry) => !exclusions.includes(entry[0]))) as any;
80-
}
81-
</script>
82+
component: any;
83+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
84+
getProps(props: WidgetPropsSet, widgetIndex: number): Record<string, any> | undefined;
85+
getSlotContent?(props: WidgetPropsSet): string;
86+
};
8287
83-
<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components -->
88+
const widgetRegistry: Record<WidgetPropsNames, WidgetConfig> = {
89+
CheckboxInput: {
90+
component: CheckboxInput,
91+
getProps: (props: WidgetTypes["CheckboxInput"], index) => ({
92+
...exclude(props),
93+
$$events: { checked: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) },
94+
}),
95+
},
96+
ColorInput: {
97+
component: ColorInput,
98+
getProps: (props: WidgetTypes["ColorInput"], index) => ({
99+
...exclude(props),
100+
$$events: {
101+
value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
102+
startHistoryTransaction: () => widgetValueCommit(index, props.value),
103+
},
104+
}),
105+
},
106+
CurveInput: {
107+
// TODO: CurvesInput is currently unused
108+
component: CurveInput,
109+
getProps: (props: WidgetTypes["CurveInput"], index) => ({
110+
...exclude(props),
111+
$$events: {
112+
value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueCommitAndUpdate(index, value, false), { debounceTime: 120 }).debounceUpdateValue(e.detail),
113+
},
114+
}),
115+
},
116+
DropdownInput: {
117+
component: DropdownInput,
118+
getProps: (props: WidgetTypes["DropdownInput"], index) => ({
119+
...exclude(props),
120+
$$events: {
121+
hoverInEntry: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
122+
hoverOutEntry: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
123+
selectedIndex: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true),
124+
},
125+
}),
126+
},
127+
ParameterExposeButton: {
128+
component: ParameterExposeButton,
129+
getProps: (props: WidgetTypes["ParameterExposeButton"], index) => ({
130+
...exclude(props),
131+
action: () => widgetValueCommitAndUpdate(index, undefined, true),
132+
}),
133+
},
134+
IconButton: {
135+
component: IconButton,
136+
getProps: (props: WidgetTypes["IconButton"], index) => ({
137+
...exclude(props),
138+
action: () => widgetValueCommitAndUpdate(index, undefined, true),
139+
}),
140+
},
141+
IconLabel: {
142+
component: IconLabel,
143+
getProps: (props: WidgetTypes["IconLabel"]) => exclude(props),
144+
},
145+
ShortcutLabel: {
146+
component: ShortcutLabel,
147+
getProps: (props: WidgetTypes["ShortcutLabel"]) => {
148+
if (!props.shortcut) return undefined;
149+
return exclude(props);
150+
},
151+
},
152+
ImageLabel: {
153+
component: ImageLabel,
154+
getProps: (props: WidgetTypes["ImageLabel"]) => exclude(props),
155+
},
156+
ImageButton: {
157+
component: ImageButton,
158+
getProps: (props: WidgetTypes["ImageButton"], index) => ({
159+
...exclude(props),
160+
action: () => widgetValueCommitAndUpdate(index, undefined, true),
161+
}),
162+
},
163+
NodeCatalog: {
164+
component: NodeCatalog,
165+
getProps: (props: WidgetTypes["NodeCatalog"], index) => ({
166+
...exclude(props),
167+
$$events: { selectNodeType: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) },
168+
}),
169+
},
170+
NumberInput: {
171+
component: NumberInput,
172+
getProps: (props: WidgetTypes["NumberInput"], index) => ({
173+
...exclude(props),
174+
incrementCallbackIncrease: () => widgetValueCommitAndUpdate(index, "Increment", false),
175+
incrementCallbackDecrease: () => widgetValueCommitAndUpdate(index, "Decrement", false),
176+
$$events: {
177+
value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueUpdate(index, value, true)).debounceUpdateValue(e.detail),
178+
startHistoryTransaction: () => widgetValueCommit(index, props.value),
179+
},
180+
}),
181+
},
182+
ReferencePointInput: {
183+
component: ReferencePointInput,
184+
getProps: (props: WidgetTypes["ReferencePointInput"], index) => ({
185+
...exclude(props),
186+
$$events: { value: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) },
187+
}),
188+
},
189+
PopoverButton: {
190+
component: PopoverButton,
191+
getProps: (props: WidgetTypes["PopoverButton"]) => ({
192+
...exclude(props),
193+
layoutTarget,
194+
}),
195+
},
196+
RadioInput: {
197+
component: RadioInput,
198+
getProps: (props: WidgetTypes["RadioInput"], index) => ({
199+
...exclude(props),
200+
$$events: { selectedIndex: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) },
201+
}),
202+
},
203+
Separator: {
204+
component: Separator,
205+
getProps: (props: WidgetTypes["Separator"]) => exclude(props),
206+
},
207+
WorkingColorsInput: {
208+
component: WorkingColorsInput,
209+
getProps: (props: WidgetTypes["WorkingColorsInput"]) => exclude(props),
210+
},
211+
TextAreaInput: {
212+
component: TextAreaInput,
213+
getProps: (props: WidgetTypes["TextAreaInput"], index) => ({
214+
...exclude(props),
215+
$$events: { commitText: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) },
216+
}),
217+
},
218+
TextButton: {
219+
component: TextButton,
220+
getProps: (props: WidgetTypes["TextButton"], index) => ({
221+
...exclude(props),
222+
action: () => widgetValueCommitAndUpdate(index, [], true),
223+
$$events: { selectedEntryValuePath: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) },
224+
}),
225+
},
226+
BreadcrumbTrailButtons: {
227+
component: BreadcrumbTrailButtons,
228+
getProps: (props: WidgetTypes["BreadcrumbTrailButtons"], index) => ({
229+
...exclude(props),
230+
action: (breadcrumbIndex: number) => widgetValueCommitAndUpdate(index, breadcrumbIndex, true),
231+
}),
232+
},
233+
TextInput: {
234+
component: TextInput,
235+
getProps: (props: WidgetTypes["TextInput"], index) => ({
236+
...exclude(props),
237+
$$events: { commitText: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) },
238+
}),
239+
},
240+
TextLabel: {
241+
component: TextLabel,
242+
getProps: (props: WidgetTypes["TextLabel"]) => exclude(props, ["value"]),
243+
getSlotContent: (props: WidgetTypes["TextLabel"]) => props.value,
244+
},
245+
};
246+
</script>
84247

85248
<div class={`widget-span ${className} ${extraClasses}`.trim()} class:narrow class:row={direction === "row"} class:column={direction === "column"}>
86-
{#each widgets as component, widgetIndex}
87-
{@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")}
88-
{#if checkboxInput}
89-
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
90-
{/if}
91-
{@const colorInput = narrowWidgetProps(component.props, "ColorInput")}
92-
{#if colorInput}
93-
<ColorInput
94-
{...exclude(colorInput)}
95-
on:value={({ detail }) => widgetValueUpdate(widgetIndex, detail, false)}
96-
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, colorInput.value)}
97-
/>
98-
{/if}
99-
<!-- TODO: Curves Input is currently unused -->
100-
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
101-
{#if curvesInput}
102-
<CurveInput
103-
{...exclude(curvesInput)}
104-
on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(widgetIndex, value, false), { debounceTime: 120 }).debounceUpdateValue(detail)}
105-
/>
106-
{/if}
107-
{@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")}
108-
{#if dropdownInput}
109-
<DropdownInput
110-
{...exclude(dropdownInput)}
111-
on:hoverInEntry={({ detail }) => {
112-
return widgetValueUpdate(widgetIndex, detail, false);
113-
}}
114-
on:hoverOutEntry={({ detail }) => {
115-
return widgetValueUpdate(widgetIndex, detail, false);
116-
}}
117-
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)}
118-
/>
119-
{/if}
120-
{@const parameterExposeButton = narrowWidgetProps(component.props, "ParameterExposeButton")}
121-
{#if parameterExposeButton}
122-
<ParameterExposeButton {...exclude(parameterExposeButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
123-
{/if}
124-
{@const iconButton = narrowWidgetProps(component.props, "IconButton")}
125-
{#if iconButton}
126-
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
127-
{/if}
128-
{@const iconLabel = narrowWidgetProps(component.props, "IconLabel")}
129-
{#if iconLabel}
130-
<IconLabel {...exclude(iconLabel)} />
131-
{/if}
132-
{@const shortcutLabel = narrowWidgetProps(component.props, "ShortcutLabel")}
133-
{@const shortcutLabelShortcut = shortcutLabel?.shortcut ? { ...shortcutLabel, shortcut: shortcutLabel.shortcut } : undefined}
134-
{#if shortcutLabel && shortcutLabelShortcut}
135-
<ShortcutLabel {...exclude(shortcutLabelShortcut)} />
136-
{/if}
137-
{@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")}
138-
{#if imageLabel}
139-
<ImageLabel {...exclude(imageLabel)} />
140-
{/if}
141-
{@const imageButton = narrowWidgetProps(component.props, "ImageButton")}
142-
{#if imageButton}
143-
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
144-
{/if}
145-
{@const nodeCatalog = narrowWidgetProps(component.props, "NodeCatalog")}
146-
{#if nodeCatalog}
147-
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(widgetIndex, e.detail, false)} />
148-
{/if}
149-
{@const numberInput = narrowWidgetProps(component.props, "NumberInput")}
150-
{#if numberInput}
151-
<NumberInput
152-
{...exclude(numberInput)}
153-
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(widgetIndex, value, true)).debounceUpdateValue(detail)}
154-
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, numberInput.value)}
155-
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(widgetIndex, "Increment", false)}
156-
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(widgetIndex, "Decrement", false)}
157-
/>
158-
{/if}
159-
{@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")}
160-
{#if referencePointInput}
161-
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
162-
{/if}
163-
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
164-
{#if popoverButton}
165-
<PopoverButton {...exclude(popoverButton)} {layoutTarget} />
166-
{/if}
167-
{@const radioInput = narrowWidgetProps(component.props, "RadioInput")}
168-
{#if radioInput}
169-
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
170-
{/if}
171-
{@const separator = narrowWidgetProps(component.props, "Separator")}
172-
{#if separator}
173-
<Separator {...exclude(separator)} />
174-
{/if}
175-
{@const workingColorsInput = narrowWidgetProps(component.props, "WorkingColorsInput")}
176-
{#if workingColorsInput}
177-
<WorkingColorsInput {...exclude(workingColorsInput)} />
178-
{/if}
179-
{@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")}
180-
{#if textAreaInput}
181-
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)} />
182-
{/if}
183-
{@const textButton = narrowWidgetProps(component.props, "TextButton")}
184-
{#if textButton}
185-
<TextButton
186-
{...exclude(textButton)}
187-
action={() => widgetValueCommitAndUpdate(widgetIndex, [], true)}
188-
on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)}
189-
/>
190-
{/if}
191-
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
192-
{#if breadcrumbTrailButtons}
193-
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(widgetIndex, breadcrumbIndex, true)} />
194-
{/if}
195-
{@const textInput = narrowWidgetProps(component.props, "TextInput")}
196-
{#if textInput}
197-
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
198-
{/if}
199-
{@const textLabel = narrowWidgetProps(component.props, "TextLabel")}
200-
{#if textLabel}
201-
<TextLabel {...exclude(textLabel, ["value"])}>{textLabel.value}</TextLabel>
249+
{#each widgets as widget, widgetIndex}
250+
{@const config = widgetRegistry[widget.props.kind]}
251+
{@const props = config?.getProps(widget.props, widgetIndex)}
252+
{@const slot = config?.getSlotContent?.(widget.props)}
253+
{#if props !== undefined && slot !== undefined}
254+
<svelte:component this={config.component} {...props}>{slot}</svelte:component>
255+
{:else if props !== undefined}
256+
<svelte:component this={config.component} {...props} />
202257
{/if}
203258
{/each}
204259
</div>
@@ -213,8 +268,8 @@
213268
.widget-span.row {
214269
flex: 0 0 auto;
215270
display: flex;
216-
--row-height: 32px;
217271
min-height: var(--row-height);
272+
--row-height: 32px;
218273
219274
&.narrow {
220275
--row-height: 24px;

0 commit comments

Comments
 (0)