mirror of
https://github.com/wizarrrr/wizarr.git
synced 2025-12-23 23:59:23 -05:00
refactor(wizard): improve combo invitation flow consistency
This commit implements two improvements to the combo wizard invitation flow: 1. Badge Display Fix: Added the "pre-invite" / "post-invite" category badge above wizard steps in combo invites. This badge was already present in single-invite pages but was missing from combo invites. 2. URL Refactoring: Cleaned up combo wizard URLs to match the pattern used by single-invite wizards: - Before: /wizard/combo/0?category=pre_invite → /j/<code> → /wizard/combo/0?category=post_invite - After: /wizard/combo/pre_invite → /j/<code> → /wizard/combo/post_invite This makes combo URLs consistent with single-invite URLs (/wizard/pre-wizard → /wizard/post-wizard) by removing the step index from the path and using path-based category routing. Changes include: - Pass step_phase to template for badge display - Update routes to use path-based categories instead of query parameters - Update templates and JavaScript to handle new URL structure - Fix form method attributes and template formatting
This commit is contained in:
@@ -532,8 +532,8 @@ def pre_wizard(idx: int = 0):
|
||||
server_order = [s.server_type for s in servers]
|
||||
session["wizard_server_order"] = server_order
|
||||
|
||||
# Redirect to combo route with pre_invite category
|
||||
return redirect(url_for("wizard.combo", idx=idx, category="pre_invite"))
|
||||
# Redirect to combo route with pre_invite category (path-based)
|
||||
return redirect(url_for("wizard.combo", category="pre_invite"))
|
||||
|
||||
# Single server invitation - handle normally
|
||||
# Determine server type from invitation
|
||||
@@ -683,10 +683,8 @@ def post_wizard(idx: int = 0):
|
||||
server_order = [s.server_type for s in servers]
|
||||
session["wizard_server_order"] = server_order
|
||||
|
||||
# Redirect to combo route with post_invite category
|
||||
return redirect(
|
||||
url_for("wizard.combo", idx=idx, category="post_invite")
|
||||
)
|
||||
# Redirect to combo route with post_invite category (path-based)
|
||||
return redirect(url_for("wizard.combo", category="post_invite"))
|
||||
|
||||
# Single server invitation
|
||||
server_type = _get_server_type_from_invitation(invitation)
|
||||
@@ -843,23 +841,29 @@ def step(server, idx):
|
||||
# ─── combined wizard for multi-server invites ─────────────────────────────
|
||||
|
||||
|
||||
@wizard_bp.route("/combo/<int:idx>")
|
||||
def combo(idx: int):
|
||||
@wizard_bp.route("/combo/<category>")
|
||||
@wizard_bp.route("/combo/<category>/<int:idx>")
|
||||
def combo(category: str, idx: int = 0):
|
||||
"""Combined wizard for multi-server invites with category support.
|
||||
|
||||
This route handles multi-server invitations by concatenating wizard steps
|
||||
from all servers in the invitation. It supports both pre-invite and post-invite
|
||||
categories, determined by the 'category' query parameter.
|
||||
categories, determined by the category path parameter.
|
||||
|
||||
Args:
|
||||
idx: Current step index
|
||||
|
||||
Query Parameters:
|
||||
category: 'pre_invite' or 'post_invite' (default: 'post_invite')
|
||||
category: 'pre_invite' or 'post_invite'
|
||||
idx: Current step index (default: 0)
|
||||
|
||||
Returns:
|
||||
Rendered wizard template or redirect response
|
||||
"""
|
||||
# Validate category parameter
|
||||
if category not in ["pre_invite", "post_invite"]:
|
||||
current_app.logger.warning(
|
||||
f"Invalid category '{category}' for combo wizard, defaulting to post_invite"
|
||||
)
|
||||
category = "post_invite"
|
||||
|
||||
try:
|
||||
cfg = _settings()
|
||||
except Exception as e:
|
||||
@@ -877,11 +881,6 @@ def combo(idx: int):
|
||||
)
|
||||
return redirect(url_for("wizard.start"))
|
||||
|
||||
# Determine category from query parameter (default: post_invite for backward compatibility)
|
||||
category = request.args.get("category", "post_invite")
|
||||
if category not in ["pre_invite", "post_invite"]:
|
||||
category = "post_invite"
|
||||
|
||||
# Determine phase for template rendering
|
||||
phase = "pre" if category == "pre_invite" else "post"
|
||||
invite_code = InviteCodeManager.get_invite_code()
|
||||
@@ -962,6 +961,7 @@ def combo(idx: int):
|
||||
direction=direction,
|
||||
require_interaction=require_interaction,
|
||||
phase=phase, # Pass phase based on category
|
||||
step_phase=phase, # Pass step_phase to enable phase badge display
|
||||
current_server_type=current_server_type, # NEW: Pass current server type for display
|
||||
completion_url=completion_url,
|
||||
completion_label=completion_label,
|
||||
@@ -976,6 +976,9 @@ def combo(idx: int):
|
||||
resp.headers["X-Require-Interaction"] = (
|
||||
"true" if require_interaction else "false"
|
||||
)
|
||||
resp.headers["X-Wizard-Step-Phase"] = (
|
||||
phase # FIX: Add phase header for badge updates
|
||||
)
|
||||
resp.headers["X-Current-Server-Type"] = (
|
||||
current_server_type # NEW: Indicate current server
|
||||
)
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
{% endif %}
|
||||
<form class="space-y-3 md:space-y-4"
|
||||
action="{{ url_for('public.process_invitation') }}"
|
||||
method="POST">
|
||||
method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-field"
|
||||
data-field="1"
|
||||
|
||||
@@ -111,12 +111,18 @@
|
||||
<!-- FLOATING NAV BUTTONS (mobile - fixed at bottom) -->
|
||||
<div class="wizard-nav-mobile md:hidden">
|
||||
<a id="wizard-prev-btn"
|
||||
{% if phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.pre_wizard', idx=idx-1) }}"
|
||||
{% if server_type == 'combo' %}
|
||||
{% if phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.combo', category='pre_invite', idx=idx-1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.combo', category='post_invite', idx=idx-1) }}"
|
||||
{% endif %}
|
||||
{% elif phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.pre_wizard', idx=idx-1) }}"
|
||||
{% elif phase == 'post' %}
|
||||
hx-get="{{ url_for('wizard.post_wizard', idx=idx-1) }}"
|
||||
hx-get="{{ url_for('wizard.post_wizard', idx=idx-1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.step', server=server_type, idx=idx-1) }}"
|
||||
hx-get="{{ url_for('wizard.step', server=server_type, idx=idx-1) }}"
|
||||
{% endif %}
|
||||
hx-vals='{"dir":"prev"}'
|
||||
hx-target="#wizard-content"
|
||||
@@ -133,13 +139,19 @@
|
||||
{% if is_pre_final %}
|
||||
href="{{ completion_href }}" hx-get="{{ completion_href }}" hx-target="#wizard-content" hx-swap="outerHTML swap:0s" hx-indicator=".htmx-indicator" data-final-step="1"
|
||||
{% else %}
|
||||
{% if phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.pre_wizard', idx=idx+1) }}"
|
||||
{% elif phase == 'post' %}
|
||||
hx-get="{{ url_for('wizard.post_wizard', idx=idx+1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.step', server=server_type, idx=idx+1) }}"
|
||||
{% endif %}
|
||||
{% if server_type == 'combo' %}
|
||||
{% if phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.combo', category='pre_invite', idx=idx+1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.combo', category='post_invite', idx=idx+1) }}"
|
||||
{% endif %}
|
||||
{% elif phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.pre_wizard', idx=idx+1) }}"
|
||||
{% elif phase == 'post' %}
|
||||
hx-get="{{ url_for('wizard.post_wizard', idx=idx+1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.step', server=server_type, idx=idx+1) }}"
|
||||
{% endif %}
|
||||
hx-vals='{"dir":"next"}'
|
||||
hx-target="#wizard-content"
|
||||
hx-swap="outerHTML swap:0s"
|
||||
@@ -160,12 +172,18 @@
|
||||
<!-- DESKTOP NAV BUTTONS (centered below card) -->
|
||||
<div class="hidden md:flex justify-center space-x-4 mt-6">
|
||||
<a id="wizard-prev-btn-desktop"
|
||||
{% if phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.pre_wizard', idx=idx-1) }}"
|
||||
{% if server_type == 'combo' %}
|
||||
{% if phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.combo', category='pre_invite', idx=idx-1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.combo', category='post_invite', idx=idx-1) }}"
|
||||
{% endif %}
|
||||
{% elif phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.pre_wizard', idx=idx-1) }}"
|
||||
{% elif phase == 'post' %}
|
||||
hx-get="{{ url_for('wizard.post_wizard', idx=idx-1) }}"
|
||||
hx-get="{{ url_for('wizard.post_wizard', idx=idx-1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.step', server=server_type, idx=idx-1) }}"
|
||||
hx-get="{{ url_for('wizard.step', server=server_type, idx=idx-1) }}"
|
||||
{% endif %}
|
||||
hx-vals='{"dir":"prev"}'
|
||||
hx-target="#wizard-content"
|
||||
@@ -177,13 +195,19 @@
|
||||
{% if is_pre_final %}
|
||||
href="{{ completion_href }}" hx-get="{{ completion_href }}" hx-target="#wizard-content" hx-swap="outerHTML swap:0s" hx-indicator=".htmx-indicator" data-final-step="1"
|
||||
{% else %}
|
||||
{% if phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.pre_wizard', idx=idx+1) }}"
|
||||
{% elif phase == 'post' %}
|
||||
hx-get="{{ url_for('wizard.post_wizard', idx=idx+1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.step', server=server_type, idx=idx+1) }}"
|
||||
{% endif %}
|
||||
{% if server_type == 'combo' %}
|
||||
{% if phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.combo', category='pre_invite', idx=idx+1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.combo', category='post_invite', idx=idx+1) }}"
|
||||
{% endif %}
|
||||
{% elif phase == 'pre' %}
|
||||
hx-get="{{ url_for('wizard.pre_wizard', idx=idx+1) }}"
|
||||
{% elif phase == 'post' %}
|
||||
hx-get="{{ url_for('wizard.post_wizard', idx=idx+1) }}"
|
||||
{% else %}
|
||||
hx-get="{{ url_for('wizard.step', server=server_type, idx=idx+1) }}"
|
||||
{% endif %}
|
||||
hx-vals='{"dir":"next"}'
|
||||
hx-target="#wizard-content"
|
||||
hx-swap="outerHTML swap:0s"
|
||||
@@ -476,9 +500,13 @@
|
||||
|
||||
const targetIdx = isPrev ? idx - 1 : idx + 1;
|
||||
|
||||
// Generate URL based on phase
|
||||
// Generate URL based on phase and server type
|
||||
let newUrl;
|
||||
if (phase === 'pre') {
|
||||
if (serverType === 'combo') {
|
||||
// Combo wizard uses path-based category routing
|
||||
const category = phase === 'pre' ? 'pre_invite' : 'post_invite';
|
||||
newUrl = `/wizard/combo/${category}/${targetIdx}`;
|
||||
} else if (phase === 'pre') {
|
||||
newUrl = `/wizard/pre-wizard/${targetIdx}`;
|
||||
} else if (phase === 'post') {
|
||||
newUrl = `/wizard/post-wizard/${targetIdx}`;
|
||||
|
||||
@@ -542,8 +542,8 @@ class TestInvitationUIComponents:
|
||||
expect(page.locator("label").filter(has_text="Password").first).to_be_visible()
|
||||
expect(page.locator("label").filter(has_text="Email")).to_be_visible()
|
||||
|
||||
# Check form uses POST method
|
||||
expect(page.locator("form")).to_have_attribute("method", "POST")
|
||||
# Check form uses POST method (lowercase is HTML standard)
|
||||
expect(page.locator("form")).to_have_attribute("method", "post")
|
||||
|
||||
# Test keyboard navigation
|
||||
page.keyboard.press("Tab") # Should focus first input
|
||||
|
||||
@@ -346,7 +346,8 @@ class TestComboWizardErrors:
|
||||
|
||||
def test_combo_wizard_without_server_order(self, client):
|
||||
"""Test combo wizard redirects when no server order in session."""
|
||||
response = client.get("/wizard/combo/0", follow_redirects=False)
|
||||
# Test with new path-based category routing
|
||||
response = client.get("/wizard/combo/pre_invite/0", follow_redirects=False)
|
||||
assert response.status_code == 302
|
||||
# Should redirect (exact location may vary)
|
||||
assert response.location is not None
|
||||
@@ -360,9 +361,8 @@ class TestComboWizardErrors:
|
||||
with patch("app.blueprints.wizard.routes._steps") as mock_steps:
|
||||
mock_steps.side_effect = Exception("Database error")
|
||||
|
||||
response = client.get(
|
||||
"/wizard/combo/0?category=pre_invite", follow_redirects=True
|
||||
)
|
||||
# Test with new path-based category routing
|
||||
response = client.get("/wizard/combo/pre_invite/0", follow_redirects=True)
|
||||
assert response.status_code == 200
|
||||
# Should handle error gracefully
|
||||
|
||||
|
||||
Reference in New Issue
Block a user