Files
thrillwiki_django_no_react/backend/templates/htmx/status_with_actions.html
pacnpal 45d97b6e68 Add test utilities and state machine diagrams for FSM models
- Introduced reusable test utilities in `backend/tests/utils` for FSM transitions, HTMX interactions, and common scenarios.
- Added factory functions for creating test submissions, parks, rides, and photo submissions.
- Implemented assertion helpers for verifying state changes, toast notifications, and transition logs.
- Created comprehensive state machine diagrams for all FSM-enabled models in `docs/STATE_DIAGRAMS.md`, detailing states, transitions, and guard conditions.
2025-12-22 08:55:39 -05:00

149 lines
7.7 KiB
HTML

{% comment %}
FSM Status Badge with Actions Partial Template
Displays current status badge alongside available transition buttons.
Combines status display with action capabilities for a cohesive UX.
Required context:
- object: The FSM-enabled model instance
- user: The current user (usually request.user)
Optional context:
- target_id: The ID of the element to swap after transition
- show_badge: Whether to show the status badge (defaults to true)
- badge_only: Only show the badge, no actions (defaults to false)
- dropdown_actions: Show actions in a dropdown menu (defaults to false)
- compact: Use compact layout (defaults to false)
{% endcomment %}
{% load fsm_tags %}
{% get_available_transitions object user as transitions %}
<div class="status-with-actions flex items-center gap-3 {% if compact %}gap-2{% endif %}">
{% if show_badge|default:True %}
<!-- Status Badge -->
<span class="status-badge inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full" data-status-badge
{% with status=object|get_state_value %}
{% if status == 'PENDING' %}
bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300
{% elif status == 'APPROVED' %}
bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300
{% elif status == 'REJECTED' %}
bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300
{% elif status == 'ESCALATED' %}
bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300
{% elif status == 'IN_PROGRESS' or status == 'PROCESSING' %}
bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300
{% elif status == 'COMPLETED' or status == 'RESOLVED' %}
bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300
{% elif status == 'CANCELLED' or status == 'DISMISSED' %}
bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300
{% else %}
bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300
{% endif %}
{% endwith %}">
{% with choice=object|get_state_choice %}
{% if choice and choice.metadata.icon %}
<i class="fas fa-{{ choice.metadata.icon }}"></i>
{% else %}
{% with status=object|get_state_value %}
<i class="fas fa-{% if status == 'PENDING' %}clock{% elif status == 'APPROVED' %}check{% elif status == 'REJECTED' %}times{% elif status == 'ESCALATED' %}exclamation{% elif status == 'IN_PROGRESS' or status == 'PROCESSING' %}spinner{% elif status == 'COMPLETED' or status == 'RESOLVED' %}check-circle{% else %}circle{% endif %}"></i>
{% endwith %}
{% endif %}
{% endwith %}
{{ object|get_state_display }}
</span>
{% endif %}
{% if not badge_only %}
{% if transitions %}
{% if dropdown_actions %}
<!-- Dropdown Actions -->
<div class="relative" x-data="{ open: false }">
<button
@click="open = !open"
@click.outside="open = false"
type="button"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700">
<span>Actions</span>
<i class="fas fa-chevron-down text-xs transition-transform" :class="{ 'rotate-180': open }"></i>
</button>
<div
x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700 z-50"
x-cloak>
<div class="py-1">
{% for transition in transitions %}
<button
type="button"
hx-post="{% url 'core:fsm_transition' app_label=object|app_label model_name=object|model_name pk=object.pk transition_name=transition.name %}"
hx-target="#{{ target_id|default:object|default_target_id }}"
hx-swap="outerHTML"
hx-include="closest .review-notes textarea[name='notes']"
{% if transition.requires_confirm %}
hx-confirm="{{ transition.confirm_message|default:'Are you sure?' }}"
{% endif %}
@click="open = false"
class="w-full flex items-center gap-2 px-4 py-2 text-sm text-left
{% if transition.style == 'green' %}text-green-700 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-900/30
{% elif transition.style == 'red' %}text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/30
{% elif transition.style == 'yellow' %}text-yellow-700 hover:bg-yellow-50 dark:text-yellow-400 dark:hover:bg-yellow-900/30
{% elif transition.style == 'blue' %}text-blue-700 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/30
{% else %}text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700
{% endif %}">
<i class="fas fa-{{ transition.icon|default:'arrow-right' }} w-4"></i>
{{ transition.label }}
</button>
{% endfor %}
</div>
</div>
</div>
{% else %}
<!-- Inline Actions -->
<div class="inline-flex items-center gap-2">
{% for transition in transitions %}
<button
type="button"
hx-post="{% url 'core:fsm_transition' app_label=object|app_label model_name=object|model_name pk=object.pk transition_name=transition.name %}"
hx-target="#{{ target_id|default:object|default_target_id }}"
hx-swap="outerHTML"
hx-include="closest .review-notes textarea[name='notes']"
{% if transition.requires_confirm %}
hx-confirm="{{ transition.confirm_message|default:'Are you sure?' }}"
{% endif %}
hx-indicator="#loading-{{ object.id }}"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-all duration-200
{% if transition.style == 'green' %}
bg-green-600 text-white hover:bg-green-500 dark:bg-green-700 dark:hover:bg-green-600
{% elif transition.style == 'red' %}
bg-red-600 text-white hover:bg-red-500 dark:bg-red-700 dark:hover:bg-red-600
{% elif transition.style == 'yellow' %}
bg-yellow-600 text-white hover:bg-yellow-500 dark:bg-yellow-700 dark:hover:bg-yellow-600
{% elif transition.style == 'blue' %}
bg-blue-600 text-white hover:bg-blue-500 dark:bg-blue-700 dark:hover:bg-blue-600
{% else %}
bg-gray-600 text-white hover:bg-gray-500 dark:bg-gray-700 dark:hover:bg-gray-600
{% endif %}">
<i class="fas fa-{{ transition.icon|default:'arrow-right' }}"></i>
<span>{{ transition.label }}</span>
</button>
{% endfor %}
<!-- Loading indicator -->
<span id="loading-{{ object.id }}" class="htmx-indicator">
<i class="fas fa-spinner fa-spin text-blue-500"></i>
</span>
</div>
{% endif %}
{% endif %}
{% endif %}
</div>