Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions app/static/js/v2/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1107,3 +1107,208 @@ window.formatTimestamp = function(isoStr) {
var d = new Date(isoStr);
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
};

// ============================================================================
// TOAST NOTIFICATION SYSTEM
// ============================================================================

// Add CSS for toast notifications
if (!document.getElementById('toast-styles')) {
var style = document.createElement('style');
style.id = 'toast-styles';
style.textContent = `
.toast {
position: fixed;
bottom: 24px;
right: 24px;
background: var(--bg-elevated);
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 12px 16px;
z-index: 10000;
min-width: 200px;
max-width: 400px;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
animation: slideIn 0.2s ease-out;
}
.toast-success {
border-color: var(--success);
color: var(--success);
}
.toast-error {
border-color: var(--error);
color: var(--error);
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
}

window.showToast = function(message, type) {
type = type || 'success';
var toast = document.createElement('div');
toast.className = 'toast toast-' + type;
toast.textContent = message;

document.body.appendChild(toast);

setTimeout(function() {
toast.style.animation = 'slideOut 0.2s ease-in';
setTimeout(function() {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 200);
}, 3000);
};

// ============================================================================
// INLINE RENAME COMPONENT
// ============================================================================

/**
* Sanitize a slug for use in filenames
* - Removes invalid filesystem characters: / \ : ? * < > |
* - Replaces whitespace with hyphens
* - Removes leading/trailing hyphens
* - Converts to lowercase
*/
window.sanitizeSlug = function(slug) {
if (!slug) return '';

// Remove invalid filesystem characters
slug = slug.replace(/[\/\\:?*<>|]/g, '');
// Replace consecutive whitespace with single hyphen
slug = slug.replace(/\s+/g, '-');
// Remove leading/trailing hyphens
slug = slug.replace(/^-+|-+$/g, '');
// Convert to lowercase
slug = slug.toLowerCase();

return slug;
};

window.initRename = function(noteId, currentFilename, onSuccess) {
return {
noteId: noteId,
currentFilename: currentFilename,
isEditing: false,
editingSlug: '',
timestampPrefix: '',
error: '',
saving: false,
onSuccess: onSuccess || function() {},

init: function() {
this.parseFilename();
},

parseFilename: function() {
// Parse YYYY-MM-DD_HH-MM_slug.md format (new)
var match = this.currentFilename.match(/^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})_(.+)\.md$/);
if (match) {
this.timestampPrefix = match[1];
this.editingSlug = match[2];
return;
}

// Parse YYYY_MM_DD_HH_MM_slug.md format (old)
match = this.currentFilename.match(/^(\d{4}_\d{2}_\d{2}_\d{2}_\d{2})_(.+)\.md$/);
if (match) {
this.timestampPrefix = match[1];
this.editingSlug = match[2];
return;
}

// Fallback: treat entire filename as slug if no timestamp pattern matched
console.warn('Filename does not match expected timestamp format:', this.currentFilename);
this.timestampPrefix = '';
this.editingSlug = this.currentFilename.replace('.md', '');
},

startEdit: function() {
this.isEditing = true;
this.error = '';
var self = this;
this.$nextTick(function() {
var input = self.$el.querySelector('.rename-input');
if (input) {
input.focus();
input.select();
}
});
},

cancelEdit: function() {
this.isEditing = false;
this.error = '';
this.parseFilename();
},

confirmEdit: function() {
var self = this;
if (self.saving) return;

var newSlug = self.editingSlug.trim();
if (!newSlug) {
self.error = 'Slug cannot be empty';
return;
}

// Sanitize slug using the helper function
newSlug = window.sanitizeSlug(newSlug);

if (!newSlug) {
self.error = 'Invalid characters';
return;
}

self.saving = true;
self.error = '';

fetch('/v2/api/registry/' + self.noteId + '/rename', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ new_slug: newSlug })
})
.then(function(res) {
if (!res.ok) {
return res.json().then(function(err) {
throw new Error(err.detail || 'Rename failed');
});
}
return res.json();
})
.then(function(data) {
self.currentFilename = data.new_filename;
self.isEditing = false;
self.parseFilename();
window.showToast('Renamed successfully', 'success');
if (self.onSuccess) self.onSuccess(data);
})
.catch(function(err) {
self.error = err.message;
if (err.message.includes('already exists')) {
self.error = 'Name already exists';
}
window.showToast(self.error, 'error');
})
.finally(function() {
self.saving = false;
});
},

handleKeydown: function(e) {
if (e.key === 'Enter') {
this.confirmEdit();
} else if (e.key === 'Escape') {
this.cancelEdit();
}
}
};
};
75 changes: 66 additions & 9 deletions app/templates/v2/inbox.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,53 @@
{% endblock %}

{% block content %}
<div x-data="noteList(window.__inboxNotes, window.__filterTag)">
<div x-data="noteList(window.__inboxNotes, window.__filterTag)"
@open-rename.window="renameNote = $event.detail; showRenameModal = true"
x-init="renameNote = null; showRenameModal = false;">

<!-- Rename Modal -->
<template x-if="showRenameModal && renameNote">
<div class="modal-backdrop" @click.self="showRenameModal = false">
<div class="modal-content"
@click.stop
x-data="window.initRename(renameNote.id, renameNote.filename, function(data) {
renameNote.filename = data.new_filename;
showRenameModal = false;
})">
<h3 class="text-lg font-semibold" style="margin-bottom: var(--space-4);">Rename Note</h3>

<div style="margin-bottom: var(--space-4);">
<label class="text-sm text-secondary" style="display: block; margin-bottom: var(--space-2);">Filename</label>
<div style="display: flex; align-items: center; gap: 4px; font-family: monospace; font-size: 0.875rem;">
<span style="color: var(--text-muted); white-space: nowrap;" x-text="timestampPrefix + '_'"></span>
<input
type="text"
class="input rename-input"
style="flex: 1; font-family: monospace;"
x-model="editingSlug"
@keydown="handleKeydown($event)"
placeholder="slug"
:disabled="saving">
<span style="color: var(--text-muted);">.md</span>
</div>
<p class="text-xs text-muted" style="margin-top: var(--space-2);">
Timestamp is locked. Only the slug portion can be edited.
</p>
<template x-if="error">
<p class="text-sm" style="color: var(--error); margin-top: var(--space-2);" x-text="error"></p>
</template>
</div>

<div style="display: flex; gap: var(--space-2); justify-content: flex-end;">
<button class="btn btn-ghost" @click="showRenameModal = false" :disabled="saving">Cancel</button>
<button class="btn btn-primary" @click="confirmEdit()" :disabled="saving">
<template x-if="saving"><span>Renaming...</span></template>
<template x-if="!saving">Rename</template>
</button>
</div>
</div>
</div>
</template>

<!-- Page Header -->
<div class="page-header">
Expand Down Expand Up @@ -111,14 +157,25 @@ <h1 class="page-title">Inbox</h1>
</template>
</div>
</div>
<a :href="note.link || ('/v2/note/' + note.id)"
class="btn btn-secondary btn-sm"
title="Open note"
@click.stop
style="flex-shrink:0; padding: 6px 12px; font-size: 0.75rem; gap: 4px;">
Open
<i data-lucide="arrow-up-right" style="width: 12px; height: 12px;"></i>
</a>
<div style="display: flex; gap: 8px; align-items: center;">
<template x-if="note.status === 'completed' && note.filename">
<button
class="btn btn-ghost btn-sm"
title="Rename note"
@click.stop="$dispatch('open-rename', note)"
style="padding: 4px 8px; font-size: 0.75rem;">
✏️
</button>
</template>
<a :href="note.link || ('/v2/note/' + note.id)"
class="btn btn-secondary btn-sm"
title="Open note"
@click.stop
style="flex-shrink:0; padding: 6px 12px; font-size: 0.75rem; gap: 4px;">
Open
<i data-lucide="arrow-up-right" style="width: 12px; height: 12px;"></i>
</a>
</div>
</div>
</template>

Expand Down
60 changes: 58 additions & 2 deletions app/templates/v2/projects.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,54 @@
{% endblock %}

{% block content %}
<div x-data="projectView(window.__projectsData)">
<div x-data="projectView(window.__projectsData)"
@open-rename.window="renameNote = $event.detail; showRenameModal = true"
x-init="renameNote = null; showRenameModal = false;">

<!-- Rename Modal -->
<template x-if="showRenameModal && renameNote">
<div class="modal-backdrop" @click.self="showRenameModal = false">
<div class="modal-content"
@click.stop
x-data="window.initRename(renameNote.id, renameNote.filename, function(data) {
renameNote.filename = data.new_filename;
renameNote.title = data.new_filename.replace(/^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}_/, '').replace(/\.md$/, '').replace(/-/g, ' ');
showRenameModal = false;
})">>
<h3 class="text-lg font-semibold" style="margin-bottom: var(--space-4);">Rename Note</h3>

<div style="margin-bottom: var(--space-4);">
<label class="text-sm text-secondary" style="display: block; margin-bottom: var(--space-2);">Filename</label>
<div style="display: flex; align-items: center; gap: 4px; font-family: monospace; font-size: 0.875rem;">
<span style="color: var(--text-muted); white-space: nowrap;" x-text="timestampPrefix + '_'"></span>
<input
type="text"
class="input rename-input"
style="flex: 1; font-family: monospace;"
x-model="editingSlug"
@keydown="handleKeydown($event)"
placeholder="slug"
:disabled="saving">
<span style="color: var(--text-muted);">.md</span>
</div>
<p class="text-xs text-muted" style="margin-top: var(--space-2);">
Timestamp is locked. Only the slug portion can be edited.
</p>
<template x-if="error">
<p class="text-sm" style="color: var(--error); margin-top: var(--space-2);" x-text="error"></p>
</template>
</div>

<div style="display: flex; gap: var(--space-2); justify-content: flex-end;">
<button class="btn btn-ghost" @click="showRenameModal = false" :disabled="saving">Cancel</button>
<button class="btn btn-primary" @click="confirmEdit()" :disabled="saving">
<template x-if="saving"><span>Renaming...</span></template>
<template x-if="!saving">Rename</template>
</button>
</div>
</div>
</div>
</template>

<div class="page-header">
<div>
Expand Down Expand Up @@ -91,7 +138,7 @@ <h2 class="text-sm text-muted" style="margin-bottom:var(--space-2); font-weight:
<template x-if="isExpanded(proj)">
<div class="project-notes" @click.stop>
<template x-for="note in expandedNotes" :key="note.id">
<div class="project-note-row">
<div class="project-note-row" style="display: flex; align-items: center; justify-content: space-between;">
<a :href="'/v2/registry-note/' + note.id" style="text-decoration:none; color:inherit; display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-3); flex:1; min-width:0;">
<span class="log-icon">
<template x-if="note.success"><span style="color:var(--success);">&#10003;</span></template>
Expand All @@ -102,6 +149,15 @@ <h2 class="text-sm text-muted" style="margin-bottom:var(--space-2); font-weight:
<span class="text-xs text-muted" x-text="note.processed_at"></span>
</div>
</a>
<template x-if="note.success && note.filename">
<button
class="btn btn-ghost btn-sm"
title="Rename note"
@click.stop="$dispatch('open-rename', note)"
style="padding: 4px 8px; font-size: 0.75rem; margin-right: var(--space-2);">
✏️
</button>
</template>
</div>
</template>
<template x-if="expandedNotes.length === 0">
Expand Down
Loading