@capillarytech/creatives-library 8.0.272 → 8.0.273

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.272",
4
+ "version": "8.0.273",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Utility functions for uploading images from URLs
3
- *
3
+ *
4
4
  * NOTE: CORS-limited; will be replaced with backend implementation.
5
5
  * Flow currently hidden (not removed) from frontend.
6
6
  */
@@ -13,14 +13,14 @@ import {
13
13
 
14
14
  /**
15
15
  * Fetches an image from a URL
16
- *
16
+ *
17
17
  * @param {string} url - The image URL to fetch
18
18
  * @returns {Promise<Response>} - The fetch response object
19
19
  * @throws {Error} - If the fetch fails (network/CORS error)
20
20
  */
21
21
  export const fetchImageFromUrl = async (url) => {
22
22
  const trimmedUrl = url?.trim() || '';
23
-
23
+
24
24
  if (!trimmedUrl) {
25
25
  throw new Error('URL is required');
26
26
  }
@@ -42,7 +42,7 @@ export const fetchImageFromUrl = async (url) => {
42
42
  /**
43
43
  * Helper function to upload image from URL
44
44
  * Fetches image, validates content type and size, converts to File, and uploads via uploadAsset
45
- *
45
+ *
46
46
  * @param {string} url - The image URL to upload
47
47
  * @param {Function} formatMessage - React Intl formatMessage function
48
48
  * @param {Object} messages - React Intl messages object
@@ -51,7 +51,7 @@ export const fetchImageFromUrl = async (url) => {
51
51
  * @param {number} maxSize - Maximum file size in bytes
52
52
  * @param {string[]} allowedContentTypes - Array of allowed MIME types (defaults to DEFAULT_ALLOWED_CONTENT_TYPES)
53
53
  * @returns {Promise<{success: boolean, error: string}>} - Result object with success status and error message
54
- *
54
+ *
55
55
  * @example
56
56
  * const result = await uploadImageFromUrlHelper(
57
57
  * 'https://example.com/image.jpg',
@@ -73,14 +73,14 @@ export const uploadImageFromUrlHelper = async (
73
73
  allowedContentTypes = DEFAULT_ALLOWED_CONTENT_TYPES,
74
74
  ) => {
75
75
  const trimmedUrl = url?.trim() || '';
76
-
76
+
77
77
  try {
78
78
  const response = await fetchImageFromUrl(trimmedUrl);
79
79
 
80
80
  // Validate Content-Type
81
81
  const contentType = response.headers?.get('Content-Type') || '';
82
82
  const normalizedContentType = contentType.split(';')[0].toLowerCase().trim();
83
-
83
+
84
84
  if (!allowedContentTypes.includes(normalizedContentType)) {
85
85
  return {
86
86
  success: false,
@@ -89,7 +89,7 @@ export const uploadImageFromUrlHelper = async (
89
89
  }
90
90
 
91
91
  const blob = await response.blob();
92
-
92
+
93
93
  if (blob.size > maxSize) {
94
94
  return {
95
95
  success: false,
@@ -101,7 +101,7 @@ export const uploadImageFromUrlHelper = async (
101
101
  return new Promise((resolve) => {
102
102
  const img = new Image();
103
103
  const objectUrl = URL.createObjectURL(blob);
104
-
104
+
105
105
  img.onload = () => {
106
106
  const extension = MIME_TYPE_TO_EXTENSION[normalizedContentType] || DEFAULT_IMAGE_EXTENSION;
107
107
  const fileName = `${fileNamePrefix}.${extension}`;
@@ -111,16 +111,16 @@ export const uploadImageFromUrlHelper = async (
111
111
  height: img.height,
112
112
  error: false,
113
113
  };
114
-
114
+
115
115
  uploadAssetFn(file, 'image', fileParams);
116
116
  URL.revokeObjectURL(objectUrl);
117
-
117
+
118
118
  resolve({
119
119
  success: true,
120
120
  error: '',
121
121
  });
122
122
  };
123
-
123
+
124
124
  img.onerror = () => {
125
125
  URL.revokeObjectURL(objectUrl);
126
126
  resolve({
@@ -128,7 +128,7 @@ export const uploadImageFromUrlHelper = async (
128
128
  error: formatMessage(messages.imageLoadError),
129
129
  });
130
130
  };
131
-
131
+
132
132
  img.src = objectUrl;
133
133
  });
134
134
  } catch (error) {
@@ -138,4 +138,3 @@ export const uploadImageFromUrlHelper = async (
138
138
  };
139
139
  }
140
140
  };
141
-
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Tests for imageUrlUpload utils: fetchImageFromUrl and uploadImageFromUrlHelper
3
+ */
4
+
5
+ import { fetchImageFromUrl, uploadImageFromUrlHelper } from '../imageUrlUpload';
6
+
7
+ describe('imageUrlUpload', () => {
8
+ const formatMessage = jest.fn((msg) => msg?.id ?? msg?.defaultMessage ?? 'formatted');
9
+ const messages = {
10
+ imageTypeInvalid: { id: 'imageTypeInvalid' },
11
+ imageSizeInvalid: { id: 'imageSizeInvalid' },
12
+ imageLoadError: { id: 'imageLoadError' },
13
+ };
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ formatMessage.mockImplementation((msg) => (msg && typeof msg === 'object' && msg.id) ? msg.id : 'formatted');
18
+ });
19
+
20
+ describe('fetchImageFromUrl', () => {
21
+ const validUrl = 'https://example.com/image.jpg';
22
+
23
+ it('throws when url is missing', async () => {
24
+ await expect(fetchImageFromUrl()).rejects.toThrow('URL is required');
25
+ await expect(fetchImageFromUrl(null)).rejects.toThrow('URL is required');
26
+ });
27
+
28
+ it('throws when url is empty string', async () => {
29
+ await expect(fetchImageFromUrl('')).rejects.toThrow('URL is required');
30
+ });
31
+
32
+ it('throws when url is only whitespace', async () => {
33
+ await expect(fetchImageFromUrl(' ')).rejects.toThrow('URL is required');
34
+ });
35
+
36
+ it('trims url before fetching', async () => {
37
+ const mockResponse = { ok: true, headers: new Headers() };
38
+ global.fetch = jest.fn().mockResolvedValue(mockResponse);
39
+
40
+ await fetchImageFromUrl(' https://example.com/img.png ');
41
+ expect(global.fetch).toHaveBeenCalledWith(
42
+ 'https://example.com/img.png',
43
+ expect.objectContaining({ method: 'GET', redirect: 'follow', mode: 'cors' })
44
+ );
45
+ });
46
+
47
+ it('returns response when fetch is ok', async () => {
48
+ const mockResponse = { ok: true, status: 200, headers: new Headers() };
49
+ global.fetch = jest.fn().mockResolvedValue(mockResponse);
50
+
51
+ const result = await fetchImageFromUrl(validUrl);
52
+ expect(result).toBe(mockResponse);
53
+ expect(global.fetch).toHaveBeenCalledWith(
54
+ validUrl,
55
+ expect.objectContaining({ method: 'GET', redirect: 'follow', mode: 'cors' })
56
+ );
57
+ });
58
+
59
+ it('throws when response is not ok', async () => {
60
+ const mockResponse = { ok: false, status: 404, statusText: 'Not Found' };
61
+ global.fetch = jest.fn().mockResolvedValue(mockResponse);
62
+
63
+ await expect(fetchImageFromUrl(validUrl)).rejects.toThrow(
64
+ 'Failed to fetch image: 404 Not Found'
65
+ );
66
+ });
67
+
68
+ it('throws on network or CORS error', async () => {
69
+ global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
70
+
71
+ await expect(fetchImageFromUrl(validUrl)).rejects.toThrow('Network error');
72
+ });
73
+ });
74
+
75
+ describe('uploadImageFromUrlHelper', () => {
76
+ const uploadAssetFn = jest.fn();
77
+ const fileNamePrefix = 'test-image';
78
+ const maxSize = 5000000;
79
+
80
+ it('returns error when content type is not allowed', async () => {
81
+ const response = {
82
+ ok: true,
83
+ headers: new Headers({ 'Content-Type': 'text/html' }),
84
+ blob: jest.fn().mockResolvedValue(new Blob()),
85
+ };
86
+ global.fetch = jest.fn().mockResolvedValue(response);
87
+
88
+ const result = await uploadImageFromUrlHelper(
89
+ 'https://example.com/page',
90
+ formatMessage,
91
+ messages,
92
+ uploadAssetFn,
93
+ fileNamePrefix,
94
+ maxSize,
95
+ ['image/jpeg', 'image/png']
96
+ );
97
+
98
+ expect(result).toEqual({ success: false, error: 'imageTypeInvalid' });
99
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageTypeInvalid);
100
+ expect(uploadAssetFn).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it('normalizes content type by stripping charset and trimming', async () => {
104
+ const response = {
105
+ ok: true,
106
+ headers: new Headers({ 'Content-Type': ' image/PNG; charset=utf-8 ' }),
107
+ blob: jest.fn().mockResolvedValue(new Blob(['x'], { type: 'image/png' })),
108
+ };
109
+ global.fetch = jest.fn().mockResolvedValue(response);
110
+
111
+ const OriginalImage = global.Image;
112
+ global.Image = class MockImage {
113
+ constructor() {
114
+ this.width = 10;
115
+ this.height = 10;
116
+ setTimeout(() => { if (this.onload) this.onload(); }, 0);
117
+ }
118
+
119
+ get src() { return this._src; }
120
+
121
+ set src(v) { this._src = v; }
122
+ };
123
+
124
+ const result = await uploadImageFromUrlHelper(
125
+ 'https://example.com/img.png',
126
+ formatMessage,
127
+ messages,
128
+ uploadAssetFn,
129
+ fileNamePrefix,
130
+ maxSize,
131
+ ['image/png']
132
+ );
133
+
134
+ global.Image = OriginalImage;
135
+
136
+ expect(result.success).toBe(true);
137
+ expect(uploadAssetFn).toHaveBeenCalledWith(
138
+ expect.any(File),
139
+ 'image',
140
+ expect.objectContaining({ width: 10, height: 10, error: false })
141
+ );
142
+ const file = uploadAssetFn.mock.calls[0][0];
143
+ expect(file.name).toMatch(/\.png$/);
144
+ });
145
+
146
+ it('returns error when blob size exceeds maxSize', async () => {
147
+ const largeBlob = new Blob([new ArrayBuffer(maxSize + 1)]);
148
+ const response = {
149
+ ok: true,
150
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
151
+ blob: jest.fn().mockResolvedValue(largeBlob),
152
+ };
153
+ global.fetch = jest.fn().mockResolvedValue(response);
154
+
155
+ const result = await uploadImageFromUrlHelper(
156
+ 'https://example.com/large.jpg',
157
+ formatMessage,
158
+ messages,
159
+ uploadAssetFn,
160
+ fileNamePrefix,
161
+ maxSize
162
+ );
163
+
164
+ expect(result).toEqual({ success: false, error: 'imageSizeInvalid' });
165
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageSizeInvalid);
166
+ expect(uploadAssetFn).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it('returns error when image fails to load (invalid image data)', async () => {
170
+ const blob = new Blob(['not-an-image'], { type: 'image/jpeg' });
171
+ const response = {
172
+ ok: true,
173
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
174
+ blob: jest.fn().mockResolvedValue(blob),
175
+ };
176
+ global.fetch = jest.fn().mockResolvedValue(response);
177
+
178
+ const OriginalImage = global.Image;
179
+ global.Image = class MockImage {
180
+ constructor() {
181
+ setTimeout(() => {
182
+ if (this.onerror) this.onerror(new Event('error'));
183
+ }, 0);
184
+ }
185
+
186
+ get src() { return this._src; }
187
+
188
+ set src(v) { this._src = v; }
189
+ };
190
+
191
+ const result = await uploadImageFromUrlHelper(
192
+ 'https://example.com/bad.jpg',
193
+ formatMessage,
194
+ messages,
195
+ uploadAssetFn,
196
+ fileNamePrefix,
197
+ maxSize
198
+ );
199
+
200
+ global.Image = OriginalImage;
201
+
202
+ expect(result.success).toBe(false);
203
+ expect(result.error).toBe('imageLoadError');
204
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageLoadError);
205
+ expect(uploadAssetFn).not.toHaveBeenCalled();
206
+ });
207
+
208
+ it('returns error on fetch failure and uses imageLoadError message', async () => {
209
+ global.fetch = jest.fn().mockRejectedValue(new Error('CORS'));
210
+
211
+ const result = await uploadImageFromUrlHelper(
212
+ 'https://example.com/image.jpg',
213
+ formatMessage,
214
+ messages,
215
+ uploadAssetFn,
216
+ fileNamePrefix,
217
+ maxSize
218
+ );
219
+
220
+ expect(result).toEqual({ success: false, error: 'imageLoadError' });
221
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageLoadError);
222
+ expect(uploadAssetFn).not.toHaveBeenCalled();
223
+ });
224
+
225
+ it('trims url before processing', async () => {
226
+ const response = {
227
+ ok: true,
228
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
229
+ blob: jest.fn().mockResolvedValue(new Blob()),
230
+ };
231
+ global.fetch = jest.fn().mockResolvedValue(response);
232
+
233
+ await uploadImageFromUrlHelper(
234
+ ' https://example.com/img.jpg ',
235
+ formatMessage,
236
+ messages,
237
+ uploadAssetFn,
238
+ fileNamePrefix,
239
+ maxSize
240
+ );
241
+
242
+ expect(global.fetch).toHaveBeenCalledWith(
243
+ 'https://example.com/img.jpg',
244
+ expect.any(Object)
245
+ );
246
+ });
247
+
248
+ it('calls uploadAssetFn with file, type and fileParams on success', async () => {
249
+ const blob = new Blob([new ArrayBuffer(100)], { type: 'image/png' });
250
+ const response = {
251
+ ok: true,
252
+ headers: new Headers({ 'Content-Type': 'image/png' }),
253
+ blob: jest.fn().mockResolvedValue(blob),
254
+ };
255
+ global.fetch = jest.fn().mockResolvedValue(response);
256
+
257
+ const OriginalImage = global.Image;
258
+ global.Image = class MockImage {
259
+ constructor() {
260
+ this.width = 100;
261
+ this.height = 80;
262
+ setTimeout(() => {
263
+ if (this.onload) this.onload();
264
+ }, 0);
265
+ }
266
+
267
+ get src() { return this._src; }
268
+
269
+ set src(v) { this._src = v; }
270
+ };
271
+
272
+ const result = await uploadImageFromUrlHelper(
273
+ 'https://example.com/valid.png',
274
+ formatMessage,
275
+ messages,
276
+ uploadAssetFn,
277
+ fileNamePrefix,
278
+ maxSize
279
+ );
280
+
281
+ global.Image = OriginalImage;
282
+
283
+ expect(result).toEqual({ success: true, error: '' });
284
+ expect(uploadAssetFn).toHaveBeenCalledTimes(1);
285
+ const [file, type, fileParams] = uploadAssetFn.mock.calls[0];
286
+ expect(file).toBeInstanceOf(File);
287
+ expect(file.name).toMatch(new RegExp(`^${fileNamePrefix}\\.png$`));
288
+ expect(type).toBe('image');
289
+ expect(fileParams).toEqual(
290
+ expect.objectContaining({
291
+ width: 100,
292
+ height: 80,
293
+ error: false,
294
+ })
295
+ );
296
+ });
297
+ });
298
+ });
@@ -1897,6 +1897,117 @@ describe('EmailHTMLEditor', () => {
1897
1897
  });
1898
1898
  });
1899
1899
 
1900
+ describe('Template content extraction (lines 291-309)', () => {
1901
+ it('extracts content and subject from templateDataProp.base branch', async () => {
1902
+ const ref = React.createRef();
1903
+ const templateData = {
1904
+ base: {
1905
+ 'template-content': '<p>Base branch content</p>',
1906
+ "subject": 'Base branch subject',
1907
+ },
1908
+ };
1909
+ render(
1910
+ <IntlProvider locale="en" messages={{}}>
1911
+ <EmailHTMLEditor
1912
+ {...defaultProps}
1913
+ ref={ref}
1914
+ params={{ id: '123' }}
1915
+ templateData={templateData}
1916
+ Email={{
1917
+ templateDetails: { _id: '123' },
1918
+ getTemplateDetailsInProgress: false,
1919
+ fetchingCmsData: false,
1920
+ }}
1921
+ />
1922
+ </IntlProvider>
1923
+ );
1924
+ await waitFor(() => {
1925
+ expect(ref.current).toBeTruthy();
1926
+ }, { timeout: 3000 });
1927
+ await waitFor(() => {
1928
+ const content = ref.current.getContentForPreview();
1929
+ const formData = ref.current.getFormDataForPreview();
1930
+ expect(content).toBe('<p>Base branch content</p>');
1931
+ expect(formData['template-subject']).toBe('Base branch subject');
1932
+ }, { timeout: 3000 });
1933
+ });
1934
+
1935
+ it('extracts content and subject from templateDataProp.versions.base branch with activeTab', async () => {
1936
+ const ref = React.createRef();
1937
+ const templateData = {
1938
+ versions: {
1939
+ base: {
1940
+ activeTab: 'en',
1941
+ en: {
1942
+ 'template-content': '<p>Versions base en content</p>',
1943
+ "html_content": '<p>fallback html_content</p>',
1944
+ },
1945
+ subject: 'Versions base subject',
1946
+ emailSubject: 'Fallback email subject',
1947
+ },
1948
+ },
1949
+ };
1950
+ render(
1951
+ <IntlProvider locale="en" messages={{}}>
1952
+ <EmailHTMLEditor
1953
+ {...defaultProps}
1954
+ ref={ref}
1955
+ params={{ id: '123' }}
1956
+ templateData={templateData}
1957
+ Email={{
1958
+ templateDetails: { _id: '123' },
1959
+ getTemplateDetailsInProgress: false,
1960
+ fetchingCmsData: false,
1961
+ }}
1962
+ />
1963
+ </IntlProvider>
1964
+ );
1965
+ await waitFor(() => {
1966
+ expect(ref.current).toBeTruthy();
1967
+ }, { timeout: 3000 });
1968
+ await waitFor(() => {
1969
+ const content = ref.current.getContentForPreview();
1970
+ const formData = ref.current.getFormDataForPreview();
1971
+ expect(content).toBe('<p>Versions base en content</p>');
1972
+ expect(formData['template-subject']).toBe('Versions base subject');
1973
+ }, { timeout: 3000 });
1974
+ });
1975
+
1976
+ it('extracts content and subject from flat templateDataProp (else branch)', async () => {
1977
+ const ref = React.createRef();
1978
+ const templateData = {
1979
+ 'template-content': '<p>Flat content</p>',
1980
+ "emailSubject": 'Flat email subject',
1981
+ "html_content": '<p>flat html_content</p>',
1982
+ "subject": 'Flat subject',
1983
+ };
1984
+ render(
1985
+ <IntlProvider locale="en" messages={{}}>
1986
+ <EmailHTMLEditor
1987
+ {...defaultProps}
1988
+ ref={ref}
1989
+ params={{ id: '123' }}
1990
+ templateData={templateData}
1991
+ Email={{
1992
+ templateDetails: { _id: '123' },
1993
+ getTemplateDetailsInProgress: false,
1994
+ fetchingCmsData: false,
1995
+ }}
1996
+ />
1997
+ </IntlProvider>
1998
+ );
1999
+ await waitFor(() => {
2000
+ expect(ref.current).toBeTruthy();
2001
+ }, { timeout: 3000 });
2002
+ await waitFor(() => {
2003
+ const content = ref.current.getContentForPreview();
2004
+ const formData = ref.current.getFormDataForPreview();
2005
+ expect(content).toBe('<p>Flat content</p>');
2006
+ expect(formData['template-subject']).toBe('Flat email subject');
2007
+ }, { timeout: 3000 });
2008
+ });
2009
+ });
2010
+
1900
2011
  describe('setIsLoadingContent callback', () => {
1901
2012
  it('should call setIsLoadingContent when uploaded content is available', async () => {
1902
2013
  const setIsLoadingContent = jest.fn();
@@ -1933,6 +2044,37 @@ describe('EmailHTMLEditor', () => {
1933
2044
  });
1934
2045
  });
1935
2046
 
2047
+ describe('location.query and tagList (lines 127, 129, 185)', () => {
2048
+ it('should handle missing location.query (destructure from empty object)', () => {
2049
+ renderWithIntl({
2050
+ location: {},
2051
+ supportedTags: [],
2052
+ metaEntities: { tags: { standard: [{ name: 'standard.tag' }] } },
2053
+ });
2054
+ expect(screen.getByTestId('html-editor')).toBeInTheDocument();
2055
+ });
2056
+
2057
+ it('should handle null location (query defaults to {})', () => {
2058
+ renderWithIntl({
2059
+ location: null,
2060
+ supportedTags: [],
2061
+ metaEntities: { tags: { standard: [] } },
2062
+ });
2063
+ expect(screen.getByTestId('html-editor')).toBeInTheDocument();
2064
+ });
2065
+
2066
+ it('should use tagList from supportedTags when type=embedded, module=library and no getDefaultTags (line 129)', () => {
2067
+ const supportedTags = [{ name: 'custom.a' }, { name: 'custom.b' }];
2068
+ renderWithIntl({
2069
+ location: { query: { type: 'embedded', module: 'library' } },
2070
+ getDefaultTags: null,
2071
+ supportedTags,
2072
+ metaEntities: { tags: { standard: [{ name: 'standard.only' }] } },
2073
+ });
2074
+ expect(screen.getByTestId('html-editor')).toBeInTheDocument();
2075
+ });
2076
+ });
2077
+
1936
2078
  describe('tags useMemo (lines 125-132)', () => {
1937
2079
  it('should use supportedTags when in EMBEDDED mode with LIBRARY module and no getDefaultTags', () => {
1938
2080
  const supportedTags = [{ name: 'custom.tag1' }, { name: 'custom.tag2' }];
@@ -2050,7 +2192,7 @@ describe('EmailHTMLEditor', () => {
2050
2192
  expect(formData).toBeDefined();
2051
2193
  expect(formData['0']).toBeDefined();
2052
2194
  expect(formData['0'].fr).toBeDefined(); // Uses base_language 'fr'
2053
- expect(formData['0'].fr['is_drag_drop']).toBe(false);
2195
+ expect(formData['0'].fr.is_drag_drop).toBe(false);
2054
2196
  expect(formData['0'].activeTab).toBe('fr');
2055
2197
  expect(formData['0'].selectedLanguages).toEqual(['fr']);
2056
2198
  expect(formData['0'].base).toBe(true);
@@ -114,7 +114,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
114
114
  if (nextProps.iosCtasData && isEmpty(this.state.templateCta)) {
115
115
  this.setState({showIosCtaTable: true});
116
116
  }
117
- if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && isEmpty(this.state.schema)) {
117
+ if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && isEmpty(this.state.schema) && isEmpty(this.state.formData)) {
118
118
  const schema = this.props.params.mode === "text" ? nextProps.metaEntities.layouts[0].definition.textSchema : nextProps.metaEntities.layouts[0].definition.imageSchema;
119
119
  const isAndroidSupported = get(this, "props.Templates.selectedWeChatAccount.configs.android") === '1';
120
120
  const isIosSupported = get(this, "props.Templates.selectedWeChatAccount.configs.ios") === '1';
@@ -167,8 +167,8 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
167
167
  };
168
168
  this.props.actions.getMobilepushTemplatesList('mobilepush', params);
169
169
  }
170
- if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema)) {
171
- this.setState({fullSchema: nextProps.metaEntities.layouts[0].definition, schema: (nextProps.location.query.module === 'loyalty') ? nextProps.metaEntities.layouts[0].definition.textSchema : {}}, () => {
170
+ if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema) && _.isEmpty(this.state.formData)) {
171
+ this.setState({ fullSchema: nextProps.metaEntities.layouts[0].definition, schema: (nextProps.location.query.module === 'loyalty') ? nextProps.metaEntities.layouts[0].definition.textSchema : {} }, () => {
172
172
  this.handleEditSchemaOnPropsChange(nextProps, selectedWeChatAccount);
173
173
  const templateId = get(this, "props.params.id");
174
174
  if (nextProps.location.query.module !== 'loyalty' && templateId && templateId !== 'temp') {
@@ -388,11 +388,11 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
388
388
  }
389
389
  });
390
390
  } else {
391
- selectedAccount = accounts[0];
391
+ selectedAccount = accounts?.[0];
392
392
  formData['mobilepush-accounts'] = selectedAccount?.id;
393
393
  }
394
394
  this.props.actions.setWeChatAccount(selectedAccount);
395
- this.setState({formData, accountsOptions: accounts.map((acc) => ({key: acc.id, label: acc.name, value: acc.id}))});
395
+ this.setState({ formData, accountsOptions: accounts?.map((acc) => ({ key: acc.id, label: acc.name, value: acc.id })) });
396
396
  };
397
397
 
398
398
  createDefinition = (account) => ({
@@ -2018,12 +2018,16 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
2018
2018
  if (this.props.supportedTags) {
2019
2019
  tags = this.props.supportedTags;
2020
2020
  }
2021
+ if (this.props.supportedTags) {
2022
+ tags = this.props.supportedTags;
2023
+ }
2021
2024
  return (
2022
2025
  <div className="creatives-mobilepush-edit mobilepush-wrapper">
2023
2026
  <CapSpin spinning={spinning}>
2024
2027
  <CapRow>
2025
2028
  <CapColumn>
2026
- {!this.props.isLoadingMetaEntities && <FormBuilder
2029
+ <FormBuilder
2030
+ key={!_.isEmpty(schema) ? 'has-schema' : 'no-schema'}
2027
2031
  channel={MOBILE_PUSH}
2028
2032
  schema={schema}
2029
2033
  showLiquidErrorInFooter={this.props.showLiquidErrorInFooter}
@@ -2058,7 +2062,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
2058
2062
  hideTestAndPreviewBtn={this.props.hideTestAndPreviewBtn}
2059
2063
  isFullMode={this.props.isFullMode}
2060
2064
  eventContextTags={this.props?.eventContextTags}
2061
- />}
2065
+ />
2062
2066
  </CapColumn>
2063
2067
  {this.props.iosCtasData && this.state.showIosCtaTable &&
2064
2068
  <CapSlideBox