create-vexcms 0.0.20 → 0.1.0-alpha.2
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/LICENSE +202 -0
- package/README.md +93 -83
- package/dist/index.js +246 -81
- package/dist/index.mjs +30976 -0
- package/package.json +35 -15
- package/templates/base-nextjs/README.md +53 -205
- package/templates/base-nextjs/_env.example +7 -9
- package/templates/base-nextjs/_gitignore +41 -1
- package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
- package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
- package/templates/base-nextjs/convex/_generated/server.js +8 -0
- package/templates/base-nextjs/convex/auth/api.ts +49 -3
- package/templates/base-nextjs/convex/auth/db.ts +42 -300
- package/templates/base-nextjs/convex/auth/index.ts +11 -7
- package/templates/base-nextjs/convex/auth/options.ts +5 -29
- package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
- package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
- package/templates/base-nextjs/convex/http.ts +18 -3
- package/templates/base-nextjs/convex/schema.ts +104 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
- package/templates/base-nextjs/convex/vex/globals.ts +19 -82
- package/templates/base-nextjs/convex/vex/media.ts +19 -246
- package/templates/base-nextjs/convex/vex.schema.ts +138 -0
- package/templates/base-nextjs/convex/vex.ts +19 -0
- package/templates/base-nextjs/convex.json +6 -0
- package/templates/base-nextjs/eslint.config.mjs +1 -21
- package/templates/base-nextjs/next.config.ts +10 -0
- package/templates/base-nextjs/package.json +47 -48
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
- package/templates/base-nextjs/src/app/globals.css +240 -104
- package/templates/base-nextjs/src/app/layout.tsx +23 -13
- package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
- package/templates/base-nextjs/src/auth/access.ts +46 -0
- package/templates/base-nextjs/src/auth/client.tsx +17 -5
- package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
- package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
- package/templates/base-nextjs/src/components/component-example.tsx +56 -464
- package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
- package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
- package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
- package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
- package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
- package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
- package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
- package/templates/base-nextjs/src/db/constants/index.ts +22 -35
- package/templates/base-nextjs/src/lib/utils.ts +0 -12
- package/templates/base-nextjs/src/proxy.ts +97 -12
- package/templates/base-nextjs/src/vex.config.ts +35 -0
- package/templates/base-nextjs/src/vex.types.ts +293 -0
- package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
- package/templates/base-nextjs/tsconfig.json +3 -0
- package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
- package/templates/marketing-site/convex/_generated/api.js +23 -0
- package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
- package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
- package/templates/marketing-site/convex/_generated/server.js +101 -0
- package/templates/marketing-site/convex/footers.ts +11 -7
- package/templates/marketing-site/convex/headers.ts +15 -5
- package/templates/marketing-site/convex/pages.ts +20 -35
- package/templates/marketing-site/convex/schema.ts +119 -0
- package/templates/marketing-site/convex/seed.ts +582 -35
- package/templates/marketing-site/convex/siteSettings.ts +19 -5
- package/templates/marketing-site/convex/theme.ts +55 -61
- package/templates/marketing-site/convex/vex.schema.ts +355 -0
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
- package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
- package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
- package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
- package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
- package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
- package/templates/marketing-site/src/lib/metadata.ts +51 -72
- package/templates/marketing-site/src/vex.config.ts +35 -0
- package/templates/marketing-site/src/vex.types.ts +634 -0
- package/templates/marketing-site/src/vexcms/api.ts +23 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
- package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
- package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
- package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
- package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
- package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
- package/templates/base-nextjs/convex/auth/config.ts +0 -10
- package/templates/base-nextjs/convex/vex/auth.ts +0 -38
- package/templates/base-nextjs/convex/vex/collections.ts +0 -333
- package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
- package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
- package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
- package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
- package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
- package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
- package/templates/base-nextjs/convex/vex/versions.ts +0 -648
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
- package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
- package/templates/base-nextjs/src/db/types.ts +0 -6
- package/templates/base-nextjs/src/vexcms/access.ts +0 -44
- package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
- package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
- package/templates/base-nextjs/vex.config.ts +0 -25
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
- package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
- package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
- package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
- package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
- package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
- package/templates/marketing-site/src/db/constants/index.ts +0 -52
- package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
- package/templates/marketing-site/src/vexcms/access.ts +0 -36
- package/templates/marketing-site/vex.config.ts +0 -32
|
@@ -1,648 +0,0 @@
|
|
|
1
|
-
import type { DataModel } from "@convex/_generated/dataModel"
|
|
2
|
-
import type { GenericQueryCtx, TableNamesInDataModel } from "convex/server"
|
|
3
|
-
|
|
4
|
-
import { mutation, query } from "@convex/_generated/server"
|
|
5
|
-
import {
|
|
6
|
-
hasPermission,
|
|
7
|
-
extractUserFields,
|
|
8
|
-
DEFAULT_MAX_VERSIONS_PER_DOC,
|
|
9
|
-
} from "@vexcms/core"
|
|
10
|
-
import { ConvexError, v } from "convex/values"
|
|
11
|
-
|
|
12
|
-
import { TABLE_SLUG_USERS } from "~/db/constants"
|
|
13
|
-
|
|
14
|
-
import config from "../../vex.config"
|
|
15
|
-
import { access } from "../../src/vexcms/access"
|
|
16
|
-
import * as Versions from "./model/versions"
|
|
17
|
-
|
|
18
|
-
function requireVersionedCollection(slug: string) {
|
|
19
|
-
const collections = [...config.collections, ...(config.media?.collections ?? []), ...(config.globals ?? [])]
|
|
20
|
-
const match = collections.find((c: any) => c.slug === slug)
|
|
21
|
-
if (!match) {
|
|
22
|
-
throw new ConvexError(`Collection not found: ${slug}`)
|
|
23
|
-
}
|
|
24
|
-
if (!match.versions?.drafts) {
|
|
25
|
-
throw new ConvexError(`Collection "${slug}" does not have versioning enabled`)
|
|
26
|
-
}
|
|
27
|
-
return match
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function getUser(ctx: GenericQueryCtx<DataModel>) {
|
|
31
|
-
const identity = await ctx.auth.getUserIdentity()
|
|
32
|
-
if (!identity?.email) return null
|
|
33
|
-
const user = await ctx.db
|
|
34
|
-
.query(TABLE_SLUG_USERS)
|
|
35
|
-
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
|
36
|
-
.first()
|
|
37
|
-
if (!user) return null
|
|
38
|
-
return {
|
|
39
|
-
user: user as Record<string, unknown>,
|
|
40
|
-
roles: (user.role as string[]) ?? [],
|
|
41
|
-
email: identity.email,
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function requireUser(ctx: GenericQueryCtx<DataModel>) {
|
|
46
|
-
const result = await getUser(ctx)
|
|
47
|
-
if (!result) throw new ConvexError("Not authenticated")
|
|
48
|
-
return result
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function checkPermission(props: Parameters<typeof hasPermission>[0]) {
|
|
52
|
-
const result = hasPermission(props)
|
|
53
|
-
const denied =
|
|
54
|
-
result === false ||
|
|
55
|
-
(typeof result === "object" && !Object.values(result).some(Boolean))
|
|
56
|
-
if (denied) {
|
|
57
|
-
throw new ConvexError(`Access denied: "${props.action}" on "${props.resource}"`)
|
|
58
|
-
}
|
|
59
|
-
return result
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Creates a new document in a versioned collection.
|
|
64
|
-
* The document starts with vex_status: "draft" and an initial version is created.
|
|
65
|
-
*/
|
|
66
|
-
export const createDraftDocument = mutation({
|
|
67
|
-
args: {
|
|
68
|
-
collectionSlug: v.string(),
|
|
69
|
-
fields: v.any(),
|
|
70
|
-
},
|
|
71
|
-
handler: async (ctx, { collectionSlug, fields }) => {
|
|
72
|
-
const collection = requireVersionedCollection(collectionSlug)
|
|
73
|
-
const { user, roles, email } = await requireUser(ctx)
|
|
74
|
-
|
|
75
|
-
checkPermission({
|
|
76
|
-
access,
|
|
77
|
-
user,
|
|
78
|
-
userRoles: roles,
|
|
79
|
-
resource: collectionSlug,
|
|
80
|
-
action: "create",
|
|
81
|
-
data: fields as Record<string, unknown>,
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
const validatedFields = fields as Record<string, unknown>
|
|
85
|
-
|
|
86
|
-
const tableName = (collection.tableName ?? collectionSlug) as TableNamesInDataModel<DataModel>
|
|
87
|
-
const newDocId = await (ctx.db as any).insert(tableName, {
|
|
88
|
-
...validatedFields,
|
|
89
|
-
vex_status: "draft",
|
|
90
|
-
vex_version: 1,
|
|
91
|
-
vex_publishedAt: undefined,
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
await Versions.createVersion<DataModel>({
|
|
95
|
-
ctx,
|
|
96
|
-
collection: collectionSlug,
|
|
97
|
-
documentId: newDocId as string,
|
|
98
|
-
snapshot: validatedFields,
|
|
99
|
-
status: "draft",
|
|
100
|
-
createdBy: email,
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
return { documentId: newDocId as string, version: 1 }
|
|
104
|
-
},
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Saves a draft version without publishing.
|
|
109
|
-
* Creates a new version record in vex_versions.
|
|
110
|
-
* Does NOT update the main document's content fields.
|
|
111
|
-
*/
|
|
112
|
-
export const saveDraft = mutation({
|
|
113
|
-
args: {
|
|
114
|
-
collectionSlug: v.string(),
|
|
115
|
-
documentId: v.string(),
|
|
116
|
-
fields: v.any(),
|
|
117
|
-
restoredFrom: v.optional(v.number()),
|
|
118
|
-
},
|
|
119
|
-
handler: async (ctx, { collectionSlug, documentId, fields, restoredFrom }) => {
|
|
120
|
-
const collection = requireVersionedCollection(collectionSlug)
|
|
121
|
-
const { user, roles, email } = await requireUser(ctx)
|
|
122
|
-
|
|
123
|
-
checkPermission({
|
|
124
|
-
access,
|
|
125
|
-
user,
|
|
126
|
-
userRoles: roles,
|
|
127
|
-
resource: collectionSlug,
|
|
128
|
-
action: "update",
|
|
129
|
-
data: fields as Record<string, unknown>,
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
const doc = await ctx.db.get(documentId as any)
|
|
133
|
-
if (!doc) throw new ConvexError("Document not found")
|
|
134
|
-
|
|
135
|
-
// If no versions exist yet, snapshot the current main doc as v1 published
|
|
136
|
-
// (preserves the original state before any edits)
|
|
137
|
-
const latestVersion = await Versions.getLatestVersion<DataModel>({
|
|
138
|
-
ctx,
|
|
139
|
-
collection: collectionSlug,
|
|
140
|
-
documentId,
|
|
141
|
-
})
|
|
142
|
-
if (!latestVersion) {
|
|
143
|
-
const originalSnapshot = extractUserFields({
|
|
144
|
-
document: doc as Record<string, unknown>,
|
|
145
|
-
})
|
|
146
|
-
await Versions.createVersion<DataModel>({
|
|
147
|
-
ctx,
|
|
148
|
-
collection: collectionSlug,
|
|
149
|
-
documentId,
|
|
150
|
-
snapshot: originalSnapshot,
|
|
151
|
-
status: "published",
|
|
152
|
-
createdBy: null,
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// The frontend sends ALL field values as a complete snapshot
|
|
157
|
-
const snapshot = fields as Record<string, unknown>
|
|
158
|
-
|
|
159
|
-
const { version } = await Versions.createVersion<DataModel>({
|
|
160
|
-
ctx,
|
|
161
|
-
collection: collectionSlug,
|
|
162
|
-
documentId,
|
|
163
|
-
snapshot,
|
|
164
|
-
status: "draft",
|
|
165
|
-
createdBy: email,
|
|
166
|
-
restoredFrom,
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
await Versions.cleanupOldVersions<DataModel>({
|
|
170
|
-
ctx,
|
|
171
|
-
collection: collectionSlug,
|
|
172
|
-
documentId,
|
|
173
|
-
maxPerDoc: collection.versions?.maxPerDoc ?? DEFAULT_MAX_VERSIONS_PER_DOC,
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
return { version }
|
|
177
|
-
},
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Publishes the latest draft version.
|
|
182
|
-
* Copies the latest version's snapshot to the main document's fields,
|
|
183
|
-
* sets vex_status to "published", and creates a published version record.
|
|
184
|
-
*/
|
|
185
|
-
export const publish = mutation({
|
|
186
|
-
args: {
|
|
187
|
-
collectionSlug: v.string(),
|
|
188
|
-
documentId: v.string(),
|
|
189
|
-
fields: v.optional(v.any()),
|
|
190
|
-
},
|
|
191
|
-
handler: async (ctx, { collectionSlug, documentId, fields }) => {
|
|
192
|
-
const collection = requireVersionedCollection(collectionSlug)
|
|
193
|
-
const { user, roles, email } = await requireUser(ctx)
|
|
194
|
-
|
|
195
|
-
checkPermission({
|
|
196
|
-
access,
|
|
197
|
-
user,
|
|
198
|
-
userRoles: roles,
|
|
199
|
-
resource: collectionSlug,
|
|
200
|
-
action: "update",
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
const doc = await ctx.db.get(documentId as any)
|
|
204
|
-
if (!doc) throw new ConvexError("Document not found")
|
|
205
|
-
|
|
206
|
-
// Check if this document has any version history yet
|
|
207
|
-
const latestVersion = await Versions.getLatestVersion<DataModel>({
|
|
208
|
-
ctx,
|
|
209
|
-
collection: collectionSlug,
|
|
210
|
-
documentId,
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
// If no versions exist, snapshot the current main doc as v1 published
|
|
214
|
-
// (preserves the original state before any edits)
|
|
215
|
-
if (!latestVersion) {
|
|
216
|
-
const originalSnapshot = extractUserFields({
|
|
217
|
-
document: doc as Record<string, unknown>,
|
|
218
|
-
})
|
|
219
|
-
await Versions.createVersion<DataModel>({
|
|
220
|
-
ctx,
|
|
221
|
-
collection: collectionSlug,
|
|
222
|
-
documentId,
|
|
223
|
-
snapshot: originalSnapshot,
|
|
224
|
-
status: "published",
|
|
225
|
-
createdBy: null,
|
|
226
|
-
})
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
let snapshot: Record<string, unknown>
|
|
230
|
-
|
|
231
|
-
if (fields) {
|
|
232
|
-
snapshot = fields as Record<string, unknown>
|
|
233
|
-
} else if (latestVersion) {
|
|
234
|
-
snapshot = latestVersion.snapshot as Record<string, unknown>
|
|
235
|
-
} else {
|
|
236
|
-
// No fields provided and no prior versions — republish current doc as-is
|
|
237
|
-
snapshot = extractUserFields({
|
|
238
|
-
document: doc as Record<string, unknown>,
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// If the latest version is already published with the same snapshot,
|
|
243
|
-
// don't create a duplicate — just ensure the main doc is up to date
|
|
244
|
-
if (
|
|
245
|
-
latestVersion &&
|
|
246
|
-
(latestVersion as any).status === "published" &&
|
|
247
|
-
JSON.stringify((latestVersion as any).snapshot) === JSON.stringify(snapshot)
|
|
248
|
-
) {
|
|
249
|
-
const existingVersion = (latestVersion as any).version as number
|
|
250
|
-
// Ensure main doc reflects published state
|
|
251
|
-
if ((doc as any).vex_status !== "published") {
|
|
252
|
-
await (ctx.db as any).patch(documentId as any, {
|
|
253
|
-
vex_status: "published",
|
|
254
|
-
vex_publishedAt: Date.now(),
|
|
255
|
-
vex_version: existingVersion,
|
|
256
|
-
})
|
|
257
|
-
}
|
|
258
|
-
return { version: existingVersion }
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// If the latest version is a draft with the same snapshot, promote it
|
|
262
|
-
// to published instead of creating a new version entry
|
|
263
|
-
if (
|
|
264
|
-
latestVersion &&
|
|
265
|
-
(latestVersion as any).status === "draft" &&
|
|
266
|
-
JSON.stringify((latestVersion as any).snapshot) === JSON.stringify(snapshot)
|
|
267
|
-
) {
|
|
268
|
-
const existingVersion = (latestVersion as any).version as number
|
|
269
|
-
await (ctx.db as any).patch((latestVersion as any)._id, {
|
|
270
|
-
status: "published",
|
|
271
|
-
createdAt: Date.now(),
|
|
272
|
-
})
|
|
273
|
-
await (ctx.db as any).patch(documentId as any, {
|
|
274
|
-
...snapshot,
|
|
275
|
-
vex_status: "published",
|
|
276
|
-
vex_publishedAt: Date.now(),
|
|
277
|
-
vex_version: existingVersion,
|
|
278
|
-
})
|
|
279
|
-
return { version: existingVersion }
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Create a new published version record
|
|
283
|
-
const { version } = await Versions.createVersion<DataModel>({
|
|
284
|
-
ctx,
|
|
285
|
-
collection: collectionSlug,
|
|
286
|
-
documentId,
|
|
287
|
-
snapshot,
|
|
288
|
-
status: "published",
|
|
289
|
-
createdBy: email,
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
// Update the main document
|
|
293
|
-
await (ctx.db as any).patch(documentId as any, {
|
|
294
|
-
...snapshot,
|
|
295
|
-
vex_status: "published",
|
|
296
|
-
vex_publishedAt: Date.now(),
|
|
297
|
-
vex_version: version,
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
await Versions.cleanupOldVersions<DataModel>({
|
|
301
|
-
ctx,
|
|
302
|
-
collection: collectionSlug,
|
|
303
|
-
documentId,
|
|
304
|
-
maxPerDoc: collection.versions?.maxPerDoc ?? DEFAULT_MAX_VERSIONS_PER_DOC,
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
return { version }
|
|
308
|
-
},
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Unpublishes a document by setting vex_status back to "draft".
|
|
313
|
-
*/
|
|
314
|
-
export const unpublish = mutation({
|
|
315
|
-
args: {
|
|
316
|
-
collectionSlug: v.string(),
|
|
317
|
-
documentId: v.string(),
|
|
318
|
-
},
|
|
319
|
-
handler: async (ctx, { collectionSlug, documentId }) => {
|
|
320
|
-
requireVersionedCollection(collectionSlug)
|
|
321
|
-
const { user, roles, email } = await requireUser(ctx)
|
|
322
|
-
|
|
323
|
-
checkPermission({
|
|
324
|
-
access,
|
|
325
|
-
user,
|
|
326
|
-
userRoles: roles,
|
|
327
|
-
resource: collectionSlug,
|
|
328
|
-
action: "update",
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
const doc = await ctx.db.get(documentId as any)
|
|
332
|
-
if (!doc) throw new ConvexError("Document not found")
|
|
333
|
-
|
|
334
|
-
if ((doc as any).vex_status !== "published") {
|
|
335
|
-
throw new ConvexError("Document is not published")
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Patch the main document status back to draft
|
|
339
|
-
await (ctx.db as any).patch(documentId as any, {
|
|
340
|
-
vex_status: "draft",
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
// Patch the latest published version back to draft instead of creating a new one
|
|
344
|
-
const latestVersion = await Versions.getLatestVersion<DataModel>({
|
|
345
|
-
ctx,
|
|
346
|
-
collection: collectionSlug,
|
|
347
|
-
documentId,
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
if (latestVersion && (latestVersion as any).status === "published") {
|
|
351
|
-
await (ctx.db as any).patch((latestVersion as any)._id, {
|
|
352
|
-
status: "draft",
|
|
353
|
-
})
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return { version: (latestVersion as any)?.version ?? 0 }
|
|
357
|
-
},
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Autosave — coalesces into a single autosave version record.
|
|
362
|
-
*/
|
|
363
|
-
export const autosave = mutation({
|
|
364
|
-
args: {
|
|
365
|
-
collectionSlug: v.string(),
|
|
366
|
-
documentId: v.string(),
|
|
367
|
-
fields: v.any(),
|
|
368
|
-
},
|
|
369
|
-
handler: async (ctx, { collectionSlug, documentId, fields }) => {
|
|
370
|
-
requireVersionedCollection(collectionSlug)
|
|
371
|
-
const { user, roles, email } = await requireUser(ctx)
|
|
372
|
-
|
|
373
|
-
checkPermission({
|
|
374
|
-
access,
|
|
375
|
-
user,
|
|
376
|
-
userRoles: roles,
|
|
377
|
-
resource: collectionSlug,
|
|
378
|
-
action: "update",
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
const doc = await ctx.db.get(documentId as any)
|
|
382
|
-
if (!doc) throw new ConvexError("Document not found")
|
|
383
|
-
|
|
384
|
-
const validatedFields = fields as Record<string, unknown>
|
|
385
|
-
|
|
386
|
-
// Get latest version to merge snapshot
|
|
387
|
-
const latestVersion = await Versions.getLatestVersion<DataModel>({
|
|
388
|
-
ctx,
|
|
389
|
-
collection: collectionSlug,
|
|
390
|
-
documentId,
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
const mergedSnapshot = {
|
|
394
|
-
...((latestVersion?.snapshot as Record<string, unknown>) ?? {}),
|
|
395
|
-
...validatedFields,
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const { version } = await Versions.coalesceAutosave<DataModel>({
|
|
399
|
-
ctx,
|
|
400
|
-
collection: collectionSlug,
|
|
401
|
-
documentId,
|
|
402
|
-
snapshot: mergedSnapshot,
|
|
403
|
-
createdBy: email,
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
return { version }
|
|
407
|
-
},
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Gets a version's snapshot for restoring into the edit form.
|
|
412
|
-
* Does NOT create a new version — the user must explicitly save
|
|
413
|
-
* after reviewing the restored values in the form.
|
|
414
|
-
*/
|
|
415
|
-
export const getVersionSnapshot = query({
|
|
416
|
-
args: {
|
|
417
|
-
collectionSlug: v.string(),
|
|
418
|
-
documentId: v.string(),
|
|
419
|
-
version: v.number(),
|
|
420
|
-
},
|
|
421
|
-
handler: async (ctx, { collectionSlug, documentId, version: versionNum }) => {
|
|
422
|
-
requireVersionedCollection(collectionSlug)
|
|
423
|
-
|
|
424
|
-
const targetVersion = await Versions.getVersion<DataModel>({
|
|
425
|
-
ctx,
|
|
426
|
-
collection: collectionSlug,
|
|
427
|
-
documentId,
|
|
428
|
-
version: versionNum,
|
|
429
|
-
})
|
|
430
|
-
if (!targetVersion) throw new ConvexError("Version not found")
|
|
431
|
-
|
|
432
|
-
return {
|
|
433
|
-
version: targetVersion.version as number,
|
|
434
|
-
snapshot: targetVersion.snapshot as Record<string, unknown>,
|
|
435
|
-
}
|
|
436
|
-
},
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Permanently deletes a specific version record.
|
|
441
|
-
*/
|
|
442
|
-
export const deleteVersion = mutation({
|
|
443
|
-
args: {
|
|
444
|
-
collectionSlug: v.string(),
|
|
445
|
-
documentId: v.string(),
|
|
446
|
-
version: v.number(),
|
|
447
|
-
},
|
|
448
|
-
handler: async (ctx, { collectionSlug, documentId, version: versionNum }) => {
|
|
449
|
-
requireVersionedCollection(collectionSlug)
|
|
450
|
-
const { user, roles } = await requireUser(ctx)
|
|
451
|
-
|
|
452
|
-
checkPermission({
|
|
453
|
-
access,
|
|
454
|
-
user,
|
|
455
|
-
userRoles: roles,
|
|
456
|
-
resource: collectionSlug,
|
|
457
|
-
action: "update",
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
const targetVersion = await Versions.getVersion<DataModel>({
|
|
461
|
-
ctx,
|
|
462
|
-
collection: collectionSlug,
|
|
463
|
-
documentId,
|
|
464
|
-
version: versionNum,
|
|
465
|
-
})
|
|
466
|
-
if (!targetVersion) throw new ConvexError("Version not found")
|
|
467
|
-
|
|
468
|
-
await (ctx.db as any).delete(targetVersion._id)
|
|
469
|
-
|
|
470
|
-
return { deleted: versionNum }
|
|
471
|
-
},
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Lists version history for a document.
|
|
476
|
-
*/
|
|
477
|
-
export const listVersions = query({
|
|
478
|
-
args: {
|
|
479
|
-
collectionSlug: v.string(),
|
|
480
|
-
documentId: v.string(),
|
|
481
|
-
limit: v.optional(v.number()),
|
|
482
|
-
},
|
|
483
|
-
handler: async (ctx, { collectionSlug, documentId, limit }) => {
|
|
484
|
-
requireVersionedCollection(collectionSlug)
|
|
485
|
-
|
|
486
|
-
return await Versions.listVersions<DataModel>({
|
|
487
|
-
ctx,
|
|
488
|
-
collection: collectionSlug,
|
|
489
|
-
documentId,
|
|
490
|
-
limit,
|
|
491
|
-
})
|
|
492
|
-
},
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Gets the document content for the admin edit view.
|
|
497
|
-
* Returns the latest version's snapshot if versions exist,
|
|
498
|
-
* otherwise falls back to the main document's fields.
|
|
499
|
-
*/
|
|
500
|
-
export const getDocumentForEdit = query({
|
|
501
|
-
args: {
|
|
502
|
-
collectionSlug: v.string(),
|
|
503
|
-
documentId: v.string(),
|
|
504
|
-
},
|
|
505
|
-
handler: async (ctx, { collectionSlug, documentId }) => {
|
|
506
|
-
const mainDoc = await ctx.db.get(documentId as any)
|
|
507
|
-
if (!mainDoc) return null
|
|
508
|
-
|
|
509
|
-
// Check if this collection/global actually has versioning enabled in config.
|
|
510
|
-
// If not (e.g. config not yet redeployed), just return the raw document.
|
|
511
|
-
const collections = [...config.collections, ...(config.media?.collections ?? []), ...(config.globals ?? [])]
|
|
512
|
-
const match = collections.find((c: any) => c.slug === collectionSlug)
|
|
513
|
-
if (!match?.versions?.drafts) {
|
|
514
|
-
const doc = mainDoc as Record<string, unknown>
|
|
515
|
-
if (!doc.vex_status) doc.vex_status = "published"
|
|
516
|
-
return doc
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const latestVersion = await Versions.getLatestVersion<DataModel>({
|
|
520
|
-
ctx,
|
|
521
|
-
collection: collectionSlug,
|
|
522
|
-
documentId,
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
if (latestVersion) {
|
|
526
|
-
return {
|
|
527
|
-
_id: mainDoc._id,
|
|
528
|
-
...(latestVersion.snapshot as Record<string, unknown>),
|
|
529
|
-
vex_status: (latestVersion as any).status ?? (mainDoc as any).vex_status ?? "draft",
|
|
530
|
-
vex_version: latestVersion.version,
|
|
531
|
-
vex_publishedAt: (mainDoc as any).vex_publishedAt,
|
|
532
|
-
_creationTime: mainDoc._creationTime,
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Fallback: return main doc as-is (e.g., versioning just enabled on existing collection)
|
|
537
|
-
// Pre-existing documents without vex_status are treated as published
|
|
538
|
-
const doc = mainDoc as Record<string, unknown>
|
|
539
|
-
if (!doc.vex_status) {
|
|
540
|
-
doc.vex_status = "published"
|
|
541
|
-
}
|
|
542
|
-
return doc
|
|
543
|
-
},
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Creates an initial version entry for a document that has none.
|
|
548
|
-
* Called by the admin edit view when it detects a versioned document
|
|
549
|
-
* without any version records (e.g., versioning was just enabled on
|
|
550
|
-
* an existing collection).
|
|
551
|
-
*
|
|
552
|
-
* Preserves the document's current status — if vex_status is "published",
|
|
553
|
-
* the version is created as published. Otherwise defaults to "draft".
|
|
554
|
-
*
|
|
555
|
-
* No-ops if the document already has version entries.
|
|
556
|
-
*/
|
|
557
|
-
export const initializeVersion = mutation({
|
|
558
|
-
args: {
|
|
559
|
-
collectionSlug: v.string(),
|
|
560
|
-
documentId: v.string(),
|
|
561
|
-
},
|
|
562
|
-
handler: async (ctx, { collectionSlug, documentId }) => {
|
|
563
|
-
const mainDoc = await ctx.db.get(documentId as any)
|
|
564
|
-
if (!mainDoc) return null
|
|
565
|
-
|
|
566
|
-
// Check if versions already exist
|
|
567
|
-
const existing = await Versions.getLatestVersion<DataModel>({
|
|
568
|
-
ctx,
|
|
569
|
-
collection: collectionSlug,
|
|
570
|
-
documentId,
|
|
571
|
-
})
|
|
572
|
-
if (existing) return existing
|
|
573
|
-
|
|
574
|
-
// Build snapshot from the main document's current fields
|
|
575
|
-
const { _id, _creationTime, ...snapshot } = mainDoc as Record<string, unknown>
|
|
576
|
-
|
|
577
|
-
// Determine status from the document's current state
|
|
578
|
-
const status = (snapshot.vex_status as string) === "published" ? "published" : "draft"
|
|
579
|
-
|
|
580
|
-
// Ensure the main doc has vex_status set
|
|
581
|
-
if (!snapshot.vex_status) {
|
|
582
|
-
await ctx.db.patch(documentId as any, { vex_status: "published" } as any)
|
|
583
|
-
snapshot.vex_status = "published"
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Create the initial version
|
|
587
|
-
const versionId = await Versions.createVersion<DataModel>({
|
|
588
|
-
ctx,
|
|
589
|
-
collection: collectionSlug,
|
|
590
|
-
documentId,
|
|
591
|
-
snapshot,
|
|
592
|
-
status: status as "draft" | "published",
|
|
593
|
-
createdBy: null,
|
|
594
|
-
})
|
|
595
|
-
|
|
596
|
-
return versionId
|
|
597
|
-
},
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Backfills vex_status on existing documents in a versioned collection
|
|
602
|
-
* that don't have the field yet (e.g. versioning was just enabled on
|
|
603
|
-
* a collection with existing data, or VEX was added to an existing project).
|
|
604
|
-
*
|
|
605
|
-
* Only sets vex_status to "published" — does NOT create version records.
|
|
606
|
-
* Version records are created lazily on first saveDraft or publish, which
|
|
607
|
-
* avoids doubling the collection data in one go.
|
|
608
|
-
*
|
|
609
|
-
* Called by the VEX CLI in a paginated loop (cursor-based) so it can
|
|
610
|
-
* handle production tables with thousands of documents without hitting
|
|
611
|
-
* Convex mutation limits.
|
|
612
|
-
*
|
|
613
|
-
* Safe to run repeatedly — skips documents that already have vex_status.
|
|
614
|
-
*/
|
|
615
|
-
export const backfillVersionStatus = mutation({
|
|
616
|
-
args: {
|
|
617
|
-
collectionSlug: v.string(),
|
|
618
|
-
cursor: v.optional(v.string()),
|
|
619
|
-
batchSize: v.optional(v.number()),
|
|
620
|
-
},
|
|
621
|
-
handler: async (ctx, { collectionSlug, cursor, batchSize = 100 }) => {
|
|
622
|
-
const collection = requireVersionedCollection(collectionSlug)
|
|
623
|
-
|
|
624
|
-
// No auth check — this is a system migration called by the VEX CLI,
|
|
625
|
-
// not a user-facing action. The ConvexHttpClient has no auth session.
|
|
626
|
-
|
|
627
|
-
const tableName = (collection.tableName ?? collectionSlug) as TableNamesInDataModel<DataModel>
|
|
628
|
-
const results = await (ctx.db as any)
|
|
629
|
-
.query(tableName)
|
|
630
|
-
.paginate({ cursor: cursor ?? null, numItems: batchSize })
|
|
631
|
-
|
|
632
|
-
let patched = 0
|
|
633
|
-
for (const doc of results.page) {
|
|
634
|
-
if (!doc.vex_status) {
|
|
635
|
-
await (ctx.db as any).patch(doc._id, {
|
|
636
|
-
vex_status: "published",
|
|
637
|
-
})
|
|
638
|
-
patched++
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return {
|
|
643
|
-
patched,
|
|
644
|
-
isDone: results.isDone,
|
|
645
|
-
cursor: results.continueCursor,
|
|
646
|
-
}
|
|
647
|
-
},
|
|
648
|
-
})
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { AdminLayout } from "@vexcms/admin-next"
|
|
4
|
-
import type { PermissionUser } from "@vexcms/admin-next"
|
|
5
|
-
import type { ClientVexConfig } from "@vexcms/core"
|
|
6
|
-
import { access } from "~/vexcms/access"
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Client wrapper that provides the access config directly (not through RSC serialization).
|
|
10
|
-
* The access config contains callback functions that can't be serialized through RSC.
|
|
11
|
-
*/
|
|
12
|
-
export function AdminLayoutWrapper({
|
|
13
|
-
config,
|
|
14
|
-
user,
|
|
15
|
-
permissionUser,
|
|
16
|
-
children,
|
|
17
|
-
}: {
|
|
18
|
-
config: ClientVexConfig
|
|
19
|
-
user?: { name: string; email: string; avatar?: string }
|
|
20
|
-
permissionUser?: PermissionUser
|
|
21
|
-
children: React.ReactNode
|
|
22
|
-
}) {
|
|
23
|
-
return (
|
|
24
|
-
<AdminLayout
|
|
25
|
-
config={config}
|
|
26
|
-
user={user}
|
|
27
|
-
permissionUser={permissionUser}
|
|
28
|
-
access={access}
|
|
29
|
-
>
|
|
30
|
-
{children}
|
|
31
|
-
</AdminLayout>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { AdminPage } from "@vexcms/admin-next"
|
|
4
|
-
import type { ClientVexConfig } from "@vexcms/core"
|
|
5
|
-
import { extractLivePreviewConfigs, sanitizeConfigForClient } from "@vexcms/core"
|
|
6
|
-
import { RichTextFieldWithMedia } from "./RichTextFieldWithMedia"
|
|
7
|
-
import originalConfig from "~/../vex.config"
|
|
8
|
-
|
|
9
|
-
const livePreviewConfigs = extractLivePreviewConfigs(originalConfig)
|
|
10
|
-
// Sanitize on the client so component references in admin.components survive
|
|
11
|
-
// (they can't cross the RSC serialization boundary)
|
|
12
|
-
const clientConfig = sanitizeConfigForClient(originalConfig)
|
|
13
|
-
|
|
14
|
-
export function AdminPageWrapper({
|
|
15
|
-
config: _serverConfig,
|
|
16
|
-
path,
|
|
17
|
-
}: {
|
|
18
|
-
config: ClientVexConfig
|
|
19
|
-
path?: string[]
|
|
20
|
-
}) {
|
|
21
|
-
return (
|
|
22
|
-
<AdminPage
|
|
23
|
-
config={clientConfig}
|
|
24
|
-
path={path}
|
|
25
|
-
livePreviewConfigs={livePreviewConfigs}
|
|
26
|
-
renderRichTextField={(props: any) => (
|
|
27
|
-
<RichTextFieldWithMedia
|
|
28
|
-
field={props.field}
|
|
29
|
-
fieldDef={props.fieldDef}
|
|
30
|
-
name={props.name}
|
|
31
|
-
generateUploadUrl={props.generateUploadUrl}
|
|
32
|
-
createMediaDocument={props.createMediaDocument}
|
|
33
|
-
onUploadNew={props.onUploadNew}
|
|
34
|
-
/>
|
|
35
|
-
)}
|
|
36
|
-
/>
|
|
37
|
-
)
|
|
38
|
-
}
|