Files
TimeTrack/templates/notes_list.html

913 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.html" %}
<!-- Folder tree rendering macro -->
{% macro render_folder_tree(tree, parent_path='') %}
{% for folder_path, children in tree.items() %}
{% set folder_name = folder_path.split('/')[-1] %}
{% set is_active = folder_filter == folder_path %}
<div class="folder-item {% if is_active %}active{% endif %}" style="padding-left: {{ (folder_path.count('/') + 1) * 20 }}px;">
<span class="folder-icon">📁</span>
<a href="{{ url_for('notes.notes_list', folder=folder_path) }}" class="folder-link">
{{ folder_name }}
</a>
<span class="folder-count">({{ folder_counts.get(folder_path, 0) }})</span>
</div>
{% if children %}
{{ render_folder_tree(children, folder_path) }}
{% endif %}
{% endfor %}
{% endmacro %}
{% block content %}
<div class="admin-container">
<div class="admin-header">
<h2>Notes & Documentation</h2>
<div class="admin-actions">
<a href="{{ url_for('notes.create_note') }}" class="btn btn-sm btn-primary">
<span>📝</span> Create New Note
</a>
<button type="button" class="btn btn-sm btn-secondary" id="toggle-sidebar">
<span>📁</span> Toggle Folders
</button>
<a href="{{ url_for('notes.notes_folders') }}" class="btn btn-sm btn-info">
<span>⚙️</span> Manage Folders
</a>
</div>
</div>
<div class="notes-layout">
<!-- Sidebar -->
<div class="notes-sidebar" id="notes-sidebar">
<!-- Search -->
<div class="sidebar-section">
<form method="GET" action="{{ url_for('notes.notes_list') }}">
<div class="search-box">
<input type="text"
name="search"
class="form-control"
placeholder="Search notes..."
value="{{ search_query }}">
<button type="submit" class="search-btn">🔍</button>
</div>
</form>
</div>
<!-- Folders -->
<div class="sidebar-section">
<h4>Folders</h4>
<div class="folder-tree">
<div class="folder-item {% if not folder_filter %}active{% endif %}">
<span class="folder-icon">🏠</span>
<a href="{{ url_for('notes.notes_list') }}" class="folder-link">All Notes</a>
<span class="folder-count">({{ notes|length }})</span>
</div>
{{ render_folder_tree(folder_tree)|safe }}
</div>
</div>
<!-- Tags -->
<div class="sidebar-section">
<h4>Tags</h4>
{% if all_tags %}
<div class="tags-list">
{% for tag in all_tags %}
<a href="{{ url_for('notes.notes_list', tag=tag) }}"
class="tag-link {% if tag_filter == tag %}active{% endif %}">
<span class="tag-icon">🏷️</span>
{{ tag }}
<span class="tag-count">({{ tag_counts.get(tag, 0) }})</span>
</a>
{% endfor %}
</div>
{% else %}
<p class="text-muted">No tags yet</p>
{% endif %}
</div>
<!-- Visibility Filter -->
<div class="sidebar-section">
<h4>Visibility</h4>
<div class="visibility-filters">
<a href="{{ url_for('notes.notes_list') }}"
class="visibility-link {% if not visibility_filter %}active{% endif %}">
<span>👁️</span> All Notes
</a>
<a href="{{ url_for('notes.notes_list', visibility='private') }}"
class="visibility-link {% if visibility_filter == 'private' %}active{% endif %}">
<span>🔒</span> Private
</a>
<a href="{{ url_for('notes.notes_list', visibility='team') }}"
class="visibility-link {% if visibility_filter == 'team' %}active{% endif %}">
<span>👥</span> Team
</a>
<a href="{{ url_for('notes.notes_list', visibility='company') }}"
class="visibility-link {% if visibility_filter == 'company' %}active{% endif %}">
<span>🏢</span> Company
</a>
</div>
</div>
</div>
<!-- Main Content -->
<div class="notes-content">
<!-- Active Filters -->
{% if folder_filter or tag_filter or visibility_filter or search_query %}
<div class="active-filters">
<span>Active filters:</span>
{% if folder_filter %}
<span class="filter-tag">
📁 {{ folder_filter }}
<a href="{{ url_for('notes.notes_list', tag=tag_filter, visibility=visibility_filter, search=search_query) }}" class="remove-filter">×</a>
</span>
{% endif %}
{% if tag_filter %}
<span class="filter-tag">
🏷️ {{ tag_filter }}
<a href="{{ url_for('notes.notes_list', folder=folder_filter, visibility=visibility_filter, search=search_query) }}" class="remove-filter">×</a>
</span>
{% endif %}
{% if visibility_filter %}
<span class="filter-tag">
👁️ {{ visibility_filter|title }}
<a href="{{ url_for('notes.notes_list', folder=folder_filter, tag=tag_filter, search=search_query) }}" class="remove-filter">×</a>
</span>
{% endif %}
{% if search_query %}
<span class="filter-tag">
🔍 "{{ search_query }}"
<a href="{{ url_for('notes.notes_list', folder=folder_filter, tag=tag_filter, visibility=visibility_filter) }}" class="remove-filter">×</a>
</span>
{% endif %}
<a href="{{ url_for('notes.notes_list') }}" class="clear-all">Clear all</a>
</div>
{% endif %}
{% if notes %}
<!-- View Toggle -->
<div class="view-controls">
<div class="view-toggle">
<button class="toggle-btn active" data-view="grid" onclick="switchView('grid')">
<span></span> Grid
</button>
<button class="toggle-btn" data-view="list" onclick="switchView('list')">
<span></span> List
</button>
</div>
</div>
<!-- Grid View -->
<div class="notes-view grid-view active" id="grid-view">
<div class="notes-grid" id="notes-grid">
{% for note in notes %}
<div class="note-card {% if note.is_pinned %}pinned{% endif %}"
data-note-id="{{ note.id }}"
draggable="true">
<div class="note-card-header">
{% if note.is_pinned %}
<span class="pin-indicator" title="Pinned">📌</span>
{% endif %}
<span class="visibility-badge visibility-{{ note.visibility.value.lower() }}">
{% if note.visibility.value == 'Private' %}🔒{% elif note.visibility.value == 'Team' %}👥{% else %}🏢{% endif %}
{{ note.visibility.value }}
</span>
</div>
<h3 class="note-title">
<a href="{{ url_for('notes.view_note', slug=note.slug) }}">{{ note.title }}</a>
</h3>
<p class="note-preview">{{ note.get_preview(150) }}</p>
{% if note.tags %}
<div class="note-tags">
{% for tag in note.get_tags_list() %}
<span class="tag-chip">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
<div class="note-footer">
<span class="note-meta">
<span>📁</span> {{ note.folder or 'No folder' }}
</span>
<span class="note-meta">
<span>👤</span> {{ note.created_by.username }}
</span>
<span class="note-meta">
<span>🕒</span> {{ note.updated_at.strftime('%b %d, %Y') }}
</span>
</div>
<div class="note-actions">
<a href="{{ url_for('notes.view_note', slug=note.slug) }}" class="btn btn-xs btn-info">View</a>
{% if note.can_user_edit(g.user) %}
<a href="{{ url_for('notes.edit_note', slug=note.slug) }}" class="btn btn-xs btn-primary">Edit</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- List View -->
<div class="notes-view list-view" id="list-view">
<table class="notes-table">
<thead>
<tr>
<th>Title</th>
<th>Folder</th>
<th>Tags</th>
<th>Visibility</th>
<th>Author</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for note in notes %}
<tr class="note-row {% if note.is_pinned %}pinned{% endif %}"
data-note-id="{{ note.id }}"
draggable="true">
<td>
{% if note.is_pinned %}<span class="pin-indicator" title="Pinned">📌</span>{% endif %}
<a href="{{ url_for('notes.view_note', slug=note.slug) }}">{{ note.title }}</a>
</td>
<td>{{ note.folder or '-' }}</td>
<td>
{% if note.tags %}
{% for tag in note.get_tags_list() %}
<span class="tag-chip">{{ tag }}</span>
{% endfor %}
{% else %}
-
{% endif %}
</td>
<td>
<span class="visibility-badge visibility-{{ note.visibility.value.lower() }}">
{% if note.visibility.value == 'Private' %}🔒{% elif note.visibility.value == 'Team' %}👥{% else %}🏢{% endif %}
{{ note.visibility.value }}
</span>
</td>
<td>{{ note.created_by.username }}</td>
<td>{{ note.updated_at.strftime('%b %d, %Y') }}</td>
<td>
<a href="{{ url_for('notes.view_note', slug=note.slug) }}" class="btn btn-xs btn-info">View</a>
{% if note.can_user_edit(g.user) %}
<a href="{{ url_for('notes.edit_note', slug=note.slug) }}" class="btn btn-xs btn-primary">Edit</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">📝</div>
<h3>No notes found</h3>
<p>
{% if folder_filter or tag_filter or visibility_filter or search_query %}
No notes match your current filters. Try adjusting your search criteria.
{% else %}
Start documenting your knowledge by creating your first note.
{% endif %}
</p>
<a href="{{ url_for('notes.create_note') }}" class="btn btn-primary">Create Your First Note</a>
</div>
{% endif %}
</div>
</div>
</div>
<style>
/* Notes styles that match User Management */
.admin-container {
padding: 20px;
}
.notes-layout {
display: flex;
gap: 20px;
margin-top: 20px;
}
/* Sidebar */
.notes-sidebar {
width: 250px;
background: #f8f9fa;
border-radius: 5px;
padding: 15px;
height: fit-content;
position: sticky;
top: 20px;
}
.notes-sidebar.collapsed {
display: none;
}
.sidebar-section {
margin-bottom: 25px;
}
.sidebar-section h4 {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 10px;
color: #495057;
}
.search-box {
position: relative;
display: flex;
max-width: 100%;
}
.search-box input {
padding-right: 35px;
width: 100%;
font-size: 0.85rem;
}
.search-btn {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
padding: 5px;
}
/* Folder Tree */
.folder-tree {
font-size: 0.85rem;
}
.folder-item {
padding: 5px 0;
display: flex;
align-items: center;
gap: 5px;
}
.folder-item.active .folder-link {
font-weight: 600;
color: #007bff;
}
.folder-item.drag-over {
background: #e3f2fd;
border-radius: 3px;
}
.folder-link {
color: #495057;
text-decoration: none;
flex: 1;
}
.folder-link:hover {
color: #007bff;
}
.folder-count {
font-size: 0.8rem;
color: #6c757d;
}
/* Tags */
.tags-list {
display: flex;
flex-direction: column;
gap: 5px;
}
.tag-link {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
background: white;
border-radius: 3px;
text-decoration: none;
color: #495057;
font-size: 0.85rem;
transition: all 0.2s;
}
.tag-link:hover {
background: #e9ecef;
}
.tag-link.active {
background: #007bff;
color: white;
}
.tag-count {
margin-left: auto;
font-size: 0.8rem;
opacity: 0.7;
}
/* Visibility Filters */
.visibility-filters {
display: flex;
flex-direction: column;
gap: 5px;
}
.visibility-link {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
background: white;
border-radius: 3px;
text-decoration: none;
color: #495057;
font-size: 0.85rem;
transition: all 0.2s;
}
.visibility-link:hover {
background: #e9ecef;
}
.visibility-link.active {
background: #007bff;
color: white;
}
/* Main Content */
.notes-content {
flex: 1;
min-width: 0;
}
/* Active Filters */
.active-filters {
background: #f8f9fa;
padding: 10px 15px;
border-radius: 5px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
font-size: 0.85rem;
}
.filter-tag {
background: white;
padding: 4px 8px;
border-radius: 3px;
border: 1px solid #dee2e6;
}
.remove-filter {
margin-left: 5px;
color: #dc3545;
text-decoration: none;
font-weight: bold;
}
.clear-all {
margin-left: auto;
color: #007bff;
text-decoration: none;
}
/* View Controls */
.view-controls {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
.view-toggle {
display: flex;
gap: 5px;
}
.toggle-btn {
padding: 5px 15px;
background: white;
border: 1px solid #dee2e6;
cursor: pointer;
transition: all 0.2s;
}
.toggle-btn:first-child {
border-radius: 3px 0 0 3px;
}
.toggle-btn:last-child {
border-radius: 0 3px 3px 0;
}
.toggle-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
/* View Containers */
.notes-view {
display: none;
}
.notes-view.active {
display: block;
}
/* Notes Grid */
.notes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
}
.note-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 12px;
position: relative;
transition: all 0.2s;
cursor: move;
}
.note-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.note-card.pinned {
border-color: #ffc107;
}
.note-card.dragging {
opacity: 0.5;
cursor: grabbing;
}
.note-card.drag-over {
border: 2px dashed #007bff;
}
.note-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.pin-indicator {
font-size: 0.875rem;
}
.visibility-badge {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 3px;
}
.visibility-private {
background: #f8d7da;
color: #721c24;
}
.visibility-team {
background: #d1ecf1;
color: #0c5460;
}
.visibility-company {
background: #d4edda;
color: #155724;
}
.note-title {
font-size: 1rem;
margin: 0 0 8px 0;
line-height: 1.3;
}
.note-title a {
color: #333;
text-decoration: none;
}
.note-title a:hover {
color: #007bff;
}
.note-preview {
color: #6c757d;
font-size: 0.8rem;
line-height: 1.3;
margin-bottom: 8px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.note-tags {
margin-bottom: 8px;
}
.tag-chip {
display: inline-block;
background: #e9ecef;
padding: 1px 6px;
border-radius: 3px;
font-size: 0.7rem;
margin-right: 4px;
}
.note-footer {
display: flex;
gap: 10px;
margin-bottom: 8px;
font-size: 0.7rem;
color: #6c757d;
}
.note-meta {
display: flex;
align-items: center;
gap: 2px;
}
.note-actions {
display: flex;
gap: 5px;
padding-top: 8px;
border-top: 1px solid #e9ecef;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 60px 20px;
background: #f8f9fa;
border-radius: 5px;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 20px;
}
/* List View / Table */
.notes-table {
width: 100%;
background: white;
border: 1px solid #dee2e6;
border-radius: 5px;
overflow: hidden;
border-collapse: collapse;
}
.notes-table th {
background: #f8f9fa;
padding: 10px;
text-align: left;
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
font-size: 0.85rem;
}
.notes-table td {
padding: 10px;
border-bottom: 1px solid #e9ecef;
font-size: 0.85rem;
}
.notes-table tr:hover {
background: #f8f9fa;
}
.notes-table tr.pinned {
background: #fff8dc;
}
.notes-table tr.pinned:hover {
background: #fff3cd;
}
/* Draggable table rows */
.note-row {
cursor: move;
}
.note-row.dragging {
opacity: 0.5;
}
.note-row a {
pointer-events: auto;
cursor: pointer;
}
/* Responsive */
@media (max-width: 768px) {
.notes-layout {
flex-direction: column;
}
.notes-sidebar {
width: 100%;
position: static;
}
.notes-grid {
grid-template-columns: 1fr;
}
}
</style>
<script>
// Sidebar toggle
document.getElementById('toggle-sidebar').addEventListener('click', function() {
document.getElementById('notes-sidebar').classList.toggle('collapsed');
});
// View switching
function switchView(view) {
// Update button states
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.view === view) {
btn.classList.add('active');
}
});
// Update view visibility
document.querySelectorAll('.notes-view').forEach(v => {
v.classList.remove('active');
});
document.getElementById(view + '-view').classList.add('active');
}
// Drag and Drop functionality
let draggedNote = null;
let draggedNoteId = null;
document.addEventListener('DOMContentLoaded', function() {
initializeDragAndDrop();
});
function initializeDragAndDrop() {
// Note cards (grid view)
const noteCards = document.querySelectorAll('.note-card');
noteCards.forEach(card => {
card.addEventListener('dragstart', handleNoteDragStart);
card.addEventListener('dragend', handleNoteDragEnd);
card.addEventListener('dragover', handleNoteDragOver);
card.addEventListener('drop', handleNoteDrop);
card.addEventListener('dragleave', handleNoteDragLeave);
});
// Note rows (list view)
const noteRows = document.querySelectorAll('.note-row');
noteRows.forEach(row => {
row.addEventListener('dragstart', handleNoteDragStart);
row.addEventListener('dragend', handleNoteDragEnd);
});
// Folder items
const folderItems = document.querySelectorAll('.folder-item');
folderItems.forEach(folder => {
folder.addEventListener('dragover', handleFolderDragOver);
folder.addEventListener('drop', handleFolderDrop);
folder.addEventListener('dragleave', handleFolderDragLeave);
});
}
// Note drag handlers
function handleNoteDragStart(e) {
draggedNote = this;
draggedNoteId = this.dataset.noteId;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedNoteId);
}
function handleNoteDragEnd(e) {
this.classList.remove('dragging');
// Remove all drag-over classes
document.querySelectorAll('.note-card, .note-row, .folder-item').forEach(el => {
el.classList.remove('drag-over');
});
}
function handleNoteDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
if (this !== draggedNote) {
this.classList.add('drag-over');
}
return false;
}
function handleNoteDragLeave(e) {
this.classList.remove('drag-over');
}
function handleNoteDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
if (draggedNote !== this) {
// Reorder notes
const notesGrid = document.getElementById('notes-grid');
const allCards = Array.from(notesGrid.querySelectorAll('.note-card'));
const draggedIndex = allCards.indexOf(draggedNote);
const targetIndex = allCards.indexOf(this);
if (draggedIndex < targetIndex) {
this.parentNode.insertBefore(draggedNote, this.nextSibling);
} else {
this.parentNode.insertBefore(draggedNote, this);
}
}
return false;
}
// Folder drag handlers
function handleFolderDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
this.classList.add('drag-over');
return false;
}
function handleFolderDragLeave(e) {
this.classList.remove('drag-over');
}
function handleFolderDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
this.classList.remove('drag-over');
// Get folder path
const folderLink = this.querySelector('.folder-link');
let folderPath = '';
if (folderLink) {
// Extract folder path from the URL
const href = folderLink.getAttribute('href');
const url = new URL(href, window.location.origin);
folderPath = url.searchParams.get('folder') || '';
}
// Move note to folder
if (draggedNoteId && confirm(`Move this note to folder "${folderPath || 'All Notes'}"?`)) {
// Here you would make an API call to update the note's folder
fetch(`/api/notes/${draggedNoteId}/move`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ folder: folderPath })
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Failed to move note: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to move note');
});
}
return false;
}
</script>
{% endblock %}