This commit is contained in:
Mateusz Gruszczyński committed 2026-10-29 13:07:22 +01:00
1 parent b15a6c54ce
commit d36a4b2451
22 files changed
+926 -114

No files matched your search

+97 -4
View File
@@ -58,6 +58,15 @@ const notesViewKey = `rustpad:workspace:${slug}:notes-view`;
let notesView = localStorage.getItem(notesViewKey) === "table" ? "table" : "grid";
let notesCache = [];
let archivedNotesCache = [];
let noteTitleOpenTimer = 0;
document.addEventListener("pointerdown", event => {
if (!noteTitleOpenTimer) return;
const target = event.target instanceof Element ? event.target : null;
if (target?.closest("[data-note-rename]")) return;
clearTimeout(noteTitleOpenTimer);
noteTitleOpenTimer = 0;
}, true);
function updateWorkspacePasswordControl() {
workspacePasswordForm.hidden = Boolean(info?.protected || !info?.can_set_password);
@@ -266,8 +275,6 @@ async function toggleNoteFavorite(button) {
{ title: t("favorites.title", {}, "Favorites") },
);
} catch (error) {
// The API client already rechecks the session after a 401.
// Only a 403 needs an additional check for an unrelated access restriction.
if (error.status === 403) {
try {
const session = await validateCurrentSession();
@@ -296,7 +303,7 @@ function renderNotes(notes = notesCache) {
notesList.innerHTML = `<div class="notes-table-scroll"><table><thead><tr>${favoriteHeader}<th>Name</th><th>Created by</th><th>Participants</th><th>Files</th><th>Revisions</th><th>Status</th><th>Updated</th><th class="notes-table-actions">Actions</th></tr></thead><tbody>${notes.map(note => `
<tr>
${accountSession ? `<td class="notes-table-favorite">${favoriteButton(note, true)}</td>` : ""}
<td><a class="note-table-link" href="${escapeHtml(safeAppUrl(note.url))}">${escapeHtml(note.title)}</a></td>
<td><a class="note-table-link" href="${escapeHtml(safeAppUrl(note.url))}"><span class="note-list-inline-title${note.can_rename ? " note-list-inline-title--renamable" : ""}" ${note.can_rename ? `data-note-rename="${escapeHtml(note.slug)}" title="${escapeHtml(t("workspace.renameHint", {}, "Double-click to rename"))}"` : ""}>${escapeHtml(note.title)}</span></a></td>
<td class="note-author">${escapeHtml(note.created_by || "Unknown")}</td>
<td>${Number(note.participant_count) || 0}</td>
<td>${Number(note.file_count) || 0} <span class="note-status">(${formatBytes(note.file_size_bytes)})</span></td>
@@ -310,13 +317,70 @@ function renderNotes(notes = notesCache) {
notesList.innerHTML = notes.map(note => `
<article class="note-card-wrap${accountSession ? " note-card-wrap--favorite-enabled" : ""}">
<a class="note-card" href="${escapeHtml(safeAppUrl(note.url))}">
<div class="note-card-title"><h3>${escapeHtml(note.title)}</h3>${note.protected ? '<span class="protect-badge">Protected</span>' : ''}</div>
<div class="note-card-title"><h3><span class="note-list-inline-title${note.can_rename ? " note-list-inline-title--renamable" : ""}" ${note.can_rename ? `data-note-rename="${escapeHtml(note.slug)}" title="${escapeHtml(t("workspace.renameHint", {}, "Double-click to rename"))}"` : ""}>${escapeHtml(note.title)}</span></h3>${note.protected ? '<span class="protect-badge">Protected</span>' : ''}</div>
<div class="note-card-meta"><span>Created by: ${escapeHtml(note.created_by || "Unknown")}</span>${noteStats(note)}<span>Updated: ${formatDate(note.updated_at)}</span></div>
</a>
${accountSession ? `<div class="note-card-favorite">${favoriteButton(note)}</div>` : ""}
${deleteButton(note)}
</article>`).join("");
}
function beginWorkspaceNoteRename(target, note) {
if (!target || !note?.can_rename || target.querySelector("input")) return;
const previous = note.title;
const input = document.createElement("input");
input.className = "note-title-inline-input";
input.type = "text";
input.maxLength = 80;
input.value = previous;
input.setAttribute("aria-label", t("workspace.renameLabel", {}, "Note name"));
target.replaceChildren(input);
target.classList.add("note-list-inline-title--editing");
let finished = false;
const restore = title => {
if (!target.isConnected) return;
target.classList.remove("note-list-inline-title--editing");
target.textContent = title;
};
const finish = async save => {
if (finished) return;
finished = true;
const next = input.value.trim();
if (!save) { restore(previous); return; }
if (!next) {
restore(previous);
toast.danger(t("workspace.renameEmpty", {}, "The note name cannot be empty."), { title: t("workspace.renameFailed", {}, "Could not rename note") });
return;
}
if (next === previous) { restore(previous); return; }
input.disabled = true;
try {
const result = await api(`/api/workspaces/${encodeURIComponent(slug)}/notes/${encodeURIComponent(note.slug)}/rename`, {
method: "POST",
body: JSON.stringify({ name: next, access_token: accessToken || null }),
});
note.title = result.title || next;
note.updated_at = result.updated_at || note.updated_at;
restore(note.title);
toast.success(t("workspace.renamed", { title: note.title }, `Renamed to “${note.title}”.`), { title: t("workspace.renamedTitle", {}, "Note renamed") });
await openWorkspace();
} catch (error) {
restore(previous);
toast.danger(error.message, { title: t("workspace.renameFailed", {}, "Could not rename note") });
}
};
input.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); });
input.addEventListener("dblclick", event => { event.preventDefault(); event.stopPropagation(); });
input.addEventListener("keydown", event => {
if (event.key === "Enter") { event.preventDefault(); event.stopPropagation(); void finish(true); }
else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); void finish(false); }
});
input.addEventListener("blur", () => { void finish(true); }, { once: true });
input.focus();
input.select();
}
async function showSystemNotFound() {
try {
const response = await fetch(`${location.pathname.replace(/\/$/, "")}/__not_found__`, {
@@ -519,6 +583,35 @@ document.querySelector("#note-form").addEventListener("submit", async e => {
} catch (err) { error.textContent = err.message; toast.danger(err.message, { title: "Could not create note" }); }
});
notesList.addEventListener("click", async event => {
const activeInput = event.target.closest(".note-title-inline-input");
if (activeInput) {
event.preventDefault();
event.stopPropagation();
return;
}
const editingTitle = notesList.querySelector(".note-list-inline-title--editing");
if (editingTitle) {
event.preventDefault();
return;
}
const renameTarget = event.target.closest("[data-note-rename]");
if (renameTarget) {
const note = notesCache.find(item => item.slug === renameTarget.dataset.noteRename);
if (note?.can_rename) {
event.preventDefault();
clearTimeout(noteTitleOpenTimer);
if (event.detail >= 2) {
noteTitleOpenTimer = 0;
beginWorkspaceNoteRename(renameTarget, note);
} else {
noteTitleOpenTimer = window.setTimeout(() => {
noteTitleOpenTimer = 0;
location.assign(safeAppUrl(note.url));
}, 400);
}
return;
}
}
const favorite = event.target.closest("[data-favorite-note]");
if (favorite) {
event.preventDefault();