@btst/stack 4.0.0 → 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/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/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/client/index.d.cts +18 -0
- package/dist/plugins/blog/client/index.d.mts +18 -0
- package/dist/plugins/blog/client/index.d.ts +18 -0
- 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/form-builder/api/index.d.cts +1 -1
- package/dist/plugins/form-builder/api/index.d.mts +1 -1
- package/dist/plugins/form-builder/api/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 +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/form-builder/query-keys.d.cts +1 -1
- package/dist/plugins/form-builder/query-keys.d.mts +1 -1
- package/dist/plugins/form-builder/query-keys.d.ts +1 -1
- 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 +1 -1
- package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +248 -0
- 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/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/dist/shared/{stack.BuxUQqL8.d.ts → stack.CGmh-Lpe.d.ts} +12 -12
- package/dist/shared/{stack.Egr3RWhA.d.mts → stack.Doy2_wgl.d.mts} +12 -12
- package/dist/shared/{stack.CX83YNuS.d.cts → stack.zch70Loj.d.cts} +12 -12
|
@@ -0,0 +1,312 @@
|
|
|
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
|
+
import { z } from "zod";
|
|
6
|
+
// Core primitives MUST be imported from the package entry (not relative src
|
|
7
|
+
// paths) so they share module identity — and React context — with the
|
|
8
|
+
// form-builder components, which resolve `@btst/stack/*` via package
|
|
9
|
+
// self-reference.
|
|
10
|
+
import { StackProvider } from "@btst/stack/context";
|
|
11
|
+
import { defineAuthorization } from "@btst/stack/authorization";
|
|
12
|
+
import { createClientAuth } from "@btst/stack/authorization/client";
|
|
13
|
+
import { FormRenderer } from "../client/components/forms/form-renderer";
|
|
14
|
+
import { FormListPage } from "../client/components/pages/form-list-page.internal";
|
|
15
|
+
import { SubmissionsPage } from "../client/components/pages/submissions-page.internal";
|
|
16
|
+
import { formBuilderClientPlugin } from "../client/plugin";
|
|
17
|
+
import { formBuilderPermissions } from "../permissions";
|
|
18
|
+
import type { SerializedForm } from "../types";
|
|
19
|
+
import { createTestClientStack } from "../../../__tests__/client-stack-test-utils";
|
|
20
|
+
|
|
21
|
+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
22
|
+
|
|
23
|
+
// jsdom lacks these APIs used by Radix
|
|
24
|
+
(globalThis as any).ResizeObserver ??= class {
|
|
25
|
+
observe() {}
|
|
26
|
+
unobserve() {}
|
|
27
|
+
disconnect() {}
|
|
28
|
+
};
|
|
29
|
+
Element.prototype.scrollIntoView ??= () => {};
|
|
30
|
+
|
|
31
|
+
const listHooks = vi.hoisted(() => ({
|
|
32
|
+
useForms: vi.fn(),
|
|
33
|
+
useSuspenseForms: vi.fn(),
|
|
34
|
+
useDeleteForm: vi.fn(),
|
|
35
|
+
useSuspenseFormById: vi.fn(),
|
|
36
|
+
useSuspenseFormForUpdate: vi.fn(),
|
|
37
|
+
useSuspenseSubmissions: vi.fn(),
|
|
38
|
+
useSubmission: vi.fn(),
|
|
39
|
+
useDeleteSubmission: vi.fn(),
|
|
40
|
+
useFormBuilderForm: vi.fn(),
|
|
41
|
+
}));
|
|
42
|
+
const rendererHooks = vi.hoisted(() => ({
|
|
43
|
+
useFormBySlug: vi.fn(),
|
|
44
|
+
useSubmitForm: vi.fn(),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock("../client/hooks", () => listHooks);
|
|
48
|
+
vi.mock("../client/hooks/form-builder-hooks", () => rendererHooks);
|
|
49
|
+
|
|
50
|
+
// The auto-form internals are unrelated to class wiring — stub them.
|
|
51
|
+
vi.mock("@workspace/ui/components/auto-form/stepped-auto-form", () => ({
|
|
52
|
+
SteppedAutoForm: ({
|
|
53
|
+
onSubmit,
|
|
54
|
+
}: {
|
|
55
|
+
onSubmit: (values: Record<string, unknown>) => void;
|
|
56
|
+
}) => (
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
data-testid="stepped-auto-form"
|
|
60
|
+
onClick={() => onSubmit({})}
|
|
61
|
+
>
|
|
62
|
+
Submit
|
|
63
|
+
</button>
|
|
64
|
+
),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const CLASS_NAMES = {
|
|
68
|
+
container: "bg-red-100",
|
|
69
|
+
table: "bg-yellow-50",
|
|
70
|
+
tableRow: "text-green-600",
|
|
71
|
+
pagination: "border-4 border-orange-500",
|
|
72
|
+
form: "bg-blue-100",
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
const form: SerializedForm = {
|
|
76
|
+
id: "f1",
|
|
77
|
+
name: "Contact Form",
|
|
78
|
+
slug: "contact-form",
|
|
79
|
+
description: null,
|
|
80
|
+
schema: JSON.stringify({ type: "object", properties: {} }),
|
|
81
|
+
successMessage: null,
|
|
82
|
+
redirectUrl: null,
|
|
83
|
+
status: "active",
|
|
84
|
+
createdBy: null,
|
|
85
|
+
createdAt: new Date("2024-01-01").toISOString(),
|
|
86
|
+
updatedAt: new Date("2024-01-01").toISOString(),
|
|
87
|
+
} as unknown as SerializedForm;
|
|
88
|
+
|
|
89
|
+
const authorization = defineAuthorization({
|
|
90
|
+
identity: z.object({ id: z.string(), role: z.string() }),
|
|
91
|
+
permissions: [formBuilderPermissions] as const,
|
|
92
|
+
rules: ({ forms }) => [
|
|
93
|
+
forms.form.render.allow(),
|
|
94
|
+
forms.submission.create.allow(),
|
|
95
|
+
forms.submission.read.allow(),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
const auth = createClientAuth({ authorization, getIdentity: () => null });
|
|
99
|
+
|
|
100
|
+
let container: HTMLDivElement;
|
|
101
|
+
let root: Root;
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
container = document.createElement("div");
|
|
105
|
+
document.body.appendChild(container);
|
|
106
|
+
root = createRoot(container);
|
|
107
|
+
|
|
108
|
+
listHooks.useSuspenseForms.mockReturnValue({
|
|
109
|
+
forms: [form],
|
|
110
|
+
total: 1,
|
|
111
|
+
loadMore: vi.fn(),
|
|
112
|
+
hasMore: false,
|
|
113
|
+
isLoadingMore: false,
|
|
114
|
+
refetch: vi.fn(),
|
|
115
|
+
});
|
|
116
|
+
listHooks.useForms.mockReturnValue({
|
|
117
|
+
forms: [],
|
|
118
|
+
total: 0,
|
|
119
|
+
isLoading: false,
|
|
120
|
+
error: null,
|
|
121
|
+
loadMore: vi.fn(),
|
|
122
|
+
hasMore: false,
|
|
123
|
+
isLoadingMore: false,
|
|
124
|
+
refetch: vi.fn(),
|
|
125
|
+
});
|
|
126
|
+
listHooks.useDeleteForm.mockReturnValue({
|
|
127
|
+
mutateAsync: vi.fn(),
|
|
128
|
+
isPending: false,
|
|
129
|
+
});
|
|
130
|
+
rendererHooks.useFormBySlug.mockReturnValue({
|
|
131
|
+
form,
|
|
132
|
+
isLoading: false,
|
|
133
|
+
error: null,
|
|
134
|
+
});
|
|
135
|
+
rendererHooks.useSubmitForm.mockReturnValue({
|
|
136
|
+
mutateAsync: vi.fn(),
|
|
137
|
+
isPending: false,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(async () => {
|
|
142
|
+
await act(async () => {
|
|
143
|
+
root.unmount();
|
|
144
|
+
});
|
|
145
|
+
container.remove();
|
|
146
|
+
vi.clearAllMocks();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
function createMockRouter() {
|
|
150
|
+
let params = new URLSearchParams();
|
|
151
|
+
return {
|
|
152
|
+
navigate: vi.fn(),
|
|
153
|
+
getSearchParams: () => new URLSearchParams(params.toString()),
|
|
154
|
+
setSearchParams: vi.fn((next: URLSearchParams) => {
|
|
155
|
+
params = new URLSearchParams(next.toString());
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function render(ui: React.ReactElement, withOverrides = true) {
|
|
161
|
+
await act(async () => {
|
|
162
|
+
root.render(
|
|
163
|
+
<StackProvider
|
|
164
|
+
stack={createTestClientStack({
|
|
165
|
+
formBuilder: formBuilderClientPlugin(),
|
|
166
|
+
})}
|
|
167
|
+
router={createMockRouter()}
|
|
168
|
+
auth={auth}
|
|
169
|
+
initialIdentity={null}
|
|
170
|
+
overrides={
|
|
171
|
+
withOverrides ? { formBuilder: { classNames: CLASS_NAMES } } : {}
|
|
172
|
+
}
|
|
173
|
+
>
|
|
174
|
+
{ui}
|
|
175
|
+
</StackProvider>,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function query(selector: string): HTMLElement {
|
|
181
|
+
const el = container.querySelector<HTMLElement>(selector);
|
|
182
|
+
expect(el, `expected to find ${selector}`).toBeTruthy();
|
|
183
|
+
return el!;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
describe("form-builder classNames overrides (issue #36)", () => {
|
|
187
|
+
it("applies container, table, tableRow and pagination on the forms list", async () => {
|
|
188
|
+
await render(<FormListPage />);
|
|
189
|
+
|
|
190
|
+
const page = query('[data-testid="form-list-page"]');
|
|
191
|
+
expect(page.className).toContain(CLASS_NAMES.container);
|
|
192
|
+
// The override must merge with the layout's own classes, not replace them.
|
|
193
|
+
expect(page.className).toContain("mx-auto");
|
|
194
|
+
|
|
195
|
+
const table = query('[data-testid="form-builder-list-table"]');
|
|
196
|
+
expect(table.className).toContain(CLASS_NAMES.table);
|
|
197
|
+
expect(table.className).toContain("rounded-lg");
|
|
198
|
+
|
|
199
|
+
const row = table.querySelector<HTMLElement>("tbody tr")!;
|
|
200
|
+
expect(row, "expected a body row").toBeTruthy();
|
|
201
|
+
expect(row.className).toContain(CLASS_NAMES.tableRow);
|
|
202
|
+
expect(row.textContent).toContain(form.name);
|
|
203
|
+
|
|
204
|
+
const pagination = query('[data-testid="form-builder-pagination"]');
|
|
205
|
+
expect(pagination.className).toContain("border-4");
|
|
206
|
+
expect(pagination.className).toContain("border-orange-500");
|
|
207
|
+
expect(pagination.className).toContain("justify-between");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("applies table, tableRow and pagination on the submissions page", async () => {
|
|
211
|
+
listHooks.useSuspenseSubmissions.mockReturnValue({
|
|
212
|
+
form,
|
|
213
|
+
submissions: [
|
|
214
|
+
{
|
|
215
|
+
id: "sub-11111111",
|
|
216
|
+
formId: form.id,
|
|
217
|
+
submittedAt: new Date("2024-01-02").toISOString(),
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
total: 1,
|
|
221
|
+
loadMore: vi.fn(),
|
|
222
|
+
hasMore: false,
|
|
223
|
+
isLoadingMore: false,
|
|
224
|
+
refetch: vi.fn(),
|
|
225
|
+
});
|
|
226
|
+
listHooks.useSubmission.mockReturnValue({
|
|
227
|
+
submission: null,
|
|
228
|
+
isLoading: false,
|
|
229
|
+
error: null,
|
|
230
|
+
refetch: vi.fn(),
|
|
231
|
+
});
|
|
232
|
+
listHooks.useDeleteSubmission.mockReturnValue({
|
|
233
|
+
mutateAsync: vi.fn(),
|
|
234
|
+
isPending: false,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await render(<SubmissionsPage formId={form.id} />);
|
|
238
|
+
|
|
239
|
+
const table = query('[data-testid="form-builder-submissions-table"]');
|
|
240
|
+
expect(table.className).toContain(CLASS_NAMES.table);
|
|
241
|
+
expect(table.className).toContain("rounded-lg");
|
|
242
|
+
|
|
243
|
+
const row = table.querySelector<HTMLElement>("tbody tr")!;
|
|
244
|
+
expect(row, "expected a body row").toBeTruthy();
|
|
245
|
+
expect(row.className).toContain(CLASS_NAMES.tableRow);
|
|
246
|
+
|
|
247
|
+
const pagination = query('[data-testid="form-builder-pagination"]');
|
|
248
|
+
expect(pagination.className).toContain("border-orange-500");
|
|
249
|
+
expect(pagination.className).toContain("justify-between");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("applies classNames.form to the rendered public form", async () => {
|
|
253
|
+
await render(<FormRenderer slug={form.slug} />);
|
|
254
|
+
|
|
255
|
+
const root = query('[data-testid="form-renderer"]');
|
|
256
|
+
expect(root.className).toContain(CLASS_NAMES.form);
|
|
257
|
+
// The real auto-form is stubbed, but it must still be mounted inside the slot.
|
|
258
|
+
expect(
|
|
259
|
+
root.querySelector('[data-testid="stepped-auto-form"]'),
|
|
260
|
+
).toBeTruthy();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it.each([
|
|
264
|
+
["loading", { form: null, isLoading: true, error: null }],
|
|
265
|
+
[
|
|
266
|
+
"error",
|
|
267
|
+
{ form: null, isLoading: false, error: new Error("Unavailable") },
|
|
268
|
+
],
|
|
269
|
+
["not found", { form: null, isLoading: false, error: null }],
|
|
270
|
+
[
|
|
271
|
+
"inactive",
|
|
272
|
+
{ form: { ...form, status: "inactive" }, isLoading: false, error: null },
|
|
273
|
+
],
|
|
274
|
+
[
|
|
275
|
+
"invalid schema",
|
|
276
|
+
{ form: { ...form, schema: "invalid" }, isLoading: false, error: null },
|
|
277
|
+
],
|
|
278
|
+
])("preserves form classes in the %s state", async (_state, result) => {
|
|
279
|
+
rendererHooks.useFormBySlug.mockReturnValue(result);
|
|
280
|
+
await render(<FormRenderer slug={form.slug} className="p-6" />);
|
|
281
|
+
const wrapper = container.firstElementChild!;
|
|
282
|
+
expect(wrapper.className).toContain(CLASS_NAMES.form);
|
|
283
|
+
expect(wrapper.className).toContain("p-6");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("preserves form classes after successful submission", async () => {
|
|
287
|
+
const submit = vi
|
|
288
|
+
.fn()
|
|
289
|
+
.mockResolvedValue({ form: { successMessage: "Thank you!" } });
|
|
290
|
+
rendererHooks.useSubmitForm.mockReturnValue({
|
|
291
|
+
mutateAsync: submit,
|
|
292
|
+
isPending: false,
|
|
293
|
+
});
|
|
294
|
+
await render(<FormRenderer slug={form.slug} className="p-6" />);
|
|
295
|
+
await act(async () => {
|
|
296
|
+
query('[data-testid="stepped-auto-form"]').click();
|
|
297
|
+
});
|
|
298
|
+
expect(submit).toHaveBeenCalled();
|
|
299
|
+
expect(container.textContent).toContain("Thank you!");
|
|
300
|
+
const wrapper = container.firstElementChild!;
|
|
301
|
+
expect(wrapper.className).toContain(CLASS_NAMES.form);
|
|
302
|
+
expect(wrapper.className).toContain("p-6");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("leaves base classes untouched when no classNames override is supplied", async () => {
|
|
306
|
+
await render(<FormListPage />, false);
|
|
307
|
+
|
|
308
|
+
const table = query('[data-testid="form-builder-list-table"]');
|
|
309
|
+
expect(table.className).toContain("rounded-lg");
|
|
310
|
+
expect(table.className).not.toContain(CLASS_NAMES.table);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -18,6 +18,7 @@ import type { FormBuilderPluginOverrides } from "../../overrides";
|
|
|
18
18
|
import type { SerializedFormSubmission } from "../../../types";
|
|
19
19
|
import { formBuilderPermissions } from "../../../permissions";
|
|
20
20
|
import { FORM_BUILDER_PLUGIN_ID } from "../../constants";
|
|
21
|
+
import { cn } from "../../../utils";
|
|
21
22
|
|
|
22
23
|
export interface FormRendererProps {
|
|
23
24
|
/** Form slug to render */
|
|
@@ -132,9 +133,13 @@ export function FormRenderer({
|
|
|
132
133
|
className,
|
|
133
134
|
}: FormRendererProps) {
|
|
134
135
|
const t = useTranslate();
|
|
135
|
-
const {
|
|
136
|
-
|
|
136
|
+
const {
|
|
137
|
+
fieldComponents: overrideFieldComponents,
|
|
138
|
+
localization,
|
|
139
|
+
classNames,
|
|
140
|
+
} = usePluginOverrides<FormBuilderPluginOverrides>(FORM_BUILDER_PLUGIN_ID);
|
|
137
141
|
|
|
142
|
+
const formClassName = cn(className, classNames?.form);
|
|
138
143
|
const { form, isLoading, error } = useFormBySlug(slug);
|
|
139
144
|
const submitMutation = useSubmitForm(slug);
|
|
140
145
|
|
|
@@ -201,7 +206,7 @@ export function FormRenderer({
|
|
|
201
206
|
// Loading state
|
|
202
207
|
if (isLoading) {
|
|
203
208
|
return (
|
|
204
|
-
<div className={
|
|
209
|
+
<div className={formClassName}>
|
|
205
210
|
<LoadingComponent />
|
|
206
211
|
</div>
|
|
207
212
|
);
|
|
@@ -210,7 +215,7 @@ export function FormRenderer({
|
|
|
210
215
|
// Error state
|
|
211
216
|
if (error) {
|
|
212
217
|
return (
|
|
213
|
-
<div className={
|
|
218
|
+
<div className={formClassName}>
|
|
214
219
|
<ErrorComponent error={error} />
|
|
215
220
|
</div>
|
|
216
221
|
);
|
|
@@ -219,7 +224,7 @@ export function FormRenderer({
|
|
|
219
224
|
// Form not found
|
|
220
225
|
if (!form) {
|
|
221
226
|
return (
|
|
222
|
-
<div className={
|
|
227
|
+
<div className={formClassName}>
|
|
223
228
|
<ErrorComponent
|
|
224
229
|
error={
|
|
225
230
|
new Error(
|
|
@@ -235,7 +240,7 @@ export function FormRenderer({
|
|
|
235
240
|
// Form not active
|
|
236
241
|
if (form.status !== "active") {
|
|
237
242
|
return (
|
|
238
|
-
<div className={
|
|
243
|
+
<div className={formClassName}>
|
|
239
244
|
<ErrorComponent
|
|
240
245
|
error={
|
|
241
246
|
new Error(
|
|
@@ -254,7 +259,7 @@ export function FormRenderer({
|
|
|
254
259
|
// Schema parsing failed
|
|
255
260
|
if (!zodSchema) {
|
|
256
261
|
return (
|
|
257
|
-
<div className={
|
|
262
|
+
<div className={formClassName}>
|
|
258
263
|
<ErrorComponent
|
|
259
264
|
error={
|
|
260
265
|
new Error(
|
|
@@ -273,7 +278,7 @@ export function FormRenderer({
|
|
|
273
278
|
// Success state
|
|
274
279
|
if (submitted && finalSuccessMessage) {
|
|
275
280
|
return (
|
|
276
|
-
<div className={
|
|
281
|
+
<div className={formClassName}>
|
|
277
282
|
<DefaultSuccessComponent message={finalSuccessMessage} />
|
|
278
283
|
</div>
|
|
279
284
|
);
|
|
@@ -300,7 +305,7 @@ export function FormRenderer({
|
|
|
300
305
|
status: form.status,
|
|
301
306
|
})}
|
|
302
307
|
>
|
|
303
|
-
<div className={
|
|
308
|
+
<div className={formClassName} data-testid="form-renderer">
|
|
304
309
|
<SteppedAutoForm
|
|
305
310
|
formSchema={zodSchema}
|
|
306
311
|
fieldConfig={fieldConfig}
|
|
@@ -52,6 +52,7 @@ import { EmptyState } from "../shared/empty-state";
|
|
|
52
52
|
import { Pagination } from "../shared/pagination";
|
|
53
53
|
import { formBuilderPermissions } from "../../../permissions";
|
|
54
54
|
import { FORM_BUILDER_PLUGIN_ID } from "../../constants";
|
|
55
|
+
import { cn } from "../../../utils";
|
|
55
56
|
|
|
56
57
|
// URL-synced search state: `?q=...` while typing (history: replace), clean
|
|
57
58
|
// URL when the query is empty (the default is omitted from the URL).
|
|
@@ -64,9 +65,8 @@ const SEARCH_DEBOUNCE_MS = 300;
|
|
|
64
65
|
export function FormListPage() {
|
|
65
66
|
const t = useTranslate();
|
|
66
67
|
const notify = useNotify();
|
|
67
|
-
const { localization } =
|
|
68
|
-
FORM_BUILDER_PLUGIN_ID
|
|
69
|
-
);
|
|
68
|
+
const { localization, classNames } =
|
|
69
|
+
usePluginOverrides<FormBuilderPluginOverrides>(FORM_BUILDER_PLUGIN_ID);
|
|
70
70
|
const { Link, navigate, resolve } = usePluginSiteNavigation(
|
|
71
71
|
FORM_BUILDER_PLUGIN_ID,
|
|
72
72
|
);
|
|
@@ -233,7 +233,10 @@ export function FormListPage() {
|
|
|
233
233
|
)
|
|
234
234
|
) : (
|
|
235
235
|
<>
|
|
236
|
-
<div
|
|
236
|
+
<div
|
|
237
|
+
className={cn("rounded-lg border", classNames?.table)}
|
|
238
|
+
data-testid="form-builder-list-table"
|
|
239
|
+
>
|
|
237
240
|
<Table>
|
|
238
241
|
<TableHeader>
|
|
239
242
|
<TableRow>
|
|
@@ -261,7 +264,7 @@ export function FormListPage() {
|
|
|
261
264
|
</TableHeader>
|
|
262
265
|
<TableBody>
|
|
263
266
|
{forms.map((form) => (
|
|
264
|
-
<TableRow key={form.id}>
|
|
267
|
+
<TableRow key={form.id} className={classNames?.tableRow}>
|
|
265
268
|
<TableCell className="font-medium">{form.name}</TableCell>
|
|
266
269
|
<TableCell className="text-muted-foreground font-mono text-sm">
|
|
267
270
|
{form.slug}
|
|
@@ -362,6 +365,7 @@ export function FormListPage() {
|
|
|
362
365
|
hasMore={hasMore}
|
|
363
366
|
isLoadingMore={isLoadingMore}
|
|
364
367
|
onLoadMore={loadMore}
|
|
368
|
+
className={classNames?.pagination}
|
|
365
369
|
labels={{
|
|
366
370
|
showing:
|
|
367
371
|
localization?.FORM_BUILDER_LIST_PAGINATION_SHOWING ??
|
|
@@ -48,6 +48,7 @@ import { EmptyState } from "../shared/empty-state";
|
|
|
48
48
|
import { Pagination } from "../shared/pagination";
|
|
49
49
|
import { formBuilderPermissions } from "../../../permissions";
|
|
50
50
|
import { FORM_BUILDER_PLUGIN_ID } from "../../constants";
|
|
51
|
+
import { cn } from "../../../utils";
|
|
51
52
|
|
|
52
53
|
export interface SubmissionsPageProps {
|
|
53
54
|
formId: string;
|
|
@@ -56,9 +57,8 @@ export interface SubmissionsPageProps {
|
|
|
56
57
|
export function SubmissionsPage({ formId }: SubmissionsPageProps) {
|
|
57
58
|
const t = useTranslate();
|
|
58
59
|
const notify = useNotify();
|
|
59
|
-
const { localization } =
|
|
60
|
-
FORM_BUILDER_PLUGIN_ID
|
|
61
|
-
);
|
|
60
|
+
const { localization, classNames } =
|
|
61
|
+
usePluginOverrides<FormBuilderPluginOverrides>(FORM_BUILDER_PLUGIN_ID);
|
|
62
62
|
const { identity } = useIdentity();
|
|
63
63
|
const { Link: LinkComponent, resolve } = usePluginSiteNavigation(
|
|
64
64
|
FORM_BUILDER_PLUGIN_ID,
|
|
@@ -143,7 +143,10 @@ export function SubmissionsPage({ formId }: SubmissionsPageProps) {
|
|
|
143
143
|
/>
|
|
144
144
|
) : (
|
|
145
145
|
<>
|
|
146
|
-
<div
|
|
146
|
+
<div
|
|
147
|
+
className={cn("rounded-lg border", classNames?.table)}
|
|
148
|
+
data-testid="form-builder-submissions-table"
|
|
149
|
+
>
|
|
147
150
|
<Table>
|
|
148
151
|
<TableHeader>
|
|
149
152
|
<TableRow>
|
|
@@ -166,7 +169,7 @@ export function SubmissionsPage({ formId }: SubmissionsPageProps) {
|
|
|
166
169
|
</TableHeader>
|
|
167
170
|
<TableBody>
|
|
168
171
|
{submissions.map((sub) => (
|
|
169
|
-
<TableRow key={sub.id}>
|
|
172
|
+
<TableRow key={sub.id} className={classNames?.tableRow}>
|
|
170
173
|
<TableCell className="font-mono text-xs">
|
|
171
174
|
{sub.id.slice(0, 8)}...
|
|
172
175
|
</TableCell>
|
|
@@ -249,6 +252,7 @@ export function SubmissionsPage({ formId }: SubmissionsPageProps) {
|
|
|
249
252
|
hasMore={hasMore}
|
|
250
253
|
isLoadingMore={isLoadingMore}
|
|
251
254
|
onLoadMore={loadMore}
|
|
255
|
+
className={classNames?.pagination}
|
|
252
256
|
labels={{
|
|
253
257
|
showing:
|
|
254
258
|
localization?.FORM_BUILDER_LIST_PAGINATION_SHOWING ??
|
|
@@ -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 { FormBuilderPluginOverrides } from "../../overrides";
|
|
6
6
|
import { FORM_BUILDER_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
|
FormBuilderPluginOverrides,
|
|
19
20
|
Partial<FormBuilderPluginOverrides>
|
|
20
21
|
>(FORM_BUILDER_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
|
>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Button } from "@workspace/ui/components/button";
|
|
4
4
|
import { ChevronRight } from "lucide-react";
|
|
5
|
+
import { cn } from "../../../utils";
|
|
5
6
|
|
|
6
7
|
interface PaginationProps {
|
|
7
8
|
total: number;
|
|
@@ -9,6 +10,7 @@ interface PaginationProps {
|
|
|
9
10
|
hasMore: boolean;
|
|
10
11
|
isLoadingMore: boolean;
|
|
11
12
|
onLoadMore: () => void;
|
|
13
|
+
className?: string;
|
|
12
14
|
labels?: {
|
|
13
15
|
showing?: string;
|
|
14
16
|
previous?: string;
|
|
@@ -23,6 +25,7 @@ export function Pagination({
|
|
|
23
25
|
hasMore,
|
|
24
26
|
isLoadingMore,
|
|
25
27
|
onLoadMore,
|
|
28
|
+
className,
|
|
26
29
|
labels = {},
|
|
27
30
|
}: PaginationProps) {
|
|
28
31
|
const {
|
|
@@ -36,7 +39,10 @@ export function Pagination({
|
|
|
36
39
|
.replace("{total}", String(total));
|
|
37
40
|
|
|
38
41
|
return (
|
|
39
|
-
<div
|
|
42
|
+
<div
|
|
43
|
+
className={cn("flex items-center justify-between py-4", className)}
|
|
44
|
+
data-testid="form-builder-pagination"
|
|
45
|
+
>
|
|
40
46
|
<p className="text-sm text-muted-foreground">{showingText}</p>
|
|
41
47
|
{hasMore && (
|
|
42
48
|
<Button
|
|
@@ -51,6 +51,23 @@ export interface FormBuilderPluginOverrides {
|
|
|
51
51
|
*/
|
|
52
52
|
fieldComponents?: Record<string, ComponentType<AutoFormInputComponentProps>>;
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Optional class names merged onto specific UI elements.
|
|
56
|
+
* Use these for targeted styling without replacing whole components.
|
|
57
|
+
*/
|
|
58
|
+
classNames?: {
|
|
59
|
+
/** Page container shared by every Form Builder page */
|
|
60
|
+
container?: string;
|
|
61
|
+
/** Wrapper around the forms or submissions table */
|
|
62
|
+
table?: string;
|
|
63
|
+
/** Each body row in the forms or submissions table */
|
|
64
|
+
tableRow?: string;
|
|
65
|
+
/** Pagination footer under the forms or submissions list */
|
|
66
|
+
pagination?: string;
|
|
67
|
+
/** Root of `FormRenderer`, including loading, error, and success states */
|
|
68
|
+
form?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
54
71
|
/**
|
|
55
72
|
* Localization object for the Form Builder plugin
|
|
56
73
|
*/
|
|
@@ -127,11 +127,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
|
|
|
127
127
|
readonly status: Form["status"];
|
|
128
128
|
readonly createdAt: string;
|
|
129
129
|
readonly updatedAt: string;
|
|
130
|
-
readonly schema: string;
|
|
131
130
|
readonly id: string;
|
|
131
|
+
readonly name: string;
|
|
132
|
+
readonly schema: string;
|
|
132
133
|
readonly description?: string | undefined;
|
|
133
134
|
readonly slug: string;
|
|
134
|
-
readonly name: string;
|
|
135
135
|
readonly successMessage?: string | undefined;
|
|
136
136
|
readonly redirectUrl?: string | undefined;
|
|
137
137
|
readonly createdBy?: string | undefined;
|
|
@@ -151,11 +151,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
|
|
|
151
151
|
readonly status: Form["status"];
|
|
152
152
|
readonly createdAt: string;
|
|
153
153
|
readonly updatedAt: string;
|
|
154
|
-
readonly schema: string;
|
|
155
154
|
readonly id: string;
|
|
155
|
+
readonly name: string;
|
|
156
|
+
readonly schema: string;
|
|
156
157
|
readonly description?: string | undefined;
|
|
157
158
|
readonly slug: string;
|
|
158
|
-
readonly name: string;
|
|
159
159
|
readonly successMessage?: string | undefined;
|
|
160
160
|
readonly redirectUrl?: string | undefined;
|
|
161
161
|
readonly createdBy?: string | undefined;
|
|
@@ -171,11 +171,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
|
|
|
171
171
|
readonly status: Form["status"];
|
|
172
172
|
readonly createdAt: string;
|
|
173
173
|
readonly updatedAt: string;
|
|
174
|
-
readonly schema: string;
|
|
175
174
|
readonly id: string;
|
|
175
|
+
readonly name: string;
|
|
176
|
+
readonly schema: string;
|
|
176
177
|
readonly description?: string | undefined;
|
|
177
178
|
readonly slug: string;
|
|
178
|
-
readonly name: string;
|
|
179
179
|
readonly successMessage?: string | undefined;
|
|
180
180
|
readonly redirectUrl?: string | undefined;
|
|
181
181
|
readonly createdBy?: string | undefined;
|
|
@@ -191,11 +191,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
|
|
|
191
191
|
readonly status: Form["status"];
|
|
192
192
|
readonly createdAt: string;
|
|
193
193
|
readonly updatedAt: string;
|
|
194
|
-
readonly schema: string;
|
|
195
194
|
readonly id: string;
|
|
195
|
+
readonly name: string;
|
|
196
|
+
readonly schema: string;
|
|
196
197
|
readonly description?: string | undefined;
|
|
197
198
|
readonly slug: string;
|
|
198
|
-
readonly name: string;
|
|
199
199
|
readonly successMessage?: string | undefined;
|
|
200
200
|
readonly redirectUrl?: string | undefined;
|
|
201
201
|
readonly createdBy?: string | undefined;
|
|
@@ -225,11 +225,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
|
|
|
225
225
|
readonly status: Form["status"];
|
|
226
226
|
readonly createdAt: string;
|
|
227
227
|
readonly updatedAt: string;
|
|
228
|
-
readonly schema: string;
|
|
229
228
|
readonly id: string;
|
|
229
|
+
readonly name: string;
|
|
230
|
+
readonly schema: string;
|
|
230
231
|
readonly description?: string | undefined;
|
|
231
232
|
readonly slug: string;
|
|
232
|
-
readonly name: string;
|
|
233
233
|
readonly successMessage?: string | undefined;
|
|
234
234
|
readonly redirectUrl?: string | undefined;
|
|
235
235
|
readonly createdBy?: string | undefined;
|
|
@@ -259,11 +259,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
|
|
|
259
259
|
readonly status: Form["status"];
|
|
260
260
|
readonly createdAt: string;
|
|
261
261
|
readonly updatedAt: string;
|
|
262
|
-
readonly schema: string;
|
|
263
262
|
readonly id: string;
|
|
263
|
+
readonly name: string;
|
|
264
|
+
readonly schema: string;
|
|
264
265
|
readonly description?: string | undefined;
|
|
265
266
|
readonly slug: string;
|
|
266
|
-
readonly name: string;
|
|
267
267
|
readonly successMessage?: string | undefined;
|
|
268
268
|
readonly redirectUrl?: string | undefined;
|
|
269
269
|
readonly createdBy?: string | undefined;
|