refactor(webui): adopt semantic page actions

This commit is contained in:
2026-08-19 18:47:46 +02:00
parent 89a142ead1
commit ed0357b146
2 changed files with 33 additions and 24 deletions
+23 -15
View File
@@ -6,8 +6,7 @@ import {
useState, useState,
type FormEvent type FormEvent
} from "react"; } from "react";
import { ActionToolbar, import { ActionBlockerHint,
ActionBlockerHint,
CountBadge, CountBadge,
DismissibleAlert, DismissibleAlert,
Button, Button,
@@ -19,6 +18,7 @@ import { ActionToolbar,
StatusBadge, StatusBadge,
ToggleSwitch, ToggleSwitch,
useGuardedNavigate, useGuardedNavigate,
WorkspaceActionBar,
WorkspaceFrame, WorkspaceFrame,
type PlatformRouteContext type PlatformRouteContext
} from "@govoplan/core-webui"; } from "@govoplan/core-webui";
@@ -41,6 +41,7 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [launchingId, setLaunchingId] = useState(""); const [launchingId, setLaunchingId] = useState("");
const [reloadKey, setReloadKey] = useState(0);
const launchAttempts = useRef(new Map<string, { const launchAttempts = useRef(new Map<string, {
idempotencyKey: string; idempotencyKey: string;
requestedAt: string; requestedAt: string;
@@ -67,7 +68,7 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
}). }).
finally(() => setLoading(false)); finally(() => setLoading(false));
return () => controller.abort(); return () => controller.abort();
}, [includeUnavailable, settings, submittedQuery]); }, [includeUnavailable, reloadKey, settings, submittedQuery]);
useEffect(() => { useEffect(() => {
const controller = new AbortController(); const controller = new AbortController();
@@ -79,7 +80,7 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
} }
}); });
return () => controller.abort(); return () => controller.abort();
}, [settings]); }, [reloadKey, settings]);
const counts = useMemo(() => ({ const counts = useMemo(() => ({
available: services.filter((entry) => entry.state === "available").length, available: services.filter((entry) => entry.state === "available").length,
@@ -131,8 +132,14 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
return ( return (
<main className="portal-page"> <main className="portal-page">
<WorkspaceFrame className="portal-shell" label="Service directory" interfaceId="portal.service-directory" helpContextId="portal.page.directory" helpModuleId="portal"> <WorkspaceFrame className="portal-shell" label="Service directory" interfaceId="portal.service-directory" helpContextId="portal.page.directory" helpModuleId="portal">
<ActionToolbar surface="panel-header" className="portal-toolbar"> <WorkspaceActionBar
<FilterBar as="form" surface="control" wrap="never" width="wide" className="portal-search" onSubmit={submit}> scope="workspace"
variant="collection"
refreshable
reloadAction={{ onReload: () => setReloadKey((value) => value + 1), loading }}
className="portal-toolbar"
contextActions={<>
<FilterBar as="form" surface="control" wrap="never" width="wide" className="portal-search" onSubmit={submit}>
<Search size={17} aria-hidden="true" /> <Search size={17} aria-hidden="true" />
<input <input
value={query} value={query}
@@ -141,17 +148,18 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
placeholder="Search services" placeholder="Search services"
/> />
<Button type="submit" variant="primary">Search</Button> <Button type="submit" variant="primary">Search</Button>
</FilterBar> </FilterBar>
<DocumentationHelpLink <ToggleSwitch
label="Show unavailable services"
checked={includeUnavailable}
onChange={setIncludeUnavailable}
/>
</>}
helpAction={<DocumentationHelpLink
reference={{ topicId: "portal.service-directory", documentationType: "user" }} reference={{ topicId: "portal.service-directory", documentationType: "user" }}
label="Open service directory documentation" label="Open service directory documentation"
/> />}
<ToggleSwitch />
label="Show unavailable services"
checked={includeUnavailable}
onChange={setIncludeUnavailable}
/>
</ActionToolbar>
<div className="portal-result-summary" aria-live="polite"> <div className="portal-result-summary" aria-live="polite">
<strong>{counts.available}</strong> available <strong>{counts.available}</strong> available
+10 -9
View File
@@ -1,8 +1,7 @@
import { Clock3, LogIn, RefreshCw, Send } from "lucide-react"; import { Clock3, LogIn, Send } from "lucide-react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useParams, useSearchParams } from "react-router"; import { useParams, useSearchParams } from "react-router";
import { import {
ActionToolbar,
Button, Button,
Card, Card,
DescriptionItem, DescriptionItem,
@@ -12,6 +11,7 @@ import {
LoadingIndicator, LoadingIndicator,
PageScrollViewport, PageScrollViewport,
StatusBadge, StatusBadge,
WorkspaceActionBar,
WorkspaceFrame, WorkspaceFrame,
type PlatformRouteContext type PlatformRouteContext
} from "@govoplan/core-webui"; } from "@govoplan/core-webui";
@@ -101,13 +101,14 @@ export default function PortalStatusPage({ settings }: PlatformRouteContext) {
helpModuleId="portal" helpModuleId="portal"
helpTopicId="portal.application-status" helpTopicId="portal.application-status"
helpContextId="portal.application-status"> helpContextId="portal.application-status">
<ActionToolbar surface="workspace" className="portal-status-toolbar"> <WorkspaceActionBar
<strong>Application status</strong> scope="workspace"
<Button onClick={() => void load()} disabled={loading}> variant="detail"
<RefreshCw size={16} aria-hidden="true" /> refreshable
Reload reloadAction={{ onReload: () => void load(), loading }}
</Button> className="portal-status-toolbar"
</ActionToolbar> contextActions={<strong>Application status</strong>}
/>
<PageScrollViewport className="portal-status-viewport"> <PageScrollViewport className="portal-status-viewport">
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>} {error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{notice && <DismissibleAlert tone="info" resetKey={notice}>{notice}</DismissibleAlert>} {notice && <DismissibleAlert tone="info" resetKey={notice}>{notice}</DismissibleAlert>}