chore(webui): enforce semantic interface patterns
This commit is contained in:
@@ -28,59 +28,22 @@ 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 = (
|
||||
pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx"),
|
||||
pathlib.Path("govoplan-core/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-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"),
|
||||
pathlib.Path("govoplan-access/webui/src/features/admin/AdminPage.tsx"),
|
||||
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"),
|
||||
pathlib.Path("govoplan-mail/webui/src/features/mail/MailBouncePage.tsx"),
|
||||
pathlib.Path("govoplan-payments/webui/src/features/payments/PaymentsPage.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"),
|
||||
pathlib.Path("govoplan-access/webui/src/features/admin/AdminPage.tsx"),
|
||||
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"),
|
||||
pathlib.Path("govoplan-organizations/webui/src/features/organizations/OrganizationsPage.tsx"),
|
||||
)
|
||||
HEADERLESS_PAGE_LAYOUT_CONSUMERS = {
|
||||
pathlib.Path("govoplan-access/webui/src/features/admin/AdminPage.tsx"),
|
||||
CENTRAL_ACTION_BARS = {
|
||||
pathlib.Path("govoplan-core/webui/src/components/PageActionBar.tsx"),
|
||||
pathlib.Path("govoplan-core/webui/src/components/WorkspaceActionBar.tsx"),
|
||||
}
|
||||
EDITOR_PAGE_CONSUMERS = (
|
||||
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-dashboard/webui/src/features/dashboard/DashboardPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/AttachmentsDataPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignFieldsPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.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/TemplateDataPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx"),
|
||||
SEMANTIC_ACTION_USAGE = re.compile(r"<(?:Page|Workspace)ActionBar\b")
|
||||
EDITOR_ACTION_USAGE = re.compile(
|
||||
r"<(?:Page|Workspace)ActionBar\b[\s\S]{0,1200}?variant=\"editor\""
|
||||
)
|
||||
DESTRUCTIVE_PAGE_CONSUMERS = (
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.tsx"),
|
||||
PANEL_HEADER_ACTION_TOOLBAR = re.compile(
|
||||
r"<ActionToolbar\b[^>]*\bsurface=\"panel-header\""
|
||||
)
|
||||
UNSAVED_GUARD_MARKERS = (
|
||||
"useUnsavedDraftGuard",
|
||||
"useCampaignDraftEditor",
|
||||
"useRegisterUnsavedChanges",
|
||||
"semantic-editor-guard:",
|
||||
)
|
||||
|
||||
|
||||
@@ -108,6 +71,22 @@ def relative(path: pathlib.Path) -> pathlib.Path:
|
||||
def main() -> int:
|
||||
sources = source_paths()
|
||||
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
|
||||
page_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path != CENTRAL_LAYOUT and PAGE_LAYOUT_USAGE.search(text)
|
||||
}
|
||||
workspace_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path != CENTRAL_WORKSPACE_LAYOUT and "<WorkspaceLayout" in text
|
||||
}
|
||||
action_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path not in CENTRAL_ACTION_BARS and SEMANTIC_ACTION_USAGE.search(text)
|
||||
}
|
||||
editor_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path not in CENTRAL_ACTION_BARS and EDITOR_ACTION_USAGE.search(text)
|
||||
}
|
||||
raw_frames = {path for path, text in source_text.items() if RAW_PAGE_FRAME in text}
|
||||
baseline = baseline_paths(BASELINE_PATH)
|
||||
available_baseline = {
|
||||
@@ -160,9 +139,9 @@ def main() -> int:
|
||||
if (
|
||||
page_layout_count
|
||||
and "actions=" in text
|
||||
and path not in HEADERLESS_PAGE_LAYOUT_CONSUMERS
|
||||
and path != pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx")
|
||||
and "<PageActionBar" not in text
|
||||
and "semantic-page-actions: delegated" not in text
|
||||
):
|
||||
errors.append(
|
||||
f"Headed page actions must use the semantic PageActionBar: {path}"
|
||||
@@ -172,51 +151,31 @@ def main() -> int:
|
||||
f"Ambiguous consequential action slots are forbidden; use destructiveActions: {path}"
|
||||
)
|
||||
|
||||
for path in REQUIRED_CONSUMERS:
|
||||
absolute_path = REPOS_ROOT / path
|
||||
if not absolute_path.exists():
|
||||
continue
|
||||
text = absolute_path.read_text(encoding="utf-8")
|
||||
if "<PageLayout" not in text:
|
||||
errors.append(f"Required shared-layout consumer no longer uses PageLayout: {path}")
|
||||
if RAW_PAGE_FRAME in text:
|
||||
errors.append(f"Required shared-layout consumer restored the raw page frame: {path}")
|
||||
if (
|
||||
path not in CENTRAL_ACTION_BARS
|
||||
and PANEL_HEADER_ACTION_TOOLBAR.search(text)
|
||||
):
|
||||
errors.append(
|
||||
"Panel-header actions must use WorkspaceActionBar so their ordering, "
|
||||
f"state, and destructive separation remain semantic: {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}")
|
||||
if "<WorkspaceFrame" in text and not SEMANTIC_ACTION_USAGE.search(text):
|
||||
errors.append(
|
||||
f"WorkspaceFrame routes must declare a semantic page or pane action bar: {path}"
|
||||
)
|
||||
|
||||
for path in EDITOR_PAGE_CONSUMERS:
|
||||
absolute_path = REPOS_ROOT / path
|
||||
if not absolute_path.exists():
|
||||
continue
|
||||
text = absolute_path.read_text(encoding="utf-8")
|
||||
for required in ('variant="editor"', "dirty=", "discardAction=", "saveAction="):
|
||||
for path in editor_consumers:
|
||||
text = source_text[path]
|
||||
for required in ('variant="editor"', "state=", "discardAction=", "saveAction="):
|
||||
if required not in text:
|
||||
errors.append(f"Editor page is missing {required}: {path}")
|
||||
if path.name != "CampaignDraftPageScaffold.tsx" and not any(
|
||||
guard in text
|
||||
for guard in (
|
||||
"useUnsavedDraftGuard",
|
||||
"useCampaignDraftEditor",
|
||||
"useRegisterUnsavedChanges",
|
||||
)
|
||||
):
|
||||
if not any(guard in text for guard in UNSAVED_GUARD_MARKERS):
|
||||
errors.append(f"Editor page is missing an unsaved-change guard: {path}")
|
||||
|
||||
for path in DESTRUCTIVE_PAGE_CONSUMERS:
|
||||
absolute_path = REPOS_ROOT / path
|
||||
if not absolute_path.exists():
|
||||
continue
|
||||
text = absolute_path.read_text(encoding="utf-8")
|
||||
if "destructiveActions=" not in text or 'variant="danger"' not in text:
|
||||
errors.append(f"Destructive page actions lost their separated slot: {path}")
|
||||
for path, text in source_text.items():
|
||||
if "destructiveActions=" in text and 'variant="danger"' not in text:
|
||||
errors.append(f"Destructive page actions must contain a danger action: {path}")
|
||||
|
||||
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"):
|
||||
@@ -225,6 +184,8 @@ def main() -> int:
|
||||
errors.append("Core must export WorkspaceLayout from @govoplan/core-webui.")
|
||||
if core_index.exists() and "PageActionBar" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export PageActionBar from @govoplan/core-webui.")
|
||||
if core_index.exists() and "WorkspaceActionBar" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export WorkspaceActionBar from @govoplan/core-webui.")
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
@@ -232,11 +193,12 @@ def main() -> int:
|
||||
|
||||
print(
|
||||
"Shared WebUI layout contract passed: "
|
||||
f"{len(REQUIRED_CONSUMERS)} central consumers, "
|
||||
f"{len(page_consumers)} discovered page consumers, "
|
||||
f"{sum(len(PAGE_LAYOUT_USAGE.findall(text)) for text in source_text.values())} semantic pages, "
|
||||
f"{len(EDITOR_PAGE_CONSUMERS)} guarded editor consumers, "
|
||||
f"{len(action_consumers)} semantic action consumers, "
|
||||
f"{len(editor_consumers)} guarded editor consumers, "
|
||||
f"{len(raw_frames)} registered legacy page-frame files; "
|
||||
f"{len(REQUIRED_WORKSPACE_CONSUMERS)} workspace consumers, "
|
||||
f"{len(workspace_consumers)} discovered workspace consumers, "
|
||||
f"{len(raw_workspaces)} registered legacy workspace files."
|
||||
)
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user