@btst/stack 2.5.5 → 2.6.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/README.md +23 -0
- package/dist/client/components/index.d.cts +9 -9
- package/dist/client/components/index.d.mts +9 -9
- package/dist/client/components/index.d.ts +9 -9
- package/dist/packages/stack/src/plugins/ai-chat/client/components/shared/default-error.cjs +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/components/shared/default-error.mjs +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +44 -35
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +44 -35
- package/dist/packages/stack/src/plugins/blog/client/components/shared/default-error.cjs +1 -1
- package/dist/packages/stack/src/plugins/blog/client/components/shared/default-error.mjs +1 -1
- package/dist/packages/stack/src/plugins/blog/client/hooks/use-debounce.cjs +22 -0
- package/dist/packages/stack/src/plugins/blog/client/hooks/use-debounce.mjs +23 -2
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +12 -6
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +12 -6
- package/dist/packages/stack/src/plugins/cms/client/components/shared/default-error.cjs +1 -1
- package/dist/packages/stack/src/plugins/cms/client/components/shared/default-error.mjs +1 -1
- package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +38 -26
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +38 -26
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/default-error.cjs +1 -1
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/default-error.mjs +1 -1
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +32 -20
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +32 -20
- package/dist/packages/stack/src/plugins/kanban/client/components/shared/default-error.cjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/shared/default-error.mjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +6 -3
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +6 -3
- package/dist/packages/stack/src/plugins/ui-builder/client/components/page-renderer.cjs +1 -1
- package/dist/packages/stack/src/plugins/ui-builder/client/components/page-renderer.mjs +1 -1
- package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +3 -1
- package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +3 -1
- package/dist/packages/stack/src/plugins/ui-builder/client/components/shared/default-error.cjs +1 -1
- package/dist/packages/stack/src/plugins/ui-builder/client/components/shared/default-error.mjs +1 -1
- package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +24 -15
- package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +24 -15
- package/dist/packages/ui/src/components/search-select.cjs +13 -3
- package/dist/packages/ui/src/components/search-select.mjs +14 -4
- package/dist/plugins/ai-chat/client/index.d.cts +17 -4
- package/dist/plugins/ai-chat/client/index.d.mts +17 -4
- package/dist/plugins/ai-chat/client/index.d.ts +17 -4
- package/dist/plugins/blog/client/hooks/index.cjs +3 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +7 -226
- package/dist/plugins/blog/client/hooks/index.d.mts +7 -226
- package/dist/plugins/blog/client/hooks/index.d.ts +7 -226
- package/dist/plugins/blog/client/hooks/index.mjs +1 -0
- package/dist/plugins/blog/client/index.d.cts +45 -21
- package/dist/plugins/blog/client/index.d.mts +45 -21
- package/dist/plugins/blog/client/index.d.ts +45 -21
- package/dist/plugins/cms/client/index.d.cts +35 -14
- package/dist/plugins/cms/client/index.d.mts +35 -14
- package/dist/plugins/cms/client/index.d.ts +35 -14
- 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/index.d.cts +32 -14
- package/dist/plugins/form-builder/client/index.d.mts +32 -14
- package/dist/plugins/form-builder/client/index.d.ts +32 -14
- 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/client/components/index.d.cts +5 -5
- package/dist/plugins/kanban/client/components/index.d.mts +5 -5
- package/dist/plugins/kanban/client/components/index.d.ts +5 -5
- package/dist/plugins/kanban/client/index.d.cts +25 -10
- package/dist/plugins/kanban/client/index.d.mts +25 -10
- package/dist/plugins/kanban/client/index.d.ts +25 -10
- 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/route-docs/client/index.d.cts +4 -4
- package/dist/plugins/route-docs/client/index.d.mts +4 -4
- package/dist/plugins/route-docs/client/index.d.ts +4 -4
- package/dist/plugins/ui-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/ui-builder/client/index.d.cts +35 -16
- package/dist/plugins/ui-builder/client/index.d.mts +35 -16
- package/dist/plugins/ui-builder/client/index.d.ts +35 -16
- package/dist/shared/stack.CNLHlv7r.d.mts +228 -0
- package/dist/shared/stack.CQAZwXhV.d.cts +228 -0
- package/dist/shared/stack.D3BsrpAz.d.ts +228 -0
- package/package.json +19 -2
- package/src/__tests__/page-component-overrides.test.tsx +147 -0
- package/src/plugins/ai-chat/client/components/shared/default-error.tsx +1 -1
- package/src/plugins/ai-chat/client/plugin.tsx +60 -32
- package/src/plugins/blog/client/components/shared/default-error.tsx +2 -1
- package/src/plugins/blog/client/hooks/index.tsx +1 -0
- package/src/plugins/blog/client/plugin.tsx +41 -6
- package/src/plugins/cms/client/components/shared/default-error.tsx +3 -2
- package/src/plugins/cms/client/plugin.tsx +65 -32
- package/src/plugins/form-builder/client/components/shared/default-error.tsx +3 -2
- package/src/plugins/form-builder/client/plugin.tsx +56 -23
- package/src/plugins/kanban/client/components/shared/default-error.tsx +3 -2
- package/src/plugins/kanban/client/plugin.tsx +23 -3
- package/src/plugins/ui-builder/client/components/page-renderer.tsx +5 -3
- package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +2 -0
- package/src/plugins/ui-builder/client/components/shared/default-error.tsx +3 -2
- package/src/plugins/ui-builder/client/overrides.ts +10 -1
- package/src/plugins/ui-builder/client/plugin.tsx +41 -15
- package/dist/shared/{stack.CxNeGV2z.d.mts → stack.Ba_Ks8qi.d.mts} +9 -9
- package/dist/shared/{stack.DSxTDZBQ.d.cts → stack.CFqqZUes.d.cts} +9 -9
- package/dist/shared/{stack.BFcg0tDz.d.ts → stack.DMobugrZ.d.ts} +9 -9
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
+
import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from './stack.BWp0hcm9.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for the usePosts hook
|
|
7
|
+
*/
|
|
8
|
+
interface UsePostsOptions {
|
|
9
|
+
/** Filter posts by tag name */
|
|
10
|
+
tag?: string;
|
|
11
|
+
/** Filter posts by tag slug */
|
|
12
|
+
tagSlug?: string;
|
|
13
|
+
/** Number of posts to fetch per page (default: 10) */
|
|
14
|
+
limit?: number;
|
|
15
|
+
/** Whether to enable the query (default: true) */
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
/** Search query to filter posts by title, content, or excerpt */
|
|
18
|
+
query?: string;
|
|
19
|
+
/** Filter by published status */
|
|
20
|
+
published?: boolean;
|
|
21
|
+
/** Filter by specific post slug */
|
|
22
|
+
slug?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Result from the usePosts hook
|
|
26
|
+
*/
|
|
27
|
+
interface UsePostsResult {
|
|
28
|
+
/** Array of fetched posts */
|
|
29
|
+
posts: SerializedPost[];
|
|
30
|
+
/** Whether the initial load is in progress */
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
/** Error if the query failed */
|
|
33
|
+
error: Error | null;
|
|
34
|
+
/** Function to load the next page of posts */
|
|
35
|
+
loadMore: () => void;
|
|
36
|
+
/** Whether there are more posts to load */
|
|
37
|
+
hasMore: boolean;
|
|
38
|
+
/** Whether the next page is being loaded */
|
|
39
|
+
isLoadingMore: boolean;
|
|
40
|
+
/** Function to refetch the posts */
|
|
41
|
+
refetch: () => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Options for the usePostSearch hook
|
|
45
|
+
*/
|
|
46
|
+
interface UsePostSearchOptions {
|
|
47
|
+
/** Search query string to filter posts */
|
|
48
|
+
query: string;
|
|
49
|
+
/** Whether to enable the search query (default: true) */
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
/** Debounce delay in milliseconds (default: 300) */
|
|
52
|
+
debounceMs?: number;
|
|
53
|
+
/** Number of results to return (default: 10) */
|
|
54
|
+
limit?: number;
|
|
55
|
+
/** Filter by published status (default: true) */
|
|
56
|
+
published?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Result from the usePostSearch hook
|
|
60
|
+
*/
|
|
61
|
+
interface UsePostSearchResult {
|
|
62
|
+
/** Array of posts matching the search query */
|
|
63
|
+
posts: SerializedPost[];
|
|
64
|
+
/** Alias for posts (React Query compatibility) */
|
|
65
|
+
data: SerializedPost[];
|
|
66
|
+
/** Whether the search is in progress */
|
|
67
|
+
isLoading: boolean;
|
|
68
|
+
/** Error if the search failed */
|
|
69
|
+
error: Error | null;
|
|
70
|
+
/** Function to refetch the search results */
|
|
71
|
+
refetch: () => void;
|
|
72
|
+
/** Whether a search is currently in progress (includes debounce time) */
|
|
73
|
+
isSearching: boolean;
|
|
74
|
+
/** The debounced search query being used */
|
|
75
|
+
searchQuery: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Result from the usePost hook
|
|
79
|
+
*/
|
|
80
|
+
interface UsePostResult {
|
|
81
|
+
/** The fetched post, or null if not found */
|
|
82
|
+
post: SerializedPost | null;
|
|
83
|
+
/** Whether the post is being loaded */
|
|
84
|
+
isLoading: boolean;
|
|
85
|
+
/** Error if the query failed */
|
|
86
|
+
error: Error | null;
|
|
87
|
+
/** Function to refetch the post */
|
|
88
|
+
refetch: () => void;
|
|
89
|
+
}
|
|
90
|
+
/** Input type for creating a new post */
|
|
91
|
+
type PostCreateInput = z.infer<typeof createPostSchema>;
|
|
92
|
+
/** Input type for updating an existing post */
|
|
93
|
+
type PostUpdateInput = z.infer<typeof updatePostSchema>;
|
|
94
|
+
/**
|
|
95
|
+
* Hook for fetching paginated posts with load more functionality
|
|
96
|
+
*/
|
|
97
|
+
declare function usePosts(options?: UsePostsOptions): UsePostsResult;
|
|
98
|
+
/** Suspense variant of usePosts */
|
|
99
|
+
declare function useSuspensePosts(options?: UsePostsOptions): {
|
|
100
|
+
posts: SerializedPost[];
|
|
101
|
+
loadMore: () => Promise<unknown>;
|
|
102
|
+
hasMore: boolean;
|
|
103
|
+
isLoadingMore: boolean;
|
|
104
|
+
refetch: () => Promise<unknown>;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Hook for fetching a single post by slug
|
|
108
|
+
*/
|
|
109
|
+
declare function usePost(slug?: string): UsePostResult;
|
|
110
|
+
/** Suspense variant of usePost */
|
|
111
|
+
declare function useSuspensePost(slug: string): {
|
|
112
|
+
post: SerializedPost | null;
|
|
113
|
+
refetch: () => Promise<unknown>;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Hook for fetching all unique tags across posts
|
|
117
|
+
*/
|
|
118
|
+
declare function useTags(): {
|
|
119
|
+
tags: SerializedTag[];
|
|
120
|
+
isLoading: boolean;
|
|
121
|
+
error: Error | null;
|
|
122
|
+
refetch: () => void;
|
|
123
|
+
};
|
|
124
|
+
/** Suspense variant of useTags */
|
|
125
|
+
declare function useSuspenseTags(): {
|
|
126
|
+
tags: SerializedTag[];
|
|
127
|
+
refetch: () => Promise<unknown>;
|
|
128
|
+
};
|
|
129
|
+
/** Create a new post */
|
|
130
|
+
declare function useCreatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
|
|
131
|
+
tags: ({
|
|
132
|
+
name: string;
|
|
133
|
+
} | {
|
|
134
|
+
id: string;
|
|
135
|
+
name: string;
|
|
136
|
+
slug: string;
|
|
137
|
+
})[];
|
|
138
|
+
published: boolean;
|
|
139
|
+
title: string;
|
|
140
|
+
content: string;
|
|
141
|
+
excerpt: string;
|
|
142
|
+
slug?: string | undefined;
|
|
143
|
+
publishedAt?: Date | undefined;
|
|
144
|
+
createdAt?: Date | undefined;
|
|
145
|
+
updatedAt?: Date | undefined;
|
|
146
|
+
image?: string | undefined;
|
|
147
|
+
}, unknown>;
|
|
148
|
+
/** Update an existing post by id */
|
|
149
|
+
declare function useUpdatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
|
|
150
|
+
id: string;
|
|
151
|
+
data: PostUpdateInput;
|
|
152
|
+
}, unknown>;
|
|
153
|
+
/** Delete a post by id */
|
|
154
|
+
declare function useDeletePost(): _tanstack_react_query.UseMutationResult<{
|
|
155
|
+
success: boolean;
|
|
156
|
+
}, Error, {
|
|
157
|
+
id: string;
|
|
158
|
+
}, unknown>;
|
|
159
|
+
/**
|
|
160
|
+
* Hook for searching posts by a free-text query. Uses `usePosts` under the hood.
|
|
161
|
+
* Debounces the query and preserves last successful results to avoid flicker.
|
|
162
|
+
*/
|
|
163
|
+
declare function usePostSearch({ query, enabled, debounceMs, limit, published, }: UsePostSearchOptions): UsePostSearchResult;
|
|
164
|
+
/**
|
|
165
|
+
* Options for the useNextPreviousPosts hook
|
|
166
|
+
*/
|
|
167
|
+
interface UseNextPreviousPostsOptions {
|
|
168
|
+
/** Whether to enable the query (default: true) */
|
|
169
|
+
enabled?: boolean;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Result from the useNextPreviousPosts hook
|
|
173
|
+
*/
|
|
174
|
+
interface UseNextPreviousPostsResult {
|
|
175
|
+
/** The previous post (older), or null if none exists */
|
|
176
|
+
previousPost: SerializedPost | null;
|
|
177
|
+
/** The next post (newer), or null if none exists */
|
|
178
|
+
nextPost: SerializedPost | null;
|
|
179
|
+
/** Whether the query is loading */
|
|
180
|
+
isLoading: boolean;
|
|
181
|
+
/** Error if the query failed */
|
|
182
|
+
error: Error | null;
|
|
183
|
+
/** Function to refetch the posts */
|
|
184
|
+
refetch: () => void;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Hook for fetching previous and next posts relative to a given date
|
|
188
|
+
* Uses useInView to only fetch when the component is in view
|
|
189
|
+
*/
|
|
190
|
+
declare function useNextPreviousPosts(createdAt: string | Date, options?: UseNextPreviousPostsOptions): UseNextPreviousPostsResult & {
|
|
191
|
+
ref: (node: Element | null) => void;
|
|
192
|
+
inView: boolean;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Options for the useRecentPosts hook
|
|
196
|
+
*/
|
|
197
|
+
interface UseRecentPostsOptions {
|
|
198
|
+
/** Maximum number of recent posts to fetch (default: 5) */
|
|
199
|
+
limit?: number;
|
|
200
|
+
/** Slug of a post to exclude from results */
|
|
201
|
+
excludeSlug?: string;
|
|
202
|
+
/** Whether to enable the query (default: true) */
|
|
203
|
+
enabled?: boolean;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Result from the useRecentPosts hook
|
|
207
|
+
*/
|
|
208
|
+
interface UseRecentPostsResult {
|
|
209
|
+
/** Array of recent posts */
|
|
210
|
+
recentPosts: SerializedPost[];
|
|
211
|
+
/** Whether the query is loading */
|
|
212
|
+
isLoading: boolean;
|
|
213
|
+
/** Error if the query failed */
|
|
214
|
+
error: Error | null;
|
|
215
|
+
/** Function to refetch the posts */
|
|
216
|
+
refetch: () => void;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Hook for fetching recent posts
|
|
220
|
+
* Uses useInView to only fetch when the component is in view
|
|
221
|
+
*/
|
|
222
|
+
declare function useRecentPosts(options?: UseRecentPostsOptions): UseRecentPostsResult & {
|
|
223
|
+
ref: (node: Element | null) => void;
|
|
224
|
+
inView: boolean;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export { useSuspensePosts as f, usePost as g, useSuspensePost as h, useTags as i, useSuspenseTags as j, useCreatePost as k, useUpdatePost as l, useDeletePost as m, usePostSearch as n, useNextPreviousPosts as q, useRecentPosts as t, usePosts as u };
|
|
228
|
+
export type { PostCreateInput as P, UsePostsOptions as U, UsePostsResult as a, UsePostSearchOptions as b, UsePostSearchResult as c, UsePostResult as d, PostUpdateInput as e, UseNextPreviousPostsOptions as o, UseNextPreviousPostsResult as p, UseRecentPostsOptions as r, UseRecentPostsResult as s };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btst/stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "A composable, plugin-based library for building full-stack applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
"build:analyze": "ANALYZE=1 unbuild --clean && node ./scripts/postbuild.cjs",
|
|
26
26
|
"stub": "unbuild --stub",
|
|
27
27
|
"test": "vitest",
|
|
28
|
-
"typecheck": "tsc --project tsconfig.json"
|
|
28
|
+
"typecheck": "tsc --project tsconfig.json",
|
|
29
|
+
"build-registry": "tsx scripts/build-registry.ts",
|
|
30
|
+
"test-registry": "bash scripts/test-registry.sh",
|
|
31
|
+
"host-registry": "npx http-server registry -p 8766 -c-1"
|
|
29
32
|
},
|
|
30
33
|
"main": "./dist/index.cjs",
|
|
31
34
|
"module": "./dist/index.mjs",
|
|
@@ -123,6 +126,16 @@
|
|
|
123
126
|
"default": "./dist/plugins/blog/client/index.cjs"
|
|
124
127
|
}
|
|
125
128
|
},
|
|
129
|
+
"./plugins/blog/client/hooks": {
|
|
130
|
+
"import": {
|
|
131
|
+
"types": "./dist/plugins/blog/client/hooks/index.d.ts",
|
|
132
|
+
"default": "./dist/plugins/blog/client/hooks/index.mjs"
|
|
133
|
+
},
|
|
134
|
+
"require": {
|
|
135
|
+
"types": "./dist/plugins/blog/client/hooks/index.d.cts",
|
|
136
|
+
"default": "./dist/plugins/blog/client/hooks/index.cjs"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
126
139
|
"./plugins/ai-chat/api": {
|
|
127
140
|
"import": {
|
|
128
141
|
"types": "./dist/plugins/ai-chat/api/index.d.ts",
|
|
@@ -390,6 +403,9 @@
|
|
|
390
403
|
"plugins/blog/client": [
|
|
391
404
|
"./dist/plugins/blog/client/index.d.ts"
|
|
392
405
|
],
|
|
406
|
+
"plugins/blog/client/hooks": [
|
|
407
|
+
"./dist/plugins/blog/client/hooks/index.d.ts"
|
|
408
|
+
],
|
|
393
409
|
"plugins/ai-chat/api": [
|
|
394
410
|
"./dist/plugins/ai-chat/api/index.d.ts"
|
|
395
411
|
],
|
|
@@ -505,6 +521,7 @@
|
|
|
505
521
|
"zod": ">=4.2.0"
|
|
506
522
|
},
|
|
507
523
|
"devDependencies": {
|
|
524
|
+
"tsx": "catalog:",
|
|
508
525
|
"@ai-sdk/react": "^2.0.94",
|
|
509
526
|
"@btst/adapter-memory": "2.0.3",
|
|
510
527
|
"@btst/yar": "1.2.0",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { defineClientPlugin } from "../plugins/client";
|
|
3
|
+
import type { ComponentType } from "react";
|
|
4
|
+
import { createRoute } from "@btst/yar";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default page components used by the mock plugin factory.
|
|
8
|
+
* These stand in for the real plugin page components (e.g. HomePageComponent).
|
|
9
|
+
*/
|
|
10
|
+
const DefaultListComponent: ComponentType = () => <div>Default List</div>;
|
|
11
|
+
const DefaultDetailComponent: ComponentType<{ id: string }> = ({ id }) => (
|
|
12
|
+
<div>Default Detail {id}</div>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lightweight mock plugin factory that mirrors the real plugin pattern:
|
|
17
|
+
* - Accepts a config with an optional `pageComponents` field
|
|
18
|
+
* - Falls back to built-in components when no override is provided
|
|
19
|
+
* - For param routes, extracts the override before the handler closure
|
|
20
|
+
*/
|
|
21
|
+
function createTestPlugin(config: {
|
|
22
|
+
pageComponents?: {
|
|
23
|
+
list?: ComponentType;
|
|
24
|
+
detail?: ComponentType<{ id: string }>;
|
|
25
|
+
};
|
|
26
|
+
}) {
|
|
27
|
+
return defineClientPlugin({
|
|
28
|
+
name: "test",
|
|
29
|
+
routes: () => ({
|
|
30
|
+
list: createRoute("/items", () => {
|
|
31
|
+
const CustomList = config.pageComponents?.list;
|
|
32
|
+
return {
|
|
33
|
+
PageComponent: CustomList ?? DefaultListComponent,
|
|
34
|
+
};
|
|
35
|
+
}),
|
|
36
|
+
detail: createRoute("/items/:id", ({ params }) => {
|
|
37
|
+
const CustomDetail = config.pageComponents?.detail;
|
|
38
|
+
return {
|
|
39
|
+
PageComponent: CustomDetail
|
|
40
|
+
? () => <CustomDetail id={params.id} />
|
|
41
|
+
: () => <DefaultDetailComponent id={params.id} />,
|
|
42
|
+
};
|
|
43
|
+
}),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("pageComponents overrides", () => {
|
|
49
|
+
describe("default components used when no override provided", () => {
|
|
50
|
+
it("uses the default component for a no-param route", () => {
|
|
51
|
+
const plugin = createTestPlugin({});
|
|
52
|
+
const routes = plugin.routes();
|
|
53
|
+
const routeData = routes.list();
|
|
54
|
+
|
|
55
|
+
expect(routeData.PageComponent).toBe(DefaultListComponent);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("uses the default component wrapper for a param route", () => {
|
|
59
|
+
const plugin = createTestPlugin({});
|
|
60
|
+
const routes = plugin.routes();
|
|
61
|
+
const routeData = routes.detail({ params: { id: "42" } });
|
|
62
|
+
|
|
63
|
+
// Should be an inline wrapper, not the custom component
|
|
64
|
+
expect(routeData.PageComponent).toBeDefined();
|
|
65
|
+
expect(typeof routeData.PageComponent).toBe("function");
|
|
66
|
+
// Verify it is NOT the custom component (no override was given)
|
|
67
|
+
const CustomDetail: ComponentType<{ id: string }> = () => (
|
|
68
|
+
<div>Custom</div>
|
|
69
|
+
);
|
|
70
|
+
expect(routeData.PageComponent).not.toBe(CustomDetail);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("custom component replaces default (no-param route)", () => {
|
|
75
|
+
it("uses the provided override instead of the default", () => {
|
|
76
|
+
const CustomList: ComponentType = () => <div>Custom List</div>;
|
|
77
|
+
|
|
78
|
+
const plugin = createTestPlugin({
|
|
79
|
+
pageComponents: { list: CustomList },
|
|
80
|
+
});
|
|
81
|
+
const routes = plugin.routes();
|
|
82
|
+
const routeData = routes.list();
|
|
83
|
+
|
|
84
|
+
expect(routeData.PageComponent).toBe(CustomList);
|
|
85
|
+
expect(routeData.PageComponent).not.toBe(DefaultListComponent);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("leaves other routes using their defaults when only one is overridden", () => {
|
|
89
|
+
const CustomList: ComponentType = () => <div>Custom List</div>;
|
|
90
|
+
|
|
91
|
+
const plugin = createTestPlugin({
|
|
92
|
+
pageComponents: { list: CustomList },
|
|
93
|
+
});
|
|
94
|
+
const routes = plugin.routes();
|
|
95
|
+
|
|
96
|
+
// list uses custom
|
|
97
|
+
expect(routes.list().PageComponent).toBe(CustomList);
|
|
98
|
+
|
|
99
|
+
// detail still uses a wrapper (no override)
|
|
100
|
+
const detailData = routes.detail({ params: { id: "1" } });
|
|
101
|
+
expect(detailData.PageComponent).not.toBe(CustomList);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("custom component replaces default (param route)", () => {
|
|
106
|
+
it("uses the override wrapper for a param route", () => {
|
|
107
|
+
const CustomDetail: ComponentType<{ id: string }> = ({ id }) => (
|
|
108
|
+
<div>Custom Detail {id}</div>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const plugin = createTestPlugin({
|
|
112
|
+
pageComponents: { detail: CustomDetail },
|
|
113
|
+
});
|
|
114
|
+
const routes = plugin.routes();
|
|
115
|
+
const routeData = routes.detail({ params: { id: "42" } });
|
|
116
|
+
|
|
117
|
+
// Should be an inline wrapper (not the raw CustomDetail directly)
|
|
118
|
+
expect(routeData.PageComponent).toBeDefined();
|
|
119
|
+
expect(typeof routeData.PageComponent).toBe("function");
|
|
120
|
+
// Should NOT be the default
|
|
121
|
+
expect(routeData.PageComponent).not.toBe(DefaultDetailComponent);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("produces a different PageComponent than when no override is given", () => {
|
|
125
|
+
const CustomDetail: ComponentType<{ id: string }> = ({ id }) => (
|
|
126
|
+
<div>Custom Detail {id}</div>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const defaultPlugin = createTestPlugin({});
|
|
130
|
+
const overridePlugin = createTestPlugin({
|
|
131
|
+
pageComponents: { detail: CustomDetail },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const defaultRouteData = defaultPlugin
|
|
135
|
+
.routes()
|
|
136
|
+
.detail({ params: { id: "1" } });
|
|
137
|
+
const overrideRouteData = overridePlugin
|
|
138
|
+
.routes()
|
|
139
|
+
.detail({ params: { id: "1" } });
|
|
140
|
+
|
|
141
|
+
// The wrapped component should be a different function reference
|
|
142
|
+
expect(overrideRouteData.PageComponent).not.toBe(
|
|
143
|
+
defaultRouteData.PageComponent,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -21,7 +21,7 @@ export function DefaultError({ error }: FallbackProps) {
|
|
|
21
21
|
process.env.NODE_ENV === "production"
|
|
22
22
|
? (localization?.CHAT_GENERIC_ERROR_MESSAGE ??
|
|
23
23
|
AI_CHAT_LOCALIZATION.CHAT_GENERIC_ERROR_MESSAGE)
|
|
24
|
-
: (error
|
|
24
|
+
: ((error instanceof Error ? error.message : undefined) ??
|
|
25
25
|
localization?.CHAT_GENERIC_ERROR_MESSAGE ??
|
|
26
26
|
AI_CHAT_LOCALIZATION.CHAT_GENERIC_ERROR_MESSAGE);
|
|
27
27
|
return <ErrorPlaceholder title={title} message={message} />;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
runClientHookWithShim,
|
|
5
5
|
} from "@btst/stack/plugins/client";
|
|
6
6
|
import { createRoute } from "@btst/yar";
|
|
7
|
+
import type { ComponentType } from "react";
|
|
7
8
|
import type { QueryClient } from "@tanstack/react-query";
|
|
8
9
|
import type { AiChatApiRouter } from "../api";
|
|
9
10
|
import { createAiChatQueryKeys } from "../query-keys";
|
|
@@ -85,6 +86,18 @@ export interface AiChatClientConfig {
|
|
|
85
86
|
|
|
86
87
|
/** Optional headers for SSR (e.g., forwarding cookies) */
|
|
87
88
|
headers?: Headers;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Optional page component overrides.
|
|
92
|
+
* Replace any plugin page with a custom React component.
|
|
93
|
+
* The built-in component is used as the fallback when not provided.
|
|
94
|
+
*/
|
|
95
|
+
pageComponents?: {
|
|
96
|
+
/** Replaces the chat home page */
|
|
97
|
+
chat?: ComponentType;
|
|
98
|
+
/** Replaces the conversation page (authenticated mode only) */
|
|
99
|
+
chatConversation?: ComponentType<{ conversationId: string }>;
|
|
100
|
+
};
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
/**
|
|
@@ -381,17 +394,22 @@ export const aiChatClientPlugin = (config: AiChatClientConfig) => {
|
|
|
381
394
|
|
|
382
395
|
routes: () => ({
|
|
383
396
|
// Chat home - simple chat interface without history
|
|
384
|
-
chat: createRoute("/chat", () =>
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
397
|
+
chat: createRoute("/chat", () => {
|
|
398
|
+
const CustomChat = config.pageComponents?.chat;
|
|
399
|
+
return {
|
|
400
|
+
PageComponent:
|
|
401
|
+
CustomChat ??
|
|
402
|
+
(() => (
|
|
403
|
+
<ChatLayout
|
|
404
|
+
apiBaseURL={config.apiBaseURL}
|
|
405
|
+
apiBasePath={config.apiBasePath}
|
|
406
|
+
showSidebar={false}
|
|
407
|
+
/>
|
|
408
|
+
)),
|
|
409
|
+
loader: createConversationsLoader(config),
|
|
410
|
+
meta: createChatHomeMeta(config),
|
|
411
|
+
};
|
|
412
|
+
}),
|
|
395
413
|
}),
|
|
396
414
|
|
|
397
415
|
sitemap: async () => [],
|
|
@@ -404,29 +422,39 @@ export const aiChatClientPlugin = (config: AiChatClientConfig) => {
|
|
|
404
422
|
|
|
405
423
|
routes: () => ({
|
|
406
424
|
// Chat home - new conversation or list
|
|
407
|
-
chat: createRoute("/chat", () =>
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
425
|
+
chat: createRoute("/chat", () => {
|
|
426
|
+
const CustomChat = config.pageComponents?.chat;
|
|
427
|
+
return {
|
|
428
|
+
PageComponent:
|
|
429
|
+
CustomChat ??
|
|
430
|
+
(() => (
|
|
431
|
+
<ChatLayout
|
|
432
|
+
apiBaseURL={config.apiBaseURL}
|
|
433
|
+
apiBasePath={config.apiBasePath}
|
|
434
|
+
/>
|
|
435
|
+
)),
|
|
436
|
+
loader: createConversationsLoader(config),
|
|
437
|
+
meta: createChatHomeMeta(config),
|
|
438
|
+
};
|
|
439
|
+
}),
|
|
417
440
|
|
|
418
441
|
// Existing conversation
|
|
419
|
-
chatConversation: createRoute("/chat/:id", ({ params }) =>
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
442
|
+
chatConversation: createRoute("/chat/:id", ({ params }) => {
|
|
443
|
+
const CustomConversation = config.pageComponents?.chatConversation;
|
|
444
|
+
return {
|
|
445
|
+
PageComponent: CustomConversation
|
|
446
|
+
? () => <CustomConversation conversationId={params.id} />
|
|
447
|
+
: () => (
|
|
448
|
+
<ChatLayout
|
|
449
|
+
apiBaseURL={config.apiBaseURL}
|
|
450
|
+
apiBasePath={config.apiBasePath}
|
|
451
|
+
conversationId={params.id}
|
|
452
|
+
/>
|
|
453
|
+
),
|
|
454
|
+
loader: createConversationLoader(params.id, config),
|
|
455
|
+
meta: createConversationMeta(params.id, config),
|
|
456
|
+
};
|
|
457
|
+
}),
|
|
430
458
|
}),
|
|
431
459
|
|
|
432
460
|
// Chat pages typically shouldn't be in sitemap, but we provide the option
|
|
@@ -18,6 +18,7 @@ export function DefaultError({ error }: FallbackProps) {
|
|
|
18
18
|
const message =
|
|
19
19
|
process.env.NODE_ENV === "production"
|
|
20
20
|
? localization.BLOG_GENERIC_ERROR_MESSAGE
|
|
21
|
-
: (error
|
|
21
|
+
: ((error instanceof Error ? error.message : undefined) ??
|
|
22
|
+
localization.BLOG_GENERIC_ERROR_MESSAGE);
|
|
22
23
|
return <ErrorPlaceholder title={title} message={message} />;
|
|
23
24
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
runClientHookWithShim,
|
|
6
6
|
} from "@btst/stack/plugins/client";
|
|
7
7
|
import { createRoute } from "@btst/yar";
|
|
8
|
+
import type { ComponentType } from "react";
|
|
8
9
|
import type { QueryClient } from "@tanstack/react-query";
|
|
9
10
|
import type { BlogApiRouter } from "../api";
|
|
10
11
|
import { createBlogQueryKeys } from "../query-keys";
|
|
@@ -84,6 +85,26 @@ export interface BlogClientConfig {
|
|
|
84
85
|
|
|
85
86
|
/** Optional headers for SSR (e.g., forwarding cookies) */
|
|
86
87
|
headers?: Headers;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Optional page component overrides.
|
|
91
|
+
* Replace any plugin page with a custom React component.
|
|
92
|
+
* The built-in component is used as the fallback when not provided.
|
|
93
|
+
*/
|
|
94
|
+
pageComponents?: {
|
|
95
|
+
/** Replaces the published posts list page */
|
|
96
|
+
posts?: ComponentType;
|
|
97
|
+
/** Replaces the drafts list page */
|
|
98
|
+
drafts?: ComponentType;
|
|
99
|
+
/** Replaces the new post page */
|
|
100
|
+
newPost?: ComponentType;
|
|
101
|
+
/** Replaces the single post page */
|
|
102
|
+
post?: ComponentType<{ slug: string }>;
|
|
103
|
+
/** Replaces the edit post page */
|
|
104
|
+
editPost?: ComponentType<{ slug: string }>;
|
|
105
|
+
/** Replaces the tag posts page */
|
|
106
|
+
tag?: ComponentType<{ tagSlug: string }>;
|
|
107
|
+
};
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
/**
|
|
@@ -700,43 +721,57 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
|
|
|
700
721
|
|
|
701
722
|
routes: () => ({
|
|
702
723
|
posts: createRoute("/blog", () => {
|
|
724
|
+
const CustomPosts = config.pageComponents?.posts;
|
|
703
725
|
return {
|
|
704
|
-
PageComponent:
|
|
726
|
+
PageComponent:
|
|
727
|
+
CustomPosts ?? (() => <HomePageComponent published={true} />),
|
|
705
728
|
loader: createPostsLoader(true, config),
|
|
706
729
|
meta: createPostsListMeta(true, config),
|
|
707
730
|
};
|
|
708
731
|
}),
|
|
709
732
|
drafts: createRoute("/blog/drafts", () => {
|
|
733
|
+
const CustomDrafts = config.pageComponents?.drafts;
|
|
710
734
|
return {
|
|
711
|
-
PageComponent:
|
|
735
|
+
PageComponent:
|
|
736
|
+
CustomDrafts ?? (() => <HomePageComponent published={false} />),
|
|
712
737
|
loader: createPostsLoader(false, config),
|
|
713
738
|
meta: createPostsListMeta(false, config),
|
|
714
739
|
};
|
|
715
740
|
}),
|
|
716
741
|
newPost: createRoute("/blog/new", () => {
|
|
742
|
+
const CustomNewPost = config.pageComponents?.newPost;
|
|
717
743
|
return {
|
|
718
|
-
PageComponent: NewPostPageComponent,
|
|
744
|
+
PageComponent: CustomNewPost ?? NewPostPageComponent,
|
|
719
745
|
loader: createNewPostLoader(config),
|
|
720
746
|
meta: createNewPostMeta(config),
|
|
721
747
|
};
|
|
722
748
|
}),
|
|
723
749
|
editPost: createRoute("/blog/:slug/edit", ({ params: { slug } }) => {
|
|
750
|
+
const CustomEditPost = config.pageComponents?.editPost;
|
|
724
751
|
return {
|
|
725
|
-
PageComponent:
|
|
752
|
+
PageComponent: CustomEditPost
|
|
753
|
+
? () => <CustomEditPost slug={slug} />
|
|
754
|
+
: () => <EditPostPageComponent slug={slug} />,
|
|
726
755
|
loader: createPostLoader(slug, config, `/blog/${slug}/edit`),
|
|
727
756
|
meta: createEditPostMeta(slug, config),
|
|
728
757
|
};
|
|
729
758
|
}),
|
|
730
759
|
tag: createRoute("/blog/tag/:tagSlug", ({ params: { tagSlug } }) => {
|
|
760
|
+
const CustomTag = config.pageComponents?.tag;
|
|
731
761
|
return {
|
|
732
|
-
PageComponent:
|
|
762
|
+
PageComponent: CustomTag
|
|
763
|
+
? () => <CustomTag tagSlug={tagSlug} />
|
|
764
|
+
: () => <TagPageComponent tagSlug={tagSlug} />,
|
|
733
765
|
loader: createTagLoader(tagSlug, config),
|
|
734
766
|
meta: createTagMeta(tagSlug, config),
|
|
735
767
|
};
|
|
736
768
|
}),
|
|
737
769
|
post: createRoute("/blog/:slug", ({ params: { slug } }) => {
|
|
770
|
+
const CustomPost = config.pageComponents?.post;
|
|
738
771
|
return {
|
|
739
|
-
PageComponent:
|
|
772
|
+
PageComponent: CustomPost
|
|
773
|
+
? () => <CustomPost slug={slug} />
|
|
774
|
+
: () => <PostPageComponent slug={slug} />,
|
|
740
775
|
loader: createPostLoader(slug, config),
|
|
741
776
|
meta: createPostMeta(slug, config),
|
|
742
777
|
};
|