Add organize_enabled toggle for folders and improve UI with loading states

This commit is contained in:
Bryce
2025-08-05 10:47:16 -07:00
parent 1eca7f3ff9
commit 31871ed8ec
12 changed files with 119 additions and 57 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
**/*.pyc **/*.pyc
./tmp/**/*

View File

@@ -31,3 +31,18 @@ is preferred, and this is less preferred:
# check if the user is an adult # check if the user is an adult
x = age >= 18 x = age >= 18
``` ```
9. Buttons should be disabled, and a spinner should replace the icon content when loading. Only the button that was clicked should be disabled though. Typically this is done something like this:
```
<div data-loading-states>
<button class="..." hx-get="..." data-loading-disable>
<i class="fas fa-cog mr-2" data-loading-class="!hidden"></i>
<span data-loading-class="!hidden">Click Me!</span>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span> </button>
</div>
```
This will scope the disable to just the button, and disabled the button from being clicked. When in a modal, the whole modal should be disabled.
### context7 documentation library ids:
* HTMX: /bigskysoftware/htmx
* HTMX extensions: /bigskysoftware/htmx-extensions

15
QWEN.md
View File

@@ -31,3 +31,18 @@ is preferred, and this is less preferred:
# check if the user is an adult # check if the user is an adult
x = age >= 18 x = age >= 18
``` ```
9. Buttons should be disabled, and a spinner should replace the icon content when loading. Only the button that was clicked should be disabled though. Typically this is done something like this:
```
<div data-loading-states>
<button class="..." hx-get="..." data-loading-disable>
<i class="fas fa-cog mr-2" data-loading-class="!hidden"></i>
<span data-loading-class="!hidden">Click Me!</span>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span> </button>
</div>
```
This will scope the disable to just the button, and disabled the button from being clicked. When in a modal, the whole modal should be disabled.
### context7 documentation library ids:
* HTMX: /bigskysoftware/htmx
* HTMX extensions: /bigskysoftware/htmx-extensions

View File

@@ -242,8 +242,9 @@ def imap_config_modal():
@login_required @login_required
def test_imap_connection(): def test_imap_connection():
"""Test IMAP connection with provided configuration.""" """Test IMAP connection with provided configuration."""
print("HELLO")
try: try:
import time
time.sleep(5)
# Get form data # Get form data
server = request.form.get('server') server = request.form.get('server')
port = request.form.get('port') port = request.form.get('port')

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Email Organizer - Prototype{% endblock %}</title> <title>{% block title %}Email Organizer - Prototype{% endblock %}</title>
<script src="https://unpkg.com/htmx.org@1.9.12"></script> <script src="https://unpkg.com/htmx.org@1.9.12"></script>
<script src="https://unpkg.com/htmx-ext-loading-states@2.0.0"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" /> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" /> <link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@@ -23,7 +24,9 @@
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body class="min-h-screen flex flex-col" x-data="{}" x-on:open-modal.window="$refs.modal.showModal()" <body class="min-h-screen flex flex-col" x-data="{}" x-on:open-modal.window="$refs.modal.showModal()"
x-on:close-modal.window="$refs.modal.close()"> x-on:close-modal.window="$refs.modal.close()"
hx-ext="loading-states"
data-loading-delay="200">
{% block header %}{% endblock %} {% block header %}{% endblock %}
{% block content %}{% endblock %} {% block content %}{% endblock %}

View File

@@ -21,39 +21,54 @@
<h2 class="text-2xl font-bold">Email Folders</h2> <h2 class="text-2xl font-bold">Email Folders</h2>
<p class="text-base-content/70">Create and manage your email organization rules</p> <p class="text-base-content/70">Create and manage your email organization rules</p>
</div> </div>
<button class="btn btn-primary" hx-get="/api/folders/new" hx-target="#modal-holder" <div data-loading-states>
hx-swap="innerHTML"> <button class="btn btn-primary" hx-get="/api/folders/new" hx-target="#modal-holder" hx-swap="innerHTML"
<i class="fas fa-plus mr-2"></i> data-loading-disable>
Add Folder <i class="fas fa-plus mr-2" data-loading-class="!hidden"></i>
</button> <span data-loading-class="!hidden">Add Folder</span>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button>
</div>
</div> </div>
<!-- Welcome Section --> <!-- Welcome Section -->
<div class="mb-8 p-6 bg-base-100 rounded-box shadow-lg border border-base-300"> <div class="mb-8 p-6 bg-base-100 rounded-box shadow-lg border border-base-300">
<h3 class="text-xl font-bold mb-2">Welcome to Email Organizer!</h3> <h3 class="text-xl font-bold mb-2">Welcome to Email Organizer!</h3>
<p class="text-base-content/80 mb-4">Organize your emails automatically with AI-powered rules. Create folders and set up rules to categorize incoming emails.</p> <p class="text-base-content/80 mb-4">Organize your emails automatically with AI-powered rules. Create folders and set up rules to categorize incoming emails.</p>
<div class="flex space-x-4"> <div class="flex space-x-4" data-loading-states>
<button class="btn btn-primary" hx-get="/api/folders/new" hx-target="#modal-holder" <button class="btn btn-primary" hx-get="/api/folders/new" hx-target="#modal-holder"
hx-swap="innerHTML"> hx-swap="innerHTML"
<i class="fas fa-plus mr-2"></i> data-loading-disable>
Create Your First Folder <i class="fas fa-plus mr-2" data-loading-class="!hidden"></i>
<span data-loading-class="!hidden">Create Your First Folder</span>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button> </button>
{% if current_user.imap_config %} {% if current_user.imap_config %}
<div data-loading-states>
<button class="btn btn-outline" hx-post="/api/imap/sync" hx-target="#folders-list" <button class="btn btn-outline" hx-post="/api/imap/sync" hx-target="#folders-list"
hx-swap="innerHTML"> hx-swap="innerHTML"
<i class="fas fa-sync mr-2"></i> data-loading-disable>
Sync Folders <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>
</button> </button>
<button class="btn btn-outline" hx-get="/api/imap/config" hx-target="#modal-holder" </div>
hx-swap="innerHTML"> <div data-loading-states >
<i class="fas fa-cog"></i> <button class="btn btn-outline" hx-get="/api/imap/config" hx-target="#modal-holder" hx-swap="innerHTML"
data-loading-disable>
<i class="fas fa-cog" data-loading-class="!hidden"></i>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button> </button>
</div>
{% else %} {% else %}
<button class="btn btn-outline" hx-get="/api/imap/config" hx-target="#modal-holder" <div data-loading-states>
hx-swap="innerHTML"> <button class="btn btn-outline" hx-get="/api/imap/config" hx-target="#modal-holder" hx-swap="innerHTML"
<i class="fas fa-cog mr-2"></i> data-loading-disable>
Configure IMAP <i class="fas fa-cog mr-2" data-loading-class="!hidden"></i>
<span data-loading-class="!hidden">Configure IMAP</span>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button> </button>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@@ -1,21 +1,28 @@
<div id="folder-{{ folder.id }}" class="card bg-base-100 shadow-xl border border-base-300 hover:shadow-lg transition-shadow duration-200"> <div id="folder-{{ folder.id }}" class="card bg-base-100 shadow-xl border border-base-300 hover:shadow-lg transition-shadow duration-200">
<div class="card-body">
<div class="card-body" data-loading-states>
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start mb-2">
<h3 class="text-xl font-bold truncate">{{ folder.name }}</h3> <h3 class="text-xl font-bold truncate">{{ folder.name }}</h3>
<div class="flex space-x-2"> <div class="flex space-x-2">
<button class="btn btn-sm btn-outline" <button class="btn btn-sm btn-outline"
hx-get="/api/folders/{{ folder.id }}/edit" hx-get="/api/folders/{{ folder.id }}/edit"
hx-target="#modal-holder" hx-target="#modal-holder"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-trigger="click"> hx-trigger="click"
<i class="fas fa-edit"></i> data-loading-disable
>
<i class="fas fa-edit" data-loading-class="!hidden"></i>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button> </button>
<button class="btn btn-sm btn-outline btn-error" <button class="btn btn-sm btn-outline btn-error"
hx-delete="/api/folders/{{ folder.id }}" hx-delete="/api/folders/{{ folder.id }}"
hx-target="#folders-list" hx-target="#folders-list"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-confirm="Are you sure you want to delete this folder?"> hx-confirm="Are you sure you want to delete this folder?"
<i class="fas fa-trash"></i> data-loading-disable
>
<i class="fas fa-trash" data-loading-class="!hidden"></i>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button> </button>
</div> </div>
</div> </div>
@@ -34,7 +41,7 @@
{% endif %} {% endif %}
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-xs">Organize:</span> <span class="text-xs">Organize:</span>
<input <input
type="checkbox" type="checkbox"
class="toggle toggle-sm toggle-success" class="toggle toggle-sm toggle-success"
{% if folder.organize_enabled %}checked="checked"{% endif %} {% if folder.organize_enabled %}checked="checked"{% endif %}
@@ -42,8 +49,11 @@
hx-target="#folder-{{ folder.id }}" hx-target="#folder-{{ folder.id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-trigger="click" hx-trigger="click"
data-loading-disable
aria-label="Toggle organize enabled"> aria-label="Toggle organize enabled">
</input> </input>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -16,7 +16,7 @@
{% endif %} {% endif %}
<div class="mb-4"> <div class="mb-4">
<label for="folder-name" class="block text-sm font-medium mb-1">Name</label> <label for="folder-name" class="block text-sm font-medium mb-1">Name</label>
<input type="text" id="folder-name" name="name" <input type="text" id="folder-name" name="name"
class="input input-bordered w-full {% if errors and errors.name %}input-error{% endif %}" class="input input-bordered w-full {% if errors and errors.name %}input-error{% endif %}"
placeholder="e.g., Work, Personal, Newsletters" required placeholder="e.g., Work, Personal, Newsletters" required
value="{% if name is defined %}{{ name }}{% elif folder %}{{ folder.name }}{% endif %}"> value="{% if name is defined %}{{ name }}{% elif folder %}{{ folder.name }}{% endif %}">
@@ -26,7 +26,7 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="folder-rule" class="block text-sm font-medium mb-1">Rule (Natural Language)</label> <label for="folder-rule" class="block text-sm font-medium mb-1">Rule (Natural Language)</label>
<textarea id="folder-rule" name="rule_text" <textarea id="folder-rule" name="rule_text"
class="textarea textarea-bordered w-full h-24 {% if errors and errors.rule_text %}textarea-error{% endif %}" class="textarea textarea-bordered w-full h-24 {% if errors and errors.rule_text %}textarea-error{% endif %}"
placeholder="e.g., Move emails from 'newsletter@company.com' to this folder" placeholder="e.g., Move emails from 'newsletter@company.com' to this folder"
required>{% if rule_text is defined %}{{ rule_text }}{% elif folder %}{{ folder.rule_text }}{% endif %}</textarea> required>{% if rule_text is defined %}{{ rule_text }}{% elif folder %}{{ folder.rule_text }}{% endif %}</textarea>
@@ -42,11 +42,12 @@
<option value="-1" {% if (priority is defined and priority == '-1') or (folder and folder.priority==-1) %}selected{% endif %}>Low</option> <option value="-1" {% if (priority is defined and priority == '-1') or (folder and folder.priority==-1) %}selected{% endif %}>Low</option>
</select> </select>
</div> </div>
<div class="modal-action"> <div class="modal-action" data-loading-states>
<button type="button" class="btn btn-outline" <button type="button" class="btn btn-outline"
@click="$dispatch('close-modal')">Cancel</button> @click="$dispatch('close-modal')">Cancel</button>
<button type="submit" class="btn btn-primary" id="submit-btn" :class="{ 'shake': errors }"> <button type="submit" class="btn btn-primary" id="submit-btn" :class="{ 'shake': errors }" >
{% if folder %}Update Folder{% else %}Add Folder{% endif %} <span data-loading-class="!hidden">{% if folder %}Update Folder{% else %}Add Folder{% endif %}</span>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button> </button>
</div> </div>
</form> </form>

View File

@@ -8,14 +8,14 @@
</div> </div>
<h3 class="text-2xl font-bold mb-2">No folders yet</h3> <h3 class="text-2xl font-bold mb-2">No folders yet</h3>
<p class="mb-6 text-base-content/70">Add your first folder to get started organizing your emails.</p> <p class="mb-6 text-base-content/70">Add your first folder to get started organizing your emails.</p>
<button class="btn btn-primary btn-lg" <div data-loading-states>
hx-get="/api/folders/new" <button class="btn btn-primary btn-lg" hx-get="/api/folders/new" hx-target="#modal-holder" hx-swap="innerHTML"
hx-target="#modal-holder"
hx-swap="innerHTML"
hx-trigger="click"> hx-trigger="click">
<i class="fas fa-plus mr-2"></i> <i class="fas fa-plus mr-2" data-loading-class="!hidden"></i>
Create Folder <span data-loading-class="!hidden">Create Folder</span>
</button> <span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button>
</div>
<div class="mt-4 text-sm text-base-content/70"> <div class="mt-4 text-sm text-base-content/70">
<p>Need help setting up your first folder?</p> <p>Need help setting up your first folder?</p>
<a href="#" class="link link-primary">View tutorial</a> <a href="#" class="link link-primary">View tutorial</a>

View File

@@ -1,4 +1,4 @@
<div id="imap-modal" @click.away="$refs.modal.close()" class="modal-box" x-data="{ errors: {{ 'true' if errors else 'false' }} }" x-init="$nextTick(() => { if (errors) { document.querySelector('#submit-btn').classList.add('shake'); } })"> <div id="imap-modal" @click.away="$refs.modal.close()" class="modal-box" x-data="{ errors: {{ 'true' if errors else 'false' }} }" x-init="$nextTick(() => { if (errors) { document.querySelector('#submit-btn').classList.add('shake'); } })" >
<h3 class="font-bold text-lg mb-4">Configure IMAP Connection</h3> <h3 class="font-bold text-lg mb-4">Configure IMAP Connection</h3>
{% if success %} {% if success %}
@@ -18,7 +18,7 @@
<form id="imap-form" hx-post="/api/imap/test" hx-target="#imap-modal" hx-swap="outerHTML"> <form id="imap-form" hx-post="/api/imap/test" hx-target="#imap-modal" hx-swap="outerHTML">
<div class="mb-4"> <div class="mb-4">
<label for="imap-server" class="block text-sm font-medium mb-1">IMAP Server</label> <label for="imap-server" class="block text-sm font-medium mb-1">IMAP Server</label>
<input type="text" id="imap-server" name="server" <input type="text" id="imap-server" name="server"
class="input input-bordered w-full {% if errors and errors.server %}input-error{% endif %}" class="input input-bordered w-full {% if errors and errors.server %}input-error{% endif %}"
placeholder="e.g., imap.gmail.com" required placeholder="e.g., imap.gmail.com" required
value="{% if server is defined %}{{ server }}{% endif %}"> value="{% if server is defined %}{{ server }}{% endif %}">
@@ -29,7 +29,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="imap-port" class="block text-sm font-medium mb-1">Port</label> <label for="imap-port" class="block text-sm font-medium mb-1">Port</label>
<input type="number" id="imap-port" name="port" <input type="number" id="imap-port" name="port"
class="input input-bordered w-full {% if errors and errors.port %}input-error{% endif %}" class="input input-bordered w-full {% if errors and errors.port %}input-error{% endif %}"
placeholder="e.g., 993" required placeholder="e.g., 993" required
value="{% if port is defined %}{{ port }}{% else %}993{% endif %}"> value="{% if port is defined %}{{ port }}{% else %}993{% endif %}">
@@ -40,7 +40,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="imap-username" class="block text-sm font-medium mb-1">Username</label> <label for="imap-username" class="block text-sm font-medium mb-1">Username</label>
<input type="text" id="imap-username" name="username" <input type="text" id="imap-username" name="username"
class="input input-bordered w-full {% if errors and errors.username %}input-error{% endif %}" class="input input-bordered w-full {% if errors and errors.username %}input-error{% endif %}"
placeholder="e.g., your-email@gmail.com" required placeholder="e.g., your-email@gmail.com" required
value="{% if username is defined %}{{ username }}{% endif %}"> value="{% if username is defined %}{{ username }}{% endif %}">
@@ -51,7 +51,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="imap-password" class="block text-sm font-medium mb-1">Password</label> <label for="imap-password" class="block text-sm font-medium mb-1">Password</label>
<input type="password" id="imap-password" name="password" <input type="password" id="imap-password" name="password"
class="input input-bordered w-full {% if errors and errors.password %}input-error{% endif %}" class="input input-bordered w-full {% if errors and errors.password %}input-error{% endif %}"
placeholder="App password or account password" required> placeholder="App password or account password" required>
{% if errors and errors.password %} {% if errors and errors.password %}
@@ -67,20 +67,21 @@
<p class="text-xs text-base-content/70 mt-1">Most IMAP servers use SSL on port 993</p> <p class="text-xs text-base-content/70 mt-1">Most IMAP servers use SSL on port 993</p>
</div> </div>
<div class="modal-action"> <div class="modal-action" data-loading-states>
<button type="button" class="btn btn-outline" <button type="button" class="btn btn-outline"
@click="$dispatch('close-modal')">Cancel</button> @click="$dispatch('close-modal')">Cancel</button>
<button type="submit" class="btn btn-primary" id="submit-btn" :class="{ 'shake': errors }"> <button type="submit" class="btn btn-primary" id="submit-btn" :class="{ 'shake': errors }" data-loading-disable >
Test Connection <span data-loading-class="!hidden">Test Connection</span>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button> </button>
</div> </div>
</form> </form>
{% if success %} {% if success %}
<div class="mt-4 pt-4 border-t border-base-300"> <div class="mt-4 pt-4 border-t border-base-300" data-loading-states>
<button class="btn btn-success w-full" hx-post="/api/imap/sync" hx-target="#folders-list" hx-swap="innerHTML"> <button class="btn btn-success w-full" hx-post="/api/imap/sync" hx-target="#folders-list" hx-swap="innerHTML" data-loading-disable>
<i class="fas fa-sync mr-2"></i> <span data-loading-class="!hidden"><i class="fas fa-sync mr-2"></i>Sync Folders</span>
Sync Folders <span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
</button> </button>
</div> </div>
{% endif %} {% endif %}

View File

@@ -1,4 +1,4 @@
<!-- Modal Holder --> <!-- Modal Holder -->
<dialog id="modal-holder" x-ref="modal" class="modal"> <dialog id="modal-holder" x-ref="modal" class="modal" data-loading-states>
<!-- Modals will be loaded here via HTMX --> <!-- Modals will be loaded here via HTMX -->
</dialog> </dialog>