+
- {m.is_visible && configEditingKey === m.module_key && (
-
diff --git a/frontend/src/components/settings/WorkspaceScopeEditor.tsx b/frontend/src/components/settings/WorkspaceScopeEditor.tsx
new file mode 100644
index 0000000..6c76ab0
--- /dev/null
+++ b/frontend/src/components/settings/WorkspaceScopeEditor.tsx
@@ -0,0 +1,166 @@
+/**
+ * WorkspaceScopeEditor — dynamic per-module scope filter UI (Phase N2).
+ *
+ * Renders controls from the N1 /scope-definitions contract: multiselects
+ * (static options OR fetched value_source), selects, toggles. Scope values
+ * are stored in workspace_modules.config — an EMPTY selection means NO
+ * restriction (Phase N invariant: scopes can only restrict, never grant).
+ *
+ * Generic like WidgetSettingsForm (M3): the component knows nothing about
+ * specific modules; plugins declare their dimensions via contracts.
+ */
+
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { useScopeValues, type ScopeDimension } from '@/api/hooks/workspaces';
+
+// Re-exported for consumers/tests — resolution logic lives with the hook.
+export { resolveScopeItems } from '@/api/hooks/workspaces';
+
+interface WorkspaceScopeEditorProps {
+ moduleKey: string;
+ dimensions: ScopeDimension[];
+ config: Record
;
+ onChange: (key: string, value: unknown) => void;
+}
+
+function MultiSelectDimension({
+ dimension,
+ selected,
+ onToggle,
+}: {
+ dimension: ScopeDimension;
+ selected: string[];
+ onToggle: (value: string) => void;
+}) {
+ const { t } = useTranslation();
+ const { data: fetched = [], isLoading, isError } = useScopeValues(dimension.value_source);
+
+ const options = dimension.options.length > 0 ? dimension.options : fetched;
+
+ if (isLoading) {
+ return …
;
+ }
+ if (isError) {
+ return (
+
+ {t('workspaces.scopeEditor.loadError', 'Werte konnten nicht geladen werden')}
+
+ );
+ }
+ if (options.length === 0) {
+ return (
+
+ {t('workspaces.scopeEditor.noValues', 'Keine Werte verfügbar')}
+
+ );
+ }
+
+ return (
+
+ {options.map((option) => {
+ const checked = selected.includes(option.value);
+ return (
+
+ );
+ })}
+
+ );
+}
+
+export function WorkspaceScopeEditor({
+ moduleKey,
+ dimensions,
+ config,
+ onChange,
+}: WorkspaceScopeEditorProps) {
+ const { t } = useTranslation();
+
+ if (dimensions.length === 0) {
+ return (
+
+ {t('workspaces.scopeEditor.noDimensions', 'Für dieses Modul sind keine Filter verfügbar.')}
+
+ );
+ }
+
+ const handleMultiToggle = (dimension: ScopeDimension, value: string) => {
+ const current = Array.isArray(config[dimension.key]) ? (config[dimension.key] as string[]) : [];
+ const next = current.includes(value)
+ ? current.filter((v) => v !== value)
+ : [...current, value];
+ onChange(dimension.key, next);
+ };
+
+ return (
+
+
+ {t(
+ 'workspaces.scopeEditor.hint',
+ 'Nichts ausgewählt = keine Einschränkung. Filter können nur einschränken, nie erweitern.',
+ )}
+
+ {dimensions.map((dimension) => {
+ const selected = Array.isArray(config[dimension.key])
+ ? (config[dimension.key] as string[])
+ : [];
+ return (
+
+
{dimension.label}
+ {dimension.control === 'multiselect' && (
+
handleMultiToggle(dimension, value)}
+ />
+ )}
+ {dimension.control === 'select' && (
+
+ )}
+ {dimension.control === 'toggle' && (
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index 585e317..83c50cf 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -1595,5 +1595,14 @@
"noBlocks": "KI hat keine gültigen Blöcke vorgeschlagen",
"failed": "KI-Vorschlag fehlgeschlagen"
}
+ },
+ "workspaces": {
+ "scopeEditor": {
+ "hint": "Nichts ausgewählt = keine Einschränkung. Filter können nur einschränken, nie erweitern.",
+ "noRestriction": "Keine Einschränkung",
+ "noDimensions": "Für dieses Modul sind keine Filter verfügbar.",
+ "noValues": "Keine Werte verfügbar",
+ "loadError": "Werte konnten nicht geladen werden"
+ }
}
}
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index 8100d2b..3371aeb 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -1595,5 +1595,14 @@
"noBlocks": "AI did not suggest valid blocks",
"failed": "AI suggestion failed"
}
+ },
+ "workspaces": {
+ "scopeEditor": {
+ "hint": "Nothing selected = no restriction. Filters can only restrict, never extend.",
+ "noRestriction": "No restriction",
+ "noDimensions": "No filters available for this module.",
+ "noValues": "No values available",
+ "loadError": "Failed to load values"
+ }
}
}