Show folder tree for notes.
This commit is contained in:
@@ -59,6 +59,19 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label for="folder">Folder</label>
|
||||
<input type="text" id="folder" name="folder" class="form-control"
|
||||
placeholder="e.g., Work/Projects or Personal"
|
||||
value="{{ note.folder if note and note.folder else '' }}"
|
||||
list="folder-suggestions">
|
||||
<datalist id="folder-suggestions">
|
||||
{% for folder in all_folders %}
|
||||
<option value="{{ folder }}">
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label for="tags">Tags (comma-separated)</label>
|
||||
<input type="text" id="tags" name="tags" class="form-control"
|
||||
|
||||
538
templates/notes_folders.html
Normal file
538
templates/notes_folders.html
Normal file
@@ -0,0 +1,538 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="timetrack-container notes-folders-container">
|
||||
<div class="admin-header">
|
||||
<h2>Note Folders</h2>
|
||||
<div class="admin-actions">
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="showCreateFolderModal()">
|
||||
<span>📁</span> Create Folder
|
||||
</button>
|
||||
<a href="{{ url_for('notes_list') }}" class="btn btn-sm btn-secondary">Back to Notes</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="folders-layout">
|
||||
<!-- Folder Tree -->
|
||||
<div class="folder-tree-panel">
|
||||
<h3>Folder Structure</h3>
|
||||
<div class="folder-tree" id="folder-tree">
|
||||
{{ render_folder_tree(folder_tree)|safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Folder Details -->
|
||||
<div class="folder-details-panel">
|
||||
<div id="folder-info">
|
||||
<p class="text-muted">Select a folder to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Folder Modal -->
|
||||
<div class="modal" id="folderModal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Create New Folder</h3>
|
||||
<button type="button" class="close-btn" onclick="closeFolderModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="folderForm">
|
||||
<div class="form-group">
|
||||
<label for="folderName">Folder Name</label>
|
||||
<input type="text" id="folderName" name="name" class="form-control" required
|
||||
placeholder="e.g., Projects, Meeting Notes">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="parentFolder">Parent Folder</label>
|
||||
<select id="parentFolder" name="parent" class="form-control">
|
||||
<option value="">Root (Top Level)</option>
|
||||
{% for folder in all_folders %}
|
||||
<option value="{{ folder }}">{{ folder }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="folderDescription">Description (Optional)</label>
|
||||
<textarea id="folderDescription" name="description" class="form-control"
|
||||
rows="3" placeholder="What kind of notes will go in this folder?"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeFolderModal()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveFolder()">Save Folder</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.notes-folders-container {
|
||||
max-width: none !important;
|
||||
width: 100% !important;
|
||||
padding: 1rem !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.folders-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 2rem;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.folder-tree-panel,
|
||||
.folder-details-panel {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.folder-tree-panel h3,
|
||||
.folder-details-panel h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Folder Tree Styles */
|
||||
.folder-tree {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
position: relative;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.folder-item.has-children > .folder-content::before {
|
||||
content: "▶";
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
transition: transform 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folder-item.has-children.expanded > .folder-content::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.folder-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-left: 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.folder-content:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.folder-content.selected {
|
||||
background: #e3f2fd;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.folder-count {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.folder-children {
|
||||
margin-left: 1.5rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.folder-item.expanded > .folder-children {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Folder Details */
|
||||
.folder-details {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.folder-details h4 {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.folder-path {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-bottom: 1rem;
|
||||
font-family: monospace;
|
||||
background: #f8f9fa;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.folder-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.folder-actions {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.notes-preview {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.notes-preview h5 {
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.note-preview-item {
|
||||
padding: 0.75rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.note-preview-item:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.note-preview-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.note-preview-date {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.folders-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.folder-tree-panel {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let selectedFolder = null;
|
||||
|
||||
function selectFolder(folderPath) {
|
||||
// Remove previous selection
|
||||
document.querySelectorAll('.folder-content').forEach(el => {
|
||||
el.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Add selection to clicked folder
|
||||
event.currentTarget.classList.add('selected');
|
||||
selectedFolder = folderPath;
|
||||
|
||||
// Load folder details
|
||||
loadFolderDetails(folderPath);
|
||||
}
|
||||
|
||||
function toggleFolder(event, folderPath) {
|
||||
event.stopPropagation();
|
||||
const folderItem = event.currentTarget.closest('.folder-item');
|
||||
folderItem.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
function loadFolderDetails(folderPath) {
|
||||
fetch(`/api/notes/folder-details?path=${encodeURIComponent(folderPath)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const detailsHtml = `
|
||||
<div class="folder-details">
|
||||
<h4><span class="folder-icon">📁</span> ${data.name}</h4>
|
||||
<div class="folder-path">${data.path}</div>
|
||||
|
||||
<div class="folder-stats">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">${data.note_count}</div>
|
||||
<div class="stat-label">Notes</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">${data.subfolder_count}</div>
|
||||
<div class="stat-label">Subfolders</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="folder-actions">
|
||||
<a href="/notes?folder=${encodeURIComponent(data.path)}" class="btn btn-sm btn-primary">
|
||||
View Notes
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-info" onclick="editFolder('${data.path}')">
|
||||
Rename
|
||||
</button>
|
||||
${data.note_count === 0 && data.subfolder_count === 0 ?
|
||||
`<button type="button" class="btn btn-sm btn-danger" onclick="deleteFolder('${data.path}')">
|
||||
Delete
|
||||
</button>` : ''}
|
||||
</div>
|
||||
|
||||
${data.recent_notes.length > 0 ? `
|
||||
<div class="notes-preview">
|
||||
<h5>Recent Notes</h5>
|
||||
${data.recent_notes.map(note => `
|
||||
<div class="note-preview-item">
|
||||
<a href="/notes/${note.slug}" class="note-preview-title">${note.title}</a>
|
||||
<div class="note-preview-date">${note.updated_at}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('folder-info').innerHTML = detailsHtml;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading folder details:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function showCreateFolderModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create New Folder';
|
||||
document.getElementById('folderForm').reset();
|
||||
document.getElementById('folderModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeFolderModal() {
|
||||
document.getElementById('folderModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function saveFolder() {
|
||||
const formData = new FormData(document.getElementById('folderForm'));
|
||||
const data = {
|
||||
name: formData.get('name'),
|
||||
parent: formData.get('parent'),
|
||||
description: formData.get('description')
|
||||
};
|
||||
|
||||
// Check if we're editing or creating
|
||||
const modalTitle = document.getElementById('modalTitle').textContent;
|
||||
const isEditing = modalTitle.includes('Edit');
|
||||
|
||||
if (isEditing && selectedFolder) {
|
||||
// Rename folder
|
||||
fetch('/api/notes/folders', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
old_path: selectedFolder,
|
||||
new_name: data.name
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert(result.message);
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error renaming folder');
|
||||
});
|
||||
} else {
|
||||
// Create new folder
|
||||
fetch('/api/notes/folders', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert(result.message);
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error creating folder');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function editFolder(folderPath) {
|
||||
const parts = folderPath.split('/');
|
||||
const folderName = parts[parts.length - 1];
|
||||
const parentPath = parts.slice(0, -1).join('/');
|
||||
|
||||
document.getElementById('modalTitle').textContent = 'Edit Folder';
|
||||
document.getElementById('folderName').value = folderName;
|
||||
document.getElementById('parentFolder').value = parentPath;
|
||||
document.getElementById('folderModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function deleteFolder(folderPath) {
|
||||
if (confirm(`Are you sure you want to delete the folder "${folderPath}"? This cannot be undone.`)) {
|
||||
fetch(`/api/notes/folders?path=${encodeURIComponent(folderPath)}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert(result.message);
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error deleting folder');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('folderModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeFolderModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_folder_tree(tree, level=0) %}
|
||||
{% for folder, children in tree.items() %}
|
||||
<div class="folder-item {% if children %}has-children{% endif %}" data-folder="{{ folder }}">
|
||||
<div class="folder-content" onclick="selectFolder('{{ folder }}')">
|
||||
{% if children %}
|
||||
<span onclick="toggleFolder(event, '{{ folder }}')" style="position: absolute; left: -15px; cursor: pointer;">▶</span>
|
||||
{% endif %}
|
||||
<span class="folder-icon">📁</span>
|
||||
<span class="folder-name">{{ folder.split('/')[-1] }}</span>
|
||||
<span class="folder-count">({{ folder_counts.get(folder, 0) }})</span>
|
||||
</div>
|
||||
{% if children %}
|
||||
<div class="folder-children">
|
||||
{{ render_folder_tree(children, level + 1)|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user