feat(M3): Dashboard-Builder — Edit-Modus, Drag&Drop, Palette, Tabs (#361)
- DashboardBuilder: @dnd-kit 12-Spalten-Flow-Grid (seed-konsistent), View/Edit-Schalter, Resize, Tab-Verwaltung, Dashboard-CRUD + Set-Default, Dirty-Save - MiniAppHost ersetzt DashboardWidgetLoader (lazy Registry + settings-Props); Palette nur renderbare Apps (component-Filter) - WidgetSettingsForm generisch aus settings_schema; Bestands-Widgets settings-fähig (RecentContacts: limit) - api/miniapps.ts + api/dashboards.ts (TanStack-Query-Hooks, documents.ts-Muster) - Dashboard.tsx = Builder-Host (StatCards/SystemMetrics bleiben bis M4); Legacy-Grid/Loader gelöscht, Geister-Test ersetzt - Tests: Builder 13/13, Page 11/11, i18n de/en, tsc clean, build OK
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Personal dashboards API hooks (Phase M2/M3).
|
||||
*
|
||||
* Layout shape (validated server-side, DashboardLayout):
|
||||
* { version: 1, tabs: [{ id, name, widgets: [{ app_id, settings, col, row, col_span, row_span }] }] }
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from './client';
|
||||
|
||||
export interface DashboardWidget {
|
||||
app_id: string;
|
||||
settings: Record<string, unknown>;
|
||||
col: number;
|
||||
row: number;
|
||||
col_span: number;
|
||||
row_span: number;
|
||||
}
|
||||
|
||||
export interface DashboardTab {
|
||||
id: string;
|
||||
name: string;
|
||||
widgets: DashboardWidget[];
|
||||
}
|
||||
|
||||
export interface DashboardLayout {
|
||||
version: number;
|
||||
tabs: DashboardTab[];
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
id: string;
|
||||
name: string;
|
||||
layout: DashboardLayout;
|
||||
is_default: boolean;
|
||||
user_id: string;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface DashboardInput {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DashboardUpdateInput {
|
||||
name?: string;
|
||||
layout?: DashboardLayout;
|
||||
}
|
||||
|
||||
export const DASHBOARD_QUERY_KEYS = {
|
||||
all: ['dashboards'] as const,
|
||||
one: (id: string) => ['dashboards', id] as const,
|
||||
};
|
||||
|
||||
export function useDashboards() {
|
||||
return useQuery({
|
||||
queryKey: DASHBOARD_QUERY_KEYS.all,
|
||||
queryFn: () => apiGet<Dashboard[]>('/dashboards'),
|
||||
staleTime: 30 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateDashboard() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (input: DashboardInput) => apiPost<Dashboard>('/dashboards', input),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: DASHBOARD_QUERY_KEYS.all }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateDashboard() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, input }: { id: string; input: DashboardUpdateInput }) =>
|
||||
apiPut<Dashboard>(`/dashboards/${id}`, input),
|
||||
onSuccess: (_data, vars) => {
|
||||
qc.invalidateQueries({ queryKey: DASHBOARD_QUERY_KEYS.all });
|
||||
qc.invalidateQueries({ queryKey: DASHBOARD_QUERY_KEYS.one(vars.id) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteDashboard() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => apiDelete<void>(`/dashboards/${id}`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: DASHBOARD_QUERY_KEYS.all }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSetDefaultDashboard() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => apiPost<Dashboard>(`/dashboards/${id}/set-default`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: DASHBOARD_QUERY_KEYS.all }),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* MiniApps API hooks (Phase M1/M3).
|
||||
*
|
||||
* The universal MiniApp registry listing is permission-filtered
|
||||
* server-side; hosts additionally filter by ?host= and by whether a
|
||||
* frontend component is available (chat-only interaction apps have no
|
||||
* component path and cannot be placed on a dashboard).
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiGet } from './client';
|
||||
|
||||
export interface MiniAppField {
|
||||
name: string;
|
||||
label?: string;
|
||||
type?: string; // text | number | boolean | select
|
||||
default?: unknown;
|
||||
options?: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
export interface MiniAppDef {
|
||||
app_id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
plugin_name: string;
|
||||
render_schema: Record<string, unknown>;
|
||||
permission: string;
|
||||
settings_schema: { fields?: MiniAppField[] } & Record<string, unknown>;
|
||||
col_span: number;
|
||||
row_span: number;
|
||||
hosts: string[];
|
||||
component: string;
|
||||
order: number;
|
||||
builtin: boolean;
|
||||
}
|
||||
|
||||
export interface MiniAppsResponse {
|
||||
items: MiniAppDef[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const MINIAPP_QUERY_KEYS = {
|
||||
all: ['miniapps'] as const,
|
||||
host: (host: string) => ['miniapps', host] as const,
|
||||
};
|
||||
|
||||
export function useMiniapps(host?: string) {
|
||||
return useQuery({
|
||||
queryKey: MINIAPP_QUERY_KEYS.host(host ?? 'all'),
|
||||
queryFn: () =>
|
||||
apiGet<MiniAppsResponse>(host ? `/miniapps?host=${encodeURIComponent(host)}` : '/miniapps'),
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Apps that can actually be rendered on a dashboard (component present). */
|
||||
export function renderableDashboardApps(apps: MiniAppDef[] | undefined): MiniAppDef[] {
|
||||
return (apps ?? []).filter((a) => Boolean(a.component));
|
||||
}
|
||||
Reference in New Issue
Block a user