feat(quick-access): expose effective preference provenance

This commit is contained in:
2026-08-20 02:39:41 +02:00
parent b5b629b357
commit 127b6ee5d0
8 changed files with 211 additions and 2 deletions
+11
View File
@@ -56,17 +56,28 @@ export type EffectiveQuickAccessTool = QuickAccessTool & {
enabled: boolean;
forced: boolean;
locked_by?: string | null;
availability_state: "available" | "forced" | "blocked";
availability_source: "module" | "system" | "tenant" | "user";
order_source: "module" | "system" | "tenant" | "user";
};
export type EffectiveQuickAccessCategory = QuickAccessCategory & {
enabled: boolean;
forced: boolean;
locked_by?: string | null;
availability_state: "available" | "forced" | "blocked";
availability_source: "module" | "system" | "tenant" | "user";
order_source: "module" | "system" | "tenant" | "user";
tools: EffectiveQuickAccessTool[];
};
export type EffectiveQuickAccess = {
categories: EffectiveQuickAccessCategory[];
stale_preferences: Array<{
id: string;
kind: "category" | "tool";
source: "system" | "tenant" | "user";
}>;
diagnostics: string[];
};
@@ -32,6 +32,11 @@ type Draft = {
tools: Record<string, QuickAccessPreference>;
};
type AvailabilityMode = "inherit" | "available" | "blocked" | "forced";
type EffectiveProvenance = {
state: "available" | "forced" | "blocked";
availabilitySource: "module" | "system" | "tenant" | "user";
orderSource: "module" | "system" | "tenant" | "user";
};
const EMPTY_DRAFT: Draft = { categories: {}, tools: {} };
@@ -186,6 +191,14 @@ export default function QuickAccessSettingsPanel({
const lockedTools = new Map(
(effective?.categories ?? []).flatMap((category) => category.tools).filter((item) => item.locked_by).map((item) => [item.id, item.locked_by])
);
const categoryProvenance = new Map(
(effective?.categories ?? []).map((item) => [item.id, effectiveProvenance(item)])
);
const toolProvenance = new Map(
(effective?.categories ?? []).flatMap((category) =>
category.tools.map((item) => [item.id, effectiveProvenance(item)] as const)
)
);
const categoryIds = categories.map((item) => item.id);
return (
@@ -219,6 +232,7 @@ export default function QuickAccessSettingsPanel({
preference={draft.categories[category.id]}
isPersonal={isPersonal}
lockedBy={editableConstraintSource(scope, lockedCategories.get(category.id))}
provenance={categoryProvenance.get(category.id)}
disabled={!canWrite || saving}
onChange={(value) => setPreference("categories", category.id, value)}
onMoveUp={() => move("categories", categoryIds, category.id, -1)}
@@ -243,6 +257,7 @@ export default function QuickAccessSettingsPanel({
defaultEnabled={tool.default_enabled}
isPersonal={isPersonal}
lockedBy={editableConstraintSource(scope, lockedTools.get(tool.id))}
provenance={toolProvenance.get(tool.id)}
disabled={!canWrite || saving}
onChange={(value) => setPreference("tools", tool.id, value)}
onMoveUp={() => move("tools", toolIds, tool.id, -1)}
@@ -268,6 +283,7 @@ function PreferenceRow({
defaultEnabled = true,
isPersonal,
lockedBy,
provenance,
disabled,
onChange,
onMoveUp,
@@ -282,6 +298,7 @@ function PreferenceRow({
defaultEnabled?: boolean;
isPersonal: boolean;
lockedBy?: string | null;
provenance?: EffectiveProvenance;
disabled: boolean;
onChange: (value: QuickAccessPreference | null) => void;
onMoveUp: () => void;
@@ -303,6 +320,15 @@ function PreferenceRow({
})}
</small>
) : null}
{provenance ? (
<small>
{i18nMessage("i18n:govoplan-quick-access.effective_provenance", {
value0: provenance.state,
value1: provenance.availabilitySource,
value2: provenance.orderSource
})}
</small>
) : null}
</div>
<div className="quick-access-preference-controls">
{isPersonal ? (
@@ -338,6 +364,18 @@ function PreferenceRow({
);
}
function effectiveProvenance(item: {
availability_state: "available" | "forced" | "blocked";
availability_source: "module" | "system" | "tenant" | "user";
order_source: "module" | "system" | "tenant" | "user";
}): EffectiveProvenance {
return {
state: item.availability_state,
availabilitySource: item.availability_source,
orderSource: item.order_source
};
}
function preferenceMode(preference?: QuickAccessPreference): AvailabilityMode {
if (!preference || preference.enabled === null || preference.enabled === undefined) return "inherit";
+4 -2
View File
@@ -44,7 +44,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-quick-access.category.messages_description": "Mail, Postfach und künftige Gesprächskanäle in einer Einblendung.",
"i18n:govoplan-quick-access.category.files": "Dateien",
"i18n:govoplan-quick-access.category.files_description": "Aktuelle und kontextbezogene Dateien, ohne die laufende Aufgabe zu verlassen.",
"i18n:govoplan-quick-access.locked_by_value": "Durch {value0} festgelegt"
"i18n:govoplan-quick-access.locked_by_value": "Durch {value0} festgelegt",
"i18n:govoplan-quick-access.effective_provenance": "Effektiv {value0}; Verfügbarkeit von {value1}, Reihenfolge von {value2}"
},
en: {
"i18n:govoplan-quick-access.quick_access": "Quick Access",
@@ -89,6 +90,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-quick-access.category.messages_description": "Mail, Postbox, and future conversational channels in one overlay.",
"i18n:govoplan-quick-access.category.files": "Files",
"i18n:govoplan-quick-access.category.files_description": "Recent and contextual files without leaving the current task.",
"i18n:govoplan-quick-access.locked_by_value": "Set by {value0}"
"i18n:govoplan-quick-access.locked_by_value": "Set by {value0}",
"i18n:govoplan-quick-access.effective_provenance": "Effective {value0}; availability from {value1}, order from {value2}"
}
};