cmap functions, email, architecture documentation.
This commit is contained in:
+398
-16
@@ -43,6 +43,7 @@
|
||||
let cmapStatusTimer = null;
|
||||
let cmapAutosaveTimer = null;
|
||||
let cmapSavePromise = null;
|
||||
let cmapEmbedHydrationTimer = null;
|
||||
|
||||
const CMAP_AUTOSAVE_DELAY = 1500;
|
||||
|
||||
@@ -58,7 +59,7 @@
|
||||
const wikiCmapLinkCombobox = new window.RacketWikiComboBox($("wiki-cmap-link-combobox"));
|
||||
|
||||
function show(viewId) {
|
||||
for (const id of ["page-view", "not-found-view", "editor-view", "rename-view", "search-view", "recent-view", "bookmarks-view", "todo-view", "graph-view", "cmap-view", "history-view", "admin-view", "alias-admin-view", "user-admin-view", "orphaned-uploads-view"]) {
|
||||
for (const id of ["page-view", "not-found-view", "editor-view", "rename-view", "search-view", "recent-view", "bookmarks-view", "todo-view", "graph-view", "cmap-view", "history-view", "profile-view", "admin-view", "alias-admin-view", "user-admin-view", "mail-admin-view", "orphaned-uploads-view"]) {
|
||||
$(id).classList.toggle("hidden", id !== viewId);
|
||||
}
|
||||
$("page-action-links").classList.toggle("hidden", viewId !== "page-view");
|
||||
@@ -492,13 +493,123 @@
|
||||
* post : The returned HTML is sanitized with DOMPurify.
|
||||
* result : Safe HTML for preview, reader view or history view.
|
||||
*/
|
||||
function extractCmapEmbeds(markdown) {
|
||||
const embeds = [];
|
||||
let fence = null;
|
||||
const lines = String(markdown || "").split("\n").map((line) => {
|
||||
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
|
||||
if (fenceMatch) {
|
||||
const marker = fenceMatch[1].charAt(0);
|
||||
if (fence === null) fence = marker;
|
||||
else if (fence === marker) fence = null;
|
||||
return line;
|
||||
}
|
||||
if (fence !== null) return line;
|
||||
const match = line.match(/^\s*\{\{cmap:([^{}\n]+)\}\}\s*$/i);
|
||||
if (!match) return line;
|
||||
const token = `RACKETWIKICMAPEMBED${embeds.length}TOKEN`;
|
||||
embeds.push({ token, reference: match[1].trim() });
|
||||
return token;
|
||||
});
|
||||
return { markdown: lines.join("\n"), embeds };
|
||||
}
|
||||
|
||||
function restoreCmapEmbeds(html, embeds) {
|
||||
let result = html;
|
||||
for (const embed of embeds) {
|
||||
const placeholder = `<section class="rw-cmap-embed" data-cmap-reference="${escapeHtml(embed.reference)}"><div class="rw-cmap-embed-loading">${escapeHtml(tr("loading", "Loading…"))}</div></section>`;
|
||||
result = result.replace(`<p>${embed.token}</p>`, placeholder).replace(embed.token, placeholder);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function queueCmapEmbedHydration() {
|
||||
if (cmapEmbedHydrationTimer !== null) window.clearTimeout(cmapEmbedHydrationTimer);
|
||||
cmapEmbedHydrationTimer = window.setTimeout(() => {
|
||||
cmapEmbedHydrationTimer = null;
|
||||
hydrateCmapEmbeds(document).catch((error) => console.error(error));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function renderMarkdown(markdown, pageSlug = null) {
|
||||
const withExplicitWikiLinks = expandNamespacedMarkdownLinks(markdown || "");
|
||||
const extracted = extractCmapEmbeds(markdown || "");
|
||||
const withExplicitWikiLinks = expandNamespacedMarkdownLinks(extracted.markdown);
|
||||
const withWikiLinks = expandWikiMentions(withExplicitWikiLinks, pageSlug);
|
||||
const withTodos = expandTodoMarkup(withWikiLinks, pageSlug);
|
||||
const html = easyMDE.markdown(withTodos);
|
||||
const withImages = applyImageWidthMarkup(html);
|
||||
return DOMPurify.sanitize(withImages);
|
||||
const withEmbeds = restoreCmapEmbeds(html, extracted.embeds);
|
||||
const withImages = applyImageWidthMarkup(withEmbeds);
|
||||
const safeHtml = DOMPurify.sanitize(withImages);
|
||||
if (extracted.embeds.length) queueCmapEmbedHydration();
|
||||
return safeHtml;
|
||||
}
|
||||
|
||||
async function hydrateCmapEmbeds(root) {
|
||||
const embeds = Array.from(root.querySelectorAll(".rw-cmap-embed:not([data-cmap-hydrated])"));
|
||||
for (const embed of embeds) {
|
||||
embed.dataset.cmapHydrated = "loading";
|
||||
const conceptMap = cmapMentionTarget(embed.dataset.cmapReference || "");
|
||||
if (!conceptMap) {
|
||||
embed.dataset.cmapHydrated = "error";
|
||||
embed.replaceChildren();
|
||||
const message = document.createElement("p");
|
||||
message.className = "error";
|
||||
message.textContent = tr("concept-map-not-found", "CMap not found");
|
||||
embed.append(message);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const stored = await api(`/api/cmaps/${encodeURIComponent(conceptMap.slug)}`);
|
||||
const documentValue = decodeStoredConceptMapDocument(stored);
|
||||
embed.replaceChildren();
|
||||
embed.dataset.cmapSlug = conceptMap.slug;
|
||||
embed.title = tr("embedded-concept-map-help", "Double-click to open this CMap.");
|
||||
const header = document.createElement("header");
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = conceptMap.title;
|
||||
const hint = document.createElement("span");
|
||||
hint.textContent = tr("embedded-concept-map-help", "Double-click to open this CMap.");
|
||||
header.append(title, hint);
|
||||
const viewport = document.createElement("div");
|
||||
viewport.className = "rw-cmap-embed-viewport";
|
||||
const canvas = document.createElement("div");
|
||||
canvas.className = "cmap-canvas cmap-page-guides-hidden rw-cmap-embed-canvas";
|
||||
viewport.append(canvas);
|
||||
embed.append(header, viewport);
|
||||
const editor = window.RacketWikiCmap.createEditor(canvas, {
|
||||
Cmap: window.Cmap,
|
||||
renderItem: (record) => cmapNodeHtml(record)
|
||||
});
|
||||
editor.loadDocument(documentValue);
|
||||
const visible = editor.items.filter((item) => editor.isEffectiveItemVisible(item));
|
||||
if (visible.length) {
|
||||
const left = Math.min(...visible.map((item) => Number(item.node.attr("x")))) - 24;
|
||||
const top = Math.min(...visible.map((item) => Number(item.node.attr("y")))) - 24;
|
||||
const right = Math.max(...visible.map((item) => Number(item.node.attr("x")) + Number(item.node.attr("width")))) + 24;
|
||||
const bottom = Math.max(...visible.map((item) => Number(item.node.attr("y")) + Number(item.node.attr("height")))) + 24;
|
||||
const availableWidth = Math.max(320, embed.clientWidth - 2);
|
||||
const scale = Math.min(1, availableWidth / Math.max(1, right - left), 520 / Math.max(1, bottom - top));
|
||||
editor.zoomFactor = scale;
|
||||
editor.map.zoom(scale);
|
||||
viewport.style.height = `${Math.max(180, Math.ceil((bottom - top) * scale))}px`;
|
||||
window.requestAnimationFrame(() => {
|
||||
viewport.scrollLeft = Math.max(0, left * scale);
|
||||
viewport.scrollTop = Math.max(0, top * scale);
|
||||
});
|
||||
}
|
||||
embed.dataset.cmapHydrated = "ready";
|
||||
embed.addEventListener("dblclick", () => {
|
||||
navigateToHash(cmapRoute(conceptMap.slug)).catch((error) => console.error(error));
|
||||
});
|
||||
} catch (error) {
|
||||
embed.dataset.cmapHydrated = "error";
|
||||
embed.replaceChildren();
|
||||
const message = document.createElement("p");
|
||||
message.className = "error";
|
||||
message.textContent = error.message;
|
||||
embed.append(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function slugTitle(slug) {
|
||||
@@ -1463,6 +1574,7 @@
|
||||
wikiCmapLinkCombobox.setOptions(
|
||||
state.conceptMaps.map((conceptMap) => titledCmapComboboxEntry(conceptMap)), "");
|
||||
$("wiki-cmap-link-submit").disabled = state.conceptMaps.length === 0;
|
||||
$("wiki-cmap-embed-submit").disabled = state.conceptMaps.length === 0;
|
||||
$("wiki-cmap-link").placeholder = state.conceptMaps.length ?
|
||||
tr("filter-concept-maps", "Filter concept maps") :
|
||||
tr("no-concept-maps", "No saved CMaps");
|
||||
@@ -1486,6 +1598,21 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function insertSelectedWikiCmapEmbed() {
|
||||
const slug = wikiCmapLinkCombobox.value();
|
||||
if (slug === null || !slug) {
|
||||
const input = $("wiki-cmap-link");
|
||||
input.setCustomValidity(tr("select-listed-concept-map", "Select a CMap from the list or clear the field."));
|
||||
input.reportValidity();
|
||||
return false;
|
||||
}
|
||||
const conceptMap = state.conceptMaps.find((item) => item.slug === slug);
|
||||
if (!conceptMap) return false;
|
||||
insertTextAtCursor(`\n\n{{cmap:${conceptMap.slug}}}\n\n`);
|
||||
$("wiki-cmap-link-dialog").close();
|
||||
return true;
|
||||
}
|
||||
|
||||
function isInlineImage(file) {
|
||||
return ["image/png", "image/jpeg", "image/gif", "image/webp"].includes(file.type);
|
||||
}
|
||||
@@ -2049,6 +2176,10 @@
|
||||
username.textContent = user.username;
|
||||
const display = document.createElement("input");
|
||||
display.value = user.displayName;
|
||||
const email = document.createElement("input");
|
||||
email.type = "email";
|
||||
email.value = user.email || "";
|
||||
email.placeholder = tr("email-address", "Email address");
|
||||
const role = document.createElement("select");
|
||||
for (const roleName of ["reader", "editor", "admin"]) {
|
||||
const option = document.createElement("option");
|
||||
@@ -2072,6 +2203,7 @@
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
displayName: display.value,
|
||||
email: email.value,
|
||||
role: role.value,
|
||||
enabled: enabled.checked,
|
||||
password: password.value || undefined
|
||||
@@ -2088,11 +2220,75 @@
|
||||
await loadUsersAdmin();
|
||||
});
|
||||
actions.append(save, remove);
|
||||
row.append(username, display, role, enabled, password, actions);
|
||||
row.append(username, display, email, role, enabled, password, actions);
|
||||
list.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
function showProfile() {
|
||||
renderBreadcrumbs([
|
||||
{ label: state.siteTitle, href: "/" },
|
||||
{ label: tr("profile", "Profile") }
|
||||
]);
|
||||
show("profile-view");
|
||||
const user = state.session.user;
|
||||
$("profile-username").value = user.username;
|
||||
$("profile-display-name").value = user.displayName;
|
||||
$("profile-email").value = user.email || "";
|
||||
$("profile-current-password").value = "";
|
||||
$("profile-new-password").value = "";
|
||||
$("profile-repeat-password").value = "";
|
||||
$("profile-status").textContent = "";
|
||||
}
|
||||
|
||||
async function loadMailSettingsAdmin() {
|
||||
renderBreadcrumbs([
|
||||
{ label: state.siteTitle, href: "/" },
|
||||
{ label: tr("admin", "Admin"), href: "#admin" },
|
||||
{ label: tr("email-and-password-reset", "Email and password reset") }
|
||||
]);
|
||||
show("mail-admin-view");
|
||||
const settings = await api("/api/admin/mail-settings");
|
||||
$("mail-public-url").value = settings.publicUrl || window.location.origin;
|
||||
$("mail-smtp-host").value = settings.smtpHost || "";
|
||||
$("mail-smtp-port").value = settings.smtpPort || "587";
|
||||
$("mail-smtp-from").value = settings.smtpFrom || "";
|
||||
$("mail-smtp-user").value = settings.smtpUser || "";
|
||||
$("mail-smtp-password").value = "";
|
||||
$("mail-smtp-tls").checked = settings.smtpTls !== false;
|
||||
$("mail-smtp-accept-untrusted-certificates").checked = settings.smtpAcceptUntrustedCertificates === true;
|
||||
$("mail-smtp-accept-untrusted-certificates").disabled = !$("mail-smtp-tls").checked;
|
||||
$("mail-reset-limit").value = settings.resetLimit || "2";
|
||||
$("mail-test-recipient").value = state.session.user.email || "";
|
||||
$("mail-password-help").textContent = settings.hasPassword ? tr("smtp-password-kept", "A password is stored; leave empty to keep it.") : "";
|
||||
$("mail-settings-status").textContent = "";
|
||||
}
|
||||
|
||||
function mailSettingsFormData() {
|
||||
return {
|
||||
publicUrl: $("mail-public-url").value,
|
||||
smtpHost: $("mail-smtp-host").value,
|
||||
smtpPort: $("mail-smtp-port").value,
|
||||
smtpFrom: $("mail-smtp-from").value,
|
||||
smtpUser: $("mail-smtp-user").value,
|
||||
smtpPassword: $("mail-smtp-password").value,
|
||||
smtpTls: $("mail-smtp-tls").checked,
|
||||
smtpAcceptUntrustedCertificates: $("mail-smtp-accept-untrusted-certificates").checked,
|
||||
resetLimit: $("mail-reset-limit").value
|
||||
};
|
||||
}
|
||||
|
||||
function smtpErrorMessage(error) {
|
||||
const message = error && error.message ? error.message : String(error);
|
||||
if (message.includes("certificate verify failed") || message.includes("TLS certificate verification failed")) {
|
||||
return tr("smtp-certificate-verification-failed", "The SMTP server certificate could not be verified. Install a valid certificate, or select the local-server exception if this is a trusted local SMTP server.");
|
||||
}
|
||||
if (message.includes("no protocols available")) {
|
||||
return tr("smtp-no-modern-tls", "The SMTP connection attempted an obsolete TLS protocol. Install the current Racket Wiki version, which negotiates modern TLS automatically.");
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function showNotFound(slug) {
|
||||
state.currentPage = null;
|
||||
state.editingNew = false;
|
||||
@@ -2131,6 +2327,10 @@
|
||||
* post : Exactly one application view is made active.
|
||||
*/
|
||||
async function route() {
|
||||
if (location.hash === "#profile") {
|
||||
showProfile();
|
||||
return;
|
||||
}
|
||||
const todoMatch = location.hash.match(/^#todo\/([^/]+)\/(\d+)$/);
|
||||
if (todoMatch) {
|
||||
await showTodos(decodeURIComponent(todoMatch[1]), Number(todoMatch[2]));
|
||||
@@ -2186,6 +2386,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.hash === "#admin/mail" && can("admin")) {
|
||||
await loadMailSettingsAdmin();
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.hash === "#admin/aliases" && can("admin")) {
|
||||
await loadAliasesAdmin();
|
||||
return;
|
||||
@@ -2899,7 +3104,7 @@
|
||||
canvas.replaceChildren();
|
||||
state.cmapPrototype = null;
|
||||
|
||||
console.info("[racket-wiki:cmap-host 0.2.84] resetCmapPrototype", {
|
||||
console.info("[racket-wiki:cmap-host 0.2.94] resetCmapPrototype", {
|
||||
cmapAvailable: typeof window.Cmap === "function",
|
||||
interactionLayerAvailable: Boolean(window.RacketWikiCmap),
|
||||
interactionLayerVersion: window.RacketWikiCmap ? window.RacketWikiCmap.version : null,
|
||||
@@ -2931,7 +3136,7 @@
|
||||
}
|
||||
},
|
||||
onOpenSubMap: (record) => {
|
||||
console.info("[racket-wiki:cmap-host 0.2.84] submap state changed", {
|
||||
console.info("[racket-wiki:cmap-host 0.2.94] submap state changed", {
|
||||
id: record.id,
|
||||
expanded: record.expanded,
|
||||
childMap: record.childMap
|
||||
@@ -2969,7 +3174,7 @@
|
||||
relationLabel: tr("relation", "Relation")
|
||||
});
|
||||
restoreCmapZoom();
|
||||
console.info("[racket-wiki:cmap-host 0.2.84] editor stored", {
|
||||
console.info("[racket-wiki:cmap-host 0.2.94] editor stored", {
|
||||
editorAvailable: Boolean(prototype.editor),
|
||||
canvasChildCount: canvas.children.length
|
||||
});
|
||||
@@ -3036,6 +3241,8 @@
|
||||
const hasStoredMap = Boolean(state.currentConceptMap);
|
||||
$("cmap-rename-map").disabled = !hasStoredMap;
|
||||
$("cmap-delete-map").disabled = !hasStoredMap;
|
||||
$("cmap-create-snapshot").disabled = !hasStoredMap;
|
||||
$("cmap-history").disabled = !hasStoredMap;
|
||||
}
|
||||
|
||||
async function loadConceptMaps() {
|
||||
@@ -3052,7 +3259,7 @@
|
||||
documentValue = JSON.parse(documentValue);
|
||||
}
|
||||
if (!documentValue || typeof documentValue !== "object" || Array.isArray(documentValue)) {
|
||||
console.error("[racket-wiki:cmap-host 0.2.84] invalid stored CMap document", {
|
||||
console.error("[racket-wiki:cmap-host 0.2.94] invalid stored CMap document", {
|
||||
slug: conceptMap.slug,
|
||||
valueType: Array.isArray(documentValue) ? "array" : typeof documentValue,
|
||||
value: documentValue
|
||||
@@ -3152,7 +3359,7 @@
|
||||
const conceptMap = await api(`/api/cmaps/${encodeURIComponent(slug)}`);
|
||||
if (loadSequence !== state.cmapLoadSequence) return;
|
||||
conceptMap.document = decodeStoredConceptMapDocument(conceptMap);
|
||||
console.info("[racket-wiki:cmap-host 0.2.84] stored CMap received", {
|
||||
console.info("[racket-wiki:cmap-host 0.2.94] stored CMap received", {
|
||||
slug: conceptMap.slug,
|
||||
version: conceptMap.currentVersion,
|
||||
itemCount: Array.isArray(conceptMap.document.items) ? conceptMap.document.items.length : 0,
|
||||
@@ -3163,7 +3370,7 @@
|
||||
resetCmapPrototype(conceptMap.document, false);
|
||||
markCurrentCmapSaved();
|
||||
const loadedEditor = cmapPrototypeState().editor;
|
||||
console.info("[racket-wiki:cmap-host 0.2.84] stored CMap loaded", {
|
||||
console.info("[racket-wiki:cmap-host 0.2.94] stored CMap loaded", {
|
||||
slug: conceptMap.slug,
|
||||
editorAvailable: Boolean(loadedEditor),
|
||||
itemCount: loadedEditor ? loadedEditor.items.length : 0,
|
||||
@@ -3172,6 +3379,68 @@
|
||||
showCmapStatus(tr("concept-map-loaded", "CMap loaded"), true);
|
||||
}
|
||||
|
||||
async function loadHistoricalConceptMapVersion(version) {
|
||||
const conceptMap = state.currentConceptMap;
|
||||
if (!conceptMap) return;
|
||||
const historical = await api(
|
||||
`/api/cmaps/${encodeURIComponent(conceptMap.slug)}/versions/${encodeURIComponent(version)}`);
|
||||
historical.document = decodeStoredConceptMapDocument(historical);
|
||||
const currentSnapshot = JSON.stringify(conceptMap.document);
|
||||
resetCmapPrototype(historical.document, false);
|
||||
state.cmapSavedSnapshot = currentSnapshot;
|
||||
showCmapStatus(
|
||||
tr("concept-map-version-loaded", "Version {version} loaded; save to make it current.")
|
||||
.replace("{version}", String(historical.version)),
|
||||
true);
|
||||
}
|
||||
|
||||
async function showConceptMapHistory() {
|
||||
const conceptMap = state.currentConceptMap;
|
||||
if (!conceptMap) return;
|
||||
const result = await api(`/api/cmaps/${encodeURIComponent(conceptMap.slug)}/history`);
|
||||
const list = $("cmap-history-list");
|
||||
list.replaceChildren();
|
||||
for (const version of result.versions || []) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "cmap-history-row";
|
||||
const label = document.createElement("div");
|
||||
const heading = document.createElement("strong");
|
||||
heading.textContent = `${tr("version", "Version")} ${version.version} — ${version.title}`;
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "muted";
|
||||
const knownSummaries = {
|
||||
create: tr("concept-map-created-version", "CMap created"),
|
||||
rename: tr("concept-map-renamed-version", "CMap renamed")
|
||||
};
|
||||
let summary = knownSummaries[version.action] || version.summary;
|
||||
if (version.action === "snapshot") {
|
||||
const snapshotLabel = tr("snapshot", "Snapshot");
|
||||
if (version.summary === "Current state when CMap history was enabled") {
|
||||
summary = tr("concept-map-initial-version", "Initial available version");
|
||||
} else {
|
||||
summary = version.summary === snapshotLabel ? snapshotLabel : `${snapshotLabel} — ${version.summary}`;
|
||||
}
|
||||
}
|
||||
if (version.summary === "Automatic save") summary = tr("automatic-save", "Automatic save");
|
||||
if (version.summary === "Manual save") summary = tr("manual-save", "Manual save");
|
||||
meta.textContent = `${pageDisplayDate(version.createdAt)} · ${version.author} · ${summary}`;
|
||||
label.append(heading, document.createElement("br"), meta);
|
||||
const load = document.createElement("button");
|
||||
load.type = "button";
|
||||
load.textContent = version.version === conceptMap.currentVersion ?
|
||||
tr("current-version", "Current") : tr("load-version", "Load version");
|
||||
load.disabled = version.version === conceptMap.currentVersion;
|
||||
load.addEventListener("click", () => {
|
||||
$("cmap-history-dialog").close();
|
||||
requestCmapTransition(() => loadHistoricalConceptMapVersion(version.version))
|
||||
.catch((error) => showCmapStatus(error.message));
|
||||
});
|
||||
row.append(label, load);
|
||||
list.append(row);
|
||||
}
|
||||
$("cmap-history-dialog").showModal();
|
||||
}
|
||||
|
||||
async function createStoredConceptMap() {
|
||||
const title = window.prompt(tr("concept-map-name", "Concept map name"), "");
|
||||
if (!title || !title.trim()) return;
|
||||
@@ -3239,7 +3508,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStoredConceptMap({ automatic = false } = {}) {
|
||||
async function saveStoredConceptMap({ automatic = false, force = false,
|
||||
summary = null, snapshotVersion = false } = {}) {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (!prototype.editor) return false;
|
||||
cancelCmapAutosave();
|
||||
@@ -3247,13 +3517,15 @@
|
||||
if (cmapSavePromise) {
|
||||
const firstSaveSucceeded = await cmapSavePromise;
|
||||
if (!firstSaveSucceeded) return false;
|
||||
if (cmapHasUnsavedChanges()) return saveStoredConceptMap({ automatic });
|
||||
if (force || cmapHasUnsavedChanges()) {
|
||||
return saveStoredConceptMap({ automatic, force, summary, snapshotVersion });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const snapshot = currentCmapSnapshot();
|
||||
if (snapshot === null) return false;
|
||||
if (snapshot === state.cmapSavedSnapshot) {
|
||||
if (!force && snapshot === state.cmapSavedSnapshot) {
|
||||
if (!automatic) showCmapStatus(tr("concept-map-saved", "CMap saved"), true);
|
||||
return true;
|
||||
}
|
||||
@@ -3261,7 +3533,8 @@
|
||||
const document = JSON.parse(snapshot);
|
||||
const conceptMapAtStart = state.currentConceptMap;
|
||||
let saveSucceeded = false;
|
||||
showCmapStatus(automatic ? tr("autosaving", "Saving automatically…") : tr("saving", "Saving…"));
|
||||
showCmapStatus(snapshotVersion ? tr("creating-snapshot", "Creating snapshot…") :
|
||||
(automatic ? tr("autosaving", "Saving automatically…") : tr("saving", "Saving…")));
|
||||
cmapSavePromise = (async () => {
|
||||
try {
|
||||
if (!conceptMapAtStart) {
|
||||
@@ -3286,6 +3559,8 @@
|
||||
body: JSON.stringify({
|
||||
title: conceptMapAtStart.title,
|
||||
baseVersion: conceptMapAtStart.currentVersion,
|
||||
summary: summary || (automatic ? tr("automatic-save", "Automatic save") : tr("manual-save", "Manual save")),
|
||||
snapshot: snapshotVersion,
|
||||
document
|
||||
})
|
||||
});
|
||||
@@ -3296,7 +3571,8 @@
|
||||
}
|
||||
markCurrentCmapSaved(snapshot);
|
||||
showCmapStatus(
|
||||
automatic ? tr("concept-map-autosaved", "CMap saved automatically") : tr("concept-map-saved", "CMap saved"),
|
||||
snapshotVersion ? tr("snapshot-created", "Snapshot created") :
|
||||
(automatic ? tr("concept-map-autosaved", "CMap saved automatically") : tr("concept-map-saved", "CMap saved")),
|
||||
true);
|
||||
saveSucceeded = true;
|
||||
return true;
|
||||
@@ -3311,6 +3587,17 @@
|
||||
return cmapSavePromise;
|
||||
}
|
||||
|
||||
async function createConceptMapSnapshot() {
|
||||
if (!state.currentConceptMap) return false;
|
||||
const description = window.prompt(tr("snapshot-description", "Snapshot description"), "");
|
||||
if (description === null) return false;
|
||||
return saveStoredConceptMap({
|
||||
force: true,
|
||||
summary: description.trim() || tr("snapshot", "Snapshot"),
|
||||
snapshotVersion: true
|
||||
});
|
||||
}
|
||||
|
||||
async function showCmapPrototype(requestedSlug = null) {
|
||||
state.previousView = state.currentPage ? "page-view" : "cmap-view";
|
||||
renderBreadcrumbs([
|
||||
@@ -3366,6 +3653,10 @@
|
||||
const pageSlug = wikiSlugFromHref(href);
|
||||
if (pageSlug) pages.add(pageSlug);
|
||||
}
|
||||
for (const embed of container.querySelectorAll(".rw-cmap-embed[data-cmap-reference]")) {
|
||||
const target = cmapMentionTarget(embed.dataset.cmapReference || "");
|
||||
if (target) conceptMaps.add(target.slug);
|
||||
}
|
||||
|
||||
return { pages, conceptMaps };
|
||||
}
|
||||
@@ -3902,6 +4193,12 @@
|
||||
$("cmap-save-map").addEventListener("click", () => saveStoredConceptMap());
|
||||
$("cmap-rename-map").addEventListener("click", () => renameStoredConceptMap());
|
||||
$("cmap-delete-map").addEventListener("click", () => deleteStoredConceptMap());
|
||||
$("cmap-create-snapshot").addEventListener("click", () => {
|
||||
createConceptMapSnapshot().catch((error) => showCmapStatus(error.message));
|
||||
});
|
||||
$("cmap-history").addEventListener("click", () => {
|
||||
showConceptMapHistory().catch((error) => showCmapStatus(error.message));
|
||||
});
|
||||
$("cmap-add-concept").addEventListener("click", () => {
|
||||
openNewCmapConceptDialog(cmapContextCreateContext || {});
|
||||
});
|
||||
@@ -4115,6 +4412,7 @@
|
||||
event.preventDefault();
|
||||
insertSelectedWikiCmapLink();
|
||||
});
|
||||
$("wiki-cmap-embed-submit").addEventListener("click", insertSelectedWikiCmapEmbed);
|
||||
$("wiki-cmap-link-dialog").addEventListener("close", () => {
|
||||
pendingWikiCmapLinkLabel = "";
|
||||
wikiCmapLinkCombobox.clear();
|
||||
@@ -4128,6 +4426,7 @@
|
||||
event.preventDefault();
|
||||
cancelCmapTransition();
|
||||
});
|
||||
$("cmap-history-close").addEventListener("click", () => $("cmap-history-dialog").close());
|
||||
$("context-link").addEventListener("click", (event) => { event.preventDefault(); showContextGraphOverlay().catch((error) => console.error(error)); });
|
||||
$("context-overlay-close").addEventListener("click", (event) => { event.preventDefault(); closeContextOverlay(); });
|
||||
$("context-overlay").addEventListener("click", (event) => { if (event.target === $("context-overlay")) closeContextOverlay(); });
|
||||
@@ -4157,6 +4456,37 @@
|
||||
await api("/api/logout", { method: "POST", body: "{}" });
|
||||
window.location.replace("/login");
|
||||
});
|
||||
$("profile-link").addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
navigateToHash("#profile").catch(console.error);
|
||||
});
|
||||
$("profile-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const newPassword = $("profile-new-password").value;
|
||||
if (newPassword !== $("profile-repeat-password").value) {
|
||||
$("profile-status").textContent = tr("passwords-do-not-match", "Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await api("/api/profile", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
displayName: $("profile-display-name").value,
|
||||
email: $("profile-email").value,
|
||||
currentPassword: $("profile-current-password").value,
|
||||
newPassword
|
||||
})
|
||||
});
|
||||
state.session = result.session;
|
||||
$("account-name").textContent = state.session.user.displayName;
|
||||
$("profile-current-password").value = "";
|
||||
$("profile-new-password").value = "";
|
||||
$("profile-repeat-password").value = "";
|
||||
$("profile-status").textContent = tr("profile-saved", "Profile saved.");
|
||||
} catch (error) {
|
||||
$("profile-status").textContent = error.message;
|
||||
}
|
||||
});
|
||||
$("admin-link").addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
navigateToHash("#admin").catch(console.error);
|
||||
@@ -4165,6 +4495,10 @@
|
||||
event.preventDefault();
|
||||
navigateToHash("#admin/users").catch(console.error);
|
||||
});
|
||||
$("admin-mail-link").addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
navigateToHash("#admin/mail").catch(console.error);
|
||||
});
|
||||
$("admin-aliases-link").addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
navigateToHash("#admin/aliases").catch(console.error);
|
||||
@@ -4195,6 +4529,10 @@
|
||||
event.preventDefault();
|
||||
navigateToHash("#admin").catch(console.error);
|
||||
});
|
||||
$("close-mail-admin").addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
navigateToHash("#admin").catch(console.error);
|
||||
});
|
||||
$("close-alias-admin").addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
navigateToHash("#admin").catch(console.error);
|
||||
@@ -4247,6 +4585,7 @@
|
||||
body: JSON.stringify({
|
||||
username: $("new-username").value,
|
||||
displayName: $("new-display-name").value || $("new-username").value,
|
||||
email: $("new-email").value,
|
||||
password: $("new-password").value,
|
||||
role: $("new-role").value,
|
||||
enabled: true
|
||||
@@ -4256,6 +4595,49 @@
|
||||
await loadUsersAdmin();
|
||||
});
|
||||
|
||||
$("mail-settings-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api("/api/admin/mail-settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(mailSettingsFormData())
|
||||
});
|
||||
$("mail-smtp-password").value = "";
|
||||
$("mail-password-help").textContent = tr("smtp-password-kept", "A password is stored; leave empty to keep it.");
|
||||
$("mail-settings-status").textContent = tr("saved", "Saved");
|
||||
} catch (error) {
|
||||
$("mail-settings-status").textContent = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
$("mail-test-button").addEventListener("click", async () => {
|
||||
const button = $("mail-test-button");
|
||||
const recipient = $("mail-test-recipient").value.trim();
|
||||
button.disabled = true;
|
||||
$("mail-settings-status").textContent = tr("sending-test-mail", "Sending test email…");
|
||||
try {
|
||||
await api("/api/admin/mail-settings/test", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
...mailSettingsFormData(),
|
||||
recipient
|
||||
})
|
||||
});
|
||||
$("mail-settings-status").textContent = tr("test-mail-sent", "Test email accepted by SMTP server");
|
||||
} catch (error) {
|
||||
console.error("SMTP test failed", error);
|
||||
$("mail-settings-status").textContent = smtpErrorMessage(error);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
$("mail-smtp-tls").addEventListener("change", () => {
|
||||
const acceptUntrustedCertificates = $("mail-smtp-accept-untrusted-certificates");
|
||||
acceptUntrustedCertificates.disabled = !$("mail-smtp-tls").checked;
|
||||
if (acceptUntrustedCertificates.disabled) acceptUntrustedCertificates.checked = false;
|
||||
});
|
||||
|
||||
window.addEventListener("hashchange", () => {
|
||||
if (cmapHasUnsavedChanges()) {
|
||||
const requestedHash = location.hash;
|
||||
|
||||
Reference in New Issue
Block a user