This commit is contained in:
Mateusz Gruszczyński committed 2026-09-25 15:07:20 +02:00
1 parent 6329ac33d1
commit 90e3c399ea
22 files changed
+729 -46

No files matched your search

+273 -2
View File
@@ -1115,12 +1115,16 @@ textarea::selection {
}
.markdown-body blockquote {
margin: .35em 0;
padding: .2em 1em;
border-left: 3px solid var(--accent);
color: var(--text-muted-strong);
}
.markdown-body .markdown-quote-line {
min-height: 1.5em;
}
.markdown-body a {
color: var(--accent-text);
}
@@ -1153,7 +1157,7 @@ textarea::selection {
bottom: 0;
z-index: 24;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
grid-template-rows: auto auto minmax(0, 1fr);
width: 340px;
max-width: 100%;
overflow: hidden;
@@ -1184,6 +1188,33 @@ textarea::selection {
padding: 18px;
}
.history-view-switch {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px;
margin: 0 18px 8px;
padding: 3px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-inset);
}
.history-view-switch button {
min-height: 30px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--muted);
font-size: .74rem;
font-weight: 700;
}
.history-view-switch button.active {
background: var(--surface-2);
color: var(--text-heading);
box-shadow: 0 0 0 1px var(--border);
}
.icon-button {
display: grid;
place-items: center;
@@ -1203,6 +1234,190 @@ textarea::selection {
padding: 10px 18px 24px;
}
.history-list--archive {
padding-top: 4px;
}
.history-archive {
display: grid;
gap: 14px;
}
.history-archive__years {
display: flex;
gap: 6px;
overflow-x: auto;
padding-bottom: 2px;
}
.history-archive__years button {
flex: 0 0 auto;
min-width: 56px;
min-height: 30px;
border: 1px solid var(--border);
border-radius: 7px;
background: transparent;
color: var(--muted);
font-size: .72rem;
font-weight: 700;
}
.history-archive__years button.active {
border-color: color-mix(in srgb, var(--accent) 56%, var(--border));
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
color: var(--text-heading);
}
.history-archive__histogram {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
align-items: end;
gap: 3px;
min-height: 98px;
padding: 10px 8px 6px;
border: 1px solid var(--border);
border-radius: 9px;
background: var(--surface-inset);
}
.history-archive__bar-cell {
display: grid;
grid-template-rows: 74px auto;
align-items: end;
gap: 4px;
min-width: 0;
}
.history-archive__bar {
display: block;
width: 100%;
height: var(--history-bar-height, 3px);
border-radius: 3px 3px 1px 1px;
background: color-mix(in srgb, var(--accent) 72%, var(--surface-2));
}
.history-archive__bar-cell small {
overflow: hidden;
color: var(--muted-2);
font-size: .52rem;
text-align: center;
text-transform: uppercase;
}
.history-calendar {
display: grid;
gap: 12px;
}
.history-calendar__month {
padding: 10px;
border: 1px solid var(--border);
border-radius: 9px;
background: var(--surface-card);
}
.history-calendar__month-head,
.history-archive__selected-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
}
.history-calendar__month-head strong,
.history-archive__selected-head strong {
font-size: .78rem;
}
.history-calendar__month-head span,
.history-archive__selected-head span {
color: var(--muted-2);
font-size: .64rem;
}
.history-calendar__weekdays,
.history-calendar__days {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 4px;
}
.history-calendar__weekdays {
margin-bottom: 4px;
}
.history-calendar__weekdays span {
color: var(--muted-2);
font-size: .56rem;
text-align: center;
}
.history-calendar__blank {
min-height: 30px;
}
.history-calendar__day {
position: relative;
display: grid;
place-items: center;
min-height: 31px;
padding: 0;
border: 1px solid transparent;
border-radius: 50%;
background: color-mix(in srgb, var(--accent) 22%, var(--surface));
color: var(--text-heading);
font-size: .66rem;
}
button.history-calendar__day {
cursor: pointer;
}
.history-calendar__day small {
position: absolute;
right: -1px;
bottom: -1px;
display: grid;
place-items: center;
min-width: 13px;
height: 13px;
padding: 0 3px;
border: 1px solid var(--surface-card);
border-radius: 999px;
background: var(--accent);
color: var(--on-accent);
font-size: .48rem;
line-height: 1;
}
.history-calendar__day.active {
border-color: var(--accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent);
}
.history-calendar__day--empty {
background: transparent;
color: var(--muted-2);
}
.history-archive__selected {
padding-top: 2px;
border-top: 1px solid var(--border);
}
.history-archive__hint {
margin: 0;
padding: 8px 0 18px;
color: var(--muted-2);
font-size: .72rem;
text-align: center;
}
.revision--compact {
padding: 10px 0;
}
.revision {
position: relative;
display: grid;
@@ -3367,6 +3582,62 @@ dialog::backdrop {
display: none;
}
.workspace-settings-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 10px 14px;
margin-top: 10px;
padding: 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-card);
}
.workspace-settings-card[hidden] {
display: none;
}
.workspace-settings-card__copy {
display: grid;
gap: 2px;
grid-column: 1 / -1;
}
.workspace-settings-card__copy strong {
font-size: .82rem;
}
.workspace-settings-card__copy small,
.workspace-settings-card>.form-message {
color: var(--muted);
font-size: .72rem;
}
.workspace-archive-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
font-size: .78rem;
}
.workspace-archive-toggle input {
width: auto;
min-height: auto;
margin: 0;
accent-color: var(--accent);
}
.workspace-settings-card>.form-message {
grid-column: 1 / -1;
margin: 0;
}
.workspace-settings-card>.form-message:empty {
display: none;
}
.notes-view-switch {
display: inline-flex;
padding: 3px;
+4
View File
@@ -272,6 +272,10 @@
<p>Author, time, and version preview</p>
</div><button id="close-history" class="icon-button">×</button>
</div>
<div class="history-view-switch" role="group" aria-label="History view">
<button type="button" data-history-view="list" class="active" aria-pressed="true">List</button>
<button type="button" data-history-view="archive" aria-pressed="false">Archive</button>
</div>
<div id="history-list" class="history-list"></div>
</aside>
</main>
+16 -1
View File
@@ -531,11 +531,27 @@ export function renderMarkdown(source, lineOffset = 0) {
const heading = line.match(/^\s{0,4}(#{1,6})\s+(.+?)(?:\s+\{#([A-Za-z][\w:.-]*)\})?\s*$/);
const listItem = listLine(line);
const quote = line.match(/^>\s?(.*)$/);
if (listItem) {
const rendered = renderList(lines, index, lineOffset, listItem.indent, listItem.type);
html += rendered.html;
index = rendered.end - 1;
} else if (quote) {
closeList();
let quoteBody = "";
let quoteIndex = index;
while (quoteIndex < lines.length) {
const quoteLine = lines[quoteIndex].match(/^>\s?(.*)$/);
if (!quoteLine) break;
const prefix = lines[quoteIndex].startsWith("> ") ? "> " : ">";
const lineAttrs = attrs(quoteIndex, true, prefix, "", lineOffset)
.replace(' class="', ' class="markdown-quote-line ');
quoteBody += `<div${lineAttrs}>${quoteLine[1] ? inline(quoteLine[1]) : "<br>"}</div>`;
quoteIndex++;
}
html += `<blockquote class="markdown-quote-group">${quoteBody}</blockquote>`;
index = quoteIndex - 1;
} else if (heading) {
closeList();
const n = heading[1].length;
@@ -555,7 +571,6 @@ export function renderMarkdown(source, lineOffset = 0) {
html += `</dl>`;
} else if (/^\[PAGEBREAK\]$/i.test(line.trim())) html += `<hr${attrs(index, false, "", "", lineOffset).replace(' class="', ' class="page-break-marker ')} data-page-break="true">`;
else if (/^---+$/.test(line.trim())) html += `<hr${attrs(index, false, "", "", lineOffset)}>`;
else if (line.startsWith("> ")) html += `<blockquote${attrs(index, true, "> ", "", lineOffset)}>${inline(line.slice(2))}</blockquote>`;
else if (line.trim()) html += `<p${attrs(index, true, "", "", lineOffset)}>${inline(line)}</p>`;
else html += `<div${attrs(index, true, "", "", lineOffset)}><br></div>`;
}
+9 -5
View File
@@ -112,18 +112,22 @@ export function createWorkspaceNoteAdapter() {
: "Delete note";
},
async deleteNote(info, accessToken) {
if (!await askConfirm(`Delete note “${info.title}”? This cannot be undone.`, {
const archive = Boolean(info.archive_deleted_notes);
const message = archive
? `Delete note “${info.title}”? It will be archived and hidden from this workspace, but kept in the database.`
: `Delete note “${info.title}”? This cannot be undone.`;
if (!await askConfirm(message, {
title: "Delete note",
confirmText: "Delete",
confirmText: archive ? "Archive" : "Delete",
danger: true,
})) return;
await api(base, {
const result = await api(base, {
method: "DELETE",
body: JSON.stringify({ access_token: accessToken || null }),
});
queueToast(`The note "${info.title}" was deleted.`, {
queueToast(result.archived ? `The note "${info.title}" was archived.` : `The note "${info.title}" was deleted.`, {
type: "success",
title: "Note deleted",
title: result.archived ? "Note archived" : "Note deleted",
});
location.assign(`/w/${encode(workspaceSlug)}`);
},
+173 -1
View File
@@ -2424,7 +2424,179 @@ export function startNoteEditor(adapter) {
}
});
document.querySelector("#password-form").addEventListener("submit", async e => { e.preventDefault(); try { password = document.querySelector("#open-password").value; const result = await adapter.requestAccess(password); setAccessToken(adapter.access.kind, adapter.access.key, result.granted); accessToken = getAccessToken(adapter.access.kind, adapter.access.key); password = ""; document.querySelector("#open-password").value = ""; document.querySelector("#password-error").textContent = ""; await loadNoteInfo(); void syncFavoriteButton(); loadFiles(); connect(); toast.success("Editing access has been unlocked.", { title: "Note unlocked" }); } catch (error) { document.querySelector("#password-error").textContent = error.message; toast.danger(error.message, { title: "Could not unlock note" }); } });
const historyPanel = document.querySelector("#history-panel"); document.querySelector("#history-button").addEventListener("click", async () => { if (info?.protected && !resourceUnlocked) { if (!passwordDialog.open) passwordDialog.showModal(); document.querySelector("#open-password")?.focus(); return; } historyPanel.classList.add("open"); historyPanel.setAttribute("aria-hidden", "false"); document.body.classList.add("history-open"); const list = document.querySelector("#history-list"); list.innerHTML = '<p class="empty">Loading…</p>'; try { const revisions = await adapter.loadHistory(accessToken); list.innerHTML = revisions.length ? revisions.map((r, i) => { const snippet = escapeHtml(r.content.trim().split("\n").slice(0, 3).join(" · ").slice(0, 150) || "Empty note"); const author = r.author || "Unknown author"; return `<article class="revision"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(r.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button data-preview="${r.id}">Preview</button><button data-revision="${r.id}">Restore</button><div class="revision__preview" id="preview-${r.id}" hidden></div></div></article>`; }).join("") : '<p class="empty">No history yet.</p>'; for (const r of revisions) { list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click", () => { const el = list.querySelector(`#preview-${r.id}`); el.hidden = !el.hidden; el.textContent = r.content; }); list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click", async () => { await adapter.restoreRevision(r.id, accessToken); toast.success("The selected revision is now the current version.", { title: "Version restored" }); }); } } catch (e) { list.innerHTML = `<p class="error">${escapeHtml(e.message)}</p>`; toast.danger(e.message, { title: "Could not load version history" }); } }); document.querySelector("#close-history").addEventListener("click", () => { historyPanel.classList.remove("open"); historyPanel.setAttribute("aria-hidden", "true"); document.body.classList.remove("history-open"); });
const historyPanel = document.querySelector("#history-panel");
const historyList = document.querySelector("#history-list");
let historyRevisions = [];
let historyView = "list";
let historyArchiveYear = null;
let historyArchiveDay = null;
function parseHistoryDate(value) {
const raw = String(value ?? "").trim();
let normalized = raw;
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}$/.test(normalized)) normalized = normalized.replace(" ", "T") + ":00";
else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}$/.test(normalized)) normalized = normalized.replace(" ", "T");
else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(normalized)) normalized = normalized.replace(" ", "T") + "Z";
const date = new Date(normalized);
return Number.isNaN(date.getTime()) ? null : date;
}
function historyRevisionCard(revision, compact = false) {
const snippet = escapeHtml(revision.content.trim().split("\n").slice(0, compact ? 2 : 3).join(" · ").slice(0, compact ? 110 : 150) || "Empty note");
const author = revision.author || "Unknown author";
return `<article class="revision${compact ? " revision--compact" : ""}" data-history-revision="${revision.id}"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(revision.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button type="button" data-preview="${revision.id}">Preview</button><button type="button" data-revision="${revision.id}">Restore</button><div class="revision__preview" hidden></div></div></article>`;
}
function renderHistoryListView() {
historyList.classList.remove("history-list--archive");
historyList.innerHTML = historyRevisions.length
? historyRevisions.map(revision => historyRevisionCard(revision)).join("")
: '<p class="empty">No history yet.</p>';
}
function dayKey(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function renderHistoryArchiveView() {
historyList.classList.add("history-list--archive");
const dated = historyRevisions
.map(revision => ({ revision, date: parseHistoryDate(revision.created_at) }))
.filter(item => item.date);
if (!dated.length) {
historyList.innerHTML = historyRevisions.length
? '<p class="empty">Archive view needs valid revision timestamps.</p>'
: '<p class="empty">No history yet.</p>';
return;
}
const years = [...new Set(dated.map(item => item.date.getFullYear()))].sort((a, b) => b - a);
if (!historyArchiveYear || !years.includes(historyArchiveYear)) historyArchiveYear = years[0];
const yearItems = dated.filter(item => item.date.getFullYear() === historyArchiveYear);
const monthCounts = Array.from({ length: 12 }, () => 0);
const byDay = new Map();
yearItems.forEach(item => {
monthCounts[item.date.getMonth()] += 1;
const key = dayKey(item.date);
if (!byDay.has(key)) byDay.set(key, []);
byDay.get(key).push(item.revision);
});
const maxMonthCount = Math.max(1, ...monthCounts);
const months = Array.from({ length: 12 }, (_, month) => month);
const monthNames = new Intl.DateTimeFormat(undefined, { month: "short" });
const monthNamesLong = new Intl.DateTimeFormat(undefined, { month: "long" });
const yearButtons = years.map(year => `<button type="button" data-history-year="${year}" class="${year === historyArchiveYear ? "active" : ""}" aria-pressed="${year === historyArchiveYear}">${year}</button>`).join("");
const histogram = months.map(month => {
const count = monthCounts[month];
const height = count ? Math.max(12, Math.round((count / maxMonthCount) * 72)) : 3;
return `<div class="history-archive__bar-cell" title="${escapeHtml(monthNamesLong.format(new Date(historyArchiveYear, month, 1)))}: ${count} revisions"><span class="history-archive__bar" style="--history-bar-height:${height}px"></span><small>${escapeHtml(monthNames.format(new Date(historyArchiveYear, month, 1)))}</small></div>`;
}).join("");
const calendars = months.filter(month => monthCounts[month] > 0).map(month => {
const first = new Date(historyArchiveYear, month, 1);
const daysInMonth = new Date(historyArchiveYear, month + 1, 0).getDate();
const mondayOffset = (first.getDay() + 6) % 7;
const blanks = Array.from({ length: mondayOffset }, () => '<span class="history-calendar__blank"></span>').join("");
let days = "";
for (let day = 1; day <= daysInMonth; day++) {
const key = `${historyArchiveYear}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
const revisions = byDay.get(key) || [];
const active = historyArchiveDay === key;
days += revisions.length
? `<button type="button" class="history-calendar__day${active ? " active" : ""}" data-history-day="${key}" title="${revisions.length} revision${revisions.length === 1 ? "" : "s"}"><span>${day}</span><small>${revisions.length}</small></button>`
: `<span class="history-calendar__day history-calendar__day--empty"><span>${day}</span></span>`;
}
return `<section class="history-calendar__month"><div class="history-calendar__month-head"><strong>${escapeHtml(monthNamesLong.format(first))}</strong><span>${monthCounts[month]} revisions</span></div><div class="history-calendar__weekdays"><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span><span>S</span></div><div class="history-calendar__days">${blanks}${days}</div></section>`;
}).join("");
const selected = historyArchiveDay && byDay.get(historyArchiveDay);
const selectedHtml = selected?.length
? `<section class="history-archive__selected"><div class="history-archive__selected-head"><strong>${escapeHtml(historyArchiveDay)}</strong><span>${selected.length} saved version${selected.length === 1 ? "" : "s"}</span></div>${selected.map(revision => historyRevisionCard(revision, true)).join("")}</section>`
: '<p class="history-archive__hint">Select a highlighted day to inspect saved versions.</p>';
historyList.innerHTML = `<div class="history-archive"><div class="history-archive__years" aria-label="Archive year">${yearButtons}</div><div class="history-archive__histogram" aria-label="Revision activity by month">${histogram}</div><div class="history-calendar">${calendars}</div>${selectedHtml}</div>`;
}
function renderHistory() {
document.querySelectorAll("[data-history-view]").forEach(button => {
const active = button.dataset.historyView === historyView;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
if (historyView === "archive") renderHistoryArchiveView();
else renderHistoryListView();
}
document.querySelectorAll("[data-history-view]").forEach(button => button.addEventListener("click", () => {
historyView = button.dataset.historyView === "archive" ? "archive" : "list";
renderHistory();
}));
historyList.addEventListener("click", async event => {
const yearButton = event.target.closest("[data-history-year]");
if (yearButton) {
historyArchiveYear = Number(yearButton.dataset.historyYear);
historyArchiveDay = null;
renderHistoryArchiveView();
return;
}
const dayButton = event.target.closest("[data-history-day]");
if (dayButton) {
historyArchiveDay = dayButton.dataset.historyDay;
renderHistoryArchiveView();
return;
}
const previewButton = event.target.closest("[data-preview]");
if (previewButton) {
const revision = historyRevisions.find(item => String(item.id) === previewButton.dataset.preview);
const previewNode = previewButton.closest(".revision")?.querySelector(".revision__preview");
if (!revision || !previewNode) return;
previewNode.hidden = !previewNode.hidden;
previewNode.textContent = revision.content;
return;
}
const restoreButton = event.target.closest("[data-revision]");
if (!restoreButton) return;
restoreButton.disabled = true;
try {
await adapter.restoreRevision(Number(restoreButton.dataset.revision), accessToken);
toast.success("The selected revision is now the current version.", { title: "Version restored" });
historyRevisions = await adapter.loadHistory(accessToken);
renderHistory();
} catch (error) {
toast.danger(error.message, { title: "Could not restore version" });
restoreButton.disabled = false;
}
});
document.querySelector("#history-button").addEventListener("click", async () => {
if (info?.protected && !resourceUnlocked) {
if (!passwordDialog.open) passwordDialog.showModal();
document.querySelector("#open-password")?.focus();
return;
}
historyPanel.classList.add("open");
historyPanel.setAttribute("aria-hidden", "false");
document.body.classList.add("history-open");
historyList.innerHTML = '<p class="empty">Loading…</p>';
try {
historyRevisions = await adapter.loadHistory(accessToken);
historyArchiveYear = null;
historyArchiveDay = null;
renderHistory();
} catch (e) {
historyList.innerHTML = `<p class="error">${escapeHtml(e.message)}</p>`;
toast.danger(e.message, { title: "Could not load version history" });
}
});
document.querySelector("#close-history").addEventListener("click", () => {
historyPanel.classList.remove("open");
historyPanel.setAttribute("aria-hidden", "true");
document.body.classList.remove("history-open");
});
const deleteNoteButton = document.querySelector("#delete-note"); if (deleteNoteButton && adapter.deleteNote) deleteNoteButton.addEventListener("click", async () => { try { await adapter.deleteNote(info, accessToken); } catch (error) { toast.danger(error.message, { title: "Could not delete note" }); } });
initialize();
+43 -3
View File
@@ -45,6 +45,9 @@ const notesPagination = document.querySelector("#notes-pagination");
const workspacePasswordForm = document.querySelector("#workspace-password-form");
const workspacePasswordInput = document.querySelector("#workspace-set-password");
const workspacePasswordError = document.querySelector("#workspace-password-error");
const workspaceSettingsForm = document.querySelector("#workspace-settings-form");
const archiveDeletedNotesToggle = document.querySelector("#archive-deleted-notes");
const workspaceSettingsStatus = document.querySelector("#workspace-settings-status");
let notesPage = 1;
let notesSearchTimer;
const notesViewKey = `rustpad:workspace:${slug}:notes-view`;
@@ -55,6 +58,11 @@ function updateWorkspacePasswordControl() {
workspacePasswordForm.hidden = Boolean(info?.protected || !info?.can_set_password);
}
function updateWorkspaceSettingsControl() {
workspaceSettingsForm.hidden = !info?.can_manage_settings;
archiveDeletedNotesToggle.checked = Boolean(info?.archive_deleted_notes);
}
function stopWorkspaceWatch() {
clearInterval(workspaceWatchPingTimer);
clearTimeout(workspaceWatchReconnectTimer);
@@ -293,6 +301,7 @@ async function openWorkspace() {
renderNotes();
renderNotesPagination(data.pagination);
updateWorkspacePasswordControl();
updateWorkspaceSettingsControl();
workspaceLockedForPassword = false;
workspaceContent.hidden = false;
if (dialog.open) dialog.close();
@@ -313,6 +322,7 @@ async function init() {
document.querySelector("#workspace-title").textContent = info.title;
document.querySelector("#workspace-url").textContent = location.pathname;
updateWorkspacePasswordControl();
updateWorkspaceSettingsControl();
if (info.protected && info.access_level === "none") lockWorkspaceForPassword("Enter the workspace password to continue.");
else await openWorkspace();
} catch (e) {
@@ -364,6 +374,29 @@ workspacePasswordForm.addEventListener("submit", async event => {
submit.disabled = false;
}
});
workspaceSettingsForm.addEventListener("submit", async event => {
event.preventDefault();
workspaceSettingsStatus.textContent = "";
const submit = workspaceSettingsForm.querySelector('button[type="submit"]');
submit.disabled = true;
try {
const updated = await api(`/api/workspaces/${encodeURIComponent(slug)}/settings`, {
method: "POST",
body: JSON.stringify({ archive_deleted_notes: archiveDeletedNotesToggle.checked }),
});
info = { ...info, ...updated };
updateWorkspaceSettingsControl();
workspaceSettingsStatus.textContent = info.archive_deleted_notes
? "Deleted notes will be archived in the database."
: "Deleted notes will be permanently removed.";
toast.success("Workspace deletion policy saved.", { title: "Workspace settings" });
} catch (error) {
workspaceSettingsStatus.textContent = error.message;
toast.danger(error.message, { title: "Could not save workspace settings" });
} finally {
submit.disabled = false;
}
});
document.querySelector("#new-note-button").addEventListener("click", () => document.querySelector("#note-dialog").showModal());
document.querySelector("#cancel-note").addEventListener("click", () => document.querySelector("#note-dialog").close());
document.querySelector("#note-form").addEventListener("submit", async e => {
@@ -387,13 +420,20 @@ notesList.addEventListener("click", async event => {
const button = event.target.closest("[data-delete-note]");
if (!button) return;
const title = button.dataset.noteTitle;
if (!await askConfirm(`Delete note “${title}”? This cannot be undone.`, { title: "Delete note", confirmText: "Delete", danger: true })) return;
const archive = Boolean(info?.archive_deleted_notes);
const message = archive
? `Delete note “${title}”? It will be archived and hidden from this workspace, but kept in the database.`
: `Delete note “${title}”? This cannot be undone.`;
if (!await askConfirm(message, { title: "Delete note", confirmText: archive ? "Archive" : "Delete", danger: true })) return;
button.disabled = true;
try {
await api(`/api/workspaces/${encodeURIComponent(slug)}/notes/${encodeURIComponent(button.dataset.deleteNote)}`, {
const result = await api(`/api/workspaces/${encodeURIComponent(slug)}/notes/${encodeURIComponent(button.dataset.deleteNote)}`, {
method: "DELETE", body: JSON.stringify({ access_token: accessToken || null })
});
toast.success(`The note "${title}" was deleted.`, { title: "Note deleted" });
toast.success(
result.archived ? `The note "${title}" was archived.` : `The note "${title}" was deleted.`,
{ title: result.archived ? "Note archived" : "Note deleted" },
);
await openWorkspace();
} catch (error) { toast.danger(error.message, { title: "Could not delete note" }); button.disabled = false; }
});
+9
View File
@@ -42,6 +42,15 @@
required><button class="secondary-button" type="submit">Set</button></div>
<small id="workspace-password-error" class="form-message error" role="alert"></small>
</form>
<form id="workspace-settings-form" class="workspace-settings-card" hidden>
<div class="workspace-settings-card__copy">
<strong>Deleted notes</strong>
<small>Workspace-wide setting. Archived notes disappear from the workspace but remain in the database with their history and files.</small>
</div>
<label class="workspace-archive-toggle"><input id="archive-deleted-notes" type="checkbox"> Archive deleted notes instead of permanently deleting them</label>
<button class="secondary-button" type="submit">Save</button>
<small id="workspace-settings-status" class="form-message" role="status"></small>
</form>
<div class="notes-toolbar">
<label class="list-search"><span class="sr-only">Search notes</span><input id="notes-search" type="search"
placeholder="Search notes…" autocomplete="off"></label>