@object-ui/plugin-form 3.0.3 → 3.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/src/ModalForm.tsx CHANGED
@@ -13,19 +13,23 @@
13
13
  * Aligns with @objectstack/spec FormView type: 'modal'
14
14
  */
15
15
 
16
- import React, { useState, useCallback, useEffect, useMemo } from 'react';
16
+ import React, { useState, useCallback, useEffect, useMemo, useId } from 'react';
17
17
  import type { FormField, DataSource } from '@object-ui/types';
18
18
  import {
19
19
  Dialog,
20
- DialogContent,
20
+ MobileDialogContent,
21
21
  DialogHeader,
22
22
  DialogTitle,
23
23
  DialogDescription,
24
+ Skeleton,
25
+ Button,
24
26
  cn,
25
27
  } from '@object-ui/components';
28
+ import { Loader2 } from 'lucide-react';
26
29
  import { FormSection } from './FormSection';
27
- import { SchemaRenderer } from '@object-ui/react';
30
+ import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
28
31
  import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields';
32
+ import { applyAutoLayout, inferModalSize } from './autoLayout';
29
33
 
30
34
  export interface ModalFormSectionConfig {
31
35
  name?: string;
@@ -101,19 +105,58 @@ const modalSizeClasses: Record<string, string> = {
101
105
  full: 'max-w-[95vw] w-full',
102
106
  };
103
107
 
108
+ /**
109
+ * Container-query-based grid classes for form field layout.
110
+ * Uses @container / @md: / @2xl: / @4xl: variants so that the grid
111
+ * responds to the modal's actual width instead of the viewport,
112
+ * ensuring single-column on narrow mobile modals regardless of viewport size.
113
+ */
114
+ const CONTAINER_GRID_COLS: Record<number, string | undefined> = {
115
+ 1: undefined, // let the form renderer use its default (space-y-4)
116
+ 2: 'grid gap-4 grid-cols-1 @md:grid-cols-2',
117
+ 3: 'grid gap-4 grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3',
118
+ 4: 'grid gap-4 grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 @4xl:grid-cols-4',
119
+ };
120
+
104
121
  export const ModalForm: React.FC<ModalFormProps> = ({
105
122
  schema,
106
123
  dataSource,
107
124
  className,
108
125
  }) => {
126
+ const { fieldLabel } = useSafeFieldLabel();
109
127
  const [objectSchema, setObjectSchema] = useState<any>(null);
110
128
  const [formFields, setFormFields] = useState<FormField[]>([]);
111
129
  const [formData, setFormData] = useState<Record<string, any>>({});
112
130
  const [loading, setLoading] = useState(true);
113
131
  const [error, setError] = useState<Error | null>(null);
132
+ const [isSubmitting, setIsSubmitting] = useState(false);
114
133
 
115
134
  const isOpen = schema.open !== false;
116
- const sizeClass = modalSizeClasses[schema.modalSize || 'default'] || modalSizeClasses.default;
135
+
136
+ // Stable form id for linking the external submit button to the form element
137
+ const formId = useId();
138
+
139
+ // Compute auto-layout for flat fields (no sections) to determine inferred columns
140
+ const autoLayoutResult = useMemo(() => {
141
+ if (schema.sections?.length || schema.customFields?.length) return null;
142
+ return applyAutoLayout(formFields, objectSchema, schema.columns, schema.mode);
143
+ }, [formFields, objectSchema, schema.columns, schema.mode, schema.sections, schema.customFields]);
144
+
145
+ // Auto-upgrade modal size when auto-layout infers multi-column and user hasn't set modalSize
146
+ const effectiveModalSize = useMemo(() => {
147
+ if (schema.modalSize) return schema.modalSize;
148
+ if (autoLayoutResult?.columns && autoLayoutResult.columns > 1) {
149
+ return inferModalSize(autoLayoutResult.columns);
150
+ }
151
+ // Auto-upgrade for sections: use the max columns across all sections
152
+ if (schema.sections?.length) {
153
+ const maxCols = Math.max(...schema.sections.map(s => Number(s.columns) || 1));
154
+ if (maxCols > 1) return inferModalSize(maxCols);
155
+ }
156
+ return 'default';
157
+ }, [schema.modalSize, autoLayoutResult, schema.sections]);
158
+
159
+ const sizeClass = modalSizeClasses[effectiveModalSize] || modalSizeClasses.default;
117
160
 
118
161
  // Fetch object schema
119
162
  useEffect(() => {
@@ -176,7 +219,7 @@ export const ModalForm: React.FC<ModalFormProps> = ({
176
219
  const field = objectSchema.fields[fieldName];
177
220
  fields.push({
178
221
  name: fieldName,
179
- label: field.label || fieldName,
222
+ label: fieldLabel(schema.objectName, fieldName, field.label || fieldName),
180
223
  type: mapFieldTypeToFormType(field.type),
181
224
  required: field.required || false,
182
225
  disabled: schema.readOnly || schema.mode === 'view' || field.readonly,
@@ -227,7 +270,7 @@ export const ModalForm: React.FC<ModalFormProps> = ({
227
270
 
228
271
  generated.push({
229
272
  name,
230
- label: field.label || name,
273
+ label: fieldLabel(schema.objectName, name, field.label || name),
231
274
  type: mapFieldTypeToFormType(field.type),
232
275
  required: field.required || false,
233
276
  disabled: schema.readOnly || schema.mode === 'view' || field.readonly,
@@ -246,16 +289,17 @@ export const ModalForm: React.FC<ModalFormProps> = ({
246
289
 
247
290
  // Handle form submission
248
291
  const handleSubmit = useCallback(async (data: Record<string, any>) => {
249
- if (!dataSource) {
250
- if (schema.onSuccess) {
251
- await schema.onSuccess(data);
292
+ setIsSubmitting(true);
293
+ try {
294
+ if (!dataSource) {
295
+ if (schema.onSuccess) {
296
+ await schema.onSuccess(data);
297
+ }
298
+ // Close modal on success
299
+ schema.onOpenChange?.(false);
300
+ return data;
252
301
  }
253
- // Close modal on success
254
- schema.onOpenChange?.(false);
255
- return data;
256
- }
257
302
 
258
- try {
259
303
  let result;
260
304
  if (schema.mode === 'create') {
261
305
  result = await dataSource.create(schema.objectName, data);
@@ -273,6 +317,8 @@ export const ModalForm: React.FC<ModalFormProps> = ({
273
317
  schema.onError(err as Error);
274
318
  }
275
319
  throw err;
320
+ } finally {
321
+ setIsSubmitting(false);
276
322
  }
277
323
  }, [schema, dataSource]);
278
324
 
@@ -290,16 +336,24 @@ export const ModalForm: React.FC<ModalFormProps> = ({
290
336
  : 'vertical';
291
337
 
292
338
  // Build base form schema
339
+ // Actions are hidden inside the form renderer — we render them in a sticky footer instead
340
+ const showSubmit = schema.showSubmit !== false && schema.mode !== 'view';
341
+ const showCancel = schema.showCancel !== false;
342
+ const submitLabel = schema.submitText || (schema.mode === 'create' ? 'Create' : 'Update');
343
+ const cancelLabel = schema.cancelText || 'Cancel';
344
+
293
345
  const baseFormSchema = {
294
346
  type: 'form' as const,
295
347
  layout: formLayout,
296
348
  defaultValues: formData,
297
- submitLabel: schema.submitText || (schema.mode === 'create' ? 'Create' : 'Update'),
298
- cancelLabel: schema.cancelText,
299
- showSubmit: schema.showSubmit !== false && schema.mode !== 'view',
300
- showCancel: schema.showCancel !== false,
349
+ submitLabel,
350
+ cancelLabel,
351
+ showSubmit,
352
+ showCancel,
301
353
  onSubmit: handleSubmit,
302
354
  onCancel: handleCancel,
355
+ showActions: false, // Hide actions — rendered in sticky footer
356
+ id: formId, // Link external submit button via form attribute
303
357
  };
304
358
 
305
359
  const renderContent = () => {
@@ -314,9 +368,13 @@ export const ModalForm: React.FC<ModalFormProps> = ({
314
368
 
315
369
  if (loading) {
316
370
  return (
317
- <div className="p-8 text-center">
318
- <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
319
- <p className="mt-2 text-sm text-gray-600">Loading form...</p>
371
+ <div className="space-y-4" data-testid="modal-form-skeleton">
372
+ {[1, 2, 3].map((i) => (
373
+ <div key={i} className="space-y-2">
374
+ <Skeleton className="h-4 w-24" />
375
+ <Skeleton className="h-10 w-full" />
376
+ </div>
377
+ ))}
320
378
  </div>
321
379
  );
322
380
  }
@@ -325,53 +383,95 @@ export const ModalForm: React.FC<ModalFormProps> = ({
325
383
  if (schema.sections?.length) {
326
384
  return (
327
385
  <div className="space-y-6">
328
- {schema.sections.map((section, index) => (
329
- <FormSection
330
- key={section.name || section.label || index}
331
- label={section.label}
332
- description={section.description}
333
- columns={section.columns || 1}
334
- >
335
- <SchemaRenderer
336
- schema={{
337
- ...baseFormSchema,
338
- fields: buildSectionFields(section),
339
- showSubmit: index === schema.sections!.length - 1 && baseFormSchema.showSubmit,
340
- showCancel: index === schema.sections!.length - 1 && baseFormSchema.showCancel,
341
- }}
342
- />
343
- </FormSection>
344
- ))}
386
+ {schema.sections.map((section, index) => {
387
+ const sectionCols = section.columns || 1;
388
+ return (
389
+ <FormSection
390
+ key={section.name || section.label || index}
391
+ label={section.label}
392
+ description={section.description}
393
+ columns={sectionCols}
394
+ gridClassName={CONTAINER_GRID_COLS[sectionCols]}
395
+ >
396
+ <SchemaRenderer
397
+ schema={{
398
+ ...baseFormSchema,
399
+ fields: buildSectionFields(section),
400
+ // Actions are in the sticky footer, not inside sections
401
+ }}
402
+ />
403
+ </FormSection>
404
+ );
405
+ })}
345
406
  </div>
346
407
  );
347
408
  }
348
409
 
349
- // Flat fields layout
410
+ // Reuse pre-computed auto-layout result for flat fields
411
+ const layoutResult = autoLayoutResult ?? applyAutoLayout(formFields, objectSchema, schema.columns, schema.mode);
412
+
413
+ // Flat fields layout — use container-query grid classes so the form
414
+ // responds to the modal width, not the viewport width.
415
+ const containerFieldClass = CONTAINER_GRID_COLS[layoutResult.columns || 1];
416
+
350
417
  return (
351
418
  <SchemaRenderer
352
419
  schema={{
353
420
  ...baseFormSchema,
354
- fields: formFields,
355
- columns: schema.columns,
421
+ fields: layoutResult.fields,
422
+ columns: layoutResult.columns,
423
+ ...(containerFieldClass ? { fieldContainerClass: containerFieldClass } : {}),
356
424
  }}
357
425
  />
358
426
  );
359
427
  };
360
428
 
429
+ const hasFooter = !loading && !error && (showSubmit || showCancel);
430
+
361
431
  return (
362
432
  <Dialog open={isOpen} onOpenChange={schema.onOpenChange}>
363
- <DialogContent className={cn(sizeClass, 'max-h-[90vh] overflow-y-auto', className, schema.className)}>
433
+ <MobileDialogContent className={cn(sizeClass, 'flex flex-col h-[100dvh] sm:h-auto sm:max-h-[90vh] overflow-hidden p-0', className, schema.className)}>
364
434
  {(schema.title || schema.description) && (
365
- <DialogHeader>
435
+ <DialogHeader className="shrink-0 px-4 pt-4 sm:px-6 sm:pt-6 pb-2 border-b">
366
436
  {schema.title && <DialogTitle>{schema.title}</DialogTitle>}
367
437
  {schema.description && <DialogDescription>{schema.description}</DialogDescription>}
368
438
  </DialogHeader>
369
439
  )}
370
440
 
371
- <div className="py-2">
441
+ <div className="@container flex-1 overflow-y-auto px-4 sm:px-6 py-4">
372
442
  {renderContent()}
373
443
  </div>
374
- </DialogContent>
444
+
445
+ {/* Sticky footer — always visible action buttons */}
446
+ {hasFooter && (
447
+ <div className="shrink-0 border-t px-4 sm:px-6 py-3 bg-background" data-testid="modal-form-footer">
448
+ <div className="flex flex-col sm:flex-row gap-2 sm:justify-end">
449
+ {showCancel && (
450
+ <Button
451
+ type="button"
452
+ variant="outline"
453
+ onClick={handleCancel}
454
+ disabled={isSubmitting}
455
+ className="w-full sm:w-auto"
456
+ >
457
+ {cancelLabel}
458
+ </Button>
459
+ )}
460
+ {showSubmit && (
461
+ <Button
462
+ type="submit"
463
+ form={formId}
464
+ disabled={isSubmitting}
465
+ className="w-full sm:w-auto"
466
+ >
467
+ {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
468
+ {submitLabel}
469
+ </Button>
470
+ )}
471
+ </div>
472
+ </div>
473
+ )}
474
+ </MobileDialogContent>
375
475
  </Dialog>
376
476
  );
377
477
  };
@@ -15,7 +15,7 @@
15
15
 
16
16
  import React, { useEffect, useState, useCallback } from 'react';
17
17
  import type { ObjectFormSchema, FormField, FormSchema, DataSource } from '@object-ui/types';
18
- import { SchemaRenderer } from '@object-ui/react';
18
+ import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
19
19
  import { mapFieldTypeToFormType, buildValidationRules, evaluateCondition, formatFileSize } from '@object-ui/fields';
20
20
  import { TabbedForm } from './TabbedForm';
21
21
  import { WizardForm } from './WizardForm';
@@ -23,6 +23,7 @@ import { SplitForm } from './SplitForm';
23
23
  import { DrawerForm } from './DrawerForm';
24
24
  import { ModalForm } from './ModalForm';
25
25
  import { FormSection } from './FormSection';
26
+ import { applyAutoLayout } from './autoLayout';
26
27
 
27
28
  export interface ObjectFormProps {
28
29
  /**
@@ -201,6 +202,7 @@ const SimpleObjectForm: React.FC<ObjectFormProps> = ({
201
202
  schema,
202
203
  dataSource,
203
204
  }) => {
205
+ const { fieldLabel } = useSafeFieldLabel();
204
206
 
205
207
  const [objectSchema, setObjectSchema] = useState<any>(null);
206
208
  const [formFields, setFormFields] = useState<FormField[]>([]);
@@ -332,7 +334,7 @@ const SimpleObjectForm: React.FC<ObjectFormProps> = ({
332
334
  // Auto-generate field from schema
333
335
  const formField: FormField = {
334
336
  name: name,
335
- label: field.label || fieldName,
337
+ label: fieldLabel(schema.objectName, name, field.label || fieldName),
336
338
  type: mapFieldTypeToFormType(field.type),
337
339
  required: field.required || false,
338
340
  disabled: schema.readOnly || schema.mode === 'view' || field.readonly,
@@ -575,12 +577,18 @@ const SimpleObjectForm: React.FC<ObjectFormProps> = ({
575
577
  );
576
578
  }
577
579
 
580
+ // Apply auto-layout: infer columns and colSpan when not explicitly configured
581
+ const hasSections = schema.sections?.length;
582
+ const autoLayoutResult = !hasSections
583
+ ? applyAutoLayout(formFields, objectSchema, schema.columns, schema.mode)
584
+ : { fields: formFields, columns: schema.columns };
585
+
578
586
  // Default flat form (no sections)
579
587
  const formSchema: FormSchema = {
580
588
  type: 'form',
581
- fields: formFields,
589
+ fields: autoLayoutResult.fields,
582
590
  layout: formLayout,
583
- columns: schema.columns,
591
+ columns: autoLayoutResult.columns,
584
592
  submitLabel: schema.submitText || (schema.mode === 'create' ? 'Create' : 'Update'),
585
593
  cancelLabel: schema.cancelText,
586
594
  showSubmit: schema.showSubmit !== false && schema.mode !== 'view',
package/src/SplitForm.tsx CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  cn,
24
24
  } from '@object-ui/components';
25
25
  import { FormSection } from './FormSection';
26
- import { SchemaRenderer } from '@object-ui/react';
26
+ import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
27
27
  import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields';
28
28
 
29
29
  export interface SplitFormSectionConfig {
@@ -85,6 +85,7 @@ export const SplitForm: React.FC<SplitFormProps> = ({
85
85
  dataSource,
86
86
  className,
87
87
  }) => {
88
+ const { fieldLabel } = useSafeFieldLabel();
88
89
  const [objectSchema, setObjectSchema] = useState<any>(null);
89
90
  const [formData, setFormData] = useState<Record<string, any>>({});
90
91
  const [loading, setLoading] = useState(true);
@@ -151,7 +152,7 @@ export const SplitForm: React.FC<SplitFormProps> = ({
151
152
  const field = objectSchema.fields[fieldName];
152
153
  fields.push({
153
154
  name: fieldName,
154
- label: field.label || fieldName,
155
+ label: fieldLabel(schema.objectName, fieldName, field.label || fieldName),
155
156
  type: mapFieldTypeToFormType(field.type),
156
157
  required: field.required || false,
157
158
  disabled: schema.readOnly || schema.mode === 'view' || field.readonly,
@@ -17,7 +17,7 @@ import React, { useState, useCallback } from 'react';
17
17
  import type { FormField, DataSource } from '@object-ui/types';
18
18
  import { Tabs, TabsContent, TabsList, TabsTrigger, cn } from '@object-ui/components';
19
19
  import { FormSection } from './FormSection';
20
- import { SchemaRenderer } from '@object-ui/react';
20
+ import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
21
21
  import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields';
22
22
 
23
23
  export interface FormSectionConfig {
@@ -166,6 +166,7 @@ export const TabbedForm: React.FC<TabbedFormProps> = ({
166
166
  dataSource,
167
167
  className,
168
168
  }) => {
169
+ const { fieldLabel } = useSafeFieldLabel();
169
170
  const [objectSchema, setObjectSchema] = useState<any>(null);
170
171
  const [formData, setFormData] = useState<Record<string, any>>({});
171
172
  const [loading, setLoading] = useState(true);
@@ -232,7 +233,7 @@ export const TabbedForm: React.FC<TabbedFormProps> = ({
232
233
  const field = objectSchema.fields[fieldName];
233
234
  fields.push({
234
235
  name: fieldName,
235
- label: field.label || fieldName,
236
+ label: fieldLabel(schema.objectName, fieldName, field.label || fieldName),
236
237
  type: mapFieldTypeToFormType(field.type),
237
238
  required: field.required || false,
238
239
  disabled: schema.readOnly || schema.mode === 'view' || field.readonly,
@@ -18,7 +18,7 @@ import type { FormField, DataSource } from '@object-ui/types';
18
18
  import { Button, cn } from '@object-ui/components';
19
19
  import { Check, ChevronLeft, ChevronRight } from 'lucide-react';
20
20
  import { FormSection } from './FormSection';
21
- import { SchemaRenderer } from '@object-ui/react';
21
+ import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
22
22
  import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields';
23
23
  import type { FormSectionConfig } from './TabbedForm';
24
24
 
@@ -153,6 +153,7 @@ export const WizardForm: React.FC<WizardFormProps> = ({
153
153
  dataSource,
154
154
  className,
155
155
  }) => {
156
+ const { fieldLabel } = useSafeFieldLabel();
156
157
  const [objectSchema, setObjectSchema] = useState<any>(null);
157
158
  const [formData, setFormData] = useState<Record<string, any>>({});
158
159
  const [loading, setLoading] = useState(true);
@@ -221,7 +222,7 @@ export const WizardForm: React.FC<WizardFormProps> = ({
221
222
  const field = objectSchema.fields[fieldName];
222
223
  fields.push({
223
224
  name: fieldName,
224
- label: field.label || fieldName,
225
+ label: fieldLabel(schema.objectName, fieldName, field.label || fieldName),
225
226
  type: mapFieldTypeToFormType(field.type),
226
227
  required: field.required || false,
227
228
  disabled: schema.readOnly || schema.mode === 'view' || field.readonly,
@@ -0,0 +1,186 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
11
+ import React from 'react';
12
+
13
+ // Capture the initialData passed to ObjectForm
14
+ let capturedSchema: any = null;
15
+ let capturedOnSuccess: ((data: any) => void) | null = null;
16
+
17
+ vi.mock('../ObjectForm', () => ({
18
+ ObjectForm: ({ schema, dataSource }: any) => {
19
+ capturedSchema = schema;
20
+ capturedOnSuccess = schema?.onSuccess ?? null;
21
+ return (
22
+ <div>
23
+ <div data-testid="object-form">
24
+ {schema?.initialData && (
25
+ <div data-testid="initial-data">
26
+ {JSON.stringify(schema.initialData)}
27
+ </div>
28
+ )}
29
+ </div>
30
+ <button
31
+ data-testid="mock-submit"
32
+ onClick={() => schema?.onSuccess?.({ name: 'submitted' })}
33
+ >
34
+ Submit
35
+ </button>
36
+ </div>
37
+ );
38
+ },
39
+ }));
40
+
41
+ vi.mock('@object-ui/types', () => ({
42
+ // DataSource type is only used for typing, provide empty default
43
+ }));
44
+
45
+ import { EmbeddableForm } from '../EmbeddableForm';
46
+ import type { EmbeddableFormConfig } from '../EmbeddableForm';
47
+
48
+ const baseConfig: EmbeddableFormConfig = {
49
+ formId: 'test-form',
50
+ objectName: 'contacts',
51
+ title: 'Test Form',
52
+ };
53
+
54
+ describe('EmbeddableForm - URL Prefill', () => {
55
+ const originalLocation = window.location;
56
+
57
+ beforeEach(() => {
58
+ capturedSchema = null;
59
+ capturedOnSuccess = null;
60
+ });
61
+
62
+ afterEach(() => {
63
+ // Restore window.location
64
+ Object.defineProperty(window, 'location', {
65
+ writable: true,
66
+ value: originalLocation,
67
+ });
68
+ });
69
+
70
+ it('reads URL search params and passes them as initialData', () => {
71
+ Object.defineProperty(window, 'location', {
72
+ writable: true,
73
+ value: { ...originalLocation, search: '?name=John&email=john@test.com' },
74
+ });
75
+
76
+ render(<EmbeddableForm config={baseConfig} />);
77
+
78
+ expect(capturedSchema).not.toBeNull();
79
+ expect(capturedSchema.initialData).toEqual({
80
+ name: 'John',
81
+ email: 'john@test.com',
82
+ });
83
+ });
84
+
85
+ it('passes explicit prefillParams as initialData', () => {
86
+ Object.defineProperty(window, 'location', {
87
+ writable: true,
88
+ value: { ...originalLocation, search: '' },
89
+ });
90
+
91
+ render(
92
+ <EmbeddableForm
93
+ config={baseConfig}
94
+ prefillParams={{ company: 'Acme', role: 'Admin' }}
95
+ />
96
+ );
97
+
98
+ expect(capturedSchema.initialData).toEqual({
99
+ company: 'Acme',
100
+ role: 'Admin',
101
+ });
102
+ });
103
+
104
+ it('explicit prefillParams override URL params for the same field', () => {
105
+ Object.defineProperty(window, 'location', {
106
+ writable: true,
107
+ value: { ...originalLocation, search: '?name=URLName&email=url@test.com' },
108
+ });
109
+
110
+ render(
111
+ <EmbeddableForm
112
+ config={baseConfig}
113
+ prefillParams={{ name: 'ExplicitName' }}
114
+ />
115
+ );
116
+
117
+ // name should come from prefillParams, email should come from URL
118
+ expect(capturedSchema.initialData).toEqual({
119
+ name: 'ExplicitName',
120
+ email: 'url@test.com',
121
+ });
122
+ });
123
+
124
+ it('passes undefined initialData when no params are provided', () => {
125
+ Object.defineProperty(window, 'location', {
126
+ writable: true,
127
+ value: { ...originalLocation, search: '' },
128
+ });
129
+
130
+ render(<EmbeddableForm config={baseConfig} />);
131
+
132
+ expect(capturedSchema.initialData).toBeUndefined();
133
+ });
134
+
135
+ it('shows the thank-you page after successful submission', async () => {
136
+ Object.defineProperty(window, 'location', {
137
+ writable: true,
138
+ value: { ...originalLocation, search: '' },
139
+ });
140
+
141
+ render(
142
+ <EmbeddableForm
143
+ config={{
144
+ ...baseConfig,
145
+ thankYouPage: {
146
+ title: 'All Done!',
147
+ message: 'We got your response.',
148
+ },
149
+ }}
150
+ />
151
+ );
152
+
153
+ // Form should be visible initially
154
+ expect(screen.getByText('Test Form')).toBeInTheDocument();
155
+
156
+ // Click mock submit to trigger onSuccess
157
+ const submitBtn = screen.getByTestId('mock-submit');
158
+ fireEvent.click(submitBtn);
159
+
160
+ // Thank-you page should appear
161
+ await waitFor(() => {
162
+ expect(screen.getByText('All Done!')).toBeInTheDocument();
163
+ expect(screen.getByText('We got your response.')).toBeInTheDocument();
164
+ });
165
+
166
+ // Form title should no longer be visible
167
+ expect(screen.queryByText('Test Form')).not.toBeInTheDocument();
168
+ });
169
+
170
+ it('shows default thank-you message when no custom thankYouPage is configured', async () => {
171
+ Object.defineProperty(window, 'location', {
172
+ writable: true,
173
+ value: { ...originalLocation, search: '' },
174
+ });
175
+
176
+ render(<EmbeddableForm config={baseConfig} />);
177
+
178
+ const submitBtn = screen.getByTestId('mock-submit');
179
+ fireEvent.click(submitBtn);
180
+
181
+ await waitFor(() => {
182
+ expect(screen.getByText('Thank You!')).toBeInTheDocument();
183
+ expect(screen.getByText('Your submission has been received successfully.')).toBeInTheDocument();
184
+ });
185
+ });
186
+ });