@elytracms/studio 0.0.86 → 0.0.87
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/convex/asset-revalidate.test.ts +186 -0
- package/convex/assets.ts +53 -19
- package/convex/cms.ts +22 -0
- package/convex/delivery.ts +33 -116
- package/convex/functions.ts +3 -3
- package/convex/rail-write-parity.test.ts +180 -0
- package/convex/redirects.ts +33 -11
- package/convex/revalidate.ts +7 -5
- package/convex/triggers.ts +53 -3
- package/dist/components/app-views/default-modules.d.ts +20 -20
- package/dist/components/app-views/default-modules.js +27 -48
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +5 -6
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +28 -19
- package/dist/components/assets/asset-metadata.js +24 -31
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +24 -5
- package/dist/components/assets/asset-selection.js +39 -9
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +17 -14
- package/dist/components/assets/assets-collection-view-source.js +25 -31
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +3 -3
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +25 -1
- package/dist/components/builder/builder-store.js +3 -2
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +0 -17
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +10 -12
- package/dist/components/builder/canvas-chrome.js +12 -13
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +8 -0
- package/dist/components/builder/canvas-dnd.js +5 -3
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +12 -24
- package/dist/components/builder/canvas-render-context.js +15 -44
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas.js +5 -11
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +14 -6
- package/dist/components/builder/clipboard-actions.js +29 -20
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +19 -26
- package/dist/components/builder/composition-field-editor.js +30 -52
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +3 -1
- package/dist/components/builder/insert-context.js +15 -10
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +29 -0
- package/dist/components/builder/page-tree.js +56 -58
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/reuse.js +5 -2
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.js +20 -32
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/cms-composition-validators.js +11 -2
- package/dist/components/cms/cms-composition-validators.js.map +1 -1
- package/dist/components/cms/editable-cell.js +5 -2
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/use-document-versions.js +1 -0
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/versions-model.d.ts +0 -12
- package/dist/components/cms/versions-model.js +3 -15
- package/dist/components/cms/versions-model.js.map +1 -1
- package/dist/components/components/component-detail.js +1 -1
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +2 -4
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +13 -10
- package/dist/components/components/transfer.js +21 -19
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/project-session.d.ts +9 -0
- package/dist/components/project-session.js +1 -1
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +17 -2
- package/dist/components/redirects/redirects-collection-view-source.js +20 -6
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/section-surface.js +3 -2
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/settings-form.js +1 -2
- package/dist/components/settings-form.js.map +1 -1
- package/dist/components/studio-context.js +1 -18
- package/dist/components/studio-context.js.map +1 -1
- package/dist/components/studio-project-screen.js +3 -5
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/lib/ai/apply-content.js +73 -29
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +35 -0
- package/dist/lib/ai/constraints.js +104 -4
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/route.js +4 -1
- package/dist/lib/ai/route.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +3 -2
- package/dist/lib/ai/shape.js +16 -24
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/types.d.ts +8 -0
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +33 -12
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/assets/asset-placements.d.ts +35 -0
- package/dist/lib/assets/asset-placements.js +74 -0
- package/dist/lib/assets/asset-placements.js.map +1 -0
- package/dist/lib/assets/backfill-thumbs.d.ts +9 -8
- package/dist/lib/assets/backfill-thumbs.js +13 -25
- package/dist/lib/assets/backfill-thumbs.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +17 -15
- package/dist/lib/clipboard/paste-issues.js +8 -45
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +25 -1
- package/dist/lib/clipboard/paste-target.js +13 -6
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/published-documents.js +1 -0
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/data/index.d.ts +0 -3
- package/dist/lib/data/index.js +4 -6
- package/dist/lib/data/index.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +45 -2
- package/dist/lib/dnd/drop-rules.js +64 -9
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +4 -56
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +4 -53
- package/dist/lib/i18n/messages.en.js +8 -57
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/locale/fallback.js +8 -2
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/locale/index.d.ts +0 -1
- package/dist/lib/locale/index.js +0 -1
- package/dist/lib/locale/index.js.map +1 -1
- package/dist/lib/locale/locale-store.d.ts +10 -3
- package/dist/lib/locale/locale-store.js +12 -5
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +9 -5
- package/dist/lib/operations/session-hydration.js +10 -10
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +63 -75
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +0 -2
- package/dist/lib/persona/labels.js +1 -7
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/registry/index.d.ts +4 -2
- package/dist/lib/registry/index.js +26 -21
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/project-components.js +0 -3
- package/dist/lib/registry/project-components.js.map +1 -1
- package/dist/lib/routes/route-issues.js +4 -5
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/theme.js +3 -2
- package/dist/lib/theme.js.map +1 -1
- package/dist/lib/webhooks/tags.js +11 -14
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/previews/enumerate.d.ts +27 -9
- package/dist/previews/enumerate.js +27 -6
- package/dist/previews/enumerate.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +9 -0
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/match.d.ts +0 -2
- package/dist/router/match.js +0 -2
- package/dist/router/match.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/package.json +2 -2
- package/src/styles/studio.css +5 -4
- package/src/styles/tokens.css +72 -55
- package/convex/content.ts +0 -291
- package/dist/components/collab/collab-context.d.ts +0 -27
- package/dist/components/collab/collab-context.js +0 -39
- package/dist/components/collab/collab-context.js.map +0 -1
- package/dist/components/collab/collab-seed.d.ts +0 -15
- package/dist/components/collab/collab-seed.js +0 -20
- package/dist/components/collab/collab-seed.js.map +0 -1
- package/dist/components/extensions/extensions-view.d.ts +0 -7
- package/dist/components/extensions/extensions-view.js +0 -127
- package/dist/components/extensions/extensions-view.js.map +0 -1
- package/dist/lib/bindings/section-bindings.d.ts +0 -24
- package/dist/lib/bindings/section-bindings.js +0 -9
- package/dist/lib/bindings/section-bindings.js.map +0 -1
- package/dist/lib/data/data-store.d.ts +0 -98
- package/dist/lib/data/data-store.js +0 -211
- package/dist/lib/data/data-store.js.map +0 -1
- package/dist/lib/data/providers.d.ts +0 -143
- package/dist/lib/data/providers.js +0 -255
- package/dist/lib/data/providers.js.map +0 -1
- package/dist/lib/data/sample-capture.d.ts +0 -30
- package/dist/lib/data/sample-capture.js +0 -70
- package/dist/lib/data/sample-capture.js.map +0 -1
- package/dist/lib/data/sources-store.d.ts +0 -42
- package/dist/lib/data/sources-store.js +0 -120
- package/dist/lib/data/sources-store.js.map +0 -1
- package/dist/lib/locale/binding-fallback.d.ts +0 -35
- package/dist/lib/locale/binding-fallback.js +0 -73
- package/dist/lib/locale/binding-fallback.js.map +0 -1
- package/dist/lib/starters/kit-session.d.ts +0 -5
- package/dist/lib/starters/kit-session.js +0 -17
- package/dist/lib/starters/kit-session.js.map +0 -1
|
@@ -14,7 +14,7 @@ import { generateVariants, planLqip, planThumb } from './thumbnail.js';
|
|
|
14
14
|
* The write goes through the injected {@link AssetRepository} — in the studio the
|
|
15
15
|
* EC-141 operations facade — so the patch flows through the one operations layer /
|
|
16
16
|
* TanStack Query cache (AD-5 / AD-17), never a direct backend client. The pure
|
|
17
|
-
* cores ({@link assetsNeedingVariants}, {@link
|
|
17
|
+
* cores ({@link assetsNeedingVariants}, {@link assetVariantWrite}) are
|
|
18
18
|
* node-tested; the byte fetch + canvas encode stay in the injectable shell.
|
|
19
19
|
*/
|
|
20
20
|
/** An image record still missing a thumb whose {@link planThumb} decision is `encode`. */
|
|
@@ -39,31 +39,19 @@ export function variantWantFor(asset) {
|
|
|
39
39
|
return { thumb: needsThumb(asset), lqip: needsLqip(asset) };
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* The
|
|
43
|
-
* variants
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
42
|
+
* The asset write for a backfill result (EC-400 AD-4 / EC-420, EC-577): ONLY the
|
|
43
|
+
* variants just generated. Every other column is absent, so it keeps its stored
|
|
44
|
+
* value — including a variant the record already had (the other half of a
|
|
45
|
+
* per-variant fill) and any metadata an editor saved while the run was going. The
|
|
46
|
+
* restate this replaced wrote the whole listed record back, which overwrote such
|
|
47
|
+
* a concurrent edit with the stale listing — and re-created an asset deleted
|
|
48
|
+
* mid-run; now a write for a vanished asset is refused and counted `failed`.
|
|
48
49
|
*/
|
|
49
|
-
export function
|
|
50
|
-
const thumb = variants.thumb ?? current.thumb;
|
|
51
|
-
const lqip = variants.lqip ?? current.lqip;
|
|
50
|
+
export function assetVariantWrite(assetId, variants) {
|
|
52
51
|
return {
|
|
53
|
-
id:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
byteSize: current.byteSize,
|
|
57
|
-
storageKey: current.storageKey,
|
|
58
|
-
...(current.width !== undefined ? { width: current.width } : {}),
|
|
59
|
-
...(current.height !== undefined ? { height: current.height } : {}),
|
|
60
|
-
...(current.url !== undefined ? { url: current.url } : {}),
|
|
61
|
-
...(current.alt !== undefined ? { alt: current.alt } : {}),
|
|
62
|
-
...(current.title !== undefined ? { title: current.title } : {}),
|
|
63
|
-
...(current.focalPoint !== undefined ? { focalPoint: current.focalPoint } : {}),
|
|
64
|
-
...(current.meta !== undefined ? { meta: current.meta } : {}),
|
|
65
|
-
...(thumb !== undefined ? { thumb } : {}),
|
|
66
|
-
...(lqip !== undefined ? { lqip } : {}),
|
|
52
|
+
id: assetId,
|
|
53
|
+
...(variants.thumb !== undefined ? { thumb: variants.thumb } : {}),
|
|
54
|
+
...(variants.lqip !== undefined ? { lqip: variants.lqip } : {}),
|
|
67
55
|
};
|
|
68
56
|
}
|
|
69
57
|
/**
|
|
@@ -102,7 +90,7 @@ export async function runVariantBackfill(deps, projectId) {
|
|
|
102
90
|
progress.skipped += 1;
|
|
103
91
|
}
|
|
104
92
|
else {
|
|
105
|
-
const patched = await deps.assets.upsertAsset(projectId,
|
|
93
|
+
const patched = await deps.assets.upsertAsset(projectId, assetVariantWrite(asset.id, variants));
|
|
106
94
|
deps.onGenerated?.(patched);
|
|
107
95
|
progress.generated += 1;
|
|
108
96
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backfill-thumbs.js","sourceRoot":"","sources":["../../../src/lib/assets/backfill-thumbs.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AssetRecord,\n AssetRepository,\n BlobStorageAdapter,\n NewAssetInput,\n} from '@elytracms/core/persistence'\nimport { generateVariants, planLqip, planThumb } from './thumbnail'\nimport type { GeneratedVariants, ThumbSource, VariantWant } from './thumbnail'\n\n/**\n * The assets-rail \"Generate previews\" backfill (EC-400 AD-1 + EC-420 AD-4).\n * Existing libraries predate the upload-time previews, so an explicit,\n * deterministic maintenance action fetches each original ONCE, runs the SAME\n * decode path as upload, and patches the record. It is idempotent PER VARIANT\n * (EC-420 AD-4): an asset needing only the lqip gets one fetch of the original\n * and fills just what's missing; an asset that already has both is skipped;\n * non-images / SVGs / (for the thumb) already-small images never qualify. The run\n * is sequential with visible progress and cancellable. Opportunistic per-open\n * generation was REJECTED (it hides egress in random editor sessions and is\n * untestable).\n *\n * The write goes through the injected {@link AssetRepository} — in the studio the\n * EC-141 operations facade — so the patch flows through the one operations layer /\n * TanStack Query cache (AD-5 / AD-17), never a direct backend client. The pure\n * cores ({@link assetsNeedingVariants}, {@link assetVariantUpsertInput}) are\n * node-tested; the byte fetch + canvas encode stay in the injectable shell.\n */\n\n/** An image record still missing a thumb whose {@link planThumb} decision is `encode`. */\nfunction needsThumb(asset: AssetRecord): boolean {\n return asset.thumb === undefined && planThumb(asset).kind === 'encode'\n}\n\n/** An image record still missing an lqip whose {@link planLqip} decision is `encode`. */\nfunction needsLqip(asset: AssetRecord): boolean {\n return asset.lqip === undefined && planLqip(asset).kind === 'encode'\n}\n\n/**\n * The assets that still need at least one variant (pure, node-tested idempotence\n * core, EC-420 AD-4): an image record missing a needed thumb OR a needed lqip.\n * Records already carrying both, non-images, SVGs, and (for the thumb) already-\n * small images are silently excluded — re-running on a done library is a no-op.\n */\nexport function assetsNeedingVariants(assets: readonly AssetRecord[]): AssetRecord[] {\n return assets.filter((asset) => needsThumb(asset) || needsLqip(asset))\n}\n\n/** The variants an asset is missing — what the per-variant backfill asks the encoder for. */\nexport function variantWantFor(asset: AssetRecord): VariantWant {\n return { thumb: needsThumb(asset), lqip: needsLqip(asset) }\n}\n\n/**\n * The full upsert input that re-states an existing record with freshly generated\n * variants attached — the targeted patch path for the backfill, within the\n * existing repository/`assets.upsert` contract (EC-400 AD-4 / EC-420). Mirrors\n * `assetMetadataUpsertInput`: every current column is restated so the full-replace\n * upsert preserves it. A newly generated variant overlays; a variant the record\n * ALREADY had (the other half of a per-variant fill) is preserved from `current`.\n */\nexport function assetVariantUpsertInput(\n current: AssetRecord,\n variants: GeneratedVariants,\n): NewAssetInput {\n const thumb = variants.thumb ?? current.thumb\n const lqip = variants.lqip ?? current.lqip\n return {\n id: current.id,\n filename: current.filename,\n contentType: current.contentType,\n byteSize: current.byteSize,\n storageKey: current.storageKey,\n ...(current.width !== undefined ? { width: current.width } : {}),\n ...(current.height !== undefined ? { height: current.height } : {}),\n ...(current.url !== undefined ? { url: current.url } : {}),\n ...(current.alt !== undefined ? { alt: current.alt } : {}),\n ...(current.title !== undefined ? { title: current.title } : {}),\n ...(current.focalPoint !== undefined ? { focalPoint: current.focalPoint } : {}),\n ...(current.meta !== undefined ? { meta: current.meta } : {}),\n ...(thumb !== undefined ? { thumb } : {}),\n ...(lqip !== undefined ? { lqip } : {}),\n }\n}\n\n/** Live tally the \"Generate previews\" UI renders (EC-400 AD-1). */\nexport interface BackfillProgress {\n /** How many assets need at least one variant (the candidate set size). */\n total: number\n /** How many candidates have been processed (generated + skipped + failed). */\n done: number\n /** How many candidates had at least one variant generated and patched in. */\n generated: number\n /** Candidates the re-plan/encode declined (e.g. undecodable) — silent, valid. */\n skipped: number\n /** Candidates whose fetch/encode/patch errored — surfaced, not fatal. */\n failed: number\n /** Filename of the asset currently in flight (for the progress label). */\n current?: string\n /** Set once the run stops early because the caller cancelled. */\n cancelled?: boolean\n}\n\n/** A mutable cancellation flag the UI flips to stop the sequential run (AD-1). */\nexport interface BackfillSignal {\n cancelled: boolean\n}\n\nexport interface BackfillDeps {\n /** Metadata reads + the variant patch write (the operations facade in the studio). */\n assets: AssetRepository\n /** Blob boundary: fetch each original's bytes, store the thumb variant. */\n blobs: BlobStorageAdapter\n /**\n * Injectable variant producer; defaults to {@link generateVariants} over `blobs`.\n * Receives the per-asset `want` so it produces ONLY the missing variants. Tests\n * pass a deterministic fake so the iterator runs in node.\n */\n generate?: (source: Blob, meta: ThumbSource, want: VariantWant) => Promise<GeneratedVariants>\n /** Fired with each patched record so the caller can update its cache in place. */\n onGenerated?: (asset: AssetRecord) => void\n /** Fired after each candidate (and once at the end) to drive the progress UI. */\n onProgress?: (progress: BackfillProgress) => void\n /** Cooperative cancellation — checked before each candidate (AD-1). */\n signal?: BackfillSignal\n}\n\n/**\n * Run the backfill sequentially over a project's images that still need a variant.\n * Resolves with the final tally; never throws (per-asset failures are counted, not\n * fatal). One fetch per candidate serves both variants; only the missing ones are\n * (re)generated (EC-420 AD-4), so an asset with a thumb but no lqip is patched with\n * just the lqip and its existing thumb blob is left untouched.\n */\nexport async function runVariantBackfill(\n deps: BackfillDeps,\n projectId: string,\n): Promise<BackfillProgress> {\n const all = await deps.assets.listAssets(projectId)\n const candidates = assetsNeedingVariants(all)\n const generate =\n deps.generate ??\n ((source: Blob, meta: ThumbSource, want: VariantWant) =>\n generateVariants({ blobs: deps.blobs }, source, meta, want))\n\n const progress: BackfillProgress = { total: candidates.length, done: 0, generated: 0, skipped: 0, failed: 0 }\n deps.onProgress?.({ ...progress })\n\n for (const asset of candidates) {\n if (deps.signal?.cancelled) {\n progress.cancelled = true\n break\n }\n deps.onProgress?.({ ...progress, current: asset.filename })\n try {\n const bytes = await deps.blobs.get(asset.storageKey)\n if (!bytes) {\n progress.failed += 1\n } else {\n const source = new Blob([bytes as BlobPart], { type: asset.contentType })\n const variants = await generate(\n source,\n {\n contentType: asset.contentType,\n ...(asset.width !== undefined ? { width: asset.width } : {}),\n ...(asset.height !== undefined ? { height: asset.height } : {}),\n },\n variantWantFor(asset),\n )\n if (variants.thumb === undefined && variants.lqip === undefined) {\n progress.skipped += 1\n } else {\n const patched = await deps.assets.upsertAsset(projectId, assetVariantUpsertInput(asset, variants))\n deps.onGenerated?.(patched)\n progress.generated += 1\n }\n }\n } catch {\n progress.failed += 1\n }\n progress.done += 1\n deps.onProgress?.({ ...progress })\n }\n\n deps.onProgress?.({ ...progress })\n return progress\n}\n"]}
|
|
1
|
+
{"version":3,"file":"backfill-thumbs.js","sourceRoot":"","sources":["../../../src/lib/assets/backfill-thumbs.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AssetRecord,\n AssetRepository,\n AssetWriteInput,\n BlobStorageAdapter,\n} from '@elytracms/core/persistence'\nimport { generateVariants, planLqip, planThumb } from './thumbnail'\nimport type { GeneratedVariants, ThumbSource, VariantWant } from './thumbnail'\n\n/**\n * The assets-rail \"Generate previews\" backfill (EC-400 AD-1 + EC-420 AD-4).\n * Existing libraries predate the upload-time previews, so an explicit,\n * deterministic maintenance action fetches each original ONCE, runs the SAME\n * decode path as upload, and patches the record. It is idempotent PER VARIANT\n * (EC-420 AD-4): an asset needing only the lqip gets one fetch of the original\n * and fills just what's missing; an asset that already has both is skipped;\n * non-images / SVGs / (for the thumb) already-small images never qualify. The run\n * is sequential with visible progress and cancellable. Opportunistic per-open\n * generation was REJECTED (it hides egress in random editor sessions and is\n * untestable).\n *\n * The write goes through the injected {@link AssetRepository} — in the studio the\n * EC-141 operations facade — so the patch flows through the one operations layer /\n * TanStack Query cache (AD-5 / AD-17), never a direct backend client. The pure\n * cores ({@link assetsNeedingVariants}, {@link assetVariantWrite}) are\n * node-tested; the byte fetch + canvas encode stay in the injectable shell.\n */\n\n/** An image record still missing a thumb whose {@link planThumb} decision is `encode`. */\nfunction needsThumb(asset: AssetRecord): boolean {\n return asset.thumb === undefined && planThumb(asset).kind === 'encode'\n}\n\n/** An image record still missing an lqip whose {@link planLqip} decision is `encode`. */\nfunction needsLqip(asset: AssetRecord): boolean {\n return asset.lqip === undefined && planLqip(asset).kind === 'encode'\n}\n\n/**\n * The assets that still need at least one variant (pure, node-tested idempotence\n * core, EC-420 AD-4): an image record missing a needed thumb OR a needed lqip.\n * Records already carrying both, non-images, SVGs, and (for the thumb) already-\n * small images are silently excluded — re-running on a done library is a no-op.\n */\nexport function assetsNeedingVariants(assets: readonly AssetRecord[]): AssetRecord[] {\n return assets.filter((asset) => needsThumb(asset) || needsLqip(asset))\n}\n\n/** The variants an asset is missing — what the per-variant backfill asks the encoder for. */\nexport function variantWantFor(asset: AssetRecord): VariantWant {\n return { thumb: needsThumb(asset), lqip: needsLqip(asset) }\n}\n\n/**\n * The asset write for a backfill result (EC-400 AD-4 / EC-420, EC-577): ONLY the\n * variants just generated. Every other column is absent, so it keeps its stored\n * value — including a variant the record already had (the other half of a\n * per-variant fill) and any metadata an editor saved while the run was going. The\n * restate this replaced wrote the whole listed record back, which overwrote such\n * a concurrent edit with the stale listing — and re-created an asset deleted\n * mid-run; now a write for a vanished asset is refused and counted `failed`.\n */\nexport function assetVariantWrite(assetId: string, variants: GeneratedVariants): AssetWriteInput {\n return {\n id: assetId,\n ...(variants.thumb !== undefined ? { thumb: variants.thumb } : {}),\n ...(variants.lqip !== undefined ? { lqip: variants.lqip } : {}),\n }\n}\n\n/** Live tally the \"Generate previews\" UI renders (EC-400 AD-1). */\nexport interface BackfillProgress {\n /** How many assets need at least one variant (the candidate set size). */\n total: number\n /** How many candidates have been processed (generated + skipped + failed). */\n done: number\n /** How many candidates had at least one variant generated and patched in. */\n generated: number\n /** Candidates the re-plan/encode declined (e.g. undecodable) — silent, valid. */\n skipped: number\n /** Candidates whose fetch/encode/patch errored — surfaced, not fatal. */\n failed: number\n /** Filename of the asset currently in flight (for the progress label). */\n current?: string\n /** Set once the run stops early because the caller cancelled. */\n cancelled?: boolean\n}\n\n/** A mutable cancellation flag the UI flips to stop the sequential run (AD-1). */\nexport interface BackfillSignal {\n cancelled: boolean\n}\n\nexport interface BackfillDeps {\n /** Metadata reads + the variant patch write (the operations facade in the studio). */\n assets: AssetRepository\n /** Blob boundary: fetch each original's bytes, store the thumb variant. */\n blobs: BlobStorageAdapter\n /**\n * Injectable variant producer; defaults to {@link generateVariants} over `blobs`.\n * Receives the per-asset `want` so it produces ONLY the missing variants. Tests\n * pass a deterministic fake so the iterator runs in node.\n */\n generate?: (source: Blob, meta: ThumbSource, want: VariantWant) => Promise<GeneratedVariants>\n /** Fired with each patched record so the caller can update its cache in place. */\n onGenerated?: (asset: AssetRecord) => void\n /** Fired after each candidate (and once at the end) to drive the progress UI. */\n onProgress?: (progress: BackfillProgress) => void\n /** Cooperative cancellation — checked before each candidate (AD-1). */\n signal?: BackfillSignal\n}\n\n/**\n * Run the backfill sequentially over a project's images that still need a variant.\n * Resolves with the final tally; never throws (per-asset failures are counted, not\n * fatal). One fetch per candidate serves both variants; only the missing ones are\n * (re)generated (EC-420 AD-4), so an asset with a thumb but no lqip is patched with\n * just the lqip and its existing thumb blob is left untouched.\n */\nexport async function runVariantBackfill(\n deps: BackfillDeps,\n projectId: string,\n): Promise<BackfillProgress> {\n const all = await deps.assets.listAssets(projectId)\n const candidates = assetsNeedingVariants(all)\n const generate =\n deps.generate ??\n ((source: Blob, meta: ThumbSource, want: VariantWant) =>\n generateVariants({ blobs: deps.blobs }, source, meta, want))\n\n const progress: BackfillProgress = { total: candidates.length, done: 0, generated: 0, skipped: 0, failed: 0 }\n deps.onProgress?.({ ...progress })\n\n for (const asset of candidates) {\n if (deps.signal?.cancelled) {\n progress.cancelled = true\n break\n }\n deps.onProgress?.({ ...progress, current: asset.filename })\n try {\n const bytes = await deps.blobs.get(asset.storageKey)\n if (!bytes) {\n progress.failed += 1\n } else {\n const source = new Blob([bytes as BlobPart], { type: asset.contentType })\n const variants = await generate(\n source,\n {\n contentType: asset.contentType,\n ...(asset.width !== undefined ? { width: asset.width } : {}),\n ...(asset.height !== undefined ? { height: asset.height } : {}),\n },\n variantWantFor(asset),\n )\n if (variants.thumb === undefined && variants.lqip === undefined) {\n progress.skipped += 1\n } else {\n const patched = await deps.assets.upsertAsset(projectId, assetVariantWrite(asset.id, variants))\n deps.onGenerated?.(patched)\n progress.generated += 1\n }\n }\n } catch {\n progress.failed += 1\n }\n progress.done += 1\n deps.onProgress?.({ ...progress })\n }\n\n deps.onProgress?.({ ...progress })\n return progress\n}\n"]}
|
|
@@ -2,19 +2,24 @@ import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
|
2
2
|
/**
|
|
3
3
|
* Permissive-paste validation (EC-175): pasting NEVER refuses a structurally
|
|
4
4
|
* valid fragment. Instead, the inserted subtree is diffed against the TARGET
|
|
5
|
-
* project's registry
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* project's registry at paste time (cross-project semantics: the fragment's
|
|
6
|
+
* source project is irrelevant — what matters is what *this* project can
|
|
7
|
+
* resolve), and every mismatch becomes a visible, node-addressed issue the
|
|
8
|
+
* notice deep-links. The renderer's existing fallbacks keep the pasted nodes
|
|
9
|
+
* visible meanwhile.
|
|
10
10
|
*
|
|
11
|
-
* EC-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* EC-574: the second half of this diff — binding source ids the target could
|
|
12
|
+
* not resolve — went with the data-binding subsystem. A pasted node carrying a
|
|
13
|
+
* retired binding member is reported by the composition validator
|
|
14
|
+
* (`legacy-node-member`) when the document is validated, not here.
|
|
15
|
+
*
|
|
16
|
+
* EC-494: this diff runs STUDIO-side (it needs the target registry), so it holds
|
|
17
|
+
* the active locale and can call `translate` directly — no message ref has to
|
|
18
|
+
* cross a boundary, unlike the engine-built clipboard errors in
|
|
19
|
+
* `@elytracms/core/clipboard`.
|
|
15
20
|
*/
|
|
16
21
|
export interface PasteIssue {
|
|
17
|
-
kind: 'missing-component'
|
|
22
|
+
kind: 'missing-component';
|
|
18
23
|
message: string;
|
|
19
24
|
/** The inserted node carrying the problem — deep-link target. */
|
|
20
25
|
nodeId: string;
|
|
@@ -22,18 +27,15 @@ export interface PasteIssue {
|
|
|
22
27
|
export interface PasteDiff {
|
|
23
28
|
/** Distinct component ids absent from the target registry. */
|
|
24
29
|
missingComponents: string[];
|
|
25
|
-
/** Distinct binding source ids the target cannot resolve. */
|
|
26
|
-
unresolvedBindings: string[];
|
|
27
30
|
issues: PasteIssue[];
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
33
|
* Diff an inserted subtree (with its final, session-assigned node ids)
|
|
31
|
-
* against the target project's registry
|
|
34
|
+
* against the target project's registry.
|
|
32
35
|
*/
|
|
33
36
|
export declare function diffPastedSubtree(args: {
|
|
34
37
|
node: ComponentNode;
|
|
35
38
|
hasComponent: (componentId: string) => boolean;
|
|
36
|
-
knownSourceIds: ReadonlySet<string>;
|
|
37
39
|
}): PasteDiff;
|
|
38
|
-
/** “Pasted —
|
|
40
|
+
/** “Pasted — 1 component missing”, or just “Pasted”. */
|
|
39
41
|
export declare function pasteSummaryMessage(diff: PasteDiff): string;
|
|
@@ -8,62 +8,25 @@ function walk(node, visit) {
|
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Diff an inserted subtree (with its final, session-assigned node ids)
|
|
11
|
-
* against the target project's registry
|
|
11
|
+
* against the target project's registry.
|
|
12
12
|
*/
|
|
13
13
|
export function diffPastedSubtree(args) {
|
|
14
14
|
const missingComponents = new Map(); // componentId -> first nodeId
|
|
15
|
-
const unresolvedBindings = new Map(); // nodeId+sourceId -> issue
|
|
16
|
-
const unresolvedSourceIds = new Set();
|
|
17
15
|
walk(args.node, (node) => {
|
|
18
16
|
if (!args.hasComponent(node.componentId) && !missingComponents.has(node.componentId)) {
|
|
19
17
|
missingComponents.set(node.componentId, node.id);
|
|
20
18
|
}
|
|
21
|
-
const bindings = [];
|
|
22
|
-
for (const value of Object.values(node.props ?? {})) {
|
|
23
|
-
if (value.kind === 'binding')
|
|
24
|
-
bindings.push(value.binding);
|
|
25
|
-
}
|
|
26
|
-
if (node.condition)
|
|
27
|
-
bindings.push(node.condition.source);
|
|
28
|
-
for (const binding of bindings) {
|
|
29
|
-
if (args.knownSourceIds.has(binding.sourceId))
|
|
30
|
-
continue;
|
|
31
|
-
unresolvedSourceIds.add(binding.sourceId);
|
|
32
|
-
const key = `${node.id} ${binding.sourceId}`;
|
|
33
|
-
if (!unresolvedBindings.has(key)) {
|
|
34
|
-
unresolvedBindings.set(key, {
|
|
35
|
-
kind: 'unresolved-binding',
|
|
36
|
-
message: translate('builder.clipboard.issue.unresolvedBinding', {
|
|
37
|
-
sourceId: binding.sourceId,
|
|
38
|
-
}),
|
|
39
|
-
nodeId: node.id,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
19
|
});
|
|
44
|
-
const issues = [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})),
|
|
51
|
-
];
|
|
52
|
-
return {
|
|
53
|
-
missingComponents: [...missingComponents.keys()],
|
|
54
|
-
unresolvedBindings: [...unresolvedSourceIds],
|
|
55
|
-
issues,
|
|
56
|
-
};
|
|
20
|
+
const issues = [...missingComponents.entries()].map(([componentId, nodeId]) => ({
|
|
21
|
+
kind: 'missing-component',
|
|
22
|
+
message: translate('builder.clipboard.issue.missingComponent', { componentId }),
|
|
23
|
+
nodeId,
|
|
24
|
+
}));
|
|
25
|
+
return { missingComponents: [...missingComponents.keys()], issues };
|
|
57
26
|
}
|
|
58
|
-
/** “Pasted —
|
|
27
|
+
/** “Pasted — 1 component missing”, or just “Pasted”. */
|
|
59
28
|
export function pasteSummaryMessage(diff) {
|
|
60
29
|
const parts = [];
|
|
61
|
-
const bindings = diff.issues.filter((i) => i.kind === 'unresolved-binding').length;
|
|
62
|
-
if (bindings > 0) {
|
|
63
|
-
parts.push(translate(bindings === 1
|
|
64
|
-
? 'builder.clipboard.summary.bindingUnresolvedOne'
|
|
65
|
-
: 'builder.clipboard.summary.bindingsUnresolved', { count: bindings }));
|
|
66
|
-
}
|
|
67
30
|
if (diff.missingComponents.length > 0) {
|
|
68
31
|
const n = diff.missingComponents.length;
|
|
69
32
|
parts.push(translate(n === 1
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paste-issues.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-issues.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"paste-issues.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-issues.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAmCtD,SAAS,IAAI,CAAC,IAAmB,EAAE,KAAoC;IACrE,KAAK,CAAC,IAAI,CAAC,CAAA;IACX,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;QACvD,KAAK,MAAM,KAAK,IAAI,QAAQ;YAAE,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IAClD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAGjC;IACC,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAkB,CAAA,CAAC,8BAA8B;IAClF,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;QACvB,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YACrF,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;QAClD,CAAC;IACH,CAAC,CAAC,CAAA;IACF,MAAM,MAAM,GAAiB,CAAC,GAAG,iBAAiB,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAC/D,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,EAAc,EAAE,CAAC,CAAC;QACtC,IAAI,EAAE,mBAAmB;QACzB,OAAO,EAAE,SAAS,CAAC,0CAA0C,EAAE,EAAE,WAAW,EAAE,CAAC;QAC/E,MAAM;KACP,CAAC,CACH,CAAA;IACD,OAAO,EAAE,iBAAiB,EAAE,CAAC,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAA;AACrE,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,mBAAmB,CAAC,IAAe;IACjD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,IAAI,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAA;QACvC,KAAK,CAAC,IAAI,CACR,SAAS,CACP,CAAC,KAAK,CAAC;YACL,CAAC,CAAC,+CAA+C;YACjD,CAAC,CAAC,6CAA6C,EACjD,EAAE,KAAK,EAAE,CAAC,EAAE,CACb,CACF,CAAA;IACH,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC;QACrB,CAAC,CAAC,SAAS,CAAC,sCAAsC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAChF,CAAC,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAA;AACnD,CAAC","sourcesContent":["import type { ComponentNode } from '@elytracms/core/project-graph'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Permissive-paste validation (EC-175): pasting NEVER refuses a structurally\n * valid fragment. Instead, the inserted subtree is diffed against the TARGET\n * project's registry at paste time (cross-project semantics: the fragment's\n * source project is irrelevant — what matters is what *this* project can\n * resolve), and every mismatch becomes a visible, node-addressed issue the\n * notice deep-links. The renderer's existing fallbacks keep the pasted nodes\n * visible meanwhile.\n *\n * EC-574: the second half of this diff — binding source ids the target could\n * not resolve — went with the data-binding subsystem. A pasted node carrying a\n * retired binding member is reported by the composition validator\n * (`legacy-node-member`) when the document is validated, not here.\n *\n * EC-494: this diff runs STUDIO-side (it needs the target registry), so it holds\n * the active locale and can call `translate` directly — no message ref has to\n * cross a boundary, unlike the engine-built clipboard errors in\n * `@elytracms/core/clipboard`.\n */\n\nexport interface PasteIssue {\n kind: 'missing-component'\n message: string\n /** The inserted node carrying the problem — deep-link target. */\n nodeId: string\n}\n\nexport interface PasteDiff {\n /** Distinct component ids absent from the target registry. */\n missingComponents: string[]\n issues: PasteIssue[]\n}\n\nfunction walk(node: ComponentNode, visit: (node: ComponentNode) => void): void {\n visit(node)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) walk(child, visit)\n }\n}\n\n/**\n * Diff an inserted subtree (with its final, session-assigned node ids)\n * against the target project's registry.\n */\nexport function diffPastedSubtree(args: {\n node: ComponentNode\n hasComponent: (componentId: string) => boolean\n}): PasteDiff {\n const missingComponents = new Map<string, string>() // componentId -> first nodeId\n walk(args.node, (node) => {\n if (!args.hasComponent(node.componentId) && !missingComponents.has(node.componentId)) {\n missingComponents.set(node.componentId, node.id)\n }\n })\n const issues: PasteIssue[] = [...missingComponents.entries()].map(\n ([componentId, nodeId]): PasteIssue => ({\n kind: 'missing-component',\n message: translate('builder.clipboard.issue.missingComponent', { componentId }),\n nodeId,\n }),\n )\n return { missingComponents: [...missingComponents.keys()], issues }\n}\n\n/** “Pasted — 1 component missing”, or just “Pasted”. */\nexport function pasteSummaryMessage(diff: PasteDiff): string {\n const parts: string[] = []\n if (diff.missingComponents.length > 0) {\n const n = diff.missingComponents.length\n parts.push(\n translate(\n n === 1\n ? 'builder.clipboard.summary.componentMissingOne'\n : 'builder.clipboard.summary.componentsMissing',\n { count: n },\n ),\n )\n }\n return parts.length > 0\n ? translate('builder.clipboard.summary.pastedWith', { parts: parts.join(', ') })\n : translate('builder.clipboard.summary.pasted')\n}\n"]}
|
|
@@ -14,6 +14,11 @@ import type { ComponentManifest } from '@elytracms/core/component-registry';
|
|
|
14
14
|
* separate member of the result union, not a flag beside the target). Nested
|
|
15
15
|
* validity of the pasted subtree stays with the publish gate, exactly like a
|
|
16
16
|
* drop.
|
|
17
|
+
*
|
|
18
|
+
* EC-554: the composition FIELD's top-level `allow` is checked here too, through
|
|
19
|
+
* the same `placementRefusal` → `fieldAllowAt` the drop rule and the insert offer
|
|
20
|
+
* read — a paste that lands as a direct child of the composition root must be in
|
|
21
|
+
* the field's vocabulary.
|
|
17
22
|
*/
|
|
18
23
|
export interface PasteTarget {
|
|
19
24
|
ok: true;
|
|
@@ -21,7 +26,8 @@ export interface PasteTarget {
|
|
|
21
26
|
/** Human label of where the paste lands (notices/debugging). */
|
|
22
27
|
destinationLabel: string;
|
|
23
28
|
}
|
|
24
|
-
|
|
29
|
+
/** The resolved slot's own `allow` list excludes the pasted root (EC-545). */
|
|
30
|
+
export interface PasteSlotRejection {
|
|
25
31
|
ok: false;
|
|
26
32
|
code: 'not-allowed';
|
|
27
33
|
/** The pasted root the slot refuses. */
|
|
@@ -31,6 +37,19 @@ export interface PasteRejection {
|
|
|
31
37
|
/** Human label of the refused destination (`Section · body`). */
|
|
32
38
|
destinationLabel: string;
|
|
33
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The composition field's top-level `allow` excludes the pasted root (EC-554).
|
|
42
|
+
* Deliberately carries NO slot/destination label: the refused slot is the
|
|
43
|
+
* invisible synthetic root's (`Stack · children`), and the reason to show is the
|
|
44
|
+
* FIELD — the notice names it from the editor's field scope.
|
|
45
|
+
*/
|
|
46
|
+
export interface PasteFieldRejection {
|
|
47
|
+
ok: false;
|
|
48
|
+
code: 'field-not-allowed';
|
|
49
|
+
/** The pasted root the field vocabulary refuses. */
|
|
50
|
+
componentId: string;
|
|
51
|
+
}
|
|
52
|
+
export type PasteRejection = PasteSlotRejection | PasteFieldRejection;
|
|
34
53
|
export type PasteResolution = PasteTarget | PasteRejection;
|
|
35
54
|
export declare function resolvePasteTarget(args: {
|
|
36
55
|
root: ComponentNode;
|
|
@@ -39,4 +58,9 @@ export declare function resolvePasteTarget(args: {
|
|
|
39
58
|
/** The pasted subtree's root component — the id the slot leash judges. */
|
|
40
59
|
componentId: string;
|
|
41
60
|
getManifest: (componentId: string) => ComponentManifest | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* The composition field's top-level `allow` (EC-554) — `undefined` when the
|
|
63
|
+
* field declares none. Required so no caller can forget to pass it.
|
|
64
|
+
*/
|
|
65
|
+
fieldAllow: readonly string[] | undefined;
|
|
42
66
|
}): PasteResolution;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { getNode, getParentInfo } from '@elytracms/core/project-graph';
|
|
2
|
-
import { manifestRules,
|
|
2
|
+
import { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
3
3
|
import { shortId } from '@elytracms/studio/lib/persona/labels';
|
|
4
4
|
export function resolvePasteTarget(args) {
|
|
5
|
-
const { root, componentId, getManifest } = args;
|
|
5
|
+
const { root, componentId, getManifest, fieldAllow } = args;
|
|
6
6
|
const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined;
|
|
7
7
|
const node = anchor ?? root;
|
|
8
|
-
const leashed = (slot, slotRule, target, destinationLabel) =>
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const leashed = (slot, slotRule, target, destinationLabel) => {
|
|
9
|
+
const refusal = placementRefusal({ root, parentId: target.parentId, slot: slotRule, fieldAllow, componentId });
|
|
10
|
+
if (refusal === 'field')
|
|
11
|
+
return { ok: false, code: 'field-not-allowed', componentId };
|
|
12
|
+
if (refusal === 'slot')
|
|
13
|
+
return { ok: false, code: 'not-allowed', componentId, slot, destinationLabel };
|
|
14
|
+
return { ok: true, target, destinationLabel };
|
|
15
|
+
};
|
|
11
16
|
// EC-386: the shared slot rule (`primaryInsertSlot` over the merged
|
|
12
17
|
// `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)
|
|
13
18
|
// receives the paste exactly like the insert dialog and a DnD `into`.
|
|
@@ -25,6 +30,8 @@ export function resolvePasteTarget(args) {
|
|
|
25
30
|
return leashed(info.slot, parentSlot, { parentId: info.parent.id, slot: info.slot, index: info.index + 1 }, `${shortId(info.parent.componentId)} · ${info.slot}`);
|
|
26
31
|
}
|
|
27
32
|
// Leaf root with no declared slots (degenerate graphs): append to `children`.
|
|
28
|
-
|
|
33
|
+
// No slot rule exists, but the paste still lands at the root — the field leash
|
|
34
|
+
// applies.
|
|
35
|
+
return leashed('children', undefined, { parentId: root.id, slot: 'children' }, 'root');
|
|
29
36
|
}
|
|
30
37
|
//# sourceMappingURL=paste-target.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGtE,OAAO,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { SlotRule } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n *\n * EC-545: the resolved slot's `allow` leash is checked HERE, for the pasted\n * subtree's root, through the same `slotAllows` the drop rule reads — so a\n * component drag-and-drop refuses is refused by clipboard too, and a caller\n * cannot reach an `InsertTarget` without passing the check (the rejection is a\n * separate member of the result union, not a flag beside the target). Nested\n * validity of the pasted subtree stays with the publish gate, exactly like a\n * drop.\n *\n * EC-554: the composition FIELD's top-level `allow` is checked here too, through\n * the same `placementRefusal` → `fieldAllowAt` the drop rule and the insert offer\n * read — a paste that lands as a direct child of the composition root must be in\n * the field's vocabulary.\n */\n\nexport interface PasteTarget {\n ok: true\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\n/** The resolved slot's own `allow` list excludes the pasted root (EC-545). */\nexport interface PasteSlotRejection {\n ok: false\n code: 'not-allowed'\n /** The pasted root the slot refuses. */\n componentId: string\n /** The resolved slot whose `allow` list excludes it. */\n slot: string\n /** Human label of the refused destination (`Section · body`). */\n destinationLabel: string\n}\n\n/**\n * The composition field's top-level `allow` excludes the pasted root (EC-554).\n * Deliberately carries NO slot/destination label: the refused slot is the\n * invisible synthetic root's (`Stack · children`), and the reason to show is the\n * FIELD — the notice names it from the editor's field scope.\n */\nexport interface PasteFieldRejection {\n ok: false\n code: 'field-not-allowed'\n /** The pasted root the field vocabulary refuses. */\n componentId: string\n}\n\nexport type PasteRejection = PasteSlotRejection | PasteFieldRejection\n\nexport type PasteResolution = PasteTarget | PasteRejection\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n /** The pasted subtree's root component — the id the slot leash judges. */\n componentId: string\n getManifest: (componentId: string) => ComponentManifest | undefined\n /**\n * The composition field's top-level `allow` (EC-554) — `undefined` when the\n * field declares none. Required so no caller can forget to pass it.\n */\n fieldAllow: readonly string[] | undefined\n}): PasteResolution {\n const { root, componentId, getManifest, fieldAllow } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n const leashed = (\n slot: string,\n slotRule: SlotRule | undefined,\n target: InsertTarget,\n destinationLabel: string,\n ): PasteResolution => {\n const refusal = placementRefusal({ root, parentId: target.parentId, slot: slotRule, fieldAllow, componentId })\n if (refusal === 'field') return { ok: false, code: 'field-not-allowed', componentId }\n if (refusal === 'slot') return { ok: false, code: 'not-allowed', componentId, slot, destinationLabel }\n return { ok: true, target, destinationLabel }\n }\n\n // EC-386: the shared slot rule (`primaryInsertSlot` over the merged\n // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)\n // receives the paste exactly like the insert dialog and a DnD `into`.\n const manifest = getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n if (childSlot) {\n return leashed(\n childSlot.name,\n childSlot,\n { parentId: node.id, slot: childSlot.name },\n `${shortId(node.componentId)} · ${childSlot.name}`,\n )\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n // A sibling paste is governed by the PARENT's slot leash (the drop rule's\n // before/after case). An unknown parent manifest imposes no allow-list;\n // validation owns that issue.\n const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot)\n return leashed(\n info.slot,\n parentSlot,\n { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n `${shortId(info.parent.componentId)} · ${info.slot}`,\n )\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n // No slot rule exists, but the paste still lands at the root — the field leash\n // applies.\n return leashed('children', undefined, { parentId: root.id, slot: 'children' }, 'root')\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAsB/D,6EAA6E;AAC7E,MAAM,oBAAoB,GAAG;IAC3B,mBAAmB;IACnB,qBAAqB;IACrB,mBAAmB;IACnB,kBAAkB;
|
|
1
|
+
{"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAsB/D,6EAA6E;AAC7E,MAAM,oBAAoB,GAAG;IAC3B,mBAAmB;IACnB,qBAAqB;IACrB,mBAAmB;IACnB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV,MAAM,UAAU,GAAqC,IAAI,GAAG,EAAE,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA4B,EAC5B,SAAiB,EACjB,YAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAA;IAChG,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,GAAG,EAAE,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC3C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACpE,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,SAA6B,EAC7B,YAAoB;IAEpB,MAAM,KAAK,GAAG,iBAAiB,CAA2B;QACxD,4EAA4E;QAC5E,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,IAAI,GAAG,EAAE,YAAY,CAAC;QAChE,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,sBAAsB,CAAC,UAAU,EAAE,SAAmB,EAAE,YAAY,CAAC;QAC7F,YAAY,EAAE,oBAAoB;QAClC,OAAO,EAAE,SAAS,KAAK,SAAS;KACjC,CAAC,CAAA;IACF,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IACzE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;AAC1F,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { OperationsClient } from '../operations'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\n\n/**\n * The PUBLISHED-perspective documents of a collection (EC-205, \"live = published\")\n * — the pinned published snapshot of each currently-published document, keyed by\n * id. Live surfaces (a document's live URL + \"View live\", EC-216) MUST derive\n * from this, never from the draft rows the studio edits: an unpublished document,\n * or an unpublished `slug`/`parent` edit, must never leak into a \"live\" URL. The\n * pool is exactly the set the live site would serve, so composing a nested page's\n * URL from these ancestors yields the URL the live site actually resolves.\n *\n * EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache\n * seam — invalidated only when a publish/unpublish/restore/delete command\n * succeeds (NOT on every autosave upsert — a draft edit doesn't change what's\n * published). Cost: ONE bulk `documents.listPublished` read (EC-307).\n */\nexport interface PublishedDocuments {\n /** Pinned published snapshot per published document id. Empty while loading. */\n byId: ReadonlyMap<string, CmsDocument>\n status: 'loading' | 'ready'\n}\n\n/** Commands that change which version is pinned live (and thus the pool). */\nconst PUBLISHED_RELOAD_OPS = [\n 'documents.publish',\n 'documents.unpublish',\n 'documents.restore',\n 'documents.delete',\n 'documents.clear',\n] as const\n\nconst EMPTY_POOL: ReadonlyMap<string, CmsDocument> = new Map()\n\n/**\n * Build the published pool of a collection (pure over the operations client): the\n * pinned published snapshot of every currently-published document, keyed by id.\n * EC-307: one bulk `documents.listPublished` read — the backend resolves each\n * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a\n * per-document `listVersions` fan-out. Read failures degrade to an empty pool.\n */\nexport async function loadPublishedDocuments(\n operations: OperationsClient,\n projectId: string,\n collectionId: string,\n): Promise<Map<string, CmsDocument>> {\n const result = await operations.documents.listPublished({ projectId, collection: collectionId })\n if (!result.ok) return new Map()\n const byId = new Map<string, CmsDocument>()\n for (const snapshot of result.value) byId.set(snapshot.id, snapshot)\n return byId\n}\n\nexport function usePublishedDocuments(\n projectId: string | undefined,\n collectionId: string,\n): PublishedDocuments {\n const query = useOperationQuery<Map<string, CmsDocument>>({\n // Playground/fixture mode (no backend) is `enabled: false` → never queried.\n queryKey: ['publishedDocuments', projectId ?? '∅', collectionId],\n loader: (operations) => loadPublishedDocuments(operations, projectId as string, collectionId),\n invalidateOn: PUBLISHED_RELOAD_OPS,\n enabled: projectId !== undefined,\n })\n if (projectId === undefined) return { byId: EMPTY_POOL, status: 'ready' }\n return { byId: query.data ?? EMPTY_POOL, status: query.isPending ? 'loading' : 'ready' }\n}\n"]}
|
|
@@ -38,8 +38,8 @@ export type FilterClause = z.infer<typeof filterClauseSchema>;
|
|
|
38
38
|
export declare const viewSortSchema: z.ZodObject<{
|
|
39
39
|
field: z.ZodString;
|
|
40
40
|
dir: z.ZodEnum<{
|
|
41
|
-
asc: "asc";
|
|
42
41
|
desc: "desc";
|
|
42
|
+
asc: "asc";
|
|
43
43
|
}>;
|
|
44
44
|
}, z.core.$strip>;
|
|
45
45
|
export type ViewSort = z.infer<typeof viewSortSchema>;
|
|
@@ -63,8 +63,8 @@ export declare const tableViewStateSchema: z.ZodObject<{
|
|
|
63
63
|
sort: z.ZodNullable<z.ZodObject<{
|
|
64
64
|
field: z.ZodString;
|
|
65
65
|
dir: z.ZodEnum<{
|
|
66
|
-
asc: "asc";
|
|
67
66
|
desc: "desc";
|
|
67
|
+
asc: "asc";
|
|
68
68
|
}>;
|
|
69
69
|
}, z.core.$strip>>;
|
|
70
70
|
columns: z.ZodArray<z.ZodString>;
|
|
@@ -305,8 +305,8 @@ export declare const configViewSchema: z.ZodObject<{
|
|
|
305
305
|
sort: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
306
306
|
field: z.ZodString;
|
|
307
307
|
dir: z.ZodEnum<{
|
|
308
|
-
asc: "asc";
|
|
309
308
|
desc: "desc";
|
|
309
|
+
asc: "asc";
|
|
310
310
|
}>;
|
|
311
311
|
}, z.core.$strip>>>;
|
|
312
312
|
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
package/dist/lib/data/index.d.ts
CHANGED
package/dist/lib/data/index.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// EC-574: the data-source store, sample capture, providers and sources-store that
|
|
2
|
+
// used to live here went with the data-binding subsystem. What remains is the
|
|
3
|
+
// canvas's content session (collection-content), its write-sync refresh and the
|
|
4
|
+
// playground unload guard.
|
|
4
5
|
export * from './collection-content.js';
|
|
5
6
|
export * from './content-session-refresh.js';
|
|
6
7
|
export * from './playground-unload-guard.js';
|
|
7
|
-
// Data providers (EC-167) are imported from './providers' directly: the module
|
|
8
|
-
// exports a `DataProvider` domain type that would collide with the store's
|
|
9
|
-
// `<DataProvider>` React provider in a wildcard barrel.
|
|
10
8
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/data/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/data/index.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,8EAA8E;AAC9E,gFAAgF;AAChF,2BAA2B;AAC3B,cAAc,sBAAsB,CAAA;AACpC,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA","sourcesContent":["// EC-574: the data-source store, sample capture, providers and sources-store that\n// used to live here went with the data-binding subsystem. What remains is the\n// canvas's content session (collection-content), its write-sync refresh and the\n// playground unload guard.\nexport * from './collection-content'\nexport * from './content-session-refresh'\nexport * from './playground-unload-guard'\n"]}
|
|
@@ -4,8 +4,9 @@ import type { ComponentManifest } from '@elytracms/core/component-registry';
|
|
|
4
4
|
* Drag & drop eligibility rules (EC-160 + EC-162, vision §3.1) — pure
|
|
5
5
|
* graph-level functions with **no DOM or UI dependencies**. The canvas and the
|
|
6
6
|
* page tree both resolve drops through `resolveDrop`, so the two surfaces can
|
|
7
|
-
* never disagree about what a manifest slot allow-list permits,
|
|
8
|
-
*
|
|
7
|
+
* never disagree about what a manifest slot allow-list permits, what the
|
|
8
|
+
* composition field's top-level vocabulary permits at the root (EC-554), which
|
|
9
|
+
* children a component accepts, or whether a move would create a cycle.
|
|
9
10
|
*
|
|
10
11
|
* The drop-zone model is position-based: `into` a slot, `before`/`after` a
|
|
11
12
|
* sibling along the parent's flow axis, and the EC-162 gesture positions
|
|
@@ -72,6 +73,11 @@ export type DropRejectionCode =
|
|
|
72
73
|
| 'no-slot'
|
|
73
74
|
/** The target slot's manifest allow-list rejects the dragged component. */
|
|
74
75
|
| 'not-allowed'
|
|
76
|
+
/**
|
|
77
|
+
* EC-554: the composition FIELD's top-level `allow` rejects the component
|
|
78
|
+
* landing as a direct child of the composition root (see {@link fieldAllowAt}).
|
|
79
|
+
*/
|
|
80
|
+
| 'field-not-allowed'
|
|
75
81
|
/** Moving a node into itself or its own subtree (cycle). */
|
|
76
82
|
| 'own-subtree'
|
|
77
83
|
/** The page root cannot be moved, and nothing can drop beside it. */
|
|
@@ -119,6 +125,37 @@ export declare function primaryInsertSlot(manifest: ManifestRules | undefined):
|
|
|
119
125
|
export declare function canNestInto(getManifest: ManifestLookup, componentId: string): boolean;
|
|
120
126
|
/** True when a slot's allow-list (if any) permits the component id. */
|
|
121
127
|
export declare function slotAllows(slot: SlotRule | undefined, componentId: string): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* The composition FIELD's top-level vocabulary as it applies to a placement under
|
|
130
|
+
* `parentId` (EC-188, EC-554) — the ONE reader of a field's `allow` on the studio
|
|
131
|
+
* side: every insertion surface (the insert offer in `insert-context`, drag-and-drop
|
|
132
|
+
* on the canvas and the page tree incl. the wrap gesture, paste) asks here, so no
|
|
133
|
+
* surface can apply the field leash differently or not at all.
|
|
134
|
+
*
|
|
135
|
+
* A field `allow` governs only the value's TOP level — a direct child of the
|
|
136
|
+
* composition root, mirroring `validateCompositionValue` — so this returns the
|
|
137
|
+
* field vocabulary when `parentId` IS the root node and `undefined` (no field
|
|
138
|
+
* constraint) anywhere deeper. Keyed on node IDENTITY, never on the component id:
|
|
139
|
+
* the composition editor's synthetic root is a `base.Stack`, which is also a
|
|
140
|
+
* nestable block, and a rule keyed on the component id would leash every nested
|
|
141
|
+
* Stack. EC-544: an omitted `allow` is `undefined` here (any registered
|
|
142
|
+
* component); a declared list is a closed set.
|
|
143
|
+
*/
|
|
144
|
+
export declare function fieldAllowAt(root: ComponentNode, parentId: string, fieldAllow: readonly string[] | undefined): readonly string[] | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* Which declared leash refuses `componentId` landing under `parentId` in `slot`
|
|
147
|
+
* (EC-545 slot leash, EC-554 field leash) — `null` when both admit it. The field
|
|
148
|
+
* is judged first: it can only refuse at the composition root, where the slot is
|
|
149
|
+
* the invisible synthetic root's and the field is the declaration the author sees.
|
|
150
|
+
*/
|
|
151
|
+
export declare function placementRefusal(args: {
|
|
152
|
+
root: ComponentNode;
|
|
153
|
+
/** The node the placement lands under (the insertion target's `parentId`). */
|
|
154
|
+
parentId: string;
|
|
155
|
+
slot: SlotRule | undefined;
|
|
156
|
+
fieldAllow: readonly string[] | undefined;
|
|
157
|
+
componentId: string;
|
|
158
|
+
}): 'field' | 'slot' | null;
|
|
122
159
|
/**
|
|
123
160
|
* Resolve a prospective drop into either an executable plan (one `move` or
|
|
124
161
|
* `insert` builder action → one operations command → one undo step) or a
|
|
@@ -128,6 +165,12 @@ export declare function slotAllows(slot: SlotRule | undefined, componentId: stri
|
|
|
128
165
|
export declare function resolveDrop(args: {
|
|
129
166
|
root: ComponentNode;
|
|
130
167
|
getManifest: ManifestLookup;
|
|
168
|
+
/**
|
|
169
|
+
* The composition field's top-level `allow` (EC-554) — `undefined` when the
|
|
170
|
+
* field declares none. Required so no surface can forget to pass it; judged
|
|
171
|
+
* through {@link fieldAllowAt} for drops whose parent is the root.
|
|
172
|
+
*/
|
|
173
|
+
fieldAllow: readonly string[] | undefined;
|
|
131
174
|
subject: DragSubject;
|
|
132
175
|
location: DropLocation;
|
|
133
176
|
}): DropResolution;
|