fix: use table_header_custom for custom headers in subscription pages
The table_header() macro doesn't support caller() - it takes a columns list.
Using {% call table_header() %} caused a Jinja2 error:
"macro 'table_header' was invoked with two values for the special caller argument"
Changes:
- Add table_header_custom() macro that supports caller() for custom headers
- Update subscriptions.html, subscription-tiers.html, billing-history.html
- Add TPL-008 architecture rule to detect this pattern
- Renumber TPL-009 (block names) and TPL-010 (Alpine vars)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -381,6 +381,37 @@ template_rules:
|
|||||||
recommended_pattern: '<template x-if="items.length === 0">No items</template>'
|
recommended_pattern: '<template x-if="items.length === 0">No items</template>'
|
||||||
|
|
||||||
- id: "TPL-008"
|
- 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 <th> 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') }}
|
||||||
|
<th class="px-4 py-3">Actions</th>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
RIGHT (supports caller):
|
||||||
|
{% call table_header_custom() %}
|
||||||
|
{{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}
|
||||||
|
<th class="px-4 py-3">Actions</th>
|
||||||
|
{% 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"
|
name: "Use valid block names from base templates"
|
||||||
severity: "error"
|
severity: "error"
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
|
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
|
||||||
{% from 'shared/macros/headers.html' import page_header_refresh %}
|
{% 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 %}
|
{% from 'shared/macros/pagination.html' import pagination_full %}
|
||||||
|
|
||||||
{% block title %}Billing History{% endblock %}
|
{% block title %}Billing History{% endblock %}
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
<!-- Invoices Table -->
|
<!-- Invoices Table -->
|
||||||
{% call table_wrapper() %}
|
{% call table_wrapper() %}
|
||||||
<table class="w-full whitespace-nowrap">
|
<table class="w-full whitespace-nowrap">
|
||||||
{% call table_header() %}
|
{% call table_header_custom() %}
|
||||||
{{ th_sortable('invoice_date', 'Date', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('invoice_date', 'Date', 'sortBy', 'sortOrder') }}
|
||||||
<th class="px-4 py-3">Invoice #</th>
|
<th class="px-4 py-3">Invoice #</th>
|
||||||
{{ th_sortable('vendor_name', 'Vendor', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('vendor_name', 'Vendor', 'sortBy', 'sortOrder') }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
|
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
|
||||||
{% from 'shared/macros/headers.html' import page_header_refresh %}
|
{% 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 %}
|
{% from 'shared/macros/modals.html' import modal_confirm %}
|
||||||
|
|
||||||
{% block title %}Subscription Tiers{% endblock %}
|
{% block title %}Subscription Tiers{% endblock %}
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<!-- Tiers Table -->
|
<!-- Tiers Table -->
|
||||||
{% call table_wrapper() %}
|
{% call table_wrapper() %}
|
||||||
<table class="w-full whitespace-nowrap">
|
<table class="w-full whitespace-nowrap">
|
||||||
{% call table_header() %}
|
{% call table_header_custom() %}
|
||||||
<th class="px-4 py-3">#</th>
|
<th class="px-4 py-3">#</th>
|
||||||
{{ th_sortable('code', 'Code', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('code', 'Code', 'sortBy', 'sortOrder') }}
|
||||||
{{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
|
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
|
||||||
{% from 'shared/macros/headers.html' import page_header_refresh %}
|
{% 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 %}
|
{% from 'shared/macros/pagination.html' import pagination_full %}
|
||||||
|
|
||||||
{% block title %}Vendor Subscriptions{% endblock %}
|
{% block title %}Vendor Subscriptions{% endblock %}
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
<!-- Subscriptions Table -->
|
<!-- Subscriptions Table -->
|
||||||
{% call table_wrapper() %}
|
{% call table_wrapper() %}
|
||||||
<table class="w-full whitespace-nowrap">
|
<table class="w-full whitespace-nowrap">
|
||||||
{% call table_header() %}
|
{% call table_header_custom() %}
|
||||||
{{ th_sortable('vendor_name', 'Vendor', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('vendor_name', 'Vendor', 'sortBy', 'sortOrder') }}
|
||||||
{{ th_sortable('tier', 'Tier', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('tier', 'Tier', 'sortBy', 'sortOrder') }}
|
||||||
{{ th_sortable('status', 'Status', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('status', 'Status', 'sortBy', 'sortOrder') }}
|
||||||
|
|||||||
@@ -48,6 +48,27 @@
|
|||||||
{% endmacro %}
|
{% 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') }}
|
||||||
|
<th class="px-4 py-3">Actions</th>
|
||||||
|
{% endcall %}
|
||||||
|
#}
|
||||||
|
{% macro table_header_custom() %}
|
||||||
|
<thead>
|
||||||
|
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||||
|
{{ caller() }}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{#
|
{#
|
||||||
Sortable Table Header
|
Sortable Table Header
|
||||||
=====================
|
=====================
|
||||||
|
|||||||
@@ -813,7 +813,10 @@ class ArchitectureValidator:
|
|||||||
# TPL-007: Check empty state implementation
|
# TPL-007: Check empty state implementation
|
||||||
self._check_template_empty_state(file_path, content, lines)
|
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:
|
if is_admin:
|
||||||
self._check_valid_block_names(file_path, content, lines)
|
self._check_valid_block_names(file_path, content, lines)
|
||||||
|
|
||||||
@@ -1058,11 +1061,41 @@ class ArchitectureValidator:
|
|||||||
suggestion='Add <template x-if="items.length === 0">No items found</template>',
|
suggestion='Add <template x-if="items.length === 0">No items found</template>',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 <th> elements",
|
||||||
|
)
|
||||||
|
|
||||||
def _check_valid_block_names(
|
def _check_valid_block_names(
|
||||||
self, file_path: Path, content: str, lines: list[str]
|
self, file_path: Path, content: str, lines: list[str]
|
||||||
):
|
):
|
||||||
"""TPL-008: Check that templates use valid block names from base template"""
|
"""TPL-009: Check that templates use valid block names from base template"""
|
||||||
if "noqa: tpl-008" in content.lower():
|
if "noqa: tpl-009" in content.lower():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Skip base templates
|
# Skip base templates
|
||||||
@@ -1092,7 +1125,7 @@ class ArchitectureValidator:
|
|||||||
block_name = match.group(1)
|
block_name = match.group(1)
|
||||||
if block_name in invalid_blocks:
|
if block_name in invalid_blocks:
|
||||||
self._add_violation(
|
self._add_violation(
|
||||||
rule_id="TPL-008",
|
rule_id="TPL-009",
|
||||||
rule_name="Use valid block names from base templates",
|
rule_name="Use valid block names from base templates",
|
||||||
severity=Severity.ERROR,
|
severity=Severity.ERROR,
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
@@ -1105,8 +1138,8 @@ class ArchitectureValidator:
|
|||||||
def _check_alpine_template_vars(
|
def _check_alpine_template_vars(
|
||||||
self, file_path: Path, content: str, lines: list[str], js_content: str
|
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"""
|
"""TPL-010: Check that Alpine variables used in templates are defined in JS"""
|
||||||
if "noqa: tpl-009" in content.lower():
|
if "noqa: tpl-010" in content.lower():
|
||||||
return
|
return
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@@ -1141,7 +1174,7 @@ class ArchitectureValidator:
|
|||||||
for i, line in enumerate(lines, 1):
|
for i, line in enumerate(lines, 1):
|
||||||
if var in line and ("error_state" in line or "action_dropdown" in line):
|
if var in line and ("error_state" in line or "action_dropdown" in line):
|
||||||
self._add_violation(
|
self._add_violation(
|
||||||
rule_id="TPL-009",
|
rule_id="TPL-010",
|
||||||
rule_name="Alpine variables must be defined in JS component",
|
rule_name="Alpine variables must be defined in JS component",
|
||||||
severity=Severity.ERROR,
|
severity=Severity.ERROR,
|
||||||
file_path=file_path,
|
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:
|
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)
|
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:
|
if not is_base_or_partial:
|
||||||
self._check_valid_block_names(file_path, content, lines)
|
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:
|
if not is_base_or_partial and not is_macro and not is_components_page:
|
||||||
# Try to find corresponding JS file
|
# Try to find corresponding JS file
|
||||||
# Template: app/templates/admin/messages.html -> JS: static/admin/js/messages.js
|
# Template: app/templates/admin/messages.html -> JS: static/admin/js/messages.js
|
||||||
|
|||||||
Reference in New Issue
Block a user