@btst/stack 1.5.2 → 1.7.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/api/index.cjs +7 -1
- package/dist/api/index.d.cts +2 -2
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.mjs +7 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- 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/better-stack/src/plugins/open-api/api/generator.cjs +300 -0
- package/dist/packages/better-stack/src/plugins/open-api/api/generator.mjs +284 -0
- package/dist/packages/better-stack/src/plugins/open-api/api/plugin.cjs +115 -0
- package/dist/packages/better-stack/src/plugins/open-api/api/plugin.mjs +113 -0
- package/dist/packages/better-stack/src/plugins/open-api/db.cjs +7 -0
- package/dist/packages/better-stack/src/plugins/open-api/db.mjs +5 -0
- package/dist/packages/better-stack/src/plugins/open-api/logo.cjs +8 -0
- package/dist/packages/better-stack/src/plugins/open-api/logo.mjs +6 -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/api/index.d.cts +2 -2
- package/dist/plugins/api/index.d.mts +2 -2
- package/dist/plugins/api/index.d.ts +2 -2
- 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/client/index.d.cts +2 -2
- package/dist/plugins/client/index.d.mts +2 -2
- package/dist/plugins/client/index.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/plugins/open-api/api/index.cjs +9 -0
- package/dist/plugins/open-api/api/index.d.cts +95 -0
- package/dist/plugins/open-api/api/index.d.mts +95 -0
- package/dist/plugins/open-api/api/index.d.ts +95 -0
- package/dist/plugins/open-api/api/index.mjs +2 -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.ByOugz9d.d.cts → stack.CSce37mX.d.cts} +15 -2
- package/dist/shared/{stack.ByOugz9d.d.mts → stack.CSce37mX.d.mts} +15 -2
- package/dist/shared/{stack.ByOugz9d.d.ts → stack.CSce37mX.d.ts} +15 -2
- 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 +67 -1
- package/src/api/index.ts +14 -2
- 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/src/plugins/open-api/api/generator.ts +433 -0
- package/src/plugins/open-api/api/index.ts +8 -0
- package/src/plugins/open-api/api/plugin.ts +243 -0
- package/src/plugins/open-api/db.ts +7 -0
- package/src/plugins/open-api/logo.ts +7 -0
- package/src/types.ts +15 -1
- package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.cts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.mts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.ts} +1 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, type ComponentType } from "react";
|
|
4
|
+
import { usePluginOverrides } from "@btst/stack/context";
|
|
5
|
+
import { SteppedAutoForm } from "@workspace/ui/components/auto-form/stepped-auto-form";
|
|
6
|
+
import { buildFieldConfigFromJsonSchema } from "@workspace/ui/components/auto-form/utils";
|
|
7
|
+
import { formSchemaToZod } from "@workspace/ui/lib/schema-converter";
|
|
8
|
+
import { Skeleton } from "@workspace/ui/components/skeleton";
|
|
9
|
+
import { AlertCircle, CheckCircle } from "lucide-react";
|
|
10
|
+
import type { AutoFormInputComponentProps } from "@workspace/ui/components/auto-form/types";
|
|
11
|
+
|
|
12
|
+
import { useFormBySlug, useSubmitForm } from "../../hooks/form-builder-hooks";
|
|
13
|
+
import type { FormBuilderPluginOverrides } from "../../overrides";
|
|
14
|
+
import { FORM_BUILDER_LOCALIZATION } from "../../localization";
|
|
15
|
+
import type { SerializedFormSubmission } from "../../../types";
|
|
16
|
+
|
|
17
|
+
export interface FormRendererProps {
|
|
18
|
+
/** Form slug to render */
|
|
19
|
+
slug: string;
|
|
20
|
+
/** Callback when form submission succeeds */
|
|
21
|
+
onSuccess?: (
|
|
22
|
+
submission: SerializedFormSubmission & {
|
|
23
|
+
form: { successMessage?: string; redirectUrl?: string };
|
|
24
|
+
},
|
|
25
|
+
) => void;
|
|
26
|
+
/** Callback when form submission fails */
|
|
27
|
+
onError?: (error: Error) => void;
|
|
28
|
+
/** Custom field components (same as FormBuilder) */
|
|
29
|
+
fieldComponents?: Record<string, ComponentType<AutoFormInputComponentProps>>;
|
|
30
|
+
/** Override success message */
|
|
31
|
+
successMessage?: React.ReactNode;
|
|
32
|
+
/** Override submit button text */
|
|
33
|
+
submitButtonText?: string;
|
|
34
|
+
/** Custom loading component */
|
|
35
|
+
LoadingComponent?: ComponentType;
|
|
36
|
+
/** Custom error component */
|
|
37
|
+
ErrorComponent?: ComponentType<{ error: Error }>;
|
|
38
|
+
/** Class name for the form container */
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function DefaultLoadingComponent() {
|
|
43
|
+
return (
|
|
44
|
+
<div className="space-y-4">
|
|
45
|
+
<Skeleton className="h-10 w-full" />
|
|
46
|
+
<Skeleton className="h-10 w-full" />
|
|
47
|
+
<Skeleton className="h-10 w-full" />
|
|
48
|
+
<Skeleton className="h-10 w-32" />
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function DefaultErrorComponent({ error }: { error: Error }) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
56
|
+
<div className="rounded-full bg-destructive/10 p-3 mb-4">
|
|
57
|
+
<AlertCircle className="h-6 w-6 text-destructive" />
|
|
58
|
+
</div>
|
|
59
|
+
<h3 className="text-lg font-medium text-foreground mb-2">
|
|
60
|
+
Failed to load form
|
|
61
|
+
</h3>
|
|
62
|
+
<p className="text-sm text-muted-foreground max-w-sm">
|
|
63
|
+
{error.message || "An unexpected error occurred"}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function DefaultSuccessComponent({ message }: { message: React.ReactNode }) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
72
|
+
<div className="rounded-full bg-green-100 dark:bg-green-900 p-3 mb-4">
|
|
73
|
+
<CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400" />
|
|
74
|
+
</div>
|
|
75
|
+
<h3 className="text-lg font-medium text-foreground mb-2">
|
|
76
|
+
Form Submitted
|
|
77
|
+
</h3>
|
|
78
|
+
<p className="text-sm text-muted-foreground max-w-sm">{message}</p>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* FormRenderer component for rendering forms on the frontend.
|
|
85
|
+
*
|
|
86
|
+
* Uses SteppedAutoForm which automatically handles both single-step and multi-step forms.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* <FormRenderer
|
|
91
|
+
* slug="contact-form"
|
|
92
|
+
* onSuccess={() => {
|
|
93
|
+
* toast.success("Thank you!");
|
|
94
|
+
* }}
|
|
95
|
+
* onError={(error) => {
|
|
96
|
+
* toast.error("Something went wrong");
|
|
97
|
+
* }}
|
|
98
|
+
* />
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function FormRenderer({
|
|
102
|
+
slug,
|
|
103
|
+
onSuccess,
|
|
104
|
+
onError,
|
|
105
|
+
fieldComponents: propFieldComponents,
|
|
106
|
+
successMessage: propSuccessMessage,
|
|
107
|
+
submitButtonText,
|
|
108
|
+
LoadingComponent = DefaultLoadingComponent,
|
|
109
|
+
ErrorComponent = DefaultErrorComponent,
|
|
110
|
+
className,
|
|
111
|
+
}: FormRendererProps) {
|
|
112
|
+
const { fieldComponents: overrideFieldComponents, localization } =
|
|
113
|
+
usePluginOverrides<
|
|
114
|
+
FormBuilderPluginOverrides,
|
|
115
|
+
Partial<FormBuilderPluginOverrides>
|
|
116
|
+
>("form-builder", {
|
|
117
|
+
localization: FORM_BUILDER_LOCALIZATION,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const loc = localization || FORM_BUILDER_LOCALIZATION;
|
|
121
|
+
|
|
122
|
+
const { form, isLoading, error } = useFormBySlug(slug);
|
|
123
|
+
const submitMutation = useSubmitForm(slug);
|
|
124
|
+
|
|
125
|
+
const [submitted, setSubmitted] = useState(false);
|
|
126
|
+
const [finalSuccessMessage, setFinalSuccessMessage] = useState<string | null>(
|
|
127
|
+
null,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Merge field components from props and overrides
|
|
131
|
+
const mergedFieldComponents = useMemo(
|
|
132
|
+
() => ({
|
|
133
|
+
...overrideFieldComponents,
|
|
134
|
+
...propFieldComponents,
|
|
135
|
+
}),
|
|
136
|
+
[overrideFieldComponents, propFieldComponents],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Parse JSON Schema and create Zod schema
|
|
140
|
+
const { zodSchema, fieldConfig } = useMemo(() => {
|
|
141
|
+
if (!form?.schema) {
|
|
142
|
+
return { zodSchema: null, fieldConfig: {} };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const parsedSchema = JSON.parse(form.schema);
|
|
147
|
+
const zod = formSchemaToZod(parsedSchema);
|
|
148
|
+
const config = buildFieldConfigFromJsonSchema(
|
|
149
|
+
parsedSchema,
|
|
150
|
+
mergedFieldComponents,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return { zodSchema: zod, fieldConfig: config };
|
|
154
|
+
} catch {
|
|
155
|
+
return { zodSchema: null, fieldConfig: {} };
|
|
156
|
+
}
|
|
157
|
+
}, [form?.schema, mergedFieldComponents]);
|
|
158
|
+
|
|
159
|
+
const handleSubmit = async (data: Record<string, unknown>) => {
|
|
160
|
+
try {
|
|
161
|
+
const result = await submitMutation.mutateAsync({ data });
|
|
162
|
+
|
|
163
|
+
// Set success message
|
|
164
|
+
const message =
|
|
165
|
+
propSuccessMessage ||
|
|
166
|
+
result.form.successMessage ||
|
|
167
|
+
"Thank you for your submission!";
|
|
168
|
+
setFinalSuccessMessage(message as string);
|
|
169
|
+
setSubmitted(true);
|
|
170
|
+
|
|
171
|
+
// Call onSuccess callback before any redirect
|
|
172
|
+
onSuccess?.(result);
|
|
173
|
+
|
|
174
|
+
// Handle redirect
|
|
175
|
+
if (result.form.redirectUrl) {
|
|
176
|
+
window.location.href = result.form.redirectUrl;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
onError?.(err as Error);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Loading state
|
|
185
|
+
if (isLoading) {
|
|
186
|
+
return (
|
|
187
|
+
<div className={className}>
|
|
188
|
+
<LoadingComponent />
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Error state
|
|
194
|
+
if (error) {
|
|
195
|
+
return (
|
|
196
|
+
<div className={className}>
|
|
197
|
+
<ErrorComponent error={error} />
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Form not found
|
|
203
|
+
if (!form) {
|
|
204
|
+
return (
|
|
205
|
+
<div className={className}>
|
|
206
|
+
<ErrorComponent error={new Error("Form not found")} />
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Form not active
|
|
212
|
+
if (form.status !== "active") {
|
|
213
|
+
return (
|
|
214
|
+
<div className={className}>
|
|
215
|
+
<ErrorComponent
|
|
216
|
+
error={new Error("This form is not currently accepting submissions")}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Schema parsing failed
|
|
223
|
+
if (!zodSchema) {
|
|
224
|
+
return (
|
|
225
|
+
<div className={className}>
|
|
226
|
+
<ErrorComponent error={new Error("Failed to parse form schema")} />
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Success state
|
|
232
|
+
if (submitted && finalSuccessMessage) {
|
|
233
|
+
return (
|
|
234
|
+
<div className={className}>
|
|
235
|
+
<DefaultSuccessComponent message={finalSuccessMessage} />
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Render form using SteppedAutoForm
|
|
241
|
+
// It automatically handles both single-step and multi-step forms
|
|
242
|
+
return (
|
|
243
|
+
<div className={className} data-testid="form-renderer">
|
|
244
|
+
<SteppedAutoForm
|
|
245
|
+
formSchema={zodSchema}
|
|
246
|
+
fieldConfig={fieldConfig}
|
|
247
|
+
onSubmit={(values) => handleSubmit(values as Record<string, unknown>)}
|
|
248
|
+
isSubmitting={submitMutation.isPending}
|
|
249
|
+
submitButtonText={submitButtonText || loc.FORM_BUILDER_BUTTON_SUBMIT}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FormListPageComponent as FormListPageImpl } from "./pages/form-list-page";
|
|
4
|
+
import { FormBuilderPageComponent as FormBuilderPageImpl } from "./pages/form-builder-page";
|
|
5
|
+
import { SubmissionsPageComponent as SubmissionsPageImpl } from "./pages/submissions-page";
|
|
6
|
+
import { FormRenderer as FormRendererImpl } from "./forms/form-renderer";
|
|
7
|
+
|
|
8
|
+
// Re-export to ensure the client boundary is preserved
|
|
9
|
+
export const FormListPage = FormListPageImpl;
|
|
10
|
+
export const FormBuilderPage = FormBuilderPageImpl;
|
|
11
|
+
export const SubmissionsPage = SubmissionsPageImpl;
|
|
12
|
+
export const FormRenderer = FormRendererImpl;
|
|
13
|
+
|
|
14
|
+
// Export loading skeletons
|
|
15
|
+
export {
|
|
16
|
+
FormListSkeleton,
|
|
17
|
+
FormBuilderSkeleton,
|
|
18
|
+
SubmissionsSkeleton,
|
|
19
|
+
} from "./loading";
|
|
20
|
+
|
|
21
|
+
// Export shared components
|
|
22
|
+
export { DefaultError } from "./shared/default-error";
|
|
23
|
+
export { EmptyState } from "./shared/empty-state";
|
|
24
|
+
export { NotFoundPage } from "./pages/404-page";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Skeleton } from "@workspace/ui/components/skeleton";
|
|
4
|
+
|
|
5
|
+
export function FormBuilderSkeleton() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex h-full flex-col" data-testid="form-builder-skeleton">
|
|
8
|
+
{/* Header */}
|
|
9
|
+
<div className="flex items-center gap-4 border-b p-4">
|
|
10
|
+
<Skeleton className="h-10 w-48" />
|
|
11
|
+
<Skeleton className="h-10 w-32" />
|
|
12
|
+
<div className="ml-auto">
|
|
13
|
+
<Skeleton className="h-10 w-24" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
{/* Main content */}
|
|
18
|
+
<div className="flex flex-1">
|
|
19
|
+
{/* Palette */}
|
|
20
|
+
<div className="w-64 border-r p-4 space-y-4">
|
|
21
|
+
<Skeleton className="h-6 w-24" />
|
|
22
|
+
{Array.from({ length: 8 }).map((_, i) => (
|
|
23
|
+
<Skeleton key={i} className="h-12 rounded-lg" />
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{/* Canvas */}
|
|
28
|
+
<div className="flex-1 p-4 space-y-4">
|
|
29
|
+
<Skeleton className="h-6 w-32" />
|
|
30
|
+
<Skeleton className="h-48 rounded-lg" />
|
|
31
|
+
<Skeleton className="h-24 rounded-lg" />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* Preview panel */}
|
|
35
|
+
<div className="w-80 border-l p-4 space-y-4">
|
|
36
|
+
<Skeleton className="h-6 w-20" />
|
|
37
|
+
<Skeleton className="h-full rounded-lg" />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Skeleton } from "@workspace/ui/components/skeleton";
|
|
4
|
+
import { PageWrapper } from "../shared/page-wrapper";
|
|
5
|
+
|
|
6
|
+
export function FormListSkeleton() {
|
|
7
|
+
return (
|
|
8
|
+
<PageWrapper testId="form-list-skeleton">
|
|
9
|
+
<div className="w-full max-w-5xl space-y-6">
|
|
10
|
+
<div className="flex items-center justify-between">
|
|
11
|
+
<div className="space-y-2">
|
|
12
|
+
<Skeleton className="h-8 w-32" />
|
|
13
|
+
<Skeleton className="h-4 w-48" />
|
|
14
|
+
</div>
|
|
15
|
+
<Skeleton className="h-10 w-28" />
|
|
16
|
+
</div>
|
|
17
|
+
<div className="space-y-4">
|
|
18
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
19
|
+
<Skeleton key={i} className="h-16 rounded-lg" />
|
|
20
|
+
))}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</PageWrapper>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Skeleton } from "@workspace/ui/components/skeleton";
|
|
4
|
+
import { PageWrapper } from "../shared/page-wrapper";
|
|
5
|
+
|
|
6
|
+
export function SubmissionsSkeleton() {
|
|
7
|
+
return (
|
|
8
|
+
<PageWrapper testId="submissions-skeleton">
|
|
9
|
+
<div className="w-full max-w-5xl space-y-6">
|
|
10
|
+
<div className="flex items-center justify-between">
|
|
11
|
+
<div className="space-y-2">
|
|
12
|
+
<Skeleton className="h-8 w-48" />
|
|
13
|
+
<Skeleton className="h-4 w-64" />
|
|
14
|
+
</div>
|
|
15
|
+
<Skeleton className="h-10 w-32" />
|
|
16
|
+
</div>
|
|
17
|
+
<div className="rounded-lg border">
|
|
18
|
+
<div className="border-b p-4">
|
|
19
|
+
<div className="flex gap-4">
|
|
20
|
+
<Skeleton className="h-4 w-24" />
|
|
21
|
+
<Skeleton className="h-4 w-48" />
|
|
22
|
+
<Skeleton className="h-4 w-32" />
|
|
23
|
+
<Skeleton className="h-4 w-20" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
27
|
+
<div key={i} className="border-b p-4 last:border-0">
|
|
28
|
+
<div className="flex gap-4">
|
|
29
|
+
<Skeleton className="h-4 w-24" />
|
|
30
|
+
<Skeleton className="h-4 w-48" />
|
|
31
|
+
<Skeleton className="h-4 w-32" />
|
|
32
|
+
<Skeleton className="h-4 w-20" />
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</PageWrapper>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@workspace/ui/components/button";
|
|
4
|
+
import { usePluginOverrides, useBasePath } from "@btst/stack/context";
|
|
5
|
+
import type { FormBuilderPluginOverrides } from "../../overrides";
|
|
6
|
+
|
|
7
|
+
export function NotFoundPage() {
|
|
8
|
+
const { navigate, Link } =
|
|
9
|
+
usePluginOverrides<FormBuilderPluginOverrides>("form-builder");
|
|
10
|
+
const basePath = useBasePath();
|
|
11
|
+
|
|
12
|
+
const LinkComponent = Link || "a";
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
|
|
16
|
+
<h1 className="text-6xl font-bold text-muted-foreground mb-4">404</h1>
|
|
17
|
+
<h2 className="text-xl font-medium text-foreground mb-2">
|
|
18
|
+
Page not found
|
|
19
|
+
</h2>
|
|
20
|
+
<p className="text-sm text-muted-foreground mb-6 max-w-sm">
|
|
21
|
+
The page you're looking for doesn't exist or has been moved.
|
|
22
|
+
</p>
|
|
23
|
+
<Button asChild>
|
|
24
|
+
<LinkComponent href={`${basePath}/forms`}>Back to Forms</LinkComponent>
|
|
25
|
+
</Button>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { usePluginOverrides, useBasePath } from "@btst/stack/context";
|
|
5
|
+
import { Button } from "@workspace/ui/components/button";
|
|
6
|
+
import { Input } from "@workspace/ui/components/input";
|
|
7
|
+
import { Label } from "@workspace/ui/components/label";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "@workspace/ui/components/select";
|
|
15
|
+
import { ArrowLeft, Save } from "lucide-react";
|
|
16
|
+
import { toast } from "sonner";
|
|
17
|
+
import { FormBuilder } from "@workspace/ui/components/form-builder";
|
|
18
|
+
import type { JSONSchema } from "@workspace/ui/components/form-builder/types";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
useSuspenseFormById,
|
|
22
|
+
useCreateForm,
|
|
23
|
+
useUpdateForm,
|
|
24
|
+
} from "../../hooks/form-builder-hooks";
|
|
25
|
+
import type { FormBuilderPluginOverrides } from "../../overrides";
|
|
26
|
+
import { FORM_BUILDER_LOCALIZATION } from "../../localization";
|
|
27
|
+
import { slugify } from "../../../utils";
|
|
28
|
+
import type { SerializedForm } from "../../../types";
|
|
29
|
+
|
|
30
|
+
export interface FormBuilderPageProps {
|
|
31
|
+
id?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Entry point component that conditionally renders the appropriate
|
|
36
|
+
* sub-component based on whether we're creating or editing a form.
|
|
37
|
+
* This avoids conditional hook calls which violate React's Rules of Hooks.
|
|
38
|
+
*/
|
|
39
|
+
export function FormBuilderPage({ id }: FormBuilderPageProps) {
|
|
40
|
+
if (id) {
|
|
41
|
+
return <EditFormBuilderPage id={id} />;
|
|
42
|
+
}
|
|
43
|
+
return <CreateFormBuilderPage />;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Component for editing an existing form.
|
|
48
|
+
* Uses useSuspenseFormById unconditionally since id is always defined.
|
|
49
|
+
*/
|
|
50
|
+
function EditFormBuilderPage({ id }: { id: string }) {
|
|
51
|
+
const { form: existingForm } = useSuspenseFormById(id);
|
|
52
|
+
return <FormBuilderPageContent id={id} existingForm={existingForm} />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Component for creating a new form.
|
|
57
|
+
* No data fetching needed.
|
|
58
|
+
*/
|
|
59
|
+
function CreateFormBuilderPage() {
|
|
60
|
+
return <FormBuilderPageContent />;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface FormBuilderPageContentProps {
|
|
64
|
+
id?: string;
|
|
65
|
+
existingForm?: SerializedForm | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function FormBuilderPageContent({
|
|
69
|
+
id,
|
|
70
|
+
existingForm,
|
|
71
|
+
}: FormBuilderPageContentProps) {
|
|
72
|
+
const { navigate, Link, localization } = usePluginOverrides<
|
|
73
|
+
FormBuilderPluginOverrides,
|
|
74
|
+
Partial<FormBuilderPluginOverrides>
|
|
75
|
+
>("form-builder", {
|
|
76
|
+
localization: FORM_BUILDER_LOCALIZATION,
|
|
77
|
+
});
|
|
78
|
+
const basePath = useBasePath();
|
|
79
|
+
|
|
80
|
+
const createMutation = useCreateForm();
|
|
81
|
+
const updateMutation = useUpdateForm();
|
|
82
|
+
|
|
83
|
+
const loc = localization || FORM_BUILDER_LOCALIZATION;
|
|
84
|
+
const LinkComponent = Link || "a";
|
|
85
|
+
|
|
86
|
+
// Form state
|
|
87
|
+
const [name, setName] = useState(existingForm?.name || "");
|
|
88
|
+
const [slug, setSlug] = useState(existingForm?.slug || "");
|
|
89
|
+
const [status, setStatus] = useState<"active" | "inactive" | "archived">(
|
|
90
|
+
(existingForm?.status as "active" | "inactive" | "archived") || "active",
|
|
91
|
+
);
|
|
92
|
+
const [schema, setSchema] = useState<JSONSchema | undefined>(() => {
|
|
93
|
+
if (existingForm?.schema) {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(existingForm.schema) as JSONSchema;
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Auto-generate slug from name
|
|
104
|
+
const [autoSlug, setAutoSlug] = useState(!id);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (autoSlug && name) {
|
|
108
|
+
setSlug(slugify(name));
|
|
109
|
+
}
|
|
110
|
+
}, [name, autoSlug]);
|
|
111
|
+
|
|
112
|
+
const handleSchemaChange = useCallback((newSchema: JSONSchema) => {
|
|
113
|
+
setSchema(newSchema);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const handleSave = async () => {
|
|
117
|
+
if (!name.trim()) {
|
|
118
|
+
toast.error("Name is required");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!slug.trim()) {
|
|
122
|
+
toast.error("Slug is required");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!schema) {
|
|
126
|
+
toast.error("Please add at least one field to the form");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const schemaStr = JSON.stringify(schema);
|
|
132
|
+
|
|
133
|
+
if (id) {
|
|
134
|
+
await updateMutation.mutateAsync({
|
|
135
|
+
id,
|
|
136
|
+
data: {
|
|
137
|
+
name,
|
|
138
|
+
schema: schemaStr,
|
|
139
|
+
status,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
toast.success(loc.FORM_BUILDER_TOAST_UPDATE_SUCCESS);
|
|
143
|
+
} else {
|
|
144
|
+
const newForm = await createMutation.mutateAsync({
|
|
145
|
+
name,
|
|
146
|
+
slug,
|
|
147
|
+
schema: schemaStr,
|
|
148
|
+
status,
|
|
149
|
+
});
|
|
150
|
+
toast.success(loc.FORM_BUILDER_TOAST_CREATE_SUCCESS);
|
|
151
|
+
navigate?.(`${basePath}/forms/${newForm.id}/edit`);
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
155
|
+
if (message.includes("slug already exists")) {
|
|
156
|
+
toast.error(loc.FORM_BUILDER_TOAST_DUPLICATE_SLUG);
|
|
157
|
+
} else {
|
|
158
|
+
toast.error(loc.FORM_BUILDER_TOAST_ERROR);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const isSaving = createMutation.isPending || updateMutation.isPending;
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="flex h-full flex-col" data-testid="form-builder-page">
|
|
167
|
+
{/* Header */}
|
|
168
|
+
<div className="flex items-center gap-4 border-b p-4">
|
|
169
|
+
<Button variant="ghost" size="icon" asChild>
|
|
170
|
+
<LinkComponent href={`${basePath}/forms`}>
|
|
171
|
+
<ArrowLeft className="h-4 w-4" />
|
|
172
|
+
</LinkComponent>
|
|
173
|
+
</Button>
|
|
174
|
+
|
|
175
|
+
<div className="flex flex-col gap-1">
|
|
176
|
+
<Label htmlFor="form-name" className="text-xs text-muted-foreground">
|
|
177
|
+
{loc.FORM_BUILDER_LABEL_NAME}
|
|
178
|
+
</Label>
|
|
179
|
+
<Input
|
|
180
|
+
id="form-name"
|
|
181
|
+
value={name}
|
|
182
|
+
onChange={(e) => setName(e.target.value)}
|
|
183
|
+
placeholder={loc.FORM_BUILDER_EDITOR_NAME_PLACEHOLDER}
|
|
184
|
+
className="h-8 w-48"
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div className="flex flex-col gap-1">
|
|
189
|
+
<Label htmlFor="form-slug" className="text-xs text-muted-foreground">
|
|
190
|
+
{loc.FORM_BUILDER_LABEL_SLUG}
|
|
191
|
+
</Label>
|
|
192
|
+
<Input
|
|
193
|
+
id="form-slug"
|
|
194
|
+
value={slug}
|
|
195
|
+
onChange={(e) => {
|
|
196
|
+
setSlug(e.target.value);
|
|
197
|
+
setAutoSlug(false);
|
|
198
|
+
}}
|
|
199
|
+
placeholder={loc.FORM_BUILDER_EDITOR_SLUG_PLACEHOLDER}
|
|
200
|
+
className="h-8 w-48 font-mono text-sm"
|
|
201
|
+
disabled={!!id}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div className="flex flex-col gap-1">
|
|
206
|
+
<Label
|
|
207
|
+
htmlFor="form-status"
|
|
208
|
+
className="text-xs text-muted-foreground"
|
|
209
|
+
>
|
|
210
|
+
{loc.FORM_BUILDER_LABEL_STATUS}
|
|
211
|
+
</Label>
|
|
212
|
+
<Select
|
|
213
|
+
value={status}
|
|
214
|
+
onValueChange={(v) => setStatus(v as typeof status)}
|
|
215
|
+
>
|
|
216
|
+
<SelectTrigger className="h-8 w-28">
|
|
217
|
+
<SelectValue />
|
|
218
|
+
</SelectTrigger>
|
|
219
|
+
<SelectContent>
|
|
220
|
+
<SelectItem value="active">
|
|
221
|
+
{loc.FORM_BUILDER_STATUS_ACTIVE}
|
|
222
|
+
</SelectItem>
|
|
223
|
+
<SelectItem value="inactive">
|
|
224
|
+
{loc.FORM_BUILDER_STATUS_INACTIVE}
|
|
225
|
+
</SelectItem>
|
|
226
|
+
<SelectItem value="archived">
|
|
227
|
+
{loc.FORM_BUILDER_STATUS_ARCHIVED}
|
|
228
|
+
</SelectItem>
|
|
229
|
+
</SelectContent>
|
|
230
|
+
</Select>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div className="ml-auto">
|
|
234
|
+
<Button onClick={handleSave} disabled={isSaving}>
|
|
235
|
+
<Save className="mr-2 h-4 w-4" />
|
|
236
|
+
{isSaving
|
|
237
|
+
? loc.FORM_BUILDER_STATUS_SAVING
|
|
238
|
+
: id
|
|
239
|
+
? loc.FORM_BUILDER_BUTTON_SAVE
|
|
240
|
+
: loc.FORM_BUILDER_BUTTON_CREATE}
|
|
241
|
+
</Button>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{/* Form Builder */}
|
|
246
|
+
<FormBuilder
|
|
247
|
+
value={schema}
|
|
248
|
+
onChange={handleSchemaChange}
|
|
249
|
+
className="flex-1"
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { lazy, Suspense } from "react";
|
|
4
|
+
import { FormBuilderSkeleton } from "../loading/form-builder-skeleton";
|
|
5
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
6
|
+
import { DefaultError } from "../shared/default-error";
|
|
7
|
+
|
|
8
|
+
const FormBuilderPage = lazy(() =>
|
|
9
|
+
import("./form-builder-page.internal").then((m) => ({
|
|
10
|
+
default: m.FormBuilderPage,
|
|
11
|
+
})),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export interface FormBuilderPageProps {
|
|
15
|
+
id?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function FormBuilderPageComponent({ id }: FormBuilderPageProps) {
|
|
19
|
+
return (
|
|
20
|
+
<ErrorBoundary FallbackComponent={DefaultError}>
|
|
21
|
+
<Suspense fallback={<FormBuilderSkeleton />}>
|
|
22
|
+
<FormBuilderPage id={id} />
|
|
23
|
+
</Suspense>
|
|
24
|
+
</ErrorBoundary>
|
|
25
|
+
);
|
|
26
|
+
}
|