removed persistant data
This commit is contained in:
parent
345a801c40
commit
627f805377
10 changed files with 412 additions and 29 deletions
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
@ -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
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -45,9 +45,9 @@ def signup():
|
||||||
|
|
||||||
# Create root category for the user
|
# Create root category for the user
|
||||||
root_category = Category(
|
root_category = Category(
|
||||||
name='My Documents',
|
name='root',
|
||||||
icon='mdi-folder',
|
icon='mdi-folder-root',
|
||||||
description='Default document category',
|
description='System root directory',
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
is_root=True
|
is_root=True
|
||||||
)
|
)
|
||||||
|
|
|
@ -57,18 +57,34 @@ def new_document():
|
||||||
category_id = request.args.get('category')
|
category_id = request.args.get('category')
|
||||||
|
|
||||||
document = None
|
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 category_id:
|
||||||
|
# If a specific category was requested, use that
|
||||||
category = Category.query.filter_by(id=category_id, user_id=current_user.id).first()
|
category = Category.query.filter_by(id=category_id, user_id=current_user.id).first()
|
||||||
if category:
|
if category:
|
||||||
document = Document(
|
preselected_category_id = int(category_id)
|
||||||
title="Untitled Document",
|
elif root_category:
|
||||||
content="",
|
# Otherwise default to the root category
|
||||||
category_id=category_id,
|
preselected_category_id = root_category.id
|
||||||
user_id=current_user.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'])
|
@main.route('/api/document', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -76,22 +92,35 @@ def save_document():
|
||||||
"""Save a document (new or existing)"""
|
"""Save a document (new or existing)"""
|
||||||
data = request.json
|
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()
|
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']:
|
if 'id' in data and data['id']:
|
||||||
# Update existing document - verify ownership
|
# Update existing document - verify ownership
|
||||||
document = Document.query.filter_by(id=data['id'], user_id=current_user.id).first_or_404()
|
document = Document.query.filter_by(id=data['id'], user_id=current_user.id).first_or_404()
|
||||||
document.title = data['title']
|
document.title = data['title']
|
||||||
document.content = data['content']
|
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:
|
else:
|
||||||
# Create new document
|
# Create new document
|
||||||
document = Document(
|
document = Document(
|
||||||
title=data['title'],
|
title=data['title'],
|
||||||
content=data['content'],
|
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
|
user_id=current_user.id
|
||||||
)
|
)
|
||||||
db.session.add(document)
|
db.session.add(document)
|
||||||
|
@ -265,4 +294,23 @@ def get_categories():
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
parent_id=None
|
parent_id=None
|
||||||
).all()
|
).all()
|
||||||
return jsonify([category.to_dict() for category in root_categories])
|
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/<int:category_id>/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)
|
|
@ -215,12 +215,80 @@
|
||||||
function createCategoryItem(category) {
|
function createCategoryItem(category) {
|
||||||
const li = document.createElement('li');
|
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');
|
const a = document.createElement('a');
|
||||||
a.href = `/category/${category.id}`;
|
a.href = `/category/${category.id}`;
|
||||||
a.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors';
|
let categoryClass = 'flex-grow flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors';
|
||||||
a.innerHTML = `<i class="mdi ${category.icon} mr-2 text-sm"></i> ${category.name}`;
|
|
||||||
li.appendChild(a);
|
|
||||||
|
|
||||||
|
// 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 = `<i class="mdi ${iconClass} mr-2 text-sm"></i> ${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 = '<i class="mdi mdi-plus text-sm"></i>';
|
||||||
|
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 = '<i class="mdi mdi-folder-plus-outline mr-2"></i> 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 = '<i class="mdi mdi-file-plus-outline mr-2"></i> 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) {
|
if (category.children && category.children.length > 0) {
|
||||||
const childrenUl = document.createElement('ul');
|
const childrenUl = document.createElement('ul');
|
||||||
childrenUl.className = 'ml-2 pl-2 border-l border-gray-700 my-1';
|
childrenUl.className = 'ml-2 pl-2 border-l border-gray-700 my-1';
|
||||||
|
|
193
app/templates/category_edit.html
Normal file
193
app/templates/category_edit.html
Normal file
|
@ -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 %}
|
||||||
|
<button id="save-category" class="inline-flex items-center px-4 py-2 bg-primary text-black rounded-md hover:bg-primary-dark transition-colors">
|
||||||
|
<i class="mdi mdi-content-save mr-2"></i> Save
|
||||||
|
</button>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-2xl mx-auto">
|
||||||
|
<div class="bg-gray-800 rounded-lg p-6 shadow-lg">
|
||||||
|
<div class="grid grid-cols-1 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="category-name" class="block text-sm font-medium text-gray-400 mb-1">Category Name *</label>
|
||||||
|
<input type="text" id="category-name" value="{% if category %}{{ category.name }}{% endif %}"
|
||||||
|
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="category-icon" class="block text-sm font-medium text-gray-400 mb-1">Icon (Material Design Icon)</label>
|
||||||
|
<input type="text" id="category-icon" value="{% if category %}{{ category.icon }}{% else %}mdi-folder-outline{% endif %}"
|
||||||
|
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Example: mdi-folder-outline, mdi-code-tags, mdi-book-open-page-variant</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="category-description" class="block text-sm font-medium text-gray-400 mb-1">Description</label>
|
||||||
|
<textarea id="category-description" rows="3"
|
||||||
|
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">{% if category %}{{ category.description }}{% endif %}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="category-parent" class="block text-sm font-medium text-gray-400 mb-1">Parent Category</label>
|
||||||
|
<select id="category-parent"
|
||||||
|
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">
|
||||||
|
<option value="">None (Root Category)</option>
|
||||||
|
<!-- Options will be populated with JavaScript -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Save notification -->
|
||||||
|
<div id="save-notification" class="fixed bottom-4 right-4 bg-green-500/90 text-white px-4 py-2 rounded-md shadow-lg transform translate-y-16 opacity-0 transition-all duration-300 flex items-center">
|
||||||
|
<i class="mdi mdi-check-circle mr-2"></i>
|
||||||
|
<span>Category saved successfully!</span>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const saveButton = document.getElementById('save-category');
|
||||||
|
const nameInput = document.getElementById('category-name');
|
||||||
|
const iconInput = document.getElementById('category-icon');
|
||||||
|
const descriptionInput = document.getElementById('category-description');
|
||||||
|
const parentSelect = document.getElementById('category-parent');
|
||||||
|
const notification = document.getElementById('save-notification');
|
||||||
|
|
||||||
|
// Load all categories for parent selection
|
||||||
|
loadCategoryOptions();
|
||||||
|
|
||||||
|
// Set default parent if specified
|
||||||
|
{% if parent %}
|
||||||
|
const parentId = {{ parent.id }};
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
function loadCategoryOptions() {
|
||||||
|
fetch('/api/categories')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(categories => {
|
||||||
|
// Find the root category
|
||||||
|
const rootCategory = categories.find(c => c.name === 'root');
|
||||||
|
|
||||||
|
// Add options recursively
|
||||||
|
function addCategoryOptions(categories, depth = 0) {
|
||||||
|
categories.forEach(category => {
|
||||||
|
// Skip the category being edited to avoid circular references
|
||||||
|
{% if category %}
|
||||||
|
if (category.id === {{ category.id }}) return;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = category.id;
|
||||||
|
|
||||||
|
// Add indentation using spaces for hierarchical display
|
||||||
|
const indent = '\u00A0\u00A0\u00A0\u00A0'.repeat(depth);
|
||||||
|
option.textContent = indent + (depth > 0 ? '└─ ' : '') + category.name;
|
||||||
|
|
||||||
|
// Select option logic:
|
||||||
|
// 1. If we have a parent specified, select that parent
|
||||||
|
// 2. If we're editing an existing category, select its current parent
|
||||||
|
// 3. If creating a new category, select root by default
|
||||||
|
let selectThisOption = false;
|
||||||
|
|
||||||
|
{% if parent %}
|
||||||
|
if (category.id === {{ parent.id }}) {
|
||||||
|
selectThisOption = true;
|
||||||
|
}
|
||||||
|
{% elif category and category.parent_id %}
|
||||||
|
if (category.id === {{ category.parent_id }}) {
|
||||||
|
selectThisOption = true;
|
||||||
|
}
|
||||||
|
{% else %}
|
||||||
|
// If no parent specified and creating new category, default to root
|
||||||
|
if (category.is_root && !rootSelected) {
|
||||||
|
selectThisOption = true;
|
||||||
|
rootSelected = true;
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
option.selected = selectThisOption;
|
||||||
|
parentSelect.appendChild(option);
|
||||||
|
|
||||||
|
if (category.children && category.children.length > 0) {
|
||||||
|
addCategoryOptions(category.children, depth + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize flag to track if root has been selected
|
||||||
|
let rootSelected = false;
|
||||||
|
|
||||||
|
// Start from root categories
|
||||||
|
addCategoryOptions(categories);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading categories:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveButton.addEventListener('click', function() {
|
||||||
|
const categoryData = {
|
||||||
|
name: nameInput.value.trim(),
|
||||||
|
icon: iconInput.value.trim() || 'mdi-folder-outline',
|
||||||
|
description: descriptionInput.value.trim(),
|
||||||
|
parent_id: parentSelect.value || null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!categoryData.name) {
|
||||||
|
alert('Please enter a category name');
|
||||||
|
nameInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% if category %}
|
||||||
|
categoryData.id = {{ category.id }};
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
fetch('/api/category', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token() }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(categoryData)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Server responded with an error: ' + response.status);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log("Category saved successfully:", data);
|
||||||
|
|
||||||
|
// Show save notification
|
||||||
|
notification.classList.remove('translate-y-16', 'opacity-0');
|
||||||
|
|
||||||
|
// Hide notification after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('translate-y-16', 'opacity-0');
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Redirect to category view
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = `/category/${data.id}`;
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error saving category:', error);
|
||||||
|
alert('Error saving category: ' + error.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -178,17 +178,7 @@
|
||||||
<div>
|
<div>
|
||||||
<label for="doc-category" class="block text-sm font-medium text-gray-400 mb-1">Category</label>
|
<label for="doc-category" class="block text-sm font-medium text-gray-400 mb-1">Category</label>
|
||||||
<select id="doc-category" class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">
|
<select id="doc-category" class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">
|
||||||
{% for category in categories %}
|
<!-- Categories will be populated via JavaScript -->
|
||||||
{% if category.is_root %}
|
|
||||||
<option value="{{ category.id }}" {% if (document and document.category_id == category.id) or (not document and (not preselected_category_id or preselected_category_id|int == category.id|int)) %}selected{% endif %}>
|
|
||||||
{{ category.name }} (Home)
|
|
||||||
</option>
|
|
||||||
{% else %}
|
|
||||||
<option value="{{ category.id }}" {% if (document and document.category_id == category.id) or (not document and preselected_category_id and preselected_category_id|int == category.id|int) %}selected{% endif %}>
|
|
||||||
{{ category.name }}
|
|
||||||
</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -315,9 +305,60 @@
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Populate categories dropdown with hierarchical structure
|
||||||
|
populateCategoriesDropdown();
|
||||||
|
|
||||||
// Initial preview
|
// Initial preview
|
||||||
updatePreview();
|
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
|
// Add debounce function for efficiency
|
||||||
function debounce(func, wait) {
|
function debounce(func, wait) {
|
||||||
let timeout;
|
let timeout;
|
||||||
|
|
BIN
instance/docs.db
BIN
instance/docs.db
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue