favourited notes
This commit is contained in:
+74
-5
@@ -12,12 +12,12 @@ installGlobalDiagnostics();
|
||||
|
||||
import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { getNickname, getAccessToken, getAuthToken, getGuestId, setAccessToken } from "@rustpad/session";
|
||||
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 } from "@rustpad/i18n";
|
||||
import { formatDateTime, formatNumber, t } from "@rustpad/i18n";
|
||||
|
||||
consumeQueuedToast();
|
||||
|
||||
@@ -27,6 +27,7 @@ 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;
|
||||
@@ -177,6 +178,57 @@ function deleteButton(note, inline = false) {
|
||||
: "Read-write access is required to delete this note";
|
||||
return `<button class="${classes}" data-delete-note="${escapeHtml(note.slug)}" data-note-title="${escapeHtml(note.title)}" ${disabled ? "disabled" : ""} title="${escapeHtml(reason)}">Delete</button>`;
|
||||
}
|
||||
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 `<button class="note-favorite-button${inline ? " note-favorite-button--inline" : ""}" type="button" data-favorite-note="${escapeHtml(note.slug)}" aria-pressed="${active}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}"><span aria-hidden="true">${active ? "★" : "☆"}</span></button>`;
|
||||
}
|
||||
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 === 401 || error.status === 403) {
|
||||
const session = await validateCurrentSession();
|
||||
if (!session) {
|
||||
accountSession = null;
|
||||
renderNotes();
|
||||
}
|
||||
}
|
||||
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);
|
||||
@@ -185,8 +237,10 @@ function renderNotes(notes = notesCache) {
|
||||
return;
|
||||
}
|
||||
if (notesView === "table") {
|
||||
notesList.innerHTML = `<div class="notes-table-scroll"><table><thead><tr><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 => `
|
||||
const favoriteHeader = accountSession ? '<th class="notes-table-favorite"><span class="sr-only">Favorites</span>★</th>' : "";
|
||||
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 class="note-author">${escapeHtml(note.created_by || "Unknown")}</td>
|
||||
<td>${Number(note.participant_count) || 0}</td>
|
||||
@@ -199,11 +253,12 @@ function renderNotes(notes = notesCache) {
|
||||
return;
|
||||
}
|
||||
notesList.innerHTML = notes.map(note => `
|
||||
<article class="note-card-wrap">
|
||||
<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-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("");
|
||||
}
|
||||
@@ -323,6 +378,12 @@ 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 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;
|
||||
@@ -350,6 +411,7 @@ document.querySelector("#copy-workspace-link").addEventListener("click", async (
|
||||
});
|
||||
async function startAuthorizedWorkspace() {
|
||||
const session = await validateCurrentSession();
|
||||
accountSession = session;
|
||||
nickname = session?.nickname || getNickname();
|
||||
|
||||
if (!nickname) {
|
||||
@@ -365,8 +427,9 @@ async function startAuthorizedWorkspace() {
|
||||
|
||||
bindIdentityDialog({
|
||||
dialog: identityDialog,
|
||||
onIdentity: async value => {
|
||||
onIdentity: async (value, session) => {
|
||||
nickname = value;
|
||||
accountSession = session;
|
||||
accessToken = shareToken || getAccessToken("workspace", slug);
|
||||
workspaceContent.hidden = false;
|
||||
await init();
|
||||
@@ -386,6 +449,12 @@ dialog.addEventListener("cancel", event => {
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("rustpad:session-change", event => {
|
||||
if (event.detail?.session) return;
|
||||
if (!accountSession) return;
|
||||
accountSession = null;
|
||||
renderNotes();
|
||||
});
|
||||
window.addEventListener("beforeunload", stopWorkspaceWatch);
|
||||
|
||||
setNotesView(notesView);
|
||||
|
||||
Reference in New Issue
Block a user