feat(N3): Backend respektiert X-Workspace-ID bei Listen — contacts/dms/mail/calendar (#367)
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- Core-Resolver resolve_workspace_scope(): Zuweisungs-Check, leere Werte fallen weg; Exemptions System-Admin + workspaces:configure_modules (Editor-Deadlock) - require_workspace_scope(module_key) FastAPI-Dependency (deps.py) - expand_folder_scope(): Ordner-Subtree (zyklensicher) für ContactFolder + DMS Folder; scope_uuid_set() fail-closed - contacts: folder_ids-Subtree + contact_types auf GET /contacts, List-Cache bei aktivem Scope deaktiviert (Cache-Leak-Gefahr) - dms: folder_ids-Subtree + file_types (semantische Matcher) auf /files, Baum-Reduktion auf /folders - mail: account_ids auf /mails, /threads, /accounts - calendar: calendar_ids auf /calendar/entries, /calendars - Frontend-Defaults: getModuleConfig() im workspaceStore, ContactsList default_saved_view_id, Calendar default_view - Tests: 21/21 neu (TDD rot→grün), Regression 81 passed, Checker 0, tsc clean, Vitest grün, Build OK
This commit is contained in:
@@ -25,6 +25,7 @@ import { useWindowStore } from '@/store/windowStore';
|
||||
import { IcsControls } from '@/components/calendar/IcsControls';
|
||||
import { SharingSettings } from '@/components/calendar/SharingSettings';
|
||||
import { useCalendarStore, type CalendarViewMode } from '@/store/calendarStore';
|
||||
import { useWorkspaceStore } from '@/store/workspaceStore';
|
||||
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
|
||||
import { ChevronLeft, ChevronRight, ExternalLink, Info, Plus } from 'lucide-react';
|
||||
import { PrintButton } from '@/components/common/PrintButton';
|
||||
@@ -112,6 +113,18 @@ export function CalendarPage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// N3: workspace default calendar view (admin-defined per workspace).
|
||||
// Applied once when the workspace context provides a default_view;
|
||||
// user view switches stay sticky (only re-applied on workspace change).
|
||||
const workspaceDefaultView = useWorkspaceStore(s => s.getModuleConfig('calendar')?.default_view) as CalendarViewMode | undefined;
|
||||
const activeWorkspaceId = useWorkspaceStore(s => s.activeWorkspaceId);
|
||||
useEffect(() => {
|
||||
if (workspaceDefaultView && ['day', 'week', 'month', 'range'].includes(workspaceDefaultView)) {
|
||||
setViewMode(workspaceDefaultView);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workspaceDefaultView, activeWorkspaceId]);
|
||||
|
||||
// Compute date range based on viewMode
|
||||
const range = useMemo(() => {
|
||||
if (viewMode === 'month') {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useWorkspaceStore } from '@/store/workspaceStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ResizablePanel } from '@/components/ui/ResizablePanel';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
@@ -331,6 +332,17 @@ export function ContactsListPage() {
|
||||
if (activeViewId === id) setActiveViewId(null);
|
||||
}, [activeViewId, deleteViewMut]);
|
||||
|
||||
// N3: workspace default saved view — applied once when views are loaded
|
||||
// and the user has not chosen a view yet (admin-defined per workspace).
|
||||
const workspaceDefaultViewId = useWorkspaceStore(s => s.getModuleConfig('contacts')?.default_saved_view_id);
|
||||
const activeWorkspaceId = useWorkspaceStore(s => s.activeWorkspaceId);
|
||||
useEffect(() => {
|
||||
if (activeViewId || !workspaceDefaultViewId || savedViews.length === 0) return;
|
||||
const view = savedViews.find(v => v.id === workspaceDefaultViewId);
|
||||
if (view) applySavedView(view);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workspaceDefaultViewId, activeWorkspaceId, savedViews]);
|
||||
|
||||
// Save current filter configuration — via API
|
||||
const handleSaveFilter = useCallback((name: string, filterState: FilterState) => {
|
||||
createFilterMut.mutate({ name, entity_type: 'contacts', filter_criteria: filterState as any });
|
||||
|
||||
@@ -136,4 +136,37 @@ describe('workspaceStore', () => {
|
||||
expect(useWorkspaceStore.getState().context).toBeNull();
|
||||
expect(useWorkspaceStore.getState().myWorkspaces).toEqual([]);
|
||||
});
|
||||
|
||||
// ─── N3: getModuleConfig (scope defaults for pages) ─────────
|
||||
|
||||
it('getModuleConfig returns the module config from the active context', () => {
|
||||
useWorkspaceStore.getState().setContext({
|
||||
workspace_id: 'ws-1',
|
||||
modules: [
|
||||
{ module_key: 'contacts', menu_order: 0, config: { default_saved_view_id: 'view-42' } },
|
||||
{ module_key: 'calendar', menu_order: 1, config: { default_view: 'week' } },
|
||||
],
|
||||
widgets: [],
|
||||
});
|
||||
expect(useWorkspaceStore.getState().getModuleConfig('contacts')).toEqual({
|
||||
default_saved_view_id: 'view-42',
|
||||
});
|
||||
expect(useWorkspaceStore.getState().getModuleConfig('calendar')).toEqual({
|
||||
default_view: 'week',
|
||||
});
|
||||
});
|
||||
|
||||
it('getModuleConfig returns empty object without workspace context (backward compatible)', () => {
|
||||
expect(useWorkspaceStore.getState().getModuleConfig('contacts')).toEqual({});
|
||||
});
|
||||
|
||||
it('getModuleConfig returns empty object for unconfigured modules', () => {
|
||||
useWorkspaceStore.getState().setContext({
|
||||
workspace_id: 'ws-1',
|
||||
modules: [{ module_key: 'contacts', menu_order: 0, config: {} }],
|
||||
widgets: [],
|
||||
});
|
||||
expect(useWorkspaceStore.getState().getModuleConfig('mail')).toEqual({});
|
||||
expect(useWorkspaceStore.getState().getModuleConfig('contacts')).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,7 @@ interface WorkspaceStoreState {
|
||||
// Helpers
|
||||
isModuleVisible: (moduleKey: string, isSystemAdmin?: boolean) => boolean;
|
||||
visibleModuleKeys: () => Set<string>;
|
||||
getModuleConfig: (moduleKey: string) => Record<string, any>;
|
||||
hasWorkspaces: () => boolean;
|
||||
// Reset
|
||||
reset: () => void;
|
||||
@@ -111,6 +112,14 @@ export const useWorkspaceStore = create<WorkspaceStoreState>()(
|
||||
);
|
||||
},
|
||||
|
||||
getModuleConfig: (moduleKey: string) => {
|
||||
// N3: scope defaults (e.g. default_saved_view_id, default_view) come
|
||||
// from the active workspace context; no context = no defaults.
|
||||
const ctx = get().context;
|
||||
const mod = (ctx?.modules ?? []).find(m => m.module_key === moduleKey);
|
||||
return mod?.config ?? {};
|
||||
},
|
||||
|
||||
hasWorkspaces: () => get().myWorkspaces.length > 0,
|
||||
|
||||
reset: () => {
|
||||
|
||||
Reference in New Issue
Block a user