From eca8dca5d23d5527a40696b92eebbc23574707a2 Mon Sep 17 00:00:00 2001 From: Jens Luedicke Date: Sun, 6 Jul 2025 20:57:17 +0200 Subject: [PATCH] Show folder tree for notes. --- app.py | 381 +++++++++++- migrate_db.py | 120 +++- models.py | 31 +- templates/note_editor.html | 13 + templates/notes_folders.html | 538 +++++++++++++++++ templates/notes_list.html | 1073 ++++++++++++++++++++++++++++------ 6 files changed, 1985 insertions(+), 171 deletions(-) create mode 100644 templates/notes_folders.html diff --git a/app.py b/app.py index decaa49..62568c3 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file, abort -from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings, Note, NoteLink, NoteVisibility +from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings, Note, NoteLink, NoteVisibility, NoteFolder from data_formatting import ( format_duration, prepare_export_data, prepare_team_hours_export_data, format_table_data, format_graph_data, format_team_data, format_burndown_data @@ -1616,6 +1616,7 @@ def notes_list(): # Get filter parameters visibility_filter = request.args.get('visibility', 'all') tag_filter = request.args.get('tag') + folder_filter = request.args.get('folder') search_query = request.args.get('search', request.args.get('q')) # Base query - all notes in user's company @@ -1654,6 +1655,10 @@ def notes_list(): if tag_filter: query = query.filter(Note.tags.like(f'%{tag_filter}%')) + # Apply folder filter + if folder_filter: + query = query.filter_by(folder=folder_filter) + # Apply search if search_query: query = query.filter( @@ -1672,16 +1677,66 @@ def notes_list(): for note in Note.query.filter_by(company_id=g.user.company_id, is_archived=False).all(): all_tags.update(note.get_tags_list()) + # Get all unique folders for filter dropdown + all_folders = set() + + # Get folders from NoteFolder table + folder_records = NoteFolder.query.filter_by(company_id=g.user.company_id).all() + for folder in folder_records: + all_folders.add(folder.path) + + # Also get folders from notes (for backward compatibility) + folder_notes = Note.query.filter_by(company_id=g.user.company_id, is_archived=False).filter(Note.folder != None).all() + for note in folder_notes: + if note.folder: + all_folders.add(note.folder) + # Get projects for filter projects = Project.query.filter_by(company_id=g.user.company_id, is_active=True).all() + # Build folder tree structure for sidebar + folder_counts = {} + for note in Note.query.filter_by(company_id=g.user.company_id, is_archived=False).all(): + if note.folder and note.can_user_view(g.user): + # Add this folder and all parent folders + parts = note.folder.split('/') + for i in range(len(parts)): + folder_path = '/'.join(parts[:i+1]) + folder_counts[folder_path] = folder_counts.get(folder_path, 0) + (1 if i == len(parts)-1 else 0) + + # Initialize counts for empty folders + for folder_path in all_folders: + if folder_path not in folder_counts: + folder_counts[folder_path] = 0 + + # Build folder tree structure + folder_tree = {} + for folder in sorted(all_folders): + parts = folder.split('/') + current = folder_tree + + for i, part in enumerate(parts): + if i == len(parts) - 1: + # Leaf folder + current[folder] = {} + else: + # Navigate to parent + parent_path = '/'.join(parts[:i+1]) + if parent_path not in current: + current[parent_path] = {} + current = current[parent_path] + return render_template('notes_list.html', title='Notes', notes=notes, visibility_filter=visibility_filter, tag_filter=tag_filter, + folder_filter=folder_filter, search_query=search_query, all_tags=sorted(list(all_tags)), + all_folders=sorted(list(all_folders)), + folder_tree=folder_tree, + folder_counts=folder_counts, projects=projects, NoteVisibility=NoteVisibility) @@ -1695,6 +1750,7 @@ def create_note(): title = request.form.get('title', '').strip() content = request.form.get('content', '').strip() visibility = request.form.get('visibility', 'Private') + folder = request.form.get('folder', '').strip() tags = request.form.get('tags', '').strip() project_id = request.form.get('project_id') task_id = request.form.get('task_id') @@ -1719,6 +1775,7 @@ def create_note(): title=title, content=content, visibility=NoteVisibility[visibility.upper()], # Convert to uppercase for enum access + folder=folder if folder else None, tags=','.join(tag_list) if tag_list else None, created_by_id=g.user.id, company_id=g.user.company_id @@ -1758,11 +1815,26 @@ def create_note(): projects = Project.query.filter_by(company_id=g.user.company_id, is_active=True).all() tasks = [] + # Get all existing folders for suggestions + all_folders = set() + + # Get folders from NoteFolder table + folder_records = NoteFolder.query.filter_by(company_id=g.user.company_id).all() + for folder in folder_records: + all_folders.add(folder.path) + + # Also get folders from notes (for backward compatibility) + folder_notes = Note.query.filter_by(company_id=g.user.company_id, is_archived=False).filter(Note.folder != None).all() + for note in folder_notes: + if note.folder: + all_folders.add(note.folder) + return render_template('note_editor.html', title='New Note', note=None, projects=projects, tasks=tasks, + all_folders=sorted(list(all_folders)), NoteVisibility=NoteVisibility) @@ -1826,6 +1898,7 @@ def edit_note(slug): title = request.form.get('title', '').strip() content = request.form.get('content', '').strip() visibility = request.form.get('visibility', 'Private') + folder = request.form.get('folder', '').strip() tags = request.form.get('tags', '').strip() project_id = request.form.get('project_id') task_id = request.form.get('task_id') @@ -1849,6 +1922,7 @@ def edit_note(slug): note.title = title note.content = content note.visibility = NoteVisibility[visibility.upper()] # Convert to uppercase for enum access + note.folder = folder if folder else None note.tags = ','.join(tag_list) if tag_list else None # Update team_id if visibility is Team @@ -1895,11 +1969,26 @@ def edit_note(slug): if note.project_id: tasks = Task.query.filter_by(project_id=note.project_id).all() + # Get all existing folders for suggestions + all_folders = set() + + # Get folders from NoteFolder table + folder_records = NoteFolder.query.filter_by(company_id=g.user.company_id).all() + for folder in folder_records: + all_folders.add(folder.path) + + # Also get folders from notes (for backward compatibility) + folder_notes = Note.query.filter_by(company_id=g.user.company_id, is_archived=False).filter(Note.folder != None).all() + for n in folder_notes: + if n.folder: + all_folders.add(n.folder) + return render_template('note_editor.html', title=f'Edit: {note.title}', note=note, projects=projects, tasks=tasks, + all_folders=sorted(list(all_folders)), NoteVisibility=NoteVisibility) @@ -1930,6 +2019,296 @@ def delete_note(slug): return redirect(url_for('view_note', slug=slug)) +@app.route('/notes/folders') +@login_required +@company_required +def notes_folders(): + """Manage note folders""" + # Get all folders from NoteFolder table + all_folders = set() + folder_records = NoteFolder.query.filter_by(company_id=g.user.company_id).all() + + for folder in folder_records: + all_folders.add(folder.path) + + # Also get folders from notes (for backward compatibility) + folder_notes = Note.query.filter_by(company_id=g.user.company_id, is_archived=False).filter(Note.folder != None).all() + + folder_counts = {} + for note in folder_notes: + if note.folder and note.can_user_view(g.user): + # Add this folder and all parent folders + parts = note.folder.split('/') + for i in range(len(parts)): + folder_path = '/'.join(parts[:i+1]) + all_folders.add(folder_path) + folder_counts[folder_path] = folder_counts.get(folder_path, 0) + (1 if i == len(parts)-1 else 0) + + # Initialize counts for empty folders + for folder_path in all_folders: + if folder_path not in folder_counts: + folder_counts[folder_path] = 0 + + # Build folder tree structure + folder_tree = {} + for folder in sorted(all_folders): + parts = folder.split('/') + current = folder_tree + + for i, part in enumerate(parts): + if i == len(parts) - 1: + # Leaf folder + current[folder] = {} + else: + # Navigate to parent + parent_path = '/'.join(parts[:i+1]) + if parent_path not in current: + current[parent_path] = {} + current = current[parent_path] + + return render_template('notes_folders.html', + title='Note Folders', + all_folders=sorted(list(all_folders)), + folder_tree=folder_tree, + folder_counts=folder_counts) + + +@app.route('/api/notes/folder-details') +@login_required +@company_required +def api_folder_details(): + """Get details about a specific folder""" + folder_path = request.args.get('path', '') + + if not folder_path: + return jsonify({'error': 'Folder path required'}), 400 + + # Get notes in this folder + notes = Note.query.filter_by( + company_id=g.user.company_id, + folder=folder_path, + is_archived=False + ).all() + + # Filter by visibility + visible_notes = [n for n in notes if n.can_user_view(g.user)] + + # Get subfolders + all_folders = set() + folder_notes = Note.query.filter_by(company_id=g.user.company_id, is_archived=False).filter( + Note.folder.like(f'{folder_path}/%') + ).all() + + for note in folder_notes: + if note.folder and note.can_user_view(g.user): + # Get immediate subfolder + subfolder = note.folder[len(folder_path)+1:] + if '/' in subfolder: + subfolder = subfolder.split('/')[0] + all_folders.add(subfolder) + + # Get recent notes (last 5) + recent_notes = sorted(visible_notes, key=lambda n: n.updated_at, reverse=True)[:5] + + return jsonify({ + 'name': folder_path.split('/')[-1], + 'path': folder_path, + 'note_count': len(visible_notes), + 'subfolder_count': len(all_folders), + 'recent_notes': [ + { + 'title': n.title, + 'slug': n.slug, + 'updated_at': n.updated_at.strftime('%Y-%m-%d %H:%M') + } for n in recent_notes + ] + }) + + +@app.route('/api/notes/folders', methods=['POST']) +@login_required +@company_required +def api_create_folder(): + """Create a new folder""" + data = request.get_json() + folder_name = data.get('name', '').strip() + parent_folder = data.get('parent', '').strip() + + if not folder_name: + return jsonify({'success': False, 'message': 'Folder name is required'}), 400 + + # Validate folder name (no special characters except dash and underscore) + import re + if not re.match(r'^[a-zA-Z0-9_\- ]+$', folder_name): + return jsonify({'success': False, 'message': 'Folder name can only contain letters, numbers, spaces, dashes, and underscores'}), 400 + + # Create full path + full_path = f"{parent_folder}/{folder_name}" if parent_folder else folder_name + + # Check if folder already exists + existing_folder = NoteFolder.query.filter_by( + company_id=g.user.company_id, + path=full_path + ).first() + + if existing_folder: + return jsonify({'success': False, 'message': 'Folder already exists'}), 400 + + # Create the folder + try: + folder = NoteFolder( + name=folder_name, + path=full_path, + parent_path=parent_folder if parent_folder else None, + description=data.get('description', ''), + created_by_id=g.user.id, + company_id=g.user.company_id + ) + + db.session.add(folder) + db.session.commit() + + return jsonify({ + 'success': True, + 'message': 'Folder created successfully', + 'folder': { + 'name': folder_name, + 'path': full_path + } + }) + except Exception as e: + db.session.rollback() + logger.error(f"Error creating folder: {str(e)}") + return jsonify({'success': False, 'message': 'Error creating folder'}), 500 + + +@app.route('/api/notes/folders', methods=['PUT']) +@login_required +@company_required +def api_rename_folder(): + """Rename an existing folder""" + data = request.get_json() + old_path = data.get('old_path', '').strip() + new_name = data.get('new_name', '').strip() + + if not old_path or not new_name: + return jsonify({'success': False, 'message': 'Old path and new name are required'}), 400 + + # Validate folder name + import re + if not re.match(r'^[a-zA-Z0-9_\- ]+$', new_name): + return jsonify({'success': False, 'message': 'Folder name can only contain letters, numbers, spaces, dashes, and underscores'}), 400 + + # Build new path + path_parts = old_path.split('/') + path_parts[-1] = new_name + new_path = '/'.join(path_parts) + + # Update all notes in this folder and subfolders + notes_to_update = Note.query.filter( + Note.company_id == g.user.company_id, + db.or_( + Note.folder == old_path, + Note.folder.like(f'{old_path}/%') + ) + ).all() + + # Check permissions for all notes + for note in notes_to_update: + if not note.can_user_edit(g.user): + return jsonify({'success': False, 'message': 'You do not have permission to modify all notes in this folder'}), 403 + + # Update folder paths + try: + for note in notes_to_update: + if note.folder == old_path: + note.folder = new_path + else: + # Update subfolder path + note.folder = new_path + note.folder[len(old_path):] + + db.session.commit() + + return jsonify({ + 'success': True, + 'message': f'Renamed folder to {new_name}', + 'updated_count': len(notes_to_update) + }) + except Exception as e: + db.session.rollback() + logger.error(f"Error renaming folder: {str(e)}") + return jsonify({'success': False, 'message': 'Error renaming folder'}), 500 + + +@app.route('/api/notes/folders', methods=['DELETE']) +@login_required +@company_required +def api_delete_folder(): + """Delete an empty folder""" + folder_path = request.args.get('path', '').strip() + + if not folder_path: + return jsonify({'success': False, 'message': 'Folder path is required'}), 400 + + # Check if folder has any notes + notes_in_folder = Note.query.filter_by( + company_id=g.user.company_id, + folder=folder_path, + is_archived=False + ).all() + + if notes_in_folder: + return jsonify({'success': False, 'message': 'Cannot delete folder that contains notes'}), 400 + + # Check if folder has subfolders with notes + notes_in_subfolders = Note.query.filter( + Note.company_id == g.user.company_id, + Note.folder.like(f'{folder_path}/%'), + Note.is_archived == False + ).first() + + if notes_in_subfolders: + return jsonify({'success': False, 'message': 'Cannot delete folder that contains subfolders with notes'}), 400 + + # Since we don't have a separate folders table, we just return success + # The folder will disappear from the UI when there are no notes in it + + return jsonify({ + 'success': True, + 'message': 'Folder deleted successfully' + }) + + +@app.route('/api/notes//folder', methods=['PUT']) +@login_required +@company_required +def update_note_folder(slug): + """Update a note's folder via drag and drop""" + note = Note.query.filter_by(slug=slug, company_id=g.user.company_id).first_or_404() + + # Check permissions + if not note.can_user_edit(g.user): + return jsonify({'success': False, 'message': 'Permission denied'}), 403 + + data = request.get_json() + folder_path = data.get('folder', '').strip() + + try: + # Update the note's folder + note.folder = folder_path if folder_path else None + db.session.commit() + + return jsonify({ + 'success': True, + 'message': 'Note moved successfully', + 'folder': folder_path + }) + except Exception as e: + db.session.rollback() + logger.error(f"Error updating note folder: {str(e)}") + return jsonify({'success': False, 'message': 'Error updating note folder'}), 500 + + @app.route('/api/notes//link', methods=['POST']) @login_required @company_required diff --git a/migrate_db.py b/migrate_db.py index bc0728c..d80a97a 100644 --- a/migrate_db.py +++ b/migrate_db.py @@ -1280,7 +1280,21 @@ def migrate_postgresql_schema(): WHERE table_name = 'note' """)) - if not result.fetchone(): + if result.fetchone(): + # Table exists, check for folder column + result = db.session.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'note' AND column_name = 'folder' + """)) + + if not result.fetchone(): + print("Adding folder column to note table...") + db.session.execute(text("ALTER TABLE note ADD COLUMN folder VARCHAR(100)")) + db.session.execute(text("CREATE INDEX IF NOT EXISTS idx_note_folder ON note(folder)")) + db.session.commit() + print("Folder column added successfully!") + else: print("Creating note and note_link tables...") # Create NoteVisibility enum type @@ -1299,6 +1313,7 @@ def migrate_postgresql_schema(): content TEXT NOT NULL, slug VARCHAR(100) NOT NULL, visibility notevisibility NOT NULL DEFAULT 'Private', + folder VARCHAR(100), company_id INTEGER NOT NULL, created_by_id INTEGER NOT NULL, project_id INTEGER, @@ -1327,6 +1342,31 @@ def migrate_postgresql_schema(): ) """)) + # Check if note_folder table exists + result = db.session.execute(text(""" + SELECT table_name + FROM information_schema.tables + WHERE table_name = 'note_folder' + """)) + + if not result.fetchone(): + print("Creating note_folder table...") + db.session.execute(text(""" + CREATE TABLE note_folder ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + path VARCHAR(500) NOT NULL, + parent_path VARCHAR(500), + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by_id INTEGER NOT NULL, + company_id INTEGER NOT NULL, + FOREIGN KEY (created_by_id) REFERENCES "user" (id), + FOREIGN KEY (company_id) REFERENCES company (id), + CONSTRAINT uq_folder_path_company UNIQUE (path, company_id) + ) + """)) + # Create indexes db.session.execute(text("CREATE INDEX idx_note_company ON note(company_id)")) db.session.execute(text("CREATE INDEX idx_note_created_by ON note(created_by_id)")) @@ -1334,11 +1374,23 @@ def migrate_postgresql_schema(): db.session.execute(text("CREATE INDEX idx_note_task ON note(task_id)")) db.session.execute(text("CREATE INDEX idx_note_slug ON note(company_id, slug)")) db.session.execute(text("CREATE INDEX idx_note_visibility ON note(visibility)")) - db.session.execute(text("CREATE INDEX idx_note_archived ON note(archived)")) + db.session.execute(text("CREATE INDEX idx_note_archived ON note(is_archived)")) db.session.execute(text("CREATE INDEX idx_note_created_at ON note(created_at DESC)")) + db.session.execute(text("CREATE INDEX idx_note_folder ON note(folder)")) db.session.execute(text("CREATE INDEX idx_note_link_source ON note_link(source_note_id)")) db.session.execute(text("CREATE INDEX idx_note_link_target ON note_link(target_note_id)")) + # Create indexes for note_folder if table was created + result = db.session.execute(text(""" + SELECT table_name + FROM information_schema.tables + WHERE table_name = 'note_folder' + """)) + if result.fetchone(): + db.session.execute(text("CREATE INDEX IF NOT EXISTS idx_note_folder_company ON note_folder(company_id)")) + db.session.execute(text("CREATE INDEX IF NOT EXISTS idx_note_folder_parent_path ON note_folder(parent_path)")) + db.session.execute(text("CREATE INDEX IF NOT EXISTS idx_note_folder_created_by ON note_folder(created_by_id)")) + db.session.commit() print("PostgreSQL schema migration completed successfully!") @@ -1568,7 +1620,46 @@ def migrate_notes_system(db_file=None): # Check if note table already exists cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='note'") if cursor.fetchone(): - print("Note table already exists. Skipping migration.") + print("Note table already exists. Checking for updates...") + + # Check if folder column exists + cursor.execute("PRAGMA table_info(note)") + columns = [column[1] for column in cursor.fetchall()] + + if 'folder' not in columns: + print("Adding folder column to note table...") + cursor.execute("ALTER TABLE note ADD COLUMN folder VARCHAR(100)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_note_folder ON note(folder)") + conn.commit() + print("Folder column added successfully!") + + # Check if note_folder table exists + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='note_folder'") + if not cursor.fetchone(): + print("Creating note_folder table...") + cursor.execute(""" + CREATE TABLE note_folder ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + path VARCHAR(500) NOT NULL, + parent_path VARCHAR(500), + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by_id INTEGER NOT NULL, + company_id INTEGER NOT NULL, + FOREIGN KEY (created_by_id) REFERENCES user(id), + FOREIGN KEY (company_id) REFERENCES company(id), + UNIQUE(path, company_id) + ) + """) + + # Create indexes for note_folder + cursor.execute("CREATE INDEX idx_note_folder_company ON note_folder(company_id)") + cursor.execute("CREATE INDEX idx_note_folder_parent_path ON note_folder(parent_path)") + cursor.execute("CREATE INDEX idx_note_folder_created_by ON note_folder(created_by_id)") + conn.commit() + print("Note folder table created successfully!") + return True print("Creating Notes system tables...") @@ -1581,6 +1672,7 @@ def migrate_notes_system(db_file=None): content TEXT NOT NULL, slug VARCHAR(100) NOT NULL, visibility VARCHAR(20) NOT NULL DEFAULT 'Private', + folder VARCHAR(100), company_id INTEGER NOT NULL, created_by_id INTEGER NOT NULL, project_id INTEGER, @@ -1625,6 +1717,28 @@ def migrate_notes_system(db_file=None): # Create indexes for note links cursor.execute("CREATE INDEX idx_note_link_source ON note_link(source_note_id)") cursor.execute("CREATE INDEX idx_note_link_target ON note_link(target_note_id)") + + # Create note_folder table + cursor.execute(""" + CREATE TABLE note_folder ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + path VARCHAR(500) NOT NULL, + parent_path VARCHAR(500), + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by_id INTEGER NOT NULL, + company_id INTEGER NOT NULL, + FOREIGN KEY (created_by_id) REFERENCES user(id), + FOREIGN KEY (company_id) REFERENCES company(id), + UNIQUE(path, company_id) + ) + """) + + # Create indexes for note_folder + cursor.execute("CREATE INDEX idx_note_folder_company ON note_folder(company_id)") + cursor.execute("CREATE INDEX idx_note_folder_parent_path ON note_folder(parent_path)") + cursor.execute("CREATE INDEX idx_note_folder_created_by ON note_folder(created_by_id)") conn.commit() print("Notes system migration completed successfully!") diff --git a/models.py b/models.py index 1c87c0c..61c232f 100644 --- a/models.py +++ b/models.py @@ -1261,6 +1261,9 @@ class Note(db.Model): # Visibility and sharing visibility = db.Column(db.Enum(NoteVisibility), nullable=False, default=NoteVisibility.PRIVATE) + # Folder organization + folder = db.Column(db.String(100), nullable=True) # Folder path like "Work/Projects" or "Personal" + # Metadata created_at = db.Column(db.DateTime, default=datetime.now) updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) @@ -1417,4 +1420,30 @@ class NoteLink(db.Model): __table_args__ = (db.UniqueConstraint('source_note_id', 'target_note_id', name='uq_note_link'),) def __repr__(self): - return f' {self.target_note_id}>' \ No newline at end of file + return f' {self.target_note_id}>' + + +class NoteFolder(db.Model): + """Represents a folder for organizing notes""" + id = db.Column(db.Integer, primary_key=True) + + # Folder properties + name = db.Column(db.String(100), nullable=False) + path = db.Column(db.String(500), nullable=False) # Full path like "Work/Projects/Q1" + parent_path = db.Column(db.String(500), nullable=True) # Parent folder path + description = db.Column(db.Text, nullable=True) + + # Metadata + created_at = db.Column(db.DateTime, default=datetime.now) + created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False) + + # Relationships + created_by = db.relationship('User', foreign_keys=[created_by_id]) + company = db.relationship('Company', foreign_keys=[company_id]) + + # Unique constraint to prevent duplicate paths within a company + __table_args__ = (db.UniqueConstraint('path', 'company_id', name='uq_folder_path_company'),) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/templates/note_editor.html b/templates/note_editor.html index 623cff8..05f7555 100644 --- a/templates/note_editor.html +++ b/templates/note_editor.html @@ -59,6 +59,19 @@ +
+ + + + {% for folder in all_folders %} + +
+
+
+

Note Folders

+
+ + Back to Notes +
+
+ +
+ +
+

Folder Structure

+
+ {{ render_folder_tree(folder_tree)|safe }} +
+
+ + +
+
+

Select a folder to view details

+
+
+
+
+ + + + + + + + +{% endblock %} + +{% macro render_folder_tree(tree, level=0) %} + {% for folder, children in tree.items() %} +
+
+ {% if children %} + + {% endif %} + 📁 + {{ folder.split('/')[-1] }} + ({{ folder_counts.get(folder, 0) }}) +
+ {% if children %} +
+ {{ render_folder_tree(children, level + 1)|safe }} +
+ {% endif %} +
+ {% endfor %} +{% endmacro %} \ No newline at end of file diff --git a/templates/notes_list.html b/templates/notes_list.html index cc56834..3e6f4b6 100644 --- a/templates/notes_list.html +++ b/templates/notes_list.html @@ -5,14 +5,60 @@

Notes

+ + + + ⚙️ Manage Folders + Create New Note
+ +
+ +
+ +
+
+
+ 🏠 + All Notes + ({{ notes|length }}) +
+
+ {{ render_folder_tree(folder_tree)|safe }} +
+
+ + +
+
+ + +
-
- - -
{% if notes %} -
- {% for note in notes %} -
-
-

- {{ note.title }} -

-
- - {% if note.visibility.value == 'Private' %}🔒{% elif note.visibility.value == 'Team' %}👥{% else %}🏢{% endif %} - {{ note.visibility.value }} - - {% if note.updated_at > note.created_at %} - Updated {{ note.updated_at|format_date }} - {% else %} - Created {{ note.created_at|format_date }} - {% endif %} -
-
- -
- {{ note.get_preview()|safe }} -
- - - -
- View - {% if note.can_user_edit(g.user) %} - Edit - - - - {% endif %} -
-
- {% endfor %} + +
+ + + + + + + + + + + + + + {% for note in notes %} + + + + + + + + + + {% endfor %} + +
TitleFolderVisibilityTagsUpdatedActions
+ {% if note.is_pinned %} + 📌 + {% endif %} + + + {{ note.title }} + +
+ {% if note.project %} + + 📁 {{ note.project.code }} + + {% endif %} + {% if note.task %} + + ✓ #{{ note.task.id }} + + {% endif %} +
+
+ {% if note.folder %} + {{ note.folder }} + {% else %} + Root + {% endif %} + + + {% if note.visibility.value == 'Private' %}🔒{% elif note.visibility.value == 'Team' %}👥{% else %}🏢{% endif %} + {{ note.visibility.value }} + + + {% if note.tags %} + {% for tag in note.get_tags_list() %} + {{ tag }} + {% endfor %} + {% endif %} + + {{ note.updated_at|format_date }} + +
+ View + {% if note.can_user_edit(g.user) %} + Edit +
+ +
+ {% endif %} +
+
+
+ + + - {% else %}

No notes found. Create your first note.

{% endif %} -
+
+
+
-{% endblock %} \ No newline at end of file + + +{% endblock %} + +{% macro render_folder_tree(tree, level=0) %} + {% for folder, children in tree.items() %} +
+
+ {% if children %} + + {% endif %} + 📁 + {{ folder.split('/')[-1] }} + ({{ folder_counts.get(folder, 0) }}) +
+ {% if children %} +
+ {{ render_folder_tree(children, level + 1)|safe }} +
+ {% endif %} +
+ {% endfor %} +{% endmacro %} \ No newline at end of file