diff --git a/.architecture-rules/frontend.yaml b/.architecture-rules/frontend.yaml
index 4189a5a4..b33dad69 100644
--- a/.architecture-rules/frontend.yaml
+++ b/.architecture-rules/frontend.yaml
@@ -381,6 +381,37 @@ template_rules:
recommended_pattern: 'No items'
- id: "TPL-008"
+ name: "Use table_header_custom for custom headers, not table_header"
+ severity: "error"
+ description: |
+ When using {% call %} to create custom table headers with th_sortable
+ or custom
elements, you MUST use table_header_custom(), not table_header().
+
+ The table_header() macro takes a columns list and does NOT support caller().
+ Using {% call table_header() %} causes a Jinja2 error:
+ "macro 'table_header' was invoked with two values for the special caller argument"
+
+ WRONG (causes 500 error):
+ {% call table_header() %}
+ {{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}
+ | Actions |
+ {% endcall %}
+
+ RIGHT (supports caller):
+ {% call table_header_custom() %}
+ {{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}
+ Actions |
+ {% endcall %}
+
+ OR for simple headers (list of column names):
+ {{ table_header(['Name', 'Email', 'Status', 'Actions']) }}
+ pattern:
+ file_pattern: "app/templates/**/*.html"
+ anti_patterns:
+ - "{%\\s*call\\s+table_header\\s*\\(\\s*\\)\\s*%}"
+ required_alternative: "table_header_custom"
+
+ - id: "TPL-009"
name: "Use valid block names from base templates"
severity: "error"
description: |
diff --git a/app/templates/admin/billing-history.html b/app/templates/admin/billing-history.html
index cfdda5e7..21048f6c 100644
--- a/app/templates/admin/billing-history.html
+++ b/app/templates/admin/billing-history.html
@@ -2,7 +2,7 @@
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
{% from 'shared/macros/headers.html' import page_header_refresh %}
-{% from 'shared/macros/tables.html' import table_wrapper, table_header, th_sortable %}
+{% from 'shared/macros/tables.html' import table_wrapper, table_header_custom, th_sortable %}
{% from 'shared/macros/pagination.html' import pagination_full %}
{% block title %}Billing History{% endblock %}
@@ -107,7 +107,7 @@
{% call table_wrapper() %}
- {% call table_header() %}
+ {% call table_header_custom() %}
{{ th_sortable('invoice_date', 'Date', 'sortBy', 'sortOrder') }}
| Invoice # |
{{ th_sortable('vendor_name', 'Vendor', 'sortBy', 'sortOrder') }}
diff --git a/app/templates/admin/subscription-tiers.html b/app/templates/admin/subscription-tiers.html
index cd83f916..6aa5402b 100644
--- a/app/templates/admin/subscription-tiers.html
+++ b/app/templates/admin/subscription-tiers.html
@@ -3,7 +3,7 @@
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
{% from 'shared/macros/headers.html' import page_header_refresh %}
-{% from 'shared/macros/tables.html' import table_wrapper, table_header, th_sortable, empty_state %}
+{% from 'shared/macros/tables.html' import table_wrapper, table_header_custom, th_sortable, empty_state %}
{% from 'shared/macros/modals.html' import modal_confirm %}
{% block title %}Subscription Tiers{% endblock %}
@@ -85,7 +85,7 @@
{% call table_wrapper() %}
- {% call table_header() %}
+ {% call table_header_custom() %}
| # |
{{ th_sortable('code', 'Code', 'sortBy', 'sortOrder') }}
{{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}
diff --git a/app/templates/admin/subscriptions.html b/app/templates/admin/subscriptions.html
index ddccef39..458f1527 100644
--- a/app/templates/admin/subscriptions.html
+++ b/app/templates/admin/subscriptions.html
@@ -2,7 +2,7 @@
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
{% from 'shared/macros/headers.html' import page_header_refresh %}
-{% from 'shared/macros/tables.html' import table_wrapper, table_header, th_sortable %}
+{% from 'shared/macros/tables.html' import table_wrapper, table_header_custom, th_sortable %}
{% from 'shared/macros/pagination.html' import pagination_full %}
{% block title %}Vendor Subscriptions{% endblock %}
@@ -139,7 +139,7 @@
{% call table_wrapper() %}
- {% call table_header() %}
+ {% call table_header_custom() %}
{{ th_sortable('vendor_name', 'Vendor', 'sortBy', 'sortOrder') }}
{{ th_sortable('tier', 'Tier', 'sortBy', 'sortOrder') }}
{{ th_sortable('status', 'Status', 'sortBy', 'sortOrder') }}
diff --git a/app/templates/shared/macros/tables.html b/app/templates/shared/macros/tables.html
index f1af74a0..2940986b 100644
--- a/app/templates/shared/macros/tables.html
+++ b/app/templates/shared/macros/tables.html
@@ -48,6 +48,27 @@
{% endmacro %}
+{#
+ Custom Table Header
+ ===================
+ Renders a table header with custom content via caller().
+ Use this when you need th_sortable or custom th elements.
+
+ Usage:
+ {% call table_header_custom() %}
+ {{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}
+ | Actions |
+ {% endcall %}
+#}
+{% macro table_header_custom() %}
+
+
+ {{ caller() }}
+
+
+{% endmacro %}
+
+
{#
Sortable Table Header
=====================
diff --git a/scripts/validate_architecture.py b/scripts/validate_architecture.py
index b8046aef..686c6783 100755
--- a/scripts/validate_architecture.py
+++ b/scripts/validate_architecture.py
@@ -813,7 +813,10 @@ class ArchitectureValidator:
# TPL-007: Check empty state implementation
self._check_template_empty_state(file_path, content, lines)
- # TPL-008: Check for invalid block names
+ # TPL-008: Check for call table_header() pattern (should be table_header_custom)
+ self._check_table_header_call_pattern(file_path, content, lines)
+
+ # TPL-009: Check for invalid block names
if is_admin:
self._check_valid_block_names(file_path, content, lines)
@@ -1058,11 +1061,41 @@ class ArchitectureValidator:
suggestion='Add No items found',
)
+ def _check_table_header_call_pattern(
+ self, file_path: Path, content: str, lines: list[str]
+ ):
+ """TPL-008: Check that {% call table_header() %} is not used.
+
+ The table_header() macro doesn't support caller() - it takes a columns list.
+ Using {% call table_header() %} causes a Jinja2 error:
+ "macro 'table_header' was invoked with two values for the special caller argument"
+
+ Use table_header_custom() instead for custom headers with caller().
+ """
+ if "noqa: tpl-008" in content.lower():
+ return
+
+ import re
+ pattern = re.compile(r"{%\s*call\s+table_header\s*\(\s*\)\s*%}")
+
+ for i, line in enumerate(lines, 1):
+ if pattern.search(line):
+ self._add_violation(
+ rule_id="TPL-008",
+ rule_name="Use table_header_custom for custom headers",
+ severity=Severity.ERROR,
+ file_path=file_path,
+ line_number=i,
+ message="{% call table_header() %} causes Jinja2 error - table_header doesn't support caller()",
+ context=line.strip()[:80],
+ suggestion="Use {% call table_header_custom() %} for custom headers with th_sortable or custom elements",
+ )
+
def _check_valid_block_names(
self, file_path: Path, content: str, lines: list[str]
):
- """TPL-008: Check that templates use valid block names from base template"""
- if "noqa: tpl-008" in content.lower():
+ """TPL-009: Check that templates use valid block names from base template"""
+ if "noqa: tpl-009" in content.lower():
return
# Skip base templates
@@ -1092,7 +1125,7 @@ class ArchitectureValidator:
block_name = match.group(1)
if block_name in invalid_blocks:
self._add_violation(
- rule_id="TPL-008",
+ rule_id="TPL-009",
rule_name="Use valid block names from base templates",
severity=Severity.ERROR,
file_path=file_path,
@@ -1105,8 +1138,8 @@ class ArchitectureValidator:
def _check_alpine_template_vars(
self, file_path: Path, content: str, lines: list[str], js_content: str
):
- """TPL-009: Check that Alpine variables used in templates are defined in JS"""
- if "noqa: tpl-009" in content.lower():
+ """TPL-010: Check that Alpine variables used in templates are defined in JS"""
+ if "noqa: tpl-010" in content.lower():
return
import re
@@ -1141,7 +1174,7 @@ class ArchitectureValidator:
for i, line in enumerate(lines, 1):
if var in line and ("error_state" in line or "action_dropdown" in line):
self._add_violation(
- rule_id="TPL-009",
+ rule_id="TPL-010",
rule_name="Alpine variables must be defined in JS component",
severity=Severity.ERROR,
file_path=file_path,
@@ -2913,11 +2946,14 @@ class ArchitectureValidator:
if not is_base_or_partial and not is_macro and not is_components_page:
self._check_number_stepper_macro_usage(file_path, content, lines)
- # TPL-008: Check for invalid block names
+ # TPL-008: Check for call table_header() pattern
+ self._check_table_header_call_pattern(file_path, content, lines)
+
+ # TPL-009: Check for invalid block names
if not is_base_or_partial:
self._check_valid_block_names(file_path, content, lines)
- # TPL-009: Check Alpine variables are defined in JS
+ # TPL-010: Check Alpine variables are defined in JS
if not is_base_or_partial and not is_macro and not is_components_page:
# Try to find corresponding JS file
# Template: app/templates/admin/messages.html -> JS: static/admin/js/messages.js
|