feat: govern task-local quick actions

This commit is contained in:
2026-08-19 18:47:46 +02:00
parent d44ed78e5c
commit 5a906185a6
10 changed files with 139 additions and 23 deletions
+5
View File
@@ -27,6 +27,7 @@ export type QuickAccessCategory = {
};
export type QuickAccessTool = {
contract_version: "1";
id: string;
module_id: string;
category_id: string;
@@ -40,6 +41,10 @@ export type QuickAccessTool = {
order: number;
default_enabled: boolean;
modes: string[];
availability: "global" | "active_object";
accepted_reference_kinds: string[];
returned_reference_kinds: string[];
help_context_id?: string | null;
};
export type QuickAccessCatalogue = {
+70 -7
View File
@@ -11,6 +11,7 @@ import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
import { Link, useLocation } from "react-router";
import {
DismissibleAlert,
DocumentationHelpLink,
IconButton,
LoadingFrame,
dispatchQuickAccessResult,
@@ -51,12 +52,46 @@ export default function QuickAccessRail({ settings, auth, tools, launchContext }
[contributions]
);
const availableToolIds = useMemo(() => new Set(tools.map((tool) => tool.id)), [tools]);
const categories = useMemo(
() => (effective?.categories ?? []).map((category) => ({
const metadataById = useMemo(() => new Map(tools.map((tool) => [tool.id, tool])), [tools]);
const focusedToolIds = useMemo(
() => new Set(launchContext.viewContext?.focusedToolIds ?? []),
[launchContext.viewContext?.focusedToolIds]
);
const recommendedToolIds = useMemo(
() => new Set(launchContext.viewContext?.recommendedToolIds ?? []),
[launchContext.viewContext?.recommendedToolIds]
);
const categories = useMemo(() => {
const eligibleCategories = (effective?.categories ?? [])
.filter((category) => category.enabled)
.map((category) => ({
...category,
tools: category.tools.filter((tool) => {
if (!tool.enabled || !availableToolIds.has(tool.id)) return false;
const metadata = metadataById.get(tool.id);
if (metadata?.availability === "active_object" && !launchContext.activeObject) return false;
if (metadata?.acceptedReferenceKinds.length && launchContext.activeObject) {
const activeKind = `${launchContext.activeObject.ownerModule}.${launchContext.activeObject.kind}`;
if (!metadata.acceptedReferenceKinds.includes(activeKind)) return false;
}
return true;
})
}));
const hasEligibleFocus = eligibleCategories.some((category) =>
category.tools.some((tool) => focusedToolIds.has(tool.id))
);
return eligibleCategories.map((category) => ({
...category,
tools: category.tools.filter((tool) => tool.enabled && availableToolIds.has(tool.id))
})).filter((category) => category.enabled && category.tools.length > 0),
[availableToolIds, effective]
tools: category.tools.filter((tool) =>
!hasEligibleFocus || focusedToolIds.has(tool.id)
).sort((left, right) =>
Number(recommendedToolIds.has(right.id)) - Number(recommendedToolIds.has(left.id))
|| left.order - right.order
|| left.id.localeCompare(right.id)
)
})).filter((category) => category.enabled && category.tools.length > 0);
},
[availableToolIds, effective, focusedToolIds, launchContext.activeObject, metadataById, recommendedToolIds]
);
const activeCategory = categories.find((category) => category.id === activeCategoryId) ?? null;
@@ -189,13 +224,27 @@ export default function QuickAccessRail({ settings, auth, tools, launchContext }
<LoadingFrame loading={loading} label="i18n:govoplan-quick-access.loading">
{activeCategory.tools.map((tool) => {
const renderer = renderers.get(tool.id);
const metadata = metadataById.get(tool.id);
return (
<section className="quick-access-tool" key={tool.id} data-tool-id={tool.id}>
<section
className={`quick-access-tool${recommendedToolIds.has(tool.id) ? " is-recommended" : ""}`}
key={tool.id}
data-tool-id={tool.id}
data-tool-contract-version={metadata?.contractVersion}
>
<div className="quick-access-tool-heading">
<div>
<strong>{translateText(tool.label)}</strong>
{recommendedToolIds.has(tool.id) ? (
<span className="quick-access-recommended">
{translateText("i18n:govoplan-quick-access.recommended_for_view")}
</span>
) : null}
{tool.description ? <small>{translateText(tool.description)}</small> : null}
</div>
{metadata?.helpContextId ? (
<DocumentationHelpLink reference={{ contextId: metadata.helpContextId, moduleId: tool.module_id }} />
) : null}
{tool.full_page_path ? (
<button
type="button"
@@ -214,7 +263,21 @@ export default function QuickAccessRail({ settings, auth, tools, launchContext }
active: true,
launchContext,
complete: (result) => {
dispatchQuickAccessResult(tool.id, launchContext, result);
if (!dispatchQuickAccessResult(tool.id, launchContext, result, metadata?.returnedReferenceKinds ?? [])) {
setError("i18n:govoplan-quick-access.invalid_result");
return;
}
closeDrawer();
},
cancel: (reason = "user") => {
if (!dispatchQuickAccessResult(tool.id, launchContext, {
contractVersion: "1",
outcome: "cancelled",
reason
}, metadata?.returnedReferenceKinds ?? [])) {
setError("i18n:govoplan-quick-access.invalid_result");
return;
}
closeDrawer();
}
})
@@ -196,8 +196,8 @@ export default function QuickAccessSettingsPanel({
<p>{isPersonal ? "i18n:govoplan-quick-access.personal_help" : "i18n:govoplan-quick-access.admin_help"}</p>
</div>
<div className="quick-access-settings-actions">
<Button variant="secondary" icon={<RotateCcw size={16} />} disabled={!dirty || saving} onClick={reset}>i18n:govoplan-quick-access.discard</Button>
<Button variant="primary" icon={<Save size={16} />} disabled={!dirty || saving || !canWrite} onClick={() => void save()}>i18n:govoplan-quick-access.save</Button>
<Button variant="secondary" disabled={!dirty || saving} onClick={reset}><RotateCcw size={16} aria-hidden="true" />i18n:govoplan-quick-access.discard</Button>
<Button variant="primary" disabled={!dirty || saving || !canWrite} onClick={() => void save()}><Save size={16} aria-hidden="true" />i18n:govoplan-quick-access.save</Button>
</div>
</div>
+4
View File
@@ -8,6 +8,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-quick-access.loading": "Schnellzugriff wird geladen",
"i18n:govoplan-quick-access.open_full_page": "Vollständige Seite öffnen",
"i18n:govoplan-quick-access.compact_view_unavailable": "Die kompakte Ansicht ist nicht verfügbar. Verwenden Sie die vollständige Seite.",
"i18n:govoplan-quick-access.recommended_for_view": "Für diese Ansicht empfohlen",
"i18n:govoplan-quick-access.invalid_result": "Das Werkzeug hat ein ungültiges oder nicht zugelassenes Ergebnis zurückgegeben.",
"i18n:govoplan-quick-access.settings_title": "Schnellzugriff",
"i18n:govoplan-quick-access.personal_help": "Werkzeuge auswählen und ordnen, die neben der aktuellen Arbeit verfügbar bleiben.",
"i18n:govoplan-quick-access.admin_help": "Verfügbarkeit, erzwungene Einträge und Standardreihenfolge für nachgeordnete Ebenen festlegen.",
@@ -45,6 +47,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-quick-access.loading": "Loading Quick Access",
"i18n:govoplan-quick-access.open_full_page": "Open full page",
"i18n:govoplan-quick-access.compact_view_unavailable": "The compact view is unavailable. Use the full page.",
"i18n:govoplan-quick-access.recommended_for_view": "Recommended for this View",
"i18n:govoplan-quick-access.invalid_result": "The tool returned an invalid or undeclared result.",
"i18n:govoplan-quick-access.settings_title": "Quick Access",
"i18n:govoplan-quick-access.personal_help": "Choose and order the tools kept beside your current work.",
"i18n:govoplan-quick-access.admin_help": "Set availability, forced items, and default ordering for lower scopes.",
+1
View File
@@ -1,2 +1,3 @@
export { default, quickAccessModule } from "./module";
export { default as QuickAccessRail } from "./components/QuickAccessRail";
export * from "./api/quickAccess";
+15
View File
@@ -124,6 +124,21 @@
padding: 14px 16px 16px;
}
.quick-access-tool.is-recommended {
border-inline-start: 3px solid var(--accent);
padding-inline-start: 13px;
}
.quick-access-recommended {
width: fit-content;
border-radius: var(--radius-round);
padding: 1px 7px;
background: var(--accent-soft);
color: var(--accent);
font-size: 11px;
font-weight: 700;
}
.quick-access-tool-heading,
.quick-access-settings-heading,
.quick-access-preference-row,