@capillarytech/creatives-library 8.0.272 → 8.0.274

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.274",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -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
+ });
@@ -26,7 +26,7 @@ import * as globalActions from '../Cap/actions';
26
26
  import './_email.scss';
27
27
  import {getMessageObject} from '../../utils/messageUtils';
28
28
  import EmailPreview from '../../v2Components/EmailPreview';
29
- import { getDecodedFileName ,hasLiquidSupportFeature} from '../../utils/common';
29
+ import { getDecodedFileName, hasLiquidSupportFeature, hasSupportCKEditor } from '../../utils/common';
30
30
  import Pagination from '../../v2Components/Pagination';
31
31
  import * as creativesContainerActions from '../CreativesContainer/actions';
32
32
  import withCreatives from '../../hoc/withCreatives';
@@ -325,6 +325,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
325
325
  }
326
326
  if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.schema)) {
327
327
  const newSchema = this.injectEvents(nextProps.metaEntities.layouts[0].definition);
328
+ this.applyTabOptionIconVisibility(newSchema);
328
329
 
329
330
  this.setState({schema: newSchema, loadingStatus: this.state.loadingStatus + 1});
330
331
  if (this.props.location.query.module !== 'library' || (this.props.location.query.module === 'library' && this.props.getDefaultTags)) {
@@ -1752,6 +1753,16 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
1752
1753
  newTabPopoverSection.cols[0].style.display = isDragDrop ? "" : "none";
1753
1754
  }
1754
1755
  });
1756
+ // Hide tab-option-icon (switch editor trigger) when SUPPORT_CK_EDITOR is disabled
1757
+ if (containerInputFieldCol.value && containerInputFieldCol.value.sections) {
1758
+ _.forEach(containerInputFieldCol.value.sections[0].inputFields, (valueInputField) => {
1759
+ _.forEach(valueInputField.cols, (valueCol) => {
1760
+ if (valueCol.id === 'tab-option-icon') {
1761
+ valueCol.colStyle = { ...valueCol.colStyle, display: hasSupportCKEditor() ? 'flex' : 'none' };
1762
+ }
1763
+ });
1764
+ });
1765
+ }
1755
1766
  }
1756
1767
  });
1757
1768
  }
@@ -1760,6 +1771,28 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
1760
1771
  this.setState({schema, isSchemaChanged: true});
1761
1772
  }
1762
1773
 
1774
+ /**
1775
+ * Hides the tab-option-icon (switch editor trigger) in schema when SUPPORT_CK_EDITOR is disabled,
1776
+ * so users cannot switch editor if the feature is not enabled.
1777
+ */
1778
+ applyTabOptionIconVisibility = (schema) => {
1779
+ if (!schema || !schema.containers) return;
1780
+ _.forEach(schema.containers, (container) => {
1781
+ if (!container.isActive || !container.tabBarExtraContent?.sections?.[0]?.inputFields?.[0]?.cols) return;
1782
+ _.forEach(container.tabBarExtraContent.sections[0].inputFields[0].cols, (col) => {
1783
+ if (col.id === 'tab-options-popover' && col.value?.sections?.[0]?.inputFields) {
1784
+ _.forEach(col.value.sections[0].inputFields, (inputField) => {
1785
+ _.forEach(inputField.cols, (c) => {
1786
+ if (c.id === 'tab-option-icon') {
1787
+ c.colStyle = { ...(c.colStyle || {}), display: hasSupportCKEditor() ? 'flex' : 'none' };
1788
+ }
1789
+ });
1790
+ });
1791
+ }
1792
+ });
1793
+ });
1794
+ };
1795
+
1763
1796
  showInsertImageButton = (passedSchema) => {
1764
1797
  const schema = passedSchema || _.cloneDeep(this.state.schema);
1765
1798
  _.forEach(schema.containers, (container) => {
@@ -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