@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
|
@@ -22,9 +22,12 @@ import { toast } from "sonner";
|
|
|
22
22
|
import UIBuilder from "@workspace/ui/components/ui-builder";
|
|
23
23
|
import type {
|
|
24
24
|
ComponentLayer,
|
|
25
|
+
ComponentRegistry,
|
|
25
26
|
Variable,
|
|
26
27
|
} from "@workspace/ui/components/ui-builder/types";
|
|
27
28
|
|
|
29
|
+
import { useLayerStore } from "@workspace/ui/lib/ui-builder/store/layer-store";
|
|
30
|
+
import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
|
|
28
31
|
import {
|
|
29
32
|
useSuspenseUIBuilderPage,
|
|
30
33
|
useCreateUIBuilderPage,
|
|
@@ -39,6 +42,104 @@ export interface PageBuilderPageProps {
|
|
|
39
42
|
id?: string;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Generate a concise AI-readable description of the available components
|
|
47
|
+
* in the component registry, including their prop names.
|
|
48
|
+
*/
|
|
49
|
+
function buildRegistryDescription(registry: ComponentRegistry): string {
|
|
50
|
+
const lines: string[] = [];
|
|
51
|
+
for (const [name, entry] of Object.entries(registry) as [
|
|
52
|
+
string,
|
|
53
|
+
{ schema?: unknown },
|
|
54
|
+
][]) {
|
|
55
|
+
let propsLine = "";
|
|
56
|
+
try {
|
|
57
|
+
const shape = (entry.schema as any)?.shape as
|
|
58
|
+
| Record<string, unknown>
|
|
59
|
+
| undefined;
|
|
60
|
+
if (shape) {
|
|
61
|
+
const fields = Object.keys(shape).join(", ");
|
|
62
|
+
propsLine = ` — props: ${fields}`;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// ignore schema introspection errors
|
|
66
|
+
}
|
|
67
|
+
lines.push(`- ${name}${propsLine}`);
|
|
68
|
+
}
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the full page description string for the AI context.
|
|
74
|
+
* Stays within the 8,000-character pageContext limit.
|
|
75
|
+
*/
|
|
76
|
+
function buildPageDescription(
|
|
77
|
+
id: string | undefined,
|
|
78
|
+
slug: string,
|
|
79
|
+
layers: ComponentLayer[],
|
|
80
|
+
registry: ComponentRegistry,
|
|
81
|
+
): string {
|
|
82
|
+
const header = id
|
|
83
|
+
? `UI Builder — editing page (slug: "${slug}")`
|
|
84
|
+
: "UI Builder — creating new page";
|
|
85
|
+
|
|
86
|
+
const layersJson = JSON.stringify(layers, null, 2);
|
|
87
|
+
|
|
88
|
+
const registryDesc = buildRegistryDescription(registry);
|
|
89
|
+
|
|
90
|
+
const layerFormat = `Each layer: { id: string, type: string, name: string, props: Record<string,any>, children?: ComponentLayer[] | string }`;
|
|
91
|
+
|
|
92
|
+
const full = [
|
|
93
|
+
header,
|
|
94
|
+
"",
|
|
95
|
+
`## Current Layers (${layers.length})`,
|
|
96
|
+
layersJson,
|
|
97
|
+
"",
|
|
98
|
+
`## Available Component Types`,
|
|
99
|
+
registryDesc,
|
|
100
|
+
"",
|
|
101
|
+
`## ComponentLayer format`,
|
|
102
|
+
layerFormat,
|
|
103
|
+
].join("\n");
|
|
104
|
+
|
|
105
|
+
// Trim to fit the 16,000-char server-side limit, cutting the layers JSON if needed
|
|
106
|
+
if (full.length <= 16000) return full;
|
|
107
|
+
|
|
108
|
+
// Re-build with truncated layers JSON
|
|
109
|
+
const overhead =
|
|
110
|
+
[
|
|
111
|
+
header,
|
|
112
|
+
"",
|
|
113
|
+
`## Current Layers (${layers.length})`,
|
|
114
|
+
"",
|
|
115
|
+
"",
|
|
116
|
+
`## Available Component Types`,
|
|
117
|
+
registryDesc,
|
|
118
|
+
"",
|
|
119
|
+
`## ComponentLayer format`,
|
|
120
|
+
layerFormat,
|
|
121
|
+
].join("\n").length + 30; // 30-char buffer for "...(truncated)"
|
|
122
|
+
|
|
123
|
+
const budget = Math.max(0, 16000 - overhead);
|
|
124
|
+
const truncatedLayers =
|
|
125
|
+
layersJson.length > budget
|
|
126
|
+
? layersJson.slice(0, budget) + "\n...(truncated)"
|
|
127
|
+
: layersJson;
|
|
128
|
+
|
|
129
|
+
return [
|
|
130
|
+
header,
|
|
131
|
+
"",
|
|
132
|
+
`## Current Layers (${layers.length})`,
|
|
133
|
+
truncatedLayers,
|
|
134
|
+
"",
|
|
135
|
+
`## Available Component Types`,
|
|
136
|
+
registryDesc,
|
|
137
|
+
"",
|
|
138
|
+
`## ComponentLayer format`,
|
|
139
|
+
layerFormat,
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
42
143
|
/**
|
|
43
144
|
* Slugify a string for URL-friendly slugs
|
|
44
145
|
*/
|
|
@@ -139,6 +240,37 @@ function PageBuilderPageContent({
|
|
|
139
240
|
// Auto-generate slug from first page name
|
|
140
241
|
const [autoSlug, setAutoSlug] = useState(!id);
|
|
141
242
|
|
|
243
|
+
// Register AI context so the chat can update the page layout
|
|
244
|
+
useRegisterPageAIContext({
|
|
245
|
+
routeName: id ? "ui-builder-edit-page" : "ui-builder-new-page",
|
|
246
|
+
pageDescription: buildPageDescription(id, slug, layers, componentRegistry),
|
|
247
|
+
suggestions: [
|
|
248
|
+
"Add a hero section",
|
|
249
|
+
"Add a 3-column feature grid",
|
|
250
|
+
"Make the layout full-width",
|
|
251
|
+
"Add a card with a title, description, and button",
|
|
252
|
+
"Replace the layout with a centered single-column design",
|
|
253
|
+
],
|
|
254
|
+
clientTools: {
|
|
255
|
+
updatePageLayers: async ({ layers: newLayers }) => {
|
|
256
|
+
// Drive the UIBuilder's Zustand store directly so the editor
|
|
257
|
+
// and layers panel update immediately. The store's onChange
|
|
258
|
+
// callback will propagate back to the parent's `layers` state.
|
|
259
|
+
const store = useLayerStore.getState();
|
|
260
|
+
store.initialize(
|
|
261
|
+
newLayers,
|
|
262
|
+
store.selectedPageId || newLayers[0]?.id,
|
|
263
|
+
undefined,
|
|
264
|
+
store.variables,
|
|
265
|
+
);
|
|
266
|
+
return {
|
|
267
|
+
success: true,
|
|
268
|
+
message: `Applied ${newLayers.length} layer(s) to the page`,
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
142
274
|
// Handle layers change from UIBuilder
|
|
143
275
|
const handleLayersChange = useCallback(
|
|
144
276
|
(newLayers: ComponentLayer[]) => {
|
|
@@ -3,6 +3,7 @@ import { lazy } from "react";
|
|
|
3
3
|
import {
|
|
4
4
|
defineClientPlugin,
|
|
5
5
|
createApiClient,
|
|
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";
|
|
@@ -69,10 +70,10 @@ function createPageListLoader(config: UIBuilderClientConfig) {
|
|
|
69
70
|
try {
|
|
70
71
|
// Before hook - authorization check
|
|
71
72
|
if (hooks?.beforeLoadPageList) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
await runClientHookWithShim(
|
|
74
|
+
() => hooks.beforeLoadPageList!(context),
|
|
75
|
+
"Load prevented by beforeLoadPageList hook",
|
|
76
|
+
);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
const client = createApiClient<CMSApiRouter>({
|
|
@@ -161,10 +162,10 @@ function createPageBuilderLoader(
|
|
|
161
162
|
try {
|
|
162
163
|
// Before hook - authorization check
|
|
163
164
|
if (hooks?.beforeLoadPageBuilder) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
await runClientHookWithShim(
|
|
166
|
+
() => hooks.beforeLoadPageBuilder!(id, context),
|
|
167
|
+
"Load prevented by beforeLoadPageBuilder hook",
|
|
168
|
+
);
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
const client = createApiClient<CMSApiRouter>({
|
|
@@ -273,11 +274,11 @@ function createPageBuilderMeta(
|
|
|
273
274
|
* hooks: {
|
|
274
275
|
* beforeLoadPageList: async (ctx) => {
|
|
275
276
|
* const session = await getSession(ctx.headers)
|
|
276
|
-
*
|
|
277
|
+
* if (!session?.user?.isAdmin) throw new Error("Admin access required")
|
|
277
278
|
* },
|
|
278
279
|
* beforeLoadPageBuilder: async (pageId, ctx) => {
|
|
279
280
|
* const session = await getSession(ctx.headers)
|
|
280
|
-
*
|
|
281
|
+
* if (!session?.user?.isAdmin) throw new Error("Admin access required")
|
|
281
282
|
* },
|
|
282
283
|
* onLoadError: () => redirect("/auth/sign-in"),
|
|
283
284
|
* },
|
|
@@ -92,24 +92,24 @@ export interface LoaderContext {
|
|
|
92
92
|
*/
|
|
93
93
|
export interface UIBuilderClientHooks {
|
|
94
94
|
/**
|
|
95
|
-
* Called before loading the page list.
|
|
95
|
+
* Called before loading the page list. Throw an error to cancel loading.
|
|
96
96
|
* @param context - Loader context with path, params, etc.
|
|
97
97
|
*/
|
|
98
|
-
beforeLoadPageList?: (context: LoaderContext) => Promise<
|
|
98
|
+
beforeLoadPageList?: (context: LoaderContext) => Promise<void> | void;
|
|
99
99
|
/**
|
|
100
100
|
* Called after the page list is loaded.
|
|
101
101
|
* @param context - Loader context
|
|
102
102
|
*/
|
|
103
103
|
afterLoadPageList?: (context: LoaderContext) => Promise<void> | void;
|
|
104
104
|
/**
|
|
105
|
-
* Called before loading the page builder.
|
|
105
|
+
* Called before loading the page builder. Throw an error to cancel loading.
|
|
106
106
|
* @param pageId - The page ID (undefined for new pages)
|
|
107
107
|
* @param context - Loader context
|
|
108
108
|
*/
|
|
109
109
|
beforeLoadPageBuilder?: (
|
|
110
110
|
pageId: string | undefined,
|
|
111
111
|
context: LoaderContext,
|
|
112
|
-
) => Promise<
|
|
112
|
+
) => Promise<void> | void;
|
|
113
113
|
/**
|
|
114
114
|
* Called after the page builder is loaded.
|
|
115
115
|
* @param pageId - The page ID (undefined for new pages)
|
package/src/plugins/utils.ts
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
import { createClient } from "better-call/client";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Runs a hook with backward-compatible denial handling.
|
|
5
|
+
* Hooks may deny by returning a falsy value (old) or throwing (new).
|
|
6
|
+
* Both are normalized to an HTTP error via `createError` (`ctx.error`).
|
|
7
|
+
* Returns the hook's result so transform hooks can apply mutations.
|
|
8
|
+
*
|
|
9
|
+
* ## Migration note (v2.4 → v2.5)
|
|
10
|
+
*
|
|
11
|
+
* Old-style hooks signalled denial by returning `false` and allowance by returning `true`.
|
|
12
|
+
* Pre-shim call sites used `if (!result)` which treated `undefined` (fall-through) as deny.
|
|
13
|
+
* New-style hooks throw an Error to deny and return void/undefined to allow.
|
|
14
|
+
*
|
|
15
|
+
* The shim detects old-style boolean returns at runtime and emits a deprecation warning so
|
|
16
|
+
* that any hook with a code path returning a boolean is surfaced immediately. Hooks that fall
|
|
17
|
+
* through to `undefined` on **every** code path (no boolean return anywhere) cannot be
|
|
18
|
+
* distinguished from new-style void hooks — those hooks must be audited manually and updated
|
|
19
|
+
* to throw explicitly when they intend to deny access.
|
|
20
|
+
*/
|
|
21
|
+
export async function runHookWithShim<T>(
|
|
22
|
+
hookFn: () => Promise<T> | T,
|
|
23
|
+
createError: (
|
|
24
|
+
status: keyof typeof statusCodes | Status,
|
|
25
|
+
body: { message: string },
|
|
26
|
+
) => any,
|
|
27
|
+
defaultMessage: string,
|
|
28
|
+
errorStatus = 403 as keyof typeof statusCodes | Status,
|
|
29
|
+
): Promise<Exclude<Awaited<T>, false>> {
|
|
30
|
+
let result: Awaited<T>;
|
|
31
|
+
try {
|
|
32
|
+
result = await hookFn();
|
|
33
|
+
} catch (e) {
|
|
34
|
+
throw createError(errorStatus, {
|
|
35
|
+
message: e instanceof Error ? e.message : defaultMessage,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// Detect old-style boolean returns (pre-v2.5 pattern).
|
|
39
|
+
// Emitting a warning here is the only reliable way to surface hooks that still rely on
|
|
40
|
+
// boolean returns — including hooks where one branch returns `false` and another falls
|
|
41
|
+
// through to `undefined`, which was previously denied by `if (!result)` at the call site
|
|
42
|
+
// but would now silently allow if the warning is not acted on.
|
|
43
|
+
if (typeof result === "boolean") {
|
|
44
|
+
if (process.env.NODE_ENV !== "production") {
|
|
45
|
+
console.warn(
|
|
46
|
+
`[btst] A lifecycle hook returned a boolean (${result}). ` +
|
|
47
|
+
`Boolean returns are deprecated — throw an Error to deny access instead. ` +
|
|
48
|
+
`IMPORTANT: any code path in this hook that falls through to undefined ` +
|
|
49
|
+
`now ALLOWS access (previously denied). ` +
|
|
50
|
+
`Update the hook to throw new Error("Unauthorized") to deny.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (!result) {
|
|
54
|
+
throw createError(errorStatus, { message: defaultMessage });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result as Exclude<Awaited<T>, false>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Client-side equivalent of runHookWithShim — throws a plain Error instead of an HTTP error.
|
|
62
|
+
* Hooks may deny by returning false (old) or throwing (new); both normalize to an Error.
|
|
63
|
+
*
|
|
64
|
+
* See `runHookWithShim` for the full migration note on boolean-vs-void semantics.
|
|
65
|
+
*/
|
|
66
|
+
export async function runClientHookWithShim<T>(
|
|
67
|
+
hookFn: () => Promise<T> | T,
|
|
68
|
+
defaultMessage: string,
|
|
69
|
+
): Promise<Exclude<Awaited<T>, false>> {
|
|
70
|
+
let result: Awaited<T>;
|
|
71
|
+
try {
|
|
72
|
+
result = await hookFn();
|
|
73
|
+
} catch (e) {
|
|
74
|
+
throw e instanceof Error ? e : new Error(defaultMessage);
|
|
75
|
+
}
|
|
76
|
+
// Detect old-style boolean returns and warn; see runHookWithShim for rationale.
|
|
77
|
+
if (typeof result === "boolean") {
|
|
78
|
+
if (process.env.NODE_ENV !== "production") {
|
|
79
|
+
console.warn(
|
|
80
|
+
`[btst] A lifecycle hook returned a boolean (${result}). ` +
|
|
81
|
+
`Boolean returns are deprecated — throw an Error to deny access instead. ` +
|
|
82
|
+
`IMPORTANT: any code path in this hook that falls through to undefined ` +
|
|
83
|
+
`now ALLOWS access (previously denied). ` +
|
|
84
|
+
`Update the hook to throw new Error("Unauthorized") to deny.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (!result) {
|
|
88
|
+
throw new Error(defaultMessage);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result as Exclude<Awaited<T>, false>;
|
|
92
|
+
}
|
|
93
|
+
|
|
3
94
|
/**
|
|
4
95
|
* Returns true when a fetch error is a connection-refused / no-server error.
|
|
5
96
|
* Used in SSR loaders to emit an actionable build-time warning when
|
|
@@ -18,7 +109,7 @@ export function isConnectionError(err: unknown): boolean {
|
|
|
18
109
|
code === "ERR_CONNECTION_REFUSED"
|
|
19
110
|
);
|
|
20
111
|
}
|
|
21
|
-
import type { Router, Endpoint } from "better-call";
|
|
112
|
+
import type { Router, Endpoint, Status, statusCodes } from "better-call";
|
|
22
113
|
|
|
23
114
|
interface CreateApiClientOptions {
|
|
24
115
|
baseURL?: string;
|
|
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
42
42
|
name: z.ZodString;
|
|
43
43
|
slug: z.ZodString;
|
|
44
44
|
}, z.core.$strip>]>>>>;
|
|
45
|
+
title: z.ZodString;
|
|
45
46
|
slug: z.ZodOptional<z.ZodString>;
|
|
46
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
47
47
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
48
48
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
49
|
-
|
|
49
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
50
50
|
content: z.ZodString;
|
|
51
51
|
excerpt: z.ZodString;
|
|
52
52
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
42
42
|
name: z.ZodString;
|
|
43
43
|
slug: z.ZodString;
|
|
44
44
|
}, z.core.$strip>]>>>>;
|
|
45
|
+
title: z.ZodString;
|
|
45
46
|
slug: z.ZodOptional<z.ZodString>;
|
|
46
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
47
47
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
48
48
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
49
|
-
|
|
49
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
50
50
|
content: z.ZodString;
|
|
51
51
|
excerpt: z.ZodString;
|
|
52
52
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
42
42
|
name: z.ZodString;
|
|
43
43
|
slug: z.ZodString;
|
|
44
44
|
}, z.core.$strip>]>>>>;
|
|
45
|
+
title: z.ZodString;
|
|
45
46
|
slug: z.ZodOptional<z.ZodString>;
|
|
46
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
47
47
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
48
48
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
49
|
-
|
|
49
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
50
50
|
content: z.ZodString;
|
|
51
51
|
excerpt: z.ZodString;
|
|
52
52
|
image: z.ZodOptional<z.ZodString>;
|