feat(N2): Dynamischer Scope-Editor — WorkspaceScopeEditor ersetzt JSON-Textarea (#366)

- WorkspaceScopeEditor.tsx: generisches Filter-UI aus /scope-definitions (multiselect mit value_source-Fetch, select mit Keine-Einschränkung-Placeholder, toggle) — WidgetSettingsForm-Philosophie
- resolveScopeItems: Wertequellen-Auflösung (items-Wrapper, Root-Listen, DMS-Ordner-Baum-Flattening), nie-crashend
- Hooks: useWorkspaceScopeDefinitions + useScopeValues (TanStack Query, staleTime 60s)
- WorkspaceManager: JSON-Textarea entfernt, Scope-Editor inline pro sichtbarem Modul, Speicherung in workspace_modules.config
- i18n: workspaces.scopeEditor.* 5 Keys de/en (Security-Invariante im UI: nichts ausgewählt = keine Einschränkung)
- Tests: 21/21 (TDD rot 4→grün), tsc clean, Production-Build OK
This commit is contained in:
Agent Zero
2026-09-01 08:31:30 +02:00
parent c25356c257
commit b40adfdd3a
8 changed files with 698 additions and 30 deletions
+92
View File
@@ -216,3 +216,95 @@ export function useSetDefaultWorkspace() {
},
});
}
// ─── N2: Workspace Scope-Registry (Phase N) ───────────────────
export interface ScopeOption {
value: string;
label: string;
}
export interface ScopeValueSource {
endpoint: string;
items_path: string;
value_key: string;
label_key: string;
}
export interface ScopeDimension {
key: string;
label: string;
control: 'multiselect' | 'select' | 'toggle';
options: ScopeOption[];
value_source: ScopeValueSource | null;
default: boolean | string | null;
}
export interface WorkspaceScopeDefinitions {
modules: Record<string, ScopeDimension[]>;
}
export function useWorkspaceScopeDefinitions() {
return useQuery<WorkspaceScopeDefinitions>({
queryKey: ['workspace-scope-definitions'],
queryFn: () => apiGet('/api/v1/workspaces/scope-definitions'),
});
}
export interface ScopeItem {
value: string;
label: string;
}
/**
* Resolve selectable items from a value-source response (N2).
*
* Supported shapes (all verified live in N1):
* - items_path "items" → { items: [...] } wrapper (contact-folders)
* - items_path "" → root list (mail accounts, calendars, saved-views)
* - root list with nested `children` arrays (dms folder tree) → flattened
*
* Never throws: malformed responses resolve to an empty list so the
* editor degrades gracefully instead of crashing.
*/
export function resolveScopeItems(
response: unknown,
itemsPath: string,
valueKey: string,
labelKey: string,
): ScopeItem[] {
let raw: unknown = response;
if (itemsPath) {
if (typeof response !== 'object' || response === null) return [];
raw = (response as Record<string, unknown>)[itemsPath];
}
if (!Array.isArray(raw)) return [];
const items: ScopeItem[] = [];
const walk = (entry: unknown): void => {
if (typeof entry !== 'object' || entry === null) return;
const record = entry as Record<string, unknown>;
const value = record[valueKey];
const label = record[labelKey];
if (value !== undefined && value !== null) {
items.push({ value: String(value), label: String(label ?? value) });
}
const children = record['children'];
if (Array.isArray(children)) children.forEach(walk);
};
raw.forEach(walk);
return items;
}
export function useScopeValues(source: ScopeValueSource | null) {
return useQuery<ScopeItem[]>({
queryKey: ['workspace-scope-values', source?.endpoint ?? null],
queryFn: async () => {
if (!source) return [];
const response = await apiGet<unknown>(source.endpoint);
return resolveScopeItems(response, source.items_path, source.value_key, source.label_key);
},
enabled: !!source,
staleTime: 60_000,
});
}