@btst/stack 1.5.2 → 1.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/dist/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.2.0/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.cjs +68 -0
- package/dist/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.2.0/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.mjs +60 -0
- package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.cjs +3937 -0
- package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.mjs +3907 -0
- package/dist/node_modules/.pnpm/@dnd-kit_modifiers@9.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/modifiers/dist/modifiers.esm.cjs +30 -0
- package/dist/node_modules/.pnpm/@dnd-kit_modifiers@9.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/modifiers/dist/modifiers.esm.mjs +28 -0
- package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.cjs +675 -0
- package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.mjs +661 -0
- package/dist/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.2.0/node_modules/@dnd-kit/utilities/dist/utilities.esm.cjs +358 -0
- package/dist/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.2.0/node_modules/@dnd-kit/utilities/dist/utilities.esm.mjs +332 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-tabs@1.1.13_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_865f042350eb43f3338b0fffb33f6246/node_modules/@radix-ui/react-tabs/dist/index.cjs +211 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-tabs@1.1.13_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_865f042350eb43f3338b0fffb33f6246/node_modules/@radix-ui/react-tabs/dist/index.mjs +188 -0
- package/dist/packages/better-stack/src/plugins/cms/api/plugin.cjs +3 -2
- package/dist/packages/better-stack/src/plugins/cms/api/plugin.mjs +3 -2
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.cjs +15 -15
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.mjs +16 -16
- package/dist/packages/better-stack/src/plugins/form-builder/api/plugin.cjs +588 -0
- package/dist/packages/better-stack/src/plugins/form-builder/api/plugin.mjs +586 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.cjs +131 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.mjs +129 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-builder-skeleton.cjs +32 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-builder-skeleton.mjs +30 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-list-skeleton.cjs +21 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-list-skeleton.mjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/submissions-skeleton.cjs +34 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/submissions-skeleton.mjs +32 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/404-page.cjs +20 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/404-page.mjs +18 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.internal.cjs +186 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.internal.mjs +184 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.cjs +165 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.mjs +163 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +177 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +175 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/default-error.cjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/default-error.mjs +15 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/empty-state.cjs +16 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/empty-state.mjs +14 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/page-wrapper.cjs +27 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/page-wrapper.mjs +25 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/pagination.cjs +39 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/pagination.mjs +37 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/hooks/form-builder-hooks.cjs +551 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/hooks/form-builder-hooks.mjs +537 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-common.cjs +36 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-common.mjs +34 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-editor.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-editor.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-list.cjs +21 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-list.mjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-submissions.cjs +19 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-submissions.mjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-toasts.cjs +14 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-toasts.mjs +12 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/index.cjs +17 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/localization/index.mjs +15 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/plugin.cjs +278 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/plugin.mjs +276 -0
- package/dist/packages/better-stack/src/plugins/form-builder/db.cjs +99 -0
- package/dist/packages/better-stack/src/plugins/form-builder/db.mjs +97 -0
- package/dist/packages/better-stack/src/plugins/form-builder/schemas.cjs +82 -0
- package/dist/packages/better-stack/src/plugins/form-builder/schemas.mjs +74 -0
- package/dist/packages/better-stack/src/plugins/form-builder/utils.cjs +37 -0
- package/dist/packages/better-stack/src/plugins/form-builder/utils.mjs +29 -0
- package/dist/packages/ui/src/components/auto-form/index.cjs +2 -12
- package/dist/packages/ui/src/components/auto-form/index.mjs +2 -9
- package/dist/packages/ui/src/components/auto-form/stepped-auto-form.cjs +377 -0
- package/dist/packages/ui/src/components/auto-form/stepped-auto-form.mjs +368 -0
- package/dist/packages/ui/src/components/auto-form/utils.cjs +1 -56
- package/dist/packages/ui/src/components/auto-form/utils.mjs +2 -56
- package/dist/packages/ui/src/components/form-builder/canvas.cjs +111 -0
- package/dist/packages/ui/src/components/form-builder/canvas.mjs +109 -0
- package/dist/packages/ui/src/components/form-builder/components/index.cjs +570 -0
- package/dist/packages/ui/src/components/form-builder/components/index.mjs +553 -0
- package/dist/packages/ui/src/components/form-builder/edit-field-dialog.cjs +131 -0
- package/dist/packages/ui/src/components/form-builder/edit-field-dialog.mjs +129 -0
- package/dist/packages/ui/src/components/form-builder/form-preview.cjs +73 -0
- package/dist/packages/ui/src/components/form-builder/form-preview.mjs +71 -0
- package/dist/packages/ui/src/components/form-builder/index.cjs +353 -0
- package/dist/packages/ui/src/components/form-builder/index.mjs +344 -0
- package/dist/packages/ui/src/components/form-builder/nested-field-editor-dialog.cjs +263 -0
- package/dist/packages/ui/src/components/form-builder/nested-field-editor-dialog.mjs +261 -0
- package/dist/packages/ui/src/components/form-builder/palette.cjs +52 -0
- package/dist/packages/ui/src/components/form-builder/palette.mjs +49 -0
- package/dist/packages/ui/src/components/form-builder/schema-utils.cjs +120 -0
- package/dist/packages/ui/src/components/form-builder/schema-utils.mjs +114 -0
- package/dist/packages/ui/src/components/form-builder/sortable-field.cjs +151 -0
- package/dist/packages/ui/src/components/form-builder/sortable-field.mjs +148 -0
- package/dist/packages/ui/src/components/form-builder/step-tabs.cjs +180 -0
- package/dist/packages/ui/src/components/form-builder/step-tabs.mjs +178 -0
- package/dist/packages/ui/src/components/form-builder/types.cjs +7 -0
- package/dist/packages/ui/src/components/form-builder/types.mjs +5 -0
- package/dist/packages/ui/src/components/form-builder/validation-schemas.cjs +67 -0
- package/dist/packages/ui/src/components/form-builder/validation-schemas.mjs +56 -0
- package/dist/packages/ui/src/components/tabs.cjs +70 -0
- package/dist/packages/ui/src/components/tabs.mjs +65 -0
- package/dist/packages/ui/src/lib/schema-converter.cjs +130 -0
- package/dist/packages/ui/src/lib/schema-converter.mjs +124 -0
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- 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 +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- 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/client/index.cjs +6 -0
- package/dist/plugins/cms/client/index.d.cts +6 -113
- package/dist/plugins/cms/client/index.d.mts +6 -113
- package/dist/plugins/cms/client/index.d.ts +6 -113
- package/dist/plugins/cms/client/index.mjs +1 -0
- package/dist/plugins/form-builder/api/index.cjs +7 -0
- package/dist/plugins/form-builder/api/index.d.cts +141 -0
- package/dist/plugins/form-builder/api/index.d.mts +141 -0
- package/dist/plugins/form-builder/api/index.d.ts +141 -0
- package/dist/plugins/form-builder/api/index.mjs +1 -0
- package/dist/plugins/form-builder/client/components/index.cjs +29 -0
- package/dist/plugins/form-builder/client/components/index.d.cts +93 -0
- package/dist/plugins/form-builder/client/components/index.d.mts +93 -0
- package/dist/plugins/form-builder/client/components/index.d.ts +93 -0
- package/dist/plugins/form-builder/client/components/index.mjs +18 -0
- package/dist/plugins/form-builder/client/hooks/index.cjs +19 -0
- package/dist/plugins/form-builder/client/hooks/index.d.cts +154 -0
- package/dist/plugins/form-builder/client/hooks/index.d.mts +154 -0
- package/dist/plugins/form-builder/client/hooks/index.d.ts +154 -0
- package/dist/plugins/form-builder/client/hooks/index.mjs +1 -0
- package/dist/plugins/form-builder/client/index.cjs +13 -0
- package/dist/plugins/form-builder/client/index.d.cts +381 -0
- package/dist/plugins/form-builder/client/index.d.mts +381 -0
- package/dist/plugins/form-builder/client/index.d.ts +381 -0
- package/dist/plugins/form-builder/client/index.mjs +2 -0
- package/dist/plugins/form-builder/client.css +3 -0
- package/dist/plugins/form-builder/query-keys.cjs +143 -0
- package/dist/plugins/form-builder/query-keys.d.cts +74 -0
- package/dist/plugins/form-builder/query-keys.d.mts +74 -0
- package/dist/plugins/form-builder/query-keys.d.ts +74 -0
- package/dist/plugins/form-builder/query-keys.mjs +141 -0
- package/dist/plugins/form-builder/style.css +19 -0
- package/dist/shared/stack.AX5nZ6A3.d.cts +86 -0
- package/dist/shared/stack.AX5nZ6A3.d.mts +86 -0
- package/dist/shared/stack.AX5nZ6A3.d.ts +86 -0
- package/dist/shared/stack.BIh2AXaW.d.cts +123 -0
- package/dist/shared/stack.BIh2AXaW.d.mts +123 -0
- package/dist/shared/stack.BIh2AXaW.d.ts +123 -0
- package/dist/shared/stack.DzH_wcvr.d.cts +195 -0
- package/dist/shared/stack.DzH_wcvr.d.mts +195 -0
- package/dist/shared/stack.DzH_wcvr.d.ts +195 -0
- package/package.json +54 -1
- package/src/plugins/cms/api/plugin.ts +9 -4
- package/src/plugins/cms/client/components/forms/content-form.tsx +23 -25
- package/src/plugins/cms/client/index.ts +11 -0
- package/src/plugins/form-builder/api/index.ts +1 -0
- package/src/plugins/form-builder/api/plugin.ts +776 -0
- package/src/plugins/form-builder/client/components/forms/form-renderer.tsx +253 -0
- package/src/plugins/form-builder/client/components/index.tsx +24 -0
- package/src/plugins/form-builder/client/components/loading/form-builder-skeleton.tsx +42 -0
- package/src/plugins/form-builder/client/components/loading/form-list-skeleton.tsx +25 -0
- package/src/plugins/form-builder/client/components/loading/index.tsx +3 -0
- package/src/plugins/form-builder/client/components/loading/submissions-skeleton.tsx +40 -0
- package/src/plugins/form-builder/client/components/pages/404-page.tsx +28 -0
- package/src/plugins/form-builder/client/components/pages/form-builder-page.internal.tsx +253 -0
- package/src/plugins/form-builder/client/components/pages/form-builder-page.tsx +26 -0
- package/src/plugins/form-builder/client/components/pages/form-list-page.internal.tsx +231 -0
- package/src/plugins/form-builder/client/components/pages/form-list-page.tsx +22 -0
- package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +268 -0
- package/src/plugins/form-builder/client/components/pages/submissions-page.tsx +26 -0
- package/src/plugins/form-builder/client/components/shared/default-error.tsx +30 -0
- package/src/plugins/form-builder/client/components/shared/empty-state.tsx +26 -0
- package/src/plugins/form-builder/client/components/shared/page-wrapper.tsx +32 -0
- package/src/plugins/form-builder/client/components/shared/pagination.tsx +52 -0
- package/src/plugins/form-builder/client/hooks/form-builder-hooks.tsx +799 -0
- package/src/plugins/form-builder/client/hooks/index.tsx +1 -0
- package/src/plugins/form-builder/client/index.ts +22 -0
- package/src/plugins/form-builder/client/localization/form-builder-common.ts +36 -0
- package/src/plugins/form-builder/client/localization/form-builder-editor.ts +18 -0
- package/src/plugins/form-builder/client/localization/form-builder-list.ts +17 -0
- package/src/plugins/form-builder/client/localization/form-builder-submissions.ts +17 -0
- package/src/plugins/form-builder/client/localization/form-builder-toasts.ts +10 -0
- package/src/plugins/form-builder/client/localization/index.ts +15 -0
- package/src/plugins/form-builder/client/overrides.ts +146 -0
- package/src/plugins/form-builder/client/plugin.tsx +488 -0
- package/src/plugins/form-builder/client.css +3 -0
- package/src/plugins/form-builder/db.ts +99 -0
- package/src/plugins/form-builder/query-keys.ts +198 -0
- package/src/plugins/form-builder/schemas.ts +122 -0
- package/src/plugins/form-builder/style.css +19 -0
- package/src/plugins/form-builder/types.ts +317 -0
- package/src/plugins/form-builder/utils.ts +63 -0
- package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.cts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.mts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.ts} +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mergeQueryKeys,
|
|
3
|
+
createQueryKeys,
|
|
4
|
+
} from "@lukemorales/query-key-factory";
|
|
5
|
+
import type { FormBuilderApiRouter } from "./api";
|
|
6
|
+
import { createApiClient } from "@btst/stack/plugins/client";
|
|
7
|
+
import type {
|
|
8
|
+
SerializedForm,
|
|
9
|
+
PaginatedForms,
|
|
10
|
+
PaginatedFormSubmissions,
|
|
11
|
+
SerializedFormSubmissionWithData,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
interface FormListParams {
|
|
15
|
+
status?: "active" | "inactive" | "archived";
|
|
16
|
+
limit?: number;
|
|
17
|
+
offset?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SubmissionListParams {
|
|
21
|
+
formId: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
offset?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Type guard for better-call error responses
|
|
27
|
+
function isErrorResponse(
|
|
28
|
+
response: unknown,
|
|
29
|
+
): response is { error: unknown; data?: never } {
|
|
30
|
+
if (typeof response !== "object" || response === null) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const obj = response as Record<string, unknown>;
|
|
34
|
+
return "error" in obj && obj.error !== null && obj.error !== undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Helper to convert error to a proper Error object with meaningful message
|
|
38
|
+
function toError(error: unknown): Error {
|
|
39
|
+
if (error instanceof Error) {
|
|
40
|
+
return error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof error === "object" && error !== null) {
|
|
44
|
+
const errorObj = error as Record<string, unknown>;
|
|
45
|
+
const message =
|
|
46
|
+
(typeof errorObj.message === "string" ? errorObj.message : null) ||
|
|
47
|
+
(typeof errorObj.error === "string" ? errorObj.error : null) ||
|
|
48
|
+
JSON.stringify(error);
|
|
49
|
+
|
|
50
|
+
const err = new Error(message);
|
|
51
|
+
Object.assign(err, error);
|
|
52
|
+
return err;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new Error(String(error));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create Form Builder query keys for React Query
|
|
60
|
+
* Used by consumers to fetch forms and submissions
|
|
61
|
+
*/
|
|
62
|
+
export function createFormBuilderQueryKeys(
|
|
63
|
+
client: ReturnType<typeof createApiClient<FormBuilderApiRouter>>,
|
|
64
|
+
headers?: HeadersInit,
|
|
65
|
+
) {
|
|
66
|
+
const forms = createFormsQueries(client, headers);
|
|
67
|
+
const submissions = createSubmissionsQueries(client, headers);
|
|
68
|
+
|
|
69
|
+
return mergeQueryKeys(forms, submissions);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createFormsQueries(
|
|
73
|
+
client: ReturnType<typeof createApiClient<FormBuilderApiRouter>>,
|
|
74
|
+
headers?: HeadersInit,
|
|
75
|
+
) {
|
|
76
|
+
return createQueryKeys("forms", {
|
|
77
|
+
list: (params: FormListParams = {}) => ({
|
|
78
|
+
queryKey: ["list", params],
|
|
79
|
+
queryFn: async () => {
|
|
80
|
+
try {
|
|
81
|
+
const response: unknown = await client("/forms", {
|
|
82
|
+
method: "GET",
|
|
83
|
+
query: {
|
|
84
|
+
status: params.status,
|
|
85
|
+
limit: params.limit ?? 20,
|
|
86
|
+
offset: params.offset ?? 0,
|
|
87
|
+
},
|
|
88
|
+
headers,
|
|
89
|
+
});
|
|
90
|
+
if (isErrorResponse(response)) {
|
|
91
|
+
throw toError(response.error);
|
|
92
|
+
}
|
|
93
|
+
return (response as { data?: unknown }).data as PaginatedForms;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
bySlug: (slug: string) => ({
|
|
101
|
+
queryKey: ["bySlug", slug],
|
|
102
|
+
queryFn: async () => {
|
|
103
|
+
if (!slug) return null;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const response: unknown = await client("/forms/:slug", {
|
|
107
|
+
method: "GET",
|
|
108
|
+
params: { slug },
|
|
109
|
+
headers,
|
|
110
|
+
});
|
|
111
|
+
if (isErrorResponse(response)) {
|
|
112
|
+
throw toError(response.error);
|
|
113
|
+
}
|
|
114
|
+
return (response as { data?: unknown }).data as SerializedForm | null;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
byId: (id: string) => ({
|
|
122
|
+
queryKey: ["byId", id],
|
|
123
|
+
queryFn: async () => {
|
|
124
|
+
if (!id) return null;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const response: unknown = await client("/forms/id/:id", {
|
|
128
|
+
method: "GET",
|
|
129
|
+
params: { id },
|
|
130
|
+
headers,
|
|
131
|
+
});
|
|
132
|
+
if (isErrorResponse(response)) {
|
|
133
|
+
throw toError(response.error);
|
|
134
|
+
}
|
|
135
|
+
return (response as { data?: unknown }).data as SerializedForm | null;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createSubmissionsQueries(
|
|
145
|
+
client: ReturnType<typeof createApiClient<FormBuilderApiRouter>>,
|
|
146
|
+
headers?: HeadersInit,
|
|
147
|
+
) {
|
|
148
|
+
return createQueryKeys("formSubmissions", {
|
|
149
|
+
list: (params: SubmissionListParams) => ({
|
|
150
|
+
queryKey: [params],
|
|
151
|
+
queryFn: async () => {
|
|
152
|
+
try {
|
|
153
|
+
const response: unknown = await client("/forms/:formId/submissions", {
|
|
154
|
+
method: "GET",
|
|
155
|
+
params: { formId: params.formId },
|
|
156
|
+
query: {
|
|
157
|
+
limit: params.limit ?? 20,
|
|
158
|
+
offset: params.offset ?? 0,
|
|
159
|
+
},
|
|
160
|
+
headers,
|
|
161
|
+
});
|
|
162
|
+
if (isErrorResponse(response)) {
|
|
163
|
+
throw toError(response.error);
|
|
164
|
+
}
|
|
165
|
+
return (response as { data?: unknown })
|
|
166
|
+
.data as PaginatedFormSubmissions;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
|
|
173
|
+
detail: (formId: string, subId: string) => ({
|
|
174
|
+
queryKey: [formId, subId],
|
|
175
|
+
queryFn: async () => {
|
|
176
|
+
if (!formId || !subId) return null;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const response: unknown = await client(
|
|
180
|
+
"/forms/:formId/submissions/:subId",
|
|
181
|
+
{
|
|
182
|
+
method: "GET",
|
|
183
|
+
params: { formId, subId },
|
|
184
|
+
headers,
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
if (isErrorResponse(response)) {
|
|
188
|
+
throw toError(response.error);
|
|
189
|
+
}
|
|
190
|
+
return (response as { data?: unknown })
|
|
191
|
+
.data as SerializedFormSubmissionWithData | null;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema for listing forms with pagination
|
|
5
|
+
*/
|
|
6
|
+
export const listFormsQuerySchema = z.object({
|
|
7
|
+
status: z.enum(["active", "inactive", "archived"]).optional(),
|
|
8
|
+
limit: z.coerce.number().min(1).max(100).optional().default(20),
|
|
9
|
+
offset: z.coerce.number().min(0).optional().default(0),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Schema for creating a form
|
|
14
|
+
*/
|
|
15
|
+
export const createFormSchema = z.object({
|
|
16
|
+
name: z.string().min(1, "Name is required"),
|
|
17
|
+
slug: z.string().min(1, "Slug is required"),
|
|
18
|
+
description: z.string().optional(),
|
|
19
|
+
schema: z.string().min(1, "Schema is required"),
|
|
20
|
+
successMessage: z.string().optional(),
|
|
21
|
+
redirectUrl: z.string().url().optional().or(z.literal("")),
|
|
22
|
+
status: z
|
|
23
|
+
.enum(["active", "inactive", "archived"])
|
|
24
|
+
.optional()
|
|
25
|
+
.default("active"),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Schema for updating a form
|
|
30
|
+
*/
|
|
31
|
+
export const updateFormSchema = z.object({
|
|
32
|
+
name: z.string().min(1, "Name is required").optional(),
|
|
33
|
+
slug: z.string().min(1, "Slug is required").optional(),
|
|
34
|
+
description: z.string().optional(),
|
|
35
|
+
schema: z.string().min(1, "Schema is required").optional(),
|
|
36
|
+
successMessage: z.string().optional(),
|
|
37
|
+
redirectUrl: z.string().url().optional().or(z.literal("")),
|
|
38
|
+
status: z.enum(["active", "inactive", "archived"]).optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Schema for form response
|
|
43
|
+
*/
|
|
44
|
+
export const formResponseSchema = z.object({
|
|
45
|
+
id: z.string(),
|
|
46
|
+
name: z.string(),
|
|
47
|
+
slug: z.string(),
|
|
48
|
+
description: z.string().nullable().optional(),
|
|
49
|
+
schema: z.string(),
|
|
50
|
+
successMessage: z.string().nullable().optional(),
|
|
51
|
+
redirectUrl: z.string().nullable().optional(),
|
|
52
|
+
status: z.string(),
|
|
53
|
+
createdBy: z.string().nullable().optional(),
|
|
54
|
+
createdAt: z.string(),
|
|
55
|
+
updatedAt: z.string(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Schema for paginated forms response
|
|
60
|
+
*/
|
|
61
|
+
export const paginatedFormsResponseSchema = z.object({
|
|
62
|
+
items: z.array(formResponseSchema),
|
|
63
|
+
total: z.number(),
|
|
64
|
+
limit: z.number(),
|
|
65
|
+
offset: z.number(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Schema for listing form submissions with pagination
|
|
70
|
+
*/
|
|
71
|
+
export const listSubmissionsQuerySchema = z.object({
|
|
72
|
+
limit: z.coerce.number().min(1).max(100).optional().default(20),
|
|
73
|
+
offset: z.coerce.number().min(0).optional().default(0),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Schema for submitting a form (public)
|
|
78
|
+
*/
|
|
79
|
+
export const submitFormSchema = z.object({
|
|
80
|
+
// Use passthrough object for dynamic form data validation
|
|
81
|
+
data: z.object({}).passthrough(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Schema for form submission response
|
|
86
|
+
*/
|
|
87
|
+
export const formSubmissionResponseSchema = z.object({
|
|
88
|
+
id: z.string(),
|
|
89
|
+
formId: z.string(),
|
|
90
|
+
data: z.string(),
|
|
91
|
+
submittedAt: z.string(),
|
|
92
|
+
submittedBy: z.string().nullable().optional(),
|
|
93
|
+
ipAddress: z.string().nullable().optional(),
|
|
94
|
+
userAgent: z.string().nullable().optional(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Schema for form submission with parsed data response
|
|
99
|
+
*/
|
|
100
|
+
export const formSubmissionWithDataResponseSchema =
|
|
101
|
+
formSubmissionResponseSchema.extend({
|
|
102
|
+
// Use passthrough object for dynamic parsed data
|
|
103
|
+
parsedData: z.object({}).passthrough(),
|
|
104
|
+
form: formResponseSchema.optional(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Schema for paginated submissions response
|
|
109
|
+
*/
|
|
110
|
+
export const paginatedSubmissionsResponseSchema = z.object({
|
|
111
|
+
items: z.array(formSubmissionWithDataResponseSchema),
|
|
112
|
+
total: z.number(),
|
|
113
|
+
limit: z.number(),
|
|
114
|
+
offset: z.number(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Export inferred types
|
|
118
|
+
export type ListFormsQuery = z.infer<typeof listFormsQuerySchema>;
|
|
119
|
+
export type CreateFormInput = z.infer<typeof createFormSchema>;
|
|
120
|
+
export type UpdateFormInput = z.infer<typeof updateFormSchema>;
|
|
121
|
+
export type ListSubmissionsQuery = z.infer<typeof listSubmissionsQuerySchema>;
|
|
122
|
+
export type SubmitFormInput = z.infer<typeof submitFormSchema>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@import "./client.css";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Form Builder Plugin CSS - Includes Tailwind class scanning
|
|
5
|
+
*
|
|
6
|
+
* When consumed from npm, Tailwind v4 will automatically scan this package's
|
|
7
|
+
* source files for Tailwind classes. Consumers only need:
|
|
8
|
+
* @import "@btst/stack/plugins/form-builder/css";
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* Scan this package's source files for Tailwind classes */
|
|
12
|
+
@source "../../../src/**/*.{ts,tsx}";
|
|
13
|
+
|
|
14
|
+
/* Scan UI package components (when installed as npm package the UI package will be in this dir) */
|
|
15
|
+
@source "../../packages/ui/src";
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* alternatively consumer can use @source "../node_modules/@btst/stack/src/**\/*.{ts,tsx}";
|
|
19
|
+
*/
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Builder Plugin Types
|
|
3
|
+
*
|
|
4
|
+
* Key distinction from CMS Plugin:
|
|
5
|
+
* - CMS uses developer-defined Zod schemas in code
|
|
6
|
+
* - Form Builder allows non-technical admins to build forms via drag-and-drop UI
|
|
7
|
+
* - Forms are serialized to/from JSON Schema for storage
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Form stored in the database
|
|
12
|
+
*/
|
|
13
|
+
export type Form = {
|
|
14
|
+
id: string;
|
|
15
|
+
/** Display name for the form */
|
|
16
|
+
name: string;
|
|
17
|
+
/** URL-friendly slug - unique identifier for public access */
|
|
18
|
+
slug: string;
|
|
19
|
+
/** Optional description for admin UI */
|
|
20
|
+
description?: string;
|
|
21
|
+
/** JSON Schema stored as string (includes steps, fieldType, stepGroup, etc.) */
|
|
22
|
+
schema: string;
|
|
23
|
+
/** Optional custom success message after submission */
|
|
24
|
+
successMessage?: string;
|
|
25
|
+
/** Optional redirect URL after submission */
|
|
26
|
+
redirectUrl?: string;
|
|
27
|
+
/** Form status: active, inactive, archived */
|
|
28
|
+
status: "active" | "inactive" | "archived";
|
|
29
|
+
/** User who created the form */
|
|
30
|
+
createdBy?: string;
|
|
31
|
+
createdAt: Date;
|
|
32
|
+
updatedAt: Date;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Form submission stored in the database
|
|
37
|
+
*/
|
|
38
|
+
export type FormSubmission = {
|
|
39
|
+
id: string;
|
|
40
|
+
/** Reference to the form */
|
|
41
|
+
formId: string;
|
|
42
|
+
/** Submitted data as JSON string */
|
|
43
|
+
data: string;
|
|
44
|
+
/** Submission timestamp */
|
|
45
|
+
submittedAt: Date;
|
|
46
|
+
/** Optional user ID if authenticated */
|
|
47
|
+
submittedBy?: string;
|
|
48
|
+
/** Client IP address for rate limiting and spam protection */
|
|
49
|
+
ipAddress?: string;
|
|
50
|
+
/** User agent for analytics */
|
|
51
|
+
userAgent?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Form submission with its parent form joined
|
|
56
|
+
*/
|
|
57
|
+
export type FormSubmissionWithForm = FormSubmission & {
|
|
58
|
+
form?: Form;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Serialized form for API responses (dates as strings)
|
|
63
|
+
*/
|
|
64
|
+
export interface SerializedForm
|
|
65
|
+
extends Omit<Form, "createdAt" | "updatedAt" | "status"> {
|
|
66
|
+
status: string;
|
|
67
|
+
createdAt: string;
|
|
68
|
+
updatedAt: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Serialized form submission for API responses (dates as strings)
|
|
73
|
+
*/
|
|
74
|
+
export interface SerializedFormSubmission
|
|
75
|
+
extends Omit<FormSubmission, "submittedAt"> {
|
|
76
|
+
submittedAt: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Serialized form submission with parsed data
|
|
81
|
+
*/
|
|
82
|
+
export interface SerializedFormSubmissionWithData<
|
|
83
|
+
TData = Record<string, unknown>,
|
|
84
|
+
> extends SerializedFormSubmission {
|
|
85
|
+
/** Parsed data object (JSON.parse of data field) */
|
|
86
|
+
parsedData: TData;
|
|
87
|
+
/** Joined form */
|
|
88
|
+
form?: SerializedForm;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Paginated list response for forms
|
|
93
|
+
*/
|
|
94
|
+
export interface PaginatedForms {
|
|
95
|
+
items: SerializedForm[];
|
|
96
|
+
total: number;
|
|
97
|
+
limit: number;
|
|
98
|
+
offset: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Paginated list response for form submissions
|
|
103
|
+
*/
|
|
104
|
+
export interface PaginatedFormSubmissions<TData = Record<string, unknown>> {
|
|
105
|
+
items: SerializedFormSubmissionWithData<TData>[];
|
|
106
|
+
total: number;
|
|
107
|
+
limit: number;
|
|
108
|
+
offset: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// BACKEND HOOKS
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Context passed to all backend hooks
|
|
117
|
+
*/
|
|
118
|
+
export interface FormBuilderHookContext {
|
|
119
|
+
/** User ID if authenticated */
|
|
120
|
+
userId?: string;
|
|
121
|
+
/** Request headers */
|
|
122
|
+
headers?: Headers;
|
|
123
|
+
/** Client IP address (for rate limiting) */
|
|
124
|
+
ipAddress?: string;
|
|
125
|
+
/** User agent string */
|
|
126
|
+
userAgent?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Context for submission-specific hooks
|
|
131
|
+
*/
|
|
132
|
+
export interface SubmissionHookContext extends FormBuilderHookContext {
|
|
133
|
+
/** Form slug being submitted */
|
|
134
|
+
formSlug: string;
|
|
135
|
+
/** Form ID */
|
|
136
|
+
formId: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Input data for creating a form
|
|
141
|
+
*/
|
|
142
|
+
export interface FormInput {
|
|
143
|
+
name: string;
|
|
144
|
+
slug: string;
|
|
145
|
+
description?: string;
|
|
146
|
+
schema: string;
|
|
147
|
+
successMessage?: string;
|
|
148
|
+
redirectUrl?: string;
|
|
149
|
+
status?: "active" | "inactive" | "archived";
|
|
150
|
+
createdBy?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Input data for updating a form
|
|
155
|
+
*/
|
|
156
|
+
export interface FormUpdate {
|
|
157
|
+
name?: string;
|
|
158
|
+
slug?: string;
|
|
159
|
+
description?: string;
|
|
160
|
+
schema?: string;
|
|
161
|
+
successMessage?: string;
|
|
162
|
+
redirectUrl?: string;
|
|
163
|
+
status?: "active" | "inactive" | "archived";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Backend hooks for Form Builder plugin
|
|
168
|
+
*
|
|
169
|
+
* All CRUD hooks receive ipAddress and headers for auth/rate limiting.
|
|
170
|
+
* Return false from onBefore* hooks to reject the operation (throws 403).
|
|
171
|
+
*/
|
|
172
|
+
export interface FormBuilderBackendHooks {
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// FORM CRUD HOOKS (Admin operations)
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
/** Called before listing forms. Return false to deny access (403). */
|
|
178
|
+
onBeforeListForms?: (
|
|
179
|
+
ctx: FormBuilderHookContext,
|
|
180
|
+
) => Promise<boolean> | boolean;
|
|
181
|
+
|
|
182
|
+
/** Called before creating a form. Return false to deny, or modified data. */
|
|
183
|
+
onBeforeFormCreated?: (
|
|
184
|
+
data: FormInput,
|
|
185
|
+
ctx: FormBuilderHookContext,
|
|
186
|
+
) => Promise<FormInput | false> | FormInput | false;
|
|
187
|
+
|
|
188
|
+
/** Called after a form is created */
|
|
189
|
+
onAfterFormCreated?: (
|
|
190
|
+
form: SerializedForm,
|
|
191
|
+
ctx: FormBuilderHookContext,
|
|
192
|
+
) => Promise<void> | void;
|
|
193
|
+
|
|
194
|
+
/** Called before getting a form by ID or slug. Return false to deny access. */
|
|
195
|
+
onBeforeGetForm?: (
|
|
196
|
+
idOrSlug: string,
|
|
197
|
+
ctx: FormBuilderHookContext,
|
|
198
|
+
) => Promise<boolean> | boolean;
|
|
199
|
+
|
|
200
|
+
/** Called before updating a form. Return false to deny, or modified data. */
|
|
201
|
+
onBeforeFormUpdated?: (
|
|
202
|
+
id: string,
|
|
203
|
+
data: FormUpdate,
|
|
204
|
+
ctx: FormBuilderHookContext,
|
|
205
|
+
) => Promise<FormUpdate | false> | FormUpdate | false;
|
|
206
|
+
|
|
207
|
+
/** Called after a form is updated */
|
|
208
|
+
onAfterFormUpdated?: (
|
|
209
|
+
form: SerializedForm,
|
|
210
|
+
ctx: FormBuilderHookContext,
|
|
211
|
+
) => Promise<void> | void;
|
|
212
|
+
|
|
213
|
+
/** Called before deleting a form. Return false to deny. */
|
|
214
|
+
onBeforeFormDeleted?: (
|
|
215
|
+
id: string,
|
|
216
|
+
ctx: FormBuilderHookContext,
|
|
217
|
+
) => Promise<boolean> | boolean;
|
|
218
|
+
|
|
219
|
+
/** Called after a form is deleted */
|
|
220
|
+
onAfterFormDeleted?: (
|
|
221
|
+
id: string,
|
|
222
|
+
ctx: FormBuilderHookContext,
|
|
223
|
+
) => Promise<void> | void;
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// SUBMISSION HOOKS (Public form submissions)
|
|
227
|
+
// ============================================================================
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Called before processing a form submission.
|
|
231
|
+
* Use for: spam protection, rate limiting, data validation/enrichment.
|
|
232
|
+
*
|
|
233
|
+
* @returns false to reject submission (400), or modified data to continue
|
|
234
|
+
*/
|
|
235
|
+
onBeforeSubmission?: (
|
|
236
|
+
formSlug: string,
|
|
237
|
+
data: Record<string, unknown>,
|
|
238
|
+
ctx: SubmissionHookContext,
|
|
239
|
+
) =>
|
|
240
|
+
| Promise<Record<string, unknown> | false>
|
|
241
|
+
| Record<string, unknown>
|
|
242
|
+
| false;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Called after a submission is saved.
|
|
246
|
+
* Use for: sending emails, storing in CRM, triggering workflows.
|
|
247
|
+
*/
|
|
248
|
+
onAfterSubmission?: (
|
|
249
|
+
submission: SerializedFormSubmission,
|
|
250
|
+
form: SerializedForm,
|
|
251
|
+
ctx: SubmissionHookContext,
|
|
252
|
+
) => Promise<void> | void;
|
|
253
|
+
|
|
254
|
+
/** Called when a submission fails */
|
|
255
|
+
onSubmissionError?: (
|
|
256
|
+
error: Error,
|
|
257
|
+
formSlug: string,
|
|
258
|
+
data: Record<string, unknown>,
|
|
259
|
+
ctx: SubmissionHookContext,
|
|
260
|
+
) => Promise<void> | void;
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// SUBMISSIONS MANAGEMENT HOOKS (Admin viewing submissions)
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
/** Called before listing submissions. Return false to deny access (403). */
|
|
267
|
+
onBeforeListSubmissions?: (
|
|
268
|
+
formId: string,
|
|
269
|
+
ctx: FormBuilderHookContext,
|
|
270
|
+
) => Promise<boolean> | boolean;
|
|
271
|
+
|
|
272
|
+
/** Called before getting a submission. Return false to deny access. */
|
|
273
|
+
onBeforeGetSubmission?: (
|
|
274
|
+
submissionId: string,
|
|
275
|
+
ctx: FormBuilderHookContext,
|
|
276
|
+
) => Promise<boolean> | boolean;
|
|
277
|
+
|
|
278
|
+
/** Called before deleting a submission. Return false to deny. */
|
|
279
|
+
onBeforeSubmissionDeleted?: (
|
|
280
|
+
submissionId: string,
|
|
281
|
+
ctx: FormBuilderHookContext,
|
|
282
|
+
) => Promise<boolean> | boolean;
|
|
283
|
+
|
|
284
|
+
/** Called after a submission is deleted */
|
|
285
|
+
onAfterSubmissionDeleted?: (
|
|
286
|
+
submissionId: string,
|
|
287
|
+
ctx: FormBuilderHookContext,
|
|
288
|
+
) => Promise<void> | void;
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// ERROR HOOK
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
/** Called on any error */
|
|
295
|
+
onError?: (
|
|
296
|
+
error: Error,
|
|
297
|
+
operation:
|
|
298
|
+
| "list"
|
|
299
|
+
| "get"
|
|
300
|
+
| "create"
|
|
301
|
+
| "update"
|
|
302
|
+
| "delete"
|
|
303
|
+
| "submit"
|
|
304
|
+
| "listSubmissions"
|
|
305
|
+
| "getSubmission"
|
|
306
|
+
| "deleteSubmission",
|
|
307
|
+
ctx: FormBuilderHookContext,
|
|
308
|
+
) => Promise<void> | void;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Configuration for the Form Builder backend plugin
|
|
313
|
+
*/
|
|
314
|
+
export interface FormBuilderBackendConfig {
|
|
315
|
+
/** Optional hooks for customizing behavior */
|
|
316
|
+
hooks?: FormBuilderBackendHooks;
|
|
317
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
import slug from "slug";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts text to a URL-friendly slug
|
|
7
|
+
*/
|
|
8
|
+
export function slugify(text: string, locale: string = "en"): string {
|
|
9
|
+
return slug(text, { lower: true, locale });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Merges class names using clsx and tailwind-merge
|
|
14
|
+
*/
|
|
15
|
+
export function cn(...inputs: ClassValue[]) {
|
|
16
|
+
return twMerge(clsx(inputs));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Safely parses JSON with a fallback
|
|
21
|
+
*/
|
|
22
|
+
export function safeJsonParse<T>(json: string, fallback: T): T {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(json) as T;
|
|
25
|
+
} catch {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extracts client IP address from request headers
|
|
32
|
+
* Handles common proxy headers
|
|
33
|
+
*/
|
|
34
|
+
export function extractIpAddress(headers?: Headers): string | undefined {
|
|
35
|
+
if (!headers) return undefined;
|
|
36
|
+
|
|
37
|
+
// Check common proxy headers in order of preference
|
|
38
|
+
const forwardedFor = headers.get("x-forwarded-for");
|
|
39
|
+
if (forwardedFor) {
|
|
40
|
+
// x-forwarded-for can contain multiple IPs, take the first one
|
|
41
|
+
return forwardedFor.split(",")[0]?.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const realIp = headers.get("x-real-ip");
|
|
45
|
+
if (realIp) {
|
|
46
|
+
return realIp.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cfConnectingIp = headers.get("cf-connecting-ip");
|
|
50
|
+
if (cfConnectingIp) {
|
|
51
|
+
return cfConnectingIp.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extracts user agent from request headers
|
|
59
|
+
*/
|
|
60
|
+
export function extractUserAgent(headers?: Headers): string | undefined {
|
|
61
|
+
if (!headers) return undefined;
|
|
62
|
+
return headers.get("user-agent") || undefined;
|
|
63
|
+
}
|