@drawnagency/primitives 0.1.98 → 0.1.100
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-EUSWEVOA.js → chunk-3TO4RRHY.js} +12 -1
- package/dist/{chunk-KP4DWTPR.js → chunk-72FMQQIA.js} +246 -3
- package/dist/chunk-BS5AQHZG.js +45 -0
- package/dist/{chunk-NA5EJM6Z.js → chunk-CZLYECHC.js} +19 -8
- package/dist/{chunk-HKHEAH5F.js → chunk-GOCFTXR3.js} +8 -60
- package/dist/{chunk-GYCBWGVW.js → chunk-LBHQWIVL.js} +79 -24
- package/dist/{chunk-HJSE6SK5.js → chunk-REDDTTGJ.js} +3 -3
- package/dist/collab/rebase.d.ts.map +1 -1
- package/dist/collab/rebase.js +3 -1
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/docfile/DocFileOverlay.d.ts +12 -0
- package/dist/components/docfile/DocFileOverlay.d.ts.map +1 -0
- package/dist/components/docfile/DocFilePane.d.ts +17 -0
- package/dist/components/docfile/DocFilePane.d.ts.map +1 -0
- package/dist/components/docfile/doc-file-state.d.ts +33 -0
- package/dist/components/docfile/doc-file-state.d.ts.map +1 -0
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/editor/index.js +1 -1
- package/dist/components/sections/Document/index.d.ts +18 -0
- package/dist/components/sections/Document/index.d.ts.map +1 -0
- package/dist/components/sections/all-sections.d.ts +10 -0
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.d.ts +10 -0
- package/dist/components/sections/brand-guide.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +3 -1
- package/dist/components/sections/register-schemas.js +5 -3
- package/dist/components/sections/register.js +5 -3
- package/dist/components/shared/DocumentSourceField.d.ts +13 -0
- package/dist/components/shared/DocumentSourceField.d.ts.map +1 -0
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shell/EditorModal.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/MediaBreadcrumb.d.ts.map +1 -1
- package/dist/components/shell/MediaFolderTile.d.ts +3 -1
- package/dist/components/shell/MediaFolderTile.d.ts.map +1 -1
- package/dist/components/shell/MediaFolderTree.d.ts +3 -0
- package/dist/components/shell/MediaFolderTree.d.ts.map +1 -1
- package/dist/components/shell/MediaLibraryContext.d.ts +18 -1
- package/dist/components/shell/MediaLibraryContext.d.ts.map +1 -1
- package/dist/components/shell/MediaLibraryModal.d.ts +21 -1
- package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
- package/dist/hooks/useDocumentUploads.d.ts +32 -0
- package/dist/hooks/useDocumentUploads.d.ts.map +1 -0
- package/dist/hooks/useEditorPublish.d.ts +23 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts +14 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/hooks/useResolvedDocument.d.ts +12 -0
- package/dist/hooks/useResolvedDocument.d.ts.map +1 -0
- package/dist/index.js +60 -34
- package/dist/lib/block-move.d.ts.map +1 -1
- package/dist/lib/dexie.d.ts +11 -0
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +32 -0
- package/dist/lib/document-refs.d.ts +22 -0
- package/dist/lib/document-refs.d.ts.map +1 -0
- package/dist/lib/document-uploads.d.ts +38 -0
- package/dist/lib/document-uploads.d.ts.map +1 -0
- package/dist/lib/documents.d.ts +65 -0
- package/dist/lib/documents.d.ts.map +1 -0
- package/dist/lib/index-ops.d.ts +1 -0
- package/dist/lib/index-ops.d.ts.map +1 -1
- package/dist/lib/index-ops.js +13 -1
- package/dist/lib/index.js +6 -6
- package/dist/lib/loader.d.ts +6 -0
- package/dist/lib/loader.d.ts.map +1 -1
- package/dist/lib/media-folders.d.ts +8 -0
- package/dist/lib/media-folders.d.ts.map +1 -1
- package/dist/lib/media-folders.js +2 -0
- package/dist/lib/nav.d.ts +29 -1
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/lib/pages.d.ts +18 -0
- package/dist/lib/pages.d.ts.map +1 -1
- package/dist/lib/platform-broker.d.ts +12 -0
- package/dist/lib/platform-broker.d.ts.map +1 -1
- package/dist/lib/platform-broker.js +12 -3
- package/dist/lib/registry.d.ts +10 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/save-chunks.d.ts +15 -0
- package/dist/lib/save-chunks.d.ts.map +1 -1
- package/dist/lib/scroll-lock.d.ts +2 -0
- package/dist/lib/scroll-lock.d.ts.map +1 -0
- package/dist/schemas/documents.d.ts +80 -0
- package/dist/schemas/documents.d.ts.map +1 -0
- package/dist/schemas/documents.js +25 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +24 -2
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +5 -1
- package/dist/storage/types.d.ts +69 -5
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/types/database.d.ts +14 -0
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/collab/rebase.ts +15 -0
- package/src/components/chat/ChatPanel.tsx +5 -4
- package/src/components/docfile/DocFileOverlay.tsx +54 -0
- package/src/components/docfile/DocFilePane.tsx +129 -0
- package/src/components/docfile/doc-file-state.ts +180 -0
- package/src/components/editor/SettingsForm.tsx +10 -0
- package/src/components/sections/Document/index.tsx +163 -0
- package/src/components/sections/brand-guide.ts +2 -1
- package/src/components/shared/DocumentSourceField.tsx +51 -0
- package/src/components/shared/Navigation.tsx +53 -10
- package/src/components/shell/EditorModal.tsx +2 -5
- package/src/components/shell/EditorShell.tsx +154 -10
- package/src/components/shell/MediaBreadcrumb.tsx +14 -1
- package/src/components/shell/MediaFolderTile.tsx +4 -2
- package/src/components/shell/MediaFolderTree.tsx +20 -3
- package/src/components/shell/MediaLibraryContext.tsx +18 -1
- package/src/components/shell/MediaLibraryModal.tsx +223 -45
- package/src/hooks/useDocumentUploads.ts +330 -0
- package/src/hooks/useEditorPublish.ts +197 -11
- package/src/hooks/useMediaPipeline.ts +136 -5
- package/src/hooks/useResolvedDocument.ts +25 -0
- package/src/lib/block-move.ts +15 -4
- package/src/lib/dexie.ts +54 -0
- package/src/lib/document-refs.ts +83 -0
- package/src/lib/document-uploads.ts +66 -0
- package/src/lib/documents.ts +138 -0
- package/src/lib/index-ops.ts +16 -0
- package/src/lib/loader.ts +32 -11
- package/src/lib/media-folders.ts +9 -0
- package/src/lib/nav.ts +46 -2
- package/src/lib/pages.ts +48 -0
- package/src/lib/platform-broker.ts +18 -3
- package/src/lib/registry.ts +9 -0
- package/src/lib/save-chunks.ts +42 -0
- package/src/lib/scroll-lock.ts +27 -0
- package/src/schemas/documents.ts +82 -0
- package/src/schemas/index.ts +1 -0
- package/src/schemas/nav.ts +3 -3
- package/src/storage/index.ts +2 -2
- package/src/storage/types.ts +71 -5
- package/src/types/database.ts +14 -0
- /package/dist/{chunk-FOY5TF7D.js → chunk-ZDQ3TT47.js} +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { hashFileBuffer } from "../media/utils";
|
|
3
|
+
import type { DocumentManifest, DocumentManifestEntry } from "../schemas/documents";
|
|
4
|
+
import {
|
|
5
|
+
planDocumentUpload,
|
|
6
|
+
documentRejectionMessage,
|
|
7
|
+
documentGitPath,
|
|
8
|
+
documentBucketPath,
|
|
9
|
+
type DocumentRejectionReason,
|
|
10
|
+
} from "../lib/document-uploads";
|
|
11
|
+
import {
|
|
12
|
+
addPendingDocument,
|
|
13
|
+
clearPendingDocumentsByIds,
|
|
14
|
+
getPendingDocuments,
|
|
15
|
+
} from "../lib/dexie";
|
|
16
|
+
import type { PendingDocument } from "../components/shell/MediaLibraryContext";
|
|
17
|
+
|
|
18
|
+
const REJECTION_CODES: ReadonlySet<string> = new Set([
|
|
19
|
+
"unsupported_type",
|
|
20
|
+
"too_large",
|
|
21
|
+
"too_small",
|
|
22
|
+
"no_documents_store",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
interface UseDocumentUploadsInput {
|
|
26
|
+
documentManifest: DocumentManifest;
|
|
27
|
+
setDocumentManifest: React.Dispatch<React.SetStateAction<DocumentManifest>>;
|
|
28
|
+
setLocalChangesExist: React.Dispatch<React.SetStateAction<boolean>>;
|
|
29
|
+
/** Drops a committed-but-locally-deleted id from the pipeline's
|
|
30
|
+
* `pendingDocumentDeletions`. That list lives in useMediaPipeline and is
|
|
31
|
+
* invisible here, so without this the dedup below cannot tell a live
|
|
32
|
+
* document from a doomed one. No-op for an id that isn't doomed. */
|
|
33
|
+
undeleteDocument?: (id: string) => void;
|
|
34
|
+
/** Same channel the media pipeline uses — surfaces in UploadRejectionAlert. */
|
|
35
|
+
onUploadError?: (message: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Document uploads. Deliberately a separate hook from useMediaPipeline: bytes
|
|
40
|
+
* pass through untouched (no rasterizer worker, no variants, no poster), the
|
|
41
|
+
* manifest is a sidecar, and every rejection is typed. Both tiers must (1) never
|
|
42
|
+
* throw to the caller, (2) call onUploadError exactly once on failure, and (3)
|
|
43
|
+
* leave no leaked progress entry.
|
|
44
|
+
*/
|
|
45
|
+
export function useDocumentUploads({
|
|
46
|
+
documentManifest,
|
|
47
|
+
setDocumentManifest,
|
|
48
|
+
setLocalChangesExist,
|
|
49
|
+
undeleteDocument,
|
|
50
|
+
onUploadError,
|
|
51
|
+
}: UseDocumentUploadsInput) {
|
|
52
|
+
const [pendingDocuments, setPendingDocuments] = useState<PendingDocument[]>([]);
|
|
53
|
+
const [pendingDocumentUrls, setPendingDocumentUrls] = useState<Record<string, string>>({});
|
|
54
|
+
const [documentUploadProgress, setDocumentUploadProgress] = useState<Record<string, number>>({});
|
|
55
|
+
|
|
56
|
+
// Read inside uploadDocument's async body, where a state snapshot would be
|
|
57
|
+
// stale across an await.
|
|
58
|
+
const pendingRef = useRef<PendingDocument[]>(pendingDocuments);
|
|
59
|
+
useEffect(() => { pendingRef.current = pendingDocuments; }, [pendingDocuments]);
|
|
60
|
+
const manifestRef = useRef<DocumentManifest>(documentManifest);
|
|
61
|
+
useEffect(() => { manifestRef.current = documentManifest; }, [documentManifest]);
|
|
62
|
+
|
|
63
|
+
/** Restore documents staged in a previous session (Dexie survives reload). */
|
|
64
|
+
const hydratePendingDocuments = useCallback(async () => {
|
|
65
|
+
// Never throws: it runs from a mount effect, before/independently of the
|
|
66
|
+
// editor store being initialized, and a failed restore must not take the
|
|
67
|
+
// shell down.
|
|
68
|
+
let rows: Awaited<ReturnType<typeof getPendingDocuments>>;
|
|
69
|
+
try {
|
|
70
|
+
rows = await getPendingDocuments();
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("[useDocumentUploads] Failed to restore staged documents:", err);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (rows.length === 0) return;
|
|
76
|
+
setPendingDocuments(rows.map(({ id, entry }) => ({ id, entry })));
|
|
77
|
+
// The persisted `blob:` URL died with the session that minted it, so a
|
|
78
|
+
// git-tier row (which carries its bytes) gets a fresh object URL here.
|
|
79
|
+
// Bucket-tier rows have no blob and keep their stored URL — theirs is a real
|
|
80
|
+
// remote one. The fallback keeps this function's never-throws contract if a
|
|
81
|
+
// round-tripped row comes back as something createObjectURL rejects.
|
|
82
|
+
setPendingDocumentUrls(
|
|
83
|
+
Object.fromEntries(
|
|
84
|
+
rows.map(({ id, localUrl, blob }) => {
|
|
85
|
+
if (!blob) return [id, localUrl];
|
|
86
|
+
try {
|
|
87
|
+
return [id, URL.createObjectURL(blob)];
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`[useDocumentUploads] Failed to re-mint a URL for "${id}":`, err);
|
|
90
|
+
return [id, localUrl];
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const stage = useCallback(
|
|
98
|
+
async (id: string, entry: DocumentManifestEntry, blob: Blob | null, localUrl: string, filename: string): Promise<boolean> => {
|
|
99
|
+
try {
|
|
100
|
+
await addPendingDocument({ id, entry, blob, localUrl });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(`[useDocumentUploads] Failed to stage "${filename}":`, err);
|
|
103
|
+
URL.revokeObjectURL(localUrl);
|
|
104
|
+
onUploadError?.(`"${filename}" couldn't be stored locally — try again.`);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
setPendingDocuments((prev) => [...prev, { id, entry }]);
|
|
108
|
+
setPendingDocumentUrls((prev) => ({ ...prev, [id]: localUrl }));
|
|
109
|
+
setLocalChangesExist(true);
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
[onUploadError, setLocalChangesExist],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const uploadDocument = useCallback(
|
|
116
|
+
async (file: File, onComplete?: (documentId: string) => void): Promise<void> => {
|
|
117
|
+
const plan = planDocumentUpload({ name: file.name, size: file.size, type: file.type });
|
|
118
|
+
if (plan.tier === "rejected") {
|
|
119
|
+
onUploadError?.(documentRejectionMessage(plan.reason, file.name));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let hash: string;
|
|
124
|
+
try {
|
|
125
|
+
hash = await hashFileBuffer(await file.arrayBuffer());
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`[useDocumentUploads] Failed to read "${file.name}":`, err);
|
|
128
|
+
onUploadError?.(`"${file.name}" couldn't be read — try again.`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Same bytes already known: reuse the id rather than staging a duplicate.
|
|
133
|
+
// Only ever hits on the GIT tier, where the content hash IS the id —
|
|
134
|
+
// bucket-tier ids are server-minted, so re-picking a large file stages a
|
|
135
|
+
// second entry. Accepted: the manifest is correct either way, and the
|
|
136
|
+
// publish-time docs GC collects whichever object stops being referenced.
|
|
137
|
+
if (manifestRef.current[hash] || pendingRef.current.some((d) => d.id === hash)) {
|
|
138
|
+
// A committed document deleted but not yet saved is only DOOMED, never
|
|
139
|
+
// gone: its bytes and its manifest entry are still on the branch, and
|
|
140
|
+
// deletion deliberately leaves documentManifest alone. So re-adding one
|
|
141
|
+
// un-dooms it — otherwise the drop zone swallows the file silently, and
|
|
142
|
+
// the picker points a section at an id the save then deletes. Nothing to
|
|
143
|
+
// re-upload either way. Tier-agnostic on purpose: a bucket-tier id can
|
|
144
|
+
// only reach this branch already staged (its ids are server-minted, so
|
|
145
|
+
// re-picking the file mints a new one), and un-dooming is right there too.
|
|
146
|
+
undeleteDocument?.(hash);
|
|
147
|
+
onComplete?.(hash);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (plan.tier === "git") {
|
|
152
|
+
const entry: DocumentManifestEntry = {
|
|
153
|
+
filename: file.name,
|
|
154
|
+
size: file.size,
|
|
155
|
+
kind: plan.kind,
|
|
156
|
+
storage: "git",
|
|
157
|
+
path: documentGitPath(file.name, hash, plan.ext),
|
|
158
|
+
};
|
|
159
|
+
const localUrl = URL.createObjectURL(file);
|
|
160
|
+
if (await stage(hash, entry, file, localUrl, file.name)) onComplete?.(hash);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The id for a BUCKET-tier document is minted server-side and comes back
|
|
165
|
+
// from upload-intent — never the client hash (decision 4). It keys both
|
|
166
|
+
// the object and the manifest entry, so staging under anything else would
|
|
167
|
+
// point the manifest at bytes that are not there. Declared out here so
|
|
168
|
+
// the `finally` can clear the progress entry it keys.
|
|
169
|
+
let documentId = "";
|
|
170
|
+
try {
|
|
171
|
+
let uploadUrl: string;
|
|
172
|
+
let filename: string;
|
|
173
|
+
try {
|
|
174
|
+
const res = await fetch("/api/documents/upload-intent", {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: { "Content-Type": "application/json" },
|
|
177
|
+
body: JSON.stringify({ name: file.name, size: file.size, contentType: file.type }),
|
|
178
|
+
});
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
181
|
+
const reason: DocumentRejectionReason =
|
|
182
|
+
body.error && REJECTION_CODES.has(body.error)
|
|
183
|
+
? (body.error as DocumentRejectionReason)
|
|
184
|
+
: "no_documents_store";
|
|
185
|
+
onUploadError?.(documentRejectionMessage(reason, file.name));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const intent = (await res.json()) as {
|
|
189
|
+
uploadUrl?: unknown; path?: unknown; documentId?: unknown; filename?: unknown; storage?: unknown;
|
|
190
|
+
};
|
|
191
|
+
if (
|
|
192
|
+
typeof intent.uploadUrl !== "string" ||
|
|
193
|
+
typeof intent.path !== "string" ||
|
|
194
|
+
typeof intent.documentId !== "string" ||
|
|
195
|
+
!/^[a-f0-9]{32}$/.test(intent.documentId)
|
|
196
|
+
) {
|
|
197
|
+
throw new Error("malformed upload-intent response");
|
|
198
|
+
}
|
|
199
|
+
uploadUrl = intent.uploadUrl;
|
|
200
|
+
documentId = intent.documentId;
|
|
201
|
+
filename = typeof intent.filename === "string" && intent.filename ? intent.filename : file.name;
|
|
202
|
+
} catch (err) {
|
|
203
|
+
onUploadError?.(`"${file.name}": couldn't start the upload — ${(err as Error).message}. Try again.`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await new Promise<void>((resolvePut, rejectPut) => {
|
|
209
|
+
const xhr = new XMLHttpRequest();
|
|
210
|
+
xhr.open("PUT", uploadUrl);
|
|
211
|
+
xhr.setRequestHeader("Content-Type", file.type);
|
|
212
|
+
xhr.upload.onprogress = (e) => {
|
|
213
|
+
if (e.lengthComputable) {
|
|
214
|
+
setDocumentUploadProgress((prev) => ({ ...prev, [documentId]: e.loaded / e.total }));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
xhr.onload = () =>
|
|
218
|
+
xhr.status >= 200 && xhr.status < 300 ? resolvePut() : rejectPut(new Error(`upload ${xhr.status}`));
|
|
219
|
+
xhr.onerror = () => rejectPut(new Error("upload failed"));
|
|
220
|
+
xhr.send(file);
|
|
221
|
+
});
|
|
222
|
+
} catch (err) {
|
|
223
|
+
onUploadError?.(`"${file.name}": upload failed — ${(err as Error).message}. Try again.`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const entry: DocumentManifestEntry = {
|
|
228
|
+
filename,
|
|
229
|
+
size: file.size,
|
|
230
|
+
kind: plan.kind,
|
|
231
|
+
storage: "bucket",
|
|
232
|
+
// SITE-RELATIVE, derived from the server's id — deliberately NOT the
|
|
233
|
+
// intent's `path`, which is the tenant-prefixed object key
|
|
234
|
+
// (sites/{siteId}/…). DocumentStore prepends that prefix itself, and
|
|
235
|
+
// the GC and the delete broker both match on the site-relative form.
|
|
236
|
+
path: documentBucketPath(documentId, plan.ext),
|
|
237
|
+
};
|
|
238
|
+
// Manifest-only: the bytes are in the bucket, never in Dexie or /api/save.
|
|
239
|
+
const localUrl = URL.createObjectURL(file);
|
|
240
|
+
if (await stage(documentId, entry, null, localUrl, file.name)) onComplete?.(documentId);
|
|
241
|
+
} finally {
|
|
242
|
+
setDocumentUploadProgress((prev) => {
|
|
243
|
+
if (!documentId || !(documentId in prev)) return prev;
|
|
244
|
+
const next = { ...prev };
|
|
245
|
+
delete next[documentId];
|
|
246
|
+
return next;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
[onUploadError, stage, undeleteDocument],
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
/** Called after a save commits these ids: promote them into the manifest,
|
|
254
|
+
* drop the staged rows, release the blob URLs. */
|
|
255
|
+
const publishDocuments = useCallback(
|
|
256
|
+
async (ids: string[]) => {
|
|
257
|
+
if (ids.length === 0) return;
|
|
258
|
+
const idSet = new Set(ids);
|
|
259
|
+
const promoted = pendingRef.current.filter((d) => idSet.has(d.id));
|
|
260
|
+
if (promoted.length > 0) {
|
|
261
|
+
setDocumentManifest((prev) => {
|
|
262
|
+
const next = { ...prev };
|
|
263
|
+
for (const { id, entry } of promoted) next[id] = entry;
|
|
264
|
+
return next;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
setPendingDocuments((prev) => prev.filter((d) => !idSet.has(d.id)));
|
|
268
|
+
setPendingDocumentUrls((prev) => {
|
|
269
|
+
const next = { ...prev };
|
|
270
|
+
for (const id of ids) {
|
|
271
|
+
if (next[id]) {
|
|
272
|
+
URL.revokeObjectURL(next[id]);
|
|
273
|
+
delete next[id];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return next;
|
|
277
|
+
});
|
|
278
|
+
try {
|
|
279
|
+
await clearPendingDocumentsByIds(ids);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.error("[useDocumentUploads] Failed to clear staged documents:", err);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
[setDocumentManifest],
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
/** Drop staged-but-never-committed documents (the local-delete path): release
|
|
288
|
+
* the blob URLs and the Dexie rows. Deliberately manifest-free — nothing was
|
|
289
|
+
* ever committed, so a save has nothing to remove. A bucket-tier row's
|
|
290
|
+
* already-PUT object is left where it is; the publish-time docs GC collects
|
|
291
|
+
* whichever object stops being referenced (same acceptance as the re-pick
|
|
292
|
+
* dedup case above). */
|
|
293
|
+
const unstageDocuments = useCallback(async (ids: string[]) => {
|
|
294
|
+
if (ids.length === 0) return;
|
|
295
|
+
const idSet = new Set(ids);
|
|
296
|
+
setPendingDocuments((prev) => prev.filter((d) => !idSet.has(d.id)));
|
|
297
|
+
setPendingDocumentUrls((prev) => {
|
|
298
|
+
const next = { ...prev };
|
|
299
|
+
for (const id of ids) {
|
|
300
|
+
if (next[id]) {
|
|
301
|
+
URL.revokeObjectURL(next[id]);
|
|
302
|
+
delete next[id];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return next;
|
|
306
|
+
});
|
|
307
|
+
try {
|
|
308
|
+
await clearPendingDocumentsByIds(ids);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
console.error("[useDocumentUploads] Failed to clear staged documents:", err);
|
|
311
|
+
}
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
// Memoized: useMediaPipeline lists this object in handleMediaUpload's and
|
|
315
|
+
// enqueueFile's dependency arrays, and those two callbacks sit in
|
|
316
|
+
// contextValue's deps — a fresh literal every render would defeat all three
|
|
317
|
+
// memos and re-render every MediaLibraryContext consumer on every keystroke.
|
|
318
|
+
return useMemo(
|
|
319
|
+
() => ({
|
|
320
|
+
pendingDocuments,
|
|
321
|
+
pendingDocumentUrls,
|
|
322
|
+
documentUploadProgress,
|
|
323
|
+
uploadDocument,
|
|
324
|
+
publishDocuments,
|
|
325
|
+
unstageDocuments,
|
|
326
|
+
hydratePendingDocuments,
|
|
327
|
+
}),
|
|
328
|
+
[pendingDocuments, pendingDocumentUrls, documentUploadProgress, uploadDocument, publishDocuments, unstageDocuments, hydratePendingDocuments],
|
|
329
|
+
);
|
|
330
|
+
}
|