448 lines
18 KiB
Python
448 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""Enforce Core ownership of repeated WebUI layout and dialog anatomy."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
|
|
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
|
REPOS_ROOT = META_ROOT.parent
|
|
CORE_INDEX = pathlib.Path("govoplan-core/webui/src/index.ts")
|
|
DIALOG_WIDTH_EXCEPTIONS = META_ROOT / "tools/checks/shared-webui-dialog-width-exceptions.txt"
|
|
|
|
RAW_ELEMENT = re.compile(
|
|
r'<(?P<tag>div|span|header|section|form)\b(?P<attrs>[^>]*?)'
|
|
r'\bclassName="(?P<classes>[^"]+)"',
|
|
re.DOTALL,
|
|
)
|
|
LEGACY_LAYOUT_TOKENS = {
|
|
"form-grid",
|
|
"admin-form-grid",
|
|
"dashboard-grid",
|
|
"settings-grid",
|
|
"admin-dialog",
|
|
"admin-dialog-wide",
|
|
"admin-details-grid",
|
|
"detail-list",
|
|
"metric-grid",
|
|
}
|
|
CENTRAL_COMPONENTS = {
|
|
"PageActionBar": pathlib.Path(
|
|
"govoplan-core/webui/src/components/PageActionBar.tsx"
|
|
),
|
|
"ActionToolbar": pathlib.Path(
|
|
"govoplan-core/webui/src/components/ActionToolbar.tsx"
|
|
),
|
|
"ToolbarGroup": pathlib.Path(
|
|
"govoplan-core/webui/src/components/ActionToolbar.tsx"
|
|
),
|
|
"ToolbarSpacer": pathlib.Path(
|
|
"govoplan-core/webui/src/components/ActionToolbar.tsx"
|
|
),
|
|
"ContentGrid": pathlib.Path(
|
|
"govoplan-core/webui/src/components/ContentGrid.tsx"
|
|
),
|
|
"ContentSection": pathlib.Path(
|
|
"govoplan-core/webui/src/components/ContentSection.tsx"
|
|
),
|
|
"FormGrid": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
|
|
"FormLayout": pathlib.Path(
|
|
"govoplan-core/webui/src/components/ContentGrid.tsx"
|
|
),
|
|
"GridItem": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
|
|
"FormSection": pathlib.Path(
|
|
"govoplan-core/webui/src/components/FormSection.tsx"
|
|
),
|
|
"DialogActions": pathlib.Path(
|
|
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
|
|
),
|
|
"DialogForm": pathlib.Path(
|
|
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
|
|
),
|
|
"DialogSection": pathlib.Path(
|
|
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
|
|
),
|
|
"DescriptionList": pathlib.Path(
|
|
"govoplan-core/webui/src/components/DescriptionList.tsx"
|
|
),
|
|
"DescriptionItem": pathlib.Path(
|
|
"govoplan-core/webui/src/components/DescriptionList.tsx"
|
|
),
|
|
"MetricGrid": pathlib.Path(
|
|
"govoplan-core/webui/src/components/MetricGrid.tsx"
|
|
),
|
|
"MetricCard": pathlib.Path(
|
|
"govoplan-core/webui/src/components/MetricCard.tsx"
|
|
),
|
|
"FilterBar": pathlib.Path(
|
|
"govoplan-core/webui/src/components/FilterBar.tsx"
|
|
),
|
|
"StatePanel": pathlib.Path(
|
|
"govoplan-core/webui/src/components/StatePanel.tsx"
|
|
),
|
|
"CountBadge": pathlib.Path(
|
|
"govoplan-core/webui/src/components/CountBadge.tsx"
|
|
),
|
|
"SelectionList": pathlib.Path(
|
|
"govoplan-core/webui/src/components/SelectionList.tsx"
|
|
),
|
|
"SelectionListItem": pathlib.Path(
|
|
"govoplan-core/webui/src/components/SelectionList.tsx"
|
|
),
|
|
"SelectionListItemContent": pathlib.Path(
|
|
"govoplan-core/webui/src/components/SelectionList.tsx"
|
|
),
|
|
"WorkspaceLayout": pathlib.Path(
|
|
"govoplan-core/webui/src/components/WorkspaceLayout.tsx"
|
|
),
|
|
"WorkspaceFrame": pathlib.Path(
|
|
"govoplan-core/webui/src/components/WorkspaceFrame.tsx"
|
|
),
|
|
"DefinitionPalette": pathlib.Path(
|
|
"govoplan-core/webui/src/components/DefinitionPalette.tsx"
|
|
),
|
|
"DefinitionPaletteGroup": pathlib.Path(
|
|
"govoplan-core/webui/src/components/DefinitionPalette.tsx"
|
|
),
|
|
"DefinitionPaletteItem": pathlib.Path(
|
|
"govoplan-core/webui/src/components/DefinitionPalette.tsx"
|
|
),
|
|
"DefinitionNodeIcon": pathlib.Path(
|
|
"govoplan-core/webui/src/components/DefinitionNodeIcon.tsx"
|
|
),
|
|
"FloatingStatus": pathlib.Path(
|
|
"govoplan-core/webui/src/components/FloatingStatus.tsx"
|
|
),
|
|
}
|
|
REQUIRED_CONSUMERS = {
|
|
"PageActionBar": (
|
|
pathlib.Path("govoplan-payments/webui/src/features/payments/PaymentsPage.tsx"),
|
|
),
|
|
"ActionToolbar": (
|
|
pathlib.Path("govoplan-core/webui/src/components/WysiwygEditor.tsx"),
|
|
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarPage.tsx"),
|
|
pathlib.Path("govoplan-files/webui/src/features/files/FilesPage.tsx"),
|
|
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
|
|
),
|
|
"ContentGrid": (
|
|
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
|
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"),
|
|
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSettingsPanel.tsx"),
|
|
),
|
|
"ContentSection": (
|
|
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
|
|
pathlib.Path("govoplan-dist-lists/webui/src/features/distributionLists/DistributionListsPage.tsx"),
|
|
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
|
|
),
|
|
"FormGrid": (
|
|
pathlib.Path("govoplan-core/webui/src/components/mail/MailServerSettingsPanel.tsx"),
|
|
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
|
|
pathlib.Path("govoplan-forms/webui/src/features/forms/FormDefinitionDialog.tsx"),
|
|
pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxAdminPanel.tsx"),
|
|
),
|
|
"FormSection": (
|
|
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
|
|
pathlib.Path("govoplan-quick-access/webui/src/features/settings/QuickAccessSettingsPanel.tsx"),
|
|
),
|
|
"DialogForm": (
|
|
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
|
|
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
|
|
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
|
|
),
|
|
"DialogSection": (
|
|
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
|
|
pathlib.Path("govoplan-files/webui/src/features/files/components/FileShareDialog.tsx"),
|
|
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
|
|
),
|
|
"DescriptionList": (
|
|
pathlib.Path("govoplan-access/webui/src/features/admin/UsersPanel.tsx"),
|
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx"),
|
|
pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"),
|
|
pathlib.Path("govoplan-policy/webui/src/features/policy/ViewPoliciesPanel.tsx"),
|
|
),
|
|
"MetricGrid": (
|
|
pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"),
|
|
pathlib.Path("govoplan-admin/webui/src/features/admin/ModuleManagementPanel.tsx"),
|
|
pathlib.Path("govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx"),
|
|
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSummaryWidget.tsx"),
|
|
),
|
|
"MetricCard": (
|
|
pathlib.Path("govoplan-admin/webui/src/features/admin/AdminOverviewPanel.tsx"),
|
|
pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"),
|
|
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/ReviewSendPage.tsx"),
|
|
pathlib.Path("govoplan-voting/webui/src/features/voting/VotingPage.tsx"),
|
|
),
|
|
"FilterBar": (
|
|
pathlib.Path("govoplan-cases/webui/src/features/cases/CasesPage.tsx"),
|
|
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
|
|
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
|
|
pathlib.Path("govoplan-tasks/webui/src/features/tasks/TasksPage.tsx"),
|
|
),
|
|
"StatePanel": (
|
|
pathlib.Path("govoplan-committee/webui/src/features/committee/CommitteePage.tsx"),
|
|
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"),
|
|
pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxPage.tsx"),
|
|
pathlib.Path("govoplan-risk-compliance/webui/src/features/riskCompliance/RiskCompliancePage.tsx"),
|
|
),
|
|
"CountBadge": (
|
|
pathlib.Path("govoplan-core/webui/src/layout/Titlebar.tsx"),
|
|
pathlib.Path("govoplan-mail/webui/src/features/mail/MailboxPage.tsx"),
|
|
pathlib.Path("govoplan-search/webui/src/features/search/SearchPage.tsx"),
|
|
),
|
|
"SelectionListItemContent": (
|
|
pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"),
|
|
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
|
|
pathlib.Path("govoplan-voting/webui/src/features/voting/VotingPage.tsx"),
|
|
),
|
|
"WorkspaceLayout": (
|
|
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
|
|
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"),
|
|
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"),
|
|
),
|
|
"WorkspaceFrame": (
|
|
pathlib.Path("govoplan-cases/webui/src/features/cases/CasesPage.tsx"),
|
|
pathlib.Path("govoplan-portal/webui/src/features/portal/PortalPage.tsx"),
|
|
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
|
|
),
|
|
"DefinitionPalette": (
|
|
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
|
|
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"),
|
|
),
|
|
"DefinitionNodeIcon": (
|
|
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowNode.tsx"),
|
|
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowNode.tsx"),
|
|
),
|
|
"FloatingStatus": (
|
|
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
|
|
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"),
|
|
),
|
|
}
|
|
LEGACY_CSS_SELECTOR = re.compile(
|
|
r"(?<![-\w])\.(?:admin-details-grid|detail-list|metric-grid)(?![-\w])"
|
|
)
|
|
RETIRED_LOCAL_CSS_CLASSES = {
|
|
"admin-assignment-grid",
|
|
"approval-metrics",
|
|
"dataflow-node-count",
|
|
"dataflow-shell",
|
|
"datasources-metrics",
|
|
"datasources-shell",
|
|
"dist-lists-metrics",
|
|
"dist-lists-shell",
|
|
"notifications-count",
|
|
"notifications-empty-state",
|
|
"notifications-shell",
|
|
"review-flow-execution-summary",
|
|
"review-flow-fact-grid",
|
|
"risk-metrics",
|
|
"search-filter-count",
|
|
"tasks-empty-detail",
|
|
"tasks-shell",
|
|
"templates-shell",
|
|
"voting-metrics",
|
|
"voting-shell",
|
|
"workflow-shell",
|
|
"dataflow-palette-items",
|
|
"workflow-palette-items",
|
|
"dataflow-working-indicator",
|
|
"workflow-working-indicator",
|
|
"datasources-detail-section",
|
|
"dist-lists-section",
|
|
"templates-section",
|
|
}
|
|
LOCAL_METRIC_HELPER = re.compile(r"\b(?:function\s+Metric\b|const\s+Metric\s*=)")
|
|
CSS_BLOCK = re.compile(r"([^{}]+)\{([^{}]*)\}")
|
|
CSS_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
|
|
DIALOG_CLASS = re.compile(r"\.([A-Za-z0-9_-]*(?:dialog|modal)[A-Za-z0-9_-]*)", re.IGNORECASE)
|
|
DIALOG_WIDTH = re.compile(r"(?:^|;)\s*(?:width|max-width)\s*:", re.MULTILINE)
|
|
DIALOG_WIDTH_VALUE = re.compile(
|
|
r"(?:^|;)\s*(?:width|max-width)\s*:\s*([^;]+)", re.MULTILINE
|
|
)
|
|
STANDARD_DIALOG_WIDTH = re.compile(r"\b(?:460|560|680|1040|1440)px\b")
|
|
DIALOG_INTERNAL_SUFFIXES = (
|
|
"-actions",
|
|
"-body",
|
|
"-close",
|
|
"-content",
|
|
"-field",
|
|
"-fields",
|
|
"-footer",
|
|
"-form",
|
|
"-header",
|
|
"-title",
|
|
)
|
|
|
|
|
|
def source_paths() -> list[pathlib.Path]:
|
|
paths: list[pathlib.Path] = []
|
|
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
|
|
source_root = repository / "webui" / "src"
|
|
if source_root.is_dir():
|
|
paths.extend(sorted(source_root.rglob("*.tsx")))
|
|
return paths
|
|
|
|
|
|
def css_paths() -> list[pathlib.Path]:
|
|
paths: list[pathlib.Path] = []
|
|
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
|
|
source_root = repository / "webui" / "src"
|
|
if source_root.is_dir():
|
|
paths.extend(sorted(source_root.rglob("*.css")))
|
|
return paths
|
|
|
|
|
|
def relative(path: pathlib.Path) -> pathlib.Path:
|
|
return path.relative_to(REPOS_ROOT)
|
|
|
|
|
|
def raw_reason(classes: str) -> str | None:
|
|
tokens = classes.split()
|
|
legacy = sorted(set(tokens) & LEGACY_LAYOUT_TOKENS)
|
|
if legacy:
|
|
return f"legacy shared layout class {', '.join(legacy)}"
|
|
toolbars = [
|
|
token
|
|
for token in tokens
|
|
if token == "admin-toolbar-row" or token.endswith("-toolbar")
|
|
]
|
|
if toolbars:
|
|
return f"raw toolbar class {', '.join(toolbars)}"
|
|
dialog_forms = [token for token in tokens if token.endswith("dialog-form")]
|
|
if dialog_forms:
|
|
return f"raw dialog form class {', '.join(dialog_forms)}"
|
|
return None
|
|
|
|
|
|
def normalized_css_selector(selector: str) -> str:
|
|
return " ".join(selector.split())
|
|
|
|
|
|
def dialog_width_exceptions(styles: dict[pathlib.Path, str]) -> dict[str, str]:
|
|
exceptions: dict[str, str] = {}
|
|
central_dialog_styles = pathlib.Path("govoplan-core/webui/src/styles/dialogs.css")
|
|
for path, content in styles.items():
|
|
if path == central_dialog_styles:
|
|
continue
|
|
without_comments = CSS_COMMENT.sub("", content)
|
|
for match in CSS_BLOCK.finditer(without_comments):
|
|
selector = normalized_css_selector(match.group(1))
|
|
declarations = match.group(2)
|
|
if not DIALOG_WIDTH.search(declarations):
|
|
continue
|
|
dialog_classes = DIALOG_CLASS.findall(selector)
|
|
if not dialog_classes:
|
|
continue
|
|
if all(name.lower().endswith(DIALOG_INTERNAL_SUFFIXES) for name in dialog_classes):
|
|
continue
|
|
signature = f"{path}|{selector}"
|
|
exceptions[signature] = declarations
|
|
return exceptions
|
|
|
|
|
|
def exception_baseline() -> set[str]:
|
|
if not DIALOG_WIDTH_EXCEPTIONS.exists():
|
|
return set()
|
|
return {
|
|
line.strip()
|
|
for line in DIALOG_WIDTH_EXCEPTIONS.read_text(encoding="utf-8").splitlines()
|
|
if line.strip() and not line.lstrip().startswith("#")
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
sources = source_paths()
|
|
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
|
|
styles = css_paths()
|
|
style_text = {relative(path): path.read_text(encoding="utf-8") for path in styles}
|
|
errors: list[str] = []
|
|
|
|
for path, content in source_text.items():
|
|
for match in RAW_ELEMENT.finditer(content):
|
|
reason = raw_reason(match.group("classes"))
|
|
if reason is None:
|
|
continue
|
|
line = content.count("\n", 0, match.start()) + 1
|
|
errors.append(
|
|
f"Raw {match.group('tag')} repeats shared anatomy ({reason}): {path}:{line}"
|
|
)
|
|
|
|
for path, content in style_text.items():
|
|
uncommented = CSS_COMMENT.sub("", content)
|
|
legacy_match = LEGACY_CSS_SELECTOR.search(uncommented)
|
|
if legacy_match:
|
|
line = content.count("\n", 0, legacy_match.start()) + 1
|
|
errors.append(f"Legacy shared layout selector is not allowed: {path}:{line}")
|
|
for class_name in sorted(RETIRED_LOCAL_CSS_CLASSES):
|
|
retired = re.search(rf"(?<![-\w])\.{re.escape(class_name)}(?![-\w])\s*(?:,|\{{)", uncommented)
|
|
if retired:
|
|
line = content.count("\n", 0, retired.start()) + 1
|
|
errors.append(f"Retired module-local shared anatomy selector is not allowed: {path}:{line} ({class_name})")
|
|
|
|
dialog_exceptions = dialog_width_exceptions(style_text)
|
|
baseline = exception_baseline()
|
|
for signature in sorted(set(dialog_exceptions) - baseline):
|
|
errors.append(f"Unreviewed local dialog width; use Dialog size or register a justified exception: {signature}")
|
|
for signature in sorted(baseline - set(dialog_exceptions)):
|
|
errors.append(f"Stale dialog width exception can be removed: {signature}")
|
|
for signature, declarations in sorted(dialog_exceptions.items()):
|
|
width_values = " ".join(DIALOG_WIDTH_VALUE.findall(declarations))
|
|
standard = STANDARD_DIALOG_WIDTH.search(width_values)
|
|
if standard:
|
|
errors.append(f"Local dialog width duplicates Core size {standard.group(0)}: {signature}")
|
|
|
|
for name, owner in CENTRAL_COMPONENTS.items():
|
|
definition = re.compile(
|
|
rf"\b(?:function|class)\s+{name}\b|\bconst\s+{name}\s*="
|
|
)
|
|
for path, content in source_text.items():
|
|
if path != owner and definition.search(content):
|
|
errors.append(f"Module-local {name} definition is not allowed: {path}")
|
|
|
|
for path, content in source_text.items():
|
|
if LOCAL_METRIC_HELPER.search(content):
|
|
errors.append(f"Module-local Metric helper is not allowed; compose MetricCard directly: {path}")
|
|
|
|
usage_counts: dict[str, int] = {}
|
|
for name in CENTRAL_COMPONENTS:
|
|
usage = re.compile(rf"<{name}\b")
|
|
usage_counts[name] = sum(bool(usage.search(content)) for content in source_text.values())
|
|
|
|
for name, consumers in REQUIRED_CONSUMERS.items():
|
|
for path in consumers:
|
|
absolute = REPOS_ROOT / path
|
|
if not absolute.exists():
|
|
continue
|
|
if f"<{name}" not in absolute.read_text(encoding="utf-8"):
|
|
errors.append(f"Required shared {name} consumer regressed: {path}")
|
|
|
|
core_index_path = REPOS_ROOT / CORE_INDEX
|
|
if core_index_path.exists():
|
|
core_index = core_index_path.read_text(encoding="utf-8")
|
|
for name in CENTRAL_COMPONENTS:
|
|
if not re.search(rf"\b{name}\b", core_index):
|
|
errors.append(f"Core must export {name} from @govoplan/core-webui.")
|
|
|
|
dialog_path = REPOS_ROOT / "govoplan-core/webui/src/components/Dialog.tsx"
|
|
if dialog_path.exists():
|
|
dialog_text = dialog_path.read_text(encoding="utf-8")
|
|
if "<DialogActions" not in dialog_text:
|
|
errors.append("Every Core Dialog footer must compose DialogActions.")
|
|
|
|
if errors:
|
|
print("\n".join(errors), file=sys.stderr)
|
|
return 1
|
|
|
|
migrated = ", ".join(
|
|
f"{name}={usage_counts[name]} files"
|
|
for name in ("PageActionBar", "ActionToolbar", "WorkspaceFrame", "WorkspaceLayout", "FilterBar", "StatePanel", "SelectionList", "CountBadge", "DefinitionPalette", "DefinitionNodeIcon", "FloatingStatus", "ContentSection", "ContentGrid", "FormGrid", "FormSection", "DialogForm", "DialogSection", "MetricGrid", "MetricCard", "DescriptionList")
|
|
)
|
|
print(f"Shared WebUI primitive contract passed: {migrated}; {len(dialog_exceptions)} reviewed dialog width exceptions; no raw legacy anatomy.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|