199 lines
9.3 KiB
JavaScript
199 lines
9.3 KiB
JavaScript
import { installGlobalDiagnostics, logInfo } from "@rustpad/logger";
|
|
installGlobalDiagnostics();
|
|
|
|
import { api } from "@rustpad/api";
|
|
import { copyText } from "@rustpad/clipboard";
|
|
import { getNickname, getAccessToken, getAuthToken, setAccessToken } from "@rustpad/session";
|
|
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
|
|
import { askConfirm } from "@rustpad/modal";
|
|
|
|
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();
|
|
if (shareToken) setAccessToken("workspace", slug, shareToken);
|
|
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 notesViewKey = `rustpad:workspace:${slug}:notes-view`;
|
|
let notesView = localStorage.getItem(notesViewKey) === "table" ? "table" : "grid";
|
|
let notesCache = [];
|
|
|
|
function toast(text) {
|
|
const el = document.querySelector("#toast");
|
|
el.textContent = text;
|
|
el.classList.add("visible");
|
|
setTimeout(() => el.classList.remove("visible"), 1600);
|
|
}
|
|
function escapeHtml(v) { const e = document.createElement("div"); e.textContent = v; return e.innerHTML; }
|
|
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()) ? "—" : date.toLocaleString("pl-PL");
|
|
}
|
|
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.protected;
|
|
const classes = `note-delete-button${inline ? " note-delete-button--inline" : ""}`;
|
|
const reason = disabled ? "Protected notes cannot be deleted" : `Delete ${note.title}`;
|
|
return `<button class="${classes}" data-delete-note="${escapeHtml(note.slug)}" data-note-title="${escapeHtml(note.title)}" ${disabled ? "disabled" : ""} title="${escapeHtml(reason)}">Delete</button>`;
|
|
}
|
|
function renderNotes(notes = notesCache) {
|
|
notesCache = notes;
|
|
setNotesView(notesView);
|
|
if (!notes.length) {
|
|
notesList.innerHTML = '<p class="empty">No notes yet.</p>';
|
|
return;
|
|
}
|
|
if (notesView === "table") {
|
|
notesList.innerHTML = `<div class="notes-table-scroll"><table><thead><tr><th>Name</th><th>Created by</th><th>Status</th><th>Updated</th><th class="notes-table-actions">Actions</th></tr></thead><tbody>${notes.map(note => `
|
|
<tr>
|
|
<td><a class="note-table-link" href="${note.url}?view=split&mode=markdown">${escapeHtml(note.title)}</a></td>
|
|
<td class="note-author">${escapeHtml(note.created_by || "Unknown")}</td>
|
|
<td>${note.protected ? '<span class="protect-badge">Protected</span>' : '<span class="note-status">Unprotected</span>'}</td>
|
|
<td>${formatDate(note.updated_at)}</td>
|
|
<td class="notes-table-actions">${deleteButton(note, true)}</td>
|
|
</tr>`).join("")}</tbody></table></div>`;
|
|
return;
|
|
}
|
|
notesList.innerHTML = notes.map(note => `
|
|
<article class="note-card-wrap">
|
|
<a class="note-card" href="${note.url}?view=split&mode=markdown">
|
|
<div class="note-card-title"><h3>${escapeHtml(note.title)}</h3>${note.protected ? '<span class="protect-badge">Protected</span>' : ''}</div>
|
|
<div class="note-card-meta"><span>Created by: ${escapeHtml(note.created_by || "Unknown")}</span><span>Updated: ${formatDate(note.updated_at)}</span></div>
|
|
</a>
|
|
${deleteButton(note)}
|
|
</article>`).join("");
|
|
}
|
|
async function openWorkspace() {
|
|
try {
|
|
const data = await api(`/api/workspaces/${encodeURIComponent(slug)}/open`, { method: "POST", body: JSON.stringify({ access_token: accessToken || null }) });
|
|
info = data.workspace;
|
|
document.querySelector("#workspace-title").textContent = info.title;
|
|
document.querySelector("#workspace-url").textContent = location.pathname;
|
|
document.title = `${info.title} · RustPad`;
|
|
notesCache = data.notes;
|
|
renderNotes();
|
|
if (dialog.open) dialog.close();
|
|
} catch (e) {
|
|
if (info?.protected || e.message.toLowerCase().includes("password")) {
|
|
document.querySelector("#password-error").textContent = e.message;
|
|
if (!dialog.open) dialog.showModal();
|
|
} else if (e.status === 403) {
|
|
location.replace("/errors/private-workspace");
|
|
} else document.querySelector("#workspace-error").textContent = e.message;
|
|
}
|
|
}
|
|
async function init() {
|
|
try {
|
|
const headers = accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
|
|
info = await api(`/api/workspaces/${encodeURIComponent(slug)}`, { headers });
|
|
document.querySelector("#workspace-title").textContent = info.title;
|
|
document.querySelector("#workspace-url").textContent = location.pathname;
|
|
if (info.protected && !accessToken) dialog.showModal(); else openWorkspace();
|
|
} catch (e) {
|
|
if (e.status === 403) location.replace("/errors/private-workspace");
|
|
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 }) });
|
|
accessToken = result.access_token;
|
|
setAccessToken("workspace", slug, accessToken);
|
|
document.querySelector("#open-password").value = "";
|
|
document.querySelector("#password-error").textContent = "";
|
|
openWorkspace();
|
|
} catch (error) { document.querySelector("#password-error").textContent = error.message; }
|
|
});
|
|
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 => {
|
|
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(`${note.url}?view=split&mode=markdown`);
|
|
} catch (err) { error.textContent = err.message; }
|
|
});
|
|
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;
|
|
button.disabled = true;
|
|
try {
|
|
await api(`/api/workspaces/${encodeURIComponent(slug)}/notes/${encodeURIComponent(button.dataset.deleteNote)}`, {
|
|
method: "DELETE", body: JSON.stringify({ access_token: accessToken || null })
|
|
});
|
|
toast("Note deleted");
|
|
await openWorkspace();
|
|
} catch (error) { toast(error.message); button.disabled = false; }
|
|
});
|
|
document.querySelectorAll("[data-notes-view]").forEach(button => button.addEventListener("click", () => {
|
|
if (button.dataset.notesView === notesView) return;
|
|
notesView = button.dataset.notesView;
|
|
renderNotes();
|
|
}));
|
|
document.querySelector("#copy-workspace-link").addEventListener("click", async () => {
|
|
try { await copyText(new URL(location.pathname, location.origin).href); toast("Link copied"); }
|
|
catch (e) { toast(e.message); }
|
|
});
|
|
async function startAuthorizedWorkspace() {
|
|
const hadAccountToken = Boolean(getAuthToken());
|
|
if (hadAccountToken) {
|
|
const session = await validateCurrentSession();
|
|
nickname = session?.nickname || getNickname();
|
|
}
|
|
|
|
if (!nickname) {
|
|
workspaceContent.hidden = true;
|
|
if (!identityDialog.open) identityDialog.showModal();
|
|
return;
|
|
}
|
|
|
|
accessToken = shareToken || getAuthToken() || getAccessToken("workspace", slug);
|
|
workspaceContent.hidden = false;
|
|
await init();
|
|
}
|
|
|
|
bindIdentityDialog({
|
|
dialog: identityDialog,
|
|
onIdentity: async value => {
|
|
nickname = value;
|
|
accessToken = shareToken || getAuthToken() || getAccessToken("workspace", slug);
|
|
workspaceContent.hidden = false;
|
|
await init();
|
|
},
|
|
});
|
|
identityDialog.addEventListener("close", () => {
|
|
if (!nickname) queueMicrotask(() => {
|
|
workspaceContent.hidden = true;
|
|
if (!identityDialog.open) identityDialog.showModal();
|
|
});
|
|
});
|
|
|
|
setNotesView(notesView);
|
|
startAuthorizedWorkspace();
|