@rigel-cms/rigel 0.1.3-beta.1 → 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/content.ts +3 -1
- package/src/editor/App.svelte +191 -55
- package/src/editor/lib/BlockEditor.svelte +18 -18
- package/src/editor/lib/CollectionPanel.svelte +6 -2
- package/src/editor/lib/ImagePicker.svelte +7 -7
- package/src/editor/lib/MarkdownEditor.svelte +7 -7
- package/src/editor/lib/PreviewFrame.svelte +64 -22
- package/src/editor/lib/PublishDialog.svelte +65 -0
- package/src/editor/lib/SchemaField.svelte +4 -4
- package/src/editor/lib/SchemaPanel.svelte +10 -5
- package/src/editor/lib/Select.svelte +3 -2
- package/src/editor/lib/SeoGuidance.svelte +1 -1
- package/src/editor/lib/Settings.svelte +27 -2
- package/src/editor/lib/SitePreview.svelte +2 -2
- package/src/editor/lib/TopBar.svelte +1 -1
- package/src/editor/lib/VisualEditor.svelte +8 -4
- package/src/editor/lib/github-auth.ts +4 -3
- package/src/editor/lib/github-backend.ts +218 -19
- package/src/editor/lib/slugify.ts +1 -10
- package/src/editor/lib/storage.ts +30 -2
- package/src/editor/lib/tooltip.ts +120 -0
- package/src/editor/styles.css +25 -1
- package/src/env.d.ts +1 -1
- package/src/preview.ts +9 -2
- package/src/routes/local-documents.ts +4 -4
- package/src/routes/local-post.ts +23 -5
- package/src/schema/paths.ts +3 -1
- package/src/schema/store.ts +21 -2
- package/src/slugs.ts +46 -0
- package/src/vite/manifest-plugin.ts +6 -2
package/package.json
CHANGED
package/src/content.ts
CHANGED
|
@@ -2,4 +2,6 @@ export type { MenuItem, MenuDocument } from "./manifest/menu";
|
|
|
2
2
|
export { defineCopy } from "./manifest/types";
|
|
3
3
|
export type { CopyField, CopyGroup, CopyPage, Manifest, ManifestCollection } from "./manifest/types";
|
|
4
4
|
export { defineSchema, defineDocument, defineObject, defineField } from "./schema/types";
|
|
5
|
-
export type { Schema, DocumentDef, ObjectDef, FieldDef, ContentDocument } from "./schema/types";
|
|
5
|
+
export type { Schema, DocumentDef, ObjectDef, FieldDef, ContentDocument } from "./schema/types";
|
|
6
|
+
export { effectiveSlug } from "./slugs";
|
|
7
|
+
export { pageRoute } from "./schema/paths";
|
package/src/editor/App.svelte
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onMount, tick } from "svelte";
|
|
3
|
+
import { tooltipRoot } from "./lib/tooltip";
|
|
4
|
+
onMount(() => tooltipRoot(document.body).destroy);
|
|
2
5
|
import type {
|
|
3
6
|
Manifest,
|
|
4
7
|
ManifestCollection,
|
|
@@ -17,22 +20,24 @@
|
|
|
17
20
|
import { isSeoField } from "../manifest/seo";
|
|
18
21
|
import { initialCollectionValues } from "../manifest/collections";
|
|
19
22
|
import { slugify } from "./lib/slugify";
|
|
23
|
+
import { effectiveSlug, normalizeUrlSlug } from "../slugs";
|
|
20
24
|
import Icon from "./lib/Icon.svelte";
|
|
21
25
|
import SidebarIcon from "./lib/SidebarIcon.svelte";
|
|
22
26
|
import logo from "./assets/rigelcms-icon.png?url";
|
|
23
27
|
import { getSettings, setSettings } from "./lib/settings";
|
|
24
|
-
import { eraseData } from "./lib/storage";
|
|
28
|
+
import { eraseData, readSession, writeSession, clearSession } from "./lib/storage";
|
|
25
29
|
import MenuEditor from "./lib/MenuEditor.svelte";
|
|
26
30
|
import type { MenuItem } from "../manifest/menu";
|
|
27
31
|
import UnsavedChanges from "./lib/UnsavedChanges.svelte";
|
|
28
32
|
import ActionDialog from "./lib/ActionDialog.svelte";
|
|
33
|
+
import PublishDialog from "./lib/PublishDialog.svelte";
|
|
29
34
|
import VisualEditor from "./lib/VisualEditor.svelte";
|
|
30
35
|
import { newDocumentFromTemplate } from "../schema/paths";
|
|
31
36
|
import { previewUrl } from "../preview";
|
|
32
37
|
import type { ContentDocument, Schema } from "../schema/types";
|
|
33
38
|
import { buildSidebar, navigationKey, type Selection } from "../manifest/navigation";
|
|
34
39
|
import { startGitHubLogin, checkGitHubAccess, type GitHubAccess } from "./lib/github-auth";
|
|
35
|
-
import { loadManifestRemote, saveRemoteSelection, deleteRemotePost, type GitContext } from "./lib/github-backend";
|
|
40
|
+
import { loadManifestRemote, saveRemoteSelection, deleteRemotePost, reviewRemotePublish, confirmRemotePublish, type GitContext, type PublishReview } from "./lib/github-backend";
|
|
36
41
|
|
|
37
42
|
type Sel = Selection;
|
|
38
43
|
|
|
@@ -40,6 +45,7 @@
|
|
|
40
45
|
let ready = $state(false);
|
|
41
46
|
let blueprint: Manifest | null = null;
|
|
42
47
|
let gitContext: GitContext | null = null;
|
|
48
|
+
let authRevision = 0;
|
|
43
49
|
let connected = $state(false);
|
|
44
50
|
let connectionBusy = $state(false);
|
|
45
51
|
let connectionError = $state("");
|
|
@@ -57,6 +63,75 @@
|
|
|
57
63
|
let saveState = $state<"idle" | "saving" | "saved" | "error">("idle");
|
|
58
64
|
let saveTask: Promise<boolean> | null = null;
|
|
59
65
|
let saveError = $state("");
|
|
66
|
+
let publishOpen = $state(false);
|
|
67
|
+
let publishLoading = $state(false);
|
|
68
|
+
let publishBusy = $state(false);
|
|
69
|
+
let publishReview = $state<PublishReview | null>(null);
|
|
70
|
+
let publishError = $state("");
|
|
71
|
+
let publishMessage = $state("");
|
|
72
|
+
let publishRevision = 0;
|
|
73
|
+
|
|
74
|
+
function closePublish() {
|
|
75
|
+
publishRevision += 1;
|
|
76
|
+
publishOpen = false;
|
|
77
|
+
publishReview = null;
|
|
78
|
+
void tick().then(() => document.querySelector<HTMLButtonElement>("#publish-button")?.focus());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function publishGuard(context: GitContext, revision: number, auth: number) {
|
|
82
|
+
if (!publishOpen || revision !== publishRevision || auth !== authRevision || gitContext !== context) throw new Error("Publish review is no longer active. Reconnect and review again.");
|
|
83
|
+
requireGitContext();
|
|
84
|
+
if (dirty || saveTask || saveState === "saving") throw new Error("Save your unsaved edits first, then review publishing. Your edits are still here.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function reviewPublish() {
|
|
88
|
+
if (publishBusy || publishLoading) return;
|
|
89
|
+
publishOpen = true;
|
|
90
|
+
publishReview = null;
|
|
91
|
+
publishError = "";
|
|
92
|
+
publishMessage = "";
|
|
93
|
+
publishLoading = true;
|
|
94
|
+
const revision = ++publishRevision, auth = authRevision;
|
|
95
|
+
try {
|
|
96
|
+
const context = requireGitContext();
|
|
97
|
+
const guard = () => publishGuard(context, revision, auth);
|
|
98
|
+
guard();
|
|
99
|
+
const review = await reviewRemotePublish(context, manifest, guard);
|
|
100
|
+
guard();
|
|
101
|
+
publishReview = review;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
handleAuthError(error);
|
|
104
|
+
if (revision === publishRevision) publishError = error instanceof Error ? error.message : "Could not review saved changes.";
|
|
105
|
+
} finally { publishLoading = false; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function publish() {
|
|
109
|
+
if (publishBusy || publishLoading || !publishReview) return;
|
|
110
|
+
publishBusy = true;
|
|
111
|
+
publishError = "";
|
|
112
|
+
const review = publishReview, revision = publishRevision, auth = authRevision;
|
|
113
|
+
try {
|
|
114
|
+
const context = requireGitContext();
|
|
115
|
+
const guard = () => publishGuard(context, revision, auth);
|
|
116
|
+
guard();
|
|
117
|
+
const result = await confirmRemotePublish(context, manifest, review, guard);
|
|
118
|
+
guard();
|
|
119
|
+
publishReview = null;
|
|
120
|
+
if (result === "synced") {
|
|
121
|
+
const remote = await loadManifestRemote(context, blueprint!, false);
|
|
122
|
+
guard();
|
|
123
|
+
manifest = remote;
|
|
124
|
+
openSelection(sel);
|
|
125
|
+
publishMessage = "Drafts synced. Nothing was published. Close this dialog and review again before publishing.";
|
|
126
|
+
} else publishMessage = `Published to ${review.publishBranch}. Your configured deployment can now start; the site is not instantly live. The drafts branch is preserved.`;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
handleAuthError(error);
|
|
129
|
+
if (revision === publishRevision) {
|
|
130
|
+
publishError = error instanceof Error ? error.message : "Could not verify publishing. Check GitHub before retrying.";
|
|
131
|
+
if (/changed|no longer matches/.test(publishError)) publishReview = null;
|
|
132
|
+
}
|
|
133
|
+
} finally { publishBusy = false; }
|
|
134
|
+
}
|
|
60
135
|
let pendingNavigation = $state<(() => void) | null>(null);
|
|
61
136
|
let actionDialog = $state<{ title: string; description: string; confirmLabel: string; fieldLabel?: string; danger?: boolean; onconfirm: (value: string) => void | Promise<void> } | null>(null);
|
|
62
137
|
let newEntry = $state<{ collection: string; post: CollectionPost } | null>(null);
|
|
@@ -76,6 +151,14 @@
|
|
|
76
151
|
blueprint = data;
|
|
77
152
|
manifest = data;
|
|
78
153
|
ready = true;
|
|
154
|
+
if (data.backend === "github" && data.github) {
|
|
155
|
+
const session = readSession(data.github);
|
|
156
|
+
if (session) {
|
|
157
|
+
gitContext = { token: session.accessToken, repo: data.github.repo, branch: data.github.branch };
|
|
158
|
+
sessionExpiresAt = session.expiresAt;
|
|
159
|
+
await recheckGitHub();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
79
162
|
} catch (error) {
|
|
80
163
|
loadError = error instanceof Error ? error.message : "Could not load the editor configuration.";
|
|
81
164
|
}
|
|
@@ -85,7 +168,7 @@
|
|
|
85
168
|
if (connectionBusy) throw new Error("Wait for GitHub access verification before saving.");
|
|
86
169
|
if (!gitContext || !connected || githubAccess?.status !== "ready") throw new Error("Connect GitHub and verify repository access before saving.");
|
|
87
170
|
if (Date.now() >= sessionExpiresAt - 30_000) {
|
|
88
|
-
|
|
171
|
+
invalidateSession();
|
|
89
172
|
throw new Error("Your GitHub session expired. Reconnect GitHub, then retry. Your unsaved edits are still here.");
|
|
90
173
|
}
|
|
91
174
|
return gitContext;
|
|
@@ -93,27 +176,56 @@
|
|
|
93
176
|
|
|
94
177
|
async function verifyGitHub() {
|
|
95
178
|
const config = blueprint?.github;
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
179
|
+
const context = gitContext;
|
|
180
|
+
if (!config || !context) return;
|
|
181
|
+
try {
|
|
182
|
+
if (Date.now() >= sessionExpiresAt - 30_000) {
|
|
183
|
+
invalidateSession();
|
|
184
|
+
throw new Error("Your GitHub session expired. Sign in again to check repository access.");
|
|
185
|
+
}
|
|
186
|
+
const access = await checkGitHubAccess(context.token, config.repo, config.appSlug);
|
|
187
|
+
if (gitContext !== context) return;
|
|
188
|
+
githubAccess = access;
|
|
189
|
+
if (access.status !== "ready") return;
|
|
190
|
+
if (!connected && blueprint) {
|
|
191
|
+
const remote = await loadManifestRemote(context, blueprint);
|
|
192
|
+
if (gitContext !== context) return;
|
|
193
|
+
manifest = remote;
|
|
194
|
+
connected = true;
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (gitContext === context) handleAuthError(error);
|
|
198
|
+
throw error;
|
|
104
199
|
}
|
|
105
200
|
}
|
|
106
201
|
|
|
202
|
+
function invalidateSession() {
|
|
203
|
+
closePublish();
|
|
204
|
+
authRevision += 1;
|
|
205
|
+
clearSession();
|
|
206
|
+
gitContext = null;
|
|
207
|
+
sessionExpiresAt = 0;
|
|
208
|
+
githubAccess = null;
|
|
209
|
+
tokenExpired = true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function handleAuthError(error: unknown) {
|
|
213
|
+
if (error instanceof Error && "status" in error && error.status === 401) invalidateSession();
|
|
214
|
+
}
|
|
215
|
+
|
|
107
216
|
async function connectGitHub() {
|
|
108
|
-
if (connectionBusy || saveState === "saving" || !manifest.github) return;
|
|
217
|
+
if (connectionBusy || saveState === "saving" || publishBusy || !manifest.github) return;
|
|
218
|
+
closePublish();
|
|
109
219
|
connectionBusy = true;
|
|
110
220
|
connectionError = "";
|
|
221
|
+
const revision = ++authRevision;
|
|
111
222
|
try {
|
|
112
223
|
const session = await startGitHubLogin(manifest.github.authOrigin);
|
|
113
|
-
if (
|
|
114
|
-
|
|
224
|
+
if (revision !== authRevision) return;
|
|
225
|
+
gitContext = { token: session.accessToken, repo: manifest.github.repo, branch: manifest.github.branch };
|
|
115
226
|
githubAccess = null;
|
|
116
227
|
sessionExpiresAt = session.expiresAt;
|
|
228
|
+
writeSession(manifest.github, session);
|
|
117
229
|
await verifyGitHub();
|
|
118
230
|
} catch (error) {
|
|
119
231
|
connectionError = error instanceof Error ? error.message : "Could not connect GitHub. Try signing in again.";
|
|
@@ -134,10 +246,10 @@
|
|
|
134
246
|
function signOut() {
|
|
135
247
|
if (saveState === "saving" || connectionBusy) return;
|
|
136
248
|
userOpen = false;
|
|
249
|
+
if (publishBusy) { invalidateSession(); connected = false; return; }
|
|
250
|
+
closePublish();
|
|
137
251
|
requestNavigation(() => {
|
|
138
|
-
|
|
139
|
-
sessionExpiresAt = 0;
|
|
140
|
-
githubAccess = null;
|
|
252
|
+
invalidateSession();
|
|
141
253
|
connected = false;
|
|
142
254
|
connectionError = "";
|
|
143
255
|
settingsOpen = false;
|
|
@@ -158,13 +270,14 @@
|
|
|
158
270
|
|
|
159
271
|
$effect(() => {
|
|
160
272
|
const expiry = sessionExpiresAt;
|
|
161
|
-
tokenExpired =
|
|
273
|
+
tokenExpired = !expiry || Date.now() >= expiry - 30_000;
|
|
162
274
|
if (!expiry) return;
|
|
163
|
-
const timer = setTimeout(
|
|
275
|
+
const timer = setTimeout(invalidateSession, Math.max(0, expiry - Date.now() - 30_000));
|
|
164
276
|
return () => clearTimeout(timer);
|
|
165
277
|
});
|
|
166
278
|
|
|
167
279
|
function save(): Promise<boolean> {
|
|
280
|
+
if (publishOpen || publishBusy) return Promise.resolve(false);
|
|
168
281
|
if (saveTask) return saveTask;
|
|
169
282
|
saveTask = (async () => {
|
|
170
283
|
if (dirty) {
|
|
@@ -178,6 +291,10 @@
|
|
|
178
291
|
const docSel = sel?.kind === "document" ? sel : null;
|
|
179
292
|
const creatingDoc = docSel !== null && newDocument?.id === docSel.id;
|
|
180
293
|
try {
|
|
294
|
+
if ((selection?.kind === "post" || (docSel && !activeDocType?.shared)) && Object.hasOwn(values, "slug")) {
|
|
295
|
+
values.slug = normalizeUrlSlug(values.slug);
|
|
296
|
+
draft.slug = values.slug;
|
|
297
|
+
}
|
|
181
298
|
if (githubMode) {
|
|
182
299
|
if (!selection) throw new Error("Select content before saving.");
|
|
183
300
|
await saveRemoteSelection(requireGitContext(), manifest, selection, values, body, !!creating || creatingDoc);
|
|
@@ -186,7 +303,7 @@
|
|
|
186
303
|
? `/rigel/api/local/documents/${encodeURIComponent(docSel.id)}`
|
|
187
304
|
: m ? `/rigel/api/local/menus/${m.slug}` : po && c ? `/rigel/api/local/${c.name}/${encodeURIComponent(po.slug)}` : "/rigel/api/local/save";
|
|
188
305
|
const payload = docSel ? { data: values } : m ? { slug: m.slug, title: values.title, items: values.items } : po ? { frontmatter: values, body } : { slug: p?.slug, values };
|
|
189
|
-
const res = await fetch(url, { method: docSel ? (creatingDoc ? "POST" : "PUT") : m ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
|
|
306
|
+
const res = await fetch(url, { method: docSel ? (creatingDoc ? "POST" : "PUT") : m ? "PUT" : "POST", headers: { "Content-Type": "application/json", ...(po && creating ? { "If-None-Match": "*" } : {}) }, body: JSON.stringify(payload) });
|
|
190
307
|
if (!res.ok) throw new Error((await res.text()) || `Save failed (${res.status})`);
|
|
191
308
|
}
|
|
192
309
|
if (docSel) {
|
|
@@ -197,7 +314,7 @@
|
|
|
197
314
|
newDocument = null;
|
|
198
315
|
docBase = JSON.parse(JSON.stringify(values));
|
|
199
316
|
} else if (m) { m.title = values.title; m.items = values.items; }
|
|
200
|
-
else if (po) { po.frontmatter =
|
|
317
|
+
else if (po) { po.frontmatter = values; po.body = body; bodyBase = body; }
|
|
201
318
|
else if (p) p.values = values;
|
|
202
319
|
if (creating && po && c) c.posts.push(po);
|
|
203
320
|
newEntry = null;
|
|
@@ -205,6 +322,7 @@
|
|
|
205
322
|
saveState = dirty ? "idle" : "saved";
|
|
206
323
|
saveError = "";
|
|
207
324
|
} catch (error) {
|
|
325
|
+
handleAuthError(error);
|
|
208
326
|
saveState = "error";
|
|
209
327
|
saveError = error instanceof Error ? error.message : "Could not save changes";
|
|
210
328
|
return false;
|
|
@@ -220,10 +338,10 @@
|
|
|
220
338
|
|
|
221
339
|
let settings = $state(getSettings());
|
|
222
340
|
let settingsError = $state("");
|
|
223
|
-
const userName = $derived(githubMode ? githubAccess?.user.login ?? "GitHub user" : "Local user");
|
|
341
|
+
const userName = $derived(githubMode ? githubAccess?.user.name ?? githubAccess?.user.login ?? "GitHub user" : "Local user");
|
|
224
342
|
const userAvatar = $derived(githubMode && /^https:\/\/avatars\.githubusercontent\.com\//.test(githubAccess?.user.avatar_url ?? "") ? githubAccess!.user.avatar_url : "");
|
|
225
343
|
let failedAvatar = $state("");
|
|
226
|
-
const
|
|
344
|
+
const userLogin = $derived(githubMode && githubAccess ? `@${githubAccess.user.login}` : "Local mode");
|
|
227
345
|
let content: HTMLDivElement;
|
|
228
346
|
let contentWidth = $state(0);
|
|
229
347
|
let resizing = $state(false);
|
|
@@ -273,7 +391,7 @@
|
|
|
273
391
|
function eraseBrowserData() {
|
|
274
392
|
actionDialog = {
|
|
275
393
|
title: "Erase browser data?", confirmLabel: "Erase data", danger: true,
|
|
276
|
-
description: "This removes Rigel settings, API keys and unsaved drafts from this browser. Saved repository files will not change. The editor will reload.",
|
|
394
|
+
description: "This removes Rigel settings, API keys, GitHub login and unsaved drafts from this browser. Saved repository files will not change. The editor will reload.",
|
|
277
395
|
onconfirm: () => {
|
|
278
396
|
if (saveState === "saving") throw new Error("Wait for the current save to finish.");
|
|
279
397
|
eraseData();
|
|
@@ -372,9 +490,9 @@
|
|
|
372
490
|
if (current?.kind === "document") map[current.id] = draft;
|
|
373
491
|
return map;
|
|
374
492
|
});
|
|
375
|
-
function documentPreview(mode: "
|
|
376
|
-
if (sel?.kind !== "document" || newDocument?.id === sel.id) return null;
|
|
377
|
-
return previewUrl(manifest.preview ?? {}, { kind: "document", type: sel.type, slug: sel.id.slice(sel.type.length + 1) }, mode);
|
|
493
|
+
function documentPreview(mode: "draft" | "website") {
|
|
494
|
+
if (sel?.kind !== "document" || (mode === "draft" && newDocument?.id === sel.id)) return null;
|
|
495
|
+
return previewUrl(manifest.preview ?? {}, { kind: "document", type: sel.type, slug: sel.id.slice(sel.type.length + 1), urlSlug: Object.hasOwn(draft, "slug") ? String(draft.slug) : undefined }, mode);
|
|
378
496
|
}
|
|
379
497
|
const standaloneDocument = $derived(sel?.kind === "document" && activeDocType?.shared === true);
|
|
380
498
|
const visualSelection = $derived(sel?.kind === "post" || (sel?.kind === "document" && !standaloneDocument));
|
|
@@ -384,13 +502,13 @@
|
|
|
384
502
|
...manifest.pages.filter((p) => !p.shared && (pageCategory === undefined || p.category === pageCategory))
|
|
385
503
|
.map((p) => ({ slug: p.slug, title: p.label, category: p.category, draft: false, target: { kind: "page", slug: p.slug } as Sel })),
|
|
386
504
|
...(pageCategory === undefined ? pageDocuments
|
|
387
|
-
.map((doc) => ({ slug: doc.slug, title: String(doc.data.title ?? doc.slug), category: undefined, draft: doc.data.draft === true, target: { kind: "document", type: doc.type, id: doc.id } as Sel })) : []),
|
|
505
|
+
.map((doc) => ({ slug: effectiveSlug(doc.data, doc.slug), title: String(doc.data.title ?? doc.slug), category: undefined, draft: doc.data.draft === true, target: { kind: "document", type: doc.type, id: doc.id } as Sel })) : []),
|
|
388
506
|
];
|
|
389
507
|
const key = sortBy === "slug" || sortBy === "category" ? sortBy : "title";
|
|
390
508
|
return rows.sort((a, b) => String(a[key] ?? "").localeCompare(String(b[key] ?? "")) * sortDir);
|
|
391
509
|
});
|
|
392
510
|
function documentCell(doc: ContentDocument, key: string): string {
|
|
393
|
-
return String(key === "title" ? doc.data.title ?? doc.data.name ?? doc.slug : key === "slug" ? doc.slug : doc.data[key] ?? "");
|
|
511
|
+
return String(key === "title" ? doc.data.title ?? doc.data.name ?? doc.slug : key === "slug" ? activeDocType?.shared ? doc.slug : effectiveSlug(doc.data, doc.slug) : doc.data[key] ?? "");
|
|
394
512
|
}
|
|
395
513
|
const typeDocuments = $derived((manifest.documents ?? []).filter((doc) => doc.type === docTypeName(sel)).sort((a, b) => documentCell(a, sortBy).localeCompare(documentCell(b, sortBy)) * sortDir));
|
|
396
514
|
|
|
@@ -469,6 +587,7 @@
|
|
|
469
587
|
}
|
|
470
588
|
|
|
471
589
|
function requestNavigation(action: () => void) {
|
|
590
|
+
if (publishOpen || publishBusy) return;
|
|
472
591
|
if (dirty || saveState === "saving") pendingNavigation = action;
|
|
473
592
|
else action();
|
|
474
593
|
}
|
|
@@ -526,7 +645,6 @@
|
|
|
526
645
|
const po = post;
|
|
527
646
|
if (!po) return;
|
|
528
647
|
const fm: Record<string, unknown> = { ...po.frontmatter };
|
|
529
|
-
delete fm.slug;
|
|
530
648
|
draft = JSON.parse(JSON.stringify(fm));
|
|
531
649
|
bodyDraft = po.body;
|
|
532
650
|
bodyBase = po.body;
|
|
@@ -552,9 +670,7 @@
|
|
|
552
670
|
}
|
|
553
671
|
const ref =
|
|
554
672
|
sel?.kind === "menu" && menu ? { title: menu.title, items: menu.items } : sel?.kind === "post" && post
|
|
555
|
-
?
|
|
556
|
-
Object.entries(post.frontmatter).filter(([k]) => k !== "slug"),
|
|
557
|
-
)
|
|
673
|
+
? post.frontmatter
|
|
558
674
|
: page?.values;
|
|
559
675
|
dirty =
|
|
560
676
|
newEntry !== null ||
|
|
@@ -564,7 +680,7 @@
|
|
|
564
680
|
}
|
|
565
681
|
|
|
566
682
|
function cellValue(p: { slug: string; frontmatter: Record<string, unknown> }, key: string): string {
|
|
567
|
-
const v = key === "slug" ? p.slug : p.frontmatter[key];
|
|
683
|
+
const v = key === "slug" ? effectiveSlug(p.frontmatter, p.slug) : p.frontmatter[key];
|
|
568
684
|
const field = coll?.fields.find(field => field.key === key);
|
|
569
685
|
if (field?.type === "reference") return referenceLabel(v, referenceOptions(field, manifest.documents)) || "—";
|
|
570
686
|
if (v === true) return "✓";
|
|
@@ -580,7 +696,7 @@
|
|
|
580
696
|
onconfirm: (title) => {
|
|
581
697
|
const slug = slugify(title);
|
|
582
698
|
if (!slug) throw new Error("Enter a title containing letters or numbers.");
|
|
583
|
-
const taken = [...(manifest.documents ?? []).filter(doc => !type.shared || doc.type === type.name).
|
|
699
|
+
const taken = [...(manifest.documents ?? []).filter(doc => !type.shared || doc.type === type.name).flatMap((doc) => [doc.slug, ...(type.shared || docTypes.find(type => type.name === doc.type)?.shared ? [] : [effectiveSlug(doc.data, doc.slug)])]), ...(!type.shared ? manifest.pages.map((page) => page.slug) : [])];
|
|
584
700
|
if (taken.includes(slug)) throw new Error(`The slug "${slug}" is already used by another document or page.`);
|
|
585
701
|
const id = `${type.name}:${slug}`;
|
|
586
702
|
const data = newDocumentFromTemplate(schema, type.name, title, slug);
|
|
@@ -596,16 +712,16 @@
|
|
|
596
712
|
actionDialog = {
|
|
597
713
|
title: `Add to ${c.label}`, description: "Enter a title to start a draft. Nothing is written until you press Save.", fieldLabel: "Title", confirmLabel: "Create draft",
|
|
598
714
|
onconfirm: (title) => {
|
|
599
|
-
const slug = slugify(title);
|
|
715
|
+
const slug = slugify(title, c.posts.flatMap((post) => [post.slug, effectiveSlug(post.frontmatter, post.slug)]));
|
|
600
716
|
if (!slug) throw new Error("Enter a title containing letters or numbers.");
|
|
601
|
-
|
|
602
|
-
newEntry = { collection: c.name, post: { slug, frontmatter: initialCollectionValues(c.fields, title), body: emptyBody() } };
|
|
717
|
+
newEntry = { collection: c.name, post: { slug, frontmatter: { ...initialCollectionValues(c.fields, title), slug }, body: emptyBody() } };
|
|
603
718
|
openSelection({ kind: "post", collection: c.name, slug });
|
|
604
719
|
},
|
|
605
720
|
};
|
|
606
721
|
}
|
|
607
722
|
|
|
608
723
|
function deletePost() {
|
|
724
|
+
if (publishOpen || publishBusy) return;
|
|
609
725
|
const current = post, collection = coll, unsaved = newEntry !== null;
|
|
610
726
|
if (!current || !collection) return;
|
|
611
727
|
actionDialog = {
|
|
@@ -613,7 +729,10 @@
|
|
|
613
729
|
description: unsaved ? "This discards the new post and its unsaved changes." : `This deletes ${current.slug}.json${githubMode ? ` from ${manifest.github?.branch}` : ""} and discards any unsaved changes. This cannot be undone in the editor.`,
|
|
614
730
|
onconfirm: async () => {
|
|
615
731
|
if (!unsaved) {
|
|
616
|
-
if (githubMode)
|
|
732
|
+
if (githubMode) {
|
|
733
|
+
try { await deleteRemotePost(requireGitContext(), manifest, collection.name, current.slug); }
|
|
734
|
+
catch (error) { handleAuthError(error); throw error; }
|
|
735
|
+
}
|
|
617
736
|
else {
|
|
618
737
|
const res = await fetch(`/rigel/api/local/${collection.name}/${encodeURIComponent(current.slug)}`, { method: "DELETE" });
|
|
619
738
|
if (!res.ok) throw new Error((await res.text()) || `Delete failed (${res.status})`);
|
|
@@ -640,10 +759,16 @@
|
|
|
640
759
|
{/snippet}
|
|
641
760
|
|
|
642
761
|
<svelte:window
|
|
762
|
+
onstorage={(event) => {
|
|
763
|
+
if ((event.key === "rigelcms.session" || event.key === null) && blueprint?.github && !readSession(blueprint.github)) {
|
|
764
|
+
invalidateSession();
|
|
765
|
+
connectionError = "Signed out in another tab. Reconnect GitHub to save. Your unsaved edits are still here.";
|
|
766
|
+
}
|
|
767
|
+
}}
|
|
643
768
|
onresize={endPreviewResize}
|
|
644
769
|
onblur={endPreviewResize}
|
|
645
770
|
oncontextmenu={(event) => { if (!settings.devMode) event.preventDefault(); }}
|
|
646
|
-
onbeforeunload={(event) => { if (dirty || saveState === "saving") { event.preventDefault(); event.returnValue = ""; } }}
|
|
771
|
+
onbeforeunload={(event) => { if (dirty || saveState === "saving" || publishBusy) { event.preventDefault(); event.returnValue = ""; } }}
|
|
647
772
|
onclick={(e) => {
|
|
648
773
|
if (!(e.target instanceof Element) || !e.target.closest('.user-menu-wrap')) userOpen = false;
|
|
649
774
|
}}
|
|
@@ -658,8 +783,8 @@
|
|
|
658
783
|
{#if !ready || (githubMode && !connected)}
|
|
659
784
|
<div class="rigel connection-shell">
|
|
660
785
|
<main id="github-connection" class="connection-card" aria-busy={connectionBusy}>
|
|
661
|
-
<img src={logo} alt="" width="64" height="64"
|
|
662
|
-
<h1>{!ready ? "
|
|
786
|
+
<div class="connection-brand"><img src={logo} alt="" width="64" height="64" /><span>Rigel <span class="brand-suffix">CMS</span></span></div>
|
|
787
|
+
<h1>{!ready ? "Loading editor" : "Connect GitHub"}</h1>
|
|
663
788
|
{#if !ready}
|
|
664
789
|
{#if loadError}<p class="connection-error" role="alert">{loadError}</p><button class="primary" onclick={loadBlueprint}>Retry loading</button>
|
|
665
790
|
{:else}<p role="status">Loading the editor configuration…</p>{/if}
|
|
@@ -693,7 +818,7 @@
|
|
|
693
818
|
<aside id="rigel-sidebar">
|
|
694
819
|
<a class="brand" href="/rigel/" role="button" aria-label="Welcome" aria-describedby="rigel-beta" onclick={(event) => { event.preventDefault(); goWelcome(); }}>
|
|
695
820
|
<img src={logo} alt="" width="32" height="32" />
|
|
696
|
-
<span>
|
|
821
|
+
<span>Rigel <span class="brand-suffix">CMS</span></span>
|
|
697
822
|
<span id="rigel-beta" class="badge">Beta</span>
|
|
698
823
|
</a>
|
|
699
824
|
<TopBar {manifest} {sel} onselect={select} />
|
|
@@ -714,7 +839,7 @@
|
|
|
714
839
|
data-document={node.target.kind === "document" ? node.target.id : undefined}
|
|
715
840
|
data-page={node.target.kind === "page" ? node.target.slug : undefined}
|
|
716
841
|
aria-label={node.label}
|
|
717
|
-
|
|
842
|
+
data-tooltip={node.label}
|
|
718
843
|
class:active={navActive(node.target)}
|
|
719
844
|
aria-current={navActive(node.target) ? "page" : undefined}
|
|
720
845
|
onclick={() => select(node.target)}
|
|
@@ -725,21 +850,26 @@
|
|
|
725
850
|
{/each}
|
|
726
851
|
</nav>
|
|
727
852
|
|
|
728
|
-
|
|
853
|
+
{#if githubMode && manifest.github?.publishBranch}
|
|
854
|
+
<button id="publish-button" class="sidebar-publish" aria-label="Publish" data-tooltip={dirty ? "Save your edits before publishing" : "Publish saved branch changes"} disabled={publishBusy || publishLoading || saveState === "saving" || connectionBusy || tokenExpired || githubAccess?.status !== "ready"} onclick={reviewPublish}>
|
|
855
|
+
<Icon name="external" size={18} /><span class="sidebar-label">Publish</span>
|
|
856
|
+
</button>
|
|
857
|
+
{/if}
|
|
858
|
+
<button class="sidebar-toggle" aria-expanded={!settings.userPref.sidebarCollapsed} aria-controls="rigel-sidebar-navigation" aria-label={settings.userPref.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} data-tooltip={settings.userPref.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} onclick={() => { settings.userPref.sidebarCollapsed = !settings.userPref.sidebarCollapsed; }}>
|
|
729
859
|
<SidebarIcon name="sidebar-simple" /><span class="sidebar-label">Collapse sidebar</span>
|
|
730
860
|
</button>
|
|
731
861
|
<div class="aside-foot">
|
|
732
862
|
<div class="user-menu-wrap">
|
|
733
|
-
<button class="user-btn" aria-label={githubMode ? "GitHub user menu" : "Local user menu"}
|
|
863
|
+
<button class="user-btn" aria-label={githubMode ? "GitHub user menu" : "Local user menu"} data-tooltip={githubMode ? "GitHub user menu" : "Local user menu"} aria-haspopup="menu" aria-expanded={userOpen} onclick={() => (userOpen = !userOpen)}>
|
|
734
864
|
{@render avatar()}
|
|
735
|
-
<span class="user-details"><span class="uname">{userName}</span><small>{
|
|
865
|
+
<span class="user-details"><span class="uname">{userName}</span><small>{userLogin}</small></span>
|
|
736
866
|
<span class="user-menu-icon"><Icon name="dotsThreeOutlineVertical" size={18} /></span>
|
|
737
867
|
</button>
|
|
738
868
|
{#if userOpen}
|
|
739
869
|
<div class="user-menu" role="menu">
|
|
740
870
|
<div class="user-menu-head">
|
|
741
871
|
{@render avatar()}
|
|
742
|
-
<div class="user-details"><strong>{userName}</strong><small>{
|
|
872
|
+
<div class="user-details"><strong>{userName}</strong><small>{userLogin}</small></div>
|
|
743
873
|
</div>
|
|
744
874
|
<div class="divider"></div>
|
|
745
875
|
<a role="menuitem" href="/" target="_blank" rel="noopener">
|
|
@@ -760,7 +890,7 @@
|
|
|
760
890
|
<a role="menuitem" href={githubAccess?.installUrl} target="_blank" rel="noopener noreferrer">Configure GitHub access</a>
|
|
761
891
|
<button role="menuitem" class="github-reconnect" disabled={connectionBusy || saveState === "saving"} onclick={() => { userOpen = false; void connectGitHub(); }}>Reconnect GitHub</button>
|
|
762
892
|
{/if}
|
|
763
|
-
<button id={githubMode ? "github-sign-out" : undefined} role="menuitem" class:muted={!githubMode} disabled={!githubMode || saveState === "saving" || connectionBusy}
|
|
893
|
+
<button id={githubMode ? "github-sign-out" : undefined} role="menuitem" class:muted={!githubMode} disabled={!githubMode || saveState === "saving" || connectionBusy} data-tooltip={githubMode ? "Sign out of this editor" : "Local mode"} onclick={signOut}>
|
|
764
894
|
<Icon name="signOut" size={13} />
|
|
765
895
|
Sign Out
|
|
766
896
|
{#if !githubMode}<span class="soon">backend: github</span>{/if}
|
|
@@ -807,7 +937,7 @@
|
|
|
807
937
|
<button class="primary" onclick={() => (sel?.kind === "collection" ? newPost(coll!) : newPage(sel?.kind === "documents" ? activeDocType : pageDocType))}>Add new</button>
|
|
808
938
|
{/if}
|
|
809
939
|
{#if sel?.kind === "post" && post}
|
|
810
|
-
<button class="icon-btn"
|
|
940
|
+
<button class="icon-btn" aria-label="Delete post" data-tooltip="Delete post" disabled={saveState === "saving"} onclick={deletePost}>
|
|
811
941
|
<Icon name="trash" size={15} />
|
|
812
942
|
</button>
|
|
813
943
|
{/if}
|
|
@@ -885,7 +1015,7 @@
|
|
|
885
1015
|
values={draft}
|
|
886
1016
|
documents={draftDocuments}
|
|
887
1017
|
previewPath={documentPreview("draft")}
|
|
888
|
-
|
|
1018
|
+
websitePath={documentPreview("website")}
|
|
889
1019
|
creating={newDocument?.id === sel.id}
|
|
890
1020
|
devMode={settings.devMode}
|
|
891
1021
|
reloadToken={documentReload}
|
|
@@ -905,6 +1035,9 @@
|
|
|
905
1035
|
<MenuEditor items={draft.items as MenuItem[]} maxDepth={manifest.menus.maxDepth} oninput={(items) => { draft.items = items; touch(); }} />
|
|
906
1036
|
|
|
907
1037
|
{:else if sel?.kind === "page" && page}
|
|
1038
|
+
{#if !page.shared}
|
|
1039
|
+
<section><label><span>Website route</span><input type="text" value={manifest.preview?.pages?.[page.slug] ?? "Not configured"} readonly /></label><p class="hint">This copy page uses a fixed Astro route. Its URL can only be changed in the website code; slug metadata cannot move it.</p></section>
|
|
1040
|
+
{/if}
|
|
908
1041
|
{#each page.groups as group (group.id)}
|
|
909
1042
|
<section>
|
|
910
1043
|
<h3>{group.label}</h3>
|
|
@@ -933,13 +1066,13 @@
|
|
|
933
1066
|
bodyInline={settings.userPref.editorExperience === "inline"}
|
|
934
1067
|
onbody={(value) => { bodyDraft = value; touch(); }}
|
|
935
1068
|
previewPath={manifest.preview ? previewUrl(manifest.preview, sel, "draft", !!newEntry) : null}
|
|
936
|
-
|
|
1069
|
+
websitePath={manifest.preview ? previewUrl(manifest.preview, { ...sel, urlSlug: Object.hasOwn(draft, "slug") ? String(draft.slug) : undefined }, "website") : null}
|
|
937
1070
|
creating={!!newEntry}
|
|
938
1071
|
devMode={settings.devMode}
|
|
939
1072
|
onchange={(next) => { draft = next; touch(); }}
|
|
940
1073
|
>
|
|
941
1074
|
{#snippet properties(selected, focusToken, bodyInline, oneditbody)}
|
|
942
|
-
<CollectionPanel fields={coll.fields} values={draft} documents={manifest.documents ?? []} body={bodyDraft as ArticleBody} {bodyInline} {oneditbody} {selected} {focusToken} devMode={settings.devMode} onchange={(key, value) => { draft[key] = value; touch(); }} onbody={(value) => { bodyDraft = value; touch(); }} />
|
|
1075
|
+
<CollectionPanel fields={coll.fields} values={draft} storageSlug={post.slug} documents={manifest.documents ?? []} body={bodyDraft as ArticleBody} {bodyInline} {oneditbody} {selected} {focusToken} devMode={settings.devMode} onchange={(key, value) => { draft[key] = value; touch(); }} onbody={(value) => { bodyDraft = value; touch(); }} />
|
|
943
1076
|
{/snippet}
|
|
944
1077
|
</VisualEditor>
|
|
945
1078
|
{/key}
|
|
@@ -947,7 +1080,7 @@
|
|
|
947
1080
|
{:else}
|
|
948
1081
|
<div class="welcome">
|
|
949
1082
|
<img src={logo} alt="" width="64" height="64" />
|
|
950
|
-
<h2>Welcome to
|
|
1083
|
+
<h2>Welcome to Rigel CMS</h2>
|
|
951
1084
|
<p>The best CMS in the whole space.</p>
|
|
952
1085
|
<p class="welcome-hint">Select a page or collection from the sidebar to start editing.</p>
|
|
953
1086
|
</div>
|
|
@@ -968,7 +1101,7 @@
|
|
|
968
1101
|
{/if}
|
|
969
1102
|
|
|
970
1103
|
{#if settingsOpen}
|
|
971
|
-
<Settings bind:s={settings} error={settingsError} saving={saveState === "saving"} onerase={eraseBrowserData} onclose={() => (settingsOpen = false)} />
|
|
1104
|
+
<Settings bind:s={settings} github={githubMode ? manifest.github : undefined} user={githubAccess?.user} error={settingsError} saving={saveState === "saving"} onerase={eraseBrowserData} onclose={() => (settingsOpen = false)} />
|
|
972
1105
|
{/if}
|
|
973
1106
|
{#if shortcutsOpen}
|
|
974
1107
|
<Shortcuts onclose={() => (shortcutsOpen = false)} />
|
|
@@ -979,3 +1112,6 @@
|
|
|
979
1112
|
{#if actionDialog}
|
|
980
1113
|
<ActionDialog {...actionDialog} onclose={() => { actionDialog = null; }} />
|
|
981
1114
|
{/if}
|
|
1115
|
+
{#if publishOpen}
|
|
1116
|
+
<PublishDialog review={publishReview} loading={publishLoading} busy={publishBusy} error={publishError} message={publishMessage} onrefresh={reviewPublish} onconfirm={publish} onclose={closePublish} />
|
|
1117
|
+
{/if}
|