@btst/stack 3.1.2 → 4.1.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 +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +112 -103
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +112 -103
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +5 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +5 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +99 -88
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +99 -88
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.cjs +12 -3
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.mjs +12 -3
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +6 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +6 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/hooks/chat-hooks.cjs +5 -5
- package/dist/packages/stack/src/plugins/ai-chat/client/hooks/chat-hooks.mjs +5 -5
- package/dist/packages/stack/src/plugins/blog/client/components/shared/page-wrapper.cjs +3 -2
- package/dist/packages/stack/src/plugins/blog/client/components/shared/page-wrapper.mjs +3 -2
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-card.cjs +81 -52
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-card.mjs +82 -53
- package/dist/packages/stack/src/plugins/blog/client/components/shared/posts-list.cjs +3 -2
- package/dist/packages/stack/src/plugins/blog/client/components/shared/posts-list.mjs +3 -2
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-list-page.internal.cjs +95 -75
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-list-page.internal.mjs +95 -75
- package/dist/packages/stack/src/plugins/cms/client/components/pages/dashboard-page.internal.cjs +16 -3
- package/dist/packages/stack/src/plugins/cms/client/components/pages/dashboard-page.internal.mjs +16 -3
- package/dist/packages/stack/src/plugins/cms/client/components/shared/page-wrapper.cjs +3 -2
- package/dist/packages/stack/src/plugins/cms/client/components/shared/page-wrapper.mjs +3 -2
- package/dist/packages/stack/src/plugins/cms/utils.cjs +6 -2
- package/dist/packages/stack/src/plugins/cms/utils.mjs +6 -3
- package/dist/packages/stack/src/plugins/form-builder/client/components/forms/form-renderer.cjs +14 -8
- package/dist/packages/stack/src/plugins/form-builder/client/components/forms/form-renderer.mjs +14 -8
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.cjs +94 -87
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.mjs +94 -87
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +81 -74
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +81 -74
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/page-wrapper.cjs +3 -2
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/page-wrapper.mjs +3 -2
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/pagination.cjs +25 -16
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/pagination.mjs +25 -16
- package/dist/packages/stack/src/plugins/form-builder/utils.cjs +6 -2
- package/dist/packages/stack/src/plugins/form-builder/utils.mjs +6 -3
- package/dist/packages/stack/src/plugins/kanban/api/getters.cjs +7 -5
- package/dist/packages/stack/src/plugins/kanban/api/getters.mjs +7 -5
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.cjs +10 -8
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.mjs +10 -8
- 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/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/index.d.ts +2 -2
- package/dist/plugins/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 +20 -2
- package/dist/plugins/blog/client/index.d.mts +20 -2
- package/dist/plugins/blog/client/index.d.ts +20 -2
- 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/cms/api/index.d.cts +1 -1
- package/dist/plugins/cms/api/index.d.mts +1 -1
- package/dist/plugins/cms/api/index.d.ts +1 -1
- package/dist/plugins/cms/client/index.d.cts +18 -0
- package/dist/plugins/cms/client/index.d.mts +18 -0
- package/dist/plugins/cms/client/index.d.ts +18 -0
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- package/dist/plugins/form-builder/client/index.d.cts +16 -0
- package/dist/plugins/form-builder/client/index.d.mts +16 -0
- package/dist/plugins/form-builder/client/index.d.ts +16 -0
- 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 +1 -1
- package/dist/plugins/ui-builder/client/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/index.d.ts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.cts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.mts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.ts +1 -1
- package/dist/shared/{stack.BTStHVTy.d.cts → stack.BGzgFx3R.d.cts} +5 -5
- package/dist/shared/{stack.CtrnoQgH.d.cts → stack.BOZRaVpu.d.cts} +35 -35
- package/dist/shared/{stack.94kN3pEK.d.mts → stack.CjCSDfWy.d.mts} +5 -5
- package/dist/shared/{stack.lPKwd_v0.d.ts → stack.DlBoNqYu.d.ts} +35 -35
- package/dist/shared/{stack.DfrRYOIl.d.ts → stack.Dpg_JjIo.d.ts} +5 -5
- package/dist/shared/{stack.BggTHKlo.d.mts → stack.Dvy7FmaA.d.mts} +35 -35
- package/dist/shared/{stack.5_zzCejP.d.cts → stack.rVF0f509.d.cts} +20 -0
- package/dist/shared/{stack.5_zzCejP.d.mts → stack.rVF0f509.d.mts} +20 -0
- package/dist/shared/{stack.5_zzCejP.d.ts → stack.rVF0f509.d.ts} +20 -0
- package/package.json +11 -7
- package/src/__tests__/package-metadata.test.ts +4 -4
- package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +248 -0
- package/src/plugins/ai-chat/__tests__/hooks.test.tsx +70 -54
- package/src/plugins/ai-chat/client/components/chat-input.tsx +6 -1
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +3 -0
- package/src/plugins/ai-chat/client/components/chat-layout.tsx +9 -2
- package/src/plugins/ai-chat/client/components/chat-message.tsx +13 -5
- package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +6 -2
- package/src/plugins/ai-chat/client/hooks/chat-hooks.tsx +6 -9
- package/src/plugins/ai-chat/client/overrides.ts +21 -0
- package/src/plugins/blog/__tests__/classnames-overrides.test.tsx +206 -0
- package/src/plugins/blog/client/components/shared/page-wrapper.tsx +3 -2
- package/src/plugins/blog/client/components/shared/post-card.tsx +32 -12
- package/src/plugins/blog/client/components/shared/posts-list.tsx +3 -2
- package/src/plugins/blog/client/overrides.ts +18 -0
- package/src/plugins/cms/__tests__/classnames-overrides.test.tsx +200 -0
- package/src/plugins/cms/client/components/pages/content-list-page.internal.tsx +17 -4
- package/src/plugins/cms/client/components/pages/dashboard-page.internal.tsx +14 -3
- package/src/plugins/cms/client/components/shared/page-wrapper.tsx +3 -2
- package/src/plugins/cms/client/overrides.ts +19 -0
- package/src/plugins/form-builder/__tests__/classnames-overrides.test.tsx +312 -0
- package/src/plugins/form-builder/client/components/forms/form-renderer.tsx +14 -9
- package/src/plugins/form-builder/client/components/pages/form-list-page.internal.tsx +9 -5
- package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +9 -5
- package/src/plugins/form-builder/client/components/shared/page-wrapper.tsx +3 -2
- package/src/plugins/form-builder/client/components/shared/pagination.tsx +7 -1
- package/src/plugins/form-builder/client/overrides.ts +17 -0
- package/src/plugins/kanban/__tests__/getters.test.ts +14 -1
- package/src/plugins/kanban/api/getters.ts +9 -7
- package/src/plugins/kanban/types.ts +4 -6
- package/src/plugins/media/client/hooks/use-media.tsx +7 -8
- package/dist/shared/{stack.lsbdKRj7.d.mts → stack.B8PG21zX.d.mts} +16 -16
- package/dist/shared/{stack.CKc_Bais.d.cts → stack.Csi-4Mgz.d.cts} +7 -7
- package/dist/shared/{stack.CKc_Bais.d.mts → stack.Csi-4Mgz.d.mts} +7 -7
- package/dist/shared/{stack.CKc_Bais.d.ts → stack.Csi-4Mgz.d.ts} +7 -7
- package/dist/shared/{stack.BMlTZnZp.d.ts → stack.CyalJ0j6.d.ts} +16 -16
- package/dist/shared/{stack.BV0uNV4f.d.cts → stack.D6Pg-ufX.d.cts} +16 -16
|
@@ -177,58 +177,74 @@ describe("useRenameConversationForm", () => {
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
describe("useDeleteConversation", () => {
|
|
180
|
-
it("
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
180
|
+
it.each(["mutate", "mutateAsync"] as const)(
|
|
181
|
+
"%s forwards callbacks and evicts only the deleted detail in the starting identity partition",
|
|
182
|
+
async (method) => {
|
|
183
|
+
const identityPartition = "anonymous" as const;
|
|
184
|
+
const otherConversation = {
|
|
185
|
+
...conversation,
|
|
186
|
+
id: "conv-2",
|
|
187
|
+
title: "Other",
|
|
188
|
+
};
|
|
189
|
+
const otherIdentity = { id: "other-user", role: "user" } as const;
|
|
190
|
+
const listKey = buildQueryKey(
|
|
191
|
+
"conversations",
|
|
192
|
+
"list",
|
|
193
|
+
aiChatResources.conversations.queries.list,
|
|
194
|
+
[identityPartition],
|
|
195
|
+
);
|
|
196
|
+
const deletedDetailKey = buildQueryKey(
|
|
197
|
+
"conversations",
|
|
198
|
+
"detail",
|
|
199
|
+
aiChatResources.conversations.queries.detail,
|
|
200
|
+
[conversation.id, identityPartition],
|
|
201
|
+
);
|
|
202
|
+
const otherDetailKey = buildQueryKey(
|
|
203
|
+
"conversations",
|
|
204
|
+
"detail",
|
|
205
|
+
aiChatResources.conversations.queries.detail,
|
|
206
|
+
[otherConversation.id, identityPartition],
|
|
207
|
+
);
|
|
208
|
+
const otherIdentityDetailKey = buildQueryKey(
|
|
209
|
+
"conversations",
|
|
210
|
+
"detail",
|
|
211
|
+
aiChatResources.conversations.queries.detail,
|
|
212
|
+
[conversation.id, otherIdentity],
|
|
213
|
+
);
|
|
214
|
+
let listFetches = 0;
|
|
215
|
+
await queryClient.fetchQuery({
|
|
216
|
+
queryKey: listKey,
|
|
217
|
+
queryFn: async () => {
|
|
218
|
+
listFetches += 1;
|
|
219
|
+
return [conversation, otherConversation];
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
listFetches = 0;
|
|
223
|
+
queryClient.setQueryData(deletedDetailKey, conversation);
|
|
224
|
+
queryClient.setQueryData(otherDetailKey, otherConversation);
|
|
225
|
+
queryClient.setQueryData(otherIdentityDetailKey, conversation);
|
|
226
|
+
fetchMock.mockResolvedValue(jsonResponse({ success: true }));
|
|
227
|
+
const getMutation = await renderDeleteProbe();
|
|
228
|
+
const onSuccess = vi.fn();
|
|
229
|
+
|
|
230
|
+
await act(async () => {
|
|
231
|
+
await getMutation()[method]({ id: conversation.id }, { onSuccess });
|
|
232
|
+
await vi.waitFor(() =>
|
|
233
|
+
expect(queryClient.getQueryData(deletedDetailKey)).toBeUndefined(),
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
238
|
+
expect(onSuccess.mock.calls[0]?.[1]).toEqual({ id: conversation.id });
|
|
239
|
+
|
|
240
|
+
expect(listFetches).toBe(1);
|
|
241
|
+
expect(queryClient.getQueryData(deletedDetailKey)).toBeUndefined();
|
|
242
|
+
expect(queryClient.getQueryData(otherDetailKey)).toEqual(
|
|
243
|
+
otherConversation,
|
|
244
|
+
);
|
|
245
|
+
expect(queryClient.getQueryData(otherIdentityDetailKey)).toEqual(
|
|
246
|
+
conversation,
|
|
247
|
+
);
|
|
248
|
+
},
|
|
249
|
+
);
|
|
234
250
|
});
|
|
@@ -93,6 +93,7 @@ export function ChatInput({
|
|
|
93
93
|
uploadFile,
|
|
94
94
|
localization: customLocalization,
|
|
95
95
|
allowedFileTypes,
|
|
96
|
+
classNames,
|
|
96
97
|
} = usePluginOverrides<AiChatPluginOverrides, Partial<AiChatPluginOverrides>>(
|
|
97
98
|
"aiChat",
|
|
98
99
|
{},
|
|
@@ -314,7 +315,11 @@ export function ChatInput({
|
|
|
314
315
|
)}
|
|
315
316
|
</PermissionCheck>
|
|
316
317
|
)}
|
|
317
|
-
<form
|
|
318
|
+
<form
|
|
319
|
+
onSubmit={handleFormSubmit}
|
|
320
|
+
className={cn("space-y-2", classNames?.input)}
|
|
321
|
+
data-testid="chat-input-form"
|
|
322
|
+
>
|
|
318
323
|
{/* Attached Files Preview */}
|
|
319
324
|
{attachedFiles.length > 0 && (
|
|
320
325
|
<div className="flex flex-wrap gap-2">
|
|
@@ -263,6 +263,7 @@ export function ChatInterface({
|
|
|
263
263
|
localization: customLocalization,
|
|
264
264
|
showAttribution,
|
|
265
265
|
chatSuggestions,
|
|
266
|
+
classNames,
|
|
266
267
|
} = usePluginOverrides<AiChatPluginOverrides, Partial<AiChatPluginOverrides>>(
|
|
267
268
|
"aiChat",
|
|
268
269
|
{ showAttribution: true },
|
|
@@ -1139,7 +1140,9 @@ export function ChatInterface({
|
|
|
1139
1140
|
className={cn(
|
|
1140
1141
|
"flex flex-col p-4",
|
|
1141
1142
|
isWidget ? "max-w-full" : "max-w-3xl mx-auto w-full",
|
|
1143
|
+
classNames?.messageList,
|
|
1142
1144
|
)}
|
|
1145
|
+
data-testid="chat-message-list"
|
|
1143
1146
|
>
|
|
1144
1147
|
{messages.length === 0 ? (
|
|
1145
1148
|
<div className="flex flex-col h-full min-h-[300px]">
|
|
@@ -86,7 +86,7 @@ export function ChatLayout(props: ChatLayoutProps) {
|
|
|
86
86
|
onMessagesChange,
|
|
87
87
|
onClear,
|
|
88
88
|
} = props;
|
|
89
|
-
const { localization } = usePluginOverrides<
|
|
89
|
+
const { localization, classNames } = usePluginOverrides<
|
|
90
90
|
AiChatPluginOverrides,
|
|
91
91
|
Partial<AiChatPluginOverrides>
|
|
92
92
|
>("aiChat", {});
|
|
@@ -133,7 +133,13 @@ export function ChatLayout(props: ChatLayoutProps) {
|
|
|
133
133
|
|
|
134
134
|
if (layout === "widget") {
|
|
135
135
|
return (
|
|
136
|
-
<div
|
|
136
|
+
<div
|
|
137
|
+
className={cn(
|
|
138
|
+
"flex flex-col items-end gap-3",
|
|
139
|
+
className,
|
|
140
|
+
classNames?.container,
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
137
143
|
{/* Chat panel — always mounted to preserve conversation state, hidden when closed */}
|
|
138
144
|
<div
|
|
139
145
|
className={cn(
|
|
@@ -238,6 +244,7 @@ export function ChatLayout(props: ChatLayoutProps) {
|
|
|
238
244
|
className={cn(
|
|
239
245
|
"flex h-[calc(100vh-4rem)] w-full overflow-hidden",
|
|
240
246
|
className,
|
|
247
|
+
classNames?.container,
|
|
241
248
|
)}
|
|
242
249
|
data-testid="chat-layout"
|
|
243
250
|
>
|
|
@@ -133,11 +133,14 @@ export function ChatMessage({
|
|
|
133
133
|
}: ChatMessageProps) {
|
|
134
134
|
const isUser = message.role === "user";
|
|
135
135
|
|
|
136
|
-
const {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
const {
|
|
137
|
+
localization: customLocalization,
|
|
138
|
+
toolRenderers,
|
|
139
|
+
classNames,
|
|
140
|
+
} = usePluginOverrides<AiChatPluginOverrides, Partial<AiChatPluginOverrides>>(
|
|
141
|
+
"aiChat",
|
|
142
|
+
{},
|
|
143
|
+
);
|
|
141
144
|
const { router } = useStack();
|
|
142
145
|
const Link = router?.Link;
|
|
143
146
|
const Image = router?.Image;
|
|
@@ -265,7 +268,10 @@ export function ChatMessage({
|
|
|
265
268
|
"flex gap-3 w-full",
|
|
266
269
|
isCompact ? "mb-3" : "mb-4",
|
|
267
270
|
isUser ? "justify-end" : "justify-start",
|
|
271
|
+
classNames?.message,
|
|
268
272
|
)}
|
|
273
|
+
data-testid="chat-message"
|
|
274
|
+
data-role={message.role}
|
|
269
275
|
aria-label={
|
|
270
276
|
isUser
|
|
271
277
|
? tr("A11Y_USER_MESSAGE", "aiChat.a11y.userMessage", "Your message")
|
|
@@ -295,7 +301,9 @@ export function ChatMessage({
|
|
|
295
301
|
"rounded-lg",
|
|
296
302
|
isCompact ? "px-3 py-2" : "px-4 py-3",
|
|
297
303
|
isUser ? "bg-primary text-primary-foreground" : "bg-muted",
|
|
304
|
+
isUser ? classNames?.userMessage : classNames?.assistantMessage,
|
|
298
305
|
)}
|
|
306
|
+
data-testid="chat-message-bubble"
|
|
299
307
|
>
|
|
300
308
|
{isUser ? (
|
|
301
309
|
// User messages: files + images + text (or edit mode)
|
|
@@ -67,7 +67,7 @@ export function ChatSidebar({
|
|
|
67
67
|
onNewChat,
|
|
68
68
|
className,
|
|
69
69
|
}: ChatSidebarProps) {
|
|
70
|
-
const { localization: customLocalization } = usePluginOverrides<
|
|
70
|
+
const { localization: customLocalization, classNames } = usePluginOverrides<
|
|
71
71
|
AiChatPluginOverrides,
|
|
72
72
|
Partial<AiChatPluginOverrides>
|
|
73
73
|
>("aiChat", {});
|
|
@@ -208,7 +208,11 @@ export function ChatSidebar({
|
|
|
208
208
|
return (
|
|
209
209
|
<div
|
|
210
210
|
data-testid="chat-sidebar"
|
|
211
|
-
className={cn(
|
|
211
|
+
className={cn(
|
|
212
|
+
"flex flex-col h-full bg-muted/30 border-r",
|
|
213
|
+
className,
|
|
214
|
+
classNames?.sidebar,
|
|
215
|
+
)}
|
|
212
216
|
>
|
|
213
217
|
{/* Header */}
|
|
214
218
|
<div className="p-4 border-b">
|
|
@@ -146,25 +146,22 @@ function withCurrentHistoryRefresh<TData, TVariables>(
|
|
|
146
146
|
refresh: ReturnType<typeof useCurrentHistoryRefresh>,
|
|
147
147
|
queryKey?: readonly unknown[],
|
|
148
148
|
afterSuccess?: (
|
|
149
|
-
variables:
|
|
149
|
+
variables: Parameters<typeof mutation.mutateAsync>[0],
|
|
150
150
|
startedAs: ReturnType<typeof useAiChatIdentityPartition>,
|
|
151
151
|
) => void,
|
|
152
152
|
): UseMutationResult<TData, Error, TVariables> {
|
|
153
|
-
const mutateAsync: typeof mutation.mutateAsync = async (
|
|
154
|
-
variables,
|
|
155
|
-
options,
|
|
156
|
-
) => {
|
|
153
|
+
const mutateAsync: typeof mutation.mutateAsync = async (...args) => {
|
|
157
154
|
const startedAs = refresh.currentPartition();
|
|
158
|
-
const result = await mutation.mutateAsync(
|
|
155
|
+
const result = await mutation.mutateAsync(...args);
|
|
159
156
|
try {
|
|
160
157
|
await refresh.refreshAfterSuccess(startedAs, queryKey);
|
|
161
158
|
} finally {
|
|
162
|
-
afterSuccess?.(
|
|
159
|
+
afterSuccess?.(args[0], startedAs);
|
|
163
160
|
}
|
|
164
161
|
return result;
|
|
165
162
|
};
|
|
166
|
-
const mutate: typeof mutation.mutate = (
|
|
167
|
-
void mutateAsync(
|
|
163
|
+
const mutate: typeof mutation.mutate = (...args) => {
|
|
164
|
+
void mutateAsync(...args).catch(() => {});
|
|
168
165
|
};
|
|
169
166
|
return { ...mutation, mutate, mutateAsync };
|
|
170
167
|
}
|
|
@@ -201,6 +201,27 @@ export interface AiChatPluginOverrides {
|
|
|
201
201
|
*/
|
|
202
202
|
toolRenderers?: Record<string, ToolCallRenderer>;
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Optional class names merged onto specific UI elements.
|
|
206
|
+
* Use these for targeted styling without replacing whole components.
|
|
207
|
+
*/
|
|
208
|
+
classNames?: {
|
|
209
|
+
/** Root of `ChatLayout` (full-page or widget mode) */
|
|
210
|
+
container?: string;
|
|
211
|
+
/** Conversation sidebar root */
|
|
212
|
+
sidebar?: string;
|
|
213
|
+
/** Wrapper around the list of messages */
|
|
214
|
+
messageList?: string;
|
|
215
|
+
/** Each message row (user and assistant) */
|
|
216
|
+
message?: string;
|
|
217
|
+
/** The bubble of a user message */
|
|
218
|
+
userMessage?: string;
|
|
219
|
+
/** The bubble of an assistant message */
|
|
220
|
+
assistantMessage?: string;
|
|
221
|
+
/** The composer form at the bottom of the chat */
|
|
222
|
+
input?: string;
|
|
223
|
+
};
|
|
224
|
+
|
|
204
225
|
// ============== Lifecycle Hooks (optional) ==============
|
|
205
226
|
|
|
206
227
|
/**
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
// Core primitives MUST be imported from the package entry (not relative src
|
|
6
|
+
// paths) so they share module identity — and React context — with the blog
|
|
7
|
+
// components, which resolve `@btst/stack/*` via package self-reference.
|
|
8
|
+
import { StackProvider } from "@btst/stack/context";
|
|
9
|
+
import { PageWrapper } from "../client/components/shared/page-wrapper";
|
|
10
|
+
import { PostsList } from "../client/components/shared/posts-list";
|
|
11
|
+
import type { SerializedPost } from "../types";
|
|
12
|
+
import { createTestClientStack } from "../../../__tests__/client-stack-test-utils";
|
|
13
|
+
import { blogClientPlugin } from "../client/plugin";
|
|
14
|
+
|
|
15
|
+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
16
|
+
|
|
17
|
+
// jsdom lacks these APIs used by Radix / cmdk inside SearchInput
|
|
18
|
+
(globalThis as any).ResizeObserver ??= class {
|
|
19
|
+
observe() {}
|
|
20
|
+
unobserve() {}
|
|
21
|
+
disconnect() {}
|
|
22
|
+
};
|
|
23
|
+
Element.prototype.scrollIntoView ??= () => {};
|
|
24
|
+
|
|
25
|
+
const hooks = vi.hoisted(() => ({
|
|
26
|
+
useSuspensePost: vi.fn(),
|
|
27
|
+
useDeletePost: vi.fn(),
|
|
28
|
+
usePostForm: vi.fn(),
|
|
29
|
+
usePostSearch: vi.fn(),
|
|
30
|
+
useSuspensePosts: vi.fn(),
|
|
31
|
+
useTags: vi.fn(),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("../client/hooks/blog-hooks", () => hooks);
|
|
35
|
+
|
|
36
|
+
const CLASS_NAMES = {
|
|
37
|
+
container: "bg-red-100",
|
|
38
|
+
postCard: "bg-blue-100",
|
|
39
|
+
postTitle: "text-purple-700",
|
|
40
|
+
postMeta: "text-green-600",
|
|
41
|
+
tagsList: "bg-yellow-50",
|
|
42
|
+
pagination: "border-4 border-orange-500",
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
const post: SerializedPost = {
|
|
46
|
+
id: "p1",
|
|
47
|
+
title: "Hello World",
|
|
48
|
+
content: "# Hello",
|
|
49
|
+
excerpt: "An excerpt",
|
|
50
|
+
slug: "hello-world",
|
|
51
|
+
published: true,
|
|
52
|
+
image: "",
|
|
53
|
+
tags: [{ id: "t1", name: "Release", slug: "release" }],
|
|
54
|
+
authorId: null,
|
|
55
|
+
publishedAt: new Date("2024-01-01").toISOString(),
|
|
56
|
+
createdAt: new Date("2024-01-01").toISOString(),
|
|
57
|
+
updatedAt: new Date("2024-01-01").toISOString(),
|
|
58
|
+
} as unknown as SerializedPost;
|
|
59
|
+
|
|
60
|
+
let container: HTMLDivElement;
|
|
61
|
+
let root: Root;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
container = document.createElement("div");
|
|
65
|
+
document.body.appendChild(container);
|
|
66
|
+
root = createRoot(container);
|
|
67
|
+
|
|
68
|
+
hooks.usePostSearch.mockReturnValue({ data: [], isLoading: false });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(async () => {
|
|
72
|
+
await act(async () => {
|
|
73
|
+
root.unmount();
|
|
74
|
+
});
|
|
75
|
+
container.remove();
|
|
76
|
+
vi.clearAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
async function render(ui: React.ReactElement) {
|
|
80
|
+
await act(async () => {
|
|
81
|
+
root.render(ui);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function withBlogOverrides(children: React.ReactNode) {
|
|
86
|
+
return (
|
|
87
|
+
<StackProvider
|
|
88
|
+
stack={createTestClientStack({ blog: blogClientPlugin() })}
|
|
89
|
+
overrides={{
|
|
90
|
+
blog: {
|
|
91
|
+
uploadImage: async () => "",
|
|
92
|
+
classNames: CLASS_NAMES,
|
|
93
|
+
},
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</StackProvider>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function query(selector: string): HTMLElement {
|
|
102
|
+
const el = container.querySelector<HTMLElement>(selector);
|
|
103
|
+
expect(el, `expected to find ${selector}`).toBeTruthy();
|
|
104
|
+
return el!;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe("blog classNames overrides (issue #36)", () => {
|
|
108
|
+
it("applies classNames.container to the page container", async () => {
|
|
109
|
+
await render(
|
|
110
|
+
withBlogOverrides(
|
|
111
|
+
<PageWrapper testId="blog-container">
|
|
112
|
+
<span>content</span>
|
|
113
|
+
</PageWrapper>,
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const el = query('[data-testid="blog-container"]');
|
|
118
|
+
expect(el.className).toContain(CLASS_NAMES.container);
|
|
119
|
+
// The override must merge with the layout's own classes, not replace them.
|
|
120
|
+
expect(el.className).toContain("container");
|
|
121
|
+
expect(el.className).toContain("mx-auto");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("applies classNames.postCard, postTitle, postMeta and tagsList inside a post card", async () => {
|
|
125
|
+
await render(withBlogOverrides(<PostsList posts={[post]} />));
|
|
126
|
+
|
|
127
|
+
// postCard — anchored on the Card primitive's structural slot.
|
|
128
|
+
const card = query('[data-slot="card"]');
|
|
129
|
+
expect(card.className).toContain(CLASS_NAMES.postCard);
|
|
130
|
+
expect(card.className).toContain("group");
|
|
131
|
+
|
|
132
|
+
// postTitle — anchored on the CardTitle primitive's structural slot.
|
|
133
|
+
const title = query('[data-slot="card-title"]');
|
|
134
|
+
expect(title.className).toContain(CLASS_NAMES.postTitle);
|
|
135
|
+
expect(title.className).toContain("line-clamp-3");
|
|
136
|
+
expect(title.textContent).toBe(post.title);
|
|
137
|
+
|
|
138
|
+
// postMeta — the wrapper holding the published date.
|
|
139
|
+
const meta = query("time").parentElement!;
|
|
140
|
+
expect(meta.className).toContain(CLASS_NAMES.postMeta);
|
|
141
|
+
// `cn` runs tailwind-merge, so the conflicting `text-muted-foreground`
|
|
142
|
+
// is intentionally replaced by the override; non-conflicting base
|
|
143
|
+
// classes must survive.
|
|
144
|
+
expect(meta.className).toContain("items-center");
|
|
145
|
+
expect(meta.className).not.toContain("text-muted-foreground");
|
|
146
|
+
|
|
147
|
+
// tagsList — the wrapper immediately following the metadata block.
|
|
148
|
+
const tags = meta.nextElementSibling as HTMLElement;
|
|
149
|
+
expect(tags.className).toContain(CLASS_NAMES.tagsList);
|
|
150
|
+
expect(tags.className).toContain("flex-wrap");
|
|
151
|
+
expect(tags.textContent).toContain("Release");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("applies classNames.pagination to the load-more wrapper", async () => {
|
|
155
|
+
await render(
|
|
156
|
+
withBlogOverrides(
|
|
157
|
+
<PostsList posts={[post]} onLoadMore={() => {}} hasMore />,
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const button = [...container.querySelectorAll("button")].find((b) =>
|
|
162
|
+
b.textContent?.includes("Load more posts"),
|
|
163
|
+
);
|
|
164
|
+
expect(button, "expected a load-more button").toBeTruthy();
|
|
165
|
+
|
|
166
|
+
const pagination = button!.parentElement!;
|
|
167
|
+
expect(pagination.className).toContain("border-4");
|
|
168
|
+
expect(pagination.className).toContain("border-orange-500");
|
|
169
|
+
expect(pagination.className).toContain("justify-center");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("leaves base classes untouched when no classNames override is supplied", async () => {
|
|
173
|
+
await render(
|
|
174
|
+
<StackProvider
|
|
175
|
+
stack={createTestClientStack({ blog: blogClientPlugin() })}
|
|
176
|
+
overrides={{ blog: { uploadImage: async () => "" } }}
|
|
177
|
+
>
|
|
178
|
+
<PostsList
|
|
179
|
+
posts={[
|
|
180
|
+
{
|
|
181
|
+
...post,
|
|
182
|
+
tags: [
|
|
183
|
+
...post.tags!,
|
|
184
|
+
{ ...post.tags![0]!, id: "t2", name: "News", slug: "news" },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
]}
|
|
188
|
+
onLoadMore={() => {}}
|
|
189
|
+
hasMore
|
|
190
|
+
/>
|
|
191
|
+
</StackProvider>,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const card = query('[data-slot="card"]');
|
|
195
|
+
expect(card.className).toContain("group");
|
|
196
|
+
expect(card.className).not.toContain(CLASS_NAMES.postCard);
|
|
197
|
+
const metadata = query("time").parentElement!;
|
|
198
|
+
const tags = metadata.parentElement!.querySelectorAll(
|
|
199
|
+
'[data-slot="badge"]',
|
|
200
|
+
);
|
|
201
|
+
expect(tags).toHaveLength(2);
|
|
202
|
+
for (const tag of tags) {
|
|
203
|
+
expect(tag.parentElement).toBe(metadata.parentElement);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -4,6 +4,7 @@ import { usePluginOverrides } from "@btst/stack/context";
|
|
|
4
4
|
import { PageWrapper as SharedPageWrapper } from "@workspace/ui/components/page-wrapper";
|
|
5
5
|
import type { BlogPluginOverrides } from "../../overrides";
|
|
6
6
|
import { BLOG_PLUGIN_ID } from "../../constants";
|
|
7
|
+
import { cn } from "../../../utils";
|
|
7
8
|
|
|
8
9
|
export function PageWrapper({
|
|
9
10
|
children,
|
|
@@ -14,7 +15,7 @@ export function PageWrapper({
|
|
|
14
15
|
className?: string;
|
|
15
16
|
testId?: string;
|
|
16
17
|
}) {
|
|
17
|
-
const { showAttribution } = usePluginOverrides<
|
|
18
|
+
const { showAttribution, classNames } = usePluginOverrides<
|
|
18
19
|
BlogPluginOverrides,
|
|
19
20
|
Partial<BlogPluginOverrides>
|
|
20
21
|
>(BLOG_PLUGIN_ID, {
|
|
@@ -23,7 +24,7 @@ export function PageWrapper({
|
|
|
23
24
|
|
|
24
25
|
return (
|
|
25
26
|
<SharedPageWrapper
|
|
26
|
-
className={className}
|
|
27
|
+
className={cn(className, classNames?.container)}
|
|
27
28
|
testId={testId}
|
|
28
29
|
showAttribution={showAttribution}
|
|
29
30
|
>
|
|
@@ -18,7 +18,7 @@ import { CalendarIcon } from "lucide-react";
|
|
|
18
18
|
import type { BlogPluginOverrides } from "../../overrides";
|
|
19
19
|
import { BLOG_PLUGIN_ID } from "../../constants";
|
|
20
20
|
import { DefaultLink, DefaultImage } from "./defaults";
|
|
21
|
-
|
|
21
|
+
import { cn } from "../../../utils";
|
|
22
22
|
// Beautiful gradient color combinations
|
|
23
23
|
const GRADIENT_PALETTES = [
|
|
24
24
|
{
|
|
@@ -222,7 +222,7 @@ function getGradientFromTitle(title: string) {
|
|
|
222
222
|
|
|
223
223
|
export function PostCard({ post }: { post: SerializedPost }) {
|
|
224
224
|
const t = useTranslate();
|
|
225
|
-
const { localization } = usePluginOverrides<
|
|
225
|
+
const { localization, classNames } = usePluginOverrides<
|
|
226
226
|
BlogPluginOverrides,
|
|
227
227
|
Partial<BlogPluginOverrides>
|
|
228
228
|
>(BLOG_PLUGIN_ID, {});
|
|
@@ -234,9 +234,19 @@ export function PostCard({ post }: { post: SerializedPost }) {
|
|
|
234
234
|
const timestamp = post.publishedAt || post.createdAt;
|
|
235
235
|
const postDate = formatPostDate(timestamp);
|
|
236
236
|
const gradient = post.image ? null : getGradientFromTitle(post.title);
|
|
237
|
+
const tags = post.tags?.map((tag) => (
|
|
238
|
+
<Badge key={tag.id} variant="secondary" className="text-xs">
|
|
239
|
+
{tag.name}
|
|
240
|
+
</Badge>
|
|
241
|
+
));
|
|
237
242
|
|
|
238
243
|
return (
|
|
239
|
-
<Card
|
|
244
|
+
<Card
|
|
245
|
+
className={cn(
|
|
246
|
+
"group relative flex h-full flex-col gap-4 pt-0! pb-4! transition-shadow duration-200 hover:shadow-lg",
|
|
247
|
+
classNames?.postCard,
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
240
250
|
{/* Image or Placeholder */}
|
|
241
251
|
<Link
|
|
242
252
|
href={blogPath}
|
|
@@ -281,24 +291,34 @@ export function PostCard({ post }: { post: SerializedPost }) {
|
|
|
281
291
|
className="block focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 grow"
|
|
282
292
|
>
|
|
283
293
|
<CardHeader className="flex-1">
|
|
284
|
-
<CardTitle
|
|
294
|
+
<CardTitle
|
|
295
|
+
className={cn(
|
|
296
|
+
"line-clamp-3 text-lg leading-tight transition-colors group-hover:underline",
|
|
297
|
+
classNames?.postTitle,
|
|
298
|
+
)}
|
|
299
|
+
>
|
|
285
300
|
{post.title}
|
|
286
301
|
</CardTitle>
|
|
287
302
|
</CardHeader>
|
|
288
303
|
|
|
289
304
|
<CardContent className="flex flex-1 flex-col gap-2">
|
|
290
305
|
<div className="flex flex-wrap gap-2">
|
|
291
|
-
<div
|
|
306
|
+
<div
|
|
307
|
+
className={cn(
|
|
308
|
+
"flex items-center gap-2 text-muted-foreground text-xs",
|
|
309
|
+
classNames?.postMeta,
|
|
310
|
+
)}
|
|
311
|
+
>
|
|
292
312
|
<CalendarIcon className="h-3 w-3" />
|
|
293
313
|
<time dateTime={timestamp}>{postDate}</time>
|
|
294
314
|
</div>
|
|
295
|
-
{
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
315
|
+
{classNames?.tagsList ? (
|
|
316
|
+
<div className={cn("flex flex-wrap gap-2", classNames.tagsList)}>
|
|
317
|
+
{tags}
|
|
318
|
+
</div>
|
|
319
|
+
) : (
|
|
320
|
+
tags
|
|
321
|
+
)}
|
|
302
322
|
</div>
|
|
303
323
|
</CardContent>
|
|
304
324
|
</Link>
|
|
@@ -6,6 +6,7 @@ import { SearchInput } from "./search-input";
|
|
|
6
6
|
import type { BlogPluginOverrides } from "../../overrides";
|
|
7
7
|
import { BLOG_PLUGIN_ID } from "../../constants";
|
|
8
8
|
import { PostCard as DefaultPostCard } from "./post-card";
|
|
9
|
+
import { cn } from "../../../utils";
|
|
9
10
|
|
|
10
11
|
interface PostsListProps {
|
|
11
12
|
posts: SerializedPost[];
|
|
@@ -21,7 +22,7 @@ export function PostsList({
|
|
|
21
22
|
isLoadingMore,
|
|
22
23
|
}: PostsListProps) {
|
|
23
24
|
const t = useTranslate();
|
|
24
|
-
const { localization, PostCard } =
|
|
25
|
+
const { localization, PostCard, classNames } =
|
|
25
26
|
usePluginOverrides<BlogPluginOverrides>(BLOG_PLUGIN_ID);
|
|
26
27
|
|
|
27
28
|
const PostCardComponent = PostCard || DefaultPostCard;
|
|
@@ -61,7 +62,7 @@ export function PostsList({
|
|
|
61
62
|
</div>
|
|
62
63
|
|
|
63
64
|
{onLoadMore && hasMore && (
|
|
64
|
-
<div className="flex justify-center">
|
|
65
|
+
<div className={cn("flex justify-center", classNames?.pagination)}>
|
|
65
66
|
<Button
|
|
66
67
|
onClick={onLoadMore}
|
|
67
68
|
disabled={isLoadingMore}
|