feat: strengthen module contracts and shared WebUI runtime

This commit is contained in:
2026-07-29 14:16:28 +02:00
parent 53e947935a
commit 68328f3d8e
57 changed files with 4358 additions and 371 deletions

View File

@@ -0,0 +1,119 @@
import {
useEffect,
useState,
type ReactNode
} from "react";
import { Link } from "react-router-dom";
import { adminErrorMessage } from "./admin/adminUtils";
export type DashboardWidgetDataState<T> = {
data: T | null;
loading: boolean;
error: string;
};
export function useDashboardWidgetData<T>(
load: () => Promise<T>,
refreshKey: number
): DashboardWidgetDataState<T> {
const [state, setState] = useState<DashboardWidgetDataState<T>>({
data: null,
loading: true,
error: ""
});
useEffect(() => {
let active = true;
setState((current) => ({
...current,
loading: true,
error: ""
}));
void load()
.then((data) => {
if (active) setState({ data, loading: false, error: "" });
})
.catch((error: unknown) => {
if (!active) return;
setState((current) => ({
...current,
loading: false,
error: adminErrorMessage(error)
}));
});
return () => {
active = false;
};
}, [load, refreshKey]);
return state;
}
export type DashboardWidgetListItem = {
id: string;
title: ReactNode;
detail?: ReactNode;
meta?: ReactNode;
leading?: ReactNode;
trailing?: ReactNode;
to?: string;
};
export function DashboardWidgetList({
items,
emptyText
}: {
items: DashboardWidgetListItem[];
emptyText: ReactNode;
}) {
if (!items.length) {
return <p className="muted dashboard-contribution-empty">{emptyText}</p>;
}
return (
<ul className="dashboard-contribution-list">
{items.map((item) => {
const content = (
<>
{item.leading && (
<span className="dashboard-contribution-leading">
{item.leading}
</span>
)}
<span className="dashboard-contribution-copy">
<span className="dashboard-contribution-title">
{item.title}
</span>
{item.detail && (
<span className="dashboard-contribution-detail">
{item.detail}
</span>
)}
</span>
{item.meta && (
<span className="dashboard-contribution-meta">
{item.meta}
</span>
)}
{item.trailing && (
<span className="dashboard-contribution-trailing">
{item.trailing}
</span>
)}
</>
);
return (
<li key={item.id}>
{item.to ? (
<Link className="dashboard-contribution-row" to={item.to}>
{content}
</Link>
) : (
<div className="dashboard-contribution-row">{content}</div>
)}
</li>
);
})}
</ul>
);
}