animations
This commit is contained in:
6
.env
6
.env
@@ -3,7 +3,7 @@ DATABASE_URL=postgresql://postgres:password@localhost:5432/email_organizer_dev
|
|||||||
# OPENAI_API_KEY=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
|
# OPENAI_API_KEY=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
|
||||||
# OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
# OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
||||||
# OPENAI_MODEL=qwen/qwen3-coder
|
# OPENAI_MODEL=qwen/qwen3-coder
|
||||||
#
|
|
||||||
OPENAI_API_KEY=aaoeu
|
OPENAI_API_KEY=aaoeu
|
||||||
OPENAI_BASE_URL=http://localhost:8082/v1
|
OPENAI_BASE_URL=http://localhost:5082/v1
|
||||||
OPENAI_MODEL=
|
OPENAI_MODEL=Qwen3-Coder-480B-A35B-Instruct-GGUF-roo
|
||||||
|
|||||||
3
QWEN.md
3
QWEN.md
@@ -13,6 +13,9 @@ Here are special rules you must follow:
|
|||||||
11. Design docs go into docs/design/*.md. These docs are always kept up to date.
|
11. Design docs go into docs/design/*.md. These docs are always kept up to date.
|
||||||
12. Before completing work, ensure that no design docs are left out of sync
|
12. Before completing work, ensure that no design docs are left out of sync
|
||||||
13. Plans go into docs/plans/*.md. These may not be kept in sync, as they are just for brainstorming.
|
13. Plans go into docs/plans/*.md. These may not be kept in sync, as they are just for brainstorming.
|
||||||
|
14. ****IMPORTANT**** Database migrations are automatically created via `flask db migrate -m 'message'`. **NEVER** create migrations by hand. You should never have to read the contents of migrations/
|
||||||
|
15. In the technical documentation, code should be used sparingly. Also, when needed, focus on the APIs, and only use snippets.
|
||||||
|
|
||||||
|
|
||||||
# Conventions
|
# Conventions
|
||||||
1. modals are rendered server-side, by targeting #modal-holder, and using an hx-trigger response attribute for open-modal.
|
1. modals are rendered server-side, by targeting #modal-holder, and using an hx-trigger response attribute for open-modal.
|
||||||
|
|||||||
@@ -22,6 +22,65 @@ def imap_config_modal():
|
|||||||
response.headers['HX-Trigger'] = 'open-modal'
|
response.headers['HX-Trigger'] = 'open-modal'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@imap_bp.route('/api/imap/folders/modal', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def imap_folders_modal():
|
||||||
|
"""Return the folder selection modal after successful IMAP connection."""
|
||||||
|
try:
|
||||||
|
if not current_user.imap_config:
|
||||||
|
return jsonify({'error': 'No IMAP configuration found. Please configure IMAP first.'}), 400
|
||||||
|
|
||||||
|
# Test connection first
|
||||||
|
imap_service = IMAPService(current_user)
|
||||||
|
|
||||||
|
# Get folders from IMAP server
|
||||||
|
imap_folders = imap_service.get_folders()
|
||||||
|
|
||||||
|
if not imap_folders:
|
||||||
|
return jsonify({'error': 'No folders found on IMAP server'}), 400
|
||||||
|
|
||||||
|
# Deduplicate folders by name to prevent creating multiple entries for the same folder
|
||||||
|
unique_folders = []
|
||||||
|
seen_names = set()
|
||||||
|
for imap_folder in imap_folders:
|
||||||
|
folder_name = imap_folder['name']
|
||||||
|
|
||||||
|
# Skip special folders that might not be needed
|
||||||
|
if folder_name.lower() in ['sent', 'drafts', 'spam', 'trash']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Use case-insensitive comparison for deduplication
|
||||||
|
folder_name_lower = folder_name.lower()
|
||||||
|
if folder_name_lower not in seen_names:
|
||||||
|
# Check if this folder already exists in the database
|
||||||
|
existing_folder = Folder.query.filter_by(
|
||||||
|
user_id=current_user.id,
|
||||||
|
name=folder_name
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# Add folder type if it exists
|
||||||
|
if existing_folder:
|
||||||
|
imap_folder['folder_type'] = existing_folder.folder_type
|
||||||
|
imap_folder['selected'] = True
|
||||||
|
else:
|
||||||
|
# Set default folder type
|
||||||
|
imap_folder['folder_type'] = 'tidy' if folder_name.lower().strip() == 'inbox' else 'destination'
|
||||||
|
imap_folder['selected'] = True
|
||||||
|
|
||||||
|
unique_folders.append(imap_folder)
|
||||||
|
seen_names.add(folder_name_lower)
|
||||||
|
|
||||||
|
# Return the folder selection modal
|
||||||
|
response = make_response(render_template('partials/folder_selection_modal.html', folders=unique_folders))
|
||||||
|
response.headers['hx-retarget'] = "#modal-holder"
|
||||||
|
response.headers['HX-Trigger'] = 'open-modal'
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception("Error getting IMAP folders modal: %s", e)
|
||||||
|
print(e)
|
||||||
|
return jsonify({'error': 'An unexpected error occurred while fetching folders'}), 500
|
||||||
|
|
||||||
@imap_bp.route('/api/imap/test', methods=['POST'])
|
@imap_bp.route('/api/imap/test', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def test_imap_connection():
|
def test_imap_connection():
|
||||||
@@ -75,10 +134,10 @@ def test_imap_connection():
|
|||||||
current_user.imap_config = test_config
|
current_user.imap_config = test_config
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = make_response(render_template('partials/imap_config_modal.html',
|
# Redirect to folder selection modal after successful connection
|
||||||
success=True, message=message))
|
response = make_response('')
|
||||||
response.headers['HX-Retarget'] = '#imap-modal'
|
response.headers['HX-Trigger'] = 'open-modal'
|
||||||
response.headers['HX-Reswap'] = 'outerHTML'
|
response.headers['HX-Location'] = '/api/imap/folders/modal'
|
||||||
else:
|
else:
|
||||||
print(message)
|
print(message)
|
||||||
response = make_response(render_template('partials/imap_config_modal.html',
|
response = make_response(render_template('partials/imap_config_modal.html',
|
||||||
@@ -97,10 +156,168 @@ def test_imap_connection():
|
|||||||
response.headers['HX-Reswap'] = 'outerHTML'
|
response.headers['HX-Reswap'] = 'outerHTML'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@imap_bp.route('/api/imap/folders', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_imap_folders():
|
||||||
|
"""Get folders from IMAP server without creating database records."""
|
||||||
|
try:
|
||||||
|
if not current_user.imap_config:
|
||||||
|
return jsonify({'error': 'No IMAP configuration found. Please configure IMAP first.'}), 400
|
||||||
|
|
||||||
|
# Test connection first
|
||||||
|
imap_service = IMAPService(current_user)
|
||||||
|
|
||||||
|
# Get folders from IMAP server
|
||||||
|
imap_folders = imap_service.get_folders()
|
||||||
|
|
||||||
|
if not imap_folders:
|
||||||
|
return jsonify({'error': 'No folders found on IMAP server'}), 400
|
||||||
|
|
||||||
|
# Deduplicate folders by name to prevent creating multiple entries for the same folder
|
||||||
|
unique_folders = []
|
||||||
|
seen_names = set()
|
||||||
|
for imap_folder in imap_folders:
|
||||||
|
folder_name = imap_folder['name']
|
||||||
|
|
||||||
|
# Skip special folders that might not be needed
|
||||||
|
if folder_name.lower() in ['sent', 'drafts', 'spam', 'trash']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Use case-insensitive comparison for deduplication
|
||||||
|
folder_name_lower = folder_name.lower()
|
||||||
|
if folder_name_lower not in seen_names:
|
||||||
|
unique_folders.append(imap_folder)
|
||||||
|
seen_names.add(folder_name_lower)
|
||||||
|
|
||||||
|
return jsonify({'folders': unique_folders})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception("Error getting IMAP folders: %s", e)
|
||||||
|
print(e)
|
||||||
|
return jsonify({'error': 'An unexpected error occurred while fetching folders'}), 500
|
||||||
|
|
||||||
|
@imap_bp.route('/api/imap/sync-selected', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def sync_selected_folders():
|
||||||
|
"""Sync only the selected folders from IMAP server with processed email tracking."""
|
||||||
|
try:
|
||||||
|
if not current_user.imap_config:
|
||||||
|
return jsonify({'error': 'No IMAP configuration found. Please configure IMAP first.'}), 400
|
||||||
|
|
||||||
|
# Get selected folder names and types from form data
|
||||||
|
selected_folders = {}
|
||||||
|
for key, value in request.form.items():
|
||||||
|
if key.startswith('folder_'):
|
||||||
|
folder_name = value
|
||||||
|
# Check if there's a corresponding folder type
|
||||||
|
type_key = f'folder_type_{key.split("_")[1]}'
|
||||||
|
folder_type = request.form.get(type_key, 'destination') # Default to 'destination'
|
||||||
|
selected_folders[folder_name] = folder_type
|
||||||
|
|
||||||
|
if not selected_folders:
|
||||||
|
return jsonify({'error': 'No folders selected'}), 400
|
||||||
|
|
||||||
|
# Test connection first
|
||||||
|
imap_service = IMAPService(current_user)
|
||||||
|
|
||||||
|
# Get folders from IMAP server
|
||||||
|
imap_folders = imap_service.get_folders()
|
||||||
|
|
||||||
|
if not imap_folders:
|
||||||
|
return jsonify({'error': 'No folders found on IMAP server'}), 400
|
||||||
|
|
||||||
|
# Deduplicate folders by name to prevent creating multiple entries for the same folder
|
||||||
|
unique_folders = []
|
||||||
|
seen_names = set()
|
||||||
|
for imap_folder in imap_folders:
|
||||||
|
folder_name = imap_folder['name']
|
||||||
|
|
||||||
|
# Skip special folders that might not be needed
|
||||||
|
if folder_name.lower() in ['sent', 'drafts', 'spam', 'trash']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Only include folders that were selected by the user
|
||||||
|
if folder_name in selected_folders:
|
||||||
|
# Use case-insensitive comparison for deduplication
|
||||||
|
folder_name_lower = folder_name.lower()
|
||||||
|
if folder_name_lower not in seen_names:
|
||||||
|
# Add the selected folder type
|
||||||
|
imap_folder['selected_type'] = selected_folders[folder_name]
|
||||||
|
unique_folders.append(imap_folder)
|
||||||
|
seen_names.add(folder_name_lower)
|
||||||
|
|
||||||
|
# Process each selected unique folder
|
||||||
|
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()
|
||||||
|
folder_type = imap_folder.get('selected_type', 'destination') # Get the selected type
|
||||||
|
|
||||||
|
# Handle nested folder names (convert slashes to underscores or keep as-is)
|
||||||
|
# According to requirements, nested folders should be created with slashes in the name
|
||||||
|
display_name = folder_name
|
||||||
|
|
||||||
|
# Check if folder already exists
|
||||||
|
existing_folder = Folder.query.filter_by(
|
||||||
|
user_id=current_user.id,
|
||||||
|
name=display_name
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not existing_folder:
|
||||||
|
# Create new folder with the selected type
|
||||||
|
new_folder = Folder(
|
||||||
|
user_id=current_user.id,
|
||||||
|
name=display_name,
|
||||||
|
rule_text=f"Auto-synced from IMAP folder: {folder_name}",
|
||||||
|
priority=0, # Default priority
|
||||||
|
folder_type=folder_type
|
||||||
|
)
|
||||||
|
db.session.add(new_folder)
|
||||||
|
synced_count += 1
|
||||||
|
folders_to_process.append(new_folder)
|
||||||
|
else:
|
||||||
|
# Update existing folder with the selected type and email counts
|
||||||
|
existing_folder.folder_type = folder_type
|
||||||
|
|
||||||
|
# Get all email UIDs in this folder
|
||||||
|
email_uids = imap_service.get_folder_email_uids(folder_name)
|
||||||
|
|
||||||
|
# Sync with processed emails service
|
||||||
|
new_emails_count = processed_emails_service.sync_folder_emails(display_name, email_uids)
|
||||||
|
print("NEW", new_emails_count)
|
||||||
|
|
||||||
|
# Update counts
|
||||||
|
pending_count = processed_emails_service.get_pending_count(display_name)
|
||||||
|
existing_folder.pending_count = pending_count
|
||||||
|
existing_folder.total_count = len(email_uids)
|
||||||
|
|
||||||
|
# Get the most recent emails for this folder
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Just trigger the folder list update and close the modal
|
||||||
|
response = make_response('')
|
||||||
|
response.headers['HX-Trigger'] = 'close-modal, folder-list-invalidated'
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception("Error syncing selected IMAP folders: %s", e)
|
||||||
|
print(e)
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': 'An unexpected error occurred'}), 500
|
||||||
|
|
||||||
@imap_bp.route('/api/imap/sync', methods=['POST'])
|
@imap_bp.route('/api/imap/sync', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def sync_imap_folders():
|
def sync_imap_folders():
|
||||||
"""Sync folders from IMAP server with processed email tracking."""
|
"""Create and sync folders from IMAP server with processed email tracking."""
|
||||||
try:
|
try:
|
||||||
if not current_user.imap_config:
|
if not current_user.imap_config:
|
||||||
return jsonify({'error': 'No IMAP configuration found. Please configure IMAP first.'}), 400
|
return jsonify({'error': 'No IMAP configuration found. Please configure IMAP first.'}), 400
|
||||||
|
|||||||
98
app/static/css/animations.css
Normal file
98
app/static/css/animations.css
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/* Modal transition animations using Tailwind classes */
|
||||||
|
/*
|
||||||
|
.slide-left-move {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 300ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-enter-from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-enter-to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 300ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-leave-from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-leave-to {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 300ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-move {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 300ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-enter-from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-enter-to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 300ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-leave-from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-leave-to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 300ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-up-move {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 300ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-up-enter-from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-up-enter-to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 300ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-up-leave-from {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-up-leave-to {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 300ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-1 .steps .step:first-child,
|
||||||
|
.step-2 .steps .step:last-child {
|
||||||
|
--tw-step-text: hsl(var(--bc));
|
||||||
|
--tw-step-primary: hsl(var(--p));
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-1 .steps .step:last-child,
|
||||||
|
.step-2 .steps .step:first-child {
|
||||||
|
--tw-step-text: hsl(var(--bc));
|
||||||
|
color: hsl(var(--bc));
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js" defer></script>
|
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js" defer></script>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/dist/tippy.css" />
|
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/dist/tippy.css" />
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/css/animations.css">
|
||||||
<style>
|
<style>
|
||||||
@keyframes shake {
|
@keyframes shake {
|
||||||
0%, 100% { transform: translateX(0); }
|
0%, 100% { transform: translateX(0); }
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
{% if current_user.imap_config %}
|
{% if current_user.imap_config %}
|
||||||
<div data-loading-states>
|
<div data-loading-states>
|
||||||
<button class="btn btn-outline" hx-post="/api/imap/sync" hx-target="#modal-holder" data-loading-disable>
|
<button class="btn btn-outline" hx-get="/api/imap/config" hx-target="#modal-holder" data-loading-disable>
|
||||||
<i class="fas fa-sync mr-2" data-loading-class="!hidden"></i>
|
<i class="fas fa-sync mr-2" data-loading-class="!hidden"></i>
|
||||||
<span data-loading-class="!hidden">Sync Folders</span>
|
<span data-loading-class="!hidden">Sync Folders</span>
|
||||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||||
|
|||||||
72
app/templates/partials/folder_selection_modal.html
Normal file
72
app/templates/partials/folder_selection_modal.html
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<div id="folder-selection-modal" class="modal-box step-2 slide-left-enter-from" @click.away="$refs.modal.close()">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="steps flex-1">
|
||||||
|
<span class="step">Step 1</span>
|
||||||
|
<span class="step step-primary">Step 2</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-bold text-lg">Configure IMAP Folders</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-4">The following folders were found on your IMAP server. Select which folders you want to sync and configure their processing types.</p>
|
||||||
|
<form id="folder-selection-form" hx-post="/api/imap/sync-selected" hx-target="#modal-holder" hx-swap="innerHTML slide-left:300ms">
|
||||||
|
|
||||||
|
<div class="overflow-x-auto mb-4">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Folder</th>
|
||||||
|
<th>Synced?</th>
|
||||||
|
<th>Select</th>
|
||||||
|
<th>Processing Type</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for folder in folders %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ folder.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if folder.subscribed %}
|
||||||
|
<span class="badge badge-success">Synced</span>
|
||||||
|
{% else %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="select select-bordered select-sm"
|
||||||
|
name="folder_type_{{ loop.index }}">
|
||||||
|
<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="alert alert-info mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||||
|
<span>Special folders like "Sent", "Drafts", "Spam", and "Trash" are automatically excluded from syncing.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action" data-loading-states>
|
||||||
|
<button type="button" class="btn btn-outline" @click="$dispatch('close-modal')">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" id="continue-btn" data-loading-disable>
|
||||||
|
<span data-loading-class="!hidden">Save and Continue</span>
|
||||||
|
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
<div id="folder-type-modal" class="modal-box" @click.away="$refs.modal.close()">
|
<div id="folder-type-modal" class="modal-box step-2 slide-left-enter-from" @click.away="$refs.modal.close()">
|
||||||
<h3 class="font-bold text-lg mb-4">Configure Folder Types</h3>
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="steps flex-1">
|
||||||
|
<span class="step">Step 1</span>
|
||||||
|
<span class="step step-primary">Step 2</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-bold text-lg">Configure Folder Types</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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>
|
<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">
|
<div class="overflow-x-auto mb-4">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -42,11 +49,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button type="button" class="btn btn-outline" hx-get="/api/imap/sync">
|
<button type="button" class="btn btn-outline" hx-get="/api/imap/config" hx-target="#modal-holder" hx-swap="innerHTML slide-right:300ms">
|
||||||
Skip and Use Defaults
|
<i class="fas fa-arrow-left mr-2"></i>Back
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-primary" hx-post="/api/imap/sync">
|
<button type="button" class="btn btn-primary" hx-post="/api/imap/sync">
|
||||||
Save and Continue
|
Save and Continue
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<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 step-1 translate-up-enter-from" 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>
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="steps flex-1">
|
||||||
|
<span class="step step-primary">Step 1</span>
|
||||||
|
<span class="step">Step 2</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-bold text-lg">Configure IMAP Connection</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if success %}
|
{% if success %}
|
||||||
<div class="alert alert-success mb-4">
|
<div class="alert alert-success mb-4">
|
||||||
@@ -80,7 +86,7 @@
|
|||||||
|
|
||||||
{% if success %}
|
{% if success %}
|
||||||
<div class="mt-4 pt-4 border-t border-base-300" data-loading-states>
|
<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="#modal-holder" hx-swap="innerHTML" data-loading-disable>
|
<button class="btn btn-success w-full" hx-post="/api/imap/sync" hx-target="#modal-holder" hx-swap="innerHTML slide-left:300ms" data-loading-disable>
|
||||||
<span data-loading-class="!hidden"><i class="fas fa-sync mr-2"></i>Configure Folder Types</span>
|
<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>
|
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ pytest==7.4.0
|
|||||||
beautifulsoup4==4.13.4
|
beautifulsoup4==4.13.4
|
||||||
Flask-Migrate==4.1.0
|
Flask-Migrate==4.1.0
|
||||||
imapclient==3.0.1
|
imapclient==3.0.1
|
||||||
|
playwright
|
||||||
|
|||||||
Reference in New Issue
Block a user