/* * Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl * Source-Available Code / Dual-Licensed. * * Free for non-commercial and evaluation use under terms of BSL/GPLv3. * Commercial or production use requires a valid paid license. * See LICENSE file in repository root for details. */ import { installGlobalDiagnostics, logInfo } from "@rustpad/logger"; installGlobalDiagnostics(); import { api } from "@rustpad/api"; import { copyText } from "@rustpad/clipboard"; import { getNickname, getAccessToken, getGuestId, setAccessToken } from "@rustpad/session"; import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui"; import { askConfirm } from "@rustpad/modal"; import { isResourceAccessError, safeAppUrl } from "@rustpad/security"; import { consumeQueuedToast, toast } from "@rustpad/toast"; import { formatDateTime, formatNumber, t } from "@rustpad/i18n"; consumeQueuedToast(); const parts = location.pathname.split("/").filter(Boolean); const slug = parts[1]; let info; const shareToken = new URLSearchParams(location.search).get("share"); let accessToken = shareToken || getAccessToken("workspace", slug); let nickname = getNickname(); let accountSession = null; getGuestId(); const workspaceWatchClientId = `workspace_watch_${crypto.randomUUID()}`; let workspaceWatchSocket; let workspaceWatchPingTimer; let workspaceWatchReconnectTimer; let workspaceWatchIntentionalClose = false; let workspaceLockedForPassword = false; const dialog = document.querySelector("#password-dialog"); const identityDialog = document.querySelector("#identity-dialog"); const workspaceContent = document.querySelector("#workspace-content"); const notesList = document.querySelector("#notes-list"); const notesSearch = document.querySelector("#notes-search"); const notesPerPage = document.querySelector("#notes-per-page"); 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 workspaceSettingsButton = document.querySelector("#workspace-settings-button"); const archiveDeletedNotesToggle = document.querySelector("#archive-deleted-notes"); const workspaceSettingsStatus = document.querySelector("#workspace-settings-status"); const archivedNotesButton = document.querySelector("#archived-notes-button"); const archivedNotesDialog = document.querySelector("#archived-notes-dialog"); const archivedNotesList = document.querySelector("#archived-notes-list"); let notesPage = 1; let notesSearchTimer; 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); } function updateWorkspaceSettingsControl() { const canManage = Boolean(info?.can_manage_settings); workspaceSettingsButton.hidden = !canManage; archivedNotesButton.hidden = !canManage; if (!canManage) { workspaceSettingsForm.hidden = true; workspaceSettingsButton.setAttribute("aria-expanded", "false"); } archiveDeletedNotesToggle.checked = Boolean(info?.archive_deleted_notes); } function stopWorkspaceWatch() { clearInterval(workspaceWatchPingTimer); clearTimeout(workspaceWatchReconnectTimer); workspaceWatchPingTimer = undefined; workspaceWatchReconnectTimer = undefined; if (workspaceWatchSocket) { workspaceWatchIntentionalClose = true; workspaceWatchSocket.close(); workspaceWatchSocket = undefined; } } function lockWorkspaceForPassword(message = "A password was set for this workspace. Enter it to continue.") { workspaceLockedForPassword = true; info = { ...info, protected: true, access_level: "none", can_set_password: false }; stopWorkspaceWatch(); workspaceContent.hidden = true; document.querySelector("#password-error").textContent = message; if (!dialog.open) dialog.showModal(); document.querySelector("#open-password")?.focus(); } function scheduleWorkspaceWatchReconnect() { clearTimeout(workspaceWatchReconnectTimer); if (workspaceLockedForPassword || !nickname) return; workspaceWatchReconnectTimer = window.setTimeout(connectWorkspaceWatch, 1500); } function connectWorkspaceWatch() { if (workspaceLockedForPassword || !nickname) return; if (workspaceWatchSocket?.readyState === WebSocket.OPEN || workspaceWatchSocket?.readyState === WebSocket.CONNECTING) return; clearTimeout(workspaceWatchReconnectTimer); const protocol = location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket(`${protocol}//${location.host}/ws/watch/workspace/${encodeURIComponent(slug)}`); workspaceWatchSocket = socket; workspaceWatchIntentionalClose = false; socket.addEventListener("open", () => { if (socket !== workspaceWatchSocket) return; socket.send(JSON.stringify({ type: "authenticate", access_token: accessToken || null, client_id: workspaceWatchClientId, })); clearInterval(workspaceWatchPingTimer); workspaceWatchPingTimer = window.setInterval(() => { if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "ping", nonce: Date.now() })); }, 10000); }); socket.addEventListener("message", event => { if (socket !== workspaceWatchSocket) return; let message; try { message = JSON.parse(event.data); } catch { return; } if (message.type === "password_required") { workspaceWatchIntentionalClose = true; lockWorkspaceForPassword(); return; } if (message.type === "password_changed") { workspaceWatchIntentionalClose = true; socket.close(); return; } if (message.type === "error") { workspaceWatchIntentionalClose = true; socket.close(); if (isResourceAccessError(message.message)) lockWorkspaceForPassword(message.message); else document.querySelector("#workspace-error").textContent = message.message || "Workspace connection error"; } }); socket.addEventListener("close", () => { if (socket !== workspaceWatchSocket) return; clearInterval(workspaceWatchPingTimer); workspaceWatchPingTimer = undefined; workspaceWatchSocket = undefined; if (!workspaceWatchIntentionalClose) scheduleWorkspaceWatchReconnect(); }); socket.addEventListener("error", () => { if (socket.readyState !== WebSocket.CLOSING && socket.readyState !== WebSocket.CLOSED) socket.close(); }); } function escapeHtml(v) { const e = document.createElement("div"); e.textContent = v; return e.innerHTML; } function formatBytes(value) { const bytes = Math.max(0, Number(value) || 0); if (bytes < 1024) return `${formatNumber(bytes)} B`; const units = ["KB", "MB", "GB", "TB"]; let amount = bytes; let unit = -1; do { amount /= 1024; unit++; } while (amount >= 1024 && unit < units.length - 1); return `${formatNumber(amount, { maximumFractionDigits: amount >= 10 ? 0 : 1 })} ${units[unit]}`; } function noteStats(note) { return `Participants: ${Number(note.participant_count) || 0}Files: ${Number(note.file_count) || 0} (${formatBytes(note.file_size_bytes)})Revisions: ${Number(note.revision_count) || 0}`; } function formatDate(value) { if (value == null || value === "") return "—"; let raw = String(value).trim(); if (/^\d+$/.test(raw)) { const number = Number(raw); raw = raw.length <= 10 ? number * 1000 : number; } else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(raw)) raw = raw.replace(" ", "T") + "Z"; const date = new Date(raw); return Number.isNaN(date.getTime()) ? "—" : formatDateTime(date); } function renderArchivedNotes(notes) { archivedNotesCache = Array.isArray(notes) ? notes : []; if (!archivedNotesCache.length) { archivedNotesList.innerHTML = `

${escapeHtml(t("workspace.archive.empty", {}, "No archived notes."))}

`; return; } archivedNotesList.innerHTML = archivedNotesCache.map(note => { const author = note.created_by || t("workspace.unknown", {}, "Unknown"); return `
${escapeHtml(note.title)} ${escapeHtml(t("workspace.archive.archivedAt", { date: formatDate(note.deleted_at) }, `Archived: ${formatDate(note.deleted_at)}`))} ${escapeHtml(t("workspace.archive.createdBy", { author }, `Created by: ${author}`))}
`; }).join(""); } async function loadArchivedNotes() { archivedNotesList.innerHTML = `

${escapeHtml(t("common.loading", {}, "Loading…"))}

`; try { const notes = await api(`/api/workspaces/${encodeURIComponent(slug)}/archived-notes`); renderArchivedNotes(notes); } catch (error) { archivedNotesList.innerHTML = `

${escapeHtml(error.message)}

`; toast.danger(error.message, { title: t("workspace.archive.loadFailed", {}, "Could not load archived notes") }); } } function setNotesView(view) { notesView = view === "table" ? "table" : "grid"; localStorage.setItem(notesViewKey, notesView); notesList.classList.toggle("notes-grid", notesView === "grid"); notesList.classList.toggle("notes-table", notesView === "table"); document.querySelectorAll("[data-notes-view]").forEach(button => { const active = button.dataset.notesView === notesView; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); } function deleteButton(note, inline = false) { const disabled = !note.can_delete; const classes = `note-delete-button${inline ? " note-delete-button--inline" : ""}`; const reason = !disabled ? `Delete ${note.title}` : note.protected ? "Protected note: only its owner can delete it" : "Read-write access is required to delete this note"; return ``; } function favoriteButton(note, inline = false) { if (!accountSession) return ""; const active = Boolean(note.favorite); const label = active ? t("favorites.remove", {}, "Remove from favorites") : t("favorites.add", {}, "Add to favorites"); return ``; } function updateFavoriteButton(button, active) { const favorite = Boolean(active); const label = favorite ? t("favorites.remove", {}, "Remove from favorites") : t("favorites.add", {}, "Add to favorites"); button.setAttribute("aria-pressed", String(favorite)); button.title = label; button.setAttribute("aria-label", label); const icon = button.querySelector("span"); if (icon) icon.textContent = favorite ? "★" : "☆"; } async function toggleNoteFavorite(button) { if (!accountSession) return; const note = notesCache.find(item => item.slug === button.dataset.favoriteNote); if (!note) return; const current = Boolean(note.favorite); button.disabled = true; try { const result = await api("/api/auth/favorites", { method: current ? "DELETE" : "PUT", body: JSON.stringify({ kind: "note", slug: note.slug, workspace_slug: slug }), }); note.favorite = Boolean(result.favorite); document.querySelectorAll(`[data-favorite-note="${CSS.escape(note.slug)}"]`).forEach(target => updateFavoriteButton(target, note.favorite)); toast.success( note.favorite ? t("favorites.added", {}, "Added to favorites.") : t("favorites.removed", {}, "Removed from favorites."), { title: t("favorites.title", {}, "Favorites") }, ); } catch (error) { if (error.status === 403) { try { const session = await validateCurrentSession(); if (!session) { accountSession = null; renderNotes(); } } catch { // A failed recheck is not evidence that the account session expired. } } toast.danger(error.message, { title: t("favorites.updateFailed", {}, "Could not update favorites") }); } finally { if (button.isConnected) button.disabled = false; } } function renderNotes(notes = notesCache) { notesCache = notes; setNotesView(notesView); if (!notes.length) { notesList.innerHTML = '

No notes yet.

'; return; } if (notesView === "table") { const favoriteHeader = accountSession ? 'Favorites★' : ""; notesList.innerHTML = `
${favoriteHeader}${notes.map(note => ` ${accountSession ? `` : ""} `).join("")}
NameCreated byParticipantsFilesRevisionsStatusUpdatedActions
${favoriteButton(note, true)}${escapeHtml(note.title)} ${escapeHtml(note.created_by || "Unknown")} ${Number(note.participant_count) || 0} ${Number(note.file_count) || 0} (${formatBytes(note.file_size_bytes)}) ${Number(note.revision_count) || 0} ${note.protected ? 'Protected' : 'Unprotected'} ${formatDate(note.updated_at)} ${deleteButton(note, true)}
`; return; } notesList.innerHTML = notes.map(note => `

