@capillarytech/creatives-library 9.0.56-alpha.7 → 9.0.56-alpha.8
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/CapActionButton/index.js +73 -60
- package/v2Components/CapActionButton/index.scss +44 -28
- package/v2Components/CapActionButton/messages.js +7 -3
- package/v2Components/CapActionButton/tests/index.test.js +17 -1
- package/v2Components/CapWhatsappCTA/messages.js +4 -0
- package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
- package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +9 -1
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +58 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +3 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +19 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +8 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +26 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
- package/v2Containers/CreativesContainer/index.js +27 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +13 -0
- package/v2Containers/MobilePush/Create/index.js +18 -1
- package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
- package/v2Containers/MobilePush/Edit/index.js +18 -1
- package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
- package/v2Containers/MobilePush/commonMethods.js +49 -1
- package/v2Containers/MobilePushNew/index.js +29 -4
- package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
- package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
- package/v2Containers/MobilePushNew/utils.js +34 -1
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/index.js +34 -0
- package/v2Containers/Rcs/index.scss +15 -0
- package/v2Containers/Rcs/tests/index.test.js +67 -0
- package/v2Containers/Sms/Create/index.js +3 -1
- package/v2Containers/Sms/Edit/index.js +25 -0
- package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
- package/v2Containers/Viber/index.js +24 -1
- package/v2Containers/Viber/tests/index.test.js +103 -0
- package/v2Containers/WebPush/Create/index.js +24 -0
- package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
- package/v2Containers/Whatsapp/index.js +19 -1
- package/v2Containers/Whatsapp/tests/index.test.js +115 -0
- package/v2Containers/Zalo/index.js +28 -1
- package/v2Containers/Zalo/tests/index.test.js +99 -0
|
@@ -158,6 +158,7 @@ const renderHelper = (args) => {
|
|
|
158
158
|
{...(args.omitGetDefaultTags ? {} : { getDefaultTags: true })}
|
|
159
159
|
isDltEnabled={args.isDltEnabled || false}
|
|
160
160
|
smsRegister={'DLT'}
|
|
161
|
+
onContentValidityChange={args.onContentValidityChange}
|
|
161
162
|
/>
|
|
162
163
|
</Provider>,
|
|
163
164
|
);
|
|
@@ -1923,6 +1924,72 @@ describe('Character Counting Functions', () => {
|
|
|
1923
1924
|
});
|
|
1924
1925
|
});
|
|
1925
1926
|
|
|
1927
|
+
describe('Rcs — onContentValidityChange reporting', () => {
|
|
1928
|
+
// Full-mode create with a fresh (non-hydrating) rcsData: templateDesc starts out empty,
|
|
1929
|
+
// same as the pre-existing "template title change" test's initial-state assumption.
|
|
1930
|
+
it('reports isContentEmpty: true on mount when the (default text_message) description is empty', () => {
|
|
1931
|
+
const onContentValidityChange = jest.fn();
|
|
1932
|
+
renderHelper({ onContentValidityChange });
|
|
1933
|
+
|
|
1934
|
+
expect(onContentValidityChange).toHaveBeenCalledWith(
|
|
1935
|
+
expect.objectContaining({ isContentEmpty: true }),
|
|
1936
|
+
);
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
it('reports isContentEmpty: false once the description has content, then true again once cleared', () => {
|
|
1940
|
+
const onContentValidityChange = jest.fn();
|
|
1941
|
+
renderHelper({ onContentValidityChange });
|
|
1942
|
+
onContentValidityChange.mockClear();
|
|
1943
|
+
|
|
1944
|
+
const descTextArea = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
|
|
1945
|
+
act(() => {
|
|
1946
|
+
descTextArea.props().onChange({ target: { value: 'Hello there' } });
|
|
1947
|
+
});
|
|
1948
|
+
renderedComponent.update();
|
|
1949
|
+
|
|
1950
|
+
expect(onContentValidityChange).toHaveBeenCalledWith(
|
|
1951
|
+
expect.objectContaining({ isContentEmpty: false }),
|
|
1952
|
+
);
|
|
1953
|
+
onContentValidityChange.mockClear();
|
|
1954
|
+
|
|
1955
|
+
const descTextAreaAfter = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
|
|
1956
|
+
act(() => {
|
|
1957
|
+
descTextAreaAfter.props().onChange({ target: { value: ' ' } });
|
|
1958
|
+
});
|
|
1959
|
+
renderedComponent.update();
|
|
1960
|
+
|
|
1961
|
+
expect(onContentValidityChange).toHaveBeenCalledWith(
|
|
1962
|
+
expect.objectContaining({ isContentEmpty: true }),
|
|
1963
|
+
);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
it('does not throw and does not report when onContentValidityChange is not provided', () => {
|
|
1967
|
+
expect(() => {
|
|
1968
|
+
renderHelper({});
|
|
1969
|
+
const descTextArea = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
|
|
1970
|
+
act(() => {
|
|
1971
|
+
descTextArea.props().onChange({ target: { value: 'abc' } });
|
|
1972
|
+
});
|
|
1973
|
+
renderedComponent.update();
|
|
1974
|
+
}).not.toThrow();
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
it('does not dispatch again on a re-render that does not change validity (dedup guard)', () => {
|
|
1978
|
+
const onContentValidityChange = jest.fn();
|
|
1979
|
+
renderHelper({ onContentValidityChange });
|
|
1980
|
+
onContentValidityChange.mockClear();
|
|
1981
|
+
|
|
1982
|
+
const descTextArea = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
|
|
1983
|
+
act(() => {
|
|
1984
|
+
// Still empty (still whitespace-only) — validity does not actually change.
|
|
1985
|
+
descTextArea.props().onChange({ target: { value: ' ' } });
|
|
1986
|
+
});
|
|
1987
|
+
renderedComponent.update();
|
|
1988
|
+
|
|
1989
|
+
expect(onContentValidityChange).not.toHaveBeenCalled();
|
|
1990
|
+
});
|
|
1991
|
+
});
|
|
1992
|
+
|
|
1926
1993
|
describe('CapActionButton (mocked within RCS tests)', () => {
|
|
1927
1994
|
it('should render the mocked CapActionButton placeholder', () => {
|
|
1928
1995
|
const wrapper = mountWithIntl(
|
|
@@ -185,7 +185,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
componentDidUpdate() {
|
|
188
|
-
|
|
188
|
+
// Reporting itself is opt-in via the callback prop — not tied to embeddedSmsFallback,
|
|
189
|
+
// which is a separate, unrelated flag (tag-popover styling for the SMS-fallback surface).
|
|
190
|
+
if (typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
|
|
189
191
|
return;
|
|
190
192
|
}
|
|
191
193
|
const validity = getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount);
|
|
@@ -34,6 +34,7 @@ import v2EditSmsReducer from './reducer';
|
|
|
34
34
|
import { v2SmsEditSagas } from './sagas';
|
|
35
35
|
import * as globalActions from '../../Cap/actions';
|
|
36
36
|
import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
|
|
37
|
+
import { getSmsEmbeddedFooterValidity } from '../smsFormDataHelpers';
|
|
37
38
|
|
|
38
39
|
export class Edit extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
|
39
40
|
constructor(props) {
|
|
@@ -54,6 +55,11 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
54
55
|
isTestAndPreviewMode: false,
|
|
55
56
|
pendingGetFormData: false,
|
|
56
57
|
};
|
|
58
|
+
// Tracks the last validity value reported to the parent so componentDidUpdate does not
|
|
59
|
+
// dispatch on every render. Intentionally undefined (not true) so the first render
|
|
60
|
+
// always reports the real validity rather than assuming the form starts invalid.
|
|
61
|
+
this._lastReportedSmsFooterTemplateNameEmpty = undefined;
|
|
62
|
+
this._lastReportedSmsFooterMessageEmpty = undefined;
|
|
57
63
|
this.saveFormData = this.saveFormData.bind(this);
|
|
58
64
|
this.onFormDataChange = this.onFormDataChange.bind(this);
|
|
59
65
|
this.onTemplateNameChange = this.onTemplateNameChange.bind(this);
|
|
@@ -189,6 +195,24 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
|
|
198
|
+
componentDidUpdate() {
|
|
199
|
+
// Reports live form validity so the host (CreativesContainer) can disable Done/Preview
|
|
200
|
+
// and Test as soon as the message body is cleared — mirrors Sms/Create's own reporting.
|
|
201
|
+
if (typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const validity = getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount);
|
|
205
|
+
const isTemplateNameEmpty = !!validity.isTemplateNameEmpty;
|
|
206
|
+
const isMessageEmpty = !!validity.isMessageEmpty;
|
|
207
|
+
if (
|
|
208
|
+
this._lastReportedSmsFooterTemplateNameEmpty === isTemplateNameEmpty &&
|
|
209
|
+
this._lastReportedSmsFooterMessageEmpty === isMessageEmpty
|
|
210
|
+
) return;
|
|
211
|
+
this._lastReportedSmsFooterTemplateNameEmpty = isTemplateNameEmpty;
|
|
212
|
+
this._lastReportedSmsFooterMessageEmpty = isMessageEmpty;
|
|
213
|
+
this.props.onEmbeddedSmsFooterValidity(validity);
|
|
214
|
+
}
|
|
215
|
+
|
|
192
216
|
componentWillUnmount() {
|
|
193
217
|
if (this.pendingGetFormDataTimeout) {
|
|
194
218
|
clearTimeout(this.pendingGetFormDataTimeout);
|
|
@@ -1138,6 +1162,7 @@ Edit.propTypes = {
|
|
|
1138
1162
|
handleTestAndPreview: PropTypes.func,
|
|
1139
1163
|
handleCloseTestAndPreview: PropTypes.func,
|
|
1140
1164
|
isTestAndPreviewMode: PropTypes.bool,
|
|
1165
|
+
onEmbeddedSmsFooterValidity: PropTypes.func,
|
|
1141
1166
|
};
|
|
1142
1167
|
|
|
1143
1168
|
const mapStateToProps = createStructuredSelector({
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { shallow } from 'enzyme';
|
|
3
|
+
import { Edit } from '../index';
|
|
4
|
+
|
|
5
|
+
// Shallow-render the plain class (bypassing the withCreatives/Redux/saga HOC) so this test
|
|
6
|
+
// only exercises the componentDidUpdate → onEmbeddedSmsFooterValidity wiring in isolation.
|
|
7
|
+
|
|
8
|
+
const baseProps = () => ({
|
|
9
|
+
actions: {
|
|
10
|
+
getTemplateDetails: jest.fn(),
|
|
11
|
+
setTemplateDetails: jest.fn(),
|
|
12
|
+
clearEditResponse: jest.fn(),
|
|
13
|
+
resetEditTemplate: jest.fn(),
|
|
14
|
+
editTemplate: jest.fn(),
|
|
15
|
+
},
|
|
16
|
+
globalActions: {
|
|
17
|
+
fetchSchemaForEntity: jest.fn(),
|
|
18
|
+
setInjectedTags: jest.fn(),
|
|
19
|
+
addMessageToQueue: jest.fn(),
|
|
20
|
+
},
|
|
21
|
+
Edit: {},
|
|
22
|
+
params: { id: 'tpl-1' },
|
|
23
|
+
location: { query: { type: 'embedded', module: 'library' } },
|
|
24
|
+
metaEntities: {},
|
|
25
|
+
intl: { formatMessage: (m) => m.defaultMessage || m.id },
|
|
26
|
+
isFullMode: false,
|
|
27
|
+
getFormSubscriptionData: jest.fn(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('Sms/Edit — onEmbeddedSmsFooterValidity reporting', () => {
|
|
31
|
+
it('reports isMessageEmpty: true on mount when the message body is empty', () => {
|
|
32
|
+
const onEmbeddedSmsFooterValidity = jest.fn();
|
|
33
|
+
const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
|
|
34
|
+
wrapper.setState({ formData: { 0: { 'sms-editor': '' }, base: { 'sms-editor': '' } }, tabCount: 1 });
|
|
35
|
+
|
|
36
|
+
expect(onEmbeddedSmsFooterValidity).toHaveBeenCalledWith(
|
|
37
|
+
expect.objectContaining({ isMessageEmpty: true }),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('reports isMessageEmpty: false once the message body has content', () => {
|
|
42
|
+
const onEmbeddedSmsFooterValidity = jest.fn();
|
|
43
|
+
const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
|
|
44
|
+
wrapper.setState({ formData: { 0: { 'sms-editor': '' }, base: { 'sms-editor': '' } }, tabCount: 1 });
|
|
45
|
+
onEmbeddedSmsFooterValidity.mockClear();
|
|
46
|
+
|
|
47
|
+
wrapper.setState({ formData: { 0: { 'sms-editor': 'Hello there' }, base: { 'sms-editor': 'Hello there' } }, tabCount: 1 });
|
|
48
|
+
|
|
49
|
+
expect(onEmbeddedSmsFooterValidity).toHaveBeenCalledWith(
|
|
50
|
+
expect.objectContaining({ isMessageEmpty: false }),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('re-reports empty when an existing (edit-loaded) message is cleared out', () => {
|
|
55
|
+
const onEmbeddedSmsFooterValidity = jest.fn();
|
|
56
|
+
const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
|
|
57
|
+
wrapper.setState({ formData: { 0: { 'sms-editor': 'Existing content' }, base: { 'sms-editor': 'Existing content' } }, tabCount: 1 });
|
|
58
|
+
onEmbeddedSmsFooterValidity.mockClear();
|
|
59
|
+
|
|
60
|
+
wrapper.setState({ formData: { 0: { 'sms-editor': '' }, base: { 'sms-editor': '' } }, tabCount: 1 });
|
|
61
|
+
|
|
62
|
+
expect(onEmbeddedSmsFooterValidity).toHaveBeenCalledWith(
|
|
63
|
+
expect.objectContaining({ isMessageEmpty: true }),
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not throw and does not report when onEmbeddedSmsFooterValidity is not provided', () => {
|
|
68
|
+
const wrapper = shallow(<Edit {...baseProps()} />);
|
|
69
|
+
expect(() => {
|
|
70
|
+
wrapper.setState({ formData: { 0: { 'sms-editor': '' } }, tabCount: 1 });
|
|
71
|
+
}).not.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does not dispatch again on a re-render that does not change validity (dedup guard)', () => {
|
|
75
|
+
const onEmbeddedSmsFooterValidity = jest.fn();
|
|
76
|
+
const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
|
|
77
|
+
wrapper.setState({ formData: { 0: { 'sms-editor': '' } }, tabCount: 1 });
|
|
78
|
+
onEmbeddedSmsFooterValidity.mockClear();
|
|
79
|
+
|
|
80
|
+
// Unrelated state change — validity tuple (isTemplateNameEmpty/isMessageEmpty) is unchanged.
|
|
81
|
+
wrapper.setState({ currentTab: 1 });
|
|
82
|
+
|
|
83
|
+
expect(onEmbeddedSmsFooterValidity).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useState, useEffect, useCallback, useRef,
|
|
3
|
+
} from 'react';
|
|
2
4
|
import { bindActionCreators } from 'redux';
|
|
3
5
|
import { createStructuredSelector } from 'reselect';
|
|
4
6
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
|
@@ -139,6 +141,7 @@ export const Viber = (props) => {
|
|
|
139
141
|
showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
|
|
140
142
|
handleTestAndPreview: propsHandleTestAndPreview,
|
|
141
143
|
handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
|
|
144
|
+
onContentValidityChange,
|
|
142
145
|
} = props || {};
|
|
143
146
|
|
|
144
147
|
const { formatMessage } = intl;
|
|
@@ -257,6 +260,26 @@ export const Viber = (props) => {
|
|
|
257
260
|
}
|
|
258
261
|
}, [viber.templateDetails || templateData]);
|
|
259
262
|
|
|
263
|
+
// Reports live message-content validity to a parent (e.g. CreativesContainer's
|
|
264
|
+
// slidebox) so it can disable its own Done/Preview-and-test buttons whenever the
|
|
265
|
+
// Viber message body is empty/whitespace-only. Mirrors the Sms Create/Edit
|
|
266
|
+
// componentDidUpdate pattern, using a ref (instead of an instance field) as the
|
|
267
|
+
// dedup guard so the callback only fires when the reported value actually changes -
|
|
268
|
+
// calling it unconditionally on every render risks an infinite update loop when the
|
|
269
|
+
// parent's setState triggers a re-render of this component.
|
|
270
|
+
const lastReportedContentEmptyRef = useRef(undefined);
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (typeof onContentValidityChange !== 'function') {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const isContentEmpty = (messageContent || '').trim() === '';
|
|
276
|
+
if (lastReportedContentEmptyRef.current === isContentEmpty) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
lastReportedContentEmptyRef.current = isContentEmpty;
|
|
280
|
+
onContentValidityChange({ isContentEmpty });
|
|
281
|
+
}, [messageContent, onContentValidityChange]);
|
|
282
|
+
|
|
260
283
|
// Text area Code start here
|
|
261
284
|
|
|
262
285
|
// Tags Code start from here
|
|
@@ -469,4 +469,107 @@ describe('Test Viber container', () => {
|
|
|
469
469
|
|
|
470
470
|
expect(await screen.findByText("URL can't be empty")).toBeInTheDocument();
|
|
471
471
|
});
|
|
472
|
+
|
|
473
|
+
describe('onContentValidityChange reporting (Done/Preview button gating in the slidebox)', () => {
|
|
474
|
+
it('reports isContentEmpty: true on mount for an empty create flow, then false once a message is typed', async () => {
|
|
475
|
+
const onContentValidityChange = jest.fn();
|
|
476
|
+
renderComponent({
|
|
477
|
+
actions: mockActions,
|
|
478
|
+
globalActions: mockGlobalActions,
|
|
479
|
+
templateData: { mode: 'create' },
|
|
480
|
+
viber: {
|
|
481
|
+
uploadedAssetData: {},
|
|
482
|
+
createTemplateInProgress: false,
|
|
483
|
+
},
|
|
484
|
+
location: {
|
|
485
|
+
pathname: '/sms/edit',
|
|
486
|
+
query: { type: false, module: 'default' },
|
|
487
|
+
search: '',
|
|
488
|
+
},
|
|
489
|
+
isFullMode: true,
|
|
490
|
+
handleClose: jest.fn(),
|
|
491
|
+
onContentValidityChange,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
await waitFor(() => {
|
|
495
|
+
expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const msgBox = screen.getAllByPlaceholderText(/Enter message/i)[0];
|
|
499
|
+
fireEvent.change(msgBox, { target: { value: 'hello there' } });
|
|
500
|
+
|
|
501
|
+
await waitFor(() => {
|
|
502
|
+
expect(onContentValidityChange).toHaveBeenLastCalledWith({ isContentEmpty: false });
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('does not re-report the same validity value (dedup guard)', async () => {
|
|
507
|
+
const onContentValidityChange = jest.fn();
|
|
508
|
+
renderComponent({
|
|
509
|
+
actions: mockActions,
|
|
510
|
+
globalActions: mockGlobalActions,
|
|
511
|
+
templateData: { mode: 'create' },
|
|
512
|
+
viber: {
|
|
513
|
+
uploadedAssetData: {},
|
|
514
|
+
createTemplateInProgress: false,
|
|
515
|
+
},
|
|
516
|
+
location: {
|
|
517
|
+
pathname: '/sms/edit',
|
|
518
|
+
query: { type: false, module: 'default' },
|
|
519
|
+
search: '',
|
|
520
|
+
},
|
|
521
|
+
isFullMode: true,
|
|
522
|
+
handleClose: jest.fn(),
|
|
523
|
+
onContentValidityChange,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await waitFor(() => {
|
|
527
|
+
expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
|
|
528
|
+
});
|
|
529
|
+
const callsAfterMount = onContentValidityChange.mock.calls.length;
|
|
530
|
+
|
|
531
|
+
// Whitespace-only change: still empty, should not trigger another call.
|
|
532
|
+
const msgBox = screen.getAllByPlaceholderText(/Enter message/i)[0];
|
|
533
|
+
fireEvent.change(msgBox, { target: { value: ' ' } });
|
|
534
|
+
|
|
535
|
+
expect(onContentValidityChange).toHaveBeenCalledTimes(callsAfterMount);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('reports isContentEmpty: false for an edit flow pre-filled with message content, then true once cleared', async () => {
|
|
539
|
+
const onContentValidityChange = jest.fn();
|
|
540
|
+
renderComponent({
|
|
541
|
+
actions: mockActions,
|
|
542
|
+
globalActions: mockGlobalActions,
|
|
543
|
+
templateData: { mode: 'create' },
|
|
544
|
+
viber: {
|
|
545
|
+
uploadedAssetData: {},
|
|
546
|
+
createTemplateInProgress: false,
|
|
547
|
+
templateDetails: templateDetailsText,
|
|
548
|
+
},
|
|
549
|
+
location: {
|
|
550
|
+
pathname: '/sms/edit',
|
|
551
|
+
query: { type: false, module: 'default' },
|
|
552
|
+
search: '',
|
|
553
|
+
},
|
|
554
|
+
isFullMode: true,
|
|
555
|
+
params: { id: 'test' },
|
|
556
|
+
handleClose: jest.fn(),
|
|
557
|
+
metaEntities,
|
|
558
|
+
getDefaultTags,
|
|
559
|
+
injectedTags,
|
|
560
|
+
onContentValidityChange,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await waitFor(() => {
|
|
564
|
+
expect(onContentValidityChange).toHaveBeenLastCalledWith({ isContentEmpty: false });
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const msgBox = screen.getAllByPlaceholderText(/Enter message/i)[0];
|
|
568
|
+
fireEvent.change(msgBox, { target: { value: '' } });
|
|
569
|
+
|
|
570
|
+
await waitFor(() => {
|
|
571
|
+
expect(onContentValidityChange).toHaveBeenLastCalledWith({ isContentEmpty: true });
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
});
|
|
472
575
|
});
|
|
@@ -164,6 +164,7 @@ const WebPushCreate = ({
|
|
|
164
164
|
templateActions: templateActionsProps,
|
|
165
165
|
Templates,
|
|
166
166
|
restrictPersonalization = false,
|
|
167
|
+
onContentValidityChange,
|
|
167
168
|
}) => {
|
|
168
169
|
const { formatMessage } = intl;
|
|
169
170
|
const aiContentBotDisabled = isAiContentBotDisabled();
|
|
@@ -194,6 +195,10 @@ const WebPushCreate = ({
|
|
|
194
195
|
const messageCountRef = useRef(null);
|
|
195
196
|
const saveInitiatedRef = useRef(false);
|
|
196
197
|
const messageTextAreaRef = useRef(null);
|
|
198
|
+
// Tracks the last isContentEmpty value reported to the parent (CreativesContainer) so the
|
|
199
|
+
// effect below does not dispatch on every render. Intentionally undefined (not a boolean)
|
|
200
|
+
// so the first render always reports the real validity rather than assuming a starting value.
|
|
201
|
+
const lastReportedContentEmptyRef = useRef(undefined);
|
|
197
202
|
|
|
198
203
|
// Custom hooks
|
|
199
204
|
const { updateCharacterCount } = useCharacterCount(formatMessage, messages);
|
|
@@ -566,6 +571,24 @@ const WebPushCreate = ({
|
|
|
566
571
|
});
|
|
567
572
|
}, [isFullMode, templateName]);
|
|
568
573
|
|
|
574
|
+
// Reports live content validity to the parent (CreativesContainer) so it can disable the
|
|
575
|
+
// Done/Preview-and-test buttons as soon as the notification title or message is cleared.
|
|
576
|
+
// Both title and message are required fields for WebPush (see isSaveDisabled below), so the
|
|
577
|
+
// content is considered empty if either is blank/whitespace-only. Mirrors the Sms Create/Edit
|
|
578
|
+
// componentDidUpdate pattern — the ref-based dedup guard prevents an infinite render loop
|
|
579
|
+
// (parent setState -> re-render -> effect re-fires -> parent setState -> ...).
|
|
580
|
+
useEffect(() => {
|
|
581
|
+
if (typeof onContentValidityChange !== 'function') {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const isContentEmpty = !notificationTitle.trim() || !message.trim();
|
|
585
|
+
if (lastReportedContentEmptyRef.current === isContentEmpty) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
lastReportedContentEmptyRef.current = isContentEmpty;
|
|
589
|
+
onContentValidityChange({ isContentEmpty });
|
|
590
|
+
}, [notificationTitle, message, onContentValidityChange]);
|
|
591
|
+
|
|
569
592
|
// Pure validator that returns boolean without setting error state
|
|
570
593
|
const validateFormSilent = () => {
|
|
571
594
|
const templateNameInvalid = isFullMode && validateTemplateName(templateName);
|
|
@@ -1175,6 +1198,7 @@ WebPushCreate.propTypes = {
|
|
|
1175
1198
|
waitEventContextTags: PropTypes.object,
|
|
1176
1199
|
templateActions: PropTypes.object,
|
|
1177
1200
|
restrictPersonalization: PropTypes.bool,
|
|
1201
|
+
onContentValidityChange: PropTypes.func,
|
|
1178
1202
|
};
|
|
1179
1203
|
|
|
1180
1204
|
WebPushCreate.defaultProps = {
|