ux pattern cleanup.

This commit is contained in:
Bryce
2025-08-08 08:46:56 -07:00
parent 65c00e062b
commit 608cd7357c
9 changed files with 304 additions and 102 deletions

View File

@@ -78,12 +78,8 @@ def add_folder():
db.session.add(folder)
db.session.commit()
# Get updated list of folders for the current user
folders = Folder.query.filter_by(user_id=current_user.id).all()
response = make_response(render_template('partials/folders_list.html', folders=folders))
response.headers['HX-Trigger'] = 'close-modal'
response.headers["HX-Target"] = '#folders-list'
response = make_response('')
response.headers['HX-Trigger'] = 'close-modal, folder-list-invalidated'
response.status_code = 201
return response
@@ -110,8 +106,9 @@ def delete_folder(folder_id):
if not folder:
# Folder not found
folders = Folder.query.filter_by(user_id=current_user.id).all()
return render_template('partials/folders_list.html', folders=folders)
response = make_response('')
response.headers['HX-Trigger'] = 'folder-list-invalidated'
return response
# Delete all associated processed emails first
from app.models import ProcessedEmail
@@ -121,17 +118,18 @@ def delete_folder(folder_id):
db.session.delete(folder)
db.session.commit()
folders = Folder.query.filter_by(user_id=current_user.id).all()
return render_template('partials/folders_list.html', folders=folders)
response = make_response('')
response.headers['HX-Trigger'] = 'folder-list-invalidated'
return response
except Exception as e:
# Print unhandled exceptions to the console as required
logging.exception("Error deleting folder: %s", e)
db.session.rollback()
# Return the folders list unchanged
folders = Folder.query.filter_by(user_id=current_user.id).all()
# Return both sections
return render_template('partials/folders_list.html', folders=folders)
response = make_response('')
response.headers['HX-Trigger'] = 'folder-list-invalidated'
return response
@folders_bp.route('/api/folders/<folder_id>/type', methods=['PUT'])
@login_required
@@ -160,10 +158,9 @@ def update_folder_type(folder_id):
db.session.commit()
# Get updated list of folders for the current user
folders = Folder.query.filter_by(user_id=current_user.id).all()
return render_template('partials/folders_list.html', folders=folders)
response = make_response('')
response.headers['HX-Trigger'] = 'folder-list-invalidated'
return response
except Exception as e:
# Print unhandled exceptions to the console as required
@@ -202,8 +199,9 @@ def update_folder(folder_id):
if not folder:
# Folder not found
folders = Folder.query.filter_by(user_id=current_user.id).all()
return render_template('partials/folders_list.html', folders=folders)
response = make_response('')
response.headers['HX-Trigger'] = 'close-modal, folder-list-invalidated'
return response
# Get form data
name = request.form.get('name')
@@ -229,8 +227,6 @@ def update_folder(folder_id):
# If there are validation errors, return the modal with errors
if errors:
response = make_response(render_template('partials/folder_modal.html', folder=folder, errors=errors, name=name, rule_text=rule_text, priority=priority))
response.headers['HX-Retarget'] = '#folder-modal'
response.headers['HX-Reswap'] = 'outerHTML'
return response
# Update folder
@@ -248,13 +244,8 @@ def update_folder(folder_id):
db.session.commit()
# Get updated list of folders for the current user
folders = Folder.query.filter_by(user_id=current_user.id).all()
# Return both sections
section = render_template('partials/folders_list.html', folders=folders)
response = make_response(section)
response.headers['HX-Trigger'] = 'close-modal'
response = make_response('')
response.headers['HX-Trigger'] = 'close-modal, folder-list-invalidated'
return response
except Exception as e:
@@ -272,11 +263,22 @@ def update_folder(folder_id):
@login_required
def get_folders():
"""Get folders with optional filtering."""
# Check if this is an event-triggered request
is_event_triggered = request.headers.get('HX-Trigger') == 'folder-list-invalidated'
# Get filter parameter from query string
filter_type = request.args.get('filter', 'all')
folder_type = request.args.get('type', None)
show_hidden = request.args.get('show_hidden', 'off').lower() == 'on'
# For event-triggered requests, maintain current filter state
if is_event_triggered:
# If this is an event trigger, we need to preserve the current filter state
# The event listener will have the current state in the DOM
# So we just return the full list with current show_hidden state
folders = Folder.query.filter_by(user_id=current_user.id).all()
return render_template('partials/folders_list.html', folders=folders, show_hidden=show_hidden)
# Get folders for the current authenticated user
if folder_type:
# Filter by folder type

View File

@@ -192,13 +192,13 @@ def sync_imap_folders():
if synced_count > 0:
# Return the folder type selection modal
response = make_response(render_template('partials/folder_type_selection_modal.html', folders=folders_to_process))
response.headers['hx-retarget'] = "#modal-holder"
response.headers['HX-Trigger'] = 'open-modal'
return response
else:
# Just return the updated folders list
folders = Folder.query.filter_by(user_id=current_user.id).all()
response=make_response(render_template('partials/folders_list.html', folders=folders))
response.headers['HX-Trigger'] = 'close-modal'
# Just trigger the folder list update
response = make_response('')
response.headers['HX-Trigger'] = 'close-modal, folder-list-invalidated'
return response
except Exception as e:

View File

@@ -24,8 +24,7 @@
<div class="flex space-x-2">
{% if current_user.imap_config %}
<div data-loading-states>
<button class="btn btn-outline" hx-post="/api/imap/sync" hx-target="#folders-list" hx-swap="outerHTML settle:300ms"
data-loading-disable>
<button class="btn btn-outline" hx-post="/api/imap/sync" hx-target="#modal-holder" data-loading-disable>
<i class="fas fa-sync mr-2" data-loading-class="!hidden"></i>
<span data-loading-class="!hidden">Sync Folders</span>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
@@ -55,59 +54,12 @@
<!-- Welcome Section - Only shown when IMAP is not configured -->
{% if not current_user.imap_config %}
{% include "partials/welcome_section.html" %}
<section id="folders-list" class="mb-12"></section>
{% include "partials/folders_list.html" %}
{% else %}
<!-- Stats Section -->
<div class="mb-8 grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
<div class="text-2xl font-bold text-primary">{{ folders|length }}</div>
<div class="text-sm text-base-content/70">Total Folders</div>
</div>
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
<div class="text-2xl font-bold text-warning">{{ folders|selectattr('folder_type', 'equalto', 'tidy')|list|length }}</div>
<div class="text-sm text-base-content/70">Folders to Tidy</div>
</div>
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
<div class="text-2xl font-bold text-secondary">{{ folders|selectattr('folder_type', 'equalto', 'destination')|list|length }}</div>
<div class="text-sm text-base-content/70">Destination Folders</div>
</div>
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
<div class="text-2xl font-bold text-info">
{{ folders|selectattr('folder_type', 'equalto', 'tidy')|sum(attribute='pending_count') }}
</div>
<div class="text-sm text-base-content/70">Pending Emails</div>
</div>
</div>
<!-- Search and Filter -->
<div class="mb-6 flex justify-between items-center">
<div class="flex items-center space-x-4">
<div class="relative w-64">
<input type="text" placeholder="Search folders..." class="input input-bordered w-full pr-10">
<i class="fas fa-search absolute right-3 top-3 text-base-content/50"></i>
</div>
<div class="flex items-center space-x-2">
<input type="checkbox" id="show-hidden"
name="show_hidden"
class="toggle"
hx-get="/api/folders"
hx-include="this"
hx-target="#folders-list"
hx-swap="outerHTML">
<label for="show-hidden" class="label-text cursor-pointer">Show Hidden</label>
</div>
</div>
<div class="flex space-x-2">
<button class="btn btn-sm btn-outline" hx-get="/api/folders?filter=all" hx-target="#folders-list" hx-swap="outerHTML settle:300ms">All</button>
<button class="btn btn-sm btn-outline" hx-get="/api/folders?filter=high" hx-target="#folders-list" hx-swap="outerHTML settle:300ms">High Priority</button>
<button class="btn btn-sm btn-outline" hx-get="/api/folders?filter=normal" hx-target="#folders-list" hx-swap="outerHTML settle:300ms">Normal</button>
</div>
</div>
{% include 'partials/folders_list.html' %}
{% endif %}
</main>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -16,8 +16,7 @@
</button>
<button class="btn btn-sm btn-outline btn-error fade-me-out delete-button"
hx-delete="/api/folders/{{ folder.id }}"
hx-target="#folders-list"
hx-swap="innerHTML swap:1s"
hx-swap="outerHTML settle:300ms"
hx-confirm="Are you sure you want to delete this folder?"
data-loading-disable
>
@@ -67,8 +66,7 @@
class="toggle toggle-sm toggle-success"
{% if folder.organize_enabled %}checked="checked"{% endif %}
hx-put="/api/folders/{{ folder.id }}/toggle"
hx-target="#folder-{{ folder.id }}"
hx-swap="outerHTML"
hx-swap="outerHTML settle:300ms"
hx-trigger="click"
data-loading-disable
aria-label="Toggle organize enabled">

View File

@@ -1,4 +1,4 @@
<div id="folder-{{ folder.id }}" class="card bg-base-100 shadow-xl border border-base-300 hover:shadow-lg">
<div class="card bg-base-100 shadow-xl border border-base-300 hover:shadow-lg folder-card" data-id="{{ folder.id }}">
<div class="card-body" data-loading-states>
<div class="flex justify-between items-start mb-2">
<h3 class="text-xl font-bold truncate flex-grow">{{ folder.name }}</h3>
@@ -15,7 +15,6 @@
</button>
<button class="btn btn-sm btn-outline btn-error fade-me-out delete-button"
hx-delete="/api/folders/{{ folder.id }}"
hx-target="#folders-list"
hx-swap="outerhtml settle:300ms"
hx-confirm="Are you sure you want to delete this folder?"
data-loading-disable
@@ -50,8 +49,7 @@
<select
class="select select-bordered select-xs"
hx-put="/api/folders/{{ folder.id }}/type"
hx-target="#folders-list"
hx-swap="innerHTML"
hx-swap="outerHTML settle:300ms"
hx-include="this"
name="folder_type"
data-loading-disable >

View File

@@ -10,7 +10,11 @@
</div>
{% endif %}
<form id="folder-form" {% if folder %} hx-put="/api/folders/{{ folder.id }}" {% else %} hx-post="/api/folders" {% endif %} hx-target="#folders-list" hx-swap="innerHTML">
<form id="folder-form"
{% if folder %} hx-put="/api/folders/{{ folder.id }}" {% else %} hx-post="/api/folders" {% endif %}
hx-target="#folder-modal"
hx-swap="outerHTML"
>
{% if folder %}
<input type="hidden" id="folder-id" name="id" value="{{ folder.id }}">
{% endif %}

View File

@@ -42,10 +42,10 @@
</div>
<div class="modal-action">
<button type="button" class="btn btn-outline" hx-get="/api/imap/sync" hx-target="#folders-list" hx-swap="outerHTML">
<button type="button" class="btn btn-outline" hx-get="/api/imap/sync">
Skip and Use Defaults
</button>
<button type="button" class="btn btn-primary" hx-post="/api/imap/sync" hx-target="#folders-list" hx-swap="outerHTML">
<button type="button" class="btn btn-primary" hx-post="/api/imap/sync">
Save and Continue
</button>
</div>

View File

@@ -1,7 +1,58 @@
<section id="folders-list" class="mb-12">
{% include 'partials/folders_to_tidy_section.html' %}
{% include 'partials/destination_folders_section.html' %}
{% if show_hidden %}
{% include 'partials/hidden_folders_section.html' %}
{% endif %}
</section>
<section id="folders-list" class="mb-12"
hx-trigger="folder-list-invalidated from:body"
hx-include="[name=filter], [name=show_hidden]"
hx-get="/api/folders"
hx-swap="outerHTML settle:300ms" >
{% if current_user.imap_config %}
<div class="mb-8 grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
<div class="text-2xl font-bold text-primary">{{ folders|length }}</div>
<div class="text-sm text-base-content/70">Total Folders</div>
</div>
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
<div class="text-2xl font-bold text-warning">{{ folders|selectattr('folder_type', 'equalto', 'tidy')|list|length }}</div>
<div class="text-sm text-base-content/70">Folders to Tidy</div>
</div>
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
<div class="text-2xl font-bold text-secondary">{{ folders|selectattr('folder_type', 'equalto', 'destination')|list|length }}</div>
<div class="text-sm text-base-content/70">Destination Folders</div>
</div>
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
<div class="text-2xl font-bold text-info">
{{ folders|selectattr('folder_type', 'equalto', 'tidy')|sum(attribute='pending_count') }}
</div>
<div class="text-sm text-base-content/70">Pending Emails</div>
</div>
</div>
<!-- Search and Filter -->
<div class="mb-6 flex justify-between items-center">
<div class="flex items-center space-x-4">
<div class="relative w-64">
<input type="text" placeholder="Search folders..." class="input input-bordered w-full pr-10">
<i class="fas fa-search absolute right-3 top-3 text-base-content/50"></i>
</div>
<div class="flex items-center space-x-2">
<input type="checkbox" id="show-hidden"
{% if show_hidden %} checked="checked" {% endif %}
name="show_hidden"
@change="$dispatch('folder-list-invalidated')"
class="toggle">
<label for="show-hidden" class="label-text cursor-pointer">Show Hidden</label>
</div>
</div>
<div class="flex space-x-2" x-data="{filter: ''}" x-effect="filter; $nextTick(() => $dispatch('folder-list-invalidated'))">
<input type="hidden" name="filter" id="filter-choice" x-model="filter">
<button class="btn btn-sm btn-outline" @click="filter=''">All</button>
<button class="btn btn-sm btn-outline" @click="filter='high'">High Priority</button>
<button class="btn btn-sm btn-outline" @click="filter='normal'">Normal</button>
</div>
</div>
{% include 'partials/folders_to_tidy_section.html' %}
{% include 'partials/destination_folders_section.html' %}
{% if show_hidden %}
{% include 'partials/hidden_folders_section.html' %}
{% endif %}
{% endif %}
</section>