diff --git a/src/containers/Search/Search.module.css b/src/containers/Search/Search.module.css
index 2311d34187..7d7905d22c 100644
--- a/src/containers/Search/Search.module.css
+++ b/src/containers/Search/Search.module.css
@@ -17,14 +17,14 @@
width: 29px;
}
-.Form>h5:nth-of-type(7) {
+.Form > h5:nth-of-type(7) {
display: inline-block;
margin-right: 173px;
margin-top: 24px;
margin-bottom: 15px;
}
-.Form>div:nth-of-type(7) {
+.Form > div:nth-of-type(7) {
display: inline;
font-weight: 400;
}
@@ -58,7 +58,6 @@
.FormSearch {
padding: 0px;
width: 100%;
-
}
.Buttons {
diff --git a/src/containers/SettingList/SettingList.test.helper.ts b/src/containers/SettingList/SettingList.test.helper.ts
index af6edddcf5..90e2598674 100644
--- a/src/containers/SettingList/SettingList.test.helper.ts
+++ b/src/containers/SettingList/SettingList.test.helper.ts
@@ -7,7 +7,7 @@ import {
getOrganizationSettings,
getCredential,
getQualityRating,
- getOrganizationSettingsAllowBot
+ getOrganizationSettingsAllowBot,
} from 'mocks/Organization';
import { FLOW_STATUS_PUBLISHED, setVariables } from 'common/constants';
import { UPDATE_ORGANIZATION } from 'graphql/mutations/Organization';
@@ -96,7 +96,7 @@ const updateOrganizationMock = {
setting: {
lowBalanceThreshold: '10',
criticalBalanceThreshold: '5',
- sendWarningMail: false
+ sendWarningMail: false,
},
},
},
@@ -163,7 +163,7 @@ const updateOrganizationMock = {
criticalBalanceThreshold: '3',
lowBalanceThreshold: '10',
sendWarningMail: true,
- allowBotNumberUpdate: false
+ allowBotNumberUpdate: false,
},
shortcode: 'glific',
},
@@ -185,7 +185,7 @@ const updateOrganizationMock2 = {
setting: {
lowBalanceThreshold: '10',
criticalBalanceThreshold: '5',
- sendWarningMail: false
+ sendWarningMail: false,
},
},
},
@@ -252,7 +252,7 @@ const updateOrganizationMock2 = {
criticalBalanceThreshold: '3',
lowBalanceThreshold: '10',
sendWarningMail: true,
- allowBotNumberUpdate: false
+ allowBotNumberUpdate: false,
},
shortcode: 'glific',
},
@@ -269,7 +269,7 @@ export const ORGANIZATION_MOCKS = [
flowsMock,
...getOrganizationQuery,
updateOrganizationMock,
- updateOrganizationMock2
+ updateOrganizationMock2,
];
export const ORGANIZATION_MOCKS2 = [
@@ -280,5 +280,5 @@ export const ORGANIZATION_MOCKS2 = [
flowsMock,
...getOrganizationSettingsAllowBot,
updateOrganizationMock,
- updateOrganizationMock2
-]
+ updateOrganizationMock2,
+];
diff --git a/src/containers/SpeedSend/SpeedSend.module.css b/src/containers/SpeedSend/SpeedSend.module.css
index d4f53d68c4..aa0804bfe9 100644
--- a/src/containers/SpeedSend/SpeedSend.module.css
+++ b/src/containers/SpeedSend/SpeedSend.module.css
@@ -91,4 +91,4 @@
font-weight: 400;
line-height: 18px;
font-size: 16px;
-}
\ No newline at end of file
+}
diff --git a/src/containers/TemplateOptions/TemplateOptions.module.css b/src/containers/TemplateOptions/TemplateOptions.module.css
index f553e07460..a000820def 100644
--- a/src/containers/TemplateOptions/TemplateOptions.module.css
+++ b/src/containers/TemplateOptions/TemplateOptions.module.css
@@ -23,16 +23,16 @@
gap: 0.5rem;
}
-.CallToActionWrapper>div {
+.CallToActionWrapper > div {
display: flex;
justify-content: space-between;
}
-.CallToActionWrapper>div>div:last-child {
+.CallToActionWrapper > div > div:last-child {
cursor: pointer;
}
-.CallToActionWrapper>div:nth-of-type(3)>div {
+.CallToActionWrapper > div:nth-of-type(3) > div {
margin-bottom: 0px;
}
@@ -42,7 +42,7 @@
align-items: center;
}
-.QuickReplyWrapper>div {
+.QuickReplyWrapper > div {
margin: 0px 4px 17px 4px;
cursor: pointer;
}
@@ -52,7 +52,7 @@
margin-bottom: 0.5rem;
}
-.RadioLabel>span:last-child {
+.RadioLabel > span:last-child {
line-height: 1 !important;
font-weight: 500 !important;
font-size: 16px !important;
@@ -64,7 +64,7 @@
display: block !important;
}
-.FormControl>p {
+.FormControl > p {
margin-left: 12px;
}
@@ -107,7 +107,7 @@
cursor: pointer;
}
-.RadioGroup>label>span:first-child {
+.RadioGroup > label > span:first-child {
padding: 0px 8px !important;
}
@@ -152,4 +152,4 @@
color: #d32f2f;
margin: 0;
font-size: 0.75rem;
-}
\ No newline at end of file
+}
diff --git a/src/containers/Ticket/Ticket.module.css b/src/containers/Ticket/Ticket.module.css
index 5f7b376d08..daee17ba4b 100644
--- a/src/containers/Ticket/Ticket.module.css
+++ b/src/containers/Ticket/Ticket.module.css
@@ -19,4 +19,4 @@
color: #073f24 !important;
width: 29px;
height: 29px;
-}
\ No newline at end of file
+}
diff --git a/src/containers/Ticket/TicketList/ExportTicket/ExportTicket.module.css b/src/containers/Ticket/TicketList/ExportTicket/ExportTicket.module.css
index 015c4f5738..2d6136add3 100644
--- a/src/containers/Ticket/TicketList/ExportTicket/ExportTicket.module.css
+++ b/src/containers/Ticket/TicketList/ExportTicket/ExportTicket.module.css
@@ -28,4 +28,4 @@
.FieldContainer {
padding-right: 8px;
width: 50%;
-}
\ No newline at end of file
+}
diff --git a/src/containers/WhatsAppForms/Configure/Configure.module.css b/src/containers/WhatsAppForms/Configure/Configure.module.css
index f3f5310246..f5a1eb9240 100644
--- a/src/containers/WhatsAppForms/Configure/Configure.module.css
+++ b/src/containers/WhatsAppForms/Configure/Configure.module.css
@@ -63,8 +63,6 @@
justify-content: center;
}
-
-
.PublishedBadge {
background-color: #d1fae5;
color: #065f46;
@@ -97,4 +95,4 @@
width: 100%;
height: 600px;
}
-}
\ No newline at end of file
+}
diff --git a/src/containers/WhatsAppForms/Configure/Configure.test.tsx b/src/containers/WhatsAppForms/Configure/Configure.test.tsx
index 3dd076c355..849d250144 100644
--- a/src/containers/WhatsAppForms/Configure/Configure.test.tsx
+++ b/src/containers/WhatsAppForms/Configure/Configure.test.tsx
@@ -880,10 +880,7 @@ describe('
', () => {
});
});
- test('it generates a random ID for screens with non-alpha names', async () => {
- // Make randomAlphaId deterministic: always returns 'aaaaaa'
- vi.spyOn(Math, 'random').mockReturnValue(0);
-
+ test('it preserves original screen ID even when name changes to non-alpha', async () => {
render(wrapper());
await waitFor(() => {
@@ -897,10 +894,9 @@ describe('
', () => {
await waitFor(() => {
const jsonText = screen.getByTestId('json-preview') as HTMLTextAreaElement;
const parsed = JSON.parse(jsonText.value);
- expect(parsed.screens[0].id).toBe('screen_aaaaaa');
+ // Screen was imported from flow JSON with id 'screen', so it preserves that ID
+ expect(parsed.screens[0].id).toBe('screen');
});
-
- vi.spyOn(Math, 'random').mockRestore();
});
test('it should create form with different types of short answers and shows them in preview', async () => {
diff --git a/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/ContentItemComponent.module.css b/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/ContentItemComponent.module.css
index 938723e554..d4cca83d68 100644
--- a/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/ContentItemComponent.module.css
+++ b/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/ContentItemComponent.module.css
@@ -52,4 +52,33 @@
.ContentBody {
padding: 20px;
-}
\ No newline at end of file
+}
+
+.UnsupportedContent {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.UnsupportedInfo {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background-color: #f0f4f8;
+ border-radius: 6px;
+ font-size: 13px;
+ color: #555;
+}
+
+.EndpointWarning {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background-color: #fff4e5;
+ border: 1px solid #ffb74d;
+ border-radius: 6px;
+ font-size: 13px;
+ color: #663c00;
+}
diff --git a/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/ContentItemComponent.tsx b/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/ContentItemComponent.tsx
index 42a159f38b..5b8b9dcd76 100644
--- a/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/ContentItemComponent.tsx
+++ b/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/ContentItemComponent.tsx
@@ -4,6 +4,7 @@ import DeleteOutlined from '@mui/icons-material/DeleteOutlined';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import WarningIcon from '@mui/icons-material/Warning';
import { IconButton } from '@mui/material';
import { ContentItem } from '../FormBuilder.types';
@@ -53,10 +54,23 @@ export const ContentItemComponent = ({
return
;
case 'Unsupported':
return (
-
- This component ({item.name}) is not editable in the form builder but will be preserved in the JSON.
+
+
+
+
+ This component ({item.name}) is view-only and will be preserved on export.
+
+
+ {(item.data.rawComponent?.type === 'PhotoPicker' || item.data.rawComponent?.type === 'DocumentPicker') && (
+
+
+ This component requires an upload endpoint to be configured.
+
+ )}
);
+ default:
+ return null;
}
};
diff --git a/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/types/ContentTypes.module.css b/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/types/ContentTypes.module.css
index ee7dbc70ca..bd204e7d6a 100644
--- a/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/types/ContentTypes.module.css
+++ b/src/containers/WhatsAppForms/Configure/FormBuilder/ContentItem/types/ContentTypes.module.css
@@ -210,4 +210,4 @@
object-fit: contain;
border-radius: 8px;
border: 1px solid #e0e0e0;
-}
\ No newline at end of file
+}
diff --git a/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.types.ts b/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.types.ts
index 0f318baed5..53fe3dd8da 100644
--- a/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.types.ts
+++ b/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.types.ts
@@ -4,6 +4,10 @@ export interface Screen {
order: number;
content: ContentItem[];
buttonLabel: string;
+ /** Original WhatsApp Flow screen ID, preserved for round-trip export */
+ flowId?: string;
+ /** Original WhatsApp Flow screen.data declarations, preserved for round-trip export */
+ flowData?: Record
;
}
export interface ContentItem {
diff --git a/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.utils.test.ts b/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.utils.test.ts
new file mode 100644
index 0000000000..e3c47f8824
--- /dev/null
+++ b/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.utils.test.ts
@@ -0,0 +1,327 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable no-template-curly-in-string */
+/* eslint-disable no-underscore-dangle */
+// eslint-disable-next-line import/no-unresolved
+import { describe, expect, test } from 'vitest';
+import {
+ convertFlowJSONToFormBuilder,
+ convertFormBuilderToFlowJSON,
+ computeFieldNames,
+ hasContentItemError,
+ validateFlowJson,
+} from './FormBuilder.utils';
+
+// ── Fixture: a realistic Flow JSON with unsupported types, custom IDs, and data ──
+
+const fixtureFlowJSON = {
+ version: '7.3',
+ screens: [
+ {
+ id: 'my_custom_screen',
+ title: 'Registration',
+ terminal: false,
+ data: {
+ custom_prop: { type: 'string', __example__: 'hello' },
+ },
+ layout: {
+ type: 'SingleColumnLayout',
+ children: [
+ {
+ type: 'Form',
+ name: 'flow_path',
+ children: [
+ { type: 'TextHeading', text: 'Welcome' },
+ {
+ type: 'TextInput',
+ name: 'user_name',
+ label: 'Name',
+ required: true,
+ 'input-type': 'text',
+ },
+ { type: 'CalendarPicker', name: 'cal_pick', label: 'Pick a date' },
+ {
+ type: 'ChipsSelector',
+ name: 'chips_sel',
+ label: 'Pick chips',
+ 'data-source': [{ id: '1', title: 'A' }],
+ },
+ { type: 'EmbeddedLink', text: 'Click here', src: 'https://example.com' },
+ { type: 'RichText', text: ['Hello ', { bold: true, text: 'World' }] },
+ {
+ type: 'If',
+ condition: '${data.flag}',
+ // biome-ignore lint/suspicious/noThenProperty: WhatsApp Flow schema requires 'then'
+ then: [{ type: 'TextBody', text: 'Yes' }],
+ else: [{ type: 'TextBody', text: 'No' }],
+ },
+ {
+ type: 'Switch',
+ value: '${data.status}',
+ cases: { open: [{ type: 'TextBody', text: 'Open' }] },
+ },
+ { type: 'PhotoPicker', name: 'photo_pick', label: 'Upload Photo' },
+ { type: 'DocumentPicker', name: 'doc_pick', label: 'Upload Doc' },
+ {
+ type: 'Footer',
+ label: '${data.custom_prop}',
+ 'on-click-action': {
+ name: 'navigate',
+ next: { name: 'final_screen', type: 'screen' },
+ payload: {},
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ id: 'final_screen',
+ title: 'Thank You',
+ terminal: true,
+ data: {},
+ layout: {
+ type: 'SingleColumnLayout',
+ children: [
+ {
+ type: 'Form',
+ name: 'flow_path',
+ children: [
+ { type: 'TextBody', text: 'Thanks!' },
+ {
+ type: 'Footer',
+ label: 'Done',
+ 'on-click-action': { name: 'complete', payload: {} },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ ],
+};
+
+// ── Tests ──
+
+describe('convertFlowJSONToFormBuilder + convertFormBuilderToFlowJSON round-trip', () => {
+ test('preserves screen IDs through import→export', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ const exported = convertFormBuilderToFlowJSON(screens);
+ expect(exported.screens[0].id).toBe('my_custom_screen');
+ expect(exported.screens[1].id).toBe('final_screen');
+ });
+
+ test('preserves screen.data declarations through import→export', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ const exported = convertFormBuilderToFlowJSON(screens);
+ expect(exported.screens[0].data).toHaveProperty('custom_prop');
+ expect(exported.screens[0].data.custom_prop).toEqual({
+ type: 'string',
+ __example__: 'hello',
+ });
+ });
+
+ test('preserves Footer label with dynamic expression exactly', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ const exported = convertFormBuilderToFlowJSON(screens);
+ const children = exported.screens[0].layout.children[0].children;
+ const footer = children.find((c: any) => c.type === 'Footer');
+ expect(footer.label).toBe('${data.custom_prop}');
+ });
+
+ test('stores the Footer label on the screen.buttonLabel without alteration', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ expect(screens[0].buttonLabel).toBe('${data.custom_prop}');
+ });
+});
+
+describe('Unsupported component import', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ const unsupported = screens[0].content.filter((item) => item.type === 'Unsupported');
+
+ test('imports CalendarPicker as Unsupported with raw JSON', () => {
+ const cal = unsupported.find((i) => i.name === 'CalendarPicker');
+ expect(cal).toBeDefined();
+ expect(cal!.data.rawComponent?.type).toBe('CalendarPicker');
+ expect(cal!.data.rawComponent?.name).toBe('cal_pick');
+ });
+
+ test('imports ChipsSelector as Unsupported with raw JSON', () => {
+ const chip = unsupported.find((i) => i.name === 'ChipsSelector');
+ expect(chip).toBeDefined();
+ expect(chip!.data.rawComponent?.type).toBe('ChipsSelector');
+ });
+
+ test('imports EmbeddedLink as Unsupported', () => {
+ const link = unsupported.find((i) => i.name === 'EmbeddedLink');
+ expect(link).toBeDefined();
+ expect(link!.data.rawComponent?.src).toBe('https://example.com');
+ });
+
+ test('imports RichText as Unsupported', () => {
+ const rt = unsupported.find((i) => i.name === 'RichText');
+ expect(rt).toBeDefined();
+ expect(rt!.data.rawComponent?.text).toEqual(['Hello ', { bold: true, text: 'World' }]);
+ });
+
+ test('imports If with nested children as Unsupported (preserves structure)', () => {
+ const ifComp = unsupported.find((i) => i.name === 'If');
+ expect(ifComp).toBeDefined();
+ expect(ifComp!.data.rawComponent?.then).toHaveLength(1);
+ expect(ifComp!.data.rawComponent?.else).toHaveLength(1);
+ });
+
+ test('imports Switch with nested children as Unsupported (preserves structure)', () => {
+ const sw = unsupported.find((i) => i.name === 'Switch');
+ expect(sw).toBeDefined();
+ expect(sw!.data.rawComponent?.cases?.open).toHaveLength(1);
+ });
+
+ test('imports PhotoPicker as Unsupported with raw JSON', () => {
+ const photo = unsupported.find((i) => i.name === 'PhotoPicker');
+ expect(photo).toBeDefined();
+ expect(photo!.data.rawComponent?.name).toBe('photo_pick');
+ });
+
+ test('imports DocumentPicker as Unsupported with raw JSON', () => {
+ const doc = unsupported.find((i) => i.name === 'DocumentPicker');
+ expect(doc).toBeDefined();
+ expect(doc!.data.rawComponent?.name).toBe('doc_pick');
+ });
+});
+
+describe('Unsupported component round-trip export', () => {
+ test('re-exports all 8 unsupported component types as deep-equal raw JSON', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ const exported = convertFormBuilderToFlowJSON(screens);
+ const formChildren = exported.screens[0].layout.children[0].children;
+
+ const original = fixtureFlowJSON.screens[0].layout.children[0].children;
+ const unsupportedTypes = [
+ 'CalendarPicker',
+ 'ChipsSelector',
+ 'EmbeddedLink',
+ 'RichText',
+ 'If',
+ 'Switch',
+ 'PhotoPicker',
+ 'DocumentPicker',
+ ];
+
+ unsupportedTypes.forEach((typeName) => {
+ const origComponent = original.find((c) => c.type === typeName);
+ const exportedComponent = formChildren.find((c: any) => c.type === typeName);
+ expect(exportedComponent).toEqual(origComponent);
+ });
+ });
+
+ test('does not mutate original raw component objects during round-trip', () => {
+ const inputCopy = JSON.parse(JSON.stringify(fixtureFlowJSON));
+ const screens = convertFlowJSONToFormBuilder(inputCopy);
+ convertFormBuilderToFlowJSON(screens);
+ expect(inputCopy).toEqual(fixtureFlowJSON);
+ });
+});
+
+describe('Known component round-trip', () => {
+ test('preserves known TextHeading through round-trip', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ const exported = convertFormBuilderToFlowJSON(screens);
+ const formChildren = exported.screens[0].layout.children[0].children;
+ const heading = formChildren.find((c: any) => c.type === 'TextHeading');
+ expect(heading).toBeDefined();
+ expect(heading.text).toBe('Welcome');
+ });
+
+ test('preserves known TextInput through round-trip', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ const exported = convertFormBuilderToFlowJSON(screens);
+ const formChildren = exported.screens[0].layout.children[0].children;
+ const input = formChildren.find((c: any) => c.type === 'TextInput');
+ expect(input).toBeDefined();
+ expect(input.label).toBe('Name');
+ expect(input.required).toBe(true);
+ });
+});
+
+describe('computeFieldNames', () => {
+ test('tracks unsupported components that have a name property', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ const fieldNames = computeFieldNames(screens);
+ const values = Array.from(fieldNames.values());
+ expect(values).toContain('cal_pick');
+ expect(values).toContain('chips_sel');
+ expect(values).toContain('photo_pick');
+ expect(values).toContain('doc_pick');
+ });
+});
+
+describe('hasContentItemError', () => {
+ test('returns false for Unsupported type items (no false errors)', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ const unsupported = screens[0].content.filter((item) => item.type === 'Unsupported');
+ unsupported.forEach((item) => {
+ expect(hasContentItemError(item)).toBe(false);
+ });
+ });
+});
+
+describe('validateFlowJson', () => {
+ test('validates flow JSON containing new component types without errors', () => {
+ const result = validateFlowJson(fixtureFlowJSON);
+ expect(result.errors).toHaveLength(0);
+ });
+
+ test('detects duplicate component names across new input types', () => {
+ const duplicate = JSON.parse(JSON.stringify(fixtureFlowJSON));
+ // Add a second CalendarPicker with same name on screen 2
+ duplicate.screens[1].layout.children[0].children.splice(0, 0, {
+ type: 'CalendarPicker',
+ name: 'cal_pick',
+ label: 'Dup',
+ });
+ const result = validateFlowJson(duplicate);
+ const dupError = result.errors.find((e: any) => e.message.includes("Duplicate component name 'cal_pick'"));
+ expect(dupError).toBeDefined();
+ });
+});
+
+describe('Edge cases', () => {
+ test('uses flowId for imported screens and generates IDs for new screens', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ // Add a new screen without flowId
+ screens.push({
+ id: '3',
+ name: 'New Screen',
+ order: 2,
+ content: [],
+ buttonLabel: 'Continue',
+ // no flowId — should get generated ID
+ });
+ const exported = convertFormBuilderToFlowJSON(screens);
+ expect(exported.screens[0].id).toBe('my_custom_screen');
+ expect(exported.screens[1].id).toBe('final_screen');
+ expect(exported.screens[2].id).toBe('new_screen');
+ });
+
+ test('handles empty flow JSON gracefully', () => {
+ const screens = convertFlowJSONToFormBuilder({});
+ expect(screens).toEqual([]);
+ });
+
+ test('preserves flowId and flowData on the imported Screen objects', () => {
+ const screens = convertFlowJSONToFormBuilder(fixtureFlowJSON);
+ expect(screens[0].flowId).toBe('my_custom_screen');
+ expect(screens[0].flowData).toEqual({
+ custom_prop: { type: 'string', __example__: 'hello' },
+ });
+ expect(screens[1].flowId).toBe('final_screen');
+ });
+
+ test('does not share references between flowData and original', () => {
+ const input = JSON.parse(JSON.stringify(fixtureFlowJSON));
+ const screens = convertFlowJSONToFormBuilder(input);
+ (screens[0].flowData!.custom_prop as Record).__example__ = 'modified';
+ expect(input.screens[0].data.custom_prop.__example__).toBe('hello');
+ });
+});
diff --git a/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.utils.ts b/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.utils.ts
index 1b64ebc8fa..e41114ef5d 100644
--- a/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.utils.ts
+++ b/src/containers/WhatsAppForms/Configure/FormBuilder/FormBuilder.utils.ts
@@ -100,10 +100,40 @@ export const hasFormErrors = (screens: Screen[]): boolean => {
});
};
+/** Builds screen IDs for export, preserving original flowId when available. */
+const buildScreenIds = (screens: Screen[]): string[] => {
+ const usedIds = new Set();
+ const ids: string[] = [];
+
+ // First pass: collect all preserved flowIds
+ screens.forEach((screen) => {
+ if (screen.flowId) {
+ usedIds.add(screen.flowId);
+ }
+ });
+
+ // Second pass: assign IDs
+ screens.forEach((screen) => {
+ if (screen.flowId) {
+ ids.push(screen.flowId);
+ } else {
+ let id = toSnakeCaseId(screen.name);
+ if (!id || usedIds.has(id)) {
+ const suffix = randomAlphaId();
+ id = id ? `${id}_${suffix}` : `screen_${suffix}`;
+ }
+ usedIds.add(id);
+ ids.push(id);
+ }
+ });
+
+ return ids;
+};
+
/** Converts all form builder screens into a complete WhatsApp Flow JSON structure with version and screens array. */
export const convertFormBuilderToFlowJSON = (screens: Screen[]): any => {
const totalScreens = screens.length;
- const screenIds = generateUniqueScreenIds(screens);
+ const screenIds = buildScreenIds(screens);
const fieldNameMap = computeFieldNames(screens);
const previousScreensPayloadData: Array<{
@@ -496,7 +526,10 @@ export const convertScreenToFlowJSON = (
});
}
- const screenData = generateScreenData(previousScreensPayloadData);
+ const generated = generateScreenData(previousScreensPayloadData);
+ const preserved = screen.flowData ?? {};
+ // Merge: preserved values take precedence over generated
+ const screenData = { ...generated, ...preserved };
return {
id: screenId,
@@ -653,7 +686,7 @@ const convertWhatsAppComponentToContentItem = (component: any, order: number): C
// Unsupported component types are stored verbatim so nothing is lost on round-trip.
if (internalType.type === 'Unsupported') {
- contentItem.data = { rawComponent: component };
+ contentItem.data = { rawComponent: JSON.parse(JSON.stringify(component)) };
return contentItem;
}
@@ -763,6 +796,8 @@ export const convertFlowJSONToFormBuilder = (flowJSON: any): Screen[] => {
order: screenIndex,
content,
buttonLabel,
+ flowId: flowScreen.id,
+ flowData: flowScreen.data ? JSON.parse(JSON.stringify(flowScreen.data)) : undefined,
};
});
};
@@ -784,6 +819,10 @@ const INPUT_COMPONENT_TYPES = [
'CheckboxGroup',
'Dropdown',
'OptIn',
+ 'CalendarPicker',
+ 'ChipsSelector',
+ 'PhotoPicker',
+ 'DocumentPicker',
];
/** Validates a WhatsApp Flow JSON structure, checking screen IDs, titles, layout, actions, components, and data properties. */
diff --git a/src/containers/WhatsAppForms/Configure/FormBuilder/Screen/Screen.module.css b/src/containers/WhatsAppForms/Configure/FormBuilder/Screen/Screen.module.css
index a46a9afa4c..991bf3be71 100644
--- a/src/containers/WhatsAppForms/Configure/FormBuilder/Screen/Screen.module.css
+++ b/src/containers/WhatsAppForms/Configure/FormBuilder/Screen/Screen.module.css
@@ -142,4 +142,4 @@
margin-top: 4px;
font-size: 12px;
color: #d32f2f;
-}
\ No newline at end of file
+}
diff --git a/src/containers/WhatsAppForms/Configure/JSONViewer/JSONViewer.module.css b/src/containers/WhatsAppForms/Configure/JSONViewer/JSONViewer.module.css
index aa6c99d09c..2dce079ee4 100644
--- a/src/containers/WhatsAppForms/Configure/JSONViewer/JSONViewer.module.css
+++ b/src/containers/WhatsAppForms/Configure/JSONViewer/JSONViewer.module.css
@@ -35,7 +35,6 @@
align-items: center;
}
-
.ErrorMessage {
background-color: #ffebee;
border: 1px solid #ef9a9a;
@@ -120,4 +119,4 @@
.JsonCode::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
-}
\ No newline at end of file
+}
diff --git a/src/containers/WhatsAppForms/Configure/Variables/Variables.module.css b/src/containers/WhatsAppForms/Configure/Variables/Variables.module.css
index 173bb66b5d..0412f645fc 100644
--- a/src/containers/WhatsAppForms/Configure/Variables/Variables.module.css
+++ b/src/containers/WhatsAppForms/Configure/Variables/Variables.module.css
@@ -27,7 +27,6 @@
border-bottom: 2px solid #e0e0e0;
display: flex;
gap: 1rem;
-
}
.VariablesHeader span {
@@ -89,4 +88,4 @@
color: #0b7a3e;
overflow-x: auto;
white-space: nowrap;
-}
\ No newline at end of file
+}
diff --git a/src/containers/WhatsAppForms/WhatsAppFormList/WhatsAppFormList.module.css b/src/containers/WhatsAppForms/WhatsAppFormList/WhatsAppFormList.module.css
index b7e5868eb8..7998d87042 100644
--- a/src/containers/WhatsAppForms/WhatsAppFormList/WhatsAppFormList.module.css
+++ b/src/containers/WhatsAppForms/WhatsAppFormList/WhatsAppFormList.module.css
@@ -8,7 +8,7 @@
min-height: 36px !important;
}
-.SearchBar>fieldset {
+.SearchBar > fieldset {
border: none !important;
}
@@ -39,7 +39,6 @@
font-weight: 500;
}
-
.Status {
display: inline-block;
padding: 4px 12px;
diff --git a/src/declaration.d.ts b/src/declaration.d.ts
index f2d12bb56c..3d673e2eb1 100644
--- a/src/declaration.d.ts
+++ b/src/declaration.d.ts
@@ -1,4 +1,4 @@
-declare module "*.module.css" {
+declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
diff --git a/src/routes/AuthenticatedRoute/AuthenticatedRoute.test.tsx b/src/routes/AuthenticatedRoute/AuthenticatedRoute.test.tsx
index 6a3109febf..8338f4f6bb 100644
--- a/src/routes/AuthenticatedRoute/AuthenticatedRoute.test.tsx
+++ b/src/routes/AuthenticatedRoute/AuthenticatedRoute.test.tsx
@@ -4,7 +4,12 @@ import { BrowserRouter } from 'react-router';
import { MockedProvider } from '@apollo/client/testing';
import { vi } from 'vitest';
-import { getOrganizationBSP, OrganizationStateMock, walletBalanceQuery, walletBalanceSubscription } from 'mocks/Organization';
+import {
+ getOrganizationBSP,
+ OrganizationStateMock,
+ walletBalanceQuery,
+ walletBalanceSubscription,
+} from 'mocks/Organization';
import { setUserSession } from 'services/AuthService';
import { collectionCountQuery, CONVERSATION_MOCKS, markAsReadMock, savedSearchStatusQuery } from 'mocks/Chat';
import { Loading } from 'components/UI/Layout/Loading/Loading';