Files
wizarr/app/templates/settings/wizard/steps.html
engels74 0fb4767457 fix(migrations): reorder wizard category migration and apply pre-rebase formatting
Migration Changes:
- Renamed migration file from 20251004 to 20251005
- Updated down_revision from fd5a34530162 to 08a6c8fb44db
- Ensures migration runs after upstream notification events migration
- Fixes migration chain branching issue
- Removed incorrect merge migration (1e83c67d9785)

Migration chain is now:
fd5a34530162 (disabled attribute) → 08a6c8fb44db (notification events)
→ 20251005_add_category_to_wizard_step (wizard category)

Wizard Refactoring:
- Enhanced uniqueness constraint for WizardStep from (server_type, position)
  to (server_type, category, position)
- Ensures steps in different categories (pre_invite vs post_invite) can have
  the same position indexing without conflict
- Updated wizard state management documentation to explicitly list and
  describe data-* attributes used in steps.html template

Code Quality:
- Applied djlint formatting to 8 template files to minimize rebase conflicts
- Applied ruff format and ruff check --fix (no changes needed)
- Removed redundant requirement comments from spec-driven development
- Fixed case sensitivity in test assertion (POST vs post)
- Updated test references from 20251004 to 20251005 to match corrected
  migration revision ID
2025-10-16 16:57:14 +02:00

227 lines
15 KiB
HTML

{# templates/settings/wizard/steps.html #}
<div id="step-modal"></div>
<script>
// Clear modal when content is loaded/refreshed
document.getElementById('step-modal').innerHTML = '';
</script>
<section class="space-y-6">
{% set is_default = True %}
{% include 'settings/wizard/_sub_nav.html' with context %}
{% if not grouped %}<p class="text-gray-600 dark:text-gray-300">{{ _("No steps found.") }}</p>{% endif %}
{% for server, categories in grouped.items() %}
<div class="border rounded-md p-4 dark:border-gray-700">
{# Server header with actions #}
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-100">{{ server|capitalize }}</h2>
<div class="flex items-center gap-2">
<form method="post"
action="{{ url_for('wizard_admin.reset_server_steps', server_type=server) }}"
class="inline"
hx-post="{{ url_for('wizard_admin.reset_server_steps', server_type=server) }}"
hx-target="#tab-body"
hx-swap="innerHTML"
hx-confirm="{{ _('Are you sure you want to reset all steps for %(server)s to defaults? This will delete all custom steps and reimport the default ones.', server=server|capitalize) }}">
<button type="submit"
class="inline-flex items-center justify-center p-2 text-orange-600 rounded-lg hover:text-white hover:bg-orange-500 dark:text-orange-400 dark:hover:text-white dark:hover:bg-orange-600"
title="{{ _('Reset %(server)s Steps to Defaults', server=server|capitalize) }}">
<svg class="w-5 h-5"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.651 7.65a7.131 7.131 0 0 0-12.68 3.15M18.001 4v4h-4m-7.652 8.35a7.13 7.13 0 0 0 12.68-3.15M6 20v-4h4" />
</svg>
</button>
</form>
<a href="{{ url_for('wizard_admin.export_server_steps', server_type=server) }}"
class="inline-flex items-center justify-center p-2 text-gray-600 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-600"
title="{{ _('Export %(server)s Steps', server=server|capitalize) }}">
<svg class="w-5 h-5"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M9 7V2.221a2 2 0 0 0-.5.365L4.586 6.5a2 2 0 0 0-.365.5H9Zm2 0V2h7a2 2 0 0 1 2 2v9.293l-2-2a1 1 0 0 0-1.414 1.414l.293.293h-6.586a1 1 0 1 0 0 2h6.586l-.293.293A1 1 0 0 0 18 16.707l2-2V20a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9h5a2 2 0 0 0 2-2Z" clip-rule="evenodd" />
</svg>
</a>
<a href="{{ url_for('wizard.step', server=server, idx=0) }}"
target="_blank"
class="inline-flex items-center justify-center p-2 text-gray-800 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:text-white dark:hover:bg-gray-600"
title="{{ _('Preview Wizard') }}">
<svg class="w-5 h-5"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 14v4.833A1.166 1.166 0 0 1 16.833 20H5.167A1.167 1.167 0 0 1 4 18.833V7.167A1.166 1.166 0 0 1 5.167 6h4.618m4.447-2H20v5.768m-7.889 2.121 7.778-7.778" />
</svg>
</a>
</div>
</div>
{# Two-column layout for pre-invite and post-invite steps #}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{# Left column: Pre-Invite Steps #}
<div class="wizard-category-section border rounded-lg p-4 bg-gray-50 dark:bg-gray-900 dark:border-gray-600">
<h3 class="text-base font-semibold text-gray-800 dark:text-gray-100 mb-2">{{ _("Before Invite Acceptance") }}</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ _("Steps shown to users before they accept the invitation") }}
</p>
{% set pre_steps = categories.get('pre_invite', []) %}
{# Always render the wizard-steps container, even when empty, so it can be a Sortable.js drop target #}
<ol class="wizard-steps bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700
{% if not pre_steps %}min-h-[120px] flex items-center justify-center{% endif %}"
data-server="{{ server }}"
data-category="pre_invite"
data-reorder-url="{{ url_for('wizard_admin.reorder_steps') }}">
{% if pre_steps %}
{% for step in pre_steps %}
<li class="flex items-center justify-between px-4 py-3"
data-id="{{ step.id }}">
<svg class="drag w-4 h-4 cursor-grab text-gray-400 mr-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd" d="M7 4a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0zM7 9a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0zM7 14a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0z" clip-rule="evenodd" />
</svg>
<span class="flex-1 text-sm font-medium text-gray-900 dark:text-white">{{ step.title|render_jinja if step.title else _("Untitled") }}</span>
<span class="flex items-center gap-2">
<button hx-get="{{ url_for('wizard_admin.edit_step', step_id=step.id) }}"
hx-target="#step-modal"
hx-trigger="click"
class="inline-flex items-center justify-center p-2 text-gray-800 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:text-white dark:hover:bg-gray-600">
<svg class="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z">
</path>
</svg>
</button>
<form method="post"
action="{{ url_for('wizard_admin.delete_step', step_id=step.id) }}"
class="inline"
hx-post="{{ url_for('wizard_admin.delete_step', step_id=step.id) }}"
hx-target="#tab-body"
hx-swap="innerHTML"
hx-confirm="{{ _('Are you sure you want to delete this step?') }}">
<button type="submit"
class="inline-flex items-center justify-center p-2 text-red-500 rounded-lg hover:text-white hover:bg-red-500 dark:text-red-400 dark:hover:text-white dark:hover:bg-red-600">
<svg class="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd">
</path>
</svg>
</button>
</form>
</span>
<svg class="drag w-4 h-4 cursor-grab text-gray-400 ml-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd" d="M7 4a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0zM7 9a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0zM7 14a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0z" clip-rule="evenodd" />
</svg>
</li>
{% endfor %}
{% else %}
{# Empty state message displayed inside the sortable container #}
<li class="empty-state-item flex flex-col items-center justify-center text-center py-8 text-gray-400 dark:text-gray-500 w-full list-none">
<svg class="w-12 h-12 mb-2 opacity-50"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
<p class="text-sm">{{ _("No pre-invite steps configured") }}</p>
</li>
{% endif %}
</ol>
</div>
{# Right column: Post-Invite Steps #}
<div class="wizard-category-section border rounded-lg p-4 bg-gray-50 dark:bg-gray-900 dark:border-gray-600">
<h3 class="text-base font-semibold text-gray-800 dark:text-gray-100 mb-2">{{ _("After Invite Acceptance") }}</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ _("Steps shown to users after they accept the invitation") }}
</p>
{% set post_steps = categories.get('post_invite', []) %}
{# Always render the wizard-steps container, even when empty, so it can be a Sortable.js drop target #}
<ol class="wizard-steps bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700
{% if not post_steps %}min-h-[120px] flex items-center justify-center{% endif %}"
data-server="{{ server }}"
data-category="post_invite"
data-reorder-url="{{ url_for('wizard_admin.reorder_steps') }}">
{% if post_steps %}
{% for step in post_steps %}
<li class="flex items-center justify-between px-4 py-3"
data-id="{{ step.id }}">
<svg class="drag w-4 h-4 cursor-grab text-gray-400 mr-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd" d="M7 4a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0zM7 9a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0zM7 14a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0z" clip-rule="evenodd" />
</svg>
<span class="flex-1 text-sm font-medium text-gray-900 dark:text-white">{{ step.title|render_jinja if step.title else _("Untitled") }}</span>
<span class="flex items-center gap-2">
<button hx-get="{{ url_for('wizard_admin.edit_step', step_id=step.id) }}"
hx-target="#step-modal"
hx-trigger="click"
class="inline-flex items-center justify-center p-2 text-gray-800 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:text-white dark:hover:bg-gray-600">
<svg class="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z">
</path>
</svg>
</button>
<form method="post"
action="{{ url_for('wizard_admin.delete_step', step_id=step.id) }}"
class="inline"
hx-post="{{ url_for('wizard_admin.delete_step', step_id=step.id) }}"
hx-target="#tab-body"
hx-swap="innerHTML"
hx-confirm="{{ _('Are you sure you want to delete this step?') }}">
<button type="submit"
class="inline-flex items-center justify-center p-2 text-red-500 rounded-lg hover:text-white hover:bg-red-500 dark:text-red-400 dark:hover:text-white dark:hover:bg-red-600">
<svg class="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd">
</path>
</svg>
</button>
</form>
</span>
<svg class="drag w-4 h-4 cursor-grab text-gray-400 ml-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd" d="M7 4a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0zM7 9a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0zM7 14a1 1 0 112 0 1 1 0 01-2 0zm4 0a1 1 0 112 0 1 1 0 01-2 0z" clip-rule="evenodd" />
</svg>
</li>
{% endfor %}
{% else %}
{# Empty state message displayed inside the sortable container #}
<li class="empty-state-item flex flex-col items-center justify-center text-center py-8 text-gray-400 dark:text-gray-500 w-full list-none">
<svg class="w-12 h-12 mb-2 opacity-50"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
<p class="text-sm">{{ _("No post-invite steps configured") }}</p>
</li>
{% endif %}
</ol>
</div>
</div>
</div>
{% endfor %}
</section>