progress
This commit is contained in:
@@ -16,7 +16,8 @@ def index():
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
return render_template('index.html',
|
||||
folders=folders)
|
||||
folders=folders,
|
||||
show_hidden=False)
|
||||
|
||||
@main.route('/api/folders/new', methods=['GET'])
|
||||
@login_required
|
||||
@@ -62,12 +63,17 @@ def add_folder():
|
||||
# Default to 'destination' type for manually created folders
|
||||
folder_type = 'tidy' if name.strip().lower() == 'inbox' else 'destination'
|
||||
|
||||
# If folder_type is 'ignore', reset emails_count to 0
|
||||
if folder_type == 'ignore':
|
||||
emails_count = 0
|
||||
|
||||
folder = Folder(
|
||||
user_id=current_user.id,
|
||||
name=name.strip(),
|
||||
rule_text=rule_text.strip(),
|
||||
priority=int(priority) if priority else 0,
|
||||
folder_type=folder_type
|
||||
folder_type=folder_type,
|
||||
emails_count=0 if folder_type == 'ignore' else None
|
||||
)
|
||||
|
||||
db.session.add(folder)
|
||||
@@ -78,7 +84,7 @@ def add_folder():
|
||||
|
||||
response = make_response(render_template('partials/folders_list.html', folders=folders))
|
||||
response.headers['HX-Trigger'] = 'close-modal'
|
||||
response.headers["HX-Target"] = 'folder-list'
|
||||
response.headers["HX-Target"] = '#folders-list'
|
||||
response.status_code = 201
|
||||
return response
|
||||
|
||||
@@ -126,9 +132,9 @@ def delete_folder(folder_id):
|
||||
# Return both sections
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
@main.route('/api/folders/<folder_id>/toggle', methods=['PUT'])
|
||||
@main.route('/api/folders/<folder_id>/type', methods=['PUT'])
|
||||
@login_required
|
||||
def toggle_folder_organize(folder_id):
|
||||
def update_folder_type(folder_id):
|
||||
try:
|
||||
# Find the folder by ID and ensure it belongs to the current user
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first()
|
||||
@@ -136,21 +142,30 @@ def toggle_folder_organize(folder_id):
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Toggle the organize_enabled flag
|
||||
folder.organize_enabled = not folder.organize_enabled
|
||||
# Get the new folder type from form data
|
||||
new_folder_type = request.form.get('folder_type')
|
||||
|
||||
# Validate the folder type
|
||||
if new_folder_type not in ['tidy', 'destination', 'ignore']:
|
||||
return jsonify({'error': 'Invalid folder type'}), 400
|
||||
|
||||
# If changing to 'ignore', reset the emails_count to 0
|
||||
if new_folder_type == 'ignore' and folder.folder_type != 'ignore':
|
||||
folder.emails_count = 0
|
||||
|
||||
# Update the folder type
|
||||
folder.folder_type = new_folder_type
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Return just the updated folder card HTML for this specific folder
|
||||
# Use the appropriate template based on folder type
|
||||
if folder.folder_type == 'tidy':
|
||||
return render_template('partials/folder_card_tidy.html', folder=folder)
|
||||
else:
|
||||
return render_template('partials/folder_card_destination.html', folder=folder)
|
||||
# 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)
|
||||
|
||||
except Exception as e:
|
||||
# Print unhandled exceptions to the console as required
|
||||
logging.exception("Error toggling folder organize flag: %s", e)
|
||||
logging.exception("Error updating folder type: %s", e)
|
||||
db.session.rollback()
|
||||
return jsonify({'error': 'An unexpected error occurred'}), 500
|
||||
|
||||
@@ -219,11 +234,13 @@ def update_folder(folder_id):
|
||||
folder.rule_text = rule_text.strip()
|
||||
folder.priority = int(priority) if priority else 0
|
||||
|
||||
# Update folder type if the name changed to/from 'inbox'
|
||||
if name.strip().lower() == 'inbox' and folder.folder_type != 'tidy':
|
||||
folder.folder_type = 'tidy'
|
||||
elif name.strip().lower() != 'inbox' and folder.folder_type != 'destination':
|
||||
folder.folder_type = 'destination'
|
||||
# Check if folder type is being changed to 'ignore'
|
||||
old_folder_type = folder.folder_type
|
||||
folder.folder_type = 'tidy' if name.strip().lower() == 'inbox' else 'destination'
|
||||
|
||||
# If changing to 'ignore', reset emails_count to 0
|
||||
if folder.folder_type == 'ignore' and old_folder_type != 'ignore':
|
||||
folder.emails_count = 0
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@@ -373,6 +390,9 @@ def sync_imap_folders():
|
||||
synced_count = 0
|
||||
processed_emails_service = ProcessedEmailsService(current_user)
|
||||
|
||||
# Create a list of folders to process
|
||||
folders_to_process = []
|
||||
|
||||
for imap_folder in unique_folders:
|
||||
folder_name = imap_folder['name'].strip()
|
||||
|
||||
@@ -400,6 +420,7 @@ def sync_imap_folders():
|
||||
)
|
||||
db.session.add(new_folder)
|
||||
synced_count += 1
|
||||
folders_to_process.append(new_folder)
|
||||
else:
|
||||
# Update existing folder with email counts and recent emails
|
||||
# Get all email UIDs in this folder
|
||||
@@ -418,12 +439,21 @@ def sync_imap_folders():
|
||||
recent_emails = imap_service.get_recent_emails(folder_name, 3)
|
||||
existing_folder.recent_emails = recent_emails
|
||||
|
||||
folders_to_process.append(existing_folder)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Get updated list of folders
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
# Check if we should show the folder type selection modal
|
||||
# Only show the modal if there are new folders to configure
|
||||
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-Trigger'] = 'open-modal'
|
||||
return response
|
||||
else:
|
||||
# Just return the updated folders list
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("Error syncing IMAP folders: %s", e)
|
||||
@@ -438,6 +468,7 @@ def get_folders():
|
||||
# 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'
|
||||
|
||||
# Get folders for the current authenticated user
|
||||
if folder_type:
|
||||
@@ -454,15 +485,26 @@ def get_folders():
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
if folder_type == 'tidy':
|
||||
response = make_response(render_template('partials/folders_to_tidy_section.html', folders=folders))
|
||||
response["HX-Retarget"] = "#folders-to-tidy"
|
||||
response = make_response(render_template('partials/folders_to_tidy_section.html', folders=folders, show_hidden=show_hidden))
|
||||
response.headers["HX-Retarget"] = "#folders-to-tidy"
|
||||
return response
|
||||
elif folder_type == 'destination':
|
||||
response = make_response(render_template('partials/destination_folders_section.html', folders=folders))
|
||||
response["HX-Retarget"] = "#destination-folders"
|
||||
response = make_response(render_template('partials/destination_folders_section.html', folders=folders, show_hidden=show_hidden))
|
||||
response.headers["HX-Retarget"] = "#destination-folders"
|
||||
return response
|
||||
elif folder_type == 'ignore':
|
||||
response = make_response(render_template('partials/hidden_folders_section.html', folders=folders, show_hidden=show_hidden))
|
||||
response.headers["HX-Retarget"] = "#hidden-folders"
|
||||
|
||||
# Show or hide the hidden folders section based on the show_hidden parameter
|
||||
if show_hidden:
|
||||
response.headers['HX-Trigger'] = 'show-hidden'
|
||||
else:
|
||||
response.headers['HX-Trigger'] = 'hide-hidden'
|
||||
|
||||
return response
|
||||
else:
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
return render_template('partials/folders_list.html', folders=folders, show_hidden=show_hidden)
|
||||
|
||||
|
||||
# Processed Emails API Endpoints
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
|
||||
<!-- Welcome Section - Only shown when IMAP is not configured -->
|
||||
{% if not current_user.imap_config %}
|
||||
{% include "partials/welcome_section.html" %}
|
||||
{% include "partials/welcome_section.html" %}
|
||||
<section id="folders-list" class="mb-12"></section>
|
||||
{% else %}
|
||||
|
||||
<!-- Stats Section -->
|
||||
@@ -81,9 +82,21 @@
|
||||
|
||||
<!-- Search and Filter -->
|
||||
<div class="mb-6 flex justify-between items-center">
|
||||
<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 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>
|
||||
|
||||
@@ -44,6 +44,22 @@
|
||||
<p class="text-base-content/80">{{ folder.rule_text }}</p>
|
||||
</div>
|
||||
|
||||
{% block additional_content %}{% endblock %}
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs">Type:</span>
|
||||
<select
|
||||
class="select select-bordered select-xs"
|
||||
hx-put="/api/folders/{{ folder.id }}/type"
|
||||
hx-target="#folders-list"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="this"
|
||||
name="folder_type"
|
||||
data-loading-disable >
|
||||
<option value="tidy" {% if folder.folder_type == 'tidy' %}selected{% endif %}>Tidy</option>
|
||||
<option value="destination" {% if folder.folder_type == 'destination' %}selected{% endif %}>Destination</option>
|
||||
<option value="ignore" {% if folder.folder_type == 'ignore' %}selected{% endif %}>Ignore</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,41 +1,5 @@
|
||||
<div id="folder-{{ folder.id }}" class="card bg-base-100 shadow-xl border border-base-300 hover:shadow-lg">
|
||||
{% extends "partials/folder_card_base.html" %}
|
||||
|
||||
<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>
|
||||
<div class="flex space-x-2">
|
||||
<button class="btn btn-sm btn-outline"
|
||||
hx-get="/api/folders/{{ folder.id }}/edit"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click"
|
||||
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 class="btn btn-sm btn-outline btn-error fade-me-out"
|
||||
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
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email count badge for destination folders -->
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex space-x-1">
|
||||
<span class="badge badge-primary">{{ folder.emails_count }} emails</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-box p-4 mb-4">
|
||||
<p class="text-base-content/80">{{ folder.rule_text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block badges %}
|
||||
<span class="badge badge-primary">{{ folder.emails_count }} emails</span>
|
||||
{% endblock %}
|
||||
6
app/templates/partials/folder_card_ignore.html
Normal file
6
app/templates/partials/folder_card_ignore.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "partials/folder_card_base.html" %}
|
||||
|
||||
{% block badges %}
|
||||
<span class="badge badge-secondary">Ignored</span>
|
||||
<span class="badge badge-outline">{{ folder.emails_count }} emails</span>
|
||||
{% endblock %}
|
||||
@@ -1,82 +1,20 @@
|
||||
<div id="folder-{{ folder.id }}" class="card bg-base-100 shadow-xl border border-base-300 hover:shadow-lg">
|
||||
{% extends "partials/folder_card_base.html" %}
|
||||
|
||||
<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>
|
||||
<div class="flex space-x-2">
|
||||
<button class="btn btn-sm btn-outline"
|
||||
hx-get="/api/folders/{{ folder.id }}/edit"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click"
|
||||
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 class="btn btn-sm btn-outline btn-error fade-me-out"
|
||||
hx-delete="/api/folders/{{ folder.id }}"
|
||||
hx-target="#folders-to-tidy-list"
|
||||
hx-swap="innerHTML swap:1s"
|
||||
hx-confirm="Are you sure you want to delete this folder?"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email count badges placed below title but in a separate row -->
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex space-x-1">
|
||||
<span class="badge badge-outline">{{ folder.total_count }} total</span>
|
||||
{% if folder.pending_count > 0 %}
|
||||
<button
|
||||
class="badge badge-warning cursor-pointer"
|
||||
hx-get="/api/folders/{{ folder.id }}/pending-emails"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click"
|
||||
title="{{ folder.pending_count }} pending emails"
|
||||
>
|
||||
{{ folder.pending_count }} pending
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary cursor-pointer" x-tooltip.raw.html="{% if folder.recent_emails %}<table class='text-xs'><tr><th class='text-left pr-2'>Subject</th><th class='text-left'>Date</th></tr>{% for email in folder.recent_emails %}<tr><td class='text-left pr-2 truncate max-w-[150px]'>{{ email.subject }}</td><td class='text-left'>{{ email.date[:10] if email.date else 'N/A' }}</td></tr>{% endfor %}</table>{% else %}No recent emails{% endif %}">{{ folder.pending_count }} pending</span>
|
||||
{% endif %}
|
||||
<span class="badge badge-success">{{ folder.total_count - folder.pending_count }} processed</span>
|
||||
</div>
|
||||
{% if folder.priority == 1 %}
|
||||
<span class="badge badge-error">High Priority</span>
|
||||
{% elif folder.priority == -1 %}
|
||||
<span class="badge badge-info">Low Priority</span>
|
||||
{% else %}
|
||||
<span class="badge badge-primary">Normal Priority</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-box p-4 mb-4">
|
||||
<p class="text-base-content/80">{{ folder.rule_text }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs">Organize:</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
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-trigger="click"
|
||||
data-loading-disable
|
||||
aria-label="Toggle organize enabled">
|
||||
</input>
|
||||
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block badges %}
|
||||
<span class="badge badge-outline">{{ folder.total_count }} total</span>
|
||||
{% if folder.pending_count > 0 %}
|
||||
<button
|
||||
class="badge badge-warning cursor-pointer"
|
||||
hx-get="/api/folders/{{ folder.id }}/pending-emails"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click"
|
||||
title="{{ folder.pending_count }} pending emails"
|
||||
>
|
||||
{{ folder.pending_count }} pending
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary cursor-pointer" x-tooltip.raw.html="{% if folder.recent_emails %}<table class='text-xs'><tr><th class='text-left pr-2'>Subject</th><th class='text-left'>Date</th></tr>{% for email in folder.recent_emails %}<tr><td class='text-left pr-2 truncate max-w-[150px]'>{{ email.subject }}</td><td class='text-left'>{{ email.date[:10] if email.date else 'N/A' }}</td></tr>{% endfor %}</table>{% else %}No recent emails{% endif %}">{{ folder.pending_count }} pending</span>
|
||||
{% endif %}
|
||||
<span class="badge badge-success">{{ folder.total_count - folder.pending_count }} processed</span>
|
||||
{% endblock %}
|
||||
|
||||
52
app/templates/partials/folder_type_selection_modal.html
Normal file
52
app/templates/partials/folder_type_selection_modal.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<div id="folder-type-modal" class="modal-box" @click.away="$refs.modal.close()">
|
||||
<h3 class="font-bold text-lg mb-4">Configure Folder Types</h3>
|
||||
<p class="mb-4">Select the processing type for each folder. Inbox will default to Tidy, while Archive/Spam/Drafts will default to Ignore.</p>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Folder Name</th>
|
||||
<th>Processing Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for folder in folders %}
|
||||
<tr>
|
||||
<td>{{ folder.name }}</td>
|
||||
<td>
|
||||
<select class="select select-bordered select-sm"
|
||||
name="folder_type_{{ folder.id }}"
|
||||
hx-put="/api/folders/{{ folder.id }}/type"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="outerHTML">
|
||||
<option value="tidy" {% if folder.folder_type == 'tidy' %}selected{% endif %}>Tidy</option>
|
||||
<option value="destination" {% if folder.folder_type == 'destination' %}selected{% endif %}>Destination</option>
|
||||
<option value="ignore" {% if folder.folder_type == 'ignore' %}selected{% endif %}>Ignore</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
{% if folder.folder_type == 'tidy' %}
|
||||
Processed by AI to organize emails
|
||||
{% elif folder.folder_type == 'destination' %}
|
||||
Target for organized emails
|
||||
{% else %}
|
||||
Ignored during processing
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn btn-outline" hx-get="/api/imap/sync" hx-target="#folders-list" hx-swap="outerHTML">
|
||||
Skip and Use Defaults
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" hx-post="/api/imap/sync" hx-target="#folders-list" hx-swap="outerHTML">
|
||||
Save and Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,7 @@
|
||||
<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>
|
||||
27
app/templates/partials/hidden_folders_section.html
Normal file
27
app/templates/partials/hidden_folders_section.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<div class="hidden-folders-section mb-8" id="hidden-folders">
|
||||
<div class="section-header bg-base-200 p-4 rounded-t-lg border-b border-base-300">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-base-content">
|
||||
<i class="fas fa-eye-slash mr-3"></i>
|
||||
Hidden Folders
|
||||
</h2>
|
||||
<p class="text-base-content/70 mt-1">Folders that are ignored during email processing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-6">
|
||||
{% for folder in folders|selectattr('folder_type', 'equalto', 'ignore') %}
|
||||
{% include 'partials/folder_card_ignore.html' %}
|
||||
{% else %}
|
||||
<div class="col-span-full text-center py-12 bg-base-100 rounded-box shadow-lg border border-dashed border-base-300">
|
||||
<div class="text-5xl mb-4 text-secondary">
|
||||
<i class="fas fa-folder-slash"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-2">No hidden folders</h3>
|
||||
<p class="mb-6 text-base-content/70">Folders that are set to ignore will appear here.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,8 +80,8 @@
|
||||
|
||||
{% if success %}
|
||||
<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="outerHTML settle:300ms" data-loading-disable>
|
||||
<span data-loading-class="!hidden"><i class="fas fa-sync mr-2"></i>Sync Folders</span>
|
||||
<button class="btn btn-success w-full" hx-post="/api/imap/sync" hx-target="#modal-holder" hx-swap="innerHTML" data-loading-disable>
|
||||
<span data-loading-class="!hidden"><i class="fas fa-sync mr-2"></i>Configure Folder Types</span>
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -229,7 +229,7 @@ The `recent_emails` field stores an array of JSON objects:
|
||||
|
||||
## Folder Types
|
||||
|
||||
The system supports two distinct types of folders, each with different purposes and behaviors:
|
||||
The system supports three distinct types of folders, each with different purposes and behaviors:
|
||||
|
||||
### Tidy Folders
|
||||
|
||||
@@ -261,14 +261,35 @@ Folders with `folder_type = 'destination'` are target folders where emails are m
|
||||
- Simpler interface without pending/processed indicators
|
||||
- Focus on folder management and viewing contents
|
||||
|
||||
### Ignore Folders
|
||||
|
||||
Folders with `folder_type = 'ignore'` are folders that are stored in the database but are neither scanned to be tidied nor used as destination folders.
|
||||
|
||||
**Characteristics:**
|
||||
- Hidden by default in the user interface
|
||||
- Not processed by AI for organization
|
||||
- No organization rules specified
|
||||
- Known emails count is reset to 0 when changed to this type
|
||||
- Example: Archive, Spam, Drafts folders
|
||||
|
||||
**UI Representation:**
|
||||
- Hidden by default unless "Show Hidden" checkbox is checked
|
||||
- When visible, shows minimal information
|
||||
- No action buttons for organization or processing
|
||||
|
||||
### Folder Type Determination
|
||||
|
||||
Folder types are determined as follows:
|
||||
- During IMAP synchronization:
|
||||
- Folders named "inbox" (case-insensitive) are automatically set as 'tidy'
|
||||
- All other folders are set as 'destination'
|
||||
- First step: Connection testing
|
||||
- Second step: Folder type selection modal with table
|
||||
- Default folder types:
|
||||
- Inbox: Tidy
|
||||
- Archive/Spam/Drafts: Ignore
|
||||
- All others: Destination
|
||||
- Manually created folders default to 'destination'
|
||||
- Folder type can be changed through administrative functions
|
||||
- Folder type can be changed through the user interface
|
||||
- When changing to 'ignore', emails_count is reset to 0
|
||||
|
||||
## Future Data Model Considerations
|
||||
|
||||
|
||||
@@ -2,227 +2,53 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the UI design changes needed to support the new folder types feature: "Folders to Tidy" and "Destination Folders". The UI will be reorganized to clearly separate these two types of folders into distinct sections.
|
||||
This document provides a high-level overview of the folder types structure in the Email Organizer application. The UI is organized into three main folder types: Tidy, Destination, and Ignore, with appropriate components for each type.
|
||||
|
||||
## UI Structure
|
||||
|
||||
### Main Page Layout
|
||||
The main page is divided into sections based on folder types:
|
||||
|
||||
The main page will be divided into two distinct sections:
|
||||
|
||||
1. **Folders to Tidy Section**
|
||||
- Contains folders with `folder_type = 'tidy'`
|
||||
- Typically includes the inbox and other source folders
|
||||
- Shows pending and processed email counts
|
||||
- Has actions for viewing and processing emails
|
||||
|
||||
2. **Destination Folders Section**
|
||||
- Contains folders with `folder_type = 'destination'`
|
||||
- Contains target folders for email organization
|
||||
- Shows count of emails moved to each folder
|
||||
- Focuses on folder management and viewing contents
|
||||
|
||||
### Visual Design
|
||||
|
||||
#### Section Headers
|
||||
Each section will have a clear header with:
|
||||
- Section title
|
||||
- Brief description of the section's purpose
|
||||
- Action button (e.g., "Add Folder" for destination folders)
|
||||
|
||||
#### Section Styling
|
||||
- Different background colors or borders to visually distinguish sections
|
||||
- Consistent spacing and layout within each section
|
||||
- Responsive grid layout that adapts to screen size
|
||||
1. **Folders to Tidy Section**: Contains folders with `folder_type = 'tidy'`
|
||||
2. **Destination Folders Section**: Contains folders with `folder_type = 'destination'`
|
||||
3. **Hidden Folders Section**: Contains folders with `folder_type = 'ignore'` (visible only when "Show Hidden" is checked)
|
||||
|
||||
## Folder Card Components
|
||||
|
||||
### Tidy Folder Card
|
||||
### Base Template
|
||||
All folder cards extend from `folder_card_base.html` which provides:
|
||||
- Common card structure with edit/delete buttons
|
||||
- Priority badge display
|
||||
- Rule text section
|
||||
- Block for additional content specific to folder type
|
||||
|
||||
For folders with `folder_type = 'tidy'`, the card will display:
|
||||
|
||||
#### Header
|
||||
- Folder name (prominent)
|
||||
- Edit and delete buttons
|
||||
|
||||
#### Count Badges
|
||||
- Total emails count
|
||||
- Pending emails count (with warning styling if > 0)
|
||||
- Processed emails count (calculated as total - pending)
|
||||
|
||||
#### Actions
|
||||
- "View Pending" button (if pending count > 0)
|
||||
### Tidy Folder Card (`folder_card_tidy.html`)
|
||||
Extends base template with:
|
||||
- Total, pending, and processed email count badges
|
||||
- Organize toggle switch
|
||||
- Priority badge
|
||||
- View pending emails button
|
||||
|
||||
#### Content
|
||||
- Rule text description
|
||||
- Recent email previews (if available)
|
||||
### Destination Folder Card (`folder_card_destination.html`)
|
||||
Extends base template with:
|
||||
- Email count badge
|
||||
- Simplified UI without processing elements
|
||||
|
||||
### Destination Folder Card
|
||||
### Ignore Folder Card (to be created)
|
||||
Will extend base template with:
|
||||
- "Ignored" status badge
|
||||
- Minimal information display
|
||||
- No processing elements
|
||||
|
||||
For folders with `folder_type = 'destination'`, the card will display:
|
||||
## IMAP Synchronization
|
||||
|
||||
#### Header
|
||||
- Folder name (prominent)
|
||||
- Edit and delete buttons
|
||||
### Two-Step Process
|
||||
1. **Connection Testing**: Test IMAP connection settings
|
||||
2. **Folder Type Selection**: Select processing type for each folder
|
||||
|
||||
#### Count Badge
|
||||
- Emails count (number of emails moved to this folder)
|
||||
### Default Folder Types
|
||||
- Inbox: Tidy
|
||||
- Archive/Spam/Drafts: Ignore
|
||||
- All others: Destination
|
||||
|
||||
#### Content
|
||||
- Rule text description (if applicable)
|
||||
## Show Hidden Folders
|
||||
|
||||
#### Actions
|
||||
- Basic folder management options
|
||||
- No organize toggle (as it's not applicable)
|
||||
|
||||
## UI Components
|
||||
|
||||
### Section Templates
|
||||
|
||||
#### Folders to Tidy Section
|
||||
```html
|
||||
<div class="folders-to-tidy-section">
|
||||
<div class="section-header">
|
||||
<h2>Folders to Tidy</h2>
|
||||
<p>Folders containing emails that need to be processed</p>
|
||||
<button class="btn btn-primary" hx-get="/api/folders/new">
|
||||
<i class="fas fa-plus"></i> Add Tidy Folder
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for folder in tidy_folders %}
|
||||
{% include 'partials/folder_card_tidy.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Destination Folders Section
|
||||
```html
|
||||
<div class="destination-folders-section">
|
||||
<div class="section-header">
|
||||
<h2>Destination Folders</h2>
|
||||
<p>Folders where emails are organized and stored</p>
|
||||
<button class="btn btn-primary" hx-get="/api/folders/new">
|
||||
<i class="fas fa-plus"></i> Add Destination Folder
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for folder in destination_folders %}
|
||||
{% include 'partials/folder_card_destination.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Folder Card Templates
|
||||
|
||||
#### Tidy Folder Card
|
||||
```html
|
||||
<div class="card bg-base-100 shadow-xl border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="text-xl font-bold truncate">{{ folder.name }}</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button class="btn btn-sm btn-outline" hx-get="/api/folders/{{ folder.id }}/edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-error" hx-delete="/api/folders/{{ folder.id }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2 mb-2">
|
||||
<span class="badge badge-outline">{{ folder.total_count }} total</span>
|
||||
{% if folder.pending_count > 0 %}
|
||||
<button class="badge badge-warning" hx-get="/api/folders/{{ folder.id }}/pending-emails">
|
||||
{{ folder.pending_count }} pending
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">{{ folder.pending_count }} pending</span>
|
||||
{% endif %}
|
||||
<span class="badge badge-success">{{ folder.total_count - folder.pending_count }} processed</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-box p-3 mb-3">
|
||||
<p class="text-sm">{{ folder.rule_text }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs">Organize:</span>
|
||||
<input type="checkbox" class="toggle toggle-sm"
|
||||
{% if folder.organize_enabled %}checked{% endif %}
|
||||
hx-put="/api/folders/{{ folder.id }}/toggle">
|
||||
</div>
|
||||
{% if folder.priority == 1 %}
|
||||
<span class="badge badge-error text-xs">High Priority</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Destination Folder Card
|
||||
```html
|
||||
<div class="card bg-base-100 shadow-xl border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="text-xl font-bold truncate">{{ folder.name }}</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button class="btn btn-sm btn-outline" hx-get="/api/folders/{{ folder.id }}/edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-error" hx-delete="/api/folders/{{ folder.id }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2 mb-2">
|
||||
<span class="badge badge-primary">{{ folder.emails_count }} emails</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-box p-3 mb-3">
|
||||
<p class="text-sm">{{ folder.rule_text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Styling Guidelines
|
||||
|
||||
### Color Scheme
|
||||
- **Tidy Folders**: Use warning colors (yellow/orange) for pending emails
|
||||
- **Destination Folders**: Use primary colors (blue) for email counts
|
||||
- Consistent use of DaisyUI badge classes for different states
|
||||
|
||||
### Responsive Design
|
||||
- Grid layout adjusts from 1 column on mobile to 3 columns on desktop
|
||||
- Cards should have consistent width across all screen sizes
|
||||
- Section headers should remain readable on mobile
|
||||
|
||||
### Interactive Elements
|
||||
- Buttons should have hover states
|
||||
- Toggle switches should be clearly labeled
|
||||
- Pending email count should be clickable to open dialog
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Template Organization
|
||||
- Create separate partial templates for each folder type
|
||||
- Main index template will include both sections
|
||||
- Use HTMX for dynamic updates without full page reloads
|
||||
|
||||
### Data Loading
|
||||
- Fetch folders filtered by type from the backend
|
||||
- Pass filtered lists to appropriate section templates
|
||||
- Maintain existing functionality for folder operations
|
||||
|
||||
### Accessibility
|
||||
- Ensure all interactive elements have proper ARIA labels
|
||||
- Use semantic HTML5 elements for section structure
|
||||
- Maintain keyboard navigation support
|
||||
A checkbox allows users to show/hide folders with `folder_type = 'ignore'`. When checked, these folders appear in a dedicated section with appropriate styling to indicate they're ignored.
|
||||
143
docs/plans/folder-types-ignore-implementation.md
Normal file
143
docs/plans/folder-types-ignore-implementation.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Folder Types Implementation Plan: Adding Ignore Type
|
||||
|
||||
## Overview
|
||||
This plan outlines the implementation of a third folder type called "Ignore" which will be stored in the database but neither scanned for tidying nor used as destination folders. When a folder is set to ignore, it will be hidden by default in the user interface.
|
||||
|
||||
## System Architecture Changes
|
||||
|
||||
### Current Folder Types
|
||||
1. **Tidy**: Folders scanned and processed by AI, sorting them into Destination folders
|
||||
2. **Destination**: Folders that are targets for email organization based on rules
|
||||
|
||||
### New Folder Types
|
||||
1. **Tidy**: Folders scanned and processed by AI, sorting them into Destination folders
|
||||
2. **Destination**: Folders that are targets for email organization based on rules
|
||||
3. **Ignore**: Folders stored in database but not processed by AI and hidden by default
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Update Design Documentation
|
||||
- Update [`docs/design/data-model.md`](docs/design/data-model.md) to include the new "ignore" folder type
|
||||
- Update [`docs/design/folder-types-ui.md`](docs/design/folder-types-ui.md) to include UI changes for the new type
|
||||
- Add documentation for folder type behavior and business rules
|
||||
|
||||
### 2. Update Database Model
|
||||
- The [`folder_type`](app/models.py:43) field in the Folder model already supports string values and can accommodate the new "ignore" type
|
||||
- No database migration is needed as the field is already a string with a default value
|
||||
- Update any business logic in the application to handle the new folder type
|
||||
|
||||
### 3. Update IMAP Synchronization Flow
|
||||
- Modify the IMAP sync process to include a second step with a folder type selection modal
|
||||
- The modal should display a table of folders with:
|
||||
- Folder name column
|
||||
- Folder type selection column (Tidy, Destination, Ignore)
|
||||
- Default folder types:
|
||||
- Inbox: Tidy
|
||||
- Archive/Spam/Drafts: Ignore
|
||||
- Create a new template for the folder selection modal
|
||||
|
||||
### 4. Update User Interface
|
||||
- Add a checkbox (unchecked by default) for "show hidden" folders
|
||||
- When checked, it should reveal folders with folder_type="ignore"
|
||||
- Modify folder card components to handle the new folder type:
|
||||
- Hide ignore folders by default
|
||||
- Show appropriate UI elements based on folder type
|
||||
- Replace the "organize this folder" toggle with a folder type selector for tidy and destination folders
|
||||
|
||||
### 5. Update Folder Management
|
||||
- Modify the folder creation and editing forms to include folder type selection
|
||||
- Update the folder card templates to handle the new type
|
||||
- Add logic to reset known emails count when changing to ignore type
|
||||
- Update the delete functionality to handle all folder types
|
||||
|
||||
### 6. Update API Endpoints
|
||||
- Modify the folder toggle endpoint to handle folder type changes instead of just organize_enabled
|
||||
- Add new endpoint for updating folder type specifically
|
||||
- Update existing endpoints to handle the new folder type in their logic
|
||||
|
||||
### 7. Update Tests
|
||||
- Add tests for the new folder type functionality
|
||||
- Test that ignore folders are hidden by default
|
||||
- Test that they can be shown when the "show hidden" checkbox is checked
|
||||
- Test folder type changes through the API
|
||||
- Test the IMAP sync flow with the new folder selection modal
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Folder Type Business Logic
|
||||
```
|
||||
Tidy Folders:
|
||||
- Scanned and processed by AI
|
||||
- Have pending/processed email counts
|
||||
- Can have organization rules enabled/disabled
|
||||
- Example: Inbox
|
||||
|
||||
Destination Folders:
|
||||
- Not processed by AI
|
||||
- Display count of emails moved to this folder
|
||||
- Focus on folder management and viewing contents
|
||||
- Example: "Projects", "Finance", "Personal"
|
||||
|
||||
Ignore Folders:
|
||||
- Stored in database but not processed by AI
|
||||
- Hidden by default in UI
|
||||
- No organization rules
|
||||
- Known emails count reset to 0 when changed to ignore
|
||||
- Example: Archive, Spam, Drafts
|
||||
```
|
||||
|
||||
### UI Changes
|
||||
1. **Main Page**:
|
||||
- Add "Show Hidden" checkbox in the search/filter section
|
||||
- When checked, display ignore folders with appropriate styling
|
||||
|
||||
2. **Folder Cards**:
|
||||
- For tidy folders: Show pending/processed counts and organize toggle
|
||||
- For destination folders: Show emails count
|
||||
- For ignore folders: Show minimal information when visible
|
||||
|
||||
3. **IMAP Sync Modal**:
|
||||
- First step: Connection testing (existing)
|
||||
- Second step: Folder type selection (new)
|
||||
- Table with folder names and type selection dropdowns
|
||||
|
||||
### API Changes
|
||||
1. **Folder Creation/Update**:
|
||||
- Include folder_type in form data
|
||||
- Default to "destination" for manually created folders
|
||||
|
||||
2. **Folder Toggle**:
|
||||
- Change from organize_enabled toggle to folder_type selector
|
||||
- Handle resetting emails_count when changing to ignore
|
||||
|
||||
### Database Schema
|
||||
- No changes needed as the existing [`folder_type`](app/models.py:43) field can accommodate the new value
|
||||
- Current field: `folder_type = db.Column(db.String(20), default='destination', nullable=False)`
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Test folder type validation
|
||||
- Test folder type change logic (resetting emails count)
|
||||
- Test UI rendering for different folder types
|
||||
|
||||
### Integration Tests
|
||||
- Test IMAP sync flow with folder type selection
|
||||
- Test "show hidden" functionality
|
||||
- Test folder CRUD operations with all types
|
||||
|
||||
### End-to-End Tests
|
||||
- Test complete user workflow for creating folders with different types
|
||||
- Test IMAP setup and folder type selection
|
||||
- Test showing/hiding ignore folders
|
||||
|
||||
## Migration Plan
|
||||
- No database migration needed as the field already supports string values
|
||||
- Existing folders will maintain their current types
|
||||
- New folders will default to "destination" type
|
||||
|
||||
## Rollback Plan
|
||||
- If issues arise, the system can be rolled back by:
|
||||
- Limiting folder_type values to only 'tidy' and 'destination'
|
||||
- Reverting UI changes to the previous two-type system
|
||||
- Disabling the "show hidden" functionality
|
||||
@@ -317,7 +317,7 @@ def test_toggle_folder_organize_enabled(authenticated_client, mock_folder):
|
||||
assert mock_folder.organize_enabled is True
|
||||
|
||||
# Toggle the flag
|
||||
response = authenticated_client.put(f'/api/folders/{mock_folder.id}/toggle')
|
||||
response = authenticated_client.put(f'/api/folders/{mock_folder.id}/type', data={'folder_type': 'ignore'}, content_type='application/x-www-form-urlencoded')
|
||||
|
||||
# Should return 200 OK
|
||||
assert response.status_code == 200
|
||||
@@ -325,10 +325,10 @@ def test_toggle_folder_organize_enabled(authenticated_client, mock_folder):
|
||||
# Verify the folder was updated in the database
|
||||
updated_folder = Folder.query.filter_by(id=mock_folder.id).first()
|
||||
assert updated_folder is not None
|
||||
assert updated_folder.organize_enabled is False
|
||||
assert updated_folder.folder_type == 'ignore'
|
||||
|
||||
# Toggle again to make sure it works both ways
|
||||
response2 = authenticated_client.put(f'/api/folders/{mock_folder.id}/toggle')
|
||||
# Toggle back to make sure it works both ways
|
||||
response2 = authenticated_client.put(f'/api/folders/{mock_folder.id}/type', data={'folder_type': 'tidy'}, content_type='application/x-www-form-urlencoded')
|
||||
|
||||
# Should return 200 OK
|
||||
assert response2.status_code == 200
|
||||
@@ -336,7 +336,7 @@ def test_toggle_folder_organize_enabled(authenticated_client, mock_folder):
|
||||
# Verify the folder was updated in the database again
|
||||
updated_folder2 = Folder.query.filter_by(id=mock_folder.id).first()
|
||||
assert updated_folder2 is not None
|
||||
assert updated_folder2.organize_enabled is True
|
||||
assert updated_folder2.folder_type == 'tidy'
|
||||
|
||||
def test_toggle_folder_organize_enabled_not_found(authenticated_client, mock_user):
|
||||
"""Test toggling organize_enabled flag for a non-existent folder."""
|
||||
|
||||
Reference in New Issue
Block a user