diff --git a/docs/QUICK_ACCESS.md b/docs/QUICK_ACCESS.md index 7f77a3b..a0f86c2 100644 --- a/docs/QUICK_ACCESS.md +++ b/docs/QUICK_ACCESS.md @@ -43,7 +43,13 @@ identifiers, temporal selection, exact View identity, View recommendations or focus, and a safe return route. Cross-tenant object references are discarded and unknown versions fail closed. View focus applies only when at least one focused tool is currently enabled, context-compatible, and authorized; a -recommendation only changes ordering and emphasis. +recommendation only changes ordering and emphasis. The rail identifies an +active View focus and offers **All available tools** as a temporary escape to +the complete permission-derived catalogue. This does not change the active +View, persist an override, or expose a tool that failed installation, +entitlement, policy, preference, context, surface, or permission checks. If no +focused tool is eligible, the rail explains that stale focus and falls back to +the permission-derived catalogue instead of becoming empty. Each renderer performs owner-side reads and effects and explicitly reports either a result-contract-version-1 completion with a typed owner reference or diff --git a/src/govoplan_quick_access/backend/manifest.py b/src/govoplan_quick_access/backend/manifest.py index 5ef2849..ff1cedc 100644 --- a/src/govoplan_quick_access/backend/manifest.py +++ b/src/govoplan_quick_access/backend/manifest.py @@ -121,6 +121,9 @@ DOCUMENTATION = ( "Messages combines enabled Mail, Postbox, and future chat contributions in one overlay. " "Personal settings can reorder or hide items that remain available under system, tenant, " "permission, and View policy. Every item retains a link to its complete owning page. " + "When a View focuses the rail, the rail names that View and offers a temporary All available tools escape that " + "shows the complete permission-derived catalogue without changing the View or personal settings. If none of a " + "View's focused tools is currently available, Quick Access explains the stale focus and falls back to that catalogue. " "Launch-context version 2 carries only versioned, bounded references to the current object, acting assignment, " "temporal selection, exact View revision, and return location. Owner modules recheck access when a tool opens and " "before each effect. A tool returns either an explicit version-1 completion with a typed owner reference or an " @@ -145,6 +148,9 @@ DOCUMENTATION = ( "Eine Kategorie in der rechten Leiste oeffnet kompakte Werkzeuge, ohne die aktuelle Aufgabe zu verlassen. " "Nachrichten fuehrt Beitraege aus Mail, Postfach und kuenftigen Chat-Modulen in einer Einblendung zusammen. " "Persoenliche Einstellungen koennen alle durch System, Mandant, Berechtigungen und Ansicht zugelassenen Eintraege ordnen oder ausblenden. " + "Wenn eine Ansicht die Leiste fokussiert, benennt die Leiste diese Ansicht und bietet voruebergehend alle " + "verfuegbaren Werkzeuge an, ohne Ansicht oder persoenliche Einstellungen zu aendern. Ist keines der fokussierten " + "Werkzeuge verfuegbar, wird der veraltete Fokus erklaert und der berechtigungsabgeleitete Katalog angezeigt. " "Startkontext Version 2 uebergibt nur versionierte, begrenzte Verweise auf Objekt, handelnde Zuordnung, " "Zeitbezug, genaue Ansichtsversion und Ruecksprungort. Das besitzende Modul prueft den Zugriff beim Oeffnen " "und vor jeder Wirkung erneut. Ein Werkzeug meldet entweder einen ausdruecklichen Abschluss mit typisiertem " @@ -170,7 +176,8 @@ DOCUMENTATION = ( "tenant settings constrain users. An item may remain available, be blocked, or be forced. " "Views and permissions form additional ceilings and Quick Access never grants access to domain data. " "A View may recommend tools or focus the rail to a subset, but only currently enabled, context-compatible, authorized " - "tools participate. Workflow receives that same presentation from the exact resolved View revision. Launch-context " + "tools participate. The All available tools escape only restores that permission-derived set for the current session; " + "it never broadens authorization or persists an override. Workflow receives that same presentation from the exact resolved View revision. Launch-context " "version 2, reference contract version 1, and result contract version 1 fail closed on unknown versions; cross-tenant " "active-object and result references are rejected." ), @@ -186,7 +193,8 @@ DOCUMENTATION = ( "Mandanteneinstellungen begrenzen Benutzer. Ein Eintrag kann verfuegbar, gesperrt oder erzwungen sein. " "Ansichten und Berechtigungen bilden weitere Grenzen; Schnellzugriff erteilt selbst keinen Datenzugriff. " "Eine Ansicht darf Werkzeuge empfehlen oder die Leiste auf eine Teilmenge fokussieren, jedoch nur innerhalb " - "der aktivierten, kontextgeeigneten und berechtigten Werkzeuge. Workflow verwendet dieselbe Darstellung aus " + "der aktivierten, kontextgeeigneten und berechtigten Werkzeuge. Alle verfuegbaren Werkzeuge stellt nur diese " + "berechtigungsabgeleitete Menge fuer die laufende Sitzung wieder her und speichert keine Umgehung. Workflow verwendet dieselbe Darstellung aus " "der genau aufgeloesten Ansichtsversion. Startkontext Version 2 sowie Verweis- und Ergebniskontrakt Version 1 " "lehnen unbekannte Versionen ab; mandantenfremde Objekt- und Ergebnisverweise werden verworfen." ), diff --git a/webui/src/components/QuickAccessRail.tsx b/webui/src/components/QuickAccessRail.tsx index 7d0940b..569510d 100644 --- a/webui/src/components/QuickAccessRail.tsx +++ b/webui/src/components/QuickAccessRail.tsx @@ -1,7 +1,9 @@ import { CalendarDays, Files, + Grid2X2, ListChecks, + ListFilter, MessagesSquare, Settings2, X, @@ -15,6 +17,7 @@ import { IconButton, LoadingFrame, dispatchQuickAccessResult, + i18nMessage, quickAccessLaunchState, useGuardedNavigate, usePlatformLanguage, @@ -39,6 +42,7 @@ const iconByCategory: Record = { export default function QuickAccessRail({ settings, auth, tools, launchContext }: QuickAccessRailProps) { const [effective, setEffective] = useState(null); const [activeCategoryId, setActiveCategoryId] = useState(null); + const [showAllAvailable, setShowAllAvailable] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const drawerRef = useRef(null); @@ -61,8 +65,8 @@ export default function QuickAccessRail({ settings, auth, tools, launchContext } () => new Set(launchContext.viewContext?.recommendedToolIds ?? []), [launchContext.viewContext?.recommendedToolIds] ); - const categories = useMemo(() => { - const eligibleCategories = (effective?.categories ?? []) + const eligibleCategories = useMemo(() => + (effective?.categories ?? []) .filter((category) => category.enabled) .map((category) => ({ ...category, @@ -76,14 +80,30 @@ export default function QuickAccessRail({ settings, auth, tools, launchContext } } return true; }) - })); - const hasEligibleFocus = eligibleCategories.some((category) => + })) + .filter((category) => category.tools.length > 0), + [availableToolIds, effective, launchContext.activeObject, metadataById] + ); + const hasEligibleFocus = useMemo( + () => eligibleCategories.some((category) => category.tools.some((tool) => focusedToolIds.has(tool.id)) - ); + ), + [eligibleCategories, focusedToolIds] + ); + const hiddenByFocusCount = useMemo( + () => hasEligibleFocus + ? eligibleCategories.reduce( + (count, category) => count + category.tools.filter((tool) => !focusedToolIds.has(tool.id)).length, + 0 + ) + : 0, + [eligibleCategories, focusedToolIds, hasEligibleFocus] + ); + const categories = useMemo(() => { return eligibleCategories.map((category) => ({ ...category, tools: category.tools.filter((tool) => - !hasEligibleFocus || focusedToolIds.has(tool.id) + !hasEligibleFocus || showAllAvailable || focusedToolIds.has(tool.id) ).sort((left, right) => Number(recommendedToolIds.has(right.id)) - Number(recommendedToolIds.has(left.id)) || left.order - right.order @@ -91,7 +111,7 @@ export default function QuickAccessRail({ settings, auth, tools, launchContext } ) })).filter((category) => category.enabled && category.tools.length > 0); }, - [availableToolIds, effective, focusedToolIds, launchContext.activeObject, metadataById, recommendedToolIds] + [eligibleCategories, focusedToolIds, hasEligibleFocus, recommendedToolIds, showAllAvailable] ); const activeCategory = categories.find((category) => category.id === activeCategoryId) ?? null; @@ -143,6 +163,10 @@ export default function QuickAccessRail({ settings, auth, tools, launchContext } setActiveCategoryId(null); }, [location.pathname, location.search]); + useEffect(() => { + setShowAllAvailable(false); + }, [launchContext.viewContext?.viewId, launchContext.viewContext?.revisionId]); + if (!loading && categories.length === 0 && !error) return null; function closeDrawer(restoreFocus = true) { @@ -169,9 +193,37 @@ export default function QuickAccessRail({ settings, auth, tools, launchContext } navigate(path, { state: quickAccessLaunchState(launchContext) }); } + function toggleViewFocus() { + setShowAllAvailable((current) => !current); + } + + const viewName = launchContext.viewContext?.name?.trim() + || translateText("i18n:govoplan-quick-access.current_view"); + const viewFocusRequested = focusedToolIds.size > 0; + const canToggleViewFocus = hasEligibleFocus && hiddenByFocusCount > 0; + const viewModeLabel = showAllAvailable + ? translateText("i18n:govoplan-quick-access.restore_view_focus") + : translateText("i18n:govoplan-quick-access.show_all_available"); + const viewModeExplanation = !hasEligibleFocus + ? translateText(i18nMessage("i18n:govoplan-quick-access.focus_unavailable", { value0: viewName })) + : showAllAvailable + ? translateText(i18nMessage("i18n:govoplan-quick-access.showing_all_for_view", { value0: viewName })) + : translateText(i18nMessage("i18n:govoplan-quick-access.focused_by_view", { value0: viewName })); + const viewFocusMode = !viewFocusRequested + ? "none" + : !hasEligibleFocus + ? "unavailable" + : showAllAvailable + ? "all" + : "focused"; + return ( <> -