feat: replace core homepage widget registry with generic extension points

This commit is contained in:
LukeGus committed 2026-09-29 13:23:51 -05:00
1 parent b8bbcd2196
commit 50d43e29fb
18 files changed
+242 -147

No files matched your search

+6 -2
View File
@@ -10,7 +10,11 @@ import { HomepagePreviewCard } from "./HomepagePreviewCard.js";
import { ServiceLinksCard } from "./ServiceLinksCard.js";
import { DashboardHomepageView } from "./DashboardHomepageView.js";
import { setHomepageApi } from "./api.js";
import { drainQueuedWidgets } from "./widgets/WidgetRegistry.js";
import {
WIDGET_POINT,
drainQueuedWidgets,
toExtension,
} from "./widgets/WidgetRegistry.js";
// Side-effect imports so widgets register themselves.
import "./widgets/ServiceLinkWidget";
@@ -54,7 +58,7 @@ export function activate(app: TermixApp): void {
app.onDispose(() => setHomepageApi(null));
for (const widget of drainQueuedWidgets()) {
app.registerHomepageWidget(widget as never);
app.registerExtension(WIDGET_POINT, toExtension(widget));
}
app.registerTab("homepage", HomepageTab as ComponentType<TabProps>, {
@@ -1,16 +1,22 @@
import {
homepageWidgetType,
useHomepageWidgetTypes,
type HomepageWidgetContribution,
getExtension,
useExtensions,
type ExtensionContribution,
} from "@termix/plugin-sdk/frontend";
import { useMemo } from "react";
import type { WidgetTypeDefinition, WidgetTypeId } from "../types.js";
/**
* Other plugins add widgets with app.registerExtension(WIDGET_POINT, ...),
* putting the widget under components.view and its edit form under
* components.editForm. Core only stores the list, so a plugin's widgets
* stay registered while this plugin is off.
*/
export const WIDGET_POINT = "homepage.widgets";
/**
* Each widget module calls this at import time (before activate(app) runs),
* so the definitions are queued here and app.registerHomepageWidget is
* called for each one from activate. Core owns the actual registry, the same
* one other plugins (docker, tunnels, host-metrics, file-manager) register
* their own widgets into.
* so the definitions are queued here and registered from activate.
*/
const queued: WidgetTypeDefinition[] = [];
@@ -23,18 +29,42 @@ export function drainQueuedWidgets(): WidgetTypeDefinition[] {
return queued.splice(0, queued.length);
}
export function toExtension(def: WidgetTypeDefinition): ExtensionContribution {
const { component, editFormComponent, ...rest } = def;
return {
...rest,
components: editFormComponent
? { view: component, editForm: editFormComponent }
: { view: component },
};
}
function toDefinition(
widget: HomepageWidgetContribution | undefined,
extension: ExtensionContribution | undefined,
): WidgetTypeDefinition | undefined {
return widget as unknown as WidgetTypeDefinition | undefined;
const view = extension?.components?.view;
if (!extension || !view) return undefined;
const { components, ...rest } = extension;
return {
...(rest as unknown as WidgetTypeDefinition),
component: view,
editFormComponent: components?.editForm,
};
}
export function getWidgetType(
id: WidgetTypeId,
): WidgetTypeDefinition | undefined {
return toDefinition(homepageWidgetType(id));
return toDefinition(getExtension(WIDGET_POINT, id));
}
export function useWidgetTypes(): WidgetTypeDefinition[] {
return useHomepageWidgetTypes() as unknown as WidgetTypeDefinition[];
const extensions = useExtensions(WIDGET_POINT);
return useMemo(
() =>
extensions
.map(toDefinition)
.filter((def): def is WidgetTypeDefinition => !!def),
[extensions],
);
}