feat(housekeeping): finish accessible command deck
This commit is contained in:
1 parent
85ad0452d3
commit
a113e48880
55 files changed
+17064
-509
No files matched your search
@@ -14,6 +14,27 @@ import {
|
||||
WidgetSettings,
|
||||
} from "../../../foundation/shell/widget-settings";
|
||||
|
||||
export interface OperationsPersonalizationLabels {
|
||||
readonly favorites: {
|
||||
readonly title: string;
|
||||
readonly pin: string;
|
||||
readonly unpin: string;
|
||||
readonly error: string;
|
||||
readonly reorderInstructions: string;
|
||||
};
|
||||
readonly widgetSettings: {
|
||||
readonly title: string;
|
||||
readonly mandatory: string;
|
||||
readonly error: string;
|
||||
};
|
||||
readonly widgets: {
|
||||
readonly title: string;
|
||||
readonly partial: string;
|
||||
readonly error: string;
|
||||
readonly reorderInstructions: string;
|
||||
};
|
||||
}
|
||||
|
||||
const persistPreferences: HousekeepingPreferenceSaver = async (preferences) => {
|
||||
const { saveHousekeepingPreferences } = await import(
|
||||
"@/actions/housekeeping-preferences"
|
||||
@@ -27,6 +48,9 @@ export function OperationsPersonalization({
|
||||
widgets,
|
||||
widgetResult,
|
||||
surface,
|
||||
labels,
|
||||
routeTitles,
|
||||
widgetTitles,
|
||||
savePreferences = persistPreferences,
|
||||
}: {
|
||||
readonly preferences: HousekeepingPreferences;
|
||||
@@ -34,6 +58,9 @@ export function OperationsPersonalization({
|
||||
readonly widgets: readonly HousekeepingWidgetDefinition[];
|
||||
readonly widgetResult: HousekeepingWidgetLoadResult;
|
||||
readonly surface: HousekeepingSurface;
|
||||
readonly labels: OperationsPersonalizationLabels;
|
||||
readonly routeTitles: Readonly<Record<string, string>>;
|
||||
readonly widgetTitles: Readonly<Record<string, string>>;
|
||||
readonly savePreferences?: HousekeepingPreferenceSaver;
|
||||
}) {
|
||||
return (
|
||||
@@ -44,22 +71,15 @@ export function OperationsPersonalization({
|
||||
routes={routes}
|
||||
surface={surface}
|
||||
savePreferences={savePreferences}
|
||||
labels={{
|
||||
title: "Favorites",
|
||||
pin: "Pin",
|
||||
unpin: "Unpin",
|
||||
error: "Preferences could not be saved",
|
||||
}}
|
||||
labels={labels.favorites}
|
||||
titles={routeTitles}
|
||||
/>
|
||||
<WidgetSettings
|
||||
preferences={preferences}
|
||||
widgets={widgets}
|
||||
savePreferences={savePreferences}
|
||||
labels={{
|
||||
title: "Widget settings",
|
||||
mandatory: "Mandatory",
|
||||
error: "Preferences could not be saved",
|
||||
}}
|
||||
labels={labels.widgetSettings}
|
||||
titles={widgetTitles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -67,11 +87,8 @@ export function OperationsPersonalization({
|
||||
preferences={preferences}
|
||||
result={widgetResult}
|
||||
savePreferences={savePreferences}
|
||||
labels={{
|
||||
title: "Widgets",
|
||||
partial: "Some widgets are unavailable",
|
||||
error: "Widget order could not be saved",
|
||||
}}
|
||||
labels={labels.widgets}
|
||||
titles={widgetTitles}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -79,6 +79,45 @@ const result: OperationsWorkspaceLoadResult = {
|
||||
};
|
||||
|
||||
const savePreferences = vi.fn(async (value) => ok(value, "saved"));
|
||||
const labels = {
|
||||
title: "Operations workspace",
|
||||
description: "Capability-derived operational home",
|
||||
noDomainsTitle: "No operational domains are available",
|
||||
noDomainsDescription: "No authorized domain",
|
||||
partial: "Some workspace sections are unavailable",
|
||||
inbox: {
|
||||
title: "Operational inbox",
|
||||
domainFilter: "Filter by domain",
|
||||
stateFilter: "Filter by state",
|
||||
priorityFilter: "Filter by priority",
|
||||
all: "All",
|
||||
loading: "Loading",
|
||||
empty: "Empty",
|
||||
partial: "Some sources are unavailable",
|
||||
open: "Open",
|
||||
},
|
||||
recent: { title: "Recent work", empty: "Nothing recent" },
|
||||
favorites: {
|
||||
title: "Favorites",
|
||||
pin: "Pin",
|
||||
unpin: "Unpin",
|
||||
error: "Preferences could not be saved",
|
||||
reorderInstructions: "Press Space, then use arrow keys.",
|
||||
},
|
||||
widgetSettings: {
|
||||
title: "Widget settings",
|
||||
mandatory: "Mandatory",
|
||||
error: "Preferences could not be saved",
|
||||
},
|
||||
widgets: {
|
||||
title: "Widgets",
|
||||
partial: "Some widgets are unavailable",
|
||||
error: "Widget order could not be saved",
|
||||
reorderInstructions: "Press Space, then use arrow keys.",
|
||||
},
|
||||
} as const;
|
||||
const routeTitles = { Users: "Users" };
|
||||
const widgetTitles = { "people.queue": "People queue" };
|
||||
|
||||
describe("OperationsWorkspacePage", () => {
|
||||
it("renders capability-filtered sections, partial failures, and preview links", () => {
|
||||
@@ -88,6 +127,9 @@ describe("OperationsWorkspacePage", () => {
|
||||
routes={routes}
|
||||
widgetDefinitions={widgets}
|
||||
surface="preview"
|
||||
labels={labels}
|
||||
routeTitles={routeTitles}
|
||||
widgetTitles={widgetTitles}
|
||||
savePreferences={savePreferences}
|
||||
/>,
|
||||
);
|
||||
@@ -97,8 +139,8 @@ describe("OperationsWorkspacePage", () => {
|
||||
expect(html).toContain("Favorites");
|
||||
expect(html).toContain("Widget settings");
|
||||
expect(html).toContain("Some workspace sections are unavailable");
|
||||
expect(html).toContain("system.alerts");
|
||||
expect(html).toContain("system.health");
|
||||
expect(html).not.toContain("system.alerts");
|
||||
expect(html).not.toContain("system.health");
|
||||
expect(html).toContain('href="/ase-next/people/users"');
|
||||
});
|
||||
|
||||
@@ -109,6 +151,9 @@ describe("OperationsWorkspacePage", () => {
|
||||
routes={[]}
|
||||
widgetDefinitions={[]}
|
||||
surface="preview"
|
||||
labels={labels}
|
||||
routeTitles={{}}
|
||||
widgetTitles={{}}
|
||||
savePreferences={savePreferences}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -8,34 +8,66 @@ import { OperationalInbox } from "../../../foundation/shell/operational-inbox";
|
||||
import { RecentWork } from "../../../foundation/shell/recent-work";
|
||||
import type { HousekeepingPreferenceSaver } from "../../../foundation/shell/widget-settings";
|
||||
import type { OperationsWorkspaceLoadResult } from "../queries";
|
||||
import { OperationsPersonalization } from "./personalization";
|
||||
import {
|
||||
OperationsPersonalization,
|
||||
type OperationsPersonalizationLabels,
|
||||
} from "./personalization";
|
||||
|
||||
export interface OperationsWorkspaceLabels
|
||||
extends OperationsPersonalizationLabels {
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly noDomainsTitle: string;
|
||||
readonly noDomainsDescription: string;
|
||||
readonly partial: string;
|
||||
readonly inbox: {
|
||||
readonly title: string;
|
||||
readonly domainFilter: string;
|
||||
readonly stateFilter: string;
|
||||
readonly priorityFilter: string;
|
||||
readonly all: string;
|
||||
readonly loading: string;
|
||||
readonly empty: string;
|
||||
readonly partial: string;
|
||||
readonly open: string;
|
||||
};
|
||||
readonly recent: {
|
||||
readonly title: string;
|
||||
readonly empty: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function OperationsWorkspacePage({
|
||||
result,
|
||||
routes,
|
||||
widgetDefinitions,
|
||||
surface,
|
||||
labels,
|
||||
routeTitles,
|
||||
widgetTitles,
|
||||
inboxTitles = {},
|
||||
savePreferences,
|
||||
}: {
|
||||
readonly result: OperationsWorkspaceLoadResult;
|
||||
readonly routes: readonly HousekeepingRouteDefinition[];
|
||||
readonly widgetDefinitions: readonly HousekeepingWidgetDefinition[];
|
||||
readonly surface: HousekeepingSurface;
|
||||
readonly labels: OperationsWorkspaceLabels;
|
||||
readonly routeTitles: Readonly<Record<string, string>>;
|
||||
readonly widgetTitles: Readonly<Record<string, string>>;
|
||||
readonly inboxTitles?: Readonly<Record<string, string>>;
|
||||
readonly savePreferences?: HousekeepingPreferenceSaver;
|
||||
}) {
|
||||
if (routes.length === 0) {
|
||||
return (
|
||||
<HousekeepingPageShell
|
||||
title="Operations workspace"
|
||||
description="Your capability-derived operational home."
|
||||
title={labels.title}
|
||||
description={labels.description}
|
||||
>
|
||||
<div className="rounded-md border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
|
||||
<h2 className="font-semibold">
|
||||
No operational domains are available
|
||||
</h2>
|
||||
<h2 className="font-semibold">{labels.noDomainsTitle}</h2>
|
||||
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
|
||||
Your account can open Housekeeping, but no operational domain is
|
||||
currently authorized.
|
||||
{labels.noDomainsDescription}
|
||||
</p>
|
||||
</div>
|
||||
</HousekeepingPageShell>
|
||||
@@ -44,25 +76,17 @@ export function OperationsWorkspacePage({
|
||||
|
||||
return (
|
||||
<HousekeepingPageShell
|
||||
title="Operations workspace"
|
||||
description="Inbox, recent work, favorites, and widgets for the domains you can operate."
|
||||
title={labels.title}
|
||||
description={labels.description}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{result.errors.length > 0 ? (
|
||||
<div
|
||||
role="status"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="rounded-md border border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)] p-3"
|
||||
>
|
||||
<p className="text-[var(--admin-warning)]">
|
||||
Some workspace sections are unavailable
|
||||
</p>
|
||||
<ul className="mt-2 list-disc pl-5 text-sm">
|
||||
{result.errors.map((error) => (
|
||||
<li key={error.section}>
|
||||
{error.section}: {error.code}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-[var(--admin-warning)]">{labels.partial}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -70,25 +94,14 @@ export function OperationsWorkspacePage({
|
||||
<OperationalInbox
|
||||
response={result.model.inbox}
|
||||
surface={surface}
|
||||
labels={{
|
||||
title: "Operational inbox",
|
||||
domainFilter: "Filter by domain",
|
||||
stateFilter: "Filter by state",
|
||||
priorityFilter: "Filter by priority",
|
||||
all: "All",
|
||||
loading: "Loading operational inbox",
|
||||
empty: "No operational work is waiting",
|
||||
partial: "Some inbox sources are unavailable",
|
||||
open: "Open",
|
||||
}}
|
||||
labels={labels.inbox}
|
||||
titles={inboxTitles}
|
||||
/>
|
||||
<RecentWork
|
||||
items={result.model.recent}
|
||||
surface={surface}
|
||||
labels={{
|
||||
title: "Recent work",
|
||||
empty: "No recent Housekeeping work",
|
||||
}}
|
||||
labels={labels.recent}
|
||||
titles={routeTitles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -98,6 +111,9 @@ export function OperationsWorkspacePage({
|
||||
widgets={widgetDefinitions}
|
||||
widgetResult={result.model.widgets}
|
||||
surface={surface}
|
||||
labels={labels}
|
||||
routeTitles={routeTitles}
|
||||
widgetTitles={widgetTitles}
|
||||
{...(savePreferences ? { savePreferences } : {})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user