@glw907/cairn-cms 0.57.1 → 0.59.0
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/CHANGELOG.md +66 -0
- package/dist/components/CairnMediaLibrary.svelte +2070 -26
- package/dist/components/CairnMediaLibrary.svelte.d.ts +10 -2
- package/dist/components/admin-icons.d.ts +5 -0
- package/dist/components/admin-icons.js +5 -0
- package/dist/components/cairn-admin.css +402 -3
- package/dist/content/media-rewrite.d.ts +65 -0
- package/dist/content/media-rewrite.js +442 -0
- package/dist/log/events.d.ts +1 -1
- package/dist/media/bulk-delete-plan.d.ts +24 -0
- package/dist/media/bulk-delete-plan.js +25 -0
- package/dist/media/orphan-scan.d.ts +37 -0
- package/dist/media/orphan-scan.js +42 -0
- package/dist/media/reconcile.d.ts +3 -0
- package/dist/media/reconcile.js +3 -2
- package/dist/media/rewrite-plan.d.ts +65 -0
- package/dist/media/rewrite-plan.js +61 -0
- package/dist/sveltekit/cairn-admin.d.ts +8 -0
- package/dist/sveltekit/cairn-admin.js +15 -0
- package/dist/sveltekit/content-routes.d.ts +118 -4
- package/dist/sveltekit/content-routes.js +572 -1
- package/dist/sveltekit/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/lib/components/CairnMediaLibrary.svelte +2070 -26
- package/src/lib/components/admin-icons.ts +5 -0
- package/src/lib/content/media-rewrite.ts +555 -0
- package/src/lib/log/events.ts +6 -1
- package/src/lib/media/bulk-delete-plan.ts +54 -0
- package/src/lib/media/orphan-scan.ts +74 -0
- package/src/lib/media/reconcile.ts +3 -2
- package/src/lib/media/rewrite-plan.ts +122 -0
- package/src/lib/sveltekit/cairn-admin.ts +15 -0
- package/src/lib/sveltekit/content-routes.ts +722 -5
- package/src/lib/sveltekit/index.ts +3 -0
package/dist/media/reconcile.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { log } from '../log/index.js';
|
|
2
|
-
/** A stored media object key parses to its short hash via `media/<aa>/<shortHash>.<ext>`.
|
|
3
|
-
|
|
2
|
+
/** A stored media object key parses to its short hash via `media/<aa>/<shortHash>.<ext>`. Exported so
|
|
3
|
+
* the orphan-scan projection derives the same hash from an orphaned key without a second grammar. */
|
|
4
|
+
export const MEDIA_KEY_RE = /^media\/[0-9a-f]{2}\/([0-9a-f]{16})\.[a-z0-9]{1,5}$/;
|
|
4
5
|
/** The pure core: compare the stored R2 keys against the manifest's content-hash keys and report
|
|
5
6
|
* both orphan directions. A stored key that does not match the media-key grammar is ignored, since
|
|
6
7
|
* it is not a content-addressed media object this reconcile owns. */
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ConceptDescriptor } from '../content/types.js';
|
|
2
|
+
import type { RepoRef } from '../github/types.js';
|
|
3
|
+
import type { Manifest } from '../content/manifest.js';
|
|
4
|
+
/** One main entry the rewrite will touch: its identity, its file path, the transform's per-placement
|
|
5
|
+
* diff, and the rewritten markdown a later apply commits. `P` is the transform's placement type
|
|
6
|
+
* (a RepointPlacement for replace, an AltPlacement for fill-alt). */
|
|
7
|
+
export interface PlannedEntry<P = unknown> {
|
|
8
|
+
/** The concept id, e.g. "posts". */
|
|
9
|
+
concept: string;
|
|
10
|
+
/** The entry id (its filename stem). */
|
|
11
|
+
id: string;
|
|
12
|
+
/** The entry's repo path, `${concept.dir}/${filenameFromId(id)}`. */
|
|
13
|
+
path: string;
|
|
14
|
+
/** The transform's diff for this entry: one placement per rewritten reference. */
|
|
15
|
+
placements: P[];
|
|
16
|
+
/** The entry's markdown after the transform, byte-identical to the source apart from the rewrite. */
|
|
17
|
+
newMarkdown: string;
|
|
18
|
+
}
|
|
19
|
+
/** One open edit branch that also references the asset, with the entries on it. Report-only: an apply
|
|
20
|
+
* rewrites main, never a branch, so the screen surfaces these as a delta the editor handles by
|
|
21
|
+
* republishing the draft. */
|
|
22
|
+
export interface BranchRef {
|
|
23
|
+
/** The cairn/* branch name. */
|
|
24
|
+
branch: string;
|
|
25
|
+
/** The entries on that branch that reference the asset. */
|
|
26
|
+
entries: {
|
|
27
|
+
concept: string;
|
|
28
|
+
id: string;
|
|
29
|
+
}[];
|
|
30
|
+
}
|
|
31
|
+
/** The preview plan: the main entries to rewrite, the report-only branch delta, and the distinct
|
|
32
|
+
* count of affected main entries (the entries the transform actually changed). */
|
|
33
|
+
export interface RewritePlan<P = unknown> {
|
|
34
|
+
entries: PlannedEntry<P>[];
|
|
35
|
+
branchDelta: BranchRef[];
|
|
36
|
+
affectedCount: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Plan a media rewrite for one asset hash. Builds the cross-branch usage index in strict mode (so an
|
|
40
|
+
* unverifiable branch read rejects, failing closed), then splits the rows for `args.hash` by origin:
|
|
41
|
+
*
|
|
42
|
+
* - Published rows are the main work. Each entry's file is read in parallel and run through
|
|
43
|
+
* `args.transform`. An entry is included only when the transform reports at least one placement, so
|
|
44
|
+
* a row whose body holds the token in a non-image position (a code span, raw HTML) drops out rather
|
|
45
|
+
* than committing an unchanged file. A row whose concept is not configured, or whose file read
|
|
46
|
+
* returns null (a stale manifest row), is skipped.
|
|
47
|
+
* - Branch rows are the report-only delta, grouped by branch in first-seen order. Branch rows are
|
|
48
|
+
* never the published origin, so main never appears in the delta.
|
|
49
|
+
*
|
|
50
|
+
* `affectedCount` is the number of distinct entries in `entries` (the ones the transform changed). The
|
|
51
|
+
* planner does not read the media manifest: the transform closure already carries the new token or
|
|
52
|
+
* the default alt, so the planner needs only the entry markdown and the usage index. Pure of the
|
|
53
|
+
* editor surface and node-safe; the only IO is the usage index build and the per-entry reads.
|
|
54
|
+
*/
|
|
55
|
+
export declare function planMediaRewrite<P = unknown>(args: {
|
|
56
|
+
backend: RepoRef;
|
|
57
|
+
token: string;
|
|
58
|
+
concepts: ConceptDescriptor[];
|
|
59
|
+
contentManifest: Manifest;
|
|
60
|
+
hash: string;
|
|
61
|
+
transform: (markdown: string) => {
|
|
62
|
+
markdown: string;
|
|
63
|
+
placements: P[];
|
|
64
|
+
};
|
|
65
|
+
}): Promise<RewritePlan<P>>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { findConcept } from '../content/concepts.js';
|
|
2
|
+
import { filenameFromId } from '../content/ids.js';
|
|
3
|
+
import { readRaw } from '../github/repo.js';
|
|
4
|
+
import { buildUsageIndex } from './usage.js';
|
|
5
|
+
/**
|
|
6
|
+
* Plan a media rewrite for one asset hash. Builds the cross-branch usage index in strict mode (so an
|
|
7
|
+
* unverifiable branch read rejects, failing closed), then splits the rows for `args.hash` by origin:
|
|
8
|
+
*
|
|
9
|
+
* - Published rows are the main work. Each entry's file is read in parallel and run through
|
|
10
|
+
* `args.transform`. An entry is included only when the transform reports at least one placement, so
|
|
11
|
+
* a row whose body holds the token in a non-image position (a code span, raw HTML) drops out rather
|
|
12
|
+
* than committing an unchanged file. A row whose concept is not configured, or whose file read
|
|
13
|
+
* returns null (a stale manifest row), is skipped.
|
|
14
|
+
* - Branch rows are the report-only delta, grouped by branch in first-seen order. Branch rows are
|
|
15
|
+
* never the published origin, so main never appears in the delta.
|
|
16
|
+
*
|
|
17
|
+
* `affectedCount` is the number of distinct entries in `entries` (the ones the transform changed). The
|
|
18
|
+
* planner does not read the media manifest: the transform closure already carries the new token or
|
|
19
|
+
* the default alt, so the planner needs only the entry markdown and the usage index. Pure of the
|
|
20
|
+
* editor surface and node-safe; the only IO is the usage index build and the per-entry reads.
|
|
21
|
+
*/
|
|
22
|
+
export async function planMediaRewrite(args) {
|
|
23
|
+
// Strict so an unverifiable branch read rejects here rather than degrading to an absent reference.
|
|
24
|
+
// Do NOT wrap this: the throw is the fail-closed contract the apply relies on.
|
|
25
|
+
const index = await buildUsageIndex(args.backend, args.token, args.concepts, args.contentManifest, {
|
|
26
|
+
strict: true,
|
|
27
|
+
});
|
|
28
|
+
const rows = index.get(args.hash) ?? [];
|
|
29
|
+
// The main arm: read each referencing published entry in parallel (one round-trip latency floor,
|
|
30
|
+
// mirroring buildUsageIndex's per-branch batch), run the transform, and keep only the entries it
|
|
31
|
+
// changed. A null is a row whose concept is not configured or whose file is absent: it is skipped.
|
|
32
|
+
const published = rows.filter((row) => row.origin.kind === 'published');
|
|
33
|
+
const planned = await Promise.all(published.map(async (row) => {
|
|
34
|
+
const concept = findConcept(args.concepts, row.concept);
|
|
35
|
+
if (!concept)
|
|
36
|
+
return null;
|
|
37
|
+
const path = `${concept.dir}/${filenameFromId(row.id)}`;
|
|
38
|
+
const markdown = await readRaw(args.backend, path, args.token);
|
|
39
|
+
if (markdown === null)
|
|
40
|
+
return null;
|
|
41
|
+
const result = args.transform(markdown);
|
|
42
|
+
if (result.placements.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
return { concept: row.concept, id: row.id, path, placements: result.placements, newMarkdown: result.markdown };
|
|
45
|
+
}));
|
|
46
|
+
const entries = planned.filter((entry) => entry !== null);
|
|
47
|
+
// The branch arm: group the branch rows by branch in first-seen order, preserving the row order the
|
|
48
|
+
// index emits within each group. Branch rows are never the published origin, so main never appears.
|
|
49
|
+
const byBranch = new Map();
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
if (row.origin.kind !== 'branch')
|
|
52
|
+
continue;
|
|
53
|
+
const list = byBranch.get(row.origin.branch);
|
|
54
|
+
if (list)
|
|
55
|
+
list.push({ concept: row.concept, id: row.id });
|
|
56
|
+
else
|
|
57
|
+
byBranch.set(row.origin.branch, [{ concept: row.concept, id: row.id }]);
|
|
58
|
+
}
|
|
59
|
+
const branchDelta = [...byBranch].map(([branch, branchEntries]) => ({ branch, entries: branchEntries }));
|
|
60
|
+
return { entries, branchDelta, affectedCount: entries.length };
|
|
61
|
+
}
|
|
@@ -81,6 +81,14 @@ export declare function createCairnAdmin(runtime: CairnRuntime, deps?: CairnAdmi
|
|
|
81
81
|
delete: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
82
82
|
mediaDelete: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
83
83
|
mediaUpdate: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
84
|
+
mediaUpload: (event: AdminEvent) => Promise<import("./content-routes.js").UploadResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
85
|
+
mediaReplacePreview: (event: AdminEvent) => Promise<import("./content-routes.js").MediaReplacePreviewPlan | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
86
|
+
mediaReplace: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
87
|
+
mediaAltPreview: (event: AdminEvent) => Promise<import("./content-routes.js").MediaAltPreviewPlan | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
88
|
+
mediaAltPropagate: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
89
|
+
mediaBulkDelete: (event: AdminEvent) => Promise<import("./content-routes.js").MediaBulkDeleteResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
90
|
+
mediaOrphanScan: (event: AdminEvent) => Promise<import("../media/orphan-scan.js").OrphanScan | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
91
|
+
mediaPurge: (event: AdminEvent) => Promise<import("./content-routes.js").MediaOrphanPurgeResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
84
92
|
publishAll: (event: AdminEvent) => Promise<never>;
|
|
85
93
|
addEditor: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<{
|
|
86
94
|
error: string;
|
|
@@ -125,6 +125,21 @@ export function createCairnAdmin(runtime, deps = {}) {
|
|
|
125
125
|
: content.listDeleteAction(contentEvent(event, { concept: view.concept.id }))),
|
|
126
126
|
mediaDelete: viewAction(['media'], (event) => content.mediaDeleteAction(contentEvent(event, {}))),
|
|
127
127
|
mediaUpdate: viewAction(['media'], (event) => content.mediaUpdateAction(contentEvent(event, {}))),
|
|
128
|
+
// The Library is not entry-scoped, so a replace uploads its new file through the same content-
|
|
129
|
+
// addressed ingest mounted media-scoped (uploadAction reads no concept/id), then previews and
|
|
130
|
+
// applies the repoint. Alt propagation previews and applies the alt fill. The preview pair are 2a
|
|
131
|
+
// fetch actions; the apply pair are form posts. All gate on the media view.
|
|
132
|
+
mediaUpload: viewAction(['media'], (event) => content.uploadAction(contentEvent(event, {}))),
|
|
133
|
+
mediaReplacePreview: viewAction(['media'], (event) => content.mediaReplacePreview(contentEvent(event, {}))),
|
|
134
|
+
mediaReplace: viewAction(['media'], (event) => content.mediaReplaceApply(contentEvent(event, {}))),
|
|
135
|
+
mediaAltPreview: viewAction(['media'], (event) => content.mediaAltPreview(contentEvent(event, {}))),
|
|
136
|
+
mediaAltPropagate: viewAction(['media'], (event) => content.mediaAltApply(contentEvent(event, {}))),
|
|
137
|
+
// Pass C library actions: a multi-select bulk delete, the on-demand orphan scan, and the
|
|
138
|
+
// irreversible byte purge. The component posts to `?/mediaBulkDelete`, `?/mediaOrphanScan`, and
|
|
139
|
+
// `?/mediaPurge` (the purge key is short of its content method name). All gate on the media view.
|
|
140
|
+
mediaBulkDelete: viewAction(['media'], (event) => content.mediaBulkDelete(contentEvent(event, {}))),
|
|
141
|
+
mediaOrphanScan: viewAction(['media'], (event) => content.mediaOrphanScan(contentEvent(event, {}))),
|
|
142
|
+
mediaPurge: viewAction(['media'], (event) => content.mediaPurgeOrphans(contentEvent(event, {}))),
|
|
128
143
|
publishAll: viewAction(authedViews, (event) => content.publishAllAction(contentEvent(event, {}))),
|
|
129
144
|
addEditor: viewAction(['editors'], (event) => editors.addEditorAction(event)),
|
|
130
145
|
removeEditor: viewAction(['editors'], (event) => editors.removeEditorAction(event)),
|
|
@@ -4,6 +4,10 @@ import { type LinkTarget, type InboundLink } from '../content/manifest.js';
|
|
|
4
4
|
import type { MediaEntry } from '../media/manifest.js';
|
|
5
5
|
import type { MediaLibrary, MediaLibraryEntry } from '../media/library-entry.js';
|
|
6
6
|
import type { UsageEntry } from '../media/usage.js';
|
|
7
|
+
import { type OrphanScan } from '../media/orphan-scan.js';
|
|
8
|
+
import type { RepointPlacement, AltPlacement } from '../content/media-rewrite.js';
|
|
9
|
+
import type { BranchRef } from '../media/rewrite-plan.js';
|
|
10
|
+
import type { BulkDeleteSkip } from '../media/bulk-delete-plan.js';
|
|
7
11
|
import type { CookieJar, EventBase } from './types.js';
|
|
8
12
|
import type { CairnRuntime, FrontmatterField, ResolvedPreview } from '../content/types.js';
|
|
9
13
|
import type { Role } from '../auth/types.js';
|
|
@@ -133,8 +137,10 @@ export interface MediaLibraryData {
|
|
|
133
137
|
* redirected commit conflict never overwrite each other. */
|
|
134
138
|
error: string | null;
|
|
135
139
|
/** The success flash a redirected action carries: `deleted` from `?deleted=1`, `updated` from
|
|
136
|
-
* `?updated=1`,
|
|
137
|
-
|
|
140
|
+
* `?updated=1`, `replaced` from `?replaced=1`, `altPropagated` from `?altPropagated=1`,
|
|
141
|
+
* `bulkDeleted` from `?bulkDeleted=1`, `orphansPurged` from `?orphansPurged=1`, null otherwise.
|
|
142
|
+
* The component renders a polite success strip for each. */
|
|
143
|
+
flash: 'deleted' | 'updated' | 'replaced' | 'altPropagated' | 'bulkDeleted' | 'orphansPurged' | null;
|
|
138
144
|
/** A redirected action's conflict error read from `?error=` (a commit-conflict bounce). Kept in
|
|
139
145
|
* its own slot rather than the degraded-load `error` above, so the two never collide. */
|
|
140
146
|
flashError: string | null;
|
|
@@ -194,11 +200,112 @@ export interface MediaUpdateFailure {
|
|
|
194
200
|
/** The one-line human summary every action failure carries. */
|
|
195
201
|
error: string;
|
|
196
202
|
}
|
|
203
|
+
/** A refused media replace: `fail(409)` when a fresh usage read finds the asset still in use and the
|
|
204
|
+
* typed-slug override was not given, or `fail(503)` when usage cannot be verified (fail closed) or the
|
|
205
|
+
* bucket is unbound. Mirrors MediaDeleteRefusal: the asset hash, the where-used rows, and the count. */
|
|
206
|
+
export interface MediaReplaceFailure {
|
|
207
|
+
error: string;
|
|
208
|
+
hash: string;
|
|
209
|
+
usage: UsageEntry[];
|
|
210
|
+
foundIn: number;
|
|
211
|
+
}
|
|
212
|
+
/** A refused media alt-propagation: `fail(503)` when usage cannot be verified across main and every
|
|
213
|
+
* open branch (fail closed), or the bucket is unbound. Just the one-line summary; alt fill has no
|
|
214
|
+
* typed-slug gate. */
|
|
215
|
+
export interface MediaAltPropagateFailure {
|
|
216
|
+
error: string;
|
|
217
|
+
}
|
|
218
|
+
/** A refused media bulk delete or orphan purge: `fail(503)` for the fail-closed strict-usage refusal
|
|
219
|
+
* (the whole batch refuses) or media-off / a missing bucket binding. The per-item outcomes ride the
|
|
220
|
+
* returned summary, not a fail. */
|
|
221
|
+
export interface MediaBulkFailure {
|
|
222
|
+
error: string;
|
|
223
|
+
}
|
|
224
|
+
/** The bulk-delete outcome the component renders: the deleted hashes, the skipped rows from the
|
|
225
|
+
* partition (with their reason and where-used), and any per-object R2 delete failure. Admin-internal,
|
|
226
|
+
* not on the package subpath, so no reference page. */
|
|
227
|
+
export interface MediaBulkDeleteResult {
|
|
228
|
+
deleted: string[];
|
|
229
|
+
skipped: BulkDeleteSkip[];
|
|
230
|
+
failed: {
|
|
231
|
+
hash: string;
|
|
232
|
+
error: string;
|
|
233
|
+
}[];
|
|
234
|
+
}
|
|
235
|
+
/** The orphan-purge outcome: the purged R2 keys, the keys skipped because their hash was claimed by a
|
|
236
|
+
* manifest row since the scan, and any per-object delete failure. Admin-internal, no reference page. */
|
|
237
|
+
export interface MediaOrphanPurgeResult {
|
|
238
|
+
purged: string[];
|
|
239
|
+
skippedClaimed: string[];
|
|
240
|
+
failed: {
|
|
241
|
+
key: string;
|
|
242
|
+
error: string;
|
|
243
|
+
}[];
|
|
244
|
+
}
|
|
245
|
+
/** One entry the replace preview will rewrite, enriched with its display title and permalink from the
|
|
246
|
+
* content manifest (the planner's PlannedEntry carries neither). The screen lists these as the
|
|
247
|
+
* confirm dialog's where-touched preview, and the apply re-derives its own plan rather than trusting
|
|
248
|
+
* this. Admin-internal: exported from content-routes for the bundled Media Library component, not
|
|
249
|
+
* added to the package's sveltekit subpath, so it carries no reference page. */
|
|
250
|
+
export interface MediaReplacePreviewEntry {
|
|
251
|
+
/** The concept id, e.g. "posts". */
|
|
252
|
+
concept: string;
|
|
253
|
+
/** The entry id (its filename stem). */
|
|
254
|
+
id: string;
|
|
255
|
+
/** The entry's display title, from the content manifest. */
|
|
256
|
+
title: string;
|
|
257
|
+
/** The entry's public permalink, from the content manifest. */
|
|
258
|
+
permalink?: string;
|
|
259
|
+
/** The per-reference diff for this entry: one placement per repointed `media:` token. */
|
|
260
|
+
placements: RepointPlacement[];
|
|
261
|
+
}
|
|
262
|
+
/** The replace preview plan: the affected main entries (enriched), the distinct affected count, and
|
|
263
|
+
* the report-only cross-branch delta (open cairn/* branches that reference the same bytes; an apply
|
|
264
|
+
* rewrites main only). Display-only: the apply re-derives a fresh plan and never trusts this. */
|
|
265
|
+
export interface MediaReplacePreviewPlan {
|
|
266
|
+
affectedCount: number;
|
|
267
|
+
entries: MediaReplacePreviewEntry[];
|
|
268
|
+
branchDelta: BranchRef[];
|
|
269
|
+
}
|
|
270
|
+
/** One entry the alt-propagation preview reports, enriched with its display title and permalink from
|
|
271
|
+
* the content manifest. Its placements carry every reference of the asset on this entry, each tagged
|
|
272
|
+
* with the bucket it falls in (a will-fill, a customized alt left as-is, or a decorative hero), so
|
|
273
|
+
* the screen can show what would change. Admin-internal: exported from content-routes for the bundled
|
|
274
|
+
* Media Library component, not added to the package's sveltekit subpath, so it carries no reference
|
|
275
|
+
* page. */
|
|
276
|
+
export interface MediaAltPreviewEntry {
|
|
277
|
+
/** The concept id, e.g. "posts". */
|
|
278
|
+
concept: string;
|
|
279
|
+
/** The entry id (its filename stem). */
|
|
280
|
+
id: string;
|
|
281
|
+
/** The entry's display title, from the content manifest. */
|
|
282
|
+
title: string;
|
|
283
|
+
/** The entry's public permalink, from the content manifest. */
|
|
284
|
+
permalink?: string;
|
|
285
|
+
/** The per-reference diff for this entry: one placement per reference of the asset. */
|
|
286
|
+
placements: AltPlacement[];
|
|
287
|
+
}
|
|
288
|
+
/** The alt-propagation preview plan: every entry that references the asset (enriched), the report-only
|
|
289
|
+
* cross-branch delta, and the bucket counts aggregated across every placement. Display-only: the
|
|
290
|
+
* apply re-derives a fresh plan and never trusts this. The preview reports an entry even when its
|
|
291
|
+
* only placements are reported-but-unchanged (a kept custom alt, a decorative hero), so the screen
|
|
292
|
+
* can show every bucket; the apply commits only the entries it actually changes. */
|
|
293
|
+
export interface MediaAltPreviewPlan {
|
|
294
|
+
entries: MediaAltPreviewEntry[];
|
|
295
|
+
branchDelta: BranchRef[];
|
|
296
|
+
/** The placement counts by bucket, summed across all entries. */
|
|
297
|
+
counts: {
|
|
298
|
+
willFill: number;
|
|
299
|
+
customized: number;
|
|
300
|
+
decorativeSkipped: number;
|
|
301
|
+
};
|
|
302
|
+
}
|
|
197
303
|
/** What a route's single `form` export presents to a view component: whichever content action
|
|
198
304
|
* last failed, merged with every field optional. `error` is always set on a failure; the richer
|
|
199
305
|
* keys identify which guard refused. The media refusals ride here too, so the Media Library's one
|
|
200
|
-
* `form` prop carries a `?/mediaDelete
|
|
201
|
-
|
|
306
|
+
* `form` prop carries a `?/mediaDelete`, `?/mediaUpdate`, `?/mediaReplace`, or `?/mediaAltPropagate`
|
|
307
|
+
* refusal without a second type. */
|
|
308
|
+
export type ContentFormFailure = Partial<SaveFailure & DeleteRefusal & RenameFailure & MediaDeleteRefusal & MediaUpdateFailure & MediaReplaceFailure & MediaAltPropagateFailure & MediaBulkFailure>;
|
|
202
309
|
/** The successful upload's response (`uploadAction`). The server-owned `record` rides the editor's
|
|
203
310
|
* optimistic client state and commits with the entry at Save (the upload itself commits nothing).
|
|
204
311
|
* `reused` is true when identical bytes were already stored, so the second upload did no second put;
|
|
@@ -225,6 +332,13 @@ export declare function createContentRoutes(runtime: CairnRuntime, deps?: Conten
|
|
|
225
332
|
renameAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
226
333
|
uploadAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | UploadResult>;
|
|
227
334
|
mediaDeleteAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
335
|
+
mediaBulkDelete: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaBulkDeleteResult>;
|
|
336
|
+
mediaOrphanScan: (event: ContentEvent) => Promise<ReturnType<typeof fail> | OrphanScan>;
|
|
337
|
+
mediaPurgeOrphans: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaOrphanPurgeResult>;
|
|
228
338
|
mediaUpdateAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
339
|
+
mediaReplacePreview: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaReplacePreviewPlan>;
|
|
340
|
+
mediaReplaceApply: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
341
|
+
mediaAltPreview: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaAltPreviewPlan>;
|
|
342
|
+
mediaAltApply: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
229
343
|
mintToken: (env: GithubKeyEnv) => string | Promise<string>;
|
|
230
344
|
};
|