${escapeHtml(note.title)}

${note.protected ? 'Protected' : ''}
Created by: ${escapeHtml(note.created_by || "Unknown")}${noteStats(note)}Updated: ${formatDate(note.updated_at)}
${accountSession ? `
${favoriteButton(note)}
` : ""} ${deleteButton(note)}
`).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__`, { cache: "no-store", credentials: "same-origin", }); const html = await response.text(); document.open(); document.write(html); document.close(); } catch { document.body.textContent = "404 Not Found"; } } function renderNotesPagination(meta) { notesPagination.innerHTML = meta.total ? `Page ${meta.page} of ${meta.total_pages} · ${meta.total} notes` : ""; } async function openWorkspace() { try { const params = new URLSearchParams({ q: notesSearch.value.trim(), page: String(notesPage), per_page: notesPerPage.value }); const data = await api(`/api/workspaces/${encodeURIComponent(slug)}/open?${params}`, { method: "POST", body: JSON.stringify({ access_token: accessToken || null }) }); info = data.workspace; document.querySelector("#workspace-title").removeAttribute("data-i18n"); document.querySelector("#workspace-title").textContent = info.title; document.querySelector("#workspace-url").textContent = location.pathname; document.title = `${info.title} · RustPad`; notesPage = data.pagination.page; notesCache = data.notes; renderNotes(); renderNotesPagination(data.pagination); updateWorkspacePasswordControl(); updateWorkspaceSettingsControl(); workspaceLockedForPassword = false; workspaceContent.hidden = false; if (dialog.open) dialog.close(); connectWorkspaceWatch(); } catch (e) { if (info?.protected && isResourceAccessError(e)) { lockWorkspaceForPassword(e.message); } else if (e.status === 403 || e.status === 404) { await showSystemNotFound(); } else document.querySelector("#workspace-error").textContent = e.message; } } async function init() { try { const headers = accessToken && accessToken !== "cookie" ? { Authorization: `Bearer ${accessToken}` } : {}; info = await api(`/api/workspaces/${encodeURIComponent(slug)}`, { headers }); document.querySelector("#workspace-title").removeAttribute("data-i18n"); 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) { if (e.status === 403 || e.status === 404) await showSystemNotFound(); else document.querySelector("#workspace-error").textContent = e.message; } } document.querySelector("#password-form").addEventListener("submit", async e => { e.preventDefault(); try { const password = document.querySelector("#open-password").value; const result = await api("/api/access-token", { method: "POST", body: JSON.stringify({ kind: "workspace", slug, password }) }); setAccessToken("workspace", slug, result.granted); accessToken = getAccessToken("workspace", slug); document.querySelector("#open-password").value = ""; document.querySelector("#password-error").textContent = ""; await openWorkspace(); toast.success("Workspace access has been unlocked.", { title: "Workspace unlocked" }); } catch (error) { document.querySelector("#password-error").textContent = error.message; toast.danger(error.message, { title: "Could not unlock workspace" }); } }); workspacePasswordForm.addEventListener("submit", async event => { event.preventDefault(); const password = workspacePasswordInput.value; workspacePasswordError.textContent = ""; if (password.length < 8) { workspacePasswordError.textContent = "Password must contain at least 8 characters."; return; } const submit = workspacePasswordForm.querySelector('button[type="submit"]'); submit.disabled = true; try { await api(`/api/workspaces/${encodeURIComponent(slug)}/password`, { method: "POST", body: JSON.stringify({ password, client_id: workspaceWatchClientId }), }); const result = await api("/api/access-token", { method: "POST", body: JSON.stringify({ kind: "workspace", slug, password }), }); setAccessToken("workspace", slug, result.granted); accessToken = getAccessToken("workspace", slug); workspacePasswordInput.value = ""; toast.success("Password protection is now enabled for this workspace.", { title: "Workspace protected" }); await openWorkspace(); } catch (error) { workspacePasswordError.textContent = error.message; toast.danger(error.message, { title: "Could not set workspace password" }); } finally { submit.disabled = false; } }); workspaceSettingsButton.addEventListener("click", () => { if (workspaceSettingsButton.hidden) return; const willOpen = workspaceSettingsForm.hidden; workspaceSettingsForm.hidden = !willOpen; workspaceSettingsButton.setAttribute("aria-expanded", String(willOpen)); if (willOpen) { workspaceSettingsStatus.textContent = ""; archiveDeletedNotesToggle.checked = Boolean(info?.archive_deleted_notes); } }); archivedNotesButton.addEventListener("click", () => { if (!archivedNotesDialog.open) archivedNotesDialog.showModal(); void loadArchivedNotes(); }); document.querySelector("#close-archived-notes").addEventListener("click", () => archivedNotesDialog.close()); archivedNotesDialog.addEventListener("click", event => { if (event.target === archivedNotesDialog) archivedNotesDialog.close(); }); archivedNotesList.addEventListener("click", async event => { const button = event.target.closest("[data-restore-archived-note]"); if (!button) return; const noteSlug = button.dataset.restoreArchivedNote; const archivedNote = archivedNotesCache.find(note => note.slug === noteSlug); const title = archivedNote?.title || noteSlug; const confirmed = await askConfirm( t("workspace.archive.restoreAsk", { title }, `Restore note “${title}” to this workspace?`), { title: t("workspace.archive.restoreTitle", {}, "Restore archived note"), confirmText: t("workspace.archive.restore", {}, "Restore"), }, ); if (!confirmed) return; button.disabled = true; try { await api(`/api/workspaces/${encodeURIComponent(slug)}/archived-notes/${encodeURIComponent(noteSlug)}/restore`, { method: "POST" }); toast.success( t("workspace.archive.restored", { title }, `The note "${title}" was restored.`), { title: t("workspace.archive.restoredTitle", {}, "Note restored") }, ); await Promise.all([loadArchivedNotes(), openWorkspace()]); } catch (error) { toast.danger(error.message, { title: t("workspace.archive.restoreFailed", {}, "Could not restore note") }); if (button.isConnected) button.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 ? t("workspace.settings.archiveEnabled", {}, "Deleted notes will be archived in the database.") : t("workspace.settings.archiveDisabled", {}, "Deleted notes will be permanently removed."); workspaceSettingsForm.hidden = true; workspaceSettingsButton.setAttribute("aria-expanded", "false"); toast.success( t("workspace.settings.saved", {}, "Workspace deletion policy saved."), { title: t("workspace.settings.title", {}, "Workspace settings") }, ); } catch (error) { workspaceSettingsStatus.textContent = error.message; toast.danger(error.message, { title: t("workspace.settings.saveFailed", {}, "Could not save workspace settings") }); } finally { submit.disabled = false; } }); function updateNoteProtectionExplanation() { const message = document.querySelector("#note-archive-explanation"); if (!message) return; message.textContent = info?.archive_deleted_notes ? t("workspace.noteProtection.archiveEnabled", {}, "Archiving is enabled: authorized deletion moves the note, its history and files to Archived notes. The workspace owner can restore it.") : t("workspace.noteProtection.archiveDisabled", {}, "Archiving is disabled: authorized deletion permanently removes the note and its files. Enable archiving in Workspace settings if recovery is important."); } document.querySelector("#new-note-button").addEventListener("click", () => { updateNoteProtectionExplanation(); document.querySelector("#note-dialog").showModal(); }); document.querySelector("#cancel-note").addEventListener("click", () => document.querySelector("#note-dialog").close()); document.querySelector("#note-form").addEventListener("submit", async e => { e.preventDefault(); const error = document.querySelector("#note-error"); error.textContent = ""; try { const note = await api(`/api/workspaces/${encodeURIComponent(slug)}/notes`, { method: "POST", body: JSON.stringify({ name: document.querySelector("#note-name").value, access_token: accessToken || null, protect: document.querySelector("#note-protect").checked, created_by: nickname || null }) }); location.assign(safeAppUrl(note.url)); } 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(); await toggleNoteFavorite(favorite); return; } const button = event.target.closest("[data-delete-note]"); if (!button) return; const title = button.dataset.noteTitle; 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 { const result = await api(`/api/workspaces/${encodeURIComponent(slug)}/notes/${encodeURIComponent(button.dataset.deleteNote)}`, { method: "DELETE", body: JSON.stringify({ access_token: accessToken || null }) }); 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; } }); document.querySelectorAll("[data-notes-view]").forEach(button => button.addEventListener("click", () => { if (button.dataset.notesView === notesView) return; notesView = button.dataset.notesView; renderNotes(); })); notesSearch.addEventListener("input", () => { clearTimeout(notesSearchTimer); notesSearchTimer = setTimeout(() => { notesPage = 1; openWorkspace(); }, 250); }); notesPerPage.addEventListener("change", () => { notesPage = 1; openWorkspace(); }); notesPagination.addEventListener("click", event => { const button = event.target.closest("[data-page]"); if (!button || button.disabled) return; notesPage = Number(button.dataset.page) || 1; openWorkspace(); }); document.querySelector("#copy-workspace-link").addEventListener("click", async () => { try { await copyText(new URL(location.pathname, location.origin).href); toast.success("Workspace link copied to the clipboard.", { title: "Link copied" }); } catch (e) { toast.danger(e.message, { title: "Could not copy workspace link" }); } }); async function startAuthorizedWorkspace() { const loading = window.__RUSTPAD_LOADING__; document.querySelector("#workspace-startup-error")?.setAttribute("hidden", ""); try { const session = await validateCurrentSession(); accountSession = session; nickname = session?.nickname || getNickname(); if (!nickname) { loading?.complete("access"); workspaceContent.hidden = true; if (!identityDialog.open) identityDialog.showModal(); loading?.skip("files"); loading?.ready(); return; } accessToken = shareToken || getAccessToken("workspace", slug); workspaceContent.hidden = false; await init(); loading?.complete("access"); if (workspaceLockedForPassword) loading?.skip("files"); else loading?.complete("files"); loading?.ready(); } catch (error) { loading?.fail(error); console.error("Workspace initialization failed", error); workspaceContent.hidden = true; const panel = document.querySelector("#workspace-startup-error"); const message = document.querySelector("#workspace-startup-message"); if (message) message.textContent = `${t("workspace.connectionError", {}, "Workspace connection error")}: ${error?.message || "Please retry."}`; if (panel) panel.hidden = false; } } document.querySelector("#workspace-startup-retry")?.addEventListener("click", async event => { const button = event.currentTarget; button.disabled = true; try { await startAuthorizedWorkspace(); } finally { button.disabled = false; } }); bindIdentityDialog({ dialog: identityDialog, onIdentity: async (value, session) => { nickname = value; accountSession = session; accessToken = shareToken || getAccessToken("workspace", slug); workspaceContent.hidden = false; await init(); }, }); identityDialog.addEventListener("close", () => { if (!nickname) queueMicrotask(() => { workspaceContent.hidden = true; if (!identityDialog.open) identityDialog.showModal(); }); }); dialog.addEventListener("cancel", event => { if (workspaceLockedForPassword) { event.preventDefault(); document.querySelector("#open-password")?.focus(); } }); window.addEventListener("rustpad:session-change", event => { if (event.detail?.session) return; if (!accountSession) return; accountSession = null; renderNotes(); }); window.addEventListener("beforeunload", stopWorkspaceWatch); setNotesView(notesView); startAuthorizedWorkspace();