wip
This commit is contained in:
parent
627f805377
commit
02582c6b06
4 changed files with 336 additions and 12 deletions
Binary file not shown.
|
@ -10,6 +10,36 @@ from sqlalchemy import or_
|
|||
|
||||
main = Blueprint('main', __name__)
|
||||
|
||||
# Helper function to convert categories to a hierarchical dictionary
|
||||
def build_category_hierarchy(categories):
|
||||
categories_dict = []
|
||||
|
||||
# Start with root categories (parent_id is None)
|
||||
root_categories = [c for c in categories if c.parent_id is None]
|
||||
|
||||
# Helper function to convert category and its children to dict
|
||||
def category_to_dict(category):
|
||||
cat_dict = {
|
||||
'id': category.id,
|
||||
'name': category.name,
|
||||
'icon': category.icon,
|
||||
'is_root': getattr(category, 'is_root', False),
|
||||
'parent_id': category.parent_id,
|
||||
'children': []
|
||||
}
|
||||
|
||||
# Find and add children
|
||||
for child in [c for c in categories if c.parent_id == category.id]:
|
||||
cat_dict['children'].append(category_to_dict(child))
|
||||
|
||||
return cat_dict
|
||||
|
||||
# Convert all root categories and their children
|
||||
for category in root_categories:
|
||||
categories_dict.append(category_to_dict(category))
|
||||
|
||||
return categories_dict
|
||||
|
||||
@main.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
|
@ -46,7 +76,11 @@ def edit_document(doc_id):
|
|||
document = Document.query.filter_by(id=doc_id, user_id=current_user.id).first_or_404()
|
||||
categories = Category.query.filter_by(user_id=current_user.id).all()
|
||||
tags = Tag.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('document_edit.html', document=document, categories=categories, tags=tags)
|
||||
|
||||
# Convert categories to dictionaries for JSON serialization
|
||||
categories_dict = build_category_hierarchy(categories)
|
||||
|
||||
return render_template('document_edit.html', document=document, categories=categories_dict, tags=tags)
|
||||
|
||||
@main.route('/document/new', methods=['GET'])
|
||||
@login_required
|
||||
|
@ -79,12 +113,10 @@ def new_document():
|
|||
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())
|
||||
# Convert categories to dictionaries for JSON serialization
|
||||
categories_dict = build_category_hierarchy(categories)
|
||||
|
||||
return render_template('document_edit.html', document=document, categories=categories, tags=tags, preselected_category_id=preselected_category_id)
|
||||
return render_template('document_edit.html', document=document, categories=categories_dict, tags=tags, preselected_category_id=preselected_category_id)
|
||||
|
||||
@main.route('/api/document', methods=['POST'])
|
||||
@login_required
|
||||
|
@ -147,6 +179,22 @@ def delete_document(doc_id):
|
|||
db.session.commit()
|
||||
return jsonify({'success': True})
|
||||
|
||||
@main.route('/api/document/<int:doc_id>', methods=['PATCH'])
|
||||
@login_required
|
||||
def update_document(doc_id):
|
||||
"""Update a document's properties (like category)"""
|
||||
document = Document.query.filter_by(id=doc_id, user_id=current_user.id).first_or_404()
|
||||
data = request.json
|
||||
|
||||
# Update category if provided
|
||||
if 'category_id' in data and data['category_id']:
|
||||
# Verify the category exists and belongs to user
|
||||
category = Category.query.filter_by(id=data['category_id'], user_id=current_user.id).first_or_404()
|
||||
document.category_id = category.id
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(document.to_dict())
|
||||
|
||||
@main.route('/api/category', methods=['POST'])
|
||||
@login_required
|
||||
def save_category():
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>{% block title %}Vim Docs{% endblock %}</title>
|
||||
|
||||
<!-- Material Design Icons -->
|
||||
|
@ -288,13 +289,98 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Add children
|
||||
if (category.children && category.children.length > 0) {
|
||||
// If this category has documents or child categories, add them
|
||||
if ((category.documents && category.documents.length > 0) ||
|
||||
(category.children && category.children.length > 0)) {
|
||||
|
||||
const childrenUl = document.createElement('ul');
|
||||
childrenUl.className = 'ml-2 pl-2 border-l border-gray-700 my-1';
|
||||
|
||||
category.children.forEach(child => {
|
||||
childrenUl.appendChild(createCategoryItem(child));
|
||||
// Add documents first
|
||||
if (category.documents && category.documents.length > 0) {
|
||||
// Sort documents by name
|
||||
const docs = [...category.documents];
|
||||
docs.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
// Add document items
|
||||
docs.forEach(docId => {
|
||||
// We need to fetch the document title since we only have IDs
|
||||
fetch(`/api/document/${docId}`)
|
||||
.then(response => response.json())
|
||||
.then(doc => {
|
||||
const docLi = document.createElement('li');
|
||||
const docLink = document.createElement('a');
|
||||
docLink.href = `/document/${doc.id}`;
|
||||
docLink.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors';
|
||||
docLink.innerHTML = `<i class="mdi mdi-file-document-outline mr-2 text-sm"></i> ${doc.title}`;
|
||||
|
||||
// Add drag functionality
|
||||
docLink.draggable = true;
|
||||
docLink.dataset.docId = doc.id;
|
||||
docLink.addEventListener('dragstart', function(e) {
|
||||
e.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'document',
|
||||
id: doc.id,
|
||||
title: doc.title
|
||||
}));
|
||||
});
|
||||
|
||||
docLi.appendChild(docLink);
|
||||
childrenUl.appendChild(docLi);
|
||||
})
|
||||
.catch(error => console.error('Error fetching document:', error));
|
||||
});
|
||||
}
|
||||
|
||||
// Add child categories after documents
|
||||
if (category.children && category.children.length > 0) {
|
||||
category.children.forEach(child => {
|
||||
childrenUl.appendChild(createCategoryItem(child));
|
||||
});
|
||||
}
|
||||
|
||||
// Add drop capability to the category
|
||||
li.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
li.classList.add('bg-gray-700/30'); // Visual feedback
|
||||
});
|
||||
|
||||
li.addEventListener('dragleave', function() {
|
||||
li.classList.remove('bg-gray-700/30');
|
||||
});
|
||||
|
||||
li.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
li.classList.remove('bg-gray-700/30');
|
||||
|
||||
try {
|
||||
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
|
||||
|
||||
if (data.type === 'document') {
|
||||
// Handle document drop - move to this category
|
||||
fetch(`/api/document/${data.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
category_id: category.id
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// Show success notification
|
||||
showNotification(`Moved "${data.title}" to ${category.name}`);
|
||||
// Refresh the category tree
|
||||
loadCategories();
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error moving document:', error));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing drop:', error);
|
||||
}
|
||||
});
|
||||
|
||||
li.appendChild(childrenUl);
|
||||
|
@ -303,6 +389,25 @@
|
|||
return li;
|
||||
}
|
||||
|
||||
// Helper function to show notifications
|
||||
function showNotification(message) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'fixed bottom-4 right-4 bg-primary/90 text-white px-4 py-2 rounded-md shadow-lg transform translate-y-0 opacity-100 transition-all duration-300 flex items-center';
|
||||
notification.innerHTML = `<i class="mdi mdi-check-circle mr-2"></i><span>${message}</span>`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-y-16', 'opacity-0');
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Helper function to get CSRF token from meta tag
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
}
|
||||
|
||||
function setupSearch() {
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
|
|
|
@ -22,8 +22,54 @@
|
|||
|
||||
<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">
|
||||
<div class="relative">
|
||||
<div class="flex items-center">
|
||||
<div class="icon-preview w-10 h-10 flex items-center justify-center bg-gray-700 rounded-md mr-3">
|
||||
<i id="icon-preview" class="mdi mdi-folder-outline text-xl"></i>
|
||||
</div>
|
||||
<input type="text" id="category-icon" value="{% if category %}{{ category.icon }}{% else %}mdi-folder-outline{% endif %}"
|
||||
class="flex-1 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">
|
||||
<button type="button" id="icon-search-btn" class="ml-2 px-3 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-500">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Icon Search Modal -->
|
||||
<div id="icon-search-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
|
||||
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div class="absolute inset-0 bg-gray-900 opacity-75"></div>
|
||||
</div>
|
||||
<div class="inline-block align-bottom bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
|
||||
<div class="bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-white mb-4">
|
||||
Select an Icon
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<!-- Search Input -->
|
||||
<div class="mb-4">
|
||||
<input type="text" id="icon-search-input" placeholder="Search icons..."
|
||||
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>
|
||||
|
||||
<!-- Icons Grid -->
|
||||
<div id="icons-grid" class="grid grid-cols-6 gap-2 max-h-96 overflow-y-auto">
|
||||
<!-- Icons will be populated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" id="close-icon-modal" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-600 shadow-sm px-4 py-2 bg-gray-700 text-white hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-1">Example: mdi-folder-outline, mdi-code-tags, mdi-book-open-page-variant</p>
|
||||
</div>
|
||||
|
||||
|
@ -188,6 +234,131 @@
|
|||
alert('Error saving category: ' + error.message);
|
||||
});
|
||||
});
|
||||
|
||||
// Icon Preview Functionality
|
||||
const iconPreview = document.getElementById('icon-preview');
|
||||
|
||||
// Update icon preview when input changes
|
||||
function updateIconPreview() {
|
||||
const iconClass = iconInput.value.trim();
|
||||
|
||||
// Remove all mdi classes
|
||||
iconPreview.className = '';
|
||||
|
||||
// Add the mdi base class and the selected icon class
|
||||
iconPreview.classList.add('mdi');
|
||||
|
||||
// If the value doesn't start with mdi-, add it
|
||||
if (iconClass.startsWith('mdi-')) {
|
||||
iconPreview.classList.add(iconClass);
|
||||
} else if (iconClass) {
|
||||
iconPreview.classList.add('mdi-' + iconClass.replace('mdi-', ''));
|
||||
} else {
|
||||
iconPreview.classList.add('mdi-folder-outline');
|
||||
}
|
||||
}
|
||||
|
||||
// Initial preview
|
||||
updateIconPreview();
|
||||
|
||||
// Update preview on input change
|
||||
iconInput.addEventListener('input', updateIconPreview);
|
||||
|
||||
// Icon Search Modal
|
||||
const iconSearchBtn = document.getElementById('icon-search-btn');
|
||||
const iconSearchModal = document.getElementById('icon-search-modal');
|
||||
const closeIconModal = document.getElementById('close-icon-modal');
|
||||
const iconSearchInput = document.getElementById('icon-search-input');
|
||||
const iconsGrid = document.getElementById('icons-grid');
|
||||
|
||||
// Common Material Design Icons
|
||||
const commonIcons = [
|
||||
'mdi-folder-outline', 'mdi-folder', 'mdi-folder-open', 'mdi-folder-plus',
|
||||
'mdi-file-document-outline', 'mdi-file-document', 'mdi-file',
|
||||
'mdi-note-outline', 'mdi-note', 'mdi-notebook', 'mdi-notebook-outline',
|
||||
'mdi-code-tags', 'mdi-code-braces', 'mdi-console', 'mdi-database',
|
||||
'mdi-server', 'mdi-desktop-tower', 'mdi-monitor', 'mdi-laptop',
|
||||
'mdi-home', 'mdi-account', 'mdi-cog', 'mdi-star', 'mdi-heart',
|
||||
'mdi-book', 'mdi-book-open', 'mdi-bookshelf', 'mdi-bookmark',
|
||||
'mdi-alert', 'mdi-information', 'mdi-help-circle', 'mdi-check-circle',
|
||||
'mdi-tools', 'mdi-wrench', 'mdi-hammer', 'mdi-screwdriver',
|
||||
'mdi-shield', 'mdi-lock', 'mdi-key', 'mdi-wifi', 'mdi-web',
|
||||
'mdi-link', 'mdi-github', 'mdi-git', 'mdi-docker', 'mdi-language-python',
|
||||
'mdi-language-javascript', 'mdi-language-html5', 'mdi-language-css3',
|
||||
'mdi-chart-bar', 'mdi-chart-line', 'mdi-chart-pie', 'mdi-chart-bubble',
|
||||
'mdi-movie', 'mdi-music', 'mdi-image', 'mdi-camera', 'mdi-video',
|
||||
'mdi-atom', 'mdi-microscope', 'mdi-flask', 'mdi-test-tube',
|
||||
'mdi-robot', 'mdi-brain', 'mdi-bug', 'mdi-rocket', 'mdi-satellite',
|
||||
'mdi-car', 'mdi-airplane', 'mdi-train', 'mdi-bike', 'mdi-walk',
|
||||
'mdi-food', 'mdi-coffee', 'mdi-beer', 'mdi-pizza', 'mdi-cake',
|
||||
'mdi-currency-usd', 'mdi-cash', 'mdi-credit-card', 'mdi-bank',
|
||||
'mdi-message', 'mdi-email', 'mdi-chat', 'mdi-forum', 'mdi-comment',
|
||||
'mdi-calendar', 'mdi-clock', 'mdi-alarm', 'mdi-timer', 'mdi-watch',
|
||||
'mdi-weather-sunny', 'mdi-weather-night', 'mdi-weather-rainy', 'mdi-weather-snowy',
|
||||
'mdi-map', 'mdi-map-marker', 'mdi-compass', 'mdi-earth', 'mdi-directions',
|
||||
'mdi-shopping', 'mdi-cart', 'mdi-store', 'mdi-tag', 'mdi-sale',
|
||||
'mdi-palette', 'mdi-brush', 'mdi-format-paint', 'mdi-pencil', 'mdi-pen',
|
||||
'mdi-printer', 'mdi-scanner', 'mdi-fax', 'mdi-file-pdf', 'mdi-file-excel',
|
||||
'mdi-file-word', 'mdi-file-powerpoint', 'mdi-file-image', 'mdi-file-video',
|
||||
'mdi-file-music', 'mdi-file-xml', 'mdi-file-code', 'mdi-zip-box'
|
||||
];
|
||||
|
||||
// Populate the icons grid
|
||||
function populateIconsGrid(searchTerm = '') {
|
||||
iconsGrid.innerHTML = '';
|
||||
|
||||
const filteredIcons = searchTerm
|
||||
? commonIcons.filter(icon => icon.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
: commonIcons;
|
||||
|
||||
filteredIcons.forEach(icon => {
|
||||
const iconDiv = document.createElement('div');
|
||||
iconDiv.className = 'cursor-pointer p-2 bg-gray-700 rounded-md flex flex-col items-center hover:bg-gray-600';
|
||||
|
||||
const iconEl = document.createElement('i');
|
||||
iconEl.className = 'mdi ' + icon + ' text-2xl mb-1';
|
||||
|
||||
const iconName = document.createElement('div');
|
||||
iconName.className = 'text-xs text-gray-400 truncate w-full text-center';
|
||||
iconName.textContent = icon.replace('mdi-', '');
|
||||
|
||||
iconDiv.appendChild(iconEl);
|
||||
iconDiv.appendChild(iconName);
|
||||
|
||||
// Select icon when clicked
|
||||
iconDiv.addEventListener('click', function() {
|
||||
iconInput.value = icon;
|
||||
updateIconPreview();
|
||||
iconSearchModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
iconsGrid.appendChild(iconDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// Open modal
|
||||
iconSearchBtn.addEventListener('click', function() {
|
||||
populateIconsGrid();
|
||||
iconSearchModal.classList.remove('hidden');
|
||||
iconSearchInput.focus();
|
||||
});
|
||||
|
||||
// Close modal
|
||||
closeIconModal.addEventListener('click', function() {
|
||||
iconSearchModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Click outside to close
|
||||
iconSearchModal.addEventListener('click', function(e) {
|
||||
if (e.target === iconSearchModal) {
|
||||
iconSearchModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
iconSearchInput.addEventListener('input', function() {
|
||||
populateIconsGrid(this.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue