@capillarytech/creatives-library 8.0.292-alpha.11 → 8.0.292-alpha.13
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/package.json +1 -1
- package/v2Components/HtmlEditor/HTMLEditor.js +24 -16
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +34 -35
- package/v2Components/HtmlEditor/index.lazy.js +7 -7
- package/v2Containers/InApp/index.js +16 -4
- package/v2Components/HtmlEditor/components/DeviceToggle/FLOW_AND_CLICK_BEHAVIOUR.md +0 -70
package/package.json
CHANGED
|
@@ -113,6 +113,8 @@ const HTMLEditor = forwardRef(({
|
|
|
113
113
|
const mainEditorRef = useRef(null);
|
|
114
114
|
const modalEditorRef = useRef(null);
|
|
115
115
|
const [isFullscreenModalOpen, setIsFullscreenModalOpen] = useState(false);
|
|
116
|
+
// InApp only: last initialContent value we applied (by value) to avoid reverting user edits on parent re-render
|
|
117
|
+
const inAppLastAppliedInitialRef = useRef({ android: '', ios: '' });
|
|
116
118
|
|
|
117
119
|
// Get the currently active editor ref based on fullscreen state
|
|
118
120
|
const getActiveEditorRef = useCallback(() => isFullscreenModalOpen ? modalEditorRef : mainEditorRef, [isFullscreenModalOpen]);
|
|
@@ -174,19 +176,22 @@ const HTMLEditor = forwardRef(({
|
|
|
174
176
|
const contentToUpdate = typeof initialContent === 'string'
|
|
175
177
|
? { [DEVICE_TYPES.ANDROID]: initialContent, [DEVICE_TYPES.IOS]: initialContent }
|
|
176
178
|
: initialContent;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
const newAndroid = contentToUpdate[DEVICE_TYPES.ANDROID] ?? '';
|
|
180
|
+
const newIos = contentToUpdate[DEVICE_TYPES.IOS] ?? '';
|
|
181
|
+
const last = inAppLastAppliedInitialRef.current;
|
|
182
|
+
// InApp: only sync when initialContent value actually changed (first load or template switch)
|
|
183
|
+
// so we don't revert user typing when parent re-renders with same content (new ref)
|
|
184
|
+
const valueChanged = newAndroid !== (last.android ?? '') || newIos !== (last.ios ?? '');
|
|
185
|
+
if (!valueChanged) return;
|
|
186
|
+
if (!inAppContent.updateContent) return;
|
|
187
|
+
const activeDevice = inAppContent.activeDevice;
|
|
188
|
+
const newContent = activeDevice in contentToUpdate
|
|
189
|
+
? (contentToUpdate[activeDevice] ?? '')
|
|
190
|
+
: (contentToUpdate[DEVICE_TYPES.ANDROID] ?? '');
|
|
191
|
+
inAppContent.updateContent(newContent, true);
|
|
192
|
+
inAppLastAppliedInitialRef.current = { android: newAndroid, ios: newIos };
|
|
188
193
|
}
|
|
189
|
-
}, [initialContent, isEmailVariant, isInAppVariant]);
|
|
194
|
+
}, [initialContent, isEmailVariant, isInAppVariant, emailContent, inAppContent]);
|
|
190
195
|
// Handle context change for tag API calls
|
|
191
196
|
// If variant is INAPP, use SMS layout; otherwise use the channel (EMAIL)
|
|
192
197
|
const handleContextChange = useCallback((contextData) => {
|
|
@@ -300,10 +305,13 @@ const HTMLEditor = forwardRef(({
|
|
|
300
305
|
apiValidationErrors, // Pass API validation errors to merge with client-side validation
|
|
301
306
|
}, formatSanitizerMessage, formatValidatorMessage);
|
|
302
307
|
|
|
303
|
-
// Expose validation and content state via ref
|
|
308
|
+
// Expose validation and content state via ref (InApp: also expose getDeviceContent for Test and Preview)
|
|
304
309
|
useImperativeHandle(ref, () => ({
|
|
305
310
|
getValidation: () => validation,
|
|
306
311
|
getContent: () => currentContent,
|
|
312
|
+
getDeviceContent: variant === HTML_EDITOR_VARIANTS.INAPP && typeof getDeviceContent === 'function'
|
|
313
|
+
? getDeviceContent
|
|
314
|
+
: undefined,
|
|
307
315
|
isContentEmpty: () => !currentContent || currentContent.trim() === '',
|
|
308
316
|
getIssueCounts: () => {
|
|
309
317
|
if (!validation || typeof validation.getAllIssues !== 'function') {
|
|
@@ -319,7 +327,7 @@ const HTMLEditor = forwardRef(({
|
|
|
319
327
|
: countIssuesBySeverity(validation.getAllIssues()),
|
|
320
328
|
})
|
|
321
329
|
,
|
|
322
|
-
}), [validation, currentContent, apiValidationErrors]);
|
|
330
|
+
}), [validation, currentContent, apiValidationErrors, variant, getDeviceContent]);
|
|
323
331
|
|
|
324
332
|
// Use ref to store callback to avoid infinite loops (callback in deps would cause re-runs)
|
|
325
333
|
const onValidationChangeRef = useRef(onValidationChange);
|
|
@@ -827,5 +835,5 @@ HTMLEditor.defaultProps = {
|
|
|
827
835
|
apiValidationErrors: null, // API validation errors from validateLiquidTemplateContent
|
|
828
836
|
};
|
|
829
837
|
|
|
830
|
-
// Export with injectIntl -
|
|
831
|
-
export default injectIntl(HTMLEditor);
|
|
838
|
+
// Export with injectIntl - forwardRef so parent (e.g. InApp) can use ref for getDeviceContent (Test and Preview)
|
|
839
|
+
export default injectIntl(HTMLEditor, { forwardRef: true });
|
|
@@ -95,52 +95,51 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
95
95
|
const previousContentRef = useRef({ android: '', ios: '' });
|
|
96
96
|
|
|
97
97
|
// Update content when initialContent prop changes (for edit mode)
|
|
98
|
-
//
|
|
98
|
+
// Only update when: first load, empty->content transition, or template switch (value changed).
|
|
99
|
+
// Value-based comparison prevents reverting user edits when parent re-renders with same content (new ref).
|
|
99
100
|
useEffect(() => {
|
|
100
101
|
const newAndroidContent = initialContent?.[ANDROID];
|
|
101
102
|
const newIosContent = initialContent?.[IOS];
|
|
103
|
+
const prevRef = previousContentRef.current;
|
|
102
104
|
|
|
103
|
-
// Check if this is meaningful initialContent (has actual content)
|
|
104
105
|
const hasMeaningfulContent = (newAndroidContent && newAndroidContent.trim() !== '')
|
|
105
106
|
|| (newIosContent && newIosContent.trim() !== '');
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
&& (!previousContentRef.current.ios || previousContentRef.current.ios.trim() === '');
|
|
108
|
+
const wasEmpty = (!prevRef.android || prevRef.android.trim() === '')
|
|
109
|
+
&& (!prevRef.ios || prevRef.ios.trim() === '');
|
|
110
110
|
const isTransitioningToContent = wasEmpty && hasMeaningfulContent;
|
|
111
111
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// This prevents the effect from overriding user edits during active editing
|
|
116
|
-
// while still allowing content to load properly in library mode
|
|
117
|
-
if (!initialContentLoadedRef.current || isTransitioningToContent) {
|
|
118
|
-
if (hasMeaningfulContent) {
|
|
119
|
-
// Mark as loaded to prevent future updates from overriding user edits
|
|
120
|
-
initialContentLoadedRef.current = true;
|
|
121
|
-
|
|
122
|
-
setDeviceContent((prev) => {
|
|
123
|
-
let updated = false;
|
|
124
|
-
const updatedContent = { ...prev };
|
|
125
|
-
|
|
126
|
-
if (newAndroidContent !== undefined && newAndroidContent !== prev[ANDROID]) {
|
|
127
|
-
updatedContent[ANDROID] = newAndroidContent;
|
|
128
|
-
updated = true;
|
|
129
|
-
}
|
|
130
|
-
if (newIosContent !== undefined && newIosContent !== prev[IOS]) {
|
|
131
|
-
updatedContent[IOS] = newIosContent;
|
|
132
|
-
updated = true;
|
|
133
|
-
}
|
|
112
|
+
// Value-based: did initialContent content actually change? (template switch)
|
|
113
|
+
const initialValueChanged = (newAndroidContent || '') !== (prevRef.android || '')
|
|
114
|
+
|| (newIosContent || '') !== (prevRef.ios || '');
|
|
134
115
|
|
|
135
|
-
|
|
136
|
-
|
|
116
|
+
const shouldSync = !initialContentLoadedRef.current
|
|
117
|
+
|| isTransitioningToContent
|
|
118
|
+
|| initialValueChanged;
|
|
137
119
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
120
|
+
if (shouldSync && hasMeaningfulContent) {
|
|
121
|
+
initialContentLoadedRef.current = true;
|
|
122
|
+
|
|
123
|
+
setDeviceContent((prev) => {
|
|
124
|
+
let updated = false;
|
|
125
|
+
const updatedContent = { ...prev };
|
|
126
|
+
|
|
127
|
+
if (newAndroidContent !== undefined && newAndroidContent !== prev[ANDROID]) {
|
|
128
|
+
updatedContent[ANDROID] = newAndroidContent;
|
|
129
|
+
updated = true;
|
|
130
|
+
}
|
|
131
|
+
if (newIosContent !== undefined && newIosContent !== prev[IOS]) {
|
|
132
|
+
updatedContent[IOS] = newIosContent;
|
|
133
|
+
updated = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return updated ? updatedContent : prev;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
previousContentRef.current = {
|
|
140
|
+
android: newAndroidContent || '',
|
|
141
|
+
ios: newIosContent || '',
|
|
142
|
+
};
|
|
144
143
|
}
|
|
145
144
|
}, [initialContent, ANDROID, IOS]);
|
|
146
145
|
|
|
@@ -40,14 +40,14 @@ const LazyHTMLEditor = React.lazy(() =>
|
|
|
40
40
|
*
|
|
41
41
|
* This component wraps the HTMLEditor with React.lazy() and Suspense
|
|
42
42
|
* to enable code splitting and reduce initial bundle size.
|
|
43
|
+
* Forwards ref so parent (e.g. InApp) can call getDeviceContent for Test and Preview.
|
|
43
44
|
*/
|
|
44
|
-
const HTMLEditorLazy = (props) =>
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
45
|
+
const HTMLEditorLazy = React.forwardRef((props, ref) => (
|
|
46
|
+
<Suspense fallback={<HTMLEditorFallback />}>
|
|
47
|
+
<LazyHTMLEditor {...props} ref={ref} />
|
|
48
|
+
</Suspense>
|
|
49
|
+
));
|
|
50
|
+
HTMLEditorLazy.displayName = 'HTMLEditorLazy';
|
|
51
51
|
|
|
52
52
|
// Forward all prop types from the original component
|
|
53
53
|
HTMLEditorLazy.propTypes = {
|
|
@@ -61,7 +61,7 @@ import { validateInAppContent } from "../../utils/commonUtils";
|
|
|
61
61
|
import { hasLiquidSupportFeature } from "../../utils/common";
|
|
62
62
|
import formBuilderMessages from "../../v2Components/FormBuilder/messages";
|
|
63
63
|
import HTMLEditor from "../../v2Components/HtmlEditor";
|
|
64
|
-
import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
|
|
64
|
+
import { HTML_EDITOR_VARIANTS, DEVICE_TYPES as HTML_DEVICE_TYPES } from "../../v2Components/HtmlEditor/constants";
|
|
65
65
|
import { INAPP_EDITOR_TYPES } from "../InAppWrapper/constants";
|
|
66
66
|
import InappAdvanced from "../InappAdvance/index";
|
|
67
67
|
import { ErrorInfoNote } from "../../v2Components/ErrorInfoNote";
|
|
@@ -145,6 +145,7 @@ export const InApp = (props) => {
|
|
|
145
145
|
// Refs to store latest content before layout changes
|
|
146
146
|
const htmlContentAndroidRef = useRef(htmlContentAndroid);
|
|
147
147
|
const htmlContentIosRef = useRef(htmlContentIos);
|
|
148
|
+
const htmlEditorRef = useRef(null);
|
|
148
149
|
const [accountId, setAccountId] = useState("");
|
|
149
150
|
const [accessToken, setAccessToken] = useState("");
|
|
150
151
|
const [accountName, setAccountName] = useState("");
|
|
@@ -177,9 +178,19 @@ export const InApp = (props) => {
|
|
|
177
178
|
// Transformation to payload structure happens in prepareTestMessagePayload
|
|
178
179
|
// Reference: Based on getPreviewSection() function (lines 490-530) which prepares content for TemplatePreview
|
|
179
180
|
const getTemplateContent = useCallback(() => {
|
|
180
|
-
// For HTML template
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
// For HTML template: prefer editor ref (source of truth) so library mode preview gets latest content
|
|
182
|
+
let androidMsg = templateMessageAndroid;
|
|
183
|
+
let iosMsg = templateMessageIos;
|
|
184
|
+
if (isHTMLTemplate) {
|
|
185
|
+
const getDeviceContentFromEditor = htmlEditorRef.current?.getDeviceContent;
|
|
186
|
+
if (typeof getDeviceContentFromEditor === 'function') {
|
|
187
|
+
androidMsg = getDeviceContentFromEditor(HTML_DEVICE_TYPES.ANDROID) ?? '';
|
|
188
|
+
iosMsg = getDeviceContentFromEditor(HTML_DEVICE_TYPES.IOS) ?? '';
|
|
189
|
+
} else {
|
|
190
|
+
androidMsg = htmlContentAndroidRef.current ?? htmlContentAndroid ?? '';
|
|
191
|
+
iosMsg = htmlContentIosRef.current ?? htmlContentIos ?? '';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
183
194
|
|
|
184
195
|
// Prepare Android content
|
|
185
196
|
const androidMediaPreview = {};
|
|
@@ -1323,6 +1334,7 @@ export const InApp = (props) => {
|
|
|
1323
1334
|
)}
|
|
1324
1335
|
{shouldUseHTMLEditor ? (
|
|
1325
1336
|
<HTMLEditor
|
|
1337
|
+
ref={htmlEditorRef}
|
|
1326
1338
|
key={`inapp-html-editor-v${htmlEditorContentVersion}`}
|
|
1327
1339
|
variant={HTML_EDITOR_VARIANTS.INAPP}
|
|
1328
1340
|
layoutType={templateLayoutType}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# DeviceToggle – Click flow and when clicks are blocked
|
|
2
|
-
|
|
3
|
-
## 1. Click flow (button → end)
|
|
4
|
-
|
|
5
|
-
1. **User clicks Android or iOS tab**
|
|
6
|
-
- `CapButton` receives the click and runs
|
|
7
|
-
- `onClick={() => handleDeviceClick(DEVICE_TYPES.ANDROID)}` or
|
|
8
|
-
- `onClick={() => handleDeviceClick(DEVICE_TYPES.IOS)}`.
|
|
9
|
-
|
|
10
|
-
2. **`handleDeviceClick(device)`** (in `DeviceToggle/index.js`):
|
|
11
|
-
- Only does something if **both**:
|
|
12
|
-
- `onDeviceChange` is truthy (passed from parent), and
|
|
13
|
-
- `device !== activeDevice` (clicked tab is not already active).
|
|
14
|
-
- If both hold: calls `onDeviceChange(device)`.
|
|
15
|
-
|
|
16
|
-
3. **Parent wiring** (`HTMLEditor.js`):
|
|
17
|
-
- `onDeviceChange={switchDevice}`
|
|
18
|
-
- So `onDeviceChange` is the `switchDevice` from `useInAppContent`.
|
|
19
|
-
|
|
20
|
-
4. **`switchDevice(device)`** (`hooks/useInAppContent.js`):
|
|
21
|
-
- `validDevices = [ANDROID, IOS]` (from `DEVICE_TYPES`: `'android'`, `'ios'`).
|
|
22
|
-
- If `device !== activeDevice` and `validDevices.includes(device)` → `setActiveDevice(device)`.
|
|
23
|
-
- If `device` is invalid → logs a warning and does nothing.
|
|
24
|
-
|
|
25
|
-
5. **Result**
|
|
26
|
-
- `activeDevice` updates → DeviceToggle and preview re-render with the new device; no explicit “block” inside this flow.
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## 2. When the click is effectively “blocked” or does nothing
|
|
31
|
-
|
|
32
|
-
- **No handler**
|
|
33
|
-
If the parent does not pass `onDeviceChange` (or passes `undefined`/`null`), `handleDeviceClick` never calls anything. The button still receives the click; there is no visual “disabled” in DeviceToggle.
|
|
34
|
-
|
|
35
|
-
- **Same device**
|
|
36
|
-
If the user clicks the already-active tab (`device === activeDevice`), `handleDeviceClick` does nothing by design.
|
|
37
|
-
|
|
38
|
-
- **Invalid device**
|
|
39
|
-
If `device` is not `'android'` or `'ios'` (e.g. wrong constant when using the repo as a library), `switchDevice` only warns and does not update state.
|
|
40
|
-
|
|
41
|
-
- **Click never reaches the button (layout/CSS)**
|
|
42
|
-
Another element is on top of the tab and captures the click (see below). This is the likely cause when “the iOS button is not clickable in edit mode” when using the repo as a library.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## 3. Why the iOS button can be unclickable in library (edit) mode
|
|
47
|
-
|
|
48
|
-
In **library mode** (`isFullMode === false`), InApp uses a special layout:
|
|
49
|
-
|
|
50
|
-
- **Header**: flex row with **DeviceToggle** first and **EditorToolbar** second.
|
|
51
|
-
- **DeviceToggle** has `flex: 1` (takes remaining space).
|
|
52
|
-
- **EditorToolbar** gets `position: absolute` (no `top`/`right`/`left`), so it is taken out of flow.
|
|
53
|
-
|
|
54
|
-
Because the toolbar is out of flow, the only flex child in flow is the DeviceToggle, so it expands to the full width of the header. The absolutely positioned toolbar is then drawn at its “static” position, which is the **right side** of the header. So the toolbar sits **on top of the right part of the header**, which is exactly where the **iOS** tab and the “Keep content same” checkbox are. Clicks on the iOS tab are then hit by the toolbar first, so the **iOS button appears not clickable** even though the handler logic is fine.
|
|
55
|
-
|
|
56
|
-
So the problem is **not** that the button is disabled or that the click handler is blocked; it’s that **the EditorToolbar overlaps the iOS tab in library mode** and steals the click.
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## 4. Recommended fix (library mode)
|
|
61
|
-
|
|
62
|
-
Ensure the toolbar does not overlap the DeviceToggle:
|
|
63
|
-
|
|
64
|
-
- Either **remove** the library-mode override that sets `position: absolute` on the toolbar so the header stays in normal flex flow, **or**
|
|
65
|
-
- If you keep `position: absolute` for the toolbar:
|
|
66
|
-
- Give the header a **positioning context** (e.g. `position: relative`).
|
|
67
|
-
- Pin the toolbar to the right (e.g. `right: 0; top: 0`).
|
|
68
|
-
- Reserve space for the toolbar so DeviceToggle doesn’t extend under it (e.g. `padding-right` on the header or a fixed/min width for the toolbar and corresponding margin on the DeviceToggle).
|
|
69
|
-
|
|
70
|
-
With that, the DeviceToggle (including the iOS tab) remains fully clickable in edit/library mode.
|