@btst/stack 2.3.0 → 2.5.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 +87 -54
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +87 -54
- 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/client/plugin.cjs +14 -21
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +15 -22
- 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 +28 -45
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +22 -39
- 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 +23 -27
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +24 -28
- 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 +21 -18
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +21 -18
- package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +11 -15
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +12 -16
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +58 -62
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +58 -62
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +12 -12
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +13 -13
- 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 +92 -118
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +89 -115
- 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 +22 -29
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +23 -30
- 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/ui-builder/client/plugin.cjs +8 -8
- package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +9 -9
- package/dist/packages/stack/src/plugins/utils.cjs +42 -0
- package/dist/packages/stack/src/plugins/utils.mjs +41 -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 +10 -10
- package/dist/plugins/ai-chat/client/index.d.mts +10 -10
- package/dist/plugins/ai-chat/client/index.d.ts +10 -10
- 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.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +13 -13
- package/dist/plugins/blog/client/index.d.mts +13 -13
- package/dist/plugins/blog/client/index.d.ts +13 -13
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- 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 +2 -0
- package/dist/plugins/cms/api/index.d.cts +2 -2
- package/dist/plugins/cms/api/index.d.mts +2 -2
- package/dist/plugins/cms/api/index.d.ts +2 -2
- package/dist/plugins/cms/api/index.mjs +1 -0
- 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/client/index.d.cts +6 -6
- package/dist/plugins/cms/client/index.d.mts +6 -6
- package/dist/plugins/cms/client/index.d.ts +6 -6
- package/dist/plugins/cms/query-keys.d.cts +2 -2
- package/dist/plugins/cms/query-keys.d.mts +2 -2
- package/dist/plugins/cms/query-keys.d.ts +2 -2
- package/dist/plugins/form-builder/api/index.d.cts +2 -2
- package/dist/plugins/form-builder/api/index.d.mts +2 -2
- package/dist/plugins/form-builder/api/index.d.ts +2 -2
- 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/client/index.d.cts +6 -6
- package/dist/plugins/form-builder/client/index.d.mts +6 -6
- package/dist/plugins/form-builder/client/index.d.ts +6 -6
- package/dist/plugins/form-builder/query-keys.d.cts +2 -2
- package/dist/plugins/form-builder/query-keys.d.mts +2 -2
- package/dist/plugins/form-builder/query-keys.d.ts +2 -2
- package/dist/plugins/kanban/api/index.cjs +4 -0
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/api/index.mjs +1 -0
- package/dist/plugins/kanban/client/index.d.cts +12 -12
- package/dist/plugins/kanban/client/index.d.mts +12 -12
- package/dist/plugins/kanban/client/index.d.ts +12 -12
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/ui-builder/client/index.d.cts +3 -3
- package/dist/plugins/ui-builder/client/index.d.mts +3 -3
- package/dist/plugins/ui-builder/client/index.d.ts +3 -3
- package/dist/plugins/ui-builder/index.d.cts +2 -2
- package/dist/plugins/ui-builder/index.d.mts +2 -2
- package/dist/plugins/ui-builder/index.d.ts +2 -2
- package/dist/shared/{stack.C-WUPMT6.d.cts → stack.B2xZTSiO.d.cts} +4 -4
- package/dist/shared/{stack.B1EeBt1b.d.ts → stack.B58oHdqm.d.mts} +33 -3
- package/dist/shared/{stack.CVDTkMoO.d.mts → stack.B8QD11QU.d.cts} +7 -7
- package/dist/shared/{stack.CVDTkMoO.d.cts → stack.B8QD11QU.d.mts} +7 -7
- package/dist/shared/{stack.CVDTkMoO.d.ts → stack.B8QD11QU.d.ts} +7 -7
- package/dist/shared/{stack.CIP6QS9l.d.ts → stack.BDVEpue1.d.ts} +1 -1
- package/dist/shared/{stack.C5dtIncc.d.mts → stack.BTvbxZvw.d.cts} +1 -1
- 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.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
- package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.mts} +19 -19
- package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.ts} +19 -19
- package/dist/shared/{stack.CP68pFEH.d.mts → stack.C9Mg2Q46.d.cts} +33 -3
- package/dist/shared/{stack.BeSm90va.d.ts → stack.CTDVxbrA.d.ts} +72 -14
- package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
- package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CxaFNQCV.d.mts} +89 -34
- package/dist/shared/{stack.CMh_EdxW.d.cts → stack.D-b5zbPm.d.cts} +72 -14
- package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.DTtmJPQO.d.mts} +1 -1
- package/dist/shared/{stack.BKfolAyK.d.ts → stack.DXnclTG7.d.ts} +11 -11
- package/dist/shared/{stack.snB1EDP7.d.cts → stack.DaZM10cp.d.cts} +11 -11
- package/dist/shared/{stack.Dg09R0oB.d.mts → stack.FVWf2JhZ.d.mts} +72 -14
- package/dist/shared/{stack.BIXEI6v_.d.mts → stack.cfCkioTe.d.mts} +11 -11
- package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
- package/dist/shared/{stack.BpolpQpf.d.cts → stack.j75TpKh2.d.ts} +89 -34
- package/dist/shared/{stack.rTy7-wQU.d.mts → stack.n1_i1p2B.d.cts} +89 -34
- package/dist/shared/{stack.IdtKDRka.d.cts → stack.sO33ZDhK.d.ts} +33 -3
- package/package.json +14 -1
- 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 +228 -72
- 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/client/plugin.tsx +23 -31
- package/src/plugins/ai-chat/schemas.ts +16 -0
- package/src/plugins/blog/api/plugin.ts +31 -47
- 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 +36 -39
- package/src/plugins/client/index.ts +5 -1
- package/src/plugins/cms/api/index.ts +4 -0
- package/src/plugins/cms/api/mutations.ts +84 -0
- package/src/plugins/cms/api/plugin.ts +23 -17
- package/src/plugins/cms/client/plugin.tsx +18 -21
- package/src/plugins/cms/types.ts +7 -7
- package/src/plugins/form-builder/api/plugin.ts +64 -64
- package/src/plugins/form-builder/client/plugin.tsx +19 -18
- package/src/plugins/form-builder/types.ts +19 -24
- package/src/plugins/kanban/api/index.ts +6 -0
- package/src/plugins/kanban/api/mutations.ts +169 -0
- package/src/plugins/kanban/api/plugin.ts +123 -136
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
- package/src/plugins/kanban/client/plugin.tsx +35 -41
- package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
- package/src/plugins/ui-builder/client/plugin.tsx +11 -10
- package/src/plugins/ui-builder/types.ts +4 -4
- package/src/plugins/utils.ts +92 -1
- package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.cts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.mts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.ts} +2 -2
|
@@ -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>
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
defineClientPlugin,
|
|
3
3
|
createApiClient,
|
|
4
4
|
isConnectionError,
|
|
5
|
+
runClientHookWithShim,
|
|
5
6
|
} from "@btst/stack/plugins/client";
|
|
6
7
|
import { createRoute } from "@btst/yar";
|
|
7
8
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -91,16 +92,16 @@ export interface BlogClientConfig {
|
|
|
91
92
|
*/
|
|
92
93
|
export interface BlogClientHooks {
|
|
93
94
|
/**
|
|
94
|
-
* Called before loading posts list.
|
|
95
|
+
* Called before loading posts list. Throw an error to cancel loading.
|
|
95
96
|
* @param filter - Filter parameters including published status
|
|
96
97
|
* @param context - Loader context with path, params, etc.
|
|
97
98
|
*/
|
|
98
99
|
beforeLoadPosts?: (
|
|
99
100
|
filter: { published: boolean },
|
|
100
101
|
context: LoaderContext,
|
|
101
|
-
) => Promise<
|
|
102
|
+
) => Promise<void> | void;
|
|
102
103
|
/**
|
|
103
|
-
* Called after posts are loaded.
|
|
104
|
+
* Called after posts are loaded. Throw an error to cancel further processing.
|
|
104
105
|
* @param posts - Array of loaded posts or null
|
|
105
106
|
* @param filter - Filter parameters used
|
|
106
107
|
* @param context - Loader context
|
|
@@ -109,18 +110,18 @@ export interface BlogClientHooks {
|
|
|
109
110
|
posts: Post[] | null,
|
|
110
111
|
filter: { published: boolean },
|
|
111
112
|
context: LoaderContext,
|
|
112
|
-
) => Promise<
|
|
113
|
+
) => Promise<void> | void;
|
|
113
114
|
/**
|
|
114
|
-
* Called before loading a single post.
|
|
115
|
+
* Called before loading a single post. Throw an error to cancel loading.
|
|
115
116
|
* @param slug - Post slug being loaded
|
|
116
117
|
* @param context - Loader context
|
|
117
118
|
*/
|
|
118
119
|
beforeLoadPost?: (
|
|
119
120
|
slug: string,
|
|
120
121
|
context: LoaderContext,
|
|
121
|
-
) => Promise<
|
|
122
|
+
) => Promise<void> | void;
|
|
122
123
|
/**
|
|
123
|
-
* Called after a post is loaded.
|
|
124
|
+
* Called after a post is loaded. Throw an error to cancel further processing.
|
|
124
125
|
* @param post - Loaded post or null if not found
|
|
125
126
|
* @param slug - Post slug that was requested
|
|
126
127
|
* @param context - Loader context
|
|
@@ -129,17 +130,17 @@ export interface BlogClientHooks {
|
|
|
129
130
|
post: Post | null,
|
|
130
131
|
slug: string,
|
|
131
132
|
context: LoaderContext,
|
|
132
|
-
) => Promise<
|
|
133
|
+
) => Promise<void> | void;
|
|
133
134
|
/**
|
|
134
|
-
* Called before loading the new post page.
|
|
135
|
+
* Called before loading the new post page. Throw an error to cancel.
|
|
135
136
|
* @param context - Loader context
|
|
136
137
|
*/
|
|
137
|
-
beforeLoadNewPost?: (context: LoaderContext) => Promise<
|
|
138
|
+
beforeLoadNewPost?: (context: LoaderContext) => Promise<void> | void;
|
|
138
139
|
/**
|
|
139
|
-
* Called after the new post page is loaded.
|
|
140
|
+
* Called after the new post page is loaded. Throw an error to cancel.
|
|
140
141
|
* @param context - Loader context
|
|
141
142
|
*/
|
|
142
|
-
afterLoadNewPost?: (context: LoaderContext) => Promise<
|
|
143
|
+
afterLoadNewPost?: (context: LoaderContext) => Promise<void> | void;
|
|
143
144
|
/**
|
|
144
145
|
* Called when a loading error occurs
|
|
145
146
|
* @param error - The error that occurred
|
|
@@ -165,10 +166,10 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
|
|
|
165
166
|
try {
|
|
166
167
|
// Before hook
|
|
167
168
|
if (hooks?.beforeLoadPosts) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
await runClientHookWithShim(
|
|
170
|
+
() => hooks.beforeLoadPosts!({ published }, context),
|
|
171
|
+
"Load prevented by beforeLoadPosts hook",
|
|
172
|
+
);
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
const limit = 10;
|
|
@@ -202,14 +203,10 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
|
|
|
202
203
|
if (hooks?.afterLoadPosts) {
|
|
203
204
|
const posts =
|
|
204
205
|
queryClient.getQueryData<Post[]>(listQuery.queryKey) || null;
|
|
205
|
-
|
|
206
|
-
posts,
|
|
207
|
-
|
|
208
|
-
context,
|
|
206
|
+
await runClientHookWithShim(
|
|
207
|
+
() => hooks.afterLoadPosts!(posts, { published }, context),
|
|
208
|
+
"Load prevented by afterLoadPosts hook",
|
|
209
209
|
);
|
|
210
|
-
if (canContinue === false) {
|
|
211
|
-
throw new Error("Load prevented by afterLoadPosts hook");
|
|
212
|
-
}
|
|
213
210
|
}
|
|
214
211
|
|
|
215
212
|
// Check if there was an error after afterLoadPosts hook
|
|
@@ -263,10 +260,10 @@ function createPostLoader(
|
|
|
263
260
|
try {
|
|
264
261
|
// Before hook
|
|
265
262
|
if (hooks?.beforeLoadPost) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
263
|
+
await runClientHookWithShim(
|
|
264
|
+
() => hooks.beforeLoadPost!(slug, context),
|
|
265
|
+
"Load prevented by beforeLoadPost hook",
|
|
266
|
+
);
|
|
270
267
|
}
|
|
271
268
|
|
|
272
269
|
const client = createApiClient<BlogApiRouter>({
|
|
@@ -285,10 +282,10 @@ function createPostLoader(
|
|
|
285
282
|
if (hooks?.afterLoadPost) {
|
|
286
283
|
const post =
|
|
287
284
|
queryClient.getQueryData<Post>(postQuery.queryKey) || null;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
285
|
+
await runClientHookWithShim(
|
|
286
|
+
() => hooks.afterLoadPost!(post, slug, context),
|
|
287
|
+
"Load prevented by afterLoadPost hook",
|
|
288
|
+
);
|
|
292
289
|
}
|
|
293
290
|
|
|
294
291
|
// Check if there was an error after afterLoadPost hook
|
|
@@ -337,18 +334,18 @@ function createNewPostLoader(config: BlogClientConfig) {
|
|
|
337
334
|
try {
|
|
338
335
|
// Before hook
|
|
339
336
|
if (hooks?.beforeLoadNewPost) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
337
|
+
await runClientHookWithShim(
|
|
338
|
+
() => hooks.beforeLoadNewPost!(context),
|
|
339
|
+
"Load prevented by beforeLoadNewPost hook",
|
|
340
|
+
);
|
|
344
341
|
}
|
|
345
342
|
|
|
346
343
|
// After hook
|
|
347
344
|
if (hooks?.afterLoadNewPost) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
345
|
+
await runClientHookWithShim(
|
|
346
|
+
() => hooks.afterLoadNewPost!(context),
|
|
347
|
+
"Load prevented by afterLoadNewPost hook",
|
|
348
|
+
);
|
|
352
349
|
}
|
|
353
350
|
} catch (error) {
|
|
354
351
|
// Error hook - log the error but don't throw during SSR
|
|
@@ -18,7 +18,11 @@ export type {
|
|
|
18
18
|
PluginOverrides,
|
|
19
19
|
} from "../../types";
|
|
20
20
|
|
|
21
|
-
export {
|
|
21
|
+
export {
|
|
22
|
+
createApiClient,
|
|
23
|
+
isConnectionError,
|
|
24
|
+
runClientHookWithShim,
|
|
25
|
+
} from "../utils";
|
|
22
26
|
|
|
23
27
|
// Re-export Yar types needed for plugins
|
|
24
28
|
export type { Route } from "@btst/yar";
|
|
@@ -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
|
+
}
|
|
@@ -30,8 +30,10 @@ import {
|
|
|
30
30
|
serializeContentItem,
|
|
31
31
|
serializeContentItemWithType,
|
|
32
32
|
} from "./getters";
|
|
33
|
+
import { createCMSContentItem } from "./mutations";
|
|
33
34
|
import type { QueryClient } from "@tanstack/react-query";
|
|
34
35
|
import { CMS_QUERY_KEYS } from "./query-key-defs";
|
|
36
|
+
import { runHookWithShim } from "../../utils";
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* Route keys for the CMS plugin — matches the keys returned by
|
|
@@ -569,6 +571,14 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
|
|
|
569
571
|
return getContentItemById(adapter, id);
|
|
570
572
|
},
|
|
571
573
|
prefetchForRoute: createCMSPrefetchForRoute(adapter),
|
|
574
|
+
// Mutations
|
|
575
|
+
createContentItem: async (
|
|
576
|
+
typeSlug: string,
|
|
577
|
+
input: Parameters<typeof createCMSContentItem>[2],
|
|
578
|
+
) => {
|
|
579
|
+
await ensureSynced(adapter);
|
|
580
|
+
return createCMSContentItem(adapter, typeSlug, input);
|
|
581
|
+
},
|
|
572
582
|
}),
|
|
573
583
|
|
|
574
584
|
routes: (adapter: Adapter) => {
|
|
@@ -770,13 +780,11 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
|
|
|
770
780
|
// Call before hook - may deny operation
|
|
771
781
|
const processedData = validation.data as Record<string, unknown>;
|
|
772
782
|
if (config.hooks?.onBeforeCreate) {
|
|
773
|
-
|
|
774
|
-
processedData,
|
|
775
|
-
|
|
783
|
+
await runHookWithShim(
|
|
784
|
+
() => config.hooks!.onBeforeCreate!(processedData, context),
|
|
785
|
+
ctx.error,
|
|
786
|
+
"Create operation denied",
|
|
776
787
|
);
|
|
777
|
-
if (result === false) {
|
|
778
|
-
throw ctx.error(403, { message: "Create operation denied" });
|
|
779
|
-
}
|
|
780
788
|
}
|
|
781
789
|
|
|
782
790
|
const item = await adapter.create<ContentItem>({
|
|
@@ -913,14 +921,11 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
|
|
|
913
921
|
// Call before hook - may deny operation
|
|
914
922
|
const processedData = validatedData;
|
|
915
923
|
if (config.hooks?.onBeforeUpdate && validatedData) {
|
|
916
|
-
|
|
917
|
-
id,
|
|
918
|
-
|
|
919
|
-
|
|
924
|
+
await runHookWithShim(
|
|
925
|
+
() => config.hooks!.onBeforeUpdate!(id, validatedData, context),
|
|
926
|
+
ctx.error,
|
|
927
|
+
"Update operation denied",
|
|
920
928
|
);
|
|
921
|
-
if (result === false) {
|
|
922
|
-
throw ctx.error(403, { message: "Update operation denied" });
|
|
923
|
-
}
|
|
924
929
|
}
|
|
925
930
|
|
|
926
931
|
// Sync relations to junction table if data was updated
|
|
@@ -987,10 +992,11 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
|
|
|
987
992
|
|
|
988
993
|
// Call before hook
|
|
989
994
|
if (config.hooks?.onBeforeDelete) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
995
|
+
await runHookWithShim(
|
|
996
|
+
() => config.hooks!.onBeforeDelete!(id, context),
|
|
997
|
+
ctx.error,
|
|
998
|
+
"Delete operation denied",
|
|
999
|
+
);
|
|
994
1000
|
}
|
|
995
1001
|
|
|
996
1002
|
await adapter.delete({
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
defineClientPlugin,
|
|
4
4
|
createApiClient,
|
|
5
5
|
isConnectionError,
|
|
6
|
+
runClientHookWithShim,
|
|
6
7
|
} from "@btst/stack/plugins/client";
|
|
7
8
|
import { createRoute } from "@btst/yar";
|
|
8
9
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -52,24 +53,24 @@ export interface LoaderContext {
|
|
|
52
53
|
*/
|
|
53
54
|
export interface CMSClientHooks {
|
|
54
55
|
/**
|
|
55
|
-
* Called before loading the dashboard page.
|
|
56
|
+
* Called before loading the dashboard page. Throw an error to cancel loading.
|
|
56
57
|
* @param context - Loader context with path, params, etc.
|
|
57
58
|
*/
|
|
58
|
-
beforeLoadDashboard?: (context: LoaderContext) => Promise<
|
|
59
|
+
beforeLoadDashboard?: (context: LoaderContext) => Promise<void> | void;
|
|
59
60
|
/**
|
|
60
61
|
* Called after the dashboard is loaded.
|
|
61
62
|
* @param context - Loader context
|
|
62
63
|
*/
|
|
63
64
|
afterLoadDashboard?: (context: LoaderContext) => Promise<void> | void;
|
|
64
65
|
/**
|
|
65
|
-
* Called before loading a content list page.
|
|
66
|
+
* Called before loading a content list page. Throw an error to cancel loading.
|
|
66
67
|
* @param typeSlug - The content type slug
|
|
67
68
|
* @param context - Loader context
|
|
68
69
|
*/
|
|
69
70
|
beforeLoadContentList?: (
|
|
70
71
|
typeSlug: string,
|
|
71
72
|
context: LoaderContext,
|
|
72
|
-
) => Promise<
|
|
73
|
+
) => Promise<void> | void;
|
|
73
74
|
/**
|
|
74
75
|
* Called after a content list is loaded.
|
|
75
76
|
* @param typeSlug - The content type slug
|
|
@@ -80,7 +81,7 @@ export interface CMSClientHooks {
|
|
|
80
81
|
context: LoaderContext,
|
|
81
82
|
) => Promise<void> | void;
|
|
82
83
|
/**
|
|
83
|
-
* Called before loading the content editor page.
|
|
84
|
+
* Called before loading the content editor page. Throw an error to cancel loading.
|
|
84
85
|
* @param typeSlug - The content type slug
|
|
85
86
|
* @param id - The content item ID (undefined for new items)
|
|
86
87
|
* @param context - Loader context
|
|
@@ -89,7 +90,7 @@ export interface CMSClientHooks {
|
|
|
89
90
|
typeSlug: string,
|
|
90
91
|
id: string | undefined,
|
|
91
92
|
context: LoaderContext,
|
|
92
|
-
) => Promise<
|
|
93
|
+
) => Promise<void> | void;
|
|
93
94
|
/**
|
|
94
95
|
* Called after the content editor is loaded.
|
|
95
96
|
* @param typeSlug - The content type slug
|
|
@@ -149,10 +150,10 @@ function createDashboardLoader(config: CMSClientConfig) {
|
|
|
149
150
|
try {
|
|
150
151
|
// Before hook - authorization check
|
|
151
152
|
if (hooks?.beforeLoadDashboard) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
await runClientHookWithShim(
|
|
154
|
+
() => hooks.beforeLoadDashboard!(context),
|
|
155
|
+
"Load prevented by beforeLoadDashboard hook",
|
|
156
|
+
);
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
const client = createApiClient<CMSApiRouter>({
|
|
@@ -217,10 +218,10 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
|
|
|
217
218
|
try {
|
|
218
219
|
// Before hook - authorization check
|
|
219
220
|
if (hooks?.beforeLoadContentList) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
await runClientHookWithShim(
|
|
222
|
+
() => hooks.beforeLoadContentList!(typeSlug, context),
|
|
223
|
+
"Load prevented by beforeLoadContentList hook",
|
|
224
|
+
);
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
const client = createApiClient<CMSApiRouter>({
|
|
@@ -321,14 +322,10 @@ function createContentEditorLoader(
|
|
|
321
322
|
try {
|
|
322
323
|
// Before hook - authorization check
|
|
323
324
|
if (hooks?.beforeLoadContentEditor) {
|
|
324
|
-
|
|
325
|
-
typeSlug,
|
|
326
|
-
|
|
327
|
-
context,
|
|
325
|
+
await runClientHookWithShim(
|
|
326
|
+
() => hooks.beforeLoadContentEditor!(typeSlug, id, context),
|
|
327
|
+
"Load prevented by beforeLoadContentEditor hook",
|
|
328
328
|
);
|
|
329
|
-
if (!canLoad) {
|
|
330
|
-
throw new Error("Load prevented by beforeLoadContentEditor hook");
|
|
331
|
-
}
|
|
332
329
|
}
|
|
333
330
|
|
|
334
331
|
const client = createApiClient<CMSApiRouter>({
|
package/src/plugins/cms/types.ts
CHANGED
|
@@ -248,16 +248,16 @@ export interface CMSHookContext {
|
|
|
248
248
|
/**
|
|
249
249
|
* Hooks for customizing CMS backend behavior
|
|
250
250
|
*
|
|
251
|
-
* Note: Before hooks
|
|
251
|
+
* Note: Before hooks deny operations by throwing an error.
|
|
252
252
|
* They cannot modify the data being saved. This ensures consistency
|
|
253
253
|
* between the stored content item data and relation junction tables.
|
|
254
254
|
*/
|
|
255
255
|
export interface CMSBackendHooks {
|
|
256
|
-
/** Called before creating a content item.
|
|
256
|
+
/** Called before creating a content item. Throw an error to deny the operation. */
|
|
257
257
|
onBeforeCreate?: (
|
|
258
258
|
data: Record<string, unknown>,
|
|
259
259
|
context: CMSHookContext,
|
|
260
|
-
) => Promise<
|
|
260
|
+
) => Promise<void> | void;
|
|
261
261
|
|
|
262
262
|
/** Called after creating a content item */
|
|
263
263
|
onAfterCreate?: (
|
|
@@ -265,12 +265,12 @@ export interface CMSBackendHooks {
|
|
|
265
265
|
context: CMSHookContext,
|
|
266
266
|
) => Promise<void> | void;
|
|
267
267
|
|
|
268
|
-
/** Called before updating a content item.
|
|
268
|
+
/** Called before updating a content item. Throw an error to deny the operation. */
|
|
269
269
|
onBeforeUpdate?: (
|
|
270
270
|
id: string,
|
|
271
271
|
data: Record<string, unknown>,
|
|
272
272
|
context: CMSHookContext,
|
|
273
|
-
) => Promise<
|
|
273
|
+
) => Promise<void> | void;
|
|
274
274
|
|
|
275
275
|
/** Called after updating a content item */
|
|
276
276
|
onAfterUpdate?: (
|
|
@@ -278,11 +278,11 @@ export interface CMSBackendHooks {
|
|
|
278
278
|
context: CMSHookContext,
|
|
279
279
|
) => Promise<void> | void;
|
|
280
280
|
|
|
281
|
-
/** Called before deleting a content item */
|
|
281
|
+
/** Called before deleting a content item. Throw an error to deny the operation. */
|
|
282
282
|
onBeforeDelete?: (
|
|
283
283
|
id: string,
|
|
284
284
|
context: CMSHookContext,
|
|
285
|
-
) => Promise<
|
|
285
|
+
) => Promise<void> | void;
|
|
286
286
|
|
|
287
287
|
/** Called after deleting a content item */
|
|
288
288
|
onAfterDelete?: (id: string, context: CMSHookContext) => Promise<void> | void;
|