@drawnagency/primitives 0.1.10 → 0.1.12
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/dist/{chunk-32H6Q6CX.js → chunk-2YYC2VJY.js} +1 -1
- package/dist/{chunk-XQXZHDNR.js → chunk-PHCEJP7I.js} +1 -1
- package/dist/{chunk-6SK5BLG3.js → chunk-Q7OKHD6I.js} +1 -1
- package/dist/components/editor/SectionWrapper.d.ts +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/StatusDots.d.ts +25 -0
- package/dist/components/editor/StatusDots.d.ts.map +1 -0
- package/dist/components/editor/StatusPicker.d.ts +1 -1
- package/dist/components/editor/StatusPicker.d.ts.map +1 -1
- package/dist/components/editor/index.d.ts +1 -0
- package/dist/components/editor/index.d.ts.map +1 -1
- package/dist/components/shared/SegmentedControl.d.ts +13 -0
- package/dist/components/shared/SegmentedControl.d.ts.map +1 -0
- package/dist/components/shared/SplitButton.d.ts +17 -0
- package/dist/components/shared/SplitButton.d.ts.map +1 -0
- package/dist/components/shell/EditorContext.d.ts +2 -0
- package/dist/components/shell/EditorContext.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useContentLifecycle.d.ts +13 -0
- package/dist/hooks/useContentLifecycle.d.ts.map +1 -0
- package/dist/hooks/useEditorPublish.d.ts +5 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/lib/dexie.d.ts +8 -1
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/index.js +2 -2
- package/dist/lib/registry.d.ts +6 -1
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -2
- package/dist/schemas/site-config.d.ts +2 -2
- package/package.json +1 -1
- package/src/components/brandguide/DoDontList.tsx +1 -1
- package/src/components/editor/SectionWrapper.tsx +44 -2
- package/src/components/editor/StatusBadge.tsx +2 -2
- package/src/components/editor/StatusDots.tsx +131 -0
- package/src/components/editor/StatusPicker.tsx +6 -6
- package/src/components/editor/index.ts +1 -0
- package/src/components/sections/SectionLayout.tsx +1 -1
- package/src/components/shared/Navigation.tsx +3 -3
- package/src/components/shared/SegmentedControl.tsx +43 -0
- package/src/components/shared/SplitButton.tsx +97 -0
- package/src/components/shell/EditorContext.tsx +5 -1
- package/src/components/shell/EditorShell.tsx +157 -52
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useContentLifecycle.ts +34 -0
- package/src/hooks/useEditorPublish.ts +230 -66
- package/src/lib/dexie.ts +43 -2
- package/src/lib/registry.ts +6 -1
- package/src/schemas/site-config.ts +1 -1
|
@@ -24,11 +24,20 @@ interface PublishDeps {
|
|
|
24
24
|
siteIndexRef: React.RefObject<SiteIndex>;
|
|
25
25
|
siteConfig: SiteConfig | null;
|
|
26
26
|
sections: LoadedSection[];
|
|
27
|
+
deletedSectionIds?: string[];
|
|
27
28
|
onSuccess: () => void;
|
|
28
29
|
mediaManifest: MediaManifest;
|
|
29
30
|
pendingMediaItems: MediaItem[];
|
|
30
31
|
pendingMediaDeletions: string[];
|
|
31
32
|
onMediaPublished: (publishedItems: MediaItem[], publishedDeletions: string[]) => void;
|
|
33
|
+
onShasUpdated: (savedSha: string | null, mainSha: string | null) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface GatheredMedia {
|
|
37
|
+
mediaUploads: { item: MediaItem; blobs: { path: string; base64: string }[] }[];
|
|
38
|
+
blobUrlsToRevoke: string[];
|
|
39
|
+
updatedManifest: MediaManifest | undefined;
|
|
40
|
+
hasMediaChanges: boolean;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
export function useEditorPublish({
|
|
@@ -39,11 +48,13 @@ export function useEditorPublish({
|
|
|
39
48
|
siteIndexRef,
|
|
40
49
|
siteConfig,
|
|
41
50
|
sections,
|
|
51
|
+
deletedSectionIds,
|
|
42
52
|
onSuccess,
|
|
43
53
|
mediaManifest,
|
|
44
54
|
pendingMediaItems,
|
|
45
55
|
pendingMediaDeletions,
|
|
46
56
|
onMediaPublished,
|
|
57
|
+
onShasUpdated,
|
|
47
58
|
}: PublishDeps) {
|
|
48
59
|
const [isPublishing, setIsPublishing] = useState(false);
|
|
49
60
|
const [publishFeedback, setPublishFeedback] = useState<string | null>(null);
|
|
@@ -55,7 +66,72 @@ export function useEditorPublish({
|
|
|
55
66
|
};
|
|
56
67
|
}, []);
|
|
57
68
|
|
|
58
|
-
const
|
|
69
|
+
const showFeedback = useCallback((message: string, duration: number) => {
|
|
70
|
+
setPublishFeedback(message);
|
|
71
|
+
if (feedbackTimerRef.current) clearTimeout(feedbackTimerRef.current);
|
|
72
|
+
feedbackTimerRef.current = setTimeout(() => setPublishFeedback(null), duration);
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
async function gatherMediaPayload(): Promise<GatheredMedia> {
|
|
76
|
+
const hasMediaChanges = pendingMediaItems.length > 0 || pendingMediaDeletions.length > 0;
|
|
77
|
+
const mediaUploads: { item: MediaItem; blobs: { path: string; base64: string }[] }[] = [];
|
|
78
|
+
const blobUrlsToRevoke: string[] = [];
|
|
79
|
+
|
|
80
|
+
for (const item of pendingMediaItems) {
|
|
81
|
+
const localUrls = await getPendingMediaLocalUrls(item.id);
|
|
82
|
+
if (!localUrls) continue;
|
|
83
|
+
|
|
84
|
+
for (const url of Object.values(localUrls)) {
|
|
85
|
+
blobUrlsToRevoke.push(url);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const blobs: { path: string; base64: string }[] = [];
|
|
89
|
+
const failedBlobFetches: string[] = [];
|
|
90
|
+
const mimeExt: Record<string, string> = {
|
|
91
|
+
"image/gif": "gif", "image/apng": "apng", "video/mp4": "mp4", "video/webm": "webm",
|
|
92
|
+
};
|
|
93
|
+
for (const [key, url] of Object.entries(localUrls)) {
|
|
94
|
+
if (key === "primary" && item.kind === "image") continue;
|
|
95
|
+
try {
|
|
96
|
+
const resp = await fetch(url);
|
|
97
|
+
const blob = await resp.blob();
|
|
98
|
+
const base64 = await blobToBase64(blob);
|
|
99
|
+
if (key === "primary") {
|
|
100
|
+
const ext = mimeExt[item.mimeType] ?? "bin";
|
|
101
|
+
blobs.push({ path: `assets/images/${item.folder}/original.${ext}`, base64 });
|
|
102
|
+
} else {
|
|
103
|
+
blobs.push({ path: `assets/images/${item.folder}/${key}.webp`, base64 });
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
failedBlobFetches.push(`${item.id}/${key}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (failedBlobFetches.length > 0) {
|
|
111
|
+
throw new Error(`Media upload failed: could not read blob data for ${failedBlobFetches.join(", ")}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (blobs.length > 0) {
|
|
115
|
+
mediaUploads.push({ item, blobs });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let updatedManifest: MediaManifest | undefined;
|
|
120
|
+
if (hasMediaChanges) {
|
|
121
|
+
const images = { ...mediaManifest.images };
|
|
122
|
+
for (const upload of mediaUploads) {
|
|
123
|
+
images[upload.item.id] = upload.item;
|
|
124
|
+
}
|
|
125
|
+
for (const id of pendingMediaDeletions) {
|
|
126
|
+
delete images[id];
|
|
127
|
+
}
|
|
128
|
+
updatedManifest = { images };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { mediaUploads, blobUrlsToRevoke, updatedManifest, hasMediaChanges };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const handleSave = useCallback(async () => {
|
|
59
135
|
if (!siteConfig) return;
|
|
60
136
|
|
|
61
137
|
setIsPublishing(true);
|
|
@@ -67,80 +143,43 @@ export function useEditorPublish({
|
|
|
67
143
|
|
|
68
144
|
const hasChanges = await hasLocalChanges();
|
|
69
145
|
const hasMediaChanges = pendingMediaItems.length > 0 || pendingMediaDeletions.length > 0;
|
|
70
|
-
|
|
146
|
+
const hasDeletedSections = (deletedSectionIds?.length ?? 0) > 0;
|
|
147
|
+
if (!hasChanges && !isConfigDirty() && !hasMediaChanges && !hasDeletedSections) {
|
|
71
148
|
setIsPublishing(false);
|
|
72
149
|
return;
|
|
73
150
|
}
|
|
74
151
|
|
|
75
152
|
const dirty = await getDirtySections();
|
|
153
|
+
const { mediaUploads, blobUrlsToRevoke, updatedManifest, hasMediaChanges: mediaChanged } = await gatherMediaPayload();
|
|
76
154
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for (const url of Object.values(localUrls)) {
|
|
86
|
-
blobUrlsToRevoke.push(url);
|
|
155
|
+
// Build a filtered siteIndex if there are deletions
|
|
156
|
+
let siteIndex = siteIndexRef.current;
|
|
157
|
+
if (deletedSectionIds?.length) {
|
|
158
|
+
const deleteSet = new Set(deletedSectionIds);
|
|
159
|
+
const filteredSections = { ...siteIndex.sections };
|
|
160
|
+
for (const id of deletedSectionIds) {
|
|
161
|
+
delete filteredSections[id];
|
|
87
162
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"image/gif": "gif", "image/apng": "apng", "video/mp4": "mp4", "video/webm": "webm",
|
|
163
|
+
siteIndex = {
|
|
164
|
+
...siteIndex,
|
|
165
|
+
order: siteIndex.order.filter((id) => !deleteSet.has(id)),
|
|
166
|
+
sections: filteredSections,
|
|
93
167
|
};
|
|
94
|
-
for (const [key, url] of Object.entries(localUrls)) {
|
|
95
|
-
if (key === "primary" && item.kind === "image") continue;
|
|
96
|
-
try {
|
|
97
|
-
const resp = await fetch(url);
|
|
98
|
-
const blob = await resp.blob();
|
|
99
|
-
const base64 = await blobToBase64(blob);
|
|
100
|
-
if (key === "primary") {
|
|
101
|
-
const ext = mimeExt[item.mimeType] ?? "bin";
|
|
102
|
-
blobs.push({ path: `assets/images/${item.folder}/original.${ext}`, base64 });
|
|
103
|
-
} else {
|
|
104
|
-
blobs.push({ path: `assets/images/${item.folder}/${key}.webp`, base64 });
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
failedBlobFetches.push(`${item.id}/${key}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (failedBlobFetches.length > 0) {
|
|
112
|
-
throw new Error(`Media upload failed: could not read blob data for ${failedBlobFetches.join(", ")}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (blobs.length > 0) {
|
|
116
|
-
mediaUploads.push({ item, blobs });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Build updated manifest
|
|
121
|
-
let updatedManifest: MediaManifest | undefined;
|
|
122
|
-
if (hasMediaChanges) {
|
|
123
|
-
const images = { ...mediaManifest.images };
|
|
124
|
-
for (const upload of mediaUploads) {
|
|
125
|
-
images[upload.item.id] = upload.item;
|
|
126
|
-
}
|
|
127
|
-
for (const id of pendingMediaDeletions) {
|
|
128
|
-
delete images[id];
|
|
129
|
-
}
|
|
130
|
-
updatedManifest = { images };
|
|
131
168
|
}
|
|
132
169
|
|
|
133
170
|
const response = await fetch("/api/save", {
|
|
134
171
|
method: "POST",
|
|
135
172
|
headers: { "Content-Type": "application/json" },
|
|
136
173
|
body: JSON.stringify({
|
|
174
|
+
targetBranch: "saved",
|
|
137
175
|
sections: dirty.map(({ sectionId, content }) => ({
|
|
138
176
|
id: sectionId,
|
|
139
177
|
content,
|
|
140
178
|
})),
|
|
141
|
-
siteIndex
|
|
179
|
+
siteIndex,
|
|
180
|
+
...(deletedSectionIds?.length ? { deletedSectionIds } : {}),
|
|
142
181
|
...(isConfigDirty() ? { siteConfig } : {}),
|
|
143
|
-
...(
|
|
182
|
+
...(mediaChanged ? {
|
|
144
183
|
media: {
|
|
145
184
|
uploads: mediaUploads.map(({ item, blobs }) => ({ item, blobs })),
|
|
146
185
|
deletions: pendingMediaDeletions.map((id) => ({
|
|
@@ -155,33 +194,158 @@ export function useEditorPublish({
|
|
|
155
194
|
|
|
156
195
|
if (!response.ok) {
|
|
157
196
|
const errorBody = await response.json().catch(() => ({}));
|
|
158
|
-
throw new Error(errorBody.error || "
|
|
197
|
+
throw new Error(errorBody.error || "Save failed");
|
|
159
198
|
}
|
|
160
199
|
|
|
161
200
|
const { sha } = await response.json();
|
|
162
201
|
|
|
163
202
|
await discardLocalChanges();
|
|
164
203
|
await clearPendingMedia();
|
|
165
|
-
await cacheContent(sha, sections, siteIndexRef.current, siteConfig);
|
|
166
204
|
clearConfigDirty();
|
|
167
205
|
onSuccess();
|
|
168
206
|
onMediaPublished(pendingMediaItems, pendingMediaDeletions);
|
|
207
|
+
onShasUpdated(sha, null);
|
|
169
208
|
for (const url of blobUrlsToRevoke) {
|
|
170
209
|
URL.revokeObjectURL(url);
|
|
171
210
|
}
|
|
172
211
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
212
|
+
showFeedback("Saved", 3000);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error("Save failed:", error);
|
|
215
|
+
showFeedback("Save failed", 5000);
|
|
216
|
+
} finally {
|
|
217
|
+
setIsPublishing(false);
|
|
218
|
+
}
|
|
219
|
+
}, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback]);
|
|
220
|
+
|
|
221
|
+
const handlePublish = useCallback(async () => {
|
|
222
|
+
setIsPublishing(true);
|
|
223
|
+
setPublishFeedback(null);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const response = await fetch("/api/publish", {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json" },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
233
|
+
throw new Error(errorBody.error || "Publish failed");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { sha } = await response.json();
|
|
237
|
+
|
|
238
|
+
onShasUpdated(null, sha);
|
|
239
|
+
showFeedback("Published", 3000);
|
|
176
240
|
} catch (error) {
|
|
177
241
|
console.error("Publish failed:", error);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
242
|
+
showFeedback("Publish failed", 5000);
|
|
243
|
+
} finally {
|
|
244
|
+
setIsPublishing(false);
|
|
245
|
+
}
|
|
246
|
+
}, [onShasUpdated, showFeedback]);
|
|
247
|
+
|
|
248
|
+
const handleSaveAndPublish = useCallback(async () => {
|
|
249
|
+
if (!siteConfig) return;
|
|
250
|
+
|
|
251
|
+
setIsPublishing(true);
|
|
252
|
+
setPublishFeedback(null);
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
cancelPendingFlush();
|
|
256
|
+
await flushNow();
|
|
257
|
+
|
|
258
|
+
const hasChanges = await hasLocalChanges();
|
|
259
|
+
const hasMediaChanges = pendingMediaItems.length > 0 || pendingMediaDeletions.length > 0;
|
|
260
|
+
const hasDeletedSections = (deletedSectionIds?.length ?? 0) > 0;
|
|
261
|
+
const hasLocalEdits = hasChanges || isConfigDirty() || hasMediaChanges || hasDeletedSections;
|
|
262
|
+
|
|
263
|
+
let blobUrlsToRevoke: string[] = [];
|
|
264
|
+
|
|
265
|
+
if (hasLocalEdits) {
|
|
266
|
+
const dirty = await getDirtySections();
|
|
267
|
+
const gathered = await gatherMediaPayload();
|
|
268
|
+
blobUrlsToRevoke = gathered.blobUrlsToRevoke;
|
|
269
|
+
|
|
270
|
+
// Build a filtered siteIndex if there are deletions
|
|
271
|
+
let siteIndex = siteIndexRef.current;
|
|
272
|
+
if (deletedSectionIds?.length) {
|
|
273
|
+
const deleteSet = new Set(deletedSectionIds);
|
|
274
|
+
const filteredSections = { ...siteIndex.sections };
|
|
275
|
+
for (const id of deletedSectionIds) {
|
|
276
|
+
delete filteredSections[id];
|
|
277
|
+
}
|
|
278
|
+
siteIndex = {
|
|
279
|
+
...siteIndex,
|
|
280
|
+
order: siteIndex.order.filter((id) => !deleteSet.has(id)),
|
|
281
|
+
sections: filteredSections,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const saveResponse = await fetch("/api/save", {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "Content-Type": "application/json" },
|
|
288
|
+
body: JSON.stringify({
|
|
289
|
+
targetBranch: "saved",
|
|
290
|
+
sections: dirty.map(({ sectionId, content }) => ({
|
|
291
|
+
id: sectionId,
|
|
292
|
+
content,
|
|
293
|
+
})),
|
|
294
|
+
siteIndex,
|
|
295
|
+
...(deletedSectionIds?.length ? { deletedSectionIds } : {}),
|
|
296
|
+
...(isConfigDirty() ? { siteConfig } : {}),
|
|
297
|
+
...(gathered.hasMediaChanges ? {
|
|
298
|
+
media: {
|
|
299
|
+
uploads: gathered.mediaUploads.map(({ item, blobs }) => ({ item, blobs })),
|
|
300
|
+
deletions: pendingMediaDeletions.map((id) => ({
|
|
301
|
+
id,
|
|
302
|
+
folder: mediaManifest.images[id]?.folder,
|
|
303
|
+
})).filter((d) => d.folder),
|
|
304
|
+
manifest: gathered.updatedManifest,
|
|
305
|
+
},
|
|
306
|
+
} : {}),
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (!saveResponse.ok) {
|
|
311
|
+
const errorBody = await saveResponse.json().catch(() => ({}));
|
|
312
|
+
throw new Error(errorBody.error || "Save failed");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const publishResponse = await fetch("/api/publish", {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": "application/json" },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (!publishResponse.ok) {
|
|
322
|
+
const errorBody = await publishResponse.json().catch(() => ({}));
|
|
323
|
+
throw new Error(errorBody.error || "Publish failed");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const { sha } = await publishResponse.json();
|
|
327
|
+
|
|
328
|
+
if (hasLocalEdits) {
|
|
329
|
+
await discardLocalChanges();
|
|
330
|
+
await clearPendingMedia();
|
|
331
|
+
await cacheContent(sha, sections, siteIndexRef.current, siteConfig);
|
|
332
|
+
clearConfigDirty();
|
|
333
|
+
onSuccess();
|
|
334
|
+
onMediaPublished(pendingMediaItems, pendingMediaDeletions);
|
|
335
|
+
for (const url of blobUrlsToRevoke) {
|
|
336
|
+
URL.revokeObjectURL(url);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
onShasUpdated(null, sha);
|
|
341
|
+
showFeedback("Published", 3000);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error("Publish failed:", error);
|
|
344
|
+
showFeedback("Publish failed", 5000);
|
|
181
345
|
} finally {
|
|
182
346
|
setIsPublishing(false);
|
|
183
347
|
}
|
|
184
|
-
}, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished]);
|
|
348
|
+
}, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback]);
|
|
185
349
|
|
|
186
|
-
return { isPublishing, publishFeedback, handlePublish };
|
|
350
|
+
return { isPublishing, publishFeedback, handleSave, handlePublish, handleSaveAndPublish };
|
|
187
351
|
}
|
package/src/lib/dexie.ts
CHANGED
|
@@ -15,12 +15,14 @@ interface SiteIndexRow {
|
|
|
15
15
|
key: string;
|
|
16
16
|
order: string[];
|
|
17
17
|
sections: Record<string, SectionMeta>;
|
|
18
|
+
deletedSections: string[];
|
|
18
19
|
updatedAt: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
interface MetaRow {
|
|
22
23
|
key: string;
|
|
23
24
|
lastSavedSha: string | null;
|
|
25
|
+
mainSha: string | null;
|
|
24
26
|
lastSavedAt: string | null;
|
|
25
27
|
siteId: string;
|
|
26
28
|
}
|
|
@@ -98,6 +100,16 @@ class EditorDatabase extends Dexie {
|
|
|
98
100
|
pendingMedia: "id",
|
|
99
101
|
pendingMediaDeletions: "id",
|
|
100
102
|
});
|
|
103
|
+
this.version(5).stores({
|
|
104
|
+
sections: "sectionId",
|
|
105
|
+
siteIndex: "key",
|
|
106
|
+
meta: "key",
|
|
107
|
+
siteConfig: "key",
|
|
108
|
+
contentCache: "key",
|
|
109
|
+
mediaManifest: "key",
|
|
110
|
+
pendingMedia: "id",
|
|
111
|
+
pendingMediaDeletions: "id",
|
|
112
|
+
});
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
115
|
|
|
@@ -132,6 +144,7 @@ export async function restoreLocalChanges(): Promise<{
|
|
|
132
144
|
sections: Record<string, SectionContent>;
|
|
133
145
|
siteIndex?: SiteIndex;
|
|
134
146
|
siteConfig?: Record<string, unknown>;
|
|
147
|
+
deletedSections: string[];
|
|
135
148
|
}> {
|
|
136
149
|
const sectionRows = await getDb().sections.toArray();
|
|
137
150
|
const sections: Record<string, SectionContent> = {};
|
|
@@ -149,10 +162,11 @@ export async function restoreLocalChanges(): Promise<{
|
|
|
149
162
|
sections,
|
|
150
163
|
siteIndex: { siteId, order: indexRow.order, sections: indexRow.sections },
|
|
151
164
|
siteConfig: configRow?.config,
|
|
165
|
+
deletedSections: indexRow.deletedSections ?? [],
|
|
152
166
|
};
|
|
153
167
|
}
|
|
154
168
|
|
|
155
|
-
return { sections, siteIndex: undefined, siteConfig: configRow?.config };
|
|
169
|
+
return { sections, siteIndex: undefined, siteConfig: configRow?.config, deletedSections: [] };
|
|
156
170
|
}
|
|
157
171
|
|
|
158
172
|
export async function discardLocalChanges(): Promise<void> {
|
|
@@ -168,7 +182,7 @@ export async function discardLocalChanges(): Promise<void> {
|
|
|
168
182
|
});
|
|
169
183
|
}
|
|
170
184
|
|
|
171
|
-
export async function persistSiteIndex(index: SiteIndex): Promise<void> {
|
|
185
|
+
export async function persistSiteIndex(index: SiteIndex, deletedSections: string[] = []): Promise<void> {
|
|
172
186
|
const now = new Date().toISOString();
|
|
173
187
|
const database = getDb();
|
|
174
188
|
await database.transaction("rw", [database.siteIndex, database.meta], async () => {
|
|
@@ -176,11 +190,13 @@ export async function persistSiteIndex(index: SiteIndex): Promise<void> {
|
|
|
176
190
|
key: "current",
|
|
177
191
|
order: index.order,
|
|
178
192
|
sections: index.sections,
|
|
193
|
+
deletedSections,
|
|
179
194
|
updatedAt: now,
|
|
180
195
|
});
|
|
181
196
|
await database.meta.put({
|
|
182
197
|
key: "current",
|
|
183
198
|
lastSavedSha: null,
|
|
199
|
+
mainSha: null,
|
|
184
200
|
lastSavedAt: null,
|
|
185
201
|
siteId: index.siteId,
|
|
186
202
|
});
|
|
@@ -246,11 +262,13 @@ export async function persistAll(
|
|
|
246
262
|
key: "current",
|
|
247
263
|
order: siteIndex.order,
|
|
248
264
|
sections: siteIndex.sections,
|
|
265
|
+
deletedSections: [],
|
|
249
266
|
updatedAt: now,
|
|
250
267
|
});
|
|
251
268
|
await database.meta.put({
|
|
252
269
|
key: "current",
|
|
253
270
|
lastSavedSha: null,
|
|
271
|
+
mainSha: null,
|
|
254
272
|
lastSavedAt: null,
|
|
255
273
|
siteId: siteIndex.siteId,
|
|
256
274
|
});
|
|
@@ -267,6 +285,29 @@ export async function persistAll(
|
|
|
267
285
|
);
|
|
268
286
|
}
|
|
269
287
|
|
|
288
|
+
export async function updateBranchShas(savedSha: string | null, mainSha: string | null): Promise<void> {
|
|
289
|
+
const database = getDb();
|
|
290
|
+
const existing = await database.meta.get("current");
|
|
291
|
+
if (!existing) return;
|
|
292
|
+
await database.meta.put({
|
|
293
|
+
...existing,
|
|
294
|
+
lastSavedSha: savedSha,
|
|
295
|
+
mainSha,
|
|
296
|
+
lastSavedAt: new Date().toISOString(),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function getBranchShas(): Promise<{ savedSha: string | null; mainSha: string | null } | null> {
|
|
301
|
+
const row = await getDb().meta.get("current");
|
|
302
|
+
if (!row) return null;
|
|
303
|
+
return { savedSha: row.lastSavedSha, mainSha: row.mainSha ?? null };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function getDeletedSections(): Promise<string[]> {
|
|
307
|
+
const row = await getDb().siteIndex.get("current");
|
|
308
|
+
return row?.deletedSections ?? [];
|
|
309
|
+
}
|
|
310
|
+
|
|
270
311
|
export async function cacheContent(
|
|
271
312
|
sha: string,
|
|
272
313
|
sections: LoadedSection[],
|
package/src/lib/registry.ts
CHANGED
|
@@ -73,11 +73,16 @@ export interface WrapperProps {
|
|
|
73
73
|
audiences: Audience[];
|
|
74
74
|
access: string[];
|
|
75
75
|
onAccessChange?: (access: string[]) => void;
|
|
76
|
-
onStatusChange?: (status: "draft" | "
|
|
76
|
+
onStatusChange?: (status: "draft" | "live" | "archived") => void;
|
|
77
77
|
onSectionChange?: (options: Record<string, unknown>) => void;
|
|
78
78
|
onReorder?: (fromIndex: number, toIndex: number) => void;
|
|
79
79
|
onRequestInsert?: (index: number) => void;
|
|
80
80
|
onDelete?: () => void;
|
|
81
|
+
isDeleted?: boolean;
|
|
82
|
+
onUndoDelete?: () => void;
|
|
83
|
+
mainStatus?: string | null;
|
|
84
|
+
contentDiffersFromMain?: boolean;
|
|
85
|
+
isLocalOnly?: boolean;
|
|
81
86
|
children: React.ReactNode;
|
|
82
87
|
}
|
|
83
88
|
|