@btst/plugin-cms 1.0.0-beta.0 → 1.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/getters.cjs +37 -25
- package/dist/api/getters.mjs +37 -25
- package/dist/api/index.cjs +1 -0
- package/dist/api/index.d.cts +17 -8
- package/dist/api/index.d.mts +17 -8
- package/dist/api/index.d.ts +17 -8
- package/dist/api/index.mjs +1 -1
- package/dist/api/mutations.cjs +46 -19
- package/dist/api/mutations.mjs +46 -20
- package/dist/api/operations.cjs +586 -177
- package/dist/api/operations.mjs +585 -178
- package/dist/api/plugin.cjs +56 -7
- package/dist/api/plugin.mjs +57 -8
- package/dist/api/publication.cjs +8 -0
- package/dist/api/publication.mjs +6 -0
- package/dist/client/components/forms/content-form.cjs +107 -1
- package/dist/client/components/forms/content-form.mjs +108 -2
- package/dist/client/components/pages/content-editor-page.internal.cjs +23 -5
- package/dist/client/components/pages/content-editor-page.internal.mjs +23 -5
- package/dist/client/components/pages/content-list-page.internal.cjs +3 -0
- package/dist/client/components/pages/content-list-page.internal.mjs +3 -0
- package/dist/client/components/pages/dashboard-page.internal.cjs +10 -30
- package/dist/client/components/pages/dashboard-page.internal.mjs +10 -30
- package/dist/client/components/revision-history.cjs +161 -0
- package/dist/client/components/revision-history.mjs +159 -0
- package/dist/client/components/shared/content-type-card.cjs +56 -0
- package/dist/client/components/shared/content-type-card.mjs +54 -0
- package/dist/client/components/shared/empty-state.cjs +18 -1
- package/dist/client/components/shared/empty-state.mjs +19 -2
- package/dist/client/hooks/cms-hooks.cjs +18 -2
- package/dist/client/hooks/cms-hooks.mjs +17 -3
- package/dist/client/hooks/index.cjs +2 -0
- package/dist/client/hooks/index.d.cts +306 -2
- package/dist/client/hooks/index.d.mts +306 -2
- package/dist/client/hooks/index.d.ts +306 -2
- package/dist/client/hooks/index.mjs +1 -1
- package/dist/client/index.d.cts +55 -2
- package/dist/client/index.d.mts +55 -2
- package/dist/client/index.d.ts +55 -2
- package/dist/db.cjs +25 -0
- package/dist/db.mjs +25 -0
- package/dist/permissions.cjs +2 -0
- package/dist/permissions.d.cts +1 -0
- package/dist/permissions.d.mts +1 -0
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.mjs +2 -0
- package/dist/query-keys.cjs +31 -1
- package/dist/query-keys.d.cts +1 -1
- package/dist/query-keys.d.mts +1 -1
- package/dist/query-keys.d.ts +1 -1
- package/dist/query-keys.mjs +31 -1
- package/dist/shared/{plugin-cms.Csx7tNDO.d.mts → plugin-cms.DAAUZ41w.d.mts} +314 -81
- package/dist/shared/{plugin-cms.eNW9_FO5.d.ts → plugin-cms.DJ_293QU.d.cts} +314 -81
- package/dist/shared/{plugin-cms.Bptwe33B.d.cts → plugin-cms.DlUSv7wz.d.ts} +314 -81
- package/dist/types.d.cts +38 -2
- package/dist/types.d.mts +38 -2
- package/dist/types.d.ts +38 -2
- package/dist/ui/src/components/auto-form/config.cjs +2 -0
- package/dist/ui/src/components/auto-form/config.mjs +2 -0
- package/dist/ui/src/components/auto-form/fields/file.cjs +68 -0
- package/dist/ui/src/components/auto-form/fields/file.mjs +63 -0
- package/dist/ui/src/components/auto-form/fields/object.cjs +9 -2
- package/dist/ui/src/components/auto-form/fields/object.mjs +9 -2
- package/dist/ui/src/components/auto-form/helpers.cjs +9 -0
- package/dist/ui/src/components/auto-form/helpers.mjs +9 -0
- package/dist/ui/src/components/auto-form/index.cjs +29 -4
- package/dist/ui/src/components/auto-form/index.mjs +31 -6
- package/dist/ui/src/components/auto-form/stepped-auto-form.cjs +34 -29
- package/dist/ui/src/components/auto-form/stepped-auto-form.mjs +35 -30
- package/dist/ui/src/components/sheet.cjs +126 -0
- package/dist/ui/src/components/sheet.mjs +105 -0
- package/dist/ui/src/lib/form-visibility.cjs +20 -0
- package/dist/ui/src/lib/form-visibility.mjs +18 -0
- package/dist/ui/src/lib/schema-converter.cjs +101 -0
- package/dist/ui/src/lib/schema-converter.mjs +100 -1
- package/package.json +5 -4
package/dist/api/plugin.cjs
CHANGED
|
@@ -74,6 +74,17 @@ async function syncContentTypes(adapter, config) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
const cmsBackendPlugin = (config) => {
|
|
77
|
+
for (const type of config.contentTypes) {
|
|
78
|
+
const policy = type.publication;
|
|
79
|
+
if (!policy) continue;
|
|
80
|
+
const field = type.schema.shape[policy.stateField];
|
|
81
|
+
if (!field || policy.scheduledValue === policy.publishedValue || !z.z.safeParse(field, policy.scheduledValue).success || !z.z.safeParse(field, policy.publishedValue).success) {
|
|
82
|
+
throw new TypeError(
|
|
83
|
+
`Invalid publication policy for content type "${type.slug}"`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const publicationFor = (typeSlug, view) => view === "content" ? config.contentTypes.find((type) => type.slug === typeSlug)?.publication : void 0;
|
|
77
88
|
let syncPromise = null;
|
|
78
89
|
const ensureSynced = (adapter) => {
|
|
79
90
|
if (!syncPromise) {
|
|
@@ -123,10 +134,16 @@ const cmsBackendPlugin = (config) => {
|
|
|
123
134
|
fields: params?.fields
|
|
124
135
|
});
|
|
125
136
|
const [contentTypes, contentItems] = await Promise.all([
|
|
126
|
-
getContentTypesWithCounts(adapter),
|
|
127
|
-
getters.getAllContentItems(
|
|
137
|
+
publicationFor(typeSlug, options.view) ? Promise.resolve(null) : getContentTypesWithCounts(adapter),
|
|
138
|
+
getters.getAllContentItems(
|
|
139
|
+
adapter,
|
|
140
|
+
typeSlug,
|
|
141
|
+
options,
|
|
142
|
+
publicationFor(typeSlug, options.view)
|
|
143
|
+
)
|
|
128
144
|
]);
|
|
129
|
-
|
|
145
|
+
if (contentTypes)
|
|
146
|
+
queryClient.setQueryData(queryKeyDefs.CMS_QUERY_KEYS.typesList(), contentTypes);
|
|
130
147
|
queryClient.setQueryData(
|
|
131
148
|
queryKeyDefs.CMS_QUERY_KEYS.contentList({ typeSlug, ...options }),
|
|
132
149
|
{
|
|
@@ -150,11 +167,18 @@ const cmsBackendPlugin = (config) => {
|
|
|
150
167
|
view: params?.view,
|
|
151
168
|
fields: params?.fields
|
|
152
169
|
});
|
|
153
|
-
const [contentTypes,
|
|
154
|
-
getContentTypesWithCounts(adapter),
|
|
155
|
-
id ? getters.getContentItemById(adapter, id
|
|
170
|
+
const [contentTypes, fullItem] = await Promise.all([
|
|
171
|
+
publicationFor(typeSlug, options.view) ? Promise.resolve(null) : getContentTypesWithCounts(adapter),
|
|
172
|
+
id ? getters.getContentItemById(adapter, id) : Promise.resolve(null)
|
|
156
173
|
]);
|
|
157
|
-
|
|
174
|
+
const item = fullItem?.contentType?.slug === typeSlug ? await getters.getContentItemById(
|
|
175
|
+
adapter,
|
|
176
|
+
id,
|
|
177
|
+
options,
|
|
178
|
+
publicationFor(typeSlug, options.view)
|
|
179
|
+
) : null;
|
|
180
|
+
if (contentTypes)
|
|
181
|
+
queryClient.setQueryData(queryKeyDefs.CMS_QUERY_KEYS.typesList(), contentTypes);
|
|
158
182
|
if (id) {
|
|
159
183
|
queryClient.setQueryData(
|
|
160
184
|
queryKeyDefs.CMS_QUERY_KEYS.contentDetail(typeSlug, id, options),
|
|
@@ -170,6 +194,7 @@ const cmsBackendPlugin = (config) => {
|
|
|
170
194
|
dbPlugin: db.cmsSchema,
|
|
171
195
|
operations: (adapter) => operations.createCMSOperations(adapter, {
|
|
172
196
|
ensureSynced: () => ensureSynced(adapter),
|
|
197
|
+
contentTypes: config.contentTypes,
|
|
173
198
|
getContentTypesWithCounts: () => getContentTypesWithCounts(adapter),
|
|
174
199
|
...config.maxPageSize !== void 0 ? { maxPageSize: config.maxPageSize } : {},
|
|
175
200
|
...config.hooks ? { hooks: config.hooks } : {}
|
|
@@ -261,6 +286,28 @@ const cmsBackendPlugin = (config) => {
|
|
|
261
286
|
},
|
|
262
287
|
operations$1.deleteContentItem.route((ctx) => ctx.params)
|
|
263
288
|
);
|
|
289
|
+
const listContentRevisions = api.createEndpoint(
|
|
290
|
+
"/content/:typeSlug/:id/revisions",
|
|
291
|
+
{
|
|
292
|
+
method: "GET",
|
|
293
|
+
params: operations.CMSContentItemParamsSchema,
|
|
294
|
+
query: operations.CMSRevisionsQuerySchema,
|
|
295
|
+
requireRequest: true
|
|
296
|
+
},
|
|
297
|
+
operations$1.listContentRevisions.route((ctx) => ({
|
|
298
|
+
...ctx.params,
|
|
299
|
+
...ctx.query
|
|
300
|
+
}))
|
|
301
|
+
);
|
|
302
|
+
const restoreContentRevision = api.createEndpoint(
|
|
303
|
+
"/content/:typeSlug/:id/revisions/:revisionId/restore",
|
|
304
|
+
{
|
|
305
|
+
method: "POST",
|
|
306
|
+
params: operations.CMSRevisionParamsSchema,
|
|
307
|
+
requireRequest: true
|
|
308
|
+
},
|
|
309
|
+
operations$1.restoreContentRevision.route((ctx) => ctx.params)
|
|
310
|
+
);
|
|
264
311
|
const getContentItemPopulated = api.createEndpoint(
|
|
265
312
|
"/content/:typeSlug/:id/populated",
|
|
266
313
|
{
|
|
@@ -320,6 +367,8 @@ const cmsBackendPlugin = (config) => {
|
|
|
320
367
|
getContentItem,
|
|
321
368
|
createContentItem,
|
|
322
369
|
updateContentItem,
|
|
370
|
+
listContentRevisions,
|
|
371
|
+
restoreContentRevision,
|
|
323
372
|
deleteContentItem,
|
|
324
373
|
getContentItemPopulated,
|
|
325
374
|
listContentByRelation,
|
package/dist/api/plugin.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { zodToFormSchema } from '../ui/src/lib/schema-converter.mjs';
|
|
|
4
4
|
import { cmsSchema } from '../db.mjs';
|
|
5
5
|
import { createListContentQuerySchema, contentReadQuerySchema, DEFAULT_MAX_PAGE_SIZE, contentReadOptionsSchema } from '../schemas.mjs';
|
|
6
6
|
import { getContentItemById, getAllContentItems, getAllContentTypes } from './getters.mjs';
|
|
7
|
-
import { CMSContentTypeParamsSchema, CMSContentItemParamsSchema, CMSCreateContentItemBodySchema, CMSUpdateContentItemBodySchema, createCMSOperations } from './operations.mjs';
|
|
7
|
+
import { CMSContentTypeParamsSchema, CMSContentItemParamsSchema, CMSCreateContentItemBodySchema, CMSUpdateContentItemBodySchema, CMSRevisionsQuerySchema, CMSRevisionParamsSchema, createCMSOperations } from './operations.mjs';
|
|
8
8
|
import { CMS_QUERY_KEYS } from './query-key-defs.mjs';
|
|
9
9
|
|
|
10
10
|
function schemasEqual(stored, configured) {
|
|
@@ -72,6 +72,17 @@ async function syncContentTypes(adapter, config) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
const cmsBackendPlugin = (config) => {
|
|
75
|
+
for (const type of config.contentTypes) {
|
|
76
|
+
const policy = type.publication;
|
|
77
|
+
if (!policy) continue;
|
|
78
|
+
const field = type.schema.shape[policy.stateField];
|
|
79
|
+
if (!field || policy.scheduledValue === policy.publishedValue || !z.safeParse(field, policy.scheduledValue).success || !z.safeParse(field, policy.publishedValue).success) {
|
|
80
|
+
throw new TypeError(
|
|
81
|
+
`Invalid publication policy for content type "${type.slug}"`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const publicationFor = (typeSlug, view) => view === "content" ? config.contentTypes.find((type) => type.slug === typeSlug)?.publication : void 0;
|
|
75
86
|
let syncPromise = null;
|
|
76
87
|
const ensureSynced = (adapter) => {
|
|
77
88
|
if (!syncPromise) {
|
|
@@ -121,10 +132,16 @@ const cmsBackendPlugin = (config) => {
|
|
|
121
132
|
fields: params?.fields
|
|
122
133
|
});
|
|
123
134
|
const [contentTypes, contentItems] = await Promise.all([
|
|
124
|
-
getContentTypesWithCounts(adapter),
|
|
125
|
-
getAllContentItems(
|
|
135
|
+
publicationFor(typeSlug, options.view) ? Promise.resolve(null) : getContentTypesWithCounts(adapter),
|
|
136
|
+
getAllContentItems(
|
|
137
|
+
adapter,
|
|
138
|
+
typeSlug,
|
|
139
|
+
options,
|
|
140
|
+
publicationFor(typeSlug, options.view)
|
|
141
|
+
)
|
|
126
142
|
]);
|
|
127
|
-
|
|
143
|
+
if (contentTypes)
|
|
144
|
+
queryClient.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
|
|
128
145
|
queryClient.setQueryData(
|
|
129
146
|
CMS_QUERY_KEYS.contentList({ typeSlug, ...options }),
|
|
130
147
|
{
|
|
@@ -148,11 +165,18 @@ const cmsBackendPlugin = (config) => {
|
|
|
148
165
|
view: params?.view,
|
|
149
166
|
fields: params?.fields
|
|
150
167
|
});
|
|
151
|
-
const [contentTypes,
|
|
152
|
-
getContentTypesWithCounts(adapter),
|
|
153
|
-
id ? getContentItemById(adapter, id
|
|
168
|
+
const [contentTypes, fullItem] = await Promise.all([
|
|
169
|
+
publicationFor(typeSlug, options.view) ? Promise.resolve(null) : getContentTypesWithCounts(adapter),
|
|
170
|
+
id ? getContentItemById(adapter, id) : Promise.resolve(null)
|
|
154
171
|
]);
|
|
155
|
-
|
|
172
|
+
const item = fullItem?.contentType?.slug === typeSlug ? await getContentItemById(
|
|
173
|
+
adapter,
|
|
174
|
+
id,
|
|
175
|
+
options,
|
|
176
|
+
publicationFor(typeSlug, options.view)
|
|
177
|
+
) : null;
|
|
178
|
+
if (contentTypes)
|
|
179
|
+
queryClient.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
|
|
156
180
|
if (id) {
|
|
157
181
|
queryClient.setQueryData(
|
|
158
182
|
CMS_QUERY_KEYS.contentDetail(typeSlug, id, options),
|
|
@@ -168,6 +192,7 @@ const cmsBackendPlugin = (config) => {
|
|
|
168
192
|
dbPlugin: cmsSchema,
|
|
169
193
|
operations: (adapter) => createCMSOperations(adapter, {
|
|
170
194
|
ensureSynced: () => ensureSynced(adapter),
|
|
195
|
+
contentTypes: config.contentTypes,
|
|
171
196
|
getContentTypesWithCounts: () => getContentTypesWithCounts(adapter),
|
|
172
197
|
...config.maxPageSize !== void 0 ? { maxPageSize: config.maxPageSize } : {},
|
|
173
198
|
...config.hooks ? { hooks: config.hooks } : {}
|
|
@@ -259,6 +284,28 @@ const cmsBackendPlugin = (config) => {
|
|
|
259
284
|
},
|
|
260
285
|
operations.deleteContentItem.route((ctx) => ctx.params)
|
|
261
286
|
);
|
|
287
|
+
const listContentRevisions = createEndpoint(
|
|
288
|
+
"/content/:typeSlug/:id/revisions",
|
|
289
|
+
{
|
|
290
|
+
method: "GET",
|
|
291
|
+
params: CMSContentItemParamsSchema,
|
|
292
|
+
query: CMSRevisionsQuerySchema,
|
|
293
|
+
requireRequest: true
|
|
294
|
+
},
|
|
295
|
+
operations.listContentRevisions.route((ctx) => ({
|
|
296
|
+
...ctx.params,
|
|
297
|
+
...ctx.query
|
|
298
|
+
}))
|
|
299
|
+
);
|
|
300
|
+
const restoreContentRevision = createEndpoint(
|
|
301
|
+
"/content/:typeSlug/:id/revisions/:revisionId/restore",
|
|
302
|
+
{
|
|
303
|
+
method: "POST",
|
|
304
|
+
params: CMSRevisionParamsSchema,
|
|
305
|
+
requireRequest: true
|
|
306
|
+
},
|
|
307
|
+
operations.restoreContentRevision.route((ctx) => ctx.params)
|
|
308
|
+
);
|
|
262
309
|
const getContentItemPopulated = createEndpoint(
|
|
263
310
|
"/content/:typeSlug/:id/populated",
|
|
264
311
|
{
|
|
@@ -318,6 +365,8 @@ const cmsBackendPlugin = (config) => {
|
|
|
318
365
|
getContentItem,
|
|
319
366
|
createContentItem,
|
|
320
367
|
updateContentItem,
|
|
368
|
+
listContentRevisions,
|
|
369
|
+
restoreContentRevision,
|
|
321
370
|
deleteContentItem,
|
|
322
371
|
getContentItemPopulated,
|
|
323
372
|
listContentByRelation,
|
|
@@ -11,6 +11,7 @@ const input = require('../../../ui/src/components/input.cjs');
|
|
|
11
11
|
const label = require('../../../ui/src/components/label.cjs');
|
|
12
12
|
const badge = require('../../../ui/src/components/badge.cjs');
|
|
13
13
|
const context = require('@btst/stack/context');
|
|
14
|
+
const pageContext = require('@btst/stack/page-context');
|
|
14
15
|
const constants = require('../../constants.cjs');
|
|
15
16
|
const utils = require('../../../utils.cjs');
|
|
16
17
|
const fileUpload = require('./file-upload.cjs');
|
|
@@ -123,6 +124,7 @@ function ContentForm({
|
|
|
123
124
|
contentType,
|
|
124
125
|
initialData = {},
|
|
125
126
|
initialSlug = "",
|
|
127
|
+
initialPublishAt,
|
|
126
128
|
isEditing = false,
|
|
127
129
|
onSubmit,
|
|
128
130
|
onCancel,
|
|
@@ -139,6 +141,13 @@ function ContentForm({
|
|
|
139
141
|
fieldComponents
|
|
140
142
|
} = context.usePluginOverrides(constants.CMS_PLUGIN_ID);
|
|
141
143
|
const [slug, setSlug] = React.useState(initialSlug);
|
|
144
|
+
const publishAtEditedRef = React.useRef(false);
|
|
145
|
+
const aiPublishAtRef = React.useRef(void 0);
|
|
146
|
+
const [publishAt, setPublishAt] = React.useState(() => {
|
|
147
|
+
if (!initialPublishAt) return "";
|
|
148
|
+
const date = new Date(initialPublishAt);
|
|
149
|
+
return new Date(date.getTime() - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 16);
|
|
150
|
+
});
|
|
142
151
|
const [slugManuallyEdited, setSlugManuallyEdited] = React.useState(isEditing);
|
|
143
152
|
const [isSubmittingLocal, setIsSubmittingLocal] = React.useState(false);
|
|
144
153
|
const [formData, setFormData] = React.useState(initialData);
|
|
@@ -179,6 +188,76 @@ function ContentForm({
|
|
|
179
188
|
return z.z.object({});
|
|
180
189
|
}
|
|
181
190
|
}, [jsonSchema]);
|
|
191
|
+
pageContext.useRegisterPageAIContext({
|
|
192
|
+
routeName: "cms-content-editor",
|
|
193
|
+
pageDescription: JSON.stringify({
|
|
194
|
+
page: "CMS entry draft. Fill only fields in this active content type; the user reviews and saves.",
|
|
195
|
+
contentType: contentType.slug,
|
|
196
|
+
schema: jsonSchema,
|
|
197
|
+
publishAt
|
|
198
|
+
}).slice(0, 16e3),
|
|
199
|
+
clientTools: {
|
|
200
|
+
fillCmsEntryForm: async (input) => {
|
|
201
|
+
if (isSubmitting)
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
message: "Wait for the entry save to finish."
|
|
205
|
+
};
|
|
206
|
+
const parsed = z.z.object({
|
|
207
|
+
data: z.z.record(z.z.string(), z.z.unknown()).optional(),
|
|
208
|
+
publishAt: z.z.iso.datetime({ offset: true }).nullable().optional()
|
|
209
|
+
}).strict().safeParse(input);
|
|
210
|
+
if (!parsed.success)
|
|
211
|
+
return { success: false, message: "Invalid entry draft." };
|
|
212
|
+
const patch = parsed.data.data ?? {};
|
|
213
|
+
const properties = jsonSchema.properties;
|
|
214
|
+
if (Object.keys(patch).some(
|
|
215
|
+
(key) => !properties || !Object.hasOwn(properties, key) || ["__proto__", "constructor", "prototype"].includes(key)
|
|
216
|
+
))
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
message: "Draft contains fields outside the active content type."
|
|
220
|
+
};
|
|
221
|
+
let validation;
|
|
222
|
+
try {
|
|
223
|
+
validation = schemaConverter.formSchemaToZod(jsonSchema).safeParse({
|
|
224
|
+
...formData,
|
|
225
|
+
...patch
|
|
226
|
+
});
|
|
227
|
+
} catch {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
message: "The active content type could not be validated."
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (!validation.success && validation.error.issues.some(
|
|
234
|
+
(issue) => issue.path.length === 0 || Object.hasOwn(patch, String(issue.path[0]))
|
|
235
|
+
))
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
message: "Draft values do not match the active content type."
|
|
239
|
+
};
|
|
240
|
+
for (const [key, value] of Object.entries(patch))
|
|
241
|
+
formInstance?.setValue(key, value, {
|
|
242
|
+
shouldDirty: true,
|
|
243
|
+
shouldValidate: true
|
|
244
|
+
});
|
|
245
|
+
setFormData((current) => ({ ...current, ...patch }));
|
|
246
|
+
if (parsed.data.publishAt !== void 0) {
|
|
247
|
+
aiPublishAtRef.current = parsed.data.publishAt;
|
|
248
|
+
publishAtEditedRef.current = true;
|
|
249
|
+
const date = parsed.data.publishAt ? new Date(parsed.data.publishAt) : null;
|
|
250
|
+
setPublishAt(
|
|
251
|
+
date ? new Date(date.getTime() - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 16) : ""
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
success: true,
|
|
256
|
+
message: "Entry draft updated. Review it before saving."
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
182
261
|
const fieldConfig = React.useMemo(
|
|
183
262
|
() => buildFieldConfigFromJsonSchema(
|
|
184
263
|
jsonSchema,
|
|
@@ -216,7 +295,11 @@ function ContentForm({
|
|
|
216
295
|
}
|
|
217
296
|
setIsSubmittingLocal(true);
|
|
218
297
|
try {
|
|
219
|
-
await onSubmit({
|
|
298
|
+
await onSubmit({
|
|
299
|
+
slug,
|
|
300
|
+
data,
|
|
301
|
+
publishAt: aiPublishAtRef.current !== void 0 ? aiPublishAtRef.current : publishAtEditedRef.current ? publishAt ? new Date(publishAt).toISOString() : null : initialPublishAt ?? null
|
|
302
|
+
});
|
|
220
303
|
} catch (error) {
|
|
221
304
|
const message = error instanceof Error ? error.message : localization?.CMS_TOAST_ERROR ?? t("cms.toasts.error", "An error occurred. Please try again.");
|
|
222
305
|
setSubmitError(message);
|
|
@@ -258,6 +341,29 @@ function ContentForm({
|
|
|
258
341
|
"URL-friendly identifier for this item"
|
|
259
342
|
) })
|
|
260
343
|
] }),
|
|
344
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
345
|
+
/* @__PURE__ */ jsxRuntime.jsx(label.Label, { htmlFor: "publishAt", children: t("cms.editor.publishAt", "Publish at") }),
|
|
346
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
347
|
+
input.Input,
|
|
348
|
+
{
|
|
349
|
+
id: "publishAt",
|
|
350
|
+
type: "datetime-local",
|
|
351
|
+
value: publishAt,
|
|
352
|
+
onChange: (event) => {
|
|
353
|
+
aiPublishAtRef.current = void 0;
|
|
354
|
+
publishAtEditedRef.current = true;
|
|
355
|
+
setPublishAt(event.target.value);
|
|
356
|
+
},
|
|
357
|
+
disabled: isSubmitting,
|
|
358
|
+
"aria-describedby": "publishAt-description"
|
|
359
|
+
}
|
|
360
|
+
),
|
|
361
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { id: "publishAt-description", className: "text-sm text-muted-foreground", children: t(
|
|
362
|
+
"cms.editor.publishAtDescription",
|
|
363
|
+
"Optional. Uses your local time zone. Leave empty to clear the schedule. Scheduling does not change this item's publication state."
|
|
364
|
+
) }),
|
|
365
|
+
serverFieldErrors.publishAt && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "text-sm text-destructive", children: serverFieldErrors.publishAt })
|
|
366
|
+
] }),
|
|
261
367
|
bannerMessage && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-md border border-destructive/50 bg-destructive/10 p-3", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: bannerMessage }) }),
|
|
262
368
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
263
369
|
steppedAutoForm.SteppedAutoForm,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
-
import { useState,
|
|
3
|
+
import { useState, useRef, useMemo, useEffect } from 'react';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { SteppedAutoForm } from '../../../ui/src/components/auto-form/stepped-auto-form.mjs';
|
|
6
6
|
import { buildFieldConfigFromJsonSchema as buildFieldConfigFromJsonSchema$1 } from '../../../ui/src/components/auto-form/helpers.mjs';
|
|
@@ -9,6 +9,7 @@ import { Input } from '../../../ui/src/components/input.mjs';
|
|
|
9
9
|
import { Label } from '../../../ui/src/components/label.mjs';
|
|
10
10
|
import { Badge } from '../../../ui/src/components/badge.mjs';
|
|
11
11
|
import { useTranslate, usePluginOverrides } from '@btst/stack/context';
|
|
12
|
+
import { useRegisterPageAIContext } from '@btst/stack/page-context';
|
|
12
13
|
import { CMS_PLUGIN_ID } from '../../constants.mjs';
|
|
13
14
|
import { slugify } from '../../../utils.mjs';
|
|
14
15
|
import { CMSFileUpload } from './file-upload.mjs';
|
|
@@ -121,6 +122,7 @@ function ContentForm({
|
|
|
121
122
|
contentType,
|
|
122
123
|
initialData = {},
|
|
123
124
|
initialSlug = "",
|
|
125
|
+
initialPublishAt,
|
|
124
126
|
isEditing = false,
|
|
125
127
|
onSubmit,
|
|
126
128
|
onCancel,
|
|
@@ -137,6 +139,13 @@ function ContentForm({
|
|
|
137
139
|
fieldComponents
|
|
138
140
|
} = usePluginOverrides(CMS_PLUGIN_ID);
|
|
139
141
|
const [slug, setSlug] = useState(initialSlug);
|
|
142
|
+
const publishAtEditedRef = useRef(false);
|
|
143
|
+
const aiPublishAtRef = useRef(void 0);
|
|
144
|
+
const [publishAt, setPublishAt] = useState(() => {
|
|
145
|
+
if (!initialPublishAt) return "";
|
|
146
|
+
const date = new Date(initialPublishAt);
|
|
147
|
+
return new Date(date.getTime() - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 16);
|
|
148
|
+
});
|
|
140
149
|
const [slugManuallyEdited, setSlugManuallyEdited] = useState(isEditing);
|
|
141
150
|
const [isSubmittingLocal, setIsSubmittingLocal] = useState(false);
|
|
142
151
|
const [formData, setFormData] = useState(initialData);
|
|
@@ -177,6 +186,76 @@ function ContentForm({
|
|
|
177
186
|
return z.object({});
|
|
178
187
|
}
|
|
179
188
|
}, [jsonSchema]);
|
|
189
|
+
useRegisterPageAIContext({
|
|
190
|
+
routeName: "cms-content-editor",
|
|
191
|
+
pageDescription: JSON.stringify({
|
|
192
|
+
page: "CMS entry draft. Fill only fields in this active content type; the user reviews and saves.",
|
|
193
|
+
contentType: contentType.slug,
|
|
194
|
+
schema: jsonSchema,
|
|
195
|
+
publishAt
|
|
196
|
+
}).slice(0, 16e3),
|
|
197
|
+
clientTools: {
|
|
198
|
+
fillCmsEntryForm: async (input) => {
|
|
199
|
+
if (isSubmitting)
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
message: "Wait for the entry save to finish."
|
|
203
|
+
};
|
|
204
|
+
const parsed = z.object({
|
|
205
|
+
data: z.record(z.string(), z.unknown()).optional(),
|
|
206
|
+
publishAt: z.iso.datetime({ offset: true }).nullable().optional()
|
|
207
|
+
}).strict().safeParse(input);
|
|
208
|
+
if (!parsed.success)
|
|
209
|
+
return { success: false, message: "Invalid entry draft." };
|
|
210
|
+
const patch = parsed.data.data ?? {};
|
|
211
|
+
const properties = jsonSchema.properties;
|
|
212
|
+
if (Object.keys(patch).some(
|
|
213
|
+
(key) => !properties || !Object.hasOwn(properties, key) || ["__proto__", "constructor", "prototype"].includes(key)
|
|
214
|
+
))
|
|
215
|
+
return {
|
|
216
|
+
success: false,
|
|
217
|
+
message: "Draft contains fields outside the active content type."
|
|
218
|
+
};
|
|
219
|
+
let validation;
|
|
220
|
+
try {
|
|
221
|
+
validation = formSchemaToZod(jsonSchema).safeParse({
|
|
222
|
+
...formData,
|
|
223
|
+
...patch
|
|
224
|
+
});
|
|
225
|
+
} catch {
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
message: "The active content type could not be validated."
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (!validation.success && validation.error.issues.some(
|
|
232
|
+
(issue) => issue.path.length === 0 || Object.hasOwn(patch, String(issue.path[0]))
|
|
233
|
+
))
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
message: "Draft values do not match the active content type."
|
|
237
|
+
};
|
|
238
|
+
for (const [key, value] of Object.entries(patch))
|
|
239
|
+
formInstance?.setValue(key, value, {
|
|
240
|
+
shouldDirty: true,
|
|
241
|
+
shouldValidate: true
|
|
242
|
+
});
|
|
243
|
+
setFormData((current) => ({ ...current, ...patch }));
|
|
244
|
+
if (parsed.data.publishAt !== void 0) {
|
|
245
|
+
aiPublishAtRef.current = parsed.data.publishAt;
|
|
246
|
+
publishAtEditedRef.current = true;
|
|
247
|
+
const date = parsed.data.publishAt ? new Date(parsed.data.publishAt) : null;
|
|
248
|
+
setPublishAt(
|
|
249
|
+
date ? new Date(date.getTime() - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 16) : ""
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
message: "Entry draft updated. Review it before saving."
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
180
259
|
const fieldConfig = useMemo(
|
|
181
260
|
() => buildFieldConfigFromJsonSchema(
|
|
182
261
|
jsonSchema,
|
|
@@ -214,7 +293,11 @@ function ContentForm({
|
|
|
214
293
|
}
|
|
215
294
|
setIsSubmittingLocal(true);
|
|
216
295
|
try {
|
|
217
|
-
await onSubmit({
|
|
296
|
+
await onSubmit({
|
|
297
|
+
slug,
|
|
298
|
+
data,
|
|
299
|
+
publishAt: aiPublishAtRef.current !== void 0 ? aiPublishAtRef.current : publishAtEditedRef.current ? publishAt ? new Date(publishAt).toISOString() : null : initialPublishAt ?? null
|
|
300
|
+
});
|
|
218
301
|
} catch (error) {
|
|
219
302
|
const message = error instanceof Error ? error.message : localization?.CMS_TOAST_ERROR ?? t("cms.toasts.error", "An error occurred. Please try again.");
|
|
220
303
|
setSubmitError(message);
|
|
@@ -256,6 +339,29 @@ function ContentForm({
|
|
|
256
339
|
"URL-friendly identifier for this item"
|
|
257
340
|
) })
|
|
258
341
|
] }),
|
|
342
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
343
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "publishAt", children: t("cms.editor.publishAt", "Publish at") }),
|
|
344
|
+
/* @__PURE__ */ jsx(
|
|
345
|
+
Input,
|
|
346
|
+
{
|
|
347
|
+
id: "publishAt",
|
|
348
|
+
type: "datetime-local",
|
|
349
|
+
value: publishAt,
|
|
350
|
+
onChange: (event) => {
|
|
351
|
+
aiPublishAtRef.current = void 0;
|
|
352
|
+
publishAtEditedRef.current = true;
|
|
353
|
+
setPublishAt(event.target.value);
|
|
354
|
+
},
|
|
355
|
+
disabled: isSubmitting,
|
|
356
|
+
"aria-describedby": "publishAt-description"
|
|
357
|
+
}
|
|
358
|
+
),
|
|
359
|
+
/* @__PURE__ */ jsx("p", { id: "publishAt-description", className: "text-sm text-muted-foreground", children: t(
|
|
360
|
+
"cms.editor.publishAtDescription",
|
|
361
|
+
"Optional. Uses your local time zone. Leave empty to clear the schedule. Scheduling does not change this item's publication state."
|
|
362
|
+
) }),
|
|
363
|
+
serverFieldErrors.publishAt && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive", children: serverFieldErrors.publishAt })
|
|
364
|
+
] }),
|
|
259
365
|
bannerMessage && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-destructive/50 bg-destructive/10 p-3", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: bannerMessage }) }),
|
|
260
366
|
/* @__PURE__ */ jsx(
|
|
261
367
|
SteppedAutoForm,
|
|
@@ -10,6 +10,7 @@ const overrides = require('../../overrides.cjs');
|
|
|
10
10
|
const constants = require('../../constants.cjs');
|
|
11
11
|
const contentForm = require('../forms/content-form.cjs');
|
|
12
12
|
const inverseRelationsPanel = require('../inverse-relations-panel.cjs');
|
|
13
|
+
const revisionHistory = require('../revision-history.cjs');
|
|
13
14
|
const emptyState = require('../shared/empty-state.cjs');
|
|
14
15
|
const pageWrapper = require('../shared/page-wrapper.cjs');
|
|
15
16
|
const editorSkeleton = require('../loading/editor-skeleton.cjs');
|
|
@@ -87,6 +88,7 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
87
88
|
);
|
|
88
89
|
const contentType = contentTypes.find((ct) => ct.slug === typeSlug);
|
|
89
90
|
const isEditing = !!id;
|
|
91
|
+
const [restoreCount, setRestoreCount] = React.useState(0);
|
|
90
92
|
const { item, isLoading: isLoadingItem } = cmsHooks.useContentItem(typeSlug, id ?? "");
|
|
91
93
|
const resourceForm = cmsHooks.useContentItemForm({
|
|
92
94
|
action: isEditing ? "edit" : "create",
|
|
@@ -95,12 +97,17 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
95
97
|
toCreateVars: (values) => ({
|
|
96
98
|
typeSlug,
|
|
97
99
|
slug: values.slug,
|
|
98
|
-
data: values.data
|
|
100
|
+
data: values.data,
|
|
101
|
+
publishAt: values.publishAt
|
|
99
102
|
}),
|
|
100
103
|
toUpdateVars: (values) => ({
|
|
101
104
|
typeSlug,
|
|
102
105
|
id: id ?? "",
|
|
103
|
-
data: {
|
|
106
|
+
data: {
|
|
107
|
+
slug: values.slug,
|
|
108
|
+
data: values.data,
|
|
109
|
+
publishAt: values.publishAt
|
|
110
|
+
}
|
|
104
111
|
}),
|
|
105
112
|
onSuccess: () => {
|
|
106
113
|
void navigate?.(context.joinBasePath(basePath, `/cms/${typeSlug}`));
|
|
@@ -110,6 +117,7 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
110
117
|
return /* @__PURE__ */ jsxRuntime.jsx(pageWrapper.PageWrapper, { testId: "cms-editor-page", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full max-w-2xl", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
111
118
|
emptyState.EmptyState,
|
|
112
119
|
{
|
|
120
|
+
page: id ? "editContent" : "newContent",
|
|
113
121
|
title: localization?.CMS_ERROR_NOT_FOUND ?? t("cms.common.notFound", "Not found"),
|
|
114
122
|
description: localization?.CMS_ERROR_TYPE_NOT_FOUND_DESCRIPTION ?? t("cms.common.typeNotFoundDescription", "Content type not found")
|
|
115
123
|
}
|
|
@@ -122,6 +130,7 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
122
130
|
return /* @__PURE__ */ jsxRuntime.jsx(pageWrapper.PageWrapper, { testId: "cms-editor-page", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full max-w-2xl", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
123
131
|
emptyState.EmptyState,
|
|
124
132
|
{
|
|
133
|
+
page: id ? "editContent" : "newContent",
|
|
125
134
|
title: localization?.CMS_ERROR_NOT_FOUND ?? t("cms.common.notFound", "Not found"),
|
|
126
135
|
description: localization?.CMS_ERROR_ITEM_NOT_FOUND_DESCRIPTION ?? t("cms.common.itemNotFoundDescription", "Content item not found")
|
|
127
136
|
}
|
|
@@ -133,7 +142,7 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
133
142
|
const hasFieldErrors = Object.keys(resourceForm.fieldErrors).length > 0;
|
|
134
143
|
const title = isEditing ? (localization?.CMS_EDITOR_TITLE_EDIT ?? t("cms.editor.titleEdit", "Edit {typeName}")).replace("{typeName}", contentType.name) : (localization?.CMS_EDITOR_TITLE_NEW ?? t("cms.editor.titleNew", "New {typeName}")).replace("{typeName}", contentType.name);
|
|
135
144
|
return /* @__PURE__ */ jsxRuntime.jsx(pageWrapper.PageWrapper, { testId: "cms-editor-page", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-2xl space-y-6", children: [
|
|
136
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
|
|
145
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [
|
|
137
146
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
138
147
|
button.Button,
|
|
139
148
|
{
|
|
@@ -143,7 +152,15 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
143
152
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "h-4 w-4" })
|
|
144
153
|
}
|
|
145
154
|
),
|
|
146
|
-
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold tracking-tight", children: title })
|
|
155
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold tracking-tight", children: title }),
|
|
156
|
+
isEditing && id && /* @__PURE__ */ jsxRuntime.jsx(
|
|
157
|
+
revisionHistory.RevisionHistory,
|
|
158
|
+
{
|
|
159
|
+
typeSlug,
|
|
160
|
+
itemId: id,
|
|
161
|
+
onRestored: () => setRestoreCount((count) => count + 1)
|
|
162
|
+
}
|
|
163
|
+
)
|
|
147
164
|
] }),
|
|
148
165
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
149
166
|
contentForm.ContentForm,
|
|
@@ -154,6 +171,7 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
154
171
|
JSON.parse(contentType.jsonSchema)
|
|
155
172
|
) : void 0,
|
|
156
173
|
initialSlug: item?.slug,
|
|
174
|
+
initialPublishAt: item?.publishAt,
|
|
157
175
|
isEditing,
|
|
158
176
|
onSubmit: handleSubmit,
|
|
159
177
|
onCancel: () => void navigate?.(context.joinBasePath(basePath, `/cms/${typeSlug}`)),
|
|
@@ -161,7 +179,7 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
161
179
|
errorMessage: hasFieldErrors ? void 0 : resourceForm.error?.message,
|
|
162
180
|
isSubmitting: resourceForm.isSubmitting
|
|
163
181
|
},
|
|
164
|
-
isEditing ? `edit-${id}` : "create"
|
|
182
|
+
isEditing ? `edit-${id}-${restoreCount}` : "create"
|
|
165
183
|
),
|
|
166
184
|
isEditing && id && /* @__PURE__ */ jsxRuntime.jsx(inverseRelationsPanel.InverseRelationsPanel, { contentTypeSlug: typeSlug, itemId: id })
|
|
167
185
|
] }) });
|