refactor: adopt shared split workspace
This commit is contained in:
+2
-1
@@ -25,6 +25,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test:approval-templates": "node tests/approval-templates-ui-structure.test.mjs"
|
||||
"test:approval-templates": "node tests/approval-templates-ui-structure.test.mjs",
|
||||
"test:workspace-layout": "node tests/workspace-layout-ui-structure.test.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AlarmClock, Check, Plus, RefreshCw, X } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActionBlockerHint, Button, ConfirmDialog, Dialog, DocumentationHelpLink, DismissibleAlert, FormField, IconButton, LoadingIndicator, PageScrollViewport, StatusBadge, hasScope, usePlatformLanguage, useUnsavedChanges, useUnsavedDraftGuard, type PlatformRouteContext } from "@govoplan/core-webui";
|
||||
import { ActionBlockerHint, Button, ConfirmDialog, Dialog, DocumentationHelpLink, DismissibleAlert, FormField, IconButton, LoadingIndicator, PageScrollViewport, StatusBadge, WorkspaceLayout, hasScope, usePlatformLanguage, useUnsavedChanges, useUnsavedDraftGuard, type PlatformRouteContext } from "@govoplan/core-webui";
|
||||
import { approvalHistory, decideApproval, escalateApproval, getApproval, listApprovals, type ApprovalEvent, type ApprovalRequest } from "../../api/approvals";
|
||||
import ApprovalRequestDialog from "./ApprovalRequestDialog";
|
||||
import { APPROVALS_DOCUMENTATION, APPROVALS_FIELD_DOCUMENTATION, APPROVALS_I18N } from "./interfacePatterns";
|
||||
@@ -54,12 +54,24 @@ export default function ApprovalsPage({ settings, auth }: PlatformRouteContext)
|
||||
await load(undefined, id);
|
||||
}
|
||||
|
||||
return <main className="approvals-page"><div className="approvals-shell">
|
||||
<aside className="approvals-list-panel">
|
||||
return <main className="approvals-page"><WorkspaceLayout
|
||||
variant="split"
|
||||
primarySize="compact"
|
||||
primaryScrollable={false}
|
||||
contentScrollable={false}
|
||||
className="approvals-shell"
|
||||
primaryClassName="approvals-list-panel"
|
||||
contentClassName="approvals-workspace"
|
||||
primaryLabel="Approval requests"
|
||||
contentLabel="Approval request details"
|
||||
interfaceId="approvals.workspace"
|
||||
helpContextId="approvals.workspace"
|
||||
helpModuleId="approvals"
|
||||
primary={<>
|
||||
<div className="approvals-toolbar"><IconButton label="Refresh approvals" icon={<RefreshCw size={16} />} disabled={loading || busy} disabledReason={loading ? APPROVALS_I18N.loading : busy ? APPROVALS_I18N.busy : undefined} onClick={() => void load()} /><Button variant="primary" disabled={!canWrite} disabledReason={!canWrite ? APPROVALS_I18N.writeReason : undefined} onClick={() => setCreating(true)}><Plus size={16} aria-hidden="true" />New request</Button><DocumentationHelpLink reference={APPROVALS_DOCUMENTATION} /></div>
|
||||
<PageScrollViewport className="approvals-list-viewport">{loading && <LoadingIndicator label="Loading approvals" />}<div className="approvals-list">{items.map((item) => <button type="button" key={item.id} className={item.id === selectedId ? "is-selected" : ""} onClick={() => setSelectedId(item.id)}><span><strong>{item.title}</strong><small>{item.subject_module} / {item.subject_type}</small></span><StatusBadge status={tone(item.state)} label={humanize(item.state)} /></button>)}</div>{!loading && items.length === 0 && <div className="approvals-empty">No approval requests</div>}</PageScrollViewport>
|
||||
</aside>
|
||||
<section className="approvals-workspace">
|
||||
</>}
|
||||
>
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||
{!canWrite && <ActionBlockerHint tone="info" reason={{ summary: "No Approval creation permission", details: APPROVALS_I18N.writeReason, requiredAction: APPROVALS_I18N.permissionAction, actor: APPROVALS_I18N.permissionActor, target: APPROVALS_I18N.permissionDestination }} labels={{ requiredAction: APPROVALS_I18N.requiredAction, actor: APPROVALS_I18N.actor, target: APPROVALS_I18N.destination }} documentation={APPROVALS_DOCUMENTATION} />}
|
||||
{selected && <PageScrollViewport className="approvals-detail-viewport"><div className="approvals-detail">
|
||||
@@ -70,8 +82,7 @@ export default function ApprovalsPage({ settings, auth }: PlatformRouteContext)
|
||||
<section><h3>History</h3><div className="approval-history">{history.map((event) => <div key={event.sequence}><span>{event.sequence}</span><strong>{humanize(event.event_type)}</strong><time>{new Date(event.recorded_at).toLocaleString(language)}</time></div>)}</div></section>
|
||||
</div></PageScrollViewport>}
|
||||
{!selected && !loading && <div className="approvals-empty">Select or create an approval request.</div>}
|
||||
</section>
|
||||
</div>
|
||||
</WorkspaceLayout>
|
||||
{creating && <ApprovalRequestDialog settings={settings} onClose={() => setCreating(false)} onSaved={(item) => { setCreating(false); setSelectedId(item.id); void load(undefined, item.id); }} />}
|
||||
{selected && decision && <DecisionDialog outcome={decision} busy={busy} signatureRequired={Boolean(selected.steps[selected.current_step_index]?.signature_required)} onClose={() => setDecision(null)} onConfirm={async (reason, signatureId) => { setBusy(true); setError(""); try { await decideApproval(settings, selected, decision, reason, signatureId ? { owner_module: "signatures", object_id: signatureId } : undefined); setDecision(null); await reload(selected.id); return true; } catch (failure) { setError(text(failure, "The Approval decision could not be recorded.")); return false; } finally { setBusy(false); } }} />}
|
||||
{selected && <ConfirmDialog open={escalating} title="Escalate approval step" message={`Escalate ${currentStep?.label ?? "the current step"}? This records an explicit lifecycle transition and lets the configured escalation workflow react.`} confirmLabel="Escalate step" busy={busy} onCancel={() => setEscalating(false)} onConfirm={() => { setBusy(true); setError(""); void escalateApproval(settings, selected).then(() => { setEscalating(false); return reload(selected.id); }).catch((failure) => setError(text(failure, "The Approval step could not be escalated."))).finally(() => setBusy(false)); }} />}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.approvals-page { height: 100%; min-height: 0; overflow: hidden; }
|
||||
.approvals-shell { display: grid; grid-template-columns: minmax(250px, 320px) minmax(0, 1fr); height: 100%; min-height: 0; }
|
||||
.approvals-list-panel { display: flex; min-height: 0; flex-direction: column; border-right: 1px solid var(--border-color, #d8dde3); }
|
||||
.approvals-toolbar { display: flex; align-items: center; gap: 8px; min-height: 50px; padding: 8px 12px; border-bottom: 1px solid var(--border-color, #d8dde3); }
|
||||
.approvals-list-viewport, .approvals-detail-viewport { min-height: 0; flex: 1; }
|
||||
@@ -45,4 +44,4 @@
|
||||
.approval-compare-toolbar { display: grid; grid-template-columns: minmax(180px, 1fr) minmax(180px, 1fr) auto; align-items: end; gap: 8px; }
|
||||
.approval-diff-value { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.approval-decision-form { display: flex; min-width: min(520px, 75vw); flex-direction: column; gap: 10px; }
|
||||
@media (max-width: 850px) { .approvals-shell { grid-template-columns: 1fr; grid-template-rows: minmax(160px, 34%) minmax(0, 1fr); } .approvals-list-panel { border-right: 0; border-bottom: 1px solid var(--border-color, #d8dde3); } .approval-metrics, .approval-editor-grid, .approval-step-fields, .approval-selector-list > div, .approval-compare-toolbar { grid-template-columns: 1fr; } .approval-editor-wide { grid-column: auto; } }
|
||||
@media (max-width: 900px) { .approvals-list-panel { border-right: 0; border-bottom: 1px solid var(--border-color, #d8dde3); } .approval-metrics, .approval-editor-grid, .approval-step-fields, .approval-selector-list > div, .approval-compare-toolbar { grid-template-columns: 1fr; } .approval-editor-wide { grid-column: auto; } }
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
|
||||
const page = fs.readFileSync("src/features/approvals/ApprovalsPage.tsx", "utf8");
|
||||
const styles = fs.readFileSync("src/styles/approvals.css", "utf8");
|
||||
|
||||
assert.ok(page.includes("<WorkspaceLayout"), "Approvals must use the shared workspace shell");
|
||||
assert.ok(page.includes('variant="split"'), "Approvals must use the shared split-pane geometry");
|
||||
assert.ok(page.includes('primarySize="compact"'), "The request list uses the bounded compact pane width");
|
||||
assert.ok(page.includes("primaryScrollable={false}"), "The request list delegates scrolling to its contained viewport");
|
||||
assert.ok(page.includes("contentScrollable={false}"), "The detail pane delegates scrolling to its contained viewport");
|
||||
assert.ok(!styles.includes(".approvals-shell { display: grid"), "Approvals must not redefine the shared split-pane grid");
|
||||
|
||||
console.log("Approval workspace layout structural contract passed.");
|
||||
Reference in New Issue
Block a user