Files
TimeTrack/templates/notes_list.html

1945 lines
58 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.html" %}
{% block content %}
<div class="timetrack-container notes-list-container">
<div class="admin-header">
<h2>Notes</h2>
<div class="admin-actions">
<button type="button" class="btn btn-sm btn-secondary" id="toggle-sidebar">
<span>📁</span> Toggle Folders
</button>
<a href="{{ url_for('notes.notes_folders') }}" class="btn btn-sm btn-info">
<span>⚙️</span> Manage Folders
</a>
</div>
</div>
<div class="notes-layout">
<!-- Folder Tree Sidebar -->
<div class="folder-sidebar" id="folder-sidebar">
<div class="create-note-section">
<a href="{{ url_for('notes.create_note') }}" class="btn btn-create-note">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
Create New Note
</a>
</div>
<div class="sidebar-header">
<h3>Folders</h3>
<button type="button" class="btn-icon" onclick="showCreateFolderModal()" title="Create Folder">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
</button>
</div>
<div class="folder-tree" id="folder-tree">
<div class="folder-item root-folder" data-folder="">
<div class="folder-content {% if not folder_filter %}active{% endif %}" onclick="filterByFolder('')">
<span class="folder-icon">🏠</span>
<span class="folder-name">All Notes</span>
<span class="folder-count">({{ notes|length }})</span>
</div>
</div>
{{ render_folder_tree(folder_tree)|safe }}
</div>
<!-- Tags Section -->
<div class="tags-section">
<div class="section-header">
<div class="section-title" onclick="toggleSection('tags')">
<span class="section-toggle" id="tags-toggle"></span>
<h3>Tags</h3>
<span class="tags-count">({{ all_tags|length }})</span>
</div>
<button type="button" class="btn-icon" onclick="showAddTagModal()" title="Add Tag">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
</button>
</div>
<div class="tags-list" id="tags-list">
{% if tag_filter %}
<div class="tag-item clear-filter" onclick="filterByTag('')">
<span class="tag-icon">✖️</span>
<span class="tag-name">Clear filter</span>
</div>
{% endif %}
{% if all_tags %}
{% for tag in all_tags %}
<div class="tag-item {% if tag_filter == tag %}active{% endif %}" onclick="filterByTag('{{ tag }}')">
<span class="tag-icon">🏷️</span>
<span class="tag-name">{{ tag }}</span>
<span class="tag-count">({{ tag_counts.get(tag, 0) }})</span>
</div>
{% endfor %}
{% else %}
<div class="no-tags">No tags yet</div>
{% endif %}
</div>
</div>
<!-- Visibility Section -->
<div class="visibility-section">
<div class="section-header">
<div class="section-title" onclick="toggleSection('visibility')">
<span class="section-toggle" id="visibility-toggle"></span>
<h3>Visibility</h3>
</div>
</div>
<div class="visibility-list" id="visibility-list">
<div class="visibility-item {% if not visibility_filter %}active{% endif %}" onclick="filterByVisibility('')">
<span class="visibility-icon">👁️</span>
<span class="visibility-name">All Notes</span>
<span class="visibility-count">({{ notes|length }})</span>
</div>
<div class="visibility-item {% if visibility_filter == 'private' %}active{% endif %}" onclick="filterByVisibility('private')">
<span class="visibility-icon">🔒</span>
<span class="visibility-name">Private</span>
<span class="visibility-count">({{ visibility_counts.get('private', 0) }})</span>
</div>
<div class="visibility-item {% if visibility_filter == 'team' %}active{% endif %}" onclick="filterByVisibility('team')">
<span class="visibility-icon">👥</span>
<span class="visibility-name">Team</span>
<span class="visibility-count">({{ visibility_counts.get('team', 0) }})</span>
</div>
<div class="visibility-item {% if visibility_filter == 'company' %}active{% endif %}" onclick="filterByVisibility('company')">
<span class="visibility-icon">🏢</span>
<span class="visibility-name">Company</span>
<span class="visibility-count">({{ visibility_counts.get('company', 0) }})</span>
</div>
</div>
</div>
</div>
<!-- Main Notes Content -->
<div class="notes-content">
<!-- Filter Section -->
<div class="notes-filter-section">
<form method="GET" action="{{ url_for('notes.notes_list') }}" class="filter-form" id="filter-form">
<!-- Hidden inputs for sidebar filters -->
<input type="hidden" name="folder" id="folder" value="{{ folder_filter or '' }}">
<input type="hidden" name="tag" id="tag" value="{{ tag_filter or '' }}">
<input type="hidden" name="visibility" id="visibility" value="{{ visibility_filter or '' }}">
<div class="filter-row">
<div class="search-container">
<div class="search-bar">
{% if folder_filter or tag_filter or visibility_filter %}
<div class="active-filters-inline">
{% if folder_filter %}
<span class="filter-chip" onclick="clearFilter('folder')">
📁 {{ folder_filter }} <span class="remove">×</span>
</span>
{% endif %}
{% if tag_filter %}
<span class="filter-chip" onclick="clearFilter('tag')">
🏷️ {{ tag_filter }} <span class="remove">×</span>
</span>
{% endif %}
{% if visibility_filter %}
<span class="filter-chip" onclick="clearFilter('visibility')">
{% if visibility_filter == 'private' %}🔒{% elif visibility_filter == 'team' %}👥{% else %}🏢{% endif %}
{{ visibility_filter|title }} <span class="remove">×</span>
</span>
{% endif %}
</div>
{% endif %}
<input type="text" name="search" id="search"
placeholder="{% if folder_filter or tag_filter or visibility_filter %}Add search terms...{% else %}Search notes by title or content...{% endif %}"
value="{{ request.args.get('search', '') }}" class="search-input">
<button type="submit" class="btn-search" title="Search">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
</svg>
</button>
</div>
</div>
</div>
</form>
</div>
{% if notes %}
<!-- Notes Toolbar -->
<div class="notes-toolbar">
<div class="toolbar-left">
<span class="notes-count">{{ notes|length }} notes</span>
</div>
<div class="toolbar-right">
<button type="button" class="btn-view-toggle" id="toggle-view" title="Toggle view">
<svg class="icon-grid" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="display: none;">
<path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"/>
</svg>
<svg class="icon-list" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
</svg>
<span class="view-label">Table View</span>
</button>
</div>
</div>
<!-- Table View (Default) -->
<div id="table-view" class="notes-view">
<table class="notes-table">
<thead>
<tr>
<th class="column-pin"></th>
<th class="column-title">Title</th>
<th class="column-folder">Folder</th>
<th class="column-visibility">Visibility</th>
<th class="column-tags">Tags</th>
<th class="column-updated">Updated</th>
<th class="column-actions">Actions</th>
</tr>
</thead>
<tbody>
{% for note in notes %}
<tr class="note-row {% if note.is_pinned %}pinned{% endif %}" data-note-slug="{{ note.slug }}">
<td class="column-pin">
{% if note.is_pinned %}
<span class="pin-icon" title="Pinned">📌</span>
{% endif %}
</td>
<td class="column-title">
<a href="{{ url_for('notes.view_note', slug=note.slug) }}" class="note-link">
{{ note.title }}
</a>
<div class="note-associations">
{% if note.project %}
<span class="association-badge project" title="Project: {{ note.project.name }}">
📁 {{ note.project.code }}
</span>
{% endif %}
{% if note.task %}
<span class="association-badge task" title="Task #{{ note.task.id }}">
✓ #{{ note.task.id }}
</span>
{% endif %}
</div>
</td>
<td class="column-folder">
{% if note.folder %}
<span class="folder-path">{{ note.folder }}</span>
{% else %}
<span class="folder-path muted">Root</span>
{% endif %}
</td>
<td class="column-visibility">
<span class="visibility-badge visibility-{{ note.visibility.value.lower() }}">
{% if note.visibility.value == 'Private' %}🔒{% elif note.visibility.value == 'Team' %}👥{% else %}🏢{% endif %}
{{ note.visibility.value }}
</span>
</td>
<td class="column-tags">
{% if note.tags %}
{% for tag in note.get_tags_list() %}
<a href="{{ url_for('notes.notes_list', tag=tag) }}" class="tag-badge">{{ tag }}</a>
{% endfor %}
{% endif %}
</td>
<td class="column-updated">
<span class="date-text">{{ note.updated_at|format_date }}</span>
</td>
<td class="column-actions">
<div class="note-actions">
<a href="{{ url_for('notes.view_note', slug=note.slug) }}" class="btn-action" title="View">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
</svg>
</a>
<a href="{{ url_for('notes.view_note_mindmap', slug=note.slug) }}" class="btn-action" title="Mind Map">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="2"/>
<circle cx="3" cy="3" r="1.5"/>
<circle cx="13" cy="3" r="1.5"/>
<circle cx="3" cy="13" r="1.5"/>
<circle cx="13" cy="13" r="1.5"/>
<path d="M6.5 6.5L4 4M9.5 6.5L12 4M6.5 9.5L4 12M9.5 9.5L12 12" stroke="currentColor" fill="none"/>
</svg>
</a>
<a href="{{ url_for('notes_download.download_note', slug=note.slug, format='md') }}" class="btn-action" title="Download as Markdown">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
</a>
{% if note.can_user_edit(g.user) %}
<a href="{{ url_for('notes.edit_note', slug=note.slug) }}" class="btn-action" title="Edit">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
</a>
<form method="POST" action="{{ url_for('notes.delete_note', slug=note.slug) }}" style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete this note?')">
<button type="submit" class="btn-action btn-action-danger" title="Delete">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Grid View (Hidden by default) -->
<div id="grid-view" class="notes-view" style="display: none;">
<div class="notes-grid">
{% for note in notes %}
<div class="note-card" data-note-slug="{{ note.slug }}">
<div class="note-header">
<h3 class="note-title">
<a href="{{ url_for('notes.view_note', slug=note.slug) }}">{{ note.title }}</a>
{% if note.is_pinned %}
<span class="pin-icon" title="Pinned">📌</span>
{% endif %}
</h3>
<div class="note-meta">
<span class="visibility-badge visibility-{{ note.visibility.value.lower() }}">
{% if note.visibility.value == 'Private' %}🔒{% elif note.visibility.value == 'Team' %}👥{% else %}🏢{% endif %}
{{ note.visibility.value }}
</span>
{% if note.folder %}
<span class="folder-badge">📁 {{ note.folder }}</span>
{% endif %}
<span class="note-date">{{ note.updated_at|format_date }}</span>
</div>
</div>
<div class="note-preview">
{{ note.get_preview()|safe }}
</div>
<div class="note-footer">
<div class="note-tags">
{% if note.tags %}
{% for tag in note.get_tags_list() %}
<a href="{{ url_for('notes.notes_list', tag=tag) }}" class="tag-badge">{{ tag }}</a>
{% endfor %}
{% endif %}
</div>
<div class="note-associations">
{% if note.project %}
<span class="association-badge project">
📁 {{ note.project.code }}
</span>
{% endif %}
{% if note.task %}
<span class="association-badge task">
✓ Task #{{ note.task.id }}
</span>
{% endif %}
</div>
</div>
<div class="note-actions">
<a href="{{ url_for('notes.view_note', slug=note.slug) }}" class="btn-action" title="View">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
</svg>
</a>
<a href="{{ url_for('notes.view_note_mindmap', slug=note.slug) }}" class="btn-action" title="Mind Map">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="2"/>
<circle cx="3" cy="3" r="1.5"/>
<circle cx="13" cy="3" r="1.5"/>
<circle cx="3" cy="13" r="1.5"/>
<circle cx="13" cy="13" r="1.5"/>
<path d="M6.5 6.5L4 4M9.5 6.5L12 4M6.5 9.5L4 12M9.5 9.5L12 12" stroke="currentColor" fill="none"/>
</svg>
</a>
<a href="{{ url_for('notes_download.download_note', slug=note.slug, format='md') }}" class="btn-action" title="Download as Markdown">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
</a>
{% if note.can_user_edit(g.user) %}
<a href="{{ url_for('notes.edit_note', slug=note.slug) }}" class="btn-action" title="Edit">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
</a>
<form method="POST" action="{{ url_for('notes.delete_note', slug=note.slug) }}" style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete this note?')">
<button type="submit" class="btn-action btn-action-danger" title="Delete">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
</button>
</form>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class="no-data">
<p>No notes found. <a href="{{ url_for('notes.create_note') }}">Create your first note</a>.</p>
</div>
{% endif %}
</div> <!-- End notes-content -->
</div> <!-- End notes-layout -->
</div> <!-- End notes-list-container -->
<!-- Folder Download Dropdown (Hidden, positioned dynamically) -->
<div id="folder-download-dropdown" class="folder-download-dropdown">
<a href="#" data-format="md">Download as Markdown</a>
<a href="#" data-format="html">Download as HTML</a>
<a href="#" data-format="txt">Download as Text</a>
</div>
<style>
/* Notes list specific styles */
.notes-list-container {
max-width: none !important;
width: 100% !important;
padding: 1rem !important;
margin: 0 !important;
}
/* Sidebar layout */
.notes-layout {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.folder-sidebar {
width: 250px;
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1rem;
max-height: calc(100vh - 200px);
overflow-y: auto;
flex-shrink: 0;
}
.create-note-section {
margin-bottom: 1.5rem;
}
.btn-create-note {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
padding: 0.75rem 1rem;
background: var(--primary-color, #007bff);
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
transition: background 0.2s;
}
.btn-create-note:hover {
background: var(--primary-dark, #0056b3);
color: white;
text-decoration: none;
}
.btn-create-note svg {
flex-shrink: 0;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #dee2e6;
}
.sidebar-header h3 {
margin: 0;
font-size: 1.1rem;
color: #333;
}
.btn-icon {
width: 30px;
height: 30px;
padding: 0;
border: 1px solid #dee2e6;
background: white;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #666;
}
.btn-icon:hover {
background: #f8f9fa;
color: #333;
}
.notes-content {
flex: 1;
min-width: 0;
}
.notes-layout.sidebar-hidden .notes-content {
margin-left: 0;
}
/* Folder tree styles */
.folder-tree {
font-size: 0.9rem;
}
.folder-item {
position: relative;
margin: 0.25rem 0;
}
.folder-item.has-children > .folder-content::before {
content: "▶";
position: absolute;
left: -15px;
transition: transform 0.2s;
cursor: pointer;
}
.folder-item.has-children.expanded > .folder-content::before {
transform: rotate(90deg);
}
.folder-content {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
margin-left: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
position: relative;
}
.folder-content:hover {
background: #f8f9fa;
}
.folder-content.active {
background: #e3f2fd;
font-weight: 500;
}
.folder-content.drag-over {
background: #e3f2fd;
border: 2px dashed #2196F3;
}
.root-folder .folder-content {
margin-left: 0;
font-weight: 500;
}
.folder-icon {
margin-right: 0.5rem;
}
.folder-name {
flex: 1;
}
.folder-count {
font-size: 0.85rem;
color: #666;
margin-left: 0.5rem;
}
.folder-download-btn {
margin-left: 0.5rem;
opacity: 0;
transition: opacity 0.2s;
cursor: pointer;
padding: 0.2rem;
border-radius: 3px;
}
.folder-content:hover .folder-download-btn {
opacity: 0.7;
}
.folder-download-btn:hover {
opacity: 1 !important;
background: rgba(0, 0, 0, 0.1);
}
/* Folder download dropdown */
.folder-download-dropdown {
position: absolute;
right: 0;
top: 100%;
z-index: 1000;
min-width: 150px;
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: none;
}
.folder-download-dropdown.show {
display: block;
}
.folder-download-dropdown a {
display: block;
padding: 0.5rem 1rem;
color: #333;
text-decoration: none;
font-size: 0.9rem;
transition: background 0.2s;
}
.folder-download-dropdown a:hover {
background: #f8f9fa;
}
.folder-children {
margin-left: 1.5rem;
display: none;
}
.folder-item.expanded > .folder-children {
display: block;
}
/* Tags section styles */
.tags-section {
margin-top: 2rem;
border-top: 1px solid #dee2e6;
padding-top: 1rem;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.section-title {
display: flex;
align-items: center;
cursor: pointer;
padding: 0.5rem;
margin: -0.5rem;
border-radius: 4px;
transition: background 0.2s;
flex: 1;
}
.section-title:hover {
background: #f8f9fa;
}
.section-title h3 {
margin: 0;
font-size: 1.1rem;
color: #333;
flex: 1;
margin-left: 0.5rem;
}
.section-toggle {
font-size: 0.8rem;
transition: transform 0.2s;
}
.section-toggle.collapsed {
transform: rotate(-90deg);
}
.tags-count {
font-size: 0.85rem;
color: #666;
}
.tags-list {
margin-top: 0.5rem;
transition: max-height 0.3s ease-out;
overflow: hidden;
}
.tags-list.collapsed {
max-height: 0;
}
.tag-item {
display: flex;
align-items: center;
padding: 0.4rem 0.75rem;
margin: 0.25rem 0;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.tag-item:hover {
background: #f8f9fa;
}
.tag-item.active {
background: #e3f2fd;
font-weight: 500;
}
.tag-item.clear-filter {
color: #dc3545;
font-size: 0.85rem;
}
.tag-icon {
margin-right: 0.5rem;
font-size: 0.9rem;
}
.tag-name {
flex: 1;
font-size: 0.9rem;
}
.tag-count {
font-size: 0.85rem;
color: #666;
margin-left: 0.5rem;
}
.no-tags {
padding: 1rem;
text-align: center;
color: #999;
font-size: 0.9rem;
}
/* Visibility section styles */
.visibility-section {
margin-top: 2rem;
border-top: 1px solid #dee2e6;
padding-top: 1rem;
}
.visibility-list {
margin-top: 0.5rem;
transition: max-height 0.3s ease-out;
overflow: hidden;
}
.visibility-list.collapsed {
max-height: 0;
}
.visibility-item {
display: flex;
align-items: center;
padding: 0.4rem 0.75rem;
margin: 0.25rem 0;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.visibility-item:hover {
background: #f8f9fa;
}
.visibility-item.active {
background: #e3f2fd;
font-weight: 500;
}
.visibility-icon {
margin-right: 0.5rem;
font-size: 0.9rem;
}
.visibility-name {
flex: 1;
font-size: 0.9rem;
}
.visibility-count {
font-size: 0.85rem;
color: #666;
margin-left: 0.5rem;
}
/* Simplified filter section */
.notes-filter-section {
background: #f8f9fa;
padding: 1rem;
margin-bottom: 1.5rem;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.filter-row {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.search-container {
flex: 1;
min-width: 400px;
}
.search-bar {
position: relative;
display: flex;
align-items: center;
background: white;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 0.25rem;
transition: border-color 0.2s;
}
.search-bar:focus-within {
border-color: var(--primary-color);
}
.active-filters-inline {
display: flex;
gap: 0.5rem;
padding-left: 0.5rem;
flex-shrink: 0;
}
.filter-chip {
background: #e3f2fd;
color: #333;
padding: 0.2rem 0.6rem;
border-radius: 16px;
font-size: 0.8rem;
cursor: pointer;
transition: background 0.2s;
display: inline-flex;
align-items: center;
gap: 0.4rem;
white-space: nowrap;
}
.filter-chip:hover {
background: #bbdefb;
}
.filter-chip .remove {
font-weight: bold;
color: #666;
font-size: 0.9rem;
}
.search-input {
flex: 1;
padding: 0.4rem 0.75rem;
border: none;
background: transparent;
font-size: 0.95rem;
outline: none;
}
.btn-search {
padding: 0.4rem;
background: transparent;
color: #666;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.btn-search:hover {
background: #e9ecef;
color: var(--primary-color, #007bff);
}
.btn-search svg {
display: block;
}
.btn-xs {
padding: 0.2rem 0.5rem;
font-size: 0.75rem;
}
/* Notes toolbar */
.notes-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #dee2e6;
margin-bottom: 1rem;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 1rem;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.notes-count {
font-size: 0.9rem;
color: #666;
font-weight: 500;
}
.btn-view-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.8rem;
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
color: #333;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-view-toggle:hover {
background: #f8f9fa;
border-color: #adb5bd;
}
.btn-view-toggle svg {
width: 16px;
height: 16px;
}
.view-label {
font-weight: 500;
}
/* Drag and drop styles */
.note-row.dragging,
.note-card.dragging {
opacity: 0.5;
cursor: move;
}
.note-row,
.note-card {
cursor: grab;
}
.note-row:active,
.note-card:active {
cursor: grabbing;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.admin-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.notes-filter-section {
background: #f8f9fa;
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.filter-form {
width: 100%;
}
.filter-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: flex-end;
}
/* Table View Styles */
.notes-table {
width: 100%;
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.notes-table thead {
background: #f8f9fa;
border-bottom: 2px solid #dee2e6;
}
.notes-table th {
padding: 1rem;
text-align: left;
font-weight: 600;
color: #495057;
font-size: 0.9rem;
white-space: nowrap;
}
.notes-table td {
padding: 1rem;
border-bottom: 1px solid #f1f3f5;
vertical-align: middle;
}
.notes-table tr:hover {
background: #f8f9fa;
}
.notes-table tr.pinned {
background: #fffbf0;
}
.column-pin {
width: 40px;
text-align: center;
}
.pin-icon {
font-size: 1rem;
}
.column-title {
min-width: 300px;
}
.note-link {
color: #333;
text-decoration: none;
font-weight: 500;
display: block;
}
.note-link:hover {
color: var(--primary-color);
}
.column-folder {
width: 150px;
}
.folder-path {
font-size: 0.85rem;
color: #666;
}
.folder-path.muted {
color: #aaa;
font-style: italic;
}
.folder-badge {
background: #e9ecef;
color: #495057;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
.column-visibility {
width: 120px;
}
.column-tags {
min-width: 200px;
}
.column-updated {
width: 150px;
}
.date-text {
font-size: 0.85rem;
color: #666;
}
.column-actions {
width: 200px;
}
.note-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.btn-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: 1px solid #dee2e6;
border-radius: 4px;
color: #495057;
transition: all 0.2s;
cursor: pointer;
}
.btn-action:hover {
background: #f8f9fa;
border-color: #adb5bd;
color: var(--primary-color, #007bff);
text-decoration: none;
}
.btn-action-danger:hover {
background: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.note-associations {
display: flex;
gap: 0.5rem;
margin-top: 0.25rem;
}
.visibility-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
display: inline-block;
}
.visibility-private {
background: #f8d7da;
color: #721c24;
}
.visibility-team {
background: #d1ecf1;
color: #0c5460;
}
.visibility-company {
background: #d4edda;
color: #155724;
}
.tag-badge {
background: #e9ecef;
color: #495057;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
text-decoration: none;
transition: background 0.2s ease;
display: inline-block;
margin-right: 0.25rem;
}
.tag-badge:hover {
background: #dee2e6;
color: #333;
}
.association-badge {
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.association-badge.project {
background: #fff3cd;
color: #856404;
}
.association-badge.task {
background: #cce5ff;
color: #004085;
}
.note-actions {
display: flex;
gap: 0.25rem;
}
.btn-xs {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
/* Grid View Styles */
.notes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.note-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 1rem;
transition: box-shadow 0.2s ease;
display: flex;
flex-direction: column;
}
.note-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.note-header {
margin-bottom: 0.75rem;
}
.note-title {
margin: 0 0 0.4rem 0;
font-size: 1.1rem;
display: flex;
align-items: center;
gap: 0.4rem;
}
.note-title a {
color: #333;
text-decoration: none;
}
.note-title a:hover {
color: var(--primary-color);
}
.note-meta {
display: flex;
gap: 0.5rem;
align-items: center;
font-size: 0.75rem;
color: #666;
flex-wrap: wrap;
}
.note-preview {
flex: 1;
margin-bottom: 0.75rem;
color: #555;
line-height: 1.4;
font-size: 0.85rem;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.note-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
flex-wrap: wrap;
gap: 0.4rem;
}
.note-tags {
display: flex;
gap: 0.3rem;
flex-wrap: wrap;
}
/* Grid View specific adjustments */
.note-card .visibility-badge {
padding: 0.15rem 0.4rem;
font-size: 0.7rem;
}
.note-card .folder-badge {
padding: 0.15rem 0.4rem;
font-size: 0.7rem;
}
.note-card .tag-badge {
padding: 0.15rem 0.4rem;
font-size: 0.7rem;
margin-right: 0.2rem;
}
.note-card .association-badge {
padding: 0.15rem 0.4rem;
font-size: 0.7rem;
}
.note-card .note-date {
font-size: 0.75rem;
}
.note-card .pin-icon {
font-size: 0.85rem;
}
.note-card .btn-action {
width: 28px;
height: 28px;
}
.note-card .btn-action svg {
width: 14px;
height: 14px;
}
/* Responsive design */
@media (max-width: 1024px) {
.column-folder,
.column-tags {
display: none;
}
}
@media (max-width: 768px) {
.filter-row {
flex-direction: column;
}
.search-container {
min-width: 100%;
}
.search-bar {
flex-wrap: wrap;
padding: 0.5rem;
}
.active-filters-inline {
width: 100%;
padding: 0 0 0.5rem 0;
}
.notes-grid {
grid-template-columns: 1fr;
}
.column-visibility,
.column-updated {
display: none;
}
.notes-table {
font-size: 0.85rem;
}
.folder-sidebar {
width: 200px;
}
}
/* Modal styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
width: 90%;
max-width: 500px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #dee2e6;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #dee2e6;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 0.95rem;
}
.form-text {
display: block;
margin-top: 0.25rem;
font-size: 0.85rem;
color: #6c757d;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const toggleBtn = document.getElementById('toggle-view');
const tableView = document.getElementById('table-view');
const gridView = document.getElementById('grid-view');
const iconGrid = toggleBtn.querySelector('.icon-grid');
const iconList = toggleBtn.querySelector('.icon-list');
const viewLabel = toggleBtn.querySelector('.view-label');
// Load saved view preference
const savedView = localStorage.getItem('notes-view') || 'table';
if (savedView === 'grid') {
tableView.style.display = 'none';
gridView.style.display = 'block';
iconGrid.style.display = 'none';
iconList.style.display = 'block';
viewLabel.textContent = 'Grid View';
} else {
iconGrid.style.display = 'block';
iconList.style.display = 'none';
viewLabel.textContent = 'Table View';
}
toggleBtn.addEventListener('click', function() {
if (tableView.style.display === 'none') {
// Switch to table view
tableView.style.display = 'block';
gridView.style.display = 'none';
iconGrid.style.display = 'block';
iconList.style.display = 'none';
viewLabel.textContent = 'Table View';
localStorage.setItem('notes-view', 'table');
} else {
// Switch to grid view
tableView.style.display = 'none';
gridView.style.display = 'block';
iconGrid.style.display = 'none';
iconList.style.display = 'block';
viewLabel.textContent = 'Grid View';
localStorage.setItem('notes-view', 'grid');
}
});
// Toggle sidebar functionality
const toggleSidebarBtn = document.getElementById('toggle-sidebar');
const folderSidebar = document.getElementById('folder-sidebar');
const notesLayout = document.querySelector('.notes-layout');
// Load saved sidebar preference
const sidebarVisible = localStorage.getItem('notes-sidebar-visible') !== 'false';
if (!sidebarVisible) {
folderSidebar.style.display = 'none';
notesLayout.classList.add('sidebar-hidden');
}
toggleSidebarBtn.addEventListener('click', function() {
if (folderSidebar.style.display === 'none') {
folderSidebar.style.display = 'block';
notesLayout.classList.remove('sidebar-hidden');
localStorage.setItem('notes-sidebar-visible', 'true');
} else {
folderSidebar.style.display = 'none';
notesLayout.classList.add('sidebar-hidden');
localStorage.setItem('notes-sidebar-visible', 'false');
}
});
// Enable drag and drop for notes
enableDragAndDrop();
// Setup folder download dropdown
setupFolderDownload();
});
// Folder tree functions
function toggleFolder(event, folderPath) {
event.stopPropagation();
const folderItem = event.currentTarget.closest('.folder-item');
folderItem.classList.toggle('expanded');
}
function filterByFolder(folderPath) {
// Update the folder filter and submit the form
const folderSelect = document.getElementById('folder');
if (folderSelect) {
folderSelect.value = folderPath;
folderSelect.form.submit();
}
}
// Tags section functions
function toggleSection(section) {
const toggle = document.getElementById(`${section}-toggle`);
const list = document.getElementById(`${section}-list`);
toggle.classList.toggle('collapsed');
list.classList.toggle('collapsed');
// Save state to localStorage
const isCollapsed = list.classList.contains('collapsed');
localStorage.setItem(`notes-${section}-collapsed`, isCollapsed);
}
// Load saved section states
document.addEventListener('DOMContentLoaded', function() {
// Check if tags section should be collapsed
const tagsCollapsed = localStorage.getItem('notes-tags-collapsed') === 'true';
if (tagsCollapsed) {
document.getElementById('tags-toggle').classList.add('collapsed');
document.getElementById('tags-list').classList.add('collapsed');
}
// Check if visibility section should be collapsed
const visibilityCollapsed = localStorage.getItem('notes-visibility-collapsed') === 'true';
if (visibilityCollapsed) {
document.getElementById('visibility-toggle').classList.add('collapsed');
document.getElementById('visibility-list').classList.add('collapsed');
}
});
function filterByTag(tag) {
// Update the tag filter and submit the form
const tagInput = document.getElementById('tag');
if (tagInput) {
tagInput.value = tag;
tagInput.form.submit();
}
}
function filterByVisibility(visibility) {
// Update the visibility filter and submit the form
const visibilityInput = document.getElementById('visibility');
if (visibilityInput) {
visibilityInput.value = visibility;
visibilityInput.form.submit();
}
}
function clearFilter(filterType) {
const input = document.getElementById(filterType);
if (input) {
input.value = '';
input.form.submit();
}
}
function clearAllFilters() {
document.getElementById('folder').value = '';
document.getElementById('tag').value = '';
document.getElementById('visibility').value = '';
document.getElementById('filter-form').submit();
}
// Drag and drop functionality
function enableDragAndDrop() {
// Make notes draggable
const noteRows = document.querySelectorAll('.note-row');
const noteCards = document.querySelectorAll('.note-card');
[...noteRows, ...noteCards].forEach(note => {
note.draggable = true;
note.addEventListener('dragstart', handleDragStart);
note.addEventListener('dragend', handleDragEnd);
});
// Make folders droppable
const folderItems = document.querySelectorAll('.folder-content');
folderItems.forEach(folder => {
folder.addEventListener('dragover', handleDragOver);
folder.addEventListener('drop', handleDrop);
folder.addEventListener('dragleave', handleDragLeave);
});
}
let draggedNote = null;
function handleDragStart(e) {
draggedNote = this;
this.classList.add('dragging');
// Get note slug from data attribute
const noteSlug = this.dataset.noteSlug;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', noteSlug);
}
function handleDragEnd(e) {
this.classList.remove('dragging');
draggedNote = null;
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
this.classList.add('drag-over');
return false;
}
function handleDragLeave(e) {
this.classList.remove('drag-over');
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
this.classList.remove('drag-over');
const noteSlug = e.dataTransfer.getData('text/html');
const folderPath = this.closest('.folder-item').dataset.folder;
if (noteSlug && draggedNote) {
// Update note folder via API
updateNoteFolder(noteSlug, folderPath);
}
return false;
}
function updateNoteFolder(noteSlug, folderPath) {
fetch(`/api/notes/${noteSlug}/folder`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ folder: folderPath })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload the page to show updated folder
window.location.reload();
} else {
alert('Error moving note: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error moving note to folder');
});
}
// Create folder modal functions
function showCreateFolderModal() {
// Create a simple modal for folder creation
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3>Create New Folder</h3>
<button type="button" class="close-btn" onclick="this.closest('.modal').remove()">&times;</button>
</div>
<div class="modal-body">
<form id="createFolderForm">
<div class="form-group">
<label for="folderName">Folder Name</label>
<input type="text" id="folderName" name="name" class="form-control" required>
</div>
<div class="form-group">
<label for="parentFolder">Parent Folder (Optional)</label>
<select id="parentFolder" name="parent" class="form-control">
<option value="">Root</option>
{% for folder in all_folders %}
<option value="{{ folder }}">{{ folder }}</option>
{% endfor %}
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal').remove()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="createFolder()">Create</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'flex';
document.getElementById('folderName').focus();
}
function createFolder() {
const form = document.getElementById('createFolderForm');
const formData = new FormData(form);
fetch('/api/notes/folders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: formData.get('name'),
parent: formData.get('parent')
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error creating folder');
});
}
// Tag management functions
function showAddTagModal() {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3>Add Tags to Notes</h3>
<button type="button" class="close-btn" onclick="this.closest('.modal').remove()">&times;</button>
</div>
<div class="modal-body">
<form id="addTagForm">
<div class="form-group">
<label for="newTags">Tags (comma-separated)</label>
<input type="text" id="newTags" name="tags" class="form-control"
placeholder="e.g., urgent, review, documentation" required>
<small class="form-text">Enter one or more tags separated by commas</small>
</div>
<div class="form-group">
<label>Select Notes</label>
<div class="notes-selection" style="max-height: 300px; overflow-y: auto; border: 1px solid #dee2e6; padding: 0.5rem; border-radius: 4px;">
{% for note in notes %}
<label class="note-checkbox" style="display: block; margin: 0.5rem 0;">
<input type="checkbox" name="note_ids" value="{{ note.id }}" style="margin-right: 0.5rem;">
{{ note.title }}
{% if note.tags %}
<small style="color: #666;">({{ note.tags }})</small>
{% endif %}
</label>
{% endfor %}
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal').remove()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="addTagsToNotes()">Add Tags</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'flex';
document.getElementById('newTags').focus();
}
function addTagsToNotes() {
const form = document.getElementById('addTagForm');
const formData = new FormData(form);
const tags = formData.get('tags');
const noteIds = Array.from(form.querySelectorAll('input[name="note_ids"]:checked'))
.map(input => input.value);
if (!tags || noteIds.length === 0) {
alert('Please enter tags and select at least one note');
return;
}
// For now, we'll update each note individually
// In a production app, you'd want a batch update endpoint
Promise.all(noteIds.map(noteId =>
fetch(`/api/notes/${noteId}/tags`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags: tags })
})
))
.then(responses => Promise.all(responses.map(r => r.json())))
.then(results => {
const failures = results.filter(r => !r.success);
if (failures.length === 0) {
window.location.reload();
} else {
alert(`Failed to update ${failures.length} notes`);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error adding tags to notes');
});
}
// Folder download functions
function setupFolderDownload() {
const dropdown = document.getElementById('folder-download-dropdown');
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.folder-download-btn') && !dropdown.contains(e.target)) {
dropdown.classList.remove('show');
}
});
// Setup download links
dropdown.querySelectorAll('a').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const format = this.getAttribute('data-format');
const folder = dropdown.getAttribute('data-folder');
if (folder) {
downloadFolder(folder, format);
}
dropdown.classList.remove('show');
});
});
}
function showFolderDownloadMenu(event, folderPath) {
event.stopPropagation();
const dropdown = document.getElementById('folder-download-dropdown');
const btn = event.currentTarget;
// Position dropdown near the button
const rect = btn.getBoundingClientRect();
dropdown.style.position = 'fixed';
dropdown.style.left = rect.left + 'px';
dropdown.style.top = (rect.bottom + 5) + 'px';
// Store the folder path
dropdown.setAttribute('data-folder', folderPath);
// Show dropdown
dropdown.classList.add('show');
}
function downloadFolder(folderPath, format) {
// Encode the folder path for URL
const encodedPath = encodeURIComponent(folderPath);
const url = `/notes/folder/${encodedPath}/download/${format}`;
// Create a temporary link and click it
const link = document.createElement('a');
link.href = url;
link.download = '';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
{% endblock %}
{% macro render_folder_tree(tree, level=0) %}
{% for folder, children in tree.items() %}
<div class="folder-item {% if children %}has-children{% endif %}" data-folder="{{ folder }}">
<div class="folder-content {% if folder_filter == folder %}active{% endif %}">
{% if children %}
<span onclick="toggleFolder(event, '{{ folder }}')" style="position: absolute; left: -15px; cursor: pointer;"></span>
{% endif %}
<span class="folder-icon" onclick="filterByFolder('{{ folder }}')" style="cursor: pointer;">📁</span>
<span class="folder-name" onclick="filterByFolder('{{ folder }}')" style="cursor: pointer;">{{ folder.split('/')[-1] }}</span>
<span class="folder-count">({{ folder_counts.get(folder, 0) }})</span>
{% if folder_counts.get(folder, 0) > 0 %}
<div class="folder-download-btn" onclick="showFolderDownloadMenu(event, '{{ folder }}')">
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
</div>
{% endif %}
</div>
{% if children %}
<div class="folder-children">
{{ render_folder_tree(children, level + 1)|safe }}
</div>
{% endif %}
</div>
{% endfor %}
{% endmacro %}