wip
This commit is contained in:
parent
f5c8e9ee23
commit
3a16f266da
15 changed files with 511 additions and 169 deletions
105
app/__init__.py
105
app/__init__.py
|
@ -21,6 +21,108 @@ class Config:
|
|||
PERMANENT_SESSION_LIFETIME = timedelta(hours=12)
|
||||
SESSION_TYPE = 'filesystem'
|
||||
|
||||
def init_db(app):
|
||||
"""Initialize the database and create tables if they don't exist."""
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
from app.models.user import User
|
||||
# Create a demo user if no users exist
|
||||
if User.query.count() == 0:
|
||||
from app.models.document import Document, Category, Tag
|
||||
from werkzeug.security import generate_password_hash
|
||||
print('Creating demo user...')
|
||||
|
||||
# Create demo user
|
||||
demo_user = User(username='demo')
|
||||
demo_user.set_password('password')
|
||||
db.session.add(demo_user)
|
||||
db.session.flush() # To get the user ID
|
||||
|
||||
# Create a root category for the demo user
|
||||
root_category = Category(
|
||||
name='My Documents',
|
||||
icon='mdi-folder',
|
||||
description='Default document category',
|
||||
user_id=demo_user.id,
|
||||
is_root=True
|
||||
)
|
||||
db.session.add(root_category)
|
||||
|
||||
# Create some sample categories
|
||||
categories = [
|
||||
Category(
|
||||
name='Vim Commands',
|
||||
icon='mdi-vim',
|
||||
user_id=demo_user.id,
|
||||
description='Essential Vim commands and shortcuts'
|
||||
),
|
||||
Category(
|
||||
name='Flask Development',
|
||||
icon='mdi-flask',
|
||||
user_id=demo_user.id,
|
||||
description='Flask web development notes'
|
||||
),
|
||||
Category(
|
||||
name='Python Snippets',
|
||||
icon='mdi-language-python',
|
||||
user_id=demo_user.id,
|
||||
description='Useful Python code snippets'
|
||||
)
|
||||
]
|
||||
|
||||
for category in categories:
|
||||
db.session.add(category)
|
||||
|
||||
# Create a sample document
|
||||
sample_doc = Document(
|
||||
title='Getting Started with Vim',
|
||||
content="""# Getting Started with Vim
|
||||
|
||||
## Basic Commands
|
||||
|
||||
### Movement
|
||||
- `h` - move left
|
||||
- `j` - move down
|
||||
- `k` - move up
|
||||
- `l` - move right
|
||||
|
||||
### Modes
|
||||
- `i` - enter insert mode
|
||||
- `Esc` - return to normal mode
|
||||
- `v` - enter visual mode
|
||||
- `:` - enter command mode
|
||||
|
||||
> Vim has a steep learning curve, but it's worth it!
|
||||
|
||||
> [!TIP]
|
||||
> Use `vimtutor` to learn Vim basics interactively.
|
||||
|
||||
> [!NOTE]
|
||||
> Vim is available on almost all Unix-like systems.
|
||||
""",
|
||||
user_id=demo_user.id,
|
||||
category_id=categories[0].id
|
||||
)
|
||||
db.session.add(sample_doc)
|
||||
|
||||
# Create some tags
|
||||
tags = [
|
||||
Tag(name='vim', user_id=demo_user.id, color='#50fa7b'),
|
||||
Tag(name='editor', user_id=demo_user.id, color='#bd93f9'),
|
||||
Tag(name='tutorial', user_id=demo_user.id, color='#ff79c6')
|
||||
]
|
||||
|
||||
for tag in tags:
|
||||
db.session.add(tag)
|
||||
|
||||
# Associate tags with the document
|
||||
sample_doc.tags = tags
|
||||
|
||||
# Commit all changes
|
||||
db.session.commit()
|
||||
|
||||
print('Demo user and sample data created successfully!')
|
||||
|
||||
def create_app(config_class=Config):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
@ -51,6 +153,9 @@ def create_app(config_class=Config):
|
|||
# Create app instance
|
||||
app = create_app()
|
||||
|
||||
# Initialize database
|
||||
init_db(app)
|
||||
|
||||
# Import models after db initialization to avoid circular imports
|
||||
from app.models.document import Document, Category, Tag
|
||||
from app.models.user import User
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -45,9 +45,9 @@ def signup():
|
|||
|
||||
# Create root category for the user
|
||||
root_category = Category(
|
||||
name='Library',
|
||||
icon='mdi-bookshelf',
|
||||
description='General storage for documents and categories',
|
||||
name='', # Empty name for the root
|
||||
icon='mdi-folder-outline',
|
||||
description='Default container for documents and categories',
|
||||
user_id=user.id,
|
||||
is_root=True
|
||||
)
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -48,10 +48,18 @@ class Category(db.Model):
|
|||
def __repr__(self):
|
||||
return f'<Category {self.name}>'
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Return a display name for the category, showing 'Root' for the root category with empty name"""
|
||||
if self.is_root and not self.name:
|
||||
return "Root"
|
||||
return self.name
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'name': self.name or '', # Ensure name is never null
|
||||
'display_name': self.display_name,
|
||||
'icon': self.icon,
|
||||
'description': self.description,
|
||||
'parent_id': self.parent_id,
|
||||
|
|
|
@ -7,6 +7,7 @@ import json
|
|||
from datetime import datetime
|
||||
import io
|
||||
from sqlalchemy import or_
|
||||
import re
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
|
||||
|
@ -164,9 +165,9 @@ def save_document():
|
|||
if not root_category:
|
||||
# Create a root category if it doesn't exist
|
||||
root_category = Category(
|
||||
name='Library',
|
||||
icon='mdi-bookshelf',
|
||||
description='General storage for documents and categories',
|
||||
name='',
|
||||
icon='mdi-folder-outline',
|
||||
description='Default container for documents and categories',
|
||||
user_id=current_user.id,
|
||||
is_root=True
|
||||
)
|
||||
|
@ -341,54 +342,64 @@ def delete_category(category_id):
|
|||
|
||||
return jsonify({'success': True})
|
||||
|
||||
@main.route('/api/search', methods=['GET'])
|
||||
@login_required
|
||||
@main.route('/search')
|
||||
def search_documents():
|
||||
"""Search for documents by title, content, or tags"""
|
||||
query = request.args.get('q', '')
|
||||
user_id = current_user.id
|
||||
|
||||
if not query or len(query) < 2:
|
||||
return jsonify({'results': []})
|
||||
if not query:
|
||||
return jsonify([])
|
||||
|
||||
# Search in title, content, and tags for current user's documents only
|
||||
docs = Document.query.filter(
|
||||
Document.user_id == current_user.id,
|
||||
or_(
|
||||
Document.title.ilike(f'%{query}%'),
|
||||
Document.content.ilike(f'%{query}%'),
|
||||
Document.tags.any(Tag.name.ilike(f'%{query}%'))
|
||||
)
|
||||
).limit(10).all()
|
||||
# Parse hashtags from the query
|
||||
hashtags = re.findall(r'#(\w+)', query)
|
||||
# Remove hashtags from the main query text
|
||||
cleaned_query = re.sub(r'#\w+', '', query).strip()
|
||||
|
||||
# Base query for documents
|
||||
base_query = Document.query.filter_by(user_id=user_id)
|
||||
|
||||
results = []
|
||||
for doc in docs:
|
||||
# Find match in content
|
||||
match = None
|
||||
if query.lower() in doc.content.lower():
|
||||
# Find the sentence containing the match
|
||||
content_lower = doc.content.lower()
|
||||
query_pos = content_lower.find(query.lower())
|
||||
|
||||
# Get a snippet around the match
|
||||
start = max(0, content_lower.rfind('.', 0, query_pos) + 1)
|
||||
end = content_lower.find('.', query_pos)
|
||||
if end == -1:
|
||||
end = min(len(doc.content), query_pos + 200)
|
||||
|
||||
match = doc.content[start:end].strip()
|
||||
|
||||
# If we have a cleaned query (non-hashtag part), search by title and content
|
||||
if cleaned_query:
|
||||
title_content_results = base_query.filter(
|
||||
or_(
|
||||
Document.title.ilike(f'%{cleaned_query}%'),
|
||||
Document.content.ilike(f'%{cleaned_query}%')
|
||||
)
|
||||
).all()
|
||||
results.extend(title_content_results)
|
||||
|
||||
# If we have hashtags, search by tags (OR operation between tags)
|
||||
if hashtags:
|
||||
tag_results = base_query.join(document_tags).join(Tag).filter(
|
||||
Tag.name.in_(hashtags)
|
||||
).all()
|
||||
|
||||
# Get category name
|
||||
category_name = doc.category.name if doc.category else None
|
||||
|
||||
results.append({
|
||||
# Add unique tag results to the results list
|
||||
for doc in tag_results:
|
||||
if doc not in results:
|
||||
results.append(doc)
|
||||
|
||||
# If no specific search criteria (cleaned query or hashtags), return empty list
|
||||
if not cleaned_query and not hashtags:
|
||||
return jsonify([])
|
||||
|
||||
# Convert to list of dictionaries for JSON response
|
||||
docs_list = []
|
||||
for doc in results:
|
||||
doc_dict = {
|
||||
'id': doc.id,
|
||||
'title': doc.title,
|
||||
'category': category_name,
|
||||
'tags': [tag.name for tag in doc.tags],
|
||||
'match': match
|
||||
})
|
||||
'preview': doc.content[:150] + '...' if len(doc.content) > 150 else doc.content,
|
||||
'category_id': doc.category_id,
|
||||
'created_at': doc.created_at.strftime('%Y-%m-%d %H:%M'),
|
||||
'updated_at': doc.updated_at.strftime('%Y-%m-%d %H:%M') if doc.updated_at else None,
|
||||
'tags': [tag.name for tag in doc.tags]
|
||||
}
|
||||
docs_list.append(doc_dict)
|
||||
|
||||
return jsonify({'results': results})
|
||||
return jsonify(docs_list)
|
||||
|
||||
@main.route('/api/tags', methods=['GET'])
|
||||
@login_required
|
||||
|
@ -433,8 +444,10 @@ def new_category():
|
|||
|
||||
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)
|
||||
return render_template('category_edit.html', category=None, parent=parent)
|
||||
else:
|
||||
# No parent specified, creating a top-level category
|
||||
return render_template('category_edit.html', category=None, parent=None)
|
||||
|
||||
@main.route('/category/<int:category_id>/edit', methods=['GET'])
|
||||
@login_required
|
||||
|
|
|
@ -35,8 +35,7 @@
|
|||
|
||||
/* Active view styling */
|
||||
.active-view {
|
||||
background-color: rgba(76, 175, 80, 0.2); /* Primary color with opacity */
|
||||
color: #4CAF50; /* Primary color */
|
||||
@apply bg-primary/20 text-primary;
|
||||
}
|
||||
|
||||
/* Fix for sidebar hiding */
|
||||
|
@ -78,6 +77,16 @@
|
|||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Sidebar section headers */
|
||||
.sidebar-section-header {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: rgba(156, 163, 175, 0.7);
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0.5rem 1rem 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Drag and drop styles */
|
||||
.drop-target {
|
||||
background-color: rgba(80, 250, 123, 0.15);
|
||||
|
@ -172,4 +181,70 @@
|
|||
|
||||
.markdown-body .admonition-danger .admonition-title {
|
||||
color: #cf222e;
|
||||
}
|
||||
|
||||
/* Modern Sidebar Styling */
|
||||
aside {
|
||||
background-color: #1a1b26;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Reduce vertical spacing of sidebar items */
|
||||
aside nav ul li {
|
||||
margin: 0.15rem 0;
|
||||
}
|
||||
|
||||
/* Better styling for sidebar links */
|
||||
aside nav ul li a {
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
aside nav ul li a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Remove the border-left styling in category trees */
|
||||
aside nav ul.ml-3.pl-3.border-l.border-gray-700,
|
||||
.ml-3.pl-3.border-l.border-gray-700,
|
||||
.ml-2.pl-2.border-l.border-gray-700 {
|
||||
margin-left: 1.25rem !important;
|
||||
padding-left: 0 !important;
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
/* Better nested document styling */
|
||||
.document-item a,
|
||||
.category-item a {
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Cleaner category headers */
|
||||
aside nav ul li div.block.font-medium {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Make the toggle buttons more subtle */
|
||||
.toggle-btn {
|
||||
opacity: 0.6;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Better styling for the primary action button */
|
||||
aside nav ul li a.bg-primary {
|
||||
margin-top: 0.5rem;
|
||||
box-shadow: 0 3px 8px rgba(80, 250, 123, 0.2);
|
||||
}
|
|
@ -56,49 +56,53 @@
|
|||
<div class="flex min-h-screen {% if request.cookies.get('sidebar_collapsed') == 'true' %}sidebar-hidden{% endif %}">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-gray-800 h-screen flex-shrink-0 fixed left-0 top-0 z-10 overflow-y-auto transition-all ease-in-out duration-300">
|
||||
<div class="p-4 border-b border-gray-700 flex items-center">
|
||||
<div class="p-4 border-b border-gray-700/30 flex items-center">
|
||||
<h1 class="text-xl font-semibold text-white flex items-center">
|
||||
<i class="mdi mdi-vim text-primary text-2xl mr-2"></i> Vim Docs
|
||||
</h1>
|
||||
</div>
|
||||
<nav class="py-4">
|
||||
<nav class="py-2">
|
||||
<ul class="list-none">
|
||||
<li class="my-1">
|
||||
<a href="{{ url_for('main.index') }}" class="flex items-center px-4 py-2 text-gray-300 hover:text-primary hover:bg-gray-700 rounded-md transition-all {{ 'bg-primary/10 text-primary' if request.endpoint == 'main.index' }}">
|
||||
<li>
|
||||
<a href="{{ url_for('main.index') }}" class="flex items-center px-4 py-2 text-gray-300 hover:text-primary hover:bg-gray-700/50 rounded-md transition-all {{ 'bg-primary/10 text-primary' if request.endpoint == 'main.index' }}">
|
||||
<i class="mdi mdi-home mr-3"></i> Home
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="my-4">
|
||||
<div class="block font-medium text-gray-400 px-4 py-2 flex items-center">
|
||||
<i class="mdi mdi-folder-multiple-outline mr-3"></i> Categories
|
||||
</div>
|
||||
<ul class="ml-3 pl-3 border-l border-gray-700 my-1" id="category-tree">
|
||||
<li class="mt-3">
|
||||
<a href="#" id="root-category-link" class="flex items-center justify-between px-4 py-2 text-gray-300 hover:text-primary hover:bg-gray-700/50 rounded-md transition-all">
|
||||
<div class="flex items-center">
|
||||
<i class="mdi mdi-folder-outline mr-3"></i>
|
||||
<span>Files & Categories</span>
|
||||
</div>
|
||||
<i class="mdi mdi-chevron-down text-sm"></i>
|
||||
</a>
|
||||
<ul class="ml-3 pt-1" id="category-tree">
|
||||
<!-- Categories will be loaded here via JS -->
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="my-4">
|
||||
<div class="block font-medium text-gray-400 px-4 py-2 flex items-center">
|
||||
<li class="mt-3">
|
||||
<div class="block font-medium text-gray-400 px-4 py-1 flex items-center">
|
||||
<i class="mdi mdi-file-document-multiple-outline mr-3"></i> Documents
|
||||
</div>
|
||||
<ul class="ml-3 pl-3 border-l border-gray-700 my-1">
|
||||
<ul class="ml-3 pt-1">
|
||||
<li>
|
||||
<a href="{{ url_for('main.new_document') }}" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors {{ 'text-primary' if request.endpoint == 'main.new_document' }}">
|
||||
<a href="{{ url_for('main.new_document') }}" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary hover:bg-gray-700/30 rounded transition-colors {{ 'text-primary' if request.endpoint == 'main.new_document' }}">
|
||||
<i class="mdi mdi-plus-circle-outline mr-2 text-sm"></i> New Document
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('main.index') }}?view=recent" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors">
|
||||
<a href="{{ url_for('main.index') }}?view=recent" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary hover:bg-gray-700/30 rounded transition-colors">
|
||||
<i class="mdi mdi-clock-outline mr-2 text-sm"></i> Recent Documents
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="my-1">
|
||||
<a href="{{ url_for('main.new_document') }}" class="flex items-center px-4 py-2 bg-primary text-black hover:bg-primary-dark rounded-md transition-all">
|
||||
<i class="mdi mdi-plus-circle mr-3"></i> New Document
|
||||
<li class="mt-4 mb-2 px-3">
|
||||
<a href="{{ url_for('main.new_document') }}" class="flex items-center justify-center px-4 py-2 bg-primary text-black hover:bg-primary-dark rounded-md transition-all">
|
||||
<i class="mdi mdi-plus-circle mr-2"></i> New Document
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -188,6 +192,55 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Root category toggle
|
||||
const rootCategoryLink = document.getElementById('root-category-link');
|
||||
const categoryTree = document.getElementById('category-tree');
|
||||
|
||||
if (rootCategoryLink && categoryTree) {
|
||||
// Initialize - expand by default
|
||||
rootCategoryLink.querySelector('i.mdi-chevron-down').classList.add('rotate-180');
|
||||
|
||||
rootCategoryLink.addEventListener('click', function(e) {
|
||||
// Only handle the toggle if clicking on the chevron icon
|
||||
if (e.target.classList.contains('mdi-chevron-down') || e.target.parentElement.classList.contains('mdi-chevron-down')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Toggle category tree visibility
|
||||
if (categoryTree.classList.contains('hidden')) {
|
||||
categoryTree.classList.remove('hidden');
|
||||
rootCategoryLink.querySelector('i.mdi-chevron-down').classList.add('rotate-180');
|
||||
} else {
|
||||
categoryTree.classList.add('hidden');
|
||||
rootCategoryLink.querySelector('i.mdi-chevron-down').classList.remove('rotate-180');
|
||||
}
|
||||
|
||||
// Save preference in localStorage
|
||||
localStorage.setItem('rootCategoryExpanded', !categoryTree.classList.contains('hidden'));
|
||||
} else {
|
||||
// If not clicking on the chevron, we need to load the root category
|
||||
fetch('/api/categories')
|
||||
.then(response => response.json())
|
||||
.then(categories => {
|
||||
const rootCategory = categories.find(c => c.is_root);
|
||||
if (rootCategory) {
|
||||
window.location.href = `/category/${rootCategory.id}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching root category:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Load saved preference
|
||||
const rootExpanded = localStorage.getItem('rootCategoryExpanded') !== 'false';
|
||||
if (!rootExpanded) {
|
||||
categoryTree.classList.add('hidden');
|
||||
rootCategoryLink.querySelector('i.mdi-chevron-down').classList.remove('rotate-180');
|
||||
}
|
||||
}
|
||||
|
||||
// Load categories
|
||||
loadCategories();
|
||||
|
||||
|
@ -205,20 +258,155 @@
|
|||
categoryTree.innerHTML = ''; // Clear existing items
|
||||
|
||||
if (categories.length === 0) {
|
||||
categoryTree.innerHTML = '<li class="px-4 py-2 text-gray-500">No categories found</li>';
|
||||
categoryTree.innerHTML = '<li class="px-4 py-2 text-gray-500">No items found</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
categories.forEach(category => {
|
||||
categoryTree.appendChild(createCategoryItem(category));
|
||||
});
|
||||
// First add documents without categories (directly in root)
|
||||
const rootCategory = categories.find(c => c.is_root);
|
||||
if (rootCategory && rootCategory.documents && rootCategory.documents.length > 0) {
|
||||
// Create a section header for root documents
|
||||
const docHeader = document.createElement('div');
|
||||
docHeader.className = 'text-xs uppercase text-gray-500 font-medium px-2 py-1 mt-2';
|
||||
docHeader.textContent = 'Files';
|
||||
categoryTree.appendChild(docHeader);
|
||||
|
||||
// Create a container for documents
|
||||
const docsContainer = document.createElement('div');
|
||||
docsContainer.className = 'mb-2';
|
||||
categoryTree.appendChild(docsContainer);
|
||||
|
||||
const documentsUl = document.createElement('ul');
|
||||
documentsUl.className = 'py-1 space-y-0.5';
|
||||
docsContainer.appendChild(documentsUl);
|
||||
|
||||
rootCategory.documents.forEach(docId => {
|
||||
// Fetch document details and add to the tree
|
||||
fetch(`/api/document/${docId}`)
|
||||
.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 hover:bg-gray-700/30 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;
|
||||
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.classList.add('dragging');
|
||||
});
|
||||
|
||||
docLink.addEventListener('dragend', function() {
|
||||
docLi.classList.remove('dragging');
|
||||
});
|
||||
|
||||
// Add document actions
|
||||
const docActions = createDocumentActions(doc, docLi, rootCategory.id);
|
||||
|
||||
docLi.appendChild(docLink);
|
||||
docLi.appendChild(docActions);
|
||||
documentsUl.appendChild(docLi);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error fetching document ${docId}:`, error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If there are any non-root categories, add a section header
|
||||
const nonRootCategories = categories.filter(c => !c.is_root);
|
||||
if (nonRootCategories.length > 0) {
|
||||
const catHeader = document.createElement('div');
|
||||
catHeader.className = 'text-xs uppercase text-gray-500 font-medium px-2 py-1 mt-3';
|
||||
catHeader.textContent = 'Categories';
|
||||
categoryTree.appendChild(catHeader);
|
||||
|
||||
// Then add all non-root categories
|
||||
nonRootCategories.forEach(category => {
|
||||
categoryTree.appendChild(createCategoryItem(category));
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading categories:', error);
|
||||
categoryTree.innerHTML = '<li class="px-4 py-2 text-red-500">Error loading categories</li>';
|
||||
categoryTree.innerHTML = '<li class="px-4 py-2 text-red-500">Error loading items</li>';
|
||||
});
|
||||
}
|
||||
|
||||
function createDocumentActions(doc, docLi, categoryId) {
|
||||
const docActions = document.createElement('div');
|
||||
docActions.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800/90 px-1 rounded-sm';
|
||||
|
||||
// Edit document button
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
|
||||
editBtn.title = 'Edit document';
|
||||
editBtn.innerHTML = '<i class="mdi mdi-pencil-outline text-sm"></i>';
|
||||
editBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.location.href = `/document/${doc.id}/edit`;
|
||||
});
|
||||
docActions.appendChild(editBtn);
|
||||
|
||||
// Delete document button
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'p-1 text-gray-500 hover:text-red-500 rounded transition-colors';
|
||||
deleteBtn.title = 'Delete document';
|
||||
deleteBtn.innerHTML = '<i class="mdi mdi-delete-outline text-sm"></i>';
|
||||
deleteBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Show confirmation dialog
|
||||
if (confirm(`Are you sure you want to delete "${doc.title}"? This cannot be undone.`)) {
|
||||
// Send delete request
|
||||
fetch(`/api/document/${doc.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Remove the document from the DOM
|
||||
docLi.remove();
|
||||
|
||||
// Show notification
|
||||
showNotification('Document deleted successfully');
|
||||
|
||||
// If we're on the document's page, redirect to the category or home
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === `/document/${doc.id}` || currentPath === `/document/${doc.id}/edit`) {
|
||||
window.location.href = categoryId ? `/category/${categoryId}` : '/';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('Error deleting document', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
docActions.appendChild(deleteBtn);
|
||||
|
||||
return docActions;
|
||||
}
|
||||
|
||||
function createCategoryItem(category) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'category-item my-1';
|
||||
|
@ -248,7 +436,7 @@
|
|||
// 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 overflow-hidden';
|
||||
let categoryClass = 'flex-grow flex items-center py-1 px-2 text-gray-400 hover:text-primary hover:bg-gray-700/30 rounded transition-colors overflow-hidden';
|
||||
|
||||
// Special styling for root
|
||||
if (category.is_root) {
|
||||
|
@ -264,7 +452,7 @@
|
|||
|
||||
// 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';
|
||||
actionsContainer.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800/90 px-1 rounded-sm';
|
||||
categoryContainer.appendChild(actionsContainer);
|
||||
|
||||
// Add document button - consistent across all views
|
||||
|
@ -305,7 +493,7 @@
|
|||
|
||||
// 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.className = 'ml-4 mt-1 mb-1 overflow-hidden transition-all duration-300';
|
||||
childrenContainer.style.display = 'none'; // Initially collapsed
|
||||
li.appendChild(childrenContainer);
|
||||
|
||||
|
@ -357,7 +545,7 @@
|
|||
// Add documents first
|
||||
if (hasDocuments) {
|
||||
const documentsUl = document.createElement('ul');
|
||||
documentsUl.className = 'py-1 space-y-1';
|
||||
documentsUl.className = 'py-1 space-y-0.5';
|
||||
childrenContainer.appendChild(documentsUl);
|
||||
|
||||
// Sort documents by name
|
||||
|
@ -374,7 +562,7 @@
|
|||
|
||||
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 truncate';
|
||||
docLink.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary hover:bg-gray-700/30 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
|
||||
|
@ -394,66 +582,7 @@
|
|||
});
|
||||
|
||||
// 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 document button
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
|
||||
editBtn.title = 'Edit document';
|
||||
editBtn.innerHTML = '<i class="mdi mdi-pencil-outline text-sm"></i>';
|
||||
editBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.location.href = `/document/${doc.id}/edit`;
|
||||
});
|
||||
docActions.appendChild(editBtn);
|
||||
|
||||
// Delete document button
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'p-1 text-gray-500 hover:text-red-500 rounded transition-colors';
|
||||
deleteBtn.title = 'Delete document';
|
||||
deleteBtn.innerHTML = '<i class="mdi mdi-delete-outline text-sm"></i>';
|
||||
deleteBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Show confirmation dialog
|
||||
if (confirm(`Are you sure you want to delete "${doc.title}"? This cannot be undone.`)) {
|
||||
// Send delete request
|
||||
fetch(`/api/document/${doc.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Remove the document from the DOM
|
||||
docLi.remove();
|
||||
|
||||
// Show notification
|
||||
showNotification('Document deleted successfully');
|
||||
|
||||
// If we're on the document's page, redirect to the category or home
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === `/document/${doc.id}` || currentPath === `/document/${doc.id}/edit`) {
|
||||
window.location.href = `/category/${category.id}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('Error deleting document', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
docActions.appendChild(deleteBtn);
|
||||
const docActions = createDocumentActions(doc, docLi, category.id);
|
||||
|
||||
docLi.appendChild(docLink);
|
||||
docLi.appendChild(docActions);
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ category.name }} - Vim Docs{% endblock %}
|
||||
{% block title %}{{ category.display_name }} - Vim Docs{% endblock %}
|
||||
|
||||
{% block header_title %}
|
||||
<i class="mdi {{ category.icon }} mr-2"></i> {{ category.name }}
|
||||
<i class="mdi {{ category.icon }} mr-2"></i> {{ category.display_name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block header_actions %}
|
||||
<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 href="{{ url_for('main.new_category') }}{% if not category.is_root %}?parent_id={{ category.id }}{% endif %}" 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> {% if category.is_root %}New Category{% else %}New Subcategory{% endif %}
|
||||
</a>
|
||||
{% if not category.is_root %}
|
||||
<a href="{{ url_for('main.edit_category', category_id=category.id) }}" 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
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('main.category_all_documents', category_id=category.id) }}" 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-file-document-multiple-outline mr-2"></i> All Documents
|
||||
</a>
|
||||
|
@ -29,14 +31,18 @@
|
|||
<div class="w-10 h-10 rounded-md bg-primary/20 flex items-center justify-center text-primary mr-3">
|
||||
<i class="mdi {{ category.icon }} text-2xl"></i>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-white">{{ category.name }}</h1>
|
||||
<h1 class="text-2xl font-bold text-white">{{ category.display_name }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-gray-400 mb-4">
|
||||
{% if category.description %}
|
||||
{{ category.description }}
|
||||
{% else %}
|
||||
{% if category.is_root %}
|
||||
The default container for all your documents and categories
|
||||
{% else %}
|
||||
A category for organizing your documents
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
@ -46,10 +52,12 @@
|
|||
<a href="{{ url_for('main.index') }}" class="hover:text-primary">Home</a>
|
||||
{% if category.parent %}
|
||||
<span class="mx-2">/</span>
|
||||
<a href="{{ url_for('main.view_category', category_id=category.parent.id) }}" class="hover:text-primary">{{ category.parent.name }}</a>
|
||||
<a href="{{ url_for('main.view_category', category_id=category.parent.id) }}" class="hover:text-primary">{{ category.parent.display_name }}</a>
|
||||
{% endif %}
|
||||
{% if not category.is_root %}
|
||||
<span class="mx-2">/</span>
|
||||
<span class="text-gray-400">{{ category.name }}</span>
|
||||
<span class="text-gray-400">{{ category.display_name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -72,7 +80,7 @@
|
|||
<div class="w-10 h-10 rounded-md bg-primary/20 flex items-center justify-center text-primary mr-3">
|
||||
<i class="mdi {{ subcategory.icon }} text-2xl"></i>
|
||||
</div>
|
||||
<h3 class="text-white font-medium truncate">{{ subcategory.name }}</h3>
|
||||
<h3 class="text-white font-medium truncate">{{ subcategory.display_name }}</h3>
|
||||
</div>
|
||||
|
||||
{% if subcategory.description %}
|
||||
|
@ -94,7 +102,7 @@
|
|||
<i class="mdi mdi-folder-plus-outline text-2xl text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-gray-400 font-medium mb-1">New Subcategory</h3>
|
||||
<p class="text-gray-500 text-sm">Add a subcategory to {{ category.name }}</p>
|
||||
<p class="text-gray-500 text-sm">Add a subcategory to {{ category.display_name }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,9 +8,15 @@
|
|||
<button id="save-button" 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>
|
||||
{% if category %}
|
||||
<a href="{{ url_for('main.view_category', category_id=category.id) }}" 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-arrow-left mr-2"></i> Back
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('main.index') }}" 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-arrow-left mr-2"></i> Back
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if category and not category.is_root %}
|
||||
<button id="delete-category-btn" class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 transition-colors ml-2">
|
||||
<i class="mdi mdi-delete mr-2"></i> Delete
|
||||
|
@ -91,7 +97,7 @@
|
|||
<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>
|
||||
<option value="">No Parent</option>
|
||||
<!-- Options will be populated with JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
|
@ -202,13 +208,13 @@
|
|||
|
||||
<div class="p-6">
|
||||
<p class="text-gray-300 mb-6">
|
||||
Are you sure you want to delete <span class="font-semibold text-white">{{ category.name if category else 'this category' }}</span>?
|
||||
{% if category and (category.documents.count() > 0 or category.children.count() > 0) %}
|
||||
Are you sure you want to delete <span class="font-semibold text-white">{{ category.name if category is not none else 'this category' }}</span>?
|
||||
{% if category is not none and (category.documents.count() > 0 or category.children.count() > 0) %}
|
||||
This category contains {{ category.documents.count() }} document(s) and {{ category.children.count() }} subcategory(ies).
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if category and (category.documents.count() > 0 or category.children.count() > 0) %}
|
||||
{% if category is not none and (category.documents.count() > 0 or category.children.count() > 0) %}
|
||||
<div class="mb-6">
|
||||
<p class="text-white mb-2">What should happen to the contents?</p>
|
||||
<div class="space-y-3">
|
||||
|
@ -266,15 +272,15 @@
|
|||
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;
|
||||
// Also skip any root categories as they shouldn't be selectable as parents
|
||||
{% if category is not none %}
|
||||
if (category.id === {{ category.id }} || category.is_root) return;
|
||||
{% else %}
|
||||
if (category.is_root) return;
|
||||
{% endif %}
|
||||
|
||||
const option = document.createElement('option');
|
||||
|
@ -287,23 +293,16 @@
|
|||
// 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 parent is not none %}
|
||||
if (category.id === {{ parent.id }}) {
|
||||
selectThisOption = true;
|
||||
}
|
||||
{% elif category and category.parent_id %}
|
||||
{% elif category is not none and category.parent_id is not none %}
|
||||
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;
|
||||
|
@ -315,10 +314,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Initialize flag to track if root has been selected
|
||||
let rootSelected = false;
|
||||
|
||||
// Start from root categories
|
||||
// Start from categories
|
||||
addCategoryOptions(categories);
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -538,7 +534,8 @@
|
|||
const queryParams = deleteOption === 'delete' ? '' : '?preserve_contents=true';
|
||||
|
||||
// Send delete request
|
||||
fetch(`/api/category/{{ category.id if category else '' }}${queryParams}`, {
|
||||
{% if category is not none %}
|
||||
fetch(`/api/category/{{ category.id }}${queryParams}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -568,7 +565,7 @@
|
|||
|
||||
// Redirect to home page or parent category
|
||||
setTimeout(() => {
|
||||
{% if category and category.parent_id %}
|
||||
{% if category.parent_id %}
|
||||
window.location.href = '/category/{{ category.parent_id }}';
|
||||
{% else %}
|
||||
window.location.href = '/';
|
||||
|
@ -590,6 +587,7 @@
|
|||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
});
|
||||
{% endif %}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -535,6 +535,12 @@
|
|||
const listView = document.getElementById('list-view');
|
||||
|
||||
if (gridViewBtn && listViewBtn) {
|
||||
// Initialize view (grid is default)
|
||||
gridViewBtn.classList.add('active-view');
|
||||
listViewBtn.classList.remove('active-view');
|
||||
gridView.classList.remove('hidden');
|
||||
listView.classList.add('hidden');
|
||||
|
||||
// Load saved preference
|
||||
const savedView = localStorage.getItem('categoryViewPreference');
|
||||
if (savedView === 'list') {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue