@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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { convexTest } from 'convex-test'
|
|
3
|
+
import { makeFunctionReference } from 'convex/server'
|
|
4
|
+
import type { ConvexHttpClient } from 'convex/browser'
|
|
5
|
+
import type { FunctionReference } from 'convex/server'
|
|
6
|
+
import { InMemoryPersistenceAdapter } from '@elytracms/core/persistence'
|
|
7
|
+
import type { PersistenceAdapter } from '@elytracms/core/persistence'
|
|
8
|
+
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
9
|
+
import { updateAssetMetadata } from '../src/components/assets/asset-metadata'
|
|
10
|
+
import { runVariantBackfill } from '../src/lib/assets/backfill-thumbs'
|
|
11
|
+
import schema from './schema'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* EC-577 — the studio's REAL asset writers against BOTH adapters.
|
|
15
|
+
*
|
|
16
|
+
* The contract scenarios (`contract-scenarios.ts`, run by `contract-parity.test.ts`)
|
|
17
|
+
* pin the adapter write rule itself. This file pins the writers on top of it —
|
|
18
|
+
* the metadata dialog's `updateAssetMetadata` and the "Generate previews"
|
|
19
|
+
* backfill — because the reported loss happened between the two: the writer
|
|
20
|
+
* expressed a clear as an omitted key and restated everything else, and the two
|
|
21
|
+
* adapters read that one write in opposite ways. Measured on 162174ce with the
|
|
22
|
+
* same writer calls:
|
|
23
|
+
*
|
|
24
|
+
* in-memory: {"focalPoint":null,"alt":null,"thumb":null,"lqip":null}
|
|
25
|
+
* convex : {"focalPoint":{"x":0.2,"y":0.8},"alt":"Harbour","thumb":{…},"lqip":"data:…"}
|
|
26
|
+
*
|
|
27
|
+
* i.e. the playground lost the previews and connected mode never cleared.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const modules = {
|
|
31
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
32
|
+
'./_generated/api.ts': async () => ({}),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PROJECT_ID = 'rail-write-parity'
|
|
36
|
+
const SESSION_USER_ID = 'user_admin'
|
|
37
|
+
|
|
38
|
+
async function convexBackend() {
|
|
39
|
+
const t = convexTest(schema, modules)
|
|
40
|
+
const now = new Date().toISOString()
|
|
41
|
+
await t.run(async (ctx) => {
|
|
42
|
+
await ctx.db.insert('members', { userId: SESSION_USER_ID, role: 'admin', createdAt: now, updatedAt: now })
|
|
43
|
+
})
|
|
44
|
+
const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
|
|
45
|
+
const client = {
|
|
46
|
+
query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => asAdmin.query(ref, args ?? {}),
|
|
47
|
+
mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) => asAdmin.mutation(ref, args ?? {}),
|
|
48
|
+
} as unknown as ConvexHttpClient
|
|
49
|
+
return { t, adapter: new ConvexPersistenceAdapter(client, PROJECT_ID, []) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const THUMB = { storageKey: 'blob_thumb', url: 'https://cdn.test/t.webp', width: 480, height: 240 }
|
|
53
|
+
const LQIP = 'data:image/webp;base64,AAAA'
|
|
54
|
+
|
|
55
|
+
async function seedHero(adapter: PersistenceAdapter) {
|
|
56
|
+
await adapter.assets.upsertAsset(PROJECT_ID, {
|
|
57
|
+
id: 'asset_hero',
|
|
58
|
+
filename: 'hero.jpg',
|
|
59
|
+
contentType: 'image/jpeg',
|
|
60
|
+
byteSize: 10,
|
|
61
|
+
width: 4000,
|
|
62
|
+
height: 2000,
|
|
63
|
+
alt: 'Harbour',
|
|
64
|
+
title: 'Hero',
|
|
65
|
+
focalPoint: { x: 0.2, y: 0.8 },
|
|
66
|
+
thumb: THUMB,
|
|
67
|
+
lqip: LQIP,
|
|
68
|
+
meta: { alt: 'Harbour', legacyKey: 'kept' },
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** "Reset to centre", then clear alt — the two connected clears that never persisted. */
|
|
73
|
+
async function metadataClears(adapter: PersistenceAdapter) {
|
|
74
|
+
await seedHero(adapter)
|
|
75
|
+
const assets = adapter.assets
|
|
76
|
+
const reset = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { focalPoint: null })
|
|
77
|
+
const clearedAlt = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { alt: '' })
|
|
78
|
+
expect([reset.ok, clearedAlt.ok]).toEqual([true, true])
|
|
79
|
+
const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_hero')
|
|
80
|
+
return {
|
|
81
|
+
focalPoint: after.focalPoint ?? null,
|
|
82
|
+
alt: after.alt ?? null,
|
|
83
|
+
title: after.title ?? null,
|
|
84
|
+
thumb: after.thumb ?? null,
|
|
85
|
+
lqip: after.lqip ?? null,
|
|
86
|
+
meta: after.meta ?? null,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('EC-577 — the asset writers mean the same thing on both adapters', () => {
|
|
91
|
+
it('a metadata clear clears, and keeps thumb/lqip/meta, in the playground AND connected', async () => {
|
|
92
|
+
const memory = await metadataClears(new InMemoryPersistenceAdapter())
|
|
93
|
+
const convex = await metadataClears((await convexBackend()).adapter)
|
|
94
|
+
const expected = {
|
|
95
|
+
focalPoint: null,
|
|
96
|
+
alt: null,
|
|
97
|
+
title: 'Hero',
|
|
98
|
+
thumb: THUMB,
|
|
99
|
+
lqip: LQIP,
|
|
100
|
+
meta: { alt: 'Harbour', legacyKey: 'kept' },
|
|
101
|
+
}
|
|
102
|
+
expect({ memory, convex }).toEqual({ memory: expected, convex: expected })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('a metadata write to a deleted asset is refused, never re-creates it', async () => {
|
|
106
|
+
for (const adapter of [new InMemoryPersistenceAdapter(), (await convexBackend()).adapter]) {
|
|
107
|
+
const result = await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_gone', {
|
|
108
|
+
focalPoint: { x: 0.5, y: 0.5 },
|
|
109
|
+
})
|
|
110
|
+
expect(result.ok).toBe(false)
|
|
111
|
+
if (!result.ok) expect(result.message).toContain('No asset “asset_gone” is stored')
|
|
112
|
+
expect(await adapter.assets.findAsset(PROJECT_ID, 'asset_gone')).toBeUndefined()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('the preview backfill writes only the variants: an alt saved mid-run survives', async () => {
|
|
117
|
+
for (const adapter of [new InMemoryPersistenceAdapter(), (await convexBackend()).adapter]) {
|
|
118
|
+
await adapter.assets.upsertAsset(PROJECT_ID, {
|
|
119
|
+
id: 'asset_big',
|
|
120
|
+
filename: 'big.jpg',
|
|
121
|
+
contentType: 'image/jpeg',
|
|
122
|
+
byteSize: 10,
|
|
123
|
+
width: 4000,
|
|
124
|
+
height: 3000,
|
|
125
|
+
storageKey: 'blob_big',
|
|
126
|
+
})
|
|
127
|
+
const blobs = {
|
|
128
|
+
async get() {
|
|
129
|
+
// An editor saves alt while the backfill is fetching this original — the
|
|
130
|
+
// backfill's write lands after it, from a listing taken before it.
|
|
131
|
+
await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_big', { alt: 'Mid-run alt' })
|
|
132
|
+
return new Uint8Array([1])
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
const progress = await runVariantBackfill(
|
|
136
|
+
{
|
|
137
|
+
assets: adapter.assets,
|
|
138
|
+
blobs: blobs as never,
|
|
139
|
+
generate: async () => ({ thumb: THUMB, lqip: LQIP }),
|
|
140
|
+
},
|
|
141
|
+
PROJECT_ID,
|
|
142
|
+
)
|
|
143
|
+
expect(progress.generated).toBe(1)
|
|
144
|
+
const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_big')
|
|
145
|
+
expect([after.alt, after.thumb, after.lqip]).toEqual(['Mid-run alt', THUMB, LQIP])
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('EC-577 (D13/F24) — the one asset row → record projection on the Convex delivery route', () => {
|
|
151
|
+
// EC-574 deleted the second route (the `createContent` kit); the snapshot is the
|
|
152
|
+
// remaining reader of the projection, and this pins the delivery cut it serves.
|
|
153
|
+
it('delivery:snapshot delivers the asset with its lqip and without editorial columns', async () => {
|
|
154
|
+
const { t, adapter } = await convexBackend()
|
|
155
|
+
await adapter.assets.upsertAsset(PROJECT_ID, {
|
|
156
|
+
id: 'asset_p',
|
|
157
|
+
filename: 'p.png',
|
|
158
|
+
contentType: 'image/png',
|
|
159
|
+
byteSize: 1,
|
|
160
|
+
url: 'https://assets.test/p.png',
|
|
161
|
+
title: 'Library title',
|
|
162
|
+
thumb: THUMB,
|
|
163
|
+
lqip: LQIP,
|
|
164
|
+
})
|
|
165
|
+
await adapter.cms.upsertDocument(PROJECT_ID, {
|
|
166
|
+
collection: 'post',
|
|
167
|
+
id: 'one',
|
|
168
|
+
values: { cover: { type: 'asset', id: 'asset_p' } },
|
|
169
|
+
})
|
|
170
|
+
const snapshot = (await t.query(makeFunctionReference<'query'>('delivery:snapshot'), {
|
|
171
|
+
projectId: PROJECT_ID,
|
|
172
|
+
})) as { assets: Record<string, unknown>[] }
|
|
173
|
+
// The snapshot is the positive control: it always carried the lqip.
|
|
174
|
+
expect(snapshot.assets[0]?.lqip).toBe(LQIP)
|
|
175
|
+
// Editorial-only columns stay off the delivery wire (EC-400 thumb; the library title).
|
|
176
|
+
expect(snapshot.assets[0]).not.toHaveProperty('thumb')
|
|
177
|
+
expect(snapshot.assets[0]).not.toHaveProperty('title')
|
|
178
|
+
expect(snapshot.assets[0]).toMatchObject({ url: 'https://assets.test/p.png' })
|
|
179
|
+
})
|
|
180
|
+
})
|
package/convex/redirects.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { v } from 'convex/values'
|
|
1
|
+
import { ConvexError, v } from 'convex/values'
|
|
2
|
+
import type { DocumentByName, WithoutSystemFields } from 'convex/server'
|
|
3
|
+
import { redirectRowSchema, rejectedWriteMessage, resolveRecordWrite } from '@elytracms/core/persistence'
|
|
4
|
+
import type { DataModel } from './functions'
|
|
2
5
|
import { mutation, query } from './functions'
|
|
3
6
|
import { requireCapability } from './guard'
|
|
4
7
|
|
|
8
|
+
type RedirectDoc = DocumentByName<DataModel, 'redirects'>
|
|
9
|
+
|
|
5
10
|
/**
|
|
6
11
|
* Editorial redirects persistence (EC-303). Redirects are data an editor owns —
|
|
7
12
|
* saved+enabled entries go live dynamically (no deploy) via the delivery
|
|
@@ -10,30 +15,47 @@ import { requireCapability } from './guard'
|
|
|
10
15
|
* are content, not administration.
|
|
11
16
|
*/
|
|
12
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Create or modify a redirect (EC-577). The ONE write rule (`resolveRecordWrite`,
|
|
20
|
+
* shared with the in-memory adapter) runs here, inside the transaction: a column
|
|
21
|
+
* absent from the args keeps its stored value — `permanent`/`enabled` included,
|
|
22
|
+
* whose defaults (the record schema's `.default(true)`) apply only when this save
|
|
23
|
+
* creates the redirect — `null` clears the optional `meta` bag, a value sets. ONE
|
|
24
|
+
* `db.patch` or ONE `db.insert`, so the EC-510 revalidation trigger fires once.
|
|
25
|
+
*/
|
|
13
26
|
export const save = mutation({
|
|
14
27
|
args: {
|
|
15
28
|
redirectId: v.string(),
|
|
16
|
-
from: v.string(),
|
|
17
|
-
to: v.string(),
|
|
18
|
-
permanent: v.boolean(),
|
|
19
|
-
enabled: v.boolean(),
|
|
20
|
-
// Schema-driven additive-field bag (EC-311 Tier 2)
|
|
21
|
-
//
|
|
29
|
+
from: v.optional(v.string()),
|
|
30
|
+
to: v.optional(v.string()),
|
|
31
|
+
permanent: v.optional(v.boolean()),
|
|
32
|
+
enabled: v.optional(v.boolean()),
|
|
33
|
+
// Schema-driven additive-field bag (EC-311 Tier 2): present replaces the bag,
|
|
34
|
+
// `null` clears it, absent keeps it.
|
|
22
35
|
meta: v.optional(v.any()),
|
|
23
36
|
createdAt: v.string(),
|
|
24
37
|
updatedAt: v.string(),
|
|
25
38
|
},
|
|
26
|
-
handler: async (ctx,
|
|
39
|
+
handler: async (ctx, { redirectId, createdAt, updatedAt, ...write }) => {
|
|
27
40
|
await requireCapability(ctx, 'content.write')
|
|
28
41
|
const existing = await ctx.db
|
|
29
42
|
.query('redirects')
|
|
30
|
-
.withIndex('by_redirect', (q) => q.eq('redirectId',
|
|
43
|
+
.withIndex('by_redirect', (q) => q.eq('redirectId', redirectId))
|
|
31
44
|
.first()
|
|
45
|
+
const resolved = resolveRecordWrite(redirectRowSchema, existing ?? undefined, write)
|
|
46
|
+
if (!resolved.ok) {
|
|
47
|
+
throw new ConvexError({
|
|
48
|
+
code: 'validation',
|
|
49
|
+
message: rejectedWriteMessage('redirect', redirectId, resolved.issues),
|
|
50
|
+
issues: resolved.issues,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
32
53
|
if (existing) {
|
|
33
|
-
await ctx.db.patch(existing._id, { ...
|
|
54
|
+
await ctx.db.patch(existing._id, { ...(resolved.patch as Partial<RedirectDoc>), updatedAt })
|
|
34
55
|
return ctx.db.get(existing._id)
|
|
35
56
|
}
|
|
36
|
-
const
|
|
57
|
+
const row: WithoutSystemFields<RedirectDoc> = { ...resolved.row, redirectId, createdAt, updatedAt }
|
|
58
|
+
const id = await ctx.db.insert('redirects', row)
|
|
37
59
|
return ctx.db.get(id)
|
|
38
60
|
},
|
|
39
61
|
})
|
package/convex/revalidate.ts
CHANGED
|
@@ -19,17 +19,19 @@ const webhooksDeliver = makeFunctionReference<'action'>('webhooks:deliver')
|
|
|
19
19
|
* var. An unconfigured deployment (local dev, no env) is a silent no-op —
|
|
20
20
|
* publishing must never depend on a reachable host.
|
|
21
21
|
*
|
|
22
|
-
* Called from the EC-286 write-chokepoint triggers only
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* saved+enabled redirect IS the live state, with no draft/published split).
|
|
22
|
+
* Called from the EC-286 write-chokepoint triggers only, so every writer of a
|
|
23
|
+
* live-wire table is covered by construction. Which tables, and which of their
|
|
24
|
+
* writes, is declared once — in `triggers.ts`, not here.
|
|
26
25
|
*
|
|
27
26
|
* Doc/collection-scoped tags (matching the client dispatcher's
|
|
28
27
|
* `webhookTagsForEvent`) would be more surgical for DOCUMENTS, but need the
|
|
29
28
|
* project locales — not available in the Convex env here. The sweep is the
|
|
30
29
|
* honest, reliable v1; threading scoped tags is a precision follow-up. For
|
|
31
30
|
* redirects the sweep is not a compromise but the correct answer — see
|
|
32
|
-
* `ROUTING_OPERATIONS` in `src/lib/webhooks/tags.ts`.
|
|
31
|
+
* `ROUTING_OPERATIONS` in `src/lib/webhooks/tags.ts`. For assets it is the only
|
|
32
|
+
* correct answer today: a page reading an asset through a document asset field
|
|
33
|
+
* carries no `asset:<id>` tag (EC-566 D16), so the per-asset tag the browser
|
|
34
|
+
* dispatcher emits would miss it.
|
|
33
35
|
*/
|
|
34
36
|
export async function scheduleRevalidate(ctx: MutationCtx, event: string): Promise<void> {
|
|
35
37
|
const env =
|
package/convex/triggers.ts
CHANGED
|
@@ -23,9 +23,22 @@ import { scheduleRevalidate } from './revalidate'
|
|
|
23
23
|
*
|
|
24
24
|
* EC-287 retired the DB-resident project graph and its publish path, so
|
|
25
25
|
* `cmsDocuments` is the SINGLE document trigger — one publish path, the
|
|
26
|
-
* dual-path footgun gone at the source.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* dual-path footgun gone at the source.
|
|
27
|
+
*
|
|
28
|
+
* **Which writes reach the host** — every table the delivery snapshot
|
|
29
|
+
* (`delivery.ts`) projects onto the live wire is registered here, and nowhere
|
|
30
|
+
* else decides it:
|
|
31
|
+
*
|
|
32
|
+
* - `cmsDocuments` — only when the live pin (`publishedVersion`) moves;
|
|
33
|
+
* - `redirects` (EC-510) — every write (the row IS the live state);
|
|
34
|
+
* - `assets` (EC-579) — every edit and delete; a create fires nothing.
|
|
35
|
+
*
|
|
36
|
+
* The snapshot reads one more table, `documentVersions`, but only the version a
|
|
37
|
+
* `cmsDocuments` row pins — version rows are insert-only (`cms.ts`) and deleted
|
|
38
|
+
* only with their document, so they reach the wire through a pin move and need
|
|
39
|
+
* no trigger of their own. The rest (`members`, `invites`, `authorityAudit`,
|
|
40
|
+
* `referenceEntries`, `referenceIndexMarker`, the auth tables) never reach the
|
|
41
|
+
* wire. A table the snapshot starts projecting belongs in the list above.
|
|
29
42
|
*/
|
|
30
43
|
export const triggers = new Triggers<DataModel>()
|
|
31
44
|
|
|
@@ -92,3 +105,40 @@ triggers.register('cmsDocuments', async (ctx, change) => {
|
|
|
92
105
|
triggers.register('redirects', async (ctx, change) => {
|
|
93
106
|
await scheduleRevalidate(ctx, change.operation === 'delete' ? 'redirects.remove' : 'redirects.save')
|
|
94
107
|
})
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Asset records (EC-579). The delivery snapshot projects every asset row onto
|
|
111
|
+
* the live wire field by field (url, dimensions, alt, focal point, lqip, the
|
|
112
|
+
* `meta` bag), and like a redirect there is no draft/published split: an alt,
|
|
113
|
+
* focal-point or re-upload edit IS live output the moment it commits. Before
|
|
114
|
+
* this, the only emitter for an asset edit was the studio's browser dispatcher
|
|
115
|
+
* (`src/lib/webhooks`, active only when the studio bundle carries
|
|
116
|
+
* `VITE_REVALIDATE_*`), so a deployment revalidating through this server path
|
|
117
|
+
* alone — and any writer other than that browser — changed the live site
|
|
118
|
+
* without the host ever hearing of it (EC-566 M5; measured by the Codex
|
|
119
|
+
* review, P07).
|
|
120
|
+
*
|
|
121
|
+
* - **Update / delete → the whole-project sweep.** Not `asset:<id>`: the host
|
|
122
|
+
* stamps that tag only on assets a page bakes through its composition, rich
|
|
123
|
+
* text or OG image (`scopeAssetsToPage` in `@elytracms/next` `route-core.ts`);
|
|
124
|
+
* an asset read through a document asset FIELD puts no tag on the page, so a
|
|
125
|
+
* per-asset tag would leave exactly those pages stale (EC-566 D16 / Codex F14).
|
|
126
|
+
* The sweep tag is on every cache entry, so it is correct by construction.
|
|
127
|
+
* - **Insert → nothing.** A brand-new asset id appears on no page yet — the
|
|
128
|
+
* same rule the browser dispatcher applies to an `assets.upsert` create.
|
|
129
|
+
*
|
|
130
|
+
* Every update fires, including one that changed nothing on the wire (the
|
|
131
|
+
* editorial-only `thumb` variant, or a no-op re-save): the trigger cannot
|
|
132
|
+
* cheaply prove the delivery projection unchanged, and deciding it here would
|
|
133
|
+
* be a second declaration of which asset fields ride the wire (`delivery.ts`
|
|
134
|
+
* owns that). Asset saves are explicit actions — the detail surface's Save,
|
|
135
|
+
* the options dialog's close, a focal-point release — not keystrokes; the one
|
|
136
|
+
* bulk writer is the rail's "Generate previews" backfill, one sweep per asset.
|
|
137
|
+
*
|
|
138
|
+
* The event names are the operation names (`assets.upsert` / `assets.remove`),
|
|
139
|
+
* matching what the browser dispatcher carries for the same writes.
|
|
140
|
+
*/
|
|
141
|
+
triggers.register('assets', async (ctx, change) => {
|
|
142
|
+
if (change.operation === 'insert') return
|
|
143
|
+
await scheduleRevalidate(ctx, change.operation === 'delete' ? 'assets.remove' : 'assets.upsert')
|
|
144
|
+
})
|
|
@@ -2,7 +2,6 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import type { LucideIcon } from 'lucide-react';
|
|
3
3
|
import type { CmsDocument, CollectionDef, CollectionFieldOverride, CollectionFormMeta, LocaleConfig } from '@elytracms/core/cms-core';
|
|
4
4
|
import type { CustomizationIssue, Project, SectionId } from '@elytracms/core/studio-core';
|
|
5
|
-
import type { InstantiatedKit } from '@elytracms/core/starter-kits';
|
|
6
5
|
import type { HydratedCms } from '@elytracms/studio/lib/operations';
|
|
7
6
|
import { type AppViewContext, type AppViewDeepLink, type AppViewModule, type NavPermission, type SidebarViewNode } from './module.js';
|
|
8
7
|
/**
|
|
@@ -81,7 +80,7 @@ export interface RedirectsViewConfig extends BuiltinViewConfig {
|
|
|
81
80
|
* different in kind: redirects' base is an engine-owned, fixed `CollectionDef`
|
|
82
81
|
* known at config-build time; the assets base is the DEPLOYMENT's own
|
|
83
82
|
* `kind:'asset'` collection (a project's `cms/collections/asset.ts`), only
|
|
84
|
-
* knowable once the deployment resolves (hydrated session →
|
|
83
|
+
* knowable once the deployment resolves (hydrated session → playground
|
|
85
84
|
* config — see {@link resolveAssetsSchema}). So the merge runs at RENDER time
|
|
86
85
|
* (inside `assetsView`'s `CmsSessionGate`), not at `nav.assets(config)` call
|
|
87
86
|
* time: a locked-violation override (e.g. changing a declared field's type)
|
|
@@ -162,7 +161,7 @@ export declare function componentsView(config?: BuiltinViewConfig): AppViewModul
|
|
|
162
161
|
export declare function redirectsView(config?: RedirectsViewConfig): AppViewModule;
|
|
163
162
|
/**
|
|
164
163
|
* Resolve the assets rail's EFFECTIVE schema (EC-319/EC-311): the deployment's
|
|
165
|
-
* OWN `kind:'asset'` collection — hydrated session →
|
|
164
|
+
* OWN `kind:'asset'` collection — hydrated session → playground config,
|
|
166
165
|
* the exact same precedence {@link resolveCmsInitialCollections} gives every
|
|
167
166
|
* other CMS collection — falling back to the engine's `assetCollection` fixture
|
|
168
167
|
* only when nothing resolves (mirroring `CmsProvider`'s own fallback). A mount's
|
|
@@ -188,8 +187,6 @@ export declare function assetsView(config?: AssetsViewConfig): AppViewModule;
|
|
|
188
187
|
* admin/config concern, so it appears/hides with a project's settings surface).
|
|
189
188
|
*/
|
|
190
189
|
export declare function usersView(config?: BuiltinViewConfig): AppViewModule;
|
|
191
|
-
/** The extensions marketplace view. */
|
|
192
|
-
export declare function extensionsView(config?: BuiltinViewConfig): AppViewModule;
|
|
193
190
|
/** The project settings view. */
|
|
194
191
|
export declare function settingsView(config?: BuiltinViewConfig): AppViewModule;
|
|
195
192
|
/** The legacy `builder` redirect — a hidden, routable view (so `/builder?page=` still lands). */
|
|
@@ -274,8 +271,6 @@ export declare const nav: {
|
|
|
274
271
|
readonly redirects: (config?: RedirectsViewConfig) => SidebarViewNode;
|
|
275
272
|
/** Project settings. */
|
|
276
273
|
readonly settings: (config?: BuiltinViewConfig) => SidebarViewNode;
|
|
277
|
-
/** The extensions marketplace. */
|
|
278
|
-
readonly extensions: (config?: BuiltinViewConfig) => SidebarViewNode;
|
|
279
274
|
/** The admin users roster (EC-240). */
|
|
280
275
|
readonly users: (config?: BuiltinViewConfig) => SidebarViewNode;
|
|
281
276
|
/** A fully custom view — pass your own `render` (and optional `parsePath`). */
|
|
@@ -302,11 +297,12 @@ export declare const nav: {
|
|
|
302
297
|
* section rail reads `resolveDeploymentCollections()` directly, and now so does
|
|
303
298
|
* the store's last rung, so the two can no longer disagree.
|
|
304
299
|
*/
|
|
305
|
-
export declare function resolveCmsInitialCollections(cms: HydratedCms | null
|
|
300
|
+
export declare function resolveCmsInitialCollections(cms: HydratedCms | null): CollectionDef[] | undefined;
|
|
306
301
|
/**
|
|
307
302
|
* The CMS documents source for a project mount — the document-side twin of
|
|
308
|
-
* {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then
|
|
309
|
-
* kit's imported documents,
|
|
303
|
+
* {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then
|
|
304
|
+
* **nothing**. (The middle rung, a starter kit's imported documents, went with
|
|
305
|
+
* the kits — EC-574; K rebuilds starter kits as seeds, EC-200/EC-475.)
|
|
310
306
|
*
|
|
311
307
|
* EC-479 — the third rung used to be the engine's own `sampleDocuments` fixture,
|
|
312
308
|
* reconciled against the project's schema (EC-424). Reconciling it was already an
|
|
@@ -323,7 +319,7 @@ export declare function resolveCmsInitialCollections(cms: HydratedCms | null, ki
|
|
|
323
319
|
* adapter seeds is `persistence/seed.ts`'s business — EC-475 (project-defined seed
|
|
324
320
|
* content) owns it.
|
|
325
321
|
*/
|
|
326
|
-
export declare function resolveCmsInitialDocuments(cms: HydratedCms | null
|
|
322
|
+
export declare function resolveCmsInitialDocuments(cms: HydratedCms | null): CmsDocument[] | undefined;
|
|
327
323
|
/**
|
|
328
324
|
* The CMS locale config for a project mount (EC-357 finding #3). Locales are
|
|
329
325
|
* structure and structure is code: when the active `elytra.config.ts` project
|
|
@@ -334,13 +330,17 @@ export declare function resolveCmsInitialDocuments(cms: HydratedCms | null, kit:
|
|
|
334
330
|
* showed `EN·/DE` against a `locales: ['de']` config.
|
|
335
331
|
*
|
|
336
332
|
* EC-479/EC-483 — the ladder is now TOTAL, and every rung is the project's own:
|
|
337
|
-
* config-declared locales, then
|
|
338
|
-
*
|
|
339
|
-
* project (`studio-context.tsx`: `configProjects()`), so
|
|
340
|
-
* config-derived too — which is what lets the CMS session
|
|
341
|
-
* of borrowing the engine's `en/de` fixture. The default
|
|
342
|
-
* storage key for every localized value, so a guessed one is
|
|
343
|
-
*
|
|
344
|
-
*
|
|
333
|
+
* config-declared locales, then its settings (a starter kit's locales used to sit
|
|
334
|
+
* between the two; the kits went in EC-574). A `Project` only ever exists for a
|
|
335
|
+
* config-declared project (`studio-context.tsx`: `configProjects()`), so
|
|
336
|
+
* `project.settings` is config-derived too — which is what lets the CMS session
|
|
337
|
+
* require a locale instead of borrowing the engine's `en/de` fixture. The default
|
|
338
|
+
* locale is the literal storage key for every localized value, so a guessed one is
|
|
339
|
+
* a data migration. Both rungs are config-derived and pass the workspace gate
|
|
340
|
+
* (`default-locale-not-in-locales` is an error there), so the value is valid by
|
|
341
|
+
* construction and no reader repairs it. `configLocales` is a parameter purely so
|
|
342
|
+
* the ladder is unit-testable (same seam pattern as
|
|
343
|
+
* {@link resolveCmsInitialCollections}); `cms` is accepted for signature parity
|
|
344
|
+
* with its twins but no longer selects a rung.
|
|
345
345
|
*/
|
|
346
|
-
export declare function resolveCmsInitialLocaleConfig(
|
|
346
|
+
export declare function resolveCmsInitialLocaleConfig(_cms: HydratedCms | null, project: Project, configLocales?: LocaleConfig | undefined): LocaleConfig;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
|
-
import { Blocks, Database, FileCog, Image, LayoutGrid,
|
|
3
|
+
import { Blocks, Database, FileCog, Image, LayoutGrid, Settings as SettingsIcon, Signpost, Users, } from 'lucide-react';
|
|
4
4
|
import { assetCollection, extendCollectionDef, isSingleton, resolveSingletonDocument, } from '@elytracms/core/cms-core';
|
|
5
5
|
import { NavigateOnce, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
|
|
6
6
|
import { Text } from '@elytracms/studio/ui';
|
|
7
7
|
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
8
|
-
import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
|
|
9
8
|
import { resolveDeploymentCollections, resolveDeploymentLocaleConfig } from '@elytracms/studio/lib/workspace';
|
|
10
9
|
import { cmsDeepLink, componentsDeepLink, usersDeepLink, redirectsDeepLink, assetsDeepLink, groupNode, separatorNode, viewNode, } from './module.js';
|
|
11
10
|
import { SectionSurface } from '../section-surface.js';
|
|
@@ -15,7 +14,6 @@ import { AssetsModule } from '../assets/assets-module.js';
|
|
|
15
14
|
import { UsersModule } from '../users/users-module.js';
|
|
16
15
|
import { RedirectsModule } from '../redirects/redirects-module.js';
|
|
17
16
|
import { redirectsCollectionDef } from '../redirects/redirects-schema.js';
|
|
18
|
-
import { ExtensionsView } from '../extensions/extensions-view.js';
|
|
19
17
|
import { RegistryBrowser } from '../components/registry-browser.js';
|
|
20
18
|
import { ComponentsIndex } from '../components/components-index.js';
|
|
21
19
|
import { ComponentDetail } from '../components/component-detail.js';
|
|
@@ -210,7 +208,7 @@ export function redirectsView(config = {}) {
|
|
|
210
208
|
}
|
|
211
209
|
/**
|
|
212
210
|
* Resolve the assets rail's EFFECTIVE schema (EC-319/EC-311): the deployment's
|
|
213
|
-
* OWN `kind:'asset'` collection — hydrated session →
|
|
211
|
+
* OWN `kind:'asset'` collection — hydrated session → playground config,
|
|
214
212
|
* the exact same precedence {@link resolveCmsInitialCollections} gives every
|
|
215
213
|
* other CMS collection — falling back to the engine's `assetCollection` fixture
|
|
216
214
|
* only when nothing resolves (mirroring `CmsProvider`'s own fallback). A mount's
|
|
@@ -243,8 +241,7 @@ export function assetsView(config = {}) {
|
|
|
243
241
|
availabilitySection: 'assets',
|
|
244
242
|
parsePath: assetsDeepLink,
|
|
245
243
|
render: (ctx) => (_jsx(SectionSurface, { project: ctx.project, section: "assets", forcedState: ctx.search.state, children: ctx.deepLink.assetId === null && ctx.search.asset ? (_jsx(NavigateOnce, { to: STUDIO_PROJECT_ROUTE, params: { projectId: ctx.projectId, _splat: studioPath.asset(ctx.search.asset) }, replace: true })) : (_jsx(CmsSessionGate, { children: (cms) => {
|
|
246
|
-
const
|
|
247
|
-
const schema = resolveAssetsSchema(resolveCmsInitialCollections(cms, kit), config);
|
|
244
|
+
const schema = resolveAssetsSchema(resolveCmsInitialCollections(cms), config);
|
|
248
245
|
return (_jsx(AssetsModule, { projectId: ctx.projectId, assetId: ctx.deepLink.assetId ?? null, schema: schema }));
|
|
249
246
|
} })) })),
|
|
250
247
|
};
|
|
@@ -273,21 +270,6 @@ export function usersView(config = {}) {
|
|
|
273
270
|
render: (ctx) => ctx.search.state && ctx.search.state !== 'working' ? (_jsx(SectionStateView, { state: ctx.search.state, title: translate('nav.users.label') })) : (_jsx(UsersModule, { projectId: ctx.projectId, userId: ctx.deepLink.userId ?? null })),
|
|
274
271
|
};
|
|
275
272
|
}
|
|
276
|
-
/** The extensions marketplace view. */
|
|
277
|
-
export function extensionsView(config = {}) {
|
|
278
|
-
return {
|
|
279
|
-
id: 'extensions',
|
|
280
|
-
get label() {
|
|
281
|
-
return config.label ?? translate('nav.extensions.label');
|
|
282
|
-
},
|
|
283
|
-
get description() {
|
|
284
|
-
return translate('nav.extensions.description');
|
|
285
|
-
},
|
|
286
|
-
icon: config.icon ?? Puzzle,
|
|
287
|
-
availabilitySection: 'extensions',
|
|
288
|
-
render: (ctx) => ctx.search.state && ctx.search.state !== 'working' ? (_jsx(SectionStateView, { state: ctx.search.state, title: translate('nav.extensions.label') })) : (_jsx(ExtensionsView, {})),
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
273
|
/** The project settings view. */
|
|
292
274
|
export function settingsView(config = {}) {
|
|
293
275
|
return {
|
|
@@ -355,7 +337,6 @@ export const DEFAULT_VIEWS = [
|
|
|
355
337
|
cmsView(),
|
|
356
338
|
redirectsView(),
|
|
357
339
|
assetsView(),
|
|
358
|
-
extensionsView(),
|
|
359
340
|
usersView(),
|
|
360
341
|
settingsView(),
|
|
361
342
|
builderView(),
|
|
@@ -505,8 +486,6 @@ export const nav = {
|
|
|
505
486
|
redirects: (config = {}) => viewNode(withNavPermission(redirectsView(config), config)),
|
|
506
487
|
/** Project settings. */
|
|
507
488
|
settings: (config = {}) => viewNode(withNavPermission(settingsView(config), config)),
|
|
508
|
-
/** The extensions marketplace. */
|
|
509
|
-
extensions: (config = {}) => viewNode(withNavPermission(extensionsView(config), config)),
|
|
510
489
|
/** The admin users roster (EC-240). */
|
|
511
490
|
users: (config = {}) => viewNode(withNavPermission(usersView(config), config)),
|
|
512
491
|
/** A fully custom view — pass your own `render` (and optional `parsePath`). */
|
|
@@ -545,13 +524,14 @@ function ComponentsScreen({ projectId, componentId, state, }) {
|
|
|
545
524
|
* section rail reads `resolveDeploymentCollections()` directly, and now so does
|
|
546
525
|
* the store's last rung, so the two can no longer disagree.
|
|
547
526
|
*/
|
|
548
|
-
export function resolveCmsInitialCollections(cms
|
|
549
|
-
return cms?.collections ??
|
|
527
|
+
export function resolveCmsInitialCollections(cms) {
|
|
528
|
+
return cms?.collections ?? resolveDeploymentCollections();
|
|
550
529
|
}
|
|
551
530
|
/**
|
|
552
531
|
* The CMS documents source for a project mount — the document-side twin of
|
|
553
|
-
* {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then
|
|
554
|
-
* kit's imported documents,
|
|
532
|
+
* {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then
|
|
533
|
+
* **nothing**. (The middle rung, a starter kit's imported documents, went with
|
|
534
|
+
* the kits — EC-574; K rebuilds starter kits as seeds, EC-200/EC-475.)
|
|
555
535
|
*
|
|
556
536
|
* EC-479 — the third rung used to be the engine's own `sampleDocuments` fixture,
|
|
557
537
|
* reconciled against the project's schema (EC-424). Reconciling it was already an
|
|
@@ -568,8 +548,8 @@ export function resolveCmsInitialCollections(cms, kit) {
|
|
|
568
548
|
* adapter seeds is `persistence/seed.ts`'s business — EC-475 (project-defined seed
|
|
569
549
|
* content) owns it.
|
|
570
550
|
*/
|
|
571
|
-
export function resolveCmsInitialDocuments(cms
|
|
572
|
-
return cms?.documents
|
|
551
|
+
export function resolveCmsInitialDocuments(cms) {
|
|
552
|
+
return cms?.documents;
|
|
573
553
|
}
|
|
574
554
|
/**
|
|
575
555
|
* The CMS locale config for a project mount (EC-357 finding #3). Locales are
|
|
@@ -581,25 +561,26 @@ export function resolveCmsInitialDocuments(cms, kit) {
|
|
|
581
561
|
* showed `EN·/DE` against a `locales: ['de']` config.
|
|
582
562
|
*
|
|
583
563
|
* EC-479/EC-483 — the ladder is now TOTAL, and every rung is the project's own:
|
|
584
|
-
* config-declared locales, then
|
|
585
|
-
*
|
|
586
|
-
* project (`studio-context.tsx`: `configProjects()`), so
|
|
587
|
-
* config-derived too — which is what lets the CMS session
|
|
588
|
-
* of borrowing the engine's `en/de` fixture. The default
|
|
589
|
-
* storage key for every localized value, so a guessed one is
|
|
590
|
-
*
|
|
591
|
-
*
|
|
564
|
+
* config-declared locales, then its settings (a starter kit's locales used to sit
|
|
565
|
+
* between the two; the kits went in EC-574). A `Project` only ever exists for a
|
|
566
|
+
* config-declared project (`studio-context.tsx`: `configProjects()`), so
|
|
567
|
+
* `project.settings` is config-derived too — which is what lets the CMS session
|
|
568
|
+
* require a locale instead of borrowing the engine's `en/de` fixture. The default
|
|
569
|
+
* locale is the literal storage key for every localized value, so a guessed one is
|
|
570
|
+
* a data migration. Both rungs are config-derived and pass the workspace gate
|
|
571
|
+
* (`default-locale-not-in-locales` is an error there), so the value is valid by
|
|
572
|
+
* construction and no reader repairs it. `configLocales` is a parameter purely so
|
|
573
|
+
* the ladder is unit-testable (same seam pattern as
|
|
574
|
+
* {@link resolveCmsInitialCollections}); `cms` is accepted for signature parity
|
|
575
|
+
* with its twins but no longer selects a rung.
|
|
592
576
|
*/
|
|
593
|
-
export function resolveCmsInitialLocaleConfig(
|
|
594
|
-
|
|
577
|
+
export function resolveCmsInitialLocaleConfig(_cms, project, configLocales = resolveDeploymentLocaleConfig()) {
|
|
578
|
+
if (configLocales)
|
|
579
|
+
return configLocales;
|
|
580
|
+
return {
|
|
595
581
|
default: project.settings.defaultLocale,
|
|
596
582
|
locales: [...project.settings.locales],
|
|
597
583
|
};
|
|
598
|
-
if (configLocales)
|
|
599
|
-
return configLocales;
|
|
600
|
-
if (cms)
|
|
601
|
-
return projectSettings;
|
|
602
|
-
return kit?.localeConfig ?? projectSettings;
|
|
603
584
|
}
|
|
604
585
|
/**
|
|
605
586
|
* CMS section: hydrates the persisted (or kit-seeded) collections + documents, then
|
|
@@ -614,10 +595,8 @@ function CmsScreen({ project, collectionId, documentId, state, field, focusColle
|
|
|
614
595
|
if (state && state !== 'working') {
|
|
615
596
|
return _jsx(SectionStateView, { state: state, title: translate('nav.cms.label') });
|
|
616
597
|
}
|
|
617
|
-
// A project created from a starter kit (EC-119) opens the CMS on the kit's schemas + content.
|
|
618
|
-
const kit = getInstantiatedKit(projectId);
|
|
619
598
|
return (_jsx(CmsSessionGate, { children: (cms) => {
|
|
620
|
-
return (_jsx(CmsProvider, { projectId: projectId, initialCollections: resolveCmsInitialCollections(cms
|
|
599
|
+
return (_jsx(CmsProvider, { projectId: projectId, initialCollections: resolveCmsInitialCollections(cms), initialDocuments: resolveCmsInitialDocuments(cms), initialPublishedKeys: cms?.publishedKeys, initialTimestamps: cms?.timestamps, initialPublishTimestamps: cms?.publishTimestamps, initialChangedKeys: cms?.changedKeys, initialLocaleConfig: resolveCmsInitialLocaleConfig(cms, project),
|
|
621
600
|
// EC-461: a hydrated mount (cms non-null — both modes now) seeds the
|
|
622
601
|
// CURRENT persisted state, so staged docs are superseded by it; only
|
|
623
602
|
// a static kit/fixture seed lets staged entries win.
|