@btst/stack 2.2.0 → 2.4.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/packages/stack/src/client/components/compose.cjs +1 -2
- package/dist/packages/stack/src/client/components/compose.mjs +1 -2
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +71 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +68 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +54 -7
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +54 -7
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +89 -22
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +90 -23
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +110 -33
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +112 -35
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +17 -1
- package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
- package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +52 -1
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +52 -1
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.cjs +18 -0
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.mjs +15 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.cjs +21 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.mjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.cjs +15 -2
- package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
- package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/cms/api/getters.cjs +10 -0
- package/dist/packages/stack/src/plugins/cms/api/getters.mjs +10 -1
- package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +48 -0
- package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +75 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +76 -1
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +29 -0
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +26 -0
- package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +9 -0
- package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +9 -1
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +62 -1
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +63 -2
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.cjs +37 -0
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.mjs +33 -0
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/kanban/api/mutations.cjs +91 -0
- package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +34 -1
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +34 -1
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.cjs +26 -0
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.mjs +23 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.cjs +30 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.mjs +26 -0
- package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
- package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
- package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +10 -0
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +11 -1
- package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +89 -0
- package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
- package/dist/packages/stack/src/plugins/utils.cjs +6 -0
- package/dist/packages/stack/src/plugins/utils.mjs +6 -1
- package/dist/plugins/ai-chat/api/index.d.cts +1 -1
- package/dist/plugins/ai-chat/api/index.d.mts +1 -1
- package/dist/plugins/ai-chat/api/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/index.d.ts +2 -2
- package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
- package/dist/plugins/blog/api/index.cjs +5 -0
- package/dist/plugins/blog/api/index.d.cts +19 -4
- package/dist/plugins/blog/api/index.d.mts +19 -4
- package/dist/plugins/blog/api/index.d.ts +19 -4
- package/dist/plugins/blog/api/index.mjs +2 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.mts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.ts +3 -3
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.cjs +6 -5
- package/dist/plugins/blog/query-keys.d.cts +8 -387
- package/dist/plugins/blog/query-keys.d.mts +8 -387
- package/dist/plugins/blog/query-keys.d.ts +8 -387
- package/dist/plugins/blog/query-keys.mjs +6 -5
- package/dist/plugins/client/index.cjs +1 -0
- package/dist/plugins/client/index.d.cts +8 -1
- package/dist/plugins/client/index.d.mts +8 -1
- package/dist/plugins/client/index.d.ts +8 -1
- package/dist/plugins/client/index.mjs +1 -1
- package/dist/plugins/cms/api/index.cjs +8 -0
- package/dist/plugins/cms/api/index.d.cts +7 -219
- package/dist/plugins/cms/api/index.d.mts +7 -219
- package/dist/plugins/cms/api/index.d.ts +7 -219
- package/dist/plugins/cms/api/index.mjs +3 -1
- package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
- package/dist/plugins/cms/query-keys.cjs +2 -1
- package/dist/plugins/cms/query-keys.d.cts +5 -9
- package/dist/plugins/cms/query-keys.d.mts +5 -9
- package/dist/plugins/cms/query-keys.d.ts +5 -9
- package/dist/plugins/cms/query-keys.mjs +2 -1
- package/dist/plugins/form-builder/api/index.cjs +6 -0
- package/dist/plugins/form-builder/api/index.d.cts +7 -211
- package/dist/plugins/form-builder/api/index.d.mts +7 -211
- package/dist/plugins/form-builder/api/index.d.ts +7 -211
- package/dist/plugins/form-builder/api/index.mjs +2 -1
- package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.cjs +3 -2
- package/dist/plugins/form-builder/query-keys.d.cts +6 -6
- package/dist/plugins/form-builder/query-keys.d.mts +6 -6
- package/dist/plugins/form-builder/query-keys.d.ts +6 -6
- package/dist/plugins/form-builder/query-keys.mjs +3 -2
- package/dist/plugins/kanban/api/index.cjs +10 -0
- package/dist/plugins/kanban/api/index.d.cts +17 -392
- package/dist/plugins/kanban/api/index.d.mts +17 -392
- package/dist/plugins/kanban/api/index.d.ts +17 -392
- package/dist/plugins/kanban/api/index.mjs +3 -0
- package/dist/plugins/kanban/client/components/index.d.cts +1 -1
- package/dist/plugins/kanban/client/components/index.d.mts +1 -1
- package/dist/plugins/kanban/client/components/index.d.ts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.cjs +2 -9
- package/dist/plugins/kanban/query-keys.d.cts +4 -16
- package/dist/plugins/kanban/query-keys.d.mts +4 -16
- package/dist/plugins/kanban/query-keys.d.ts +4 -16
- package/dist/plugins/kanban/query-keys.mjs +2 -9
- package/dist/plugins/ui-builder/index.d.cts +1 -1
- package/dist/plugins/ui-builder/index.d.mts +1 -1
- package/dist/plugins/ui-builder/index.d.ts +1 -1
- package/dist/shared/stack.B7ONvlD_.d.mts +293 -0
- package/dist/shared/{stack.BeSm90va.d.ts → stack.BEn34wW6.d.ts} +60 -2
- package/dist/shared/stack.BUkC2EsZ.d.cts +327 -0
- package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
- package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
- package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
- package/dist/shared/stack.BepFXT3w.d.mts +500 -0
- package/dist/shared/stack.CL8ts1Mu.d.ts +419 -0
- package/dist/shared/{stack.CXjzTMsb.d.cts → stack.CVDTkMoO.d.cts} +7 -1
- package/dist/shared/{stack.CXjzTMsb.d.mts → stack.CVDTkMoO.d.mts} +7 -1
- package/dist/shared/{stack.CXjzTMsb.d.ts → stack.CVDTkMoO.d.ts} +7 -1
- package/dist/shared/stack.CczspVn2.d.mts +327 -0
- package/dist/shared/stack.CgWzG5jH.d.ts +500 -0
- package/dist/shared/stack.D3GB6wKv.d.cts +500 -0
- package/dist/shared/stack.DASmUVjX.d.ts +327 -0
- package/dist/shared/{stack.QD1y_7NY.d.cts → stack.DJaKVY7v.d.cts} +1 -1
- package/dist/shared/{stack.QD1y_7NY.d.mts → stack.DJaKVY7v.d.mts} +1 -1
- package/dist/shared/{stack.QD1y_7NY.d.ts → stack.DJaKVY7v.d.ts} +1 -1
- package/dist/shared/{stack.Dg09R0oB.d.mts → stack.DTDxgFj8.d.mts} +60 -2
- package/dist/shared/{stack.CMh_EdxW.d.cts → stack.DWoCZff7.d.cts} +60 -2
- package/dist/shared/{stack.CIrIsc-A.d.cts → stack.DdI5W6MB.d.cts} +7 -1
- package/dist/shared/{stack.CIrIsc-A.d.mts → stack.DdI5W6MB.d.mts} +7 -1
- package/dist/shared/{stack.CIrIsc-A.d.ts → stack.DdI5W6MB.d.ts} +7 -1
- package/dist/shared/stack.Dk5r4W1F.d.mts +419 -0
- package/dist/shared/stack.Kq2-QzOC.d.ts +293 -0
- package/dist/shared/stack.heOA9gzA.d.cts +419 -0
- package/dist/shared/stack.kcdnD4gA.d.cts +293 -0
- package/package.json +16 -3
- package/src/client/components/compose.tsx +7 -4
- package/src/plugins/ai-chat/api/page-tools.ts +111 -0
- package/src/plugins/ai-chat/api/plugin.ts +180 -9
- package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
- package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
- package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
- package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
- package/src/plugins/ai-chat/schemas.ts +16 -0
- package/src/plugins/blog/api/index.ts +2 -0
- package/src/plugins/blog/api/plugin.ts +85 -0
- package/src/plugins/blog/api/query-key-defs.ts +46 -0
- package/src/plugins/blog/api/serializers.ts +27 -0
- package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
- package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
- package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
- package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
- package/src/plugins/blog/client/plugin.tsx +19 -0
- package/src/plugins/blog/query-keys.ts +5 -7
- package/src/plugins/client/index.ts +1 -1
- package/src/plugins/cms/api/getters.ts +24 -0
- package/src/plugins/cms/api/index.ts +14 -1
- package/src/plugins/cms/api/mutations.ts +84 -0
- package/src/plugins/cms/api/plugin.ts +114 -0
- package/src/plugins/cms/api/query-key-defs.ts +53 -0
- package/src/plugins/cms/api/serializers.ts +12 -0
- package/src/plugins/cms/client/plugin.tsx +19 -0
- package/src/plugins/cms/query-keys.ts +2 -1
- package/src/plugins/form-builder/api/getters.ts +23 -0
- package/src/plugins/form-builder/api/index.ts +15 -2
- package/src/plugins/form-builder/api/plugin.ts +91 -0
- package/src/plugins/form-builder/api/query-key-defs.ts +79 -0
- package/src/plugins/form-builder/api/serializers.ts +12 -0
- package/src/plugins/form-builder/client/plugin.tsx +19 -0
- package/src/plugins/form-builder/query-keys.ts +6 -2
- package/src/plugins/kanban/api/index.ts +9 -0
- package/src/plugins/kanban/api/mutations.ts +169 -0
- package/src/plugins/kanban/api/plugin.ts +61 -0
- package/src/plugins/kanban/api/query-key-defs.ts +54 -0
- package/src/plugins/kanban/api/serializers.ts +49 -0
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
- package/src/plugins/kanban/client/plugin.tsx +13 -0
- package/src/plugins/kanban/query-keys.ts +2 -9
- package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
- package/src/plugins/utils.ts +19 -0
- package/dist/shared/{stack.BkYlUT_8.d.cts → stack.BQmuNl5p.d.cts} +6 -6
- package/dist/shared/{stack.BkYlUT_8.d.mts → stack.BQmuNl5p.d.mts} +6 -6
- package/dist/shared/{stack.BkYlUT_8.d.ts → stack.BQmuNl5p.d.ts} +6 -6
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Post, Tag, SerializedPost, SerializedTag } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serialize a Tag for SSR/SSG use (convert dates to strings).
|
|
5
|
+
* Pure function — no DB access, no hooks.
|
|
6
|
+
*/
|
|
7
|
+
export function serializeTag(tag: Tag): SerializedTag {
|
|
8
|
+
return {
|
|
9
|
+
...tag,
|
|
10
|
+
createdAt: tag.createdAt.toISOString(),
|
|
11
|
+
updatedAt: tag.updatedAt.toISOString(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Serialize a Post (with tags) for SSR/SSG use (convert dates to strings).
|
|
17
|
+
* Pure function — no DB access, no hooks.
|
|
18
|
+
*/
|
|
19
|
+
export function serializePost(post: Post & { tags: Tag[] }): SerializedPost {
|
|
20
|
+
return {
|
|
21
|
+
...post,
|
|
22
|
+
createdAt: post.createdAt.toISOString(),
|
|
23
|
+
updatedAt: post.updatedAt.toISOString(),
|
|
24
|
+
publishedAt: post.publishedAt?.toISOString(),
|
|
25
|
+
tags: post.tags.map(serializeTag),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
|
|
41
41
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
42
42
|
import { Loader2 } from "lucide-react";
|
|
43
|
-
import { lazy, memo, Suspense, useMemo, useState } from "react";
|
|
43
|
+
import { lazy, memo, Suspense, useEffect, useMemo, useState } from "react";
|
|
44
44
|
import {
|
|
45
45
|
type FieldPath,
|
|
46
46
|
type SubmitHandler,
|
|
@@ -325,6 +325,10 @@ const CustomPostUpdateSchema = PostUpdateSchema.omit({
|
|
|
325
325
|
type AddPostFormProps = {
|
|
326
326
|
onClose: () => void;
|
|
327
327
|
onSuccess: (post: { published: boolean }) => void;
|
|
328
|
+
/** Called once with the form instance so parent components can access form state */
|
|
329
|
+
onFormReady?: (
|
|
330
|
+
form: UseFormReturn<z.input<typeof CustomPostCreateSchema>>,
|
|
331
|
+
) => void;
|
|
328
332
|
};
|
|
329
333
|
|
|
330
334
|
const addPostFormPropsAreEqual = (
|
|
@@ -333,10 +337,15 @@ const addPostFormPropsAreEqual = (
|
|
|
333
337
|
): boolean => {
|
|
334
338
|
if (prevProps.onClose !== nextProps.onClose) return false;
|
|
335
339
|
if (prevProps.onSuccess !== nextProps.onSuccess) return false;
|
|
340
|
+
if (prevProps.onFormReady !== nextProps.onFormReady) return false;
|
|
336
341
|
return true;
|
|
337
342
|
};
|
|
338
343
|
|
|
339
|
-
const AddPostFormComponent = ({
|
|
344
|
+
const AddPostFormComponent = ({
|
|
345
|
+
onClose,
|
|
346
|
+
onSuccess,
|
|
347
|
+
onFormReady,
|
|
348
|
+
}: AddPostFormProps) => {
|
|
340
349
|
const [featuredImageUploading, setFeaturedImageUploading] = useState(false);
|
|
341
350
|
const { localization } = usePluginOverrides<
|
|
342
351
|
BlogPluginOverrides,
|
|
@@ -393,6 +402,12 @@ const AddPostFormComponent = ({ onClose, onSuccess }: AddPostFormProps) => {
|
|
|
393
402
|
},
|
|
394
403
|
});
|
|
395
404
|
|
|
405
|
+
// Expose form instance to parent for AI context integration
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
onFormReady?.(form);
|
|
408
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
409
|
+
}, []);
|
|
410
|
+
|
|
396
411
|
return (
|
|
397
412
|
<PostFormBody
|
|
398
413
|
form={form}
|
|
@@ -417,6 +432,10 @@ type EditPostFormProps = {
|
|
|
417
432
|
onClose: () => void;
|
|
418
433
|
onSuccess: (post: { slug: string; published: boolean }) => void;
|
|
419
434
|
onDelete?: () => void;
|
|
435
|
+
/** Called once with the form instance so parent components can access form state */
|
|
436
|
+
onFormReady?: (
|
|
437
|
+
form: UseFormReturn<z.input<typeof CustomPostUpdateSchema>>,
|
|
438
|
+
) => void;
|
|
420
439
|
};
|
|
421
440
|
|
|
422
441
|
const editPostFormPropsAreEqual = (
|
|
@@ -427,6 +446,7 @@ const editPostFormPropsAreEqual = (
|
|
|
427
446
|
if (prevProps.onClose !== nextProps.onClose) return false;
|
|
428
447
|
if (prevProps.onSuccess !== nextProps.onSuccess) return false;
|
|
429
448
|
if (prevProps.onDelete !== nextProps.onDelete) return false;
|
|
449
|
+
if (prevProps.onFormReady !== nextProps.onFormReady) return false;
|
|
430
450
|
return true;
|
|
431
451
|
};
|
|
432
452
|
|
|
@@ -435,6 +455,7 @@ const EditPostFormComponent = ({
|
|
|
435
455
|
onClose,
|
|
436
456
|
onSuccess,
|
|
437
457
|
onDelete,
|
|
458
|
+
onFormReady,
|
|
438
459
|
}: EditPostFormProps) => {
|
|
439
460
|
const [featuredImageUploading, setFeaturedImageUploading] = useState(false);
|
|
440
461
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
@@ -537,6 +558,12 @@ const EditPostFormComponent = ({
|
|
|
537
558
|
values: initialData as z.input<typeof schema>,
|
|
538
559
|
});
|
|
539
560
|
|
|
561
|
+
// Expose form instance to parent for AI context integration
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
onFormReady?.(form);
|
|
564
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
565
|
+
}, []);
|
|
566
|
+
|
|
540
567
|
if (!post) {
|
|
541
568
|
return <EmptyList message={localization.BLOG_PAGE_NOT_FOUND_DESCRIPTION} />;
|
|
542
569
|
}
|
|
@@ -7,6 +7,10 @@ import { PageWrapper } from "../shared/page-wrapper";
|
|
|
7
7
|
import { BLOG_LOCALIZATION } from "../../localization";
|
|
8
8
|
import type { BlogPluginOverrides } from "../../overrides";
|
|
9
9
|
import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
|
|
10
|
+
import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
|
|
11
|
+
import { useRef, useCallback } from "react";
|
|
12
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
13
|
+
import { createFillBlogFormHandler } from "./fill-blog-form-handler";
|
|
10
14
|
|
|
11
15
|
// Internal component with actual page content
|
|
12
16
|
export function EditPostPage({ slug }: { slug: string }) {
|
|
@@ -36,6 +40,29 @@ export function EditPostPage({ slug }: { slug: string }) {
|
|
|
36
40
|
},
|
|
37
41
|
});
|
|
38
42
|
|
|
43
|
+
// Ref to capture the form instance from EditPostForm via onFormReady callback
|
|
44
|
+
const formRef = useRef<UseFormReturn<any> | null>(null);
|
|
45
|
+
const handleFormReady = useCallback((form: UseFormReturn<any>) => {
|
|
46
|
+
formRef.current = form;
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
// Register AI context so the chat can fill in the edit form
|
|
50
|
+
useRegisterPageAIContext({
|
|
51
|
+
routeName: "blog-edit-post",
|
|
52
|
+
pageDescription: `User is editing a blog post (slug: "${slug}") in the admin editor.`,
|
|
53
|
+
suggestions: [
|
|
54
|
+
"Improve this post's title",
|
|
55
|
+
"Rewrite the intro paragraph",
|
|
56
|
+
"Suggest better tags",
|
|
57
|
+
],
|
|
58
|
+
clientTools: {
|
|
59
|
+
fillBlogForm: createFillBlogFormHandler(
|
|
60
|
+
formRef,
|
|
61
|
+
"Form updated successfully",
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
39
66
|
const handleClose = () => {
|
|
40
67
|
navigate(`${basePath}/blog`);
|
|
41
68
|
};
|
|
@@ -61,6 +88,7 @@ export function EditPostPage({ slug }: { slug: string }) {
|
|
|
61
88
|
onClose={handleClose}
|
|
62
89
|
onSuccess={handleSuccess}
|
|
63
90
|
onDelete={handleDelete}
|
|
91
|
+
onFormReady={handleFormReady}
|
|
64
92
|
/>
|
|
65
93
|
</PageWrapper>
|
|
66
94
|
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a `fillBlogForm` client tool handler bound to a form ref.
|
|
6
|
+
* Used by both the new-post and edit-post pages so the field-mapping
|
|
7
|
+
* logic stays in one place when the form schema changes.
|
|
8
|
+
*/
|
|
9
|
+
export function createFillBlogFormHandler(
|
|
10
|
+
formRef: RefObject<UseFormReturn<any> | null>,
|
|
11
|
+
successMessage: string,
|
|
12
|
+
) {
|
|
13
|
+
return async ({
|
|
14
|
+
title,
|
|
15
|
+
content,
|
|
16
|
+
excerpt,
|
|
17
|
+
tags,
|
|
18
|
+
}: {
|
|
19
|
+
title?: string;
|
|
20
|
+
content?: string;
|
|
21
|
+
excerpt?: string;
|
|
22
|
+
tags?: string[];
|
|
23
|
+
}) => {
|
|
24
|
+
const form = formRef.current;
|
|
25
|
+
if (!form) return { success: false, message: "Form not ready" };
|
|
26
|
+
if (title !== undefined)
|
|
27
|
+
form.setValue("title", title, { shouldValidate: true });
|
|
28
|
+
if (content !== undefined)
|
|
29
|
+
form.setValue("content", content, { shouldValidate: true });
|
|
30
|
+
if (excerpt !== undefined) form.setValue("excerpt", excerpt);
|
|
31
|
+
if (tags !== undefined)
|
|
32
|
+
form.setValue(
|
|
33
|
+
"tags",
|
|
34
|
+
tags.map((name: string) => ({ name })),
|
|
35
|
+
);
|
|
36
|
+
return { success: true, message: successMessage };
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -7,6 +7,10 @@ import { PageWrapper } from "../shared/page-wrapper";
|
|
|
7
7
|
import type { BlogPluginOverrides } from "../../overrides";
|
|
8
8
|
import { BLOG_LOCALIZATION } from "../../localization";
|
|
9
9
|
import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
|
|
10
|
+
import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
|
|
11
|
+
import { useRef, useCallback } from "react";
|
|
12
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
13
|
+
import { createFillBlogFormHandler } from "./fill-blog-form-handler";
|
|
10
14
|
|
|
11
15
|
// Internal component with actual page content
|
|
12
16
|
export function NewPostPage() {
|
|
@@ -35,6 +39,30 @@ export function NewPostPage() {
|
|
|
35
39
|
},
|
|
36
40
|
});
|
|
37
41
|
|
|
42
|
+
// Ref to capture the form instance from AddPostForm via onFormReady callback
|
|
43
|
+
const formRef = useRef<UseFormReturn<any> | null>(null);
|
|
44
|
+
const handleFormReady = useCallback((form: UseFormReturn<any>) => {
|
|
45
|
+
formRef.current = form;
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Register AI context so the chat can fill in the new post form
|
|
49
|
+
useRegisterPageAIContext({
|
|
50
|
+
routeName: "blog-new-post",
|
|
51
|
+
pageDescription:
|
|
52
|
+
"User is creating a new blog post in the admin editor. IMPORTANT: When asked to write, draft, or create a blog post, you MUST call the fillBlogForm tool to populate the form fields directly — do NOT just output the text in your response.",
|
|
53
|
+
suggestions: [
|
|
54
|
+
"Write a post about AI trends",
|
|
55
|
+
"Draft an intro paragraph",
|
|
56
|
+
"Suggest 5 tags for this post",
|
|
57
|
+
],
|
|
58
|
+
clientTools: {
|
|
59
|
+
fillBlogForm: createFillBlogFormHandler(
|
|
60
|
+
formRef,
|
|
61
|
+
"Form filled successfully",
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
38
66
|
const handleClose = () => {
|
|
39
67
|
navigate(`${basePath}/blog`);
|
|
40
68
|
};
|
|
@@ -54,7 +82,11 @@ export function NewPostPage() {
|
|
|
54
82
|
title={localization.BLOG_POST_ADD_TITLE}
|
|
55
83
|
description={localization.BLOG_POST_ADD_DESCRIPTION}
|
|
56
84
|
/>
|
|
57
|
-
<AddPostForm
|
|
85
|
+
<AddPostForm
|
|
86
|
+
onClose={handleClose}
|
|
87
|
+
onSuccess={handleSuccess}
|
|
88
|
+
onFormReady={handleFormReady}
|
|
89
|
+
/>
|
|
58
90
|
</PageWrapper>
|
|
59
91
|
);
|
|
60
92
|
}
|
|
@@ -20,6 +20,7 @@ import { Badge } from "@workspace/ui/components/badge";
|
|
|
20
20
|
import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
|
|
21
21
|
import { OnThisPage, OnThisPageSelect } from "../shared/on-this-page";
|
|
22
22
|
import type { SerializedPost } from "../../../types";
|
|
23
|
+
import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
|
|
23
24
|
|
|
24
25
|
// Internal component with actual page content
|
|
25
26
|
export function PostPage({ slug }: { slug: string }) {
|
|
@@ -64,6 +65,25 @@ export function PostPage({ slug }: { slug: string }) {
|
|
|
64
65
|
enabled: !!post,
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
// Register page AI context so the chat can summarize and discuss this post
|
|
69
|
+
useRegisterPageAIContext(
|
|
70
|
+
post
|
|
71
|
+
? {
|
|
72
|
+
routeName: "blog-post",
|
|
73
|
+
pageDescription:
|
|
74
|
+
`Blog post: "${post.title}"\nAuthor: ${post.authorId ?? "Unknown"}\n\n${post.content ?? ""}`.slice(
|
|
75
|
+
0,
|
|
76
|
+
16000,
|
|
77
|
+
),
|
|
78
|
+
suggestions: [
|
|
79
|
+
"Summarize this post",
|
|
80
|
+
"What are the key takeaways?",
|
|
81
|
+
"Explain this in simpler terms",
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
: null,
|
|
85
|
+
);
|
|
86
|
+
|
|
67
87
|
if (!slug || !post) {
|
|
68
88
|
return (
|
|
69
89
|
<PageWrapper>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineClientPlugin,
|
|
3
3
|
createApiClient,
|
|
4
|
+
isConnectionError,
|
|
4
5
|
} from "@btst/stack/plugins/client";
|
|
5
6
|
import { createRoute } from "@btst/yar";
|
|
6
7
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -226,6 +227,12 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
|
|
|
226
227
|
} catch (error) {
|
|
227
228
|
// Error hook - log the error but don't throw during SSR
|
|
228
229
|
// Let Error Boundaries handle errors when components render
|
|
230
|
+
if (isConnectionError(error)) {
|
|
231
|
+
console.warn(
|
|
232
|
+
"[btst/blog] route.loader() failed — no server running at build time. " +
|
|
233
|
+
"Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
229
236
|
if (hooks?.onLoadError) {
|
|
230
237
|
await hooks.onLoadError(error as Error, context);
|
|
231
238
|
}
|
|
@@ -299,6 +306,12 @@ function createPostLoader(
|
|
|
299
306
|
} catch (error) {
|
|
300
307
|
// Error hook - log the error but don't throw during SSR
|
|
301
308
|
// Let Error Boundaries handle errors when components render
|
|
309
|
+
if (isConnectionError(error)) {
|
|
310
|
+
console.warn(
|
|
311
|
+
"[btst/blog] route.loader() failed — no server running at build time. " +
|
|
312
|
+
"Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
302
315
|
if (hooks?.onLoadError) {
|
|
303
316
|
await hooks.onLoadError(error as Error, context);
|
|
304
317
|
}
|
|
@@ -398,6 +411,12 @@ function createTagLoader(tagSlug: string, config: BlogClientConfig) {
|
|
|
398
411
|
await hooks.onLoadError(error, context);
|
|
399
412
|
}
|
|
400
413
|
} catch (error) {
|
|
414
|
+
if (isConnectionError(error)) {
|
|
415
|
+
console.warn(
|
|
416
|
+
"[btst/blog] route.loader() failed — no server running at build time. " +
|
|
417
|
+
"Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
|
|
418
|
+
);
|
|
419
|
+
}
|
|
401
420
|
if (hooks?.onLoadError) {
|
|
402
421
|
await hooks.onLoadError(error as Error, context);
|
|
403
422
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import type { BlogApiRouter } from "./api";
|
|
6
6
|
import { createApiClient } from "@btst/stack/plugins/client";
|
|
7
7
|
import type { SerializedPost, SerializedTag } from "./types";
|
|
8
|
+
import { postsListDiscriminator } from "./api/query-key-defs";
|
|
8
9
|
|
|
9
10
|
interface PostsListParams {
|
|
10
11
|
query?: string;
|
|
@@ -71,15 +72,12 @@ function createPostsQueries(
|
|
|
71
72
|
return createQueryKeys("posts", {
|
|
72
73
|
list: (params?: PostsListParams) => ({
|
|
73
74
|
queryKey: [
|
|
74
|
-
{
|
|
75
|
-
query:
|
|
76
|
-
params?.query !== undefined && params?.query?.trim() === ""
|
|
77
|
-
? undefined
|
|
78
|
-
: params?.query,
|
|
79
|
-
limit: params?.limit ?? 10,
|
|
75
|
+
postsListDiscriminator({
|
|
80
76
|
published: params?.published ?? true,
|
|
77
|
+
limit: params?.limit ?? 10,
|
|
81
78
|
tagSlug: params?.tagSlug,
|
|
82
|
-
|
|
79
|
+
query: params?.query,
|
|
80
|
+
}),
|
|
83
81
|
],
|
|
84
82
|
queryFn: async ({ pageParam }: { pageParam?: number }) => {
|
|
85
83
|
try {
|
|
@@ -18,7 +18,7 @@ export type {
|
|
|
18
18
|
PluginOverrides,
|
|
19
19
|
} from "../../types";
|
|
20
20
|
|
|
21
|
-
export { createApiClient } from "../utils";
|
|
21
|
+
export { createApiClient, isConnectionError } from "../utils";
|
|
22
22
|
|
|
23
23
|
// Re-export Yar types needed for plugins
|
|
24
24
|
export type { Route } from "@btst/yar";
|
|
@@ -191,6 +191,30 @@ export async function getAllContentItems(
|
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Retrieve a single content item by its ID.
|
|
196
|
+
* Returns null if the item is not found.
|
|
197
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
198
|
+
*
|
|
199
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
200
|
+
* responsible for any access-control checks before invoking this function.
|
|
201
|
+
*
|
|
202
|
+
* @param adapter - The database adapter
|
|
203
|
+
* @param id - The content item ID (UUID)
|
|
204
|
+
*/
|
|
205
|
+
export async function getContentItemById(
|
|
206
|
+
adapter: Adapter,
|
|
207
|
+
id: string,
|
|
208
|
+
): Promise<SerializedContentItemWithType | null> {
|
|
209
|
+
const item = await adapter.findOne<ContentItemWithType>({
|
|
210
|
+
model: "contentItem",
|
|
211
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
212
|
+
join: { contentType: true },
|
|
213
|
+
});
|
|
214
|
+
if (!item) return null;
|
|
215
|
+
return serializeContentItemWithType(item);
|
|
216
|
+
}
|
|
217
|
+
|
|
194
218
|
/**
|
|
195
219
|
* Retrieve a single content item by its slug within a content type.
|
|
196
220
|
* Returns null if the content type or item is not found.
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
cmsBackendPlugin,
|
|
3
|
+
type CMSApiRouter,
|
|
4
|
+
type CMSRouteKey,
|
|
5
|
+
} from "./plugin";
|
|
2
6
|
export {
|
|
3
7
|
getAllContentTypes,
|
|
4
8
|
getAllContentItems,
|
|
5
9
|
getContentItemBySlug,
|
|
10
|
+
getContentItemById,
|
|
11
|
+
serializeContentType,
|
|
12
|
+
serializeContentItem,
|
|
13
|
+
serializeContentItemWithType,
|
|
6
14
|
} from "./getters";
|
|
15
|
+
export {
|
|
16
|
+
createCMSContentItem,
|
|
17
|
+
type CreateCMSContentItemInput,
|
|
18
|
+
} from "./mutations";
|
|
19
|
+
export { CMS_QUERY_KEYS } from "./query-key-defs";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Adapter } from "@btst/db";
|
|
2
|
+
import type { ContentType, ContentItem } from "../types";
|
|
3
|
+
import { serializeContentItem } from "./getters";
|
|
4
|
+
import type { SerializedContentItem } from "../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Input for creating a new CMS content item.
|
|
8
|
+
*/
|
|
9
|
+
export interface CreateCMSContentItemInput {
|
|
10
|
+
/** URL-safe slug for the item */
|
|
11
|
+
slug: string;
|
|
12
|
+
/** Arbitrary data payload — should match the content type schema */
|
|
13
|
+
data: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a new content item for a content type (looked up by slug).
|
|
18
|
+
*
|
|
19
|
+
* Bypasses Zod schema validation and relation processing — the caller is
|
|
20
|
+
* responsible for providing valid, relation-free data. For relation fields or
|
|
21
|
+
* schema validation, use the HTTP endpoint instead.
|
|
22
|
+
*
|
|
23
|
+
* Throws if the content type is not found or the slug is already taken within
|
|
24
|
+
* that content type.
|
|
25
|
+
*
|
|
26
|
+
* @remarks **Security:** No authorization hooks (`onBeforeCreate`, `onAfterCreate`)
|
|
27
|
+
* are called. The caller is responsible for any access-control checks before
|
|
28
|
+
* invoking this function.
|
|
29
|
+
*
|
|
30
|
+
* @param adapter - The database adapter
|
|
31
|
+
* @param contentTypeSlug - Slug of the target content type
|
|
32
|
+
* @param input - Item slug and data payload
|
|
33
|
+
*/
|
|
34
|
+
export async function createCMSContentItem(
|
|
35
|
+
adapter: Adapter,
|
|
36
|
+
contentTypeSlug: string,
|
|
37
|
+
input: CreateCMSContentItemInput,
|
|
38
|
+
): Promise<SerializedContentItem> {
|
|
39
|
+
const contentType = await adapter.findOne<ContentType>({
|
|
40
|
+
model: "contentType",
|
|
41
|
+
where: [
|
|
42
|
+
{
|
|
43
|
+
field: "slug",
|
|
44
|
+
value: contentTypeSlug,
|
|
45
|
+
operator: "eq" as const,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!contentType) {
|
|
51
|
+
throw new Error(`Content type "${contentTypeSlug}" not found`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const existing = await adapter.findOne<ContentItem>({
|
|
55
|
+
model: "contentItem",
|
|
56
|
+
where: [
|
|
57
|
+
{
|
|
58
|
+
field: "contentTypeId",
|
|
59
|
+
value: contentType.id,
|
|
60
|
+
operator: "eq" as const,
|
|
61
|
+
},
|
|
62
|
+
{ field: "slug", value: input.slug, operator: "eq" as const },
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (existing) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Content item with slug "${input.slug}" already exists in type "${contentTypeSlug}"`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const item = await adapter.create<ContentItem>({
|
|
73
|
+
model: "contentItem",
|
|
74
|
+
data: {
|
|
75
|
+
contentTypeId: contentType.id,
|
|
76
|
+
slug: input.slug,
|
|
77
|
+
data: JSON.stringify(input.data),
|
|
78
|
+
createdAt: new Date(),
|
|
79
|
+
updatedAt: new Date(),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return serializeContentItem(item);
|
|
84
|
+
}
|
|
@@ -25,10 +25,38 @@ import {
|
|
|
25
25
|
getAllContentTypes,
|
|
26
26
|
getAllContentItems,
|
|
27
27
|
getContentItemBySlug,
|
|
28
|
+
getContentItemById,
|
|
28
29
|
serializeContentType,
|
|
29
30
|
serializeContentItem,
|
|
30
31
|
serializeContentItemWithType,
|
|
31
32
|
} from "./getters";
|
|
33
|
+
import { createCMSContentItem } from "./mutations";
|
|
34
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
35
|
+
import { CMS_QUERY_KEYS } from "./query-key-defs";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Route keys for the CMS plugin — matches the keys returned by
|
|
39
|
+
* `stackClient.router.getRoute(path).routeKey`.
|
|
40
|
+
*/
|
|
41
|
+
export type CMSRouteKey =
|
|
42
|
+
| "dashboard"
|
|
43
|
+
| "contentList"
|
|
44
|
+
| "newContent"
|
|
45
|
+
| "editContent";
|
|
46
|
+
|
|
47
|
+
interface CMSPrefetchForRoute {
|
|
48
|
+
(key: "dashboard" | "newContent", qc: QueryClient): Promise<void>;
|
|
49
|
+
(
|
|
50
|
+
key: "contentList",
|
|
51
|
+
qc: QueryClient,
|
|
52
|
+
params: { typeSlug: string },
|
|
53
|
+
): Promise<void>;
|
|
54
|
+
(
|
|
55
|
+
key: "editContent",
|
|
56
|
+
qc: QueryClient,
|
|
57
|
+
params: { typeSlug: string; id: string },
|
|
58
|
+
): Promise<void>;
|
|
59
|
+
}
|
|
32
60
|
|
|
33
61
|
/**
|
|
34
62
|
* Sync content types from config to database
|
|
@@ -443,6 +471,79 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
|
|
|
443
471
|
return syncPromise;
|
|
444
472
|
};
|
|
445
473
|
|
|
474
|
+
const getContentTypesWithCounts = async (adapter: Adapter) => {
|
|
475
|
+
const contentTypes = await getAllContentTypes(adapter);
|
|
476
|
+
return Promise.all(
|
|
477
|
+
contentTypes.map(async (ct) => {
|
|
478
|
+
const count: number = await adapter.count({
|
|
479
|
+
model: "contentItem",
|
|
480
|
+
where: [
|
|
481
|
+
{ field: "contentTypeId", value: ct.id, operator: "eq" as const },
|
|
482
|
+
],
|
|
483
|
+
});
|
|
484
|
+
return { ...ct, itemCount: count };
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const createCMSPrefetchForRoute = (adapter: Adapter): CMSPrefetchForRoute => {
|
|
490
|
+
return async function prefetchForRoute(
|
|
491
|
+
key: CMSRouteKey,
|
|
492
|
+
qc: QueryClient,
|
|
493
|
+
params?: Record<string, string>,
|
|
494
|
+
): Promise<void> {
|
|
495
|
+
// Sync content types once at the top — idempotent for concurrent SSG calls
|
|
496
|
+
await ensureSynced(adapter);
|
|
497
|
+
|
|
498
|
+
switch (key) {
|
|
499
|
+
case "dashboard":
|
|
500
|
+
case "newContent": {
|
|
501
|
+
const typesWithCounts = await getContentTypesWithCounts(adapter);
|
|
502
|
+
qc.setQueryData(CMS_QUERY_KEYS.typesList(), typesWithCounts);
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
case "contentList": {
|
|
506
|
+
const typeSlug = params?.typeSlug ?? "";
|
|
507
|
+
const [contentTypes, contentItems] = await Promise.all([
|
|
508
|
+
getContentTypesWithCounts(adapter),
|
|
509
|
+
getAllContentItems(adapter, typeSlug, { limit: 20, offset: 0 }),
|
|
510
|
+
]);
|
|
511
|
+
qc.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
|
|
512
|
+
qc.setQueryData(
|
|
513
|
+
CMS_QUERY_KEYS.contentList({ typeSlug, limit: 20, offset: 0 }),
|
|
514
|
+
{
|
|
515
|
+
pages: [
|
|
516
|
+
{
|
|
517
|
+
items: contentItems.items,
|
|
518
|
+
total: contentItems.total,
|
|
519
|
+
limit: contentItems.limit ?? 20,
|
|
520
|
+
offset: contentItems.offset ?? 0,
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
pageParams: [0],
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
case "editContent": {
|
|
529
|
+
const typeSlug = params?.typeSlug ?? "";
|
|
530
|
+
const id = params?.id ?? "";
|
|
531
|
+
const [contentTypes, item] = await Promise.all([
|
|
532
|
+
getContentTypesWithCounts(adapter),
|
|
533
|
+
id ? getContentItemById(adapter, id) : Promise.resolve(null),
|
|
534
|
+
]);
|
|
535
|
+
qc.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
|
|
536
|
+
if (id) {
|
|
537
|
+
qc.setQueryData(CMS_QUERY_KEYS.contentDetail(typeSlug, id), item);
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
default:
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
} as CMSPrefetchForRoute;
|
|
545
|
+
};
|
|
546
|
+
|
|
446
547
|
return defineBackendPlugin({
|
|
447
548
|
name: "cms",
|
|
448
549
|
|
|
@@ -464,6 +565,19 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
|
|
|
464
565
|
await ensureSynced(adapter);
|
|
465
566
|
return getContentItemBySlug(adapter, contentTypeSlug, slug);
|
|
466
567
|
},
|
|
568
|
+
getContentItemById: async (id: string) => {
|
|
569
|
+
await ensureSynced(adapter);
|
|
570
|
+
return getContentItemById(adapter, id);
|
|
571
|
+
},
|
|
572
|
+
prefetchForRoute: createCMSPrefetchForRoute(adapter),
|
|
573
|
+
// Mutations
|
|
574
|
+
createContentItem: async (
|
|
575
|
+
typeSlug: string,
|
|
576
|
+
input: Parameters<typeof createCMSContentItem>[2],
|
|
577
|
+
) => {
|
|
578
|
+
await ensureSynced(adapter);
|
|
579
|
+
return createCMSContentItem(adapter, typeSlug, input);
|
|
580
|
+
},
|
|
467
581
|
}),
|
|
468
582
|
|
|
469
583
|
routes: (adapter: Adapter) => {
|