This commit is contained in:
pika 2025-04-14 23:33:50 +02:00
parent 02582c6b06
commit 5473beb35d
7 changed files with 774 additions and 110 deletions

View file

@ -43,6 +43,59 @@
};
</script>
<!-- Custom Styles -->
<style>
/* Fix for sidebar tree view */
.sidebar-hidden .sidebar {
transform: translateX(-100%);
}
.sidebar-hidden main {
margin-left: 0;
}
/* Category tree styling */
#category-tree {
margin-left: 0;
}
.category-item > div {
padding: 4px 0;
border-radius: 4px;
}
.document-item {
position: relative;
}
.document-item a {
border-radius: 4px;
display: block;
}
.category-item .actions,
.document-item .actions {
right: 0;
top: 50%;
transform: translateY(-50%);
background-color: #282a36;
padding: 0 4px;
border-radius: 4px;
z-index: 10;
}
/* Drag and drop styles */
.drop-target {
background-color: rgba(80, 250, 123, 0.15);
border-radius: 4px;
outline: 1px dashed #50fa7b;
}
.dragging {
opacity: 0.5;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body class="bg-gray-900 text-gray-300 min-h-screen font-sans">
@ -215,16 +268,34 @@
function createCategoryItem(category) {
const li = document.createElement('li');
li.className = 'category-item my-1';
li.dataset.categoryId = category.id;
// Create the category item container with flexbox to place the + icon
const categoryContainer = document.createElement('div');
categoryContainer.className = 'flex items-center justify-between group';
categoryContainer.className = 'flex items-center group relative';
li.appendChild(categoryContainer);
// Add toggle button for collapsing/expanding if has children
const hasChildren = category.children && category.children.length > 0;
const hasDocuments = category.documents && category.documents.length > 0;
const isExpandable = hasChildren || hasDocuments;
// Add category toggle/expand button
const toggleButton = document.createElement('button');
toggleButton.className = 'w-4 text-gray-500 hover:text-primary flex-shrink-0';
// Use simpler conditional to avoid linter issues with template strings in JS
if (isExpandable) {
toggleButton.innerHTML = '<i class="mdi mdi-chevron-right text-sm"></i>';
} else {
toggleButton.innerHTML = '<i class="mdi mdi-chevron-right text-sm opacity-0"></i>';
}
categoryContainer.appendChild(toggleButton);
// Create the link to view the category
const a = document.createElement('a');
a.href = `/category/${category.id}`;
let categoryClass = 'flex-grow 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 overflow-hidden';
// Special styling for root
if (category.is_root) {
@ -235,72 +306,109 @@
// 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}`;
a.innerHTML = `<i class="mdi ${iconClass} mr-2 text-sm"></i> <span class="truncate">${category.name}</span>`;
categoryContainer.appendChild(a);
// Create the dropdown menu container
const dropdownContainer = document.createElement('div');
dropdownContainer.className = 'relative';
categoryContainer.appendChild(dropdownContainer);
// Create actions container that appears on hover
const actionsContainer = document.createElement('div');
actionsContainer.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800 px-1';
categoryContainer.appendChild(actionsContainer);
// 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) {
// Add document button - consistent across all views
const newDocButton = document.createElement('button');
newDocButton.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
newDocButton.title = 'Add Document';
newDocButton.innerHTML = '<i class="mdi mdi-file-plus-outline text-sm"></i>';
newDocButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
window.location.href = `/document/new?category=${category.id}`;
});
actionsContainer.appendChild(newDocButton);
// Add subcategory button - consistent across all views
const newSubfolderButton = document.createElement('button');
newSubfolderButton.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
newSubfolderButton.title = 'Add Sub-category';
newSubfolderButton.innerHTML = '<i class="mdi mdi-folder-plus-outline text-sm"></i>';
newSubfolderButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
window.location.href = `/category/new?parent_id=${category.id}`;
});
actionsContainer.appendChild(newSubfolderButton);
// Edit category button - consistent across all views
const editButton = document.createElement('button');
editButton.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
editButton.title = 'Edit Category';
editButton.innerHTML = '<i class="mdi mdi-pencil-outline text-sm"></i>';
editButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
window.location.href = `/category/${category.id}/edit`;
});
actionsContainer.appendChild(editButton);
// Create the child container for documents and subcategories
const childrenContainer = document.createElement('div');
childrenContainer.className = 'ml-2 pl-2 border-l border-gray-700 mt-1 mb-1 overflow-hidden transition-all duration-300';
childrenContainer.style.display = 'none'; // Initially collapsed
li.appendChild(childrenContainer);
// Toggle button functionality
toggleButton.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');
// Toggle visibility
const isCollapsed = childrenContainer.style.display === 'none';
childrenContainer.style.display = isCollapsed ? 'block' : 'none';
// Update icon - simpler approach to avoid linter issues
if (isCollapsed) {
toggleButton.innerHTML = '<i class="mdi mdi-chevron-down text-sm"></i>';
} else {
toggleButton.innerHTML = '<i class="mdi mdi-chevron-right text-sm"></i>';
}
// Save the category expanded state in localStorage
const expandedCategories = JSON.parse(localStorage.getItem('expandedCategories') || '[]');
const categoryId = category.id.toString();
if (isCollapsed) {
// Add to expanded list if not already there
if (!expandedCategories.includes(categoryId)) {
expandedCategories.push(categoryId);
}
} else {
// Remove from expanded list
const index = expandedCategories.indexOf(categoryId);
if (index > -1) {
expandedCategories.splice(index, 1);
}
}
localStorage.setItem('expandedCategories', JSON.stringify(expandedCategories));
});
// Initialize expanded state from localStorage if needed
const expandedCategories = JSON.parse(localStorage.getItem('expandedCategories') || '[]');
if (expandedCategories.includes(category.id.toString())) {
childrenContainer.style.display = 'block';
toggleButton.innerHTML = '<i class="mdi mdi-chevron-down text-sm"></i>';
}
// 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';
if (isExpandable) {
// Add documents first
if (category.documents && category.documents.length > 0) {
if (hasDocuments) {
const documentsUl = document.createElement('ul');
documentsUl.className = 'py-1 space-y-1';
childrenContainer.appendChild(documentsUl);
// Sort documents by name
const docs = [...category.documents];
docs.sort((a, b) => a.title.localeCompare(b.title));
// Add document items
docs.forEach(docId => {
@ -309,10 +417,12 @@
.then(response => response.json())
.then(doc => {
const docLi = document.createElement('li');
docLi.className = 'document-item relative group';
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}`;
docLink.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors truncate';
docLink.innerHTML = `<i class="mdi mdi-file-document-outline mr-2 text-sm"></i> <span class="truncate">${doc.title}</span>`;
// Add drag functionality
docLink.draggable = true;
@ -323,69 +433,93 @@
id: doc.id,
title: doc.title
}));
docLi.classList.add('dragging');
});
docLink.addEventListener('dragend', function() {
docLi.classList.remove('dragging');
});
// Add actions for documents
const docActions = document.createElement('div');
docActions.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800 px-1';
// Edit doc button
const editDocButton = document.createElement('button');
editDocButton.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
editDocButton.title = 'Edit Document';
editDocButton.innerHTML = '<i class="mdi mdi-pencil-outline text-sm"></i>';
editDocButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
window.location.href = `/document/${doc.id}/edit`;
});
docActions.appendChild(editDocButton);
docLi.appendChild(docLink);
childrenUl.appendChild(docLi);
docLi.appendChild(docActions);
documentsUl.appendChild(docLi);
})
.catch(error => console.error('Error fetching document:', error));
});
}
// Add child categories after documents
if (category.children && category.children.length > 0) {
if (hasChildren) {
const categoriesUl = document.createElement('ul');
categoriesUl.className = 'space-y-1 mt-1';
childrenContainer.appendChild(categoriesUl);
category.children.forEach(child => {
childrenUl.appendChild(createCategoryItem(child));
categoriesUl.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);
}
// Add drop capability to the category
li.addEventListener('dragover', function(e) {
e.preventDefault();
li.classList.add('drop-target');
});
li.addEventListener('dragleave', function() {
li.classList.remove('drop-target');
});
li.addEventListener('drop', function(e) {
e.preventDefault();
li.classList.remove('drop-target');
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);
}
});
return li;
}