diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fc790f --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# tests and coverage +*.pytest_cache +.coverage + +# database & logs +*.db +*.sqlite3 +*.log + +# venv +env +venv + +# other +.DS_Store + +# sphinx docs +_build +_static +_templates + +# javascript +package-lock.json +.vscode/symbols.json + +apps/static/assets/node_modules +apps/static/assets/yarn.lock +apps/static/assets/.temp + diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 039e464..0000000 Binary files a/app/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/app/__pycache__/routes.cpython-313.pyc b/app/__pycache__/routes.cpython-313.pyc deleted file mode 100644 index e35c0ac..0000000 Binary files a/app/__pycache__/routes.cpython-313.pyc and /dev/null differ diff --git a/app/auth/__pycache__/routes.cpython-313.pyc b/app/auth/__pycache__/routes.cpython-313.pyc index e33ac2d..53ee881 100644 Binary files a/app/auth/__pycache__/routes.cpython-313.pyc and b/app/auth/__pycache__/routes.cpython-313.pyc differ diff --git a/app/auth/routes.py b/app/auth/routes.py index d4693da..8d833bf 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -45,9 +45,9 @@ def signup(): # Create root category for the user root_category = Category( - name='My Documents', - icon='mdi-folder', - description='Default document category', + name='root', + icon='mdi-folder-root', + description='System root directory', user_id=user.id, is_root=True ) diff --git a/app/routes.py b/app/routes.py index bbf63b6..dc61b53 100644 --- a/app/routes.py +++ b/app/routes.py @@ -57,18 +57,34 @@ def new_document(): category_id = request.args.get('category') document = None + preselected_category_id = None + + # Find the root category + root_category = Category.query.filter_by(user_id=current_user.id, is_root=True).first() if category_id: + # If a specific category was requested, use that category = Category.query.filter_by(id=category_id, user_id=current_user.id).first() if category: - document = Document( - title="Untitled Document", - content="", - category_id=category_id, - user_id=current_user.id - ) + preselected_category_id = int(category_id) + elif root_category: + # Otherwise default to the root category + preselected_category_id = root_category.id - return render_template('document_edit.html', document=document, categories=categories, tags=tags, preselected_category_id=category_id) + # Create a blank document + document = Document( + title="Untitled Document", + content="", + category_id=preselected_category_id, + user_id=current_user.id + ) + + # Make sure all categories include their children for the hierarchical dropdown + for category in categories: + if hasattr(category, 'to_dict'): + setattr(category, '_serialized', category.to_dict()) + + return render_template('document_edit.html', document=document, categories=categories, tags=tags, preselected_category_id=preselected_category_id) @main.route('/api/document', methods=['POST']) @login_required @@ -76,22 +92,35 @@ def save_document(): """Save a document (new or existing)""" data = request.json - # Get root category as default + # Find the root category to use as default root_category = Category.query.filter_by(user_id=current_user.id, is_root=True).first() - default_category_id = root_category.id if root_category else None + # Ensure we have a root category + if not root_category: + # Create a root category if it doesn't exist + root_category = Category( + name='root', + icon='mdi-folder-root', + description='System root directory', + user_id=current_user.id, + is_root=True + ) + db.session.add(root_category) + db.session.flush() # Get the ID without committing yet + + # All documents must have a category - defaults to root if 'id' in data and data['id']: # Update existing document - verify ownership document = Document.query.filter_by(id=data['id'], user_id=current_user.id).first_or_404() document.title = data['title'] document.content = data['content'] - document.category_id = data['category_id'] if data['category_id'] else default_category_id + document.category_id = data['category_id'] if data['category_id'] else root_category.id else: # Create new document document = Document( title=data['title'], content=data['content'], - category_id=data['category_id'] if data['category_id'] else default_category_id, + category_id=data['category_id'] if data['category_id'] else root_category.id, user_id=current_user.id ) db.session.add(document) @@ -265,4 +294,23 @@ def get_categories(): user_id=current_user.id, parent_id=None ).all() - return jsonify([category.to_dict() for category in root_categories]) \ No newline at end of file + return jsonify([category.to_dict() for category in root_categories]) + +@main.route('/category/new', methods=['GET']) +@login_required +def new_category(): + """Create a new category or subcategory""" + parent_id = request.args.get('parent_id') + parent = None + + if parent_id: + parent = Category.query.filter_by(id=parent_id, user_id=current_user.id).first_or_404() + + return render_template('category_edit.html', category=None, parent=parent) + +@main.route('/category//edit', methods=['GET']) +@login_required +def edit_category(category_id): + """Edit an existing category""" + category = Category.query.filter_by(id=category_id, user_id=current_user.id).first_or_404() + return render_template('category_edit.html', category=category, parent=category.parent) \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 4ffd79f..f773f9e 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -215,12 +215,80 @@ function createCategoryItem(category) { const li = document.createElement('li'); + // Create the category item container with flexbox to place the + icon + const categoryContainer = document.createElement('div'); + categoryContainer.className = 'flex items-center justify-between group'; + li.appendChild(categoryContainer); + + // Create the link to view the category const a = document.createElement('a'); a.href = `/category/${category.id}`; - a.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors'; - a.innerHTML = ` ${category.name}`; - li.appendChild(a); + let categoryClass = 'flex-grow flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors'; + // Special styling for root + if (category.is_root) { + categoryClass += ' font-semibold text-primary'; + } + + a.className = categoryClass; + + // Special icon for root + const iconClass = category.is_root ? 'mdi-folder-root' : category.icon; + a.innerHTML = ` ${category.name}`; + categoryContainer.appendChild(a); + + // Create the dropdown menu container + const dropdownContainer = document.createElement('div'); + dropdownContainer.className = 'relative'; + categoryContainer.appendChild(dropdownContainer); + + // Create the plus button + const plusButton = document.createElement('button'); + plusButton.className = 'ml-1 p-1 text-gray-500 hover:text-primary rounded-full opacity-0 group-hover:opacity-100 transition-opacity'; + plusButton.innerHTML = ''; + dropdownContainer.appendChild(plusButton); + + // Create dropdown menu + const dropdown = document.createElement('div'); + dropdown.className = 'absolute right-0 top-full mt-1 py-1 bg-gray-800 border border-gray-700 rounded-md shadow-lg z-20 hidden w-48'; + dropdownContainer.appendChild(dropdown); + + // Add menu items + const newSubcategory = document.createElement('a'); + newSubcategory.href = `/category/new?parent_id=${category.id}`; + newSubcategory.className = 'block px-4 py-2 text-sm text-gray-300 hover:bg-gray-700 hover:text-white w-full text-left'; + newSubcategory.innerHTML = ' New Subcategory'; + dropdown.appendChild(newSubcategory); + + const newDocument = document.createElement('a'); + newDocument.href = `/document/new?category=${category.id}`; + newDocument.className = 'block px-4 py-2 text-sm text-gray-300 hover:bg-gray-700 hover:text-white w-full text-left'; + newDocument.innerHTML = ' New Document'; + dropdown.appendChild(newDocument); + + // Toggle dropdown + plusButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + dropdown.classList.toggle('hidden'); + + // Close other open dropdowns + document.querySelectorAll('.category-dropdown:not(.hidden)').forEach(el => { + if (el !== dropdown) el.classList.add('hidden'); + }); + }); + + // Add class for easy reference + dropdown.classList.add('category-dropdown'); + + // Add click handler to close dropdown when clicking outside + document.addEventListener('click', function(e) { + if (!plusButton.contains(e.target) && !dropdown.contains(e.target)) { + dropdown.classList.add('hidden'); + } + }); + + // Add children if (category.children && category.children.length > 0) { const childrenUl = document.createElement('ul'); childrenUl.className = 'ml-2 pl-2 border-l border-gray-700 my-1'; diff --git a/app/templates/category_edit.html b/app/templates/category_edit.html new file mode 100644 index 0000000..fb93c97 --- /dev/null +++ b/app/templates/category_edit.html @@ -0,0 +1,193 @@ +{% extends "base.html" %} + +{% block title %}{% if category %}Edit Category{% else %}New Category{% endif %} - Vim Docs{% endblock %} + +{% block header_title %}{% if category %}Edit Category{% else %}New Category{% endif %}{% endblock %} + +{% block header_actions %} + +{% endblock %} + +{% block content %} +
+
+
+
+ + +
+ +
+ + +

Example: mdi-folder-outline, mdi-code-tags, mdi-book-open-page-variant

+
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
+ + Category saved successfully! +
+{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/document_edit.html b/app/templates/document_edit.html index dac0651..777ddf6 100644 --- a/app/templates/document_edit.html +++ b/app/templates/document_edit.html @@ -178,17 +178,7 @@
@@ -315,9 +305,60 @@ // Initialize document.addEventListener('DOMContentLoaded', function() { + // Populate categories dropdown with hierarchical structure + populateCategoriesDropdown(); + // Initial preview updatePreview(); + // Function to create hierarchical category dropdown + function populateCategoriesDropdown() { + const categorySelect = document.getElementById('doc-category'); + const categories = {{ categories|tojson|safe }}; + const preselectedId = {% if document and document.category_id %}{{ document.category_id }}{% elif preselected_category_id %}{{ preselected_category_id }}{% else %}null{% endif %}; + + // Find the root category + const rootCategory = categories.find(c => c.is_root); + let rootCategoryId = null; + if (rootCategory) { + rootCategoryId = rootCategory.id; + } + + function addCategoryOptions(categoryList, depth = 0) { + categoryList.forEach(category => { + const option = document.createElement('option'); + option.value = category.id; + + // Create indentation for hierarchy + const indent = '\u00A0\u00A0\u00A0\u00A0'.repeat(depth); + let prefix = ''; + if (depth > 0) { + prefix = '└─ '; + } + + option.textContent = indent + prefix + category.name; + + // Select this option if it matches the preselected ID + // Or if this is the root category and no preselection was made + option.selected = (category.id == preselectedId) || + (category.is_root && !preselectedId); + + categorySelect.appendChild(option); + + // Add children recursively if any + if (category.children && category.children.length > 0) { + addCategoryOptions(category.children, depth + 1); + } + }); + } + + // Get root categories and their children + const rootCategories = categories.filter(c => c.parent_id === null); + addCategoryOptions(rootCategories); + + console.log("Preselected category ID:", preselectedId || (rootCategoryId + " (root by default)")); + } + // Add debounce function for efficiency function debounce(func, wait) { let timeout; diff --git a/instance/docs.db b/instance/docs.db deleted file mode 100644 index a77b8df..0000000 Binary files a/instance/docs.db and /dev/null differ