mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 14:51:09 -05:00
- 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.
149 lines
7.7 KiB
HTML
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>
|