wip
This commit is contained in:
parent
02582c6b06
commit
5473beb35d
7 changed files with 774 additions and 110 deletions
|
@ -159,12 +159,31 @@ def save_document():
|
|||
|
||||
# Handle tags
|
||||
if 'tags' in data:
|
||||
# Clear existing tags
|
||||
document.tags = []
|
||||
|
||||
# Process tags, preventing duplicates
|
||||
processed_tag_names = set()
|
||||
for tag_name in data['tags']:
|
||||
tag = Tag.query.filter_by(name=tag_name, user_id=current_user.id).first()
|
||||
# Skip if we've already added this tag (case insensitive)
|
||||
if tag_name.lower() in processed_tag_names:
|
||||
continue
|
||||
|
||||
# Add to our processed set to prevent duplicates in this request
|
||||
processed_tag_names.add(tag_name.lower())
|
||||
|
||||
# Check if tag already exists for this user
|
||||
tag = Tag.query.filter(
|
||||
Tag.name.ilike(tag_name),
|
||||
Tag.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not tag:
|
||||
# Create new tag
|
||||
tag = Tag(name=tag_name, user_id=current_user.id)
|
||||
db.session.add(tag)
|
||||
|
||||
# Add tag to document
|
||||
document.tags.append(tag)
|
||||
|
||||
db.session.commit()
|
||||
|
|
|
@ -134,12 +134,90 @@ button {
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Category tree in sidebar */
|
||||
.category-tree {
|
||||
margin-left: 15px;
|
||||
padding-left: 10px;
|
||||
border-left: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
/* Category item and nested items */
|
||||
.category-item {
|
||||
position: relative;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.category-item:hover > div {
|
||||
background-color: rgba(80, 250, 123, 0.05);
|
||||
}
|
||||
|
||||
.category-item .actions {
|
||||
z-index: 5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Document item styling */
|
||||
.document-item {
|
||||
position: relative;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.document-item:hover {
|
||||
background-color: rgba(80, 250, 123, 0.05);
|
||||
}
|
||||
|
||||
/* Animation for collapsing/expanding */
|
||||
.category-item .mdi-chevron-down,
|
||||
.category-item .mdi-chevron-right {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* Nested children container */
|
||||
.children-container {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hover effects for category items */
|
||||
.category-item:hover > .category-row {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Toggle button hover */
|
||||
.category-item button:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Document and category action buttons */
|
||||
.actions button {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.actions button:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Drag and drop styling */
|
||||
.category-item.drop-target {
|
||||
background-color: rgba(80, 250, 123, 0.15);
|
||||
outline: 1px dashed var(--primary-color);
|
||||
}
|
||||
|
||||
.document-item.dragging {
|
||||
opacity: 0.5;
|
||||
background-color: rgba(80, 250, 123, 0.1);
|
||||
}
|
||||
|
||||
/* Proper truncation for long category/document names */
|
||||
.truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
/* Main content area */
|
||||
.content {
|
||||
flex: 1;
|
||||
|
@ -688,4 +766,69 @@ button {
|
|||
.category-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Category list view */
|
||||
.active-view {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--bg-color);
|
||||
}
|
||||
|
||||
.category-list-item {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.category-list-item .toggle-btn .mdi {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.category-list-item .toggle-btn .rotate-90 {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.category-children {
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* Animation for sliding down content */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-down {
|
||||
animation: slideDown 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Hover effects for list items */
|
||||
.document-list-item:hover,
|
||||
.subcategory-list-item:hover {
|
||||
background-color: rgba(80, 250, 123, 0.05);
|
||||
}
|
||||
|
||||
/* Interactive hover effects */
|
||||
.category-list-item > div {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.category-list-item > div:hover {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
/* Document and subcategory items */
|
||||
.document-list-item,
|
||||
.subcategory-list-item {
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.document-list-item:hover a,
|
||||
.subcategory-list-item:hover a {
|
||||
color: var(--primary-color);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
<a href="{{ url_for('main.new_document') }}?category={{ category.id }}" 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-plus mr-2"></i> New Document
|
||||
</a>
|
||||
<a href="{{ url_for('main.new_category') }}?parent_id={{ category.id }}" class="inline-flex items-center px-4 py-2 bg-primary/80 text-black rounded-md hover:bg-primary-dark transition-colors ml-2">
|
||||
<i class="mdi mdi-folder-plus-outline mr-2"></i> New Subcategory
|
||||
</a>
|
||||
<button id="edit-category-btn" class="inline-flex items-center px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors ml-2">
|
||||
<i class="mdi mdi-pencil mr-2"></i> Edit Category
|
||||
</button>
|
||||
|
|
|
@ -89,6 +89,91 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if category %}
|
||||
<!-- Category Contents Section -->
|
||||
<div class="mt-8">
|
||||
<h3 class="text-lg font-medium text-white mb-4">Category Contents</h3>
|
||||
|
||||
<!-- Documents -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-md font-medium text-gray-300">Documents</h4>
|
||||
<a href="{{ url_for('main.new_document') }}?category={{ category.id }}" class="text-primary hover:text-primary-light text-sm flex items-center">
|
||||
<i class="mdi mdi-file-plus-outline mr-1"></i> Add Document
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 rounded-lg shadow">
|
||||
{% if category.documents.count() > 0 %}
|
||||
<ul class="divide-y divide-gray-700">
|
||||
{% for doc in category.documents %}
|
||||
<li class="p-3 hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="{{ url_for('main.view_document', doc_id=doc.id) }}" class="flex items-center text-gray-300 hover:text-primary">
|
||||
<i class="mdi mdi-file-document-outline mr-2"></i>
|
||||
<span>{{ doc.title }}</span>
|
||||
</a>
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="{{ url_for('main.edit_document', doc_id=doc.id) }}" class="text-gray-400 hover:text-primary" title="Edit">
|
||||
<i class="mdi mdi-pencil-outline"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('main.export_document', doc_id=doc.id) }}" class="text-gray-400 hover:text-primary" title="Export">
|
||||
<i class="mdi mdi-download-outline"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="p-4 text-center text-gray-500">
|
||||
No documents in this category
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subcategories -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-md font-medium text-gray-300">Subcategories</h4>
|
||||
<a href="{{ url_for('main.new_category') }}?parent_id={{ category.id }}" class="text-primary hover:text-primary-light text-sm flex items-center">
|
||||
<i class="mdi mdi-folder-plus-outline mr-1"></i> Add Subcategory
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 rounded-lg shadow">
|
||||
{% if category.children.count() > 0 %}
|
||||
<ul class="divide-y divide-gray-700">
|
||||
{% for subcategory in category.children %}
|
||||
<li class="p-3 hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="{{ url_for('main.view_category', category_id=subcategory.id) }}" class="flex items-center text-gray-300 hover:text-primary">
|
||||
<i class="mdi {{ subcategory.icon }} mr-2"></i>
|
||||
<span>{{ subcategory.name }}</span>
|
||||
</a>
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="{{ url_for('main.edit_category', category_id=subcategory.id) }}" class="text-gray-400 hover:text-primary" title="Edit">
|
||||
<i class="mdi mdi-pencil-outline"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('main.new_document') }}?category={{ subcategory.id }}" class="text-gray-400 hover:text-primary" title="Add Document">
|
||||
<i class="mdi mdi-file-plus-outline"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="p-4 text-center text-gray-500">
|
||||
No subcategories
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Save notification -->
|
||||
|
|
|
@ -249,7 +249,14 @@
|
|||
styleActiveLine: true,
|
||||
autoCloseBrackets: true,
|
||||
extraKeys: {
|
||||
"Enter": "newlineAndIndentContinueMarkdownList"
|
||||
"Enter": "newlineAndIndentContinueMarkdownList",
|
||||
"Ctrl-S": function(cm) {
|
||||
// Prevent browser's save dialog
|
||||
event.preventDefault();
|
||||
// Trigger document save
|
||||
document.getElementById('save-document').click();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -510,13 +517,21 @@
|
|||
|
||||
const tagName = tagInput.value.trim();
|
||||
if (tagName) {
|
||||
// Check for duplicate
|
||||
// Check for duplicate (case insensitive)
|
||||
const existingTags = Array.from(document.querySelectorAll('.tag')).map(tag =>
|
||||
tag.textContent.trim().toLowerCase());
|
||||
|
||||
if (!existingTags.includes(tagName.toLowerCase())) {
|
||||
addTag(tagName);
|
||||
} else {
|
||||
// Flash the input to indicate duplicate
|
||||
tagInput.classList.add('border-red-500');
|
||||
setTimeout(() => {
|
||||
tagInput.classList.remove('border-red-500');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
tagInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -107,9 +107,20 @@
|
|||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-white">My Categories</h2>
|
||||
|
||||
<!-- View toggle buttons -->
|
||||
<div class="flex bg-gray-700 rounded-md p-1">
|
||||
<button id="grid-view-btn" class="px-3 py-1 rounded text-sm flex items-center active-view">
|
||||
<i class="mdi mdi-view-grid mr-1"></i> Grid
|
||||
</button>
|
||||
<button id="list-view-btn" class="px-3 py-1 rounded text-sm flex items-center">
|
||||
<i class="mdi mdi-view-list mr-1"></i> List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<!-- Grid view (default) -->
|
||||
<div id="grid-view" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{% if categories %}
|
||||
{% for category in categories %}
|
||||
<div class="bg-gray-800 rounded-lg overflow-hidden shadow hover:shadow-lg transition-all hover:-translate-y-1 duration-200">
|
||||
|
@ -154,6 +165,111 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- List view (initially hidden) -->
|
||||
<div id="list-view" class="hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow">
|
||||
{% if categories %}
|
||||
<div class="category-list p-4">
|
||||
{% for category in categories %}
|
||||
<div class="category-list-item mb-3 group" data-category-id="{{ category.id }}">
|
||||
<div class="flex items-center justify-between py-3 px-4 bg-gray-700/50 rounded-lg cursor-pointer hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center">
|
||||
<button class="toggle-btn w-6 text-gray-400 hover:text-primary mr-2 flex justify-center items-center">
|
||||
<i class="mdi mdi-chevron-right text-xl transition-transform duration-200"></i>
|
||||
</button>
|
||||
<div class="w-8 h-8 rounded bg-primary/20 flex items-center justify-center text-primary mr-3">
|
||||
<i class="mdi {{ category.icon }}"></i>
|
||||
</div>
|
||||
<a href="{{ url_for('main.view_category', category_id=category.id) }}" class="text-white font-medium hover:text-primary transition-colors">{{ category.name }}</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 hidden group-hover:flex">
|
||||
<a href="{{ url_for('main.new_document') }}?category={{ category.id }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="New Document">
|
||||
<i class="mdi mdi-file-plus-outline"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('main.view_category', category_id=category.id) }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="View Category">
|
||||
<i class="mdi mdi-eye-outline"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('main.edit_category', category_id=category.id) }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="Edit Category">
|
||||
<i class="mdi mdi-pencil-outline"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="category-children ml-8 mt-2 pl-4 border-l border-gray-700 hidden animate-collapse origin-top">
|
||||
<!-- Documents -->
|
||||
{% if category.documents.count() > 0 %}
|
||||
<div class="mb-2">
|
||||
<div class="text-gray-500 text-xs font-medium mb-1 mt-1">DOCUMENTS</div>
|
||||
<div class="space-y-1">
|
||||
{% for doc in category.documents %}
|
||||
<div class="document-list-item group">
|
||||
<div class="flex items-center justify-between py-2 px-4 rounded-md hover:bg-gray-700/50 transition-colors">
|
||||
<div class="flex items-center">
|
||||
<i class="mdi mdi-file-document-outline text-gray-400 mr-3"></i>
|
||||
<a href="{{ url_for('main.view_document', doc_id=doc.id) }}" class="text-gray-300 hover:text-primary transition-colors">{{ doc.title }}</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 hidden group-hover:flex">
|
||||
<a href="{{ url_for('main.edit_document', doc_id=doc.id) }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="Edit Document">
|
||||
<i class="mdi mdi-pencil-outline"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('main.export_document', doc_id=doc.id) }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="Export Document">
|
||||
<i class="mdi mdi-download-outline"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Subcategories -->
|
||||
{% if category.children.count() > 0 %}
|
||||
<div>
|
||||
<div class="text-gray-500 text-xs font-medium mb-1 mt-3">SUBCATEGORIES</div>
|
||||
<div class="space-y-1">
|
||||
{% for subcategory in category.children %}
|
||||
<div class="subcategory-list-item group">
|
||||
<div class="flex items-center justify-between py-2 px-4 rounded-md hover:bg-gray-700/50 transition-colors">
|
||||
<div class="flex items-center">
|
||||
<i class="mdi {{ subcategory.icon }} text-gray-400 mr-3"></i>
|
||||
<a href="{{ url_for('main.view_category', category_id=subcategory.id) }}" class="text-gray-300 hover:text-primary transition-colors">{{ subcategory.name }}</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 hidden group-hover:flex">
|
||||
<a href="{{ url_for('main.new_document') }}?category={{ subcategory.id }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="New Document">
|
||||
<i class="mdi mdi-file-plus-outline"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('main.view_category', category_id=subcategory.id) }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="View Category">
|
||||
<i class="mdi mdi-eye-outline"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Add category button -->
|
||||
<button id="list-add-category-btn" class="w-full flex items-center justify-center py-3 px-4 mt-4 border-2 border-dashed border-gray-700 rounded-lg text-gray-400 hover:text-primary hover:border-primary/50 transition-all">
|
||||
<i class="mdi mdi-folder-plus-outline text-xl mr-2"></i> New Category
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-gray-800/50 rounded-lg p-8 text-center">
|
||||
<i class="mdi mdi-folder-outline text-6xl text-gray-700 mb-3"></i>
|
||||
<h3 class="text-lg text-gray-400 mb-3">No categories yet</h3>
|
||||
<p class="text-gray-500 mb-4">Organize your documents by creating categories</p>
|
||||
<button id="list-empty-add-category-btn" 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-plus mr-2"></i> Create Category
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -395,6 +511,155 @@
|
|||
menu.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// View toggle functionality
|
||||
const gridViewBtn = document.getElementById('grid-view-btn');
|
||||
const listViewBtn = document.getElementById('list-view-btn');
|
||||
const gridView = document.getElementById('grid-view');
|
||||
const listView = document.getElementById('list-view');
|
||||
|
||||
if (gridViewBtn && listViewBtn) {
|
||||
// Load saved preference
|
||||
const savedView = localStorage.getItem('categoryViewPreference');
|
||||
if (savedView === 'list') {
|
||||
gridView.classList.add('hidden');
|
||||
listView.classList.remove('hidden');
|
||||
listViewBtn.classList.add('active-view');
|
||||
gridViewBtn.classList.remove('active-view');
|
||||
}
|
||||
|
||||
gridViewBtn.addEventListener('click', function() {
|
||||
gridView.classList.remove('hidden');
|
||||
listView.classList.add('hidden');
|
||||
gridViewBtn.classList.add('active-view');
|
||||
listViewBtn.classList.remove('active-view');
|
||||
|
||||
// Save preference in localStorage
|
||||
localStorage.setItem('categoryViewPreference', 'grid');
|
||||
});
|
||||
|
||||
listViewBtn.addEventListener('click', function() {
|
||||
gridView.classList.add('hidden');
|
||||
listView.classList.remove('hidden');
|
||||
listViewBtn.classList.add('active-view');
|
||||
gridViewBtn.classList.remove('active-view');
|
||||
|
||||
// Save preference in localStorage
|
||||
localStorage.setItem('categoryViewPreference', 'list');
|
||||
});
|
||||
}
|
||||
|
||||
// Category list toggle functionality for tree view
|
||||
const categoryItems = document.querySelectorAll('.category-list-item');
|
||||
|
||||
categoryItems.forEach(item => {
|
||||
const toggleBtn = item.querySelector('.toggle-btn');
|
||||
const categoryHeader = item.querySelector('.flex.items-center.justify-between');
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
toggleCategory(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Also toggle when clicking anywhere on the category header
|
||||
if (categoryHeader) {
|
||||
categoryHeader.addEventListener('click', function(e) {
|
||||
// Don't toggle if clicking a link or button inside the header
|
||||
if (e.target.closest('a') || e.target.closest('button:not(.toggle-btn)')) {
|
||||
return;
|
||||
}
|
||||
toggleCategory(item);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function toggleCategory(categoryItem) {
|
||||
const chevron = categoryItem.querySelector('.toggle-btn i');
|
||||
const childrenContainer = categoryItem.querySelector('.category-children');
|
||||
|
||||
if (!childrenContainer) return;
|
||||
|
||||
if (childrenContainer.classList.contains('hidden')) {
|
||||
// Open category
|
||||
childrenContainer.classList.remove('hidden');
|
||||
childrenContainer.classList.add('animate-slide-down');
|
||||
chevron.classList.add('rotate-90');
|
||||
|
||||
setTimeout(() => {
|
||||
childrenContainer.classList.remove('animate-slide-down');
|
||||
}, 300);
|
||||
} else {
|
||||
// Close category
|
||||
childrenContainer.classList.add('animate-slide-up');
|
||||
chevron.classList.remove('rotate-90');
|
||||
|
||||
setTimeout(() => {
|
||||
childrenContainer.classList.add('hidden');
|
||||
childrenContainer.classList.remove('animate-slide-up');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Make list-add-category-btn open the category modal
|
||||
const listAddCategoryBtn = document.getElementById('list-add-category-btn');
|
||||
if (listAddCategoryBtn) {
|
||||
listAddCategoryBtn.addEventListener('click', function() {
|
||||
openModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Make list-empty-add-category-btn open the category modal
|
||||
const listEmptyAddCategoryBtn = document.getElementById('list-empty-add-category-btn');
|
||||
if (listEmptyAddCategoryBtn) {
|
||||
listEmptyAddCategoryBtn.addEventListener('click', function() {
|
||||
openModal();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Animation for expanding/collapsing */
|
||||
.animate-slide-down {
|
||||
animation: slideDown 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
max-height: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
max-height: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
max-height: 1000px;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
max-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Active view styling */
|
||||
.active-view {
|
||||
background-color: rgba(76, 175, 80, 0.2); /* Primary color with opacity */
|
||||
color: #4CAF50; /* Primary color */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue