chore: enforce shared workspace layouts
This commit is contained in:
@@ -71,13 +71,20 @@ entity, permission, workflow state, endpoint, or policy decision. Reuse does
|
|||||||
not justify moving domain semantics into Core.
|
not justify moving domain semantics into Core.
|
||||||
|
|
||||||
`PageLayout` is the standard frame for headed workflow, dashboard,
|
`PageLayout` is the standard frame for headed workflow, dashboard,
|
||||||
configuration, monitoring and explanatory pages. It owns the scroll viewport,
|
configuration, monitoring and explanatory pages. It owns the content inset,
|
||||||
content inset, sticky responsive header, title and description geometry,
|
sticky responsive header, title and rich-description geometry, route-action
|
||||||
route-action placement, transient page notices, loading boundary and page help
|
placement, transient and custom notices, loading boundary and page help
|
||||||
identity. `PageHeader` is the escape hatch for full-canvas archetypes that need
|
identity. Its modes make scroll ownership explicit: `standalone` owns a page
|
||||||
the same header contract but must own their workspace scroll. Specialized
|
viewport, `workspace` defers scrolling to a full-canvas content pane while
|
||||||
layouts such as `AdminPageLayout` compose these lower-level Core contracts;
|
retaining the standard inset, and `embedded` owns neither scroll nor inset.
|
||||||
they do not repeat their markup or responsive CSS.
|
|
||||||
|
`WorkspaceLayout` is the standard full-canvas shell. Its `navigation` variant
|
||||||
|
owns module/resource subnavigation plus content; its `split` variant owns
|
||||||
|
collection/detail panes. It centralizes pane sizing, internal scroll,
|
||||||
|
responsive collapse/stacking, accessible pane labels and workspace help
|
||||||
|
identity. `PageHeader` remains available when an exceptional canvas needs only
|
||||||
|
the shared heading. Specialized layouts such as `AdminPageLayout` compose
|
||||||
|
these lower-level Core contracts; they do not repeat markup or responsive CSS.
|
||||||
|
|
||||||
Module CSS may arrange domain content inside a shared layout. It must not
|
Module CSS may arrange domain content inside a shared layout. It must not
|
||||||
override Core layout internals or copy the outer page, dialog, toolbar, form or
|
override Core layout internals or copy the outer page, dialog, toolbar, form or
|
||||||
@@ -95,10 +102,10 @@ Migration is incremental and enforceable:
|
|||||||
5. promote the next repeated structure only after its variants and extension
|
5. promote the next repeated structure only after its variants and extension
|
||||||
points are understood.
|
points are understood.
|
||||||
|
|
||||||
The intended next structural contracts are workspace/split-pane layout,
|
The intended next structural contracts are responsive page toolbar, content
|
||||||
responsive page toolbar, content grid, form-section layout, dialog body/footer
|
grid, form-section layout, dialog body/footer layout and shared empty/error
|
||||||
layout and shared empty/error state composition. Their APIs must remain
|
state composition. Their APIs must remain composable; a central component is
|
||||||
composable; a central component is not a single oversized page template.
|
not a single oversized page template.
|
||||||
|
|
||||||
## Surface Archetypes
|
## Surface Archetypes
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ cd /mnt/DATA/git/govoplan-workflow/webui
|
|||||||
cd /mnt/DATA/git/govoplan-dashboard/webui
|
cd /mnt/DATA/git/govoplan-dashboard/webui
|
||||||
"$NPM" run test:dashboard-layout
|
"$NPM" run test:dashboard-layout
|
||||||
|
|
||||||
|
cd /mnt/DATA/git/govoplan-approvals/webui
|
||||||
|
"$NPM" run test:workspace-layout
|
||||||
|
|
||||||
cd /mnt/DATA/git/govoplan-postbox/webui
|
cd /mnt/DATA/git/govoplan-postbox/webui
|
||||||
"$NPM" run test:ui-structure
|
"$NPM" run test:ui-structure
|
||||||
|
|
||||||
@@ -141,3 +144,4 @@ cd /mnt/DATA/git/govoplan-mail/webui
|
|||||||
cd /mnt/DATA/git/govoplan-campaign/webui
|
cd /mnt/DATA/git/govoplan-campaign/webui
|
||||||
"$NPM" run test:policy-ui
|
"$NPM" run test:policy-ui
|
||||||
"$NPM" run test:template-preview
|
"$NPM" run test:template-preview
|
||||||
|
"$NPM" run test:accessibility-contract
|
||||||
|
|||||||
@@ -11,21 +11,53 @@ import sys
|
|||||||
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||||
REPOS_ROOT = META_ROOT.parent
|
REPOS_ROOT = META_ROOT.parent
|
||||||
BASELINE_PATH = pathlib.Path(__file__).with_name("shared-webui-layout-baseline.txt")
|
BASELINE_PATH = pathlib.Path(__file__).with_name("shared-webui-layout-baseline.txt")
|
||||||
|
WORKSPACE_BASELINE_PATH = pathlib.Path(__file__).with_name(
|
||||||
|
"shared-webui-workspace-baseline.txt"
|
||||||
|
)
|
||||||
RAW_PAGE_FRAME = 'className="content-pad workspace-data-page'
|
RAW_PAGE_FRAME = 'className="content-pad workspace-data-page'
|
||||||
|
RAW_WORKSPACE = re.compile(r'<div\s+className="workspace(?:\s|\")')
|
||||||
LOCAL_PAGE_LAYOUT = re.compile(r"\b(?:function|class|const)\s+PageLayout\b")
|
LOCAL_PAGE_LAYOUT = re.compile(r"\b(?:function|class|const)\s+PageLayout\b")
|
||||||
|
LOCAL_WORKSPACE_LAYOUT = re.compile(
|
||||||
|
r"\b(?:function|class|const)\s+WorkspaceLayout\b"
|
||||||
|
)
|
||||||
CENTRAL_LAYOUT = pathlib.Path("govoplan-core/webui/src/components/PageLayout.tsx")
|
CENTRAL_LAYOUT = pathlib.Path("govoplan-core/webui/src/components/PageLayout.tsx")
|
||||||
|
CENTRAL_WORKSPACE_LAYOUT = pathlib.Path(
|
||||||
|
"govoplan-core/webui/src/components/WorkspaceLayout.tsx"
|
||||||
|
)
|
||||||
REQUIRED_CONSUMERS = (
|
REQUIRED_CONSUMERS = (
|
||||||
pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx"),
|
pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx"),
|
||||||
pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"),
|
pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"),
|
||||||
pathlib.Path("govoplan-dashboard/webui/src/features/dashboard/DashboardPage.tsx"),
|
pathlib.Path("govoplan-dashboard/webui/src/features/dashboard/DashboardPage.tsx"),
|
||||||
pathlib.Path("govoplan-ops/webui/src/features/ops/OpsPage.tsx"),
|
pathlib.Path("govoplan-ops/webui/src/features/ops/OpsPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/AttachmentsDataPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignAuditPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignFieldsPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignJsonView.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignListPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/MailSettingsPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/ReviewSendPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/TemplateDataPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/reports/AggregateReportsPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/templates/TemplatesPage.tsx"),
|
||||||
|
)
|
||||||
|
REQUIRED_WORKSPACE_CONSUMERS = (
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignWorkspace.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignModulePage.tsx"),
|
||||||
|
pathlib.Path("govoplan-campaign/webui/src/features/templates/TemplatesPage.tsx"),
|
||||||
|
pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def baseline_paths() -> set[pathlib.Path]:
|
def baseline_paths(path: pathlib.Path) -> set[pathlib.Path]:
|
||||||
return {
|
return {
|
||||||
pathlib.Path(line.strip())
|
pathlib.Path(line.strip())
|
||||||
for line in BASELINE_PATH.read_text(encoding="utf-8").splitlines()
|
for line in path.read_text(encoding="utf-8").splitlines()
|
||||||
if line.strip() and not line.lstrip().startswith("#")
|
if line.strip() and not line.lstrip().startswith("#")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +79,7 @@ def main() -> int:
|
|||||||
sources = source_paths()
|
sources = source_paths()
|
||||||
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
|
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
|
||||||
raw_frames = {path for path, text in source_text.items() if RAW_PAGE_FRAME in text}
|
raw_frames = {path for path, text in source_text.items() if RAW_PAGE_FRAME in text}
|
||||||
baseline = baseline_paths()
|
baseline = baseline_paths(BASELINE_PATH)
|
||||||
available_baseline = {
|
available_baseline = {
|
||||||
path for path in baseline if (REPOS_ROOT / path.parts[0]).is_dir()
|
path for path in baseline if (REPOS_ROOT / path.parts[0]).is_dir()
|
||||||
}
|
}
|
||||||
@@ -63,9 +95,30 @@ def main() -> int:
|
|||||||
errors.append("Remove migrated page frames from the shared-layout baseline:")
|
errors.append("Remove migrated page frames from the shared-layout baseline:")
|
||||||
errors.extend(f"- {path}" for path in resolved)
|
errors.extend(f"- {path}" for path in resolved)
|
||||||
|
|
||||||
|
raw_workspaces = {
|
||||||
|
path for path, text in source_text.items() if RAW_WORKSPACE.search(text)
|
||||||
|
}
|
||||||
|
workspace_baseline = baseline_paths(WORKSPACE_BASELINE_PATH)
|
||||||
|
available_workspace_baseline = {
|
||||||
|
path
|
||||||
|
for path in workspace_baseline
|
||||||
|
if (REPOS_ROOT / path.parts[0]).is_dir()
|
||||||
|
}
|
||||||
|
unexpected_workspaces = sorted(raw_workspaces - available_workspace_baseline)
|
||||||
|
if unexpected_workspaces:
|
||||||
|
errors.append("New raw workspaces must use @govoplan/core-webui WorkspaceLayout:")
|
||||||
|
errors.extend(f"- {path}" for path in unexpected_workspaces)
|
||||||
|
|
||||||
|
resolved_workspaces = sorted(available_workspace_baseline - raw_workspaces)
|
||||||
|
if resolved_workspaces:
|
||||||
|
errors.append("Remove migrated workspaces from the shared-workspace baseline:")
|
||||||
|
errors.extend(f"- {path}" for path in resolved_workspaces)
|
||||||
|
|
||||||
for path, text in source_text.items():
|
for path, text in source_text.items():
|
||||||
if path != CENTRAL_LAYOUT and LOCAL_PAGE_LAYOUT.search(text):
|
if path != CENTRAL_LAYOUT and LOCAL_PAGE_LAYOUT.search(text):
|
||||||
errors.append(f"Module-local PageLayout definition is not allowed: {path}")
|
errors.append(f"Module-local PageLayout definition is not allowed: {path}")
|
||||||
|
if path != CENTRAL_WORKSPACE_LAYOUT and LOCAL_WORKSPACE_LAYOUT.search(text):
|
||||||
|
errors.append(f"Module-local WorkspaceLayout definition is not allowed: {path}")
|
||||||
|
|
||||||
for path in REQUIRED_CONSUMERS:
|
for path in REQUIRED_CONSUMERS:
|
||||||
absolute_path = REPOS_ROOT / path
|
absolute_path = REPOS_ROOT / path
|
||||||
@@ -77,9 +130,21 @@ def main() -> int:
|
|||||||
if RAW_PAGE_FRAME in text:
|
if RAW_PAGE_FRAME in text:
|
||||||
errors.append(f"Required shared-layout consumer restored the raw page frame: {path}")
|
errors.append(f"Required shared-layout consumer restored the raw page frame: {path}")
|
||||||
|
|
||||||
|
for path in REQUIRED_WORKSPACE_CONSUMERS:
|
||||||
|
absolute_path = REPOS_ROOT / path
|
||||||
|
if not absolute_path.exists():
|
||||||
|
continue
|
||||||
|
text = absolute_path.read_text(encoding="utf-8")
|
||||||
|
if "<WorkspaceLayout" not in text:
|
||||||
|
errors.append(f"Required shared-workspace consumer no longer uses WorkspaceLayout: {path}")
|
||||||
|
if RAW_WORKSPACE.search(text):
|
||||||
|
errors.append(f"Required shared-workspace consumer restored the raw workspace: {path}")
|
||||||
|
|
||||||
core_index = REPOS_ROOT / "govoplan-core/webui/src/index.ts"
|
core_index = REPOS_ROOT / "govoplan-core/webui/src/index.ts"
|
||||||
if core_index.exists() and "PageLayout, PageHeader" not in core_index.read_text(encoding="utf-8"):
|
if core_index.exists() and "PageLayout, PageHeader" not in core_index.read_text(encoding="utf-8"):
|
||||||
errors.append("Core must export PageLayout and PageHeader from @govoplan/core-webui.")
|
errors.append("Core must export PageLayout and PageHeader from @govoplan/core-webui.")
|
||||||
|
if core_index.exists() and "WorkspaceLayout" not in core_index.read_text(encoding="utf-8"):
|
||||||
|
errors.append("Core must export WorkspaceLayout from @govoplan/core-webui.")
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
print("\n".join(errors), file=sys.stderr)
|
print("\n".join(errors), file=sys.stderr)
|
||||||
@@ -88,7 +153,9 @@ def main() -> int:
|
|||||||
print(
|
print(
|
||||||
"Shared WebUI layout contract passed: "
|
"Shared WebUI layout contract passed: "
|
||||||
f"{len(REQUIRED_CONSUMERS)} central consumers, "
|
f"{len(REQUIRED_CONSUMERS)} central consumers, "
|
||||||
f"{len(raw_frames)} registered legacy page-frame files."
|
f"{len(raw_frames)} registered legacy page-frame files; "
|
||||||
|
f"{len(REQUIRED_WORKSPACE_CONSUMERS)} workspace consumers, "
|
||||||
|
f"{len(raw_workspaces)} registered legacy workspace files."
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,6 @@
|
|||||||
# Existing raw page frames. Remove an entry when that surface adopts the Core
|
# Existing raw page frames. Remove an entry when that surface adopts the Core
|
||||||
# PageLayout contract. New entries are rejected by check-shared-webui-layouts.py.
|
# PageLayout contract. New entries are rejected by check-shared-webui-layouts.py.
|
||||||
govoplan-access/webui/src/features/admin/AdminPage.tsx
|
govoplan-access/webui/src/features/admin/AdminPage.tsx
|
||||||
govoplan-campaign/webui/src/features/campaigns/AttachmentsDataPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/CampaignAuditPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/CampaignFieldsPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/CampaignJsonView.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/CampaignListPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/MailSettingsPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/ReviewSendPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/TemplateDataPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx
|
|
||||||
govoplan-campaign/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/reports/AggregateReportsPage.tsx
|
|
||||||
govoplan-campaign/webui/src/features/templates/TemplatesPage.tsx
|
|
||||||
govoplan-core/webui/src/features/settings/SettingsPage.tsx
|
govoplan-core/webui/src/features/settings/SettingsPage.tsx
|
||||||
govoplan-docs/webui/src/features/docs/DocsPage.tsx
|
govoplan-docs/webui/src/features/docs/DocsPage.tsx
|
||||||
govoplan-mail/webui/src/features/mail/MailBouncePage.tsx
|
govoplan-mail/webui/src/features/mail/MailBouncePage.tsx
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Existing raw full-canvas workspaces. Remove an entry when that surface adopts
|
||||||
|
# WorkspaceLayout. New entries are rejected by check-shared-webui-layouts.py.
|
||||||
|
govoplan-access/webui/src/features/admin/AdminPage.tsx
|
||||||
|
govoplan-core/webui/src/features/settings/SettingsPage.tsx
|
||||||
|
govoplan-docs/webui/src/features/docs/DocsPage.tsx
|
||||||
|
govoplan-organizations/webui/src/features/organizations/OrganizationsPage.tsx
|
||||||
Reference in New Issue
Block a user