@capillarytech/creatives-library 9.0.2 → 9.0.4
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/NewCallTask/index.js +5 -8
- package/v2Components/NewCallTask/tests/insertAtCursor.test.js +85 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +0 -4
- package/v2Components/TemplatePreview/index.js +7 -7
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +1 -1
- package/v2Containers/FTP/index.js +5 -8
- package/v2Containers/WebPush/Create/components/MessageSection.js +8 -2
- package/v2Containers/WebPush/Create/components/MessageSection.test.js +25 -2
- package/v2Containers/WebPush/Create/components/__snapshots__/MessageSection.test.js.snap +0 -1
- package/v2Containers/WebPush/Create/preview/PreviewControls.js +7 -0
package/package.json
CHANGED
|
@@ -40,18 +40,15 @@ class NewCallTask extends React.Component { // eslint-disable-line react/prefer-
|
|
|
40
40
|
this.insertAtCursor(`{{${data}}}`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
setInputRef = (textarea) => {
|
|
44
|
-
this.textArea = textarea;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
43
|
insertAtCursor = (myValue) => {
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
const textAreaRef = document.getElementById('new-call-task-message-body');
|
|
45
|
+
if (!textAreaRef) return;
|
|
46
|
+
textAreaRef.focus();
|
|
50
47
|
let pos = textAreaRef.selectionEnd + myValue.length;
|
|
51
48
|
//IE support
|
|
52
49
|
let newMessage = textAreaRef.value + myValue;
|
|
53
50
|
if (document.selection) {
|
|
54
|
-
|
|
51
|
+
textAreaRef.focus();
|
|
55
52
|
const sel = document.selection.createRange();
|
|
56
53
|
sel.text = myValue;
|
|
57
54
|
} else if (textAreaRef.selectionStart || textAreaRef.selectionStart === '0') { //MOZILLA and others
|
|
@@ -128,7 +125,7 @@ class NewCallTask extends React.Component { // eslint-disable-line react/prefer-
|
|
|
128
125
|
injectedTags={injectedTags}
|
|
129
126
|
/>
|
|
130
127
|
<CapInput.TextArea
|
|
131
|
-
|
|
128
|
+
id="new-call-task-message-body"
|
|
132
129
|
label={formatMessage(messages.messageHeader)}
|
|
133
130
|
onChange={this.updateMessageBody}
|
|
134
131
|
value={messageBody}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { configure, mount } from 'enzyme';
|
|
3
|
+
import Adapter from '@cfaester/enzyme-adapter-react-18';
|
|
4
|
+
import { IntlProvider } from 'react-intl';
|
|
5
|
+
import { act } from 'react-dom/test-utils';
|
|
6
|
+
|
|
7
|
+
// TagList is a redux-auth-wrapper-connected container that needs full app routing
|
|
8
|
+
// state to mount; CallTaskPreview is irrelevant to insertAtCursor. Stub both so we
|
|
9
|
+
// can mount the real NewCallTask and exercise its real insertAtCursor method.
|
|
10
|
+
jest.mock('../../../v2Containers/TagList', () => {
|
|
11
|
+
const Stub = () => null;
|
|
12
|
+
return { __esModule: true, default: Stub, TagList: Stub };
|
|
13
|
+
});
|
|
14
|
+
jest.mock('../../CallTaskPreview', () => {
|
|
15
|
+
const Stub = () => null;
|
|
16
|
+
return { __esModule: true, default: Stub };
|
|
17
|
+
});
|
|
18
|
+
// hasStore2DoorFeature() reads app/org config that isn't present in jsdom.
|
|
19
|
+
jest.mock('../../../utils/common', () => ({
|
|
20
|
+
...jest.requireActual('../../../utils/common'),
|
|
21
|
+
hasStore2DoorFeature: () => false,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line import/first
|
|
25
|
+
import NewCallTask from '../index';
|
|
26
|
+
// eslint-disable-next-line import/first
|
|
27
|
+
import mockData from './mockData';
|
|
28
|
+
|
|
29
|
+
configure({ adapter: new Adapter() });
|
|
30
|
+
|
|
31
|
+
let container;
|
|
32
|
+
let wrapper;
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
if (wrapper) wrapper.unmount();
|
|
36
|
+
if (container) container.remove();
|
|
37
|
+
wrapper = undefined;
|
|
38
|
+
container = undefined;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// attachTo a real node in document.body so document.getElementById can find the
|
|
42
|
+
// antd-rendered <textarea> (enzyme `mount` is detached from `document` otherwise).
|
|
43
|
+
const mountNewCallTask = (extraProps = {}) => {
|
|
44
|
+
container = document.createElement('div');
|
|
45
|
+
document.body.appendChild(container);
|
|
46
|
+
wrapper = mount(
|
|
47
|
+
<IntlProvider locale="en">
|
|
48
|
+
<NewCallTask {...mockData} onCallTaskSubmit={jest.fn()} {...extraProps} />
|
|
49
|
+
</IntlProvider>,
|
|
50
|
+
{ attachTo: container },
|
|
51
|
+
);
|
|
52
|
+
return wrapper;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Regression coverage for the antd v3 -> v6 (cap-ui-library v6) migration: the lib
|
|
56
|
+
// dropped the `setInputRef` callback, so selecting a personalisation tag used to
|
|
57
|
+
// crash with "Cannot read properties of undefined (reading 'focus')". The fix
|
|
58
|
+
// resolves the DOM <textarea> via document.getElementById instead.
|
|
59
|
+
describe('NewCallTask insertAtCursor (antd v6 setInputRef removal)', () => {
|
|
60
|
+
it('forwards the id to a real DOM <textarea>', () => {
|
|
61
|
+
mountNewCallTask();
|
|
62
|
+
const textarea = document.getElementById('new-call-task-message-body');
|
|
63
|
+
expect(textarea).not.toBeNull();
|
|
64
|
+
expect(textarea.tagName).toBe('TEXTAREA');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('inserts a personalisation tag at the caret without throwing', () => {
|
|
68
|
+
const wrapper = mountNewCallTask();
|
|
69
|
+
const textarea = document.getElementById('new-call-task-message-body');
|
|
70
|
+
|
|
71
|
+
// user typed "Hello world" and put the caret right after "Hello "
|
|
72
|
+
textarea.value = 'Hello world';
|
|
73
|
+
textarea.setSelectionRange(6, 6);
|
|
74
|
+
|
|
75
|
+
const instance = wrapper.find('NewCallTask').instance();
|
|
76
|
+
expect(() => {
|
|
77
|
+
act(() => {
|
|
78
|
+
instance.onTagSelect('firstName');
|
|
79
|
+
});
|
|
80
|
+
}).not.toThrow();
|
|
81
|
+
|
|
82
|
+
// tag spliced in at the caret, not appended blindly
|
|
83
|
+
expect(instance.state.messageBody).toBe('Hello {{firstName}}world');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -644,7 +644,7 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
|
|
|
644
644
|
src={smsMobileAndroid}
|
|
645
645
|
alt={this.props.intl.formatMessage(messages.previewGenerated)}
|
|
646
646
|
/>
|
|
647
|
-
<CapRow
|
|
647
|
+
<CapRow className="msg-container sms">
|
|
648
648
|
<CapRow useLegacy className={"message-pop sms"}>
|
|
649
649
|
{content &&
|
|
650
650
|
content.length &&
|
|
@@ -748,12 +748,12 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
|
|
|
748
748
|
>
|
|
749
749
|
{this.props.device === "android" ? (
|
|
750
750
|
<CapRow className="message-pop align-left">
|
|
751
|
-
<CapRow
|
|
752
|
-
<CapRow
|
|
751
|
+
<CapRow className="app-header">
|
|
752
|
+
<CapRow className="app-header-left">
|
|
753
753
|
<span className="app-icon">{""}</span>
|
|
754
754
|
<CapLabel className="title">{content.header}</CapLabel>
|
|
755
755
|
</CapRow>
|
|
756
|
-
<CapRow
|
|
756
|
+
<CapRow className="app-header-right">
|
|
757
757
|
<span>2:29 PM</span>
|
|
758
758
|
<CapIcon type="chevron-down" size="sm" />
|
|
759
759
|
</CapRow>
|
|
@@ -812,12 +812,12 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
|
|
|
812
812
|
</CapRow>
|
|
813
813
|
) : (
|
|
814
814
|
<CapRow className="message-pop align-left">
|
|
815
|
-
<CapRow
|
|
816
|
-
<CapRow
|
|
815
|
+
<CapRow className="app-header">
|
|
816
|
+
<CapRow className="app-header-left">
|
|
817
817
|
<span className="app-icon">{""}</span>
|
|
818
818
|
<CapLabel className="title">{content.header}</CapLabel>
|
|
819
819
|
</CapRow>
|
|
820
|
-
<CapRow
|
|
820
|
+
<CapRow className="app-header-right">
|
|
821
821
|
<span>2:29 PM</span>
|
|
822
822
|
<CapIcon type="chevron-down" size="sm" />
|
|
823
823
|
</CapRow>
|
|
@@ -361,7 +361,7 @@ const ChannelSelectionStep = ({
|
|
|
361
361
|
// Add incentive in actions (campaigns-ui style: cap-card-action-container)
|
|
362
362
|
if (hasIncentiveOptions) {
|
|
363
363
|
card.actions = [
|
|
364
|
-
<CapRow className="cap-card-action-container add-incentive-action-row" key="add-incentive">
|
|
364
|
+
<CapRow type='flex' align="middle" className="cap-card-action-container add-incentive-action-row" key="add-incentive">
|
|
365
365
|
<CapDropdown
|
|
366
366
|
overlay={renderIncentiveDropdownOverlay((menuClickEvent) => handleIncentiveMenuClick(menuClickEvent, item.contentId))}
|
|
367
367
|
onVisibleChange={(visible) => handleIncentivesVisibleChange(visible, item.contentId)}
|
|
@@ -166,10 +166,6 @@ export class FTP extends React.Component {
|
|
|
166
166
|
this.insertAtCursor(`{{${data}}}`);
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
-
setInputRef = (textarea) => {
|
|
170
|
-
this.textArea = textarea;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
169
|
getServerSelectionContent = () => {
|
|
174
170
|
const { getFTPServersInProgress } = this.props.FTP || {};
|
|
175
171
|
const { formatMessage } = this.props.intl;
|
|
@@ -257,7 +253,7 @@ export class FTP extends React.Component {
|
|
|
257
253
|
injectedTags={injectedTags || {}}
|
|
258
254
|
/>
|
|
259
255
|
<CapInput.TextArea
|
|
260
|
-
|
|
256
|
+
id="ftp-message-content"
|
|
261
257
|
label={formatMessage(messages.messageHeader)}
|
|
262
258
|
onChange={this.updateMessageBody}
|
|
263
259
|
value={messageContent}
|
|
@@ -529,13 +525,14 @@ export class FTP extends React.Component {
|
|
|
529
525
|
};
|
|
530
526
|
|
|
531
527
|
insertAtCursor = (myValue) => {
|
|
532
|
-
|
|
533
|
-
|
|
528
|
+
const textAreaRef = document.getElementById('ftp-message-content');
|
|
529
|
+
if (!textAreaRef) return;
|
|
530
|
+
textAreaRef.focus();
|
|
534
531
|
let pos = textAreaRef.selectionEnd + myValue.length;
|
|
535
532
|
//IE support
|
|
536
533
|
let newMessage = textAreaRef.value + myValue;
|
|
537
534
|
if (document.selection) {
|
|
538
|
-
|
|
535
|
+
textAreaRef.focus();
|
|
539
536
|
const sel = document.selection.createRange();
|
|
540
537
|
sel.text = myValue;
|
|
541
538
|
} else if (textAreaRef.selectionStart || textAreaRef.selectionStart === '0') { //MOZILLA and others
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef } from 'react';
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { FormattedMessage } from 'react-intl';
|
|
4
4
|
import CapInput from '@capillarytech/cap-ui-library/CapInput';
|
|
@@ -73,6 +73,13 @@ export const MessageSection = ({
|
|
|
73
73
|
handleMessageTextAreaRef,
|
|
74
74
|
isAiContentBotDisabled,
|
|
75
75
|
}) => {
|
|
76
|
+
// resolve the underlying DOM
|
|
77
|
+
// <textarea> by id and feed it to the existing ref handler. CapEmojiPicker and
|
|
78
|
+
// the Aira trigger both read `messageTextAreaRef.current` as that DOM node.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
handleMessageTextAreaRef(document.getElementById('webpush-message-input'));
|
|
81
|
+
}, [handleMessageTextAreaRef]);
|
|
82
|
+
|
|
76
83
|
const renderCharacterCount = () => {
|
|
77
84
|
if (!SHOW_CHARACTER_COUNT) return null;
|
|
78
85
|
|
|
@@ -119,7 +126,6 @@ export const MessageSection = ({
|
|
|
119
126
|
size="default"
|
|
120
127
|
isRequired
|
|
121
128
|
autosize={{ minRows: 3, maxRows: 5 }}
|
|
122
|
-
setInputRef={handleMessageTextAreaRef}
|
|
123
129
|
errorMessage={
|
|
124
130
|
error && (
|
|
125
131
|
<CapError className="webpush-template-message-error">
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { mount } from 'enzyme';
|
|
3
|
+
import { IntlProvider } from 'react-intl';
|
|
2
4
|
import { mountWithIntl, shallowWithIntl } from '../../../../helpers/intl-enzym-test-helpers';
|
|
3
5
|
import MessageSection from './MessageSection';
|
|
4
6
|
import CapInput from '@capillarytech/cap-ui-library/CapInput';
|
|
@@ -230,10 +232,31 @@ describe('MessageSection', () => {
|
|
|
230
232
|
});
|
|
231
233
|
|
|
232
234
|
describe('TextArea Ref Handling', () => {
|
|
233
|
-
it('should
|
|
235
|
+
it('should not pass the dropped setInputRef prop to CapInput.TextArea', () => {
|
|
234
236
|
const wrapper = mountWithIntl(<MessageSection {...defaultProps} />);
|
|
235
237
|
const textArea = wrapper.find(CapInput.TextArea);
|
|
236
|
-
expect(textArea.prop('setInputRef')).
|
|
238
|
+
expect(textArea.prop('setInputRef')).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should resolve the DOM textarea by id and feed it to handleMessageTextAreaRef on mount', () => {
|
|
242
|
+
// attachTo a real DOM node so document.getElementById can find the
|
|
243
|
+
// antd-rendered <textarea> (enzyme mount is detached from document otherwise).
|
|
244
|
+
const container = document.createElement('div');
|
|
245
|
+
document.body.appendChild(container);
|
|
246
|
+
const wrapper = mount(
|
|
247
|
+
<IntlProvider locale="en">
|
|
248
|
+
<MessageSection {...defaultProps} />
|
|
249
|
+
</IntlProvider>,
|
|
250
|
+
{ attachTo: container }
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const textarea = document.getElementById('webpush-message-input');
|
|
254
|
+
expect(textarea).not.toBeNull();
|
|
255
|
+
expect(textarea.tagName).toBe('TEXTAREA');
|
|
256
|
+
expect(mockHandleMessageTextAreaRef).toHaveBeenCalledWith(textarea);
|
|
257
|
+
|
|
258
|
+
wrapper.unmount();
|
|
259
|
+
container.remove();
|
|
237
260
|
});
|
|
238
261
|
|
|
239
262
|
it('should pass messageTextAreaRef to CapEmojiPicker.Wrapper', () => {
|
|
@@ -10,6 +10,9 @@ import { getBrowserOptionsForOS, getSupportedStates } from './config/notificatio
|
|
|
10
10
|
import messages from '../messages';
|
|
11
11
|
import './preview.scss';
|
|
12
12
|
|
|
13
|
+
const getDropdownPopupContainer = (triggerNode) =>
|
|
14
|
+
triggerNode.closest('.ant-modal-container, .ant-modal-content') || document.body;
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* PreviewControls Component
|
|
15
18
|
*
|
|
@@ -67,6 +70,7 @@ const PreviewControls = ({
|
|
|
67
70
|
options={stateOptions}
|
|
68
71
|
value={selectedState}
|
|
69
72
|
onChange={onStateChange}
|
|
73
|
+
getPopupContainer={getDropdownPopupContainer}
|
|
70
74
|
// Note: Add tooltip prop here when tooltip mechanism is implemented
|
|
71
75
|
// tooltip={tooltipMessage ? tooltipMessage : undefined}
|
|
72
76
|
/>
|
|
@@ -198,6 +202,7 @@ const PreviewControls = ({
|
|
|
198
202
|
options={options}
|
|
199
203
|
value={value}
|
|
200
204
|
onChange={onChange}
|
|
205
|
+
getPopupContainer={getDropdownPopupContainer}
|
|
201
206
|
/>
|
|
202
207
|
</>
|
|
203
208
|
);
|
|
@@ -235,6 +240,7 @@ const PreviewControls = ({
|
|
|
235
240
|
options={osOptions}
|
|
236
241
|
value={selectedOS}
|
|
237
242
|
onChange={handleOSChangeWithCascade}
|
|
243
|
+
getPopupContainer={getDropdownPopupContainer}
|
|
238
244
|
/>
|
|
239
245
|
</CapColumn>
|
|
240
246
|
<CapColumn span={12} className="preview-control-browser">
|
|
@@ -246,6 +252,7 @@ const PreviewControls = ({
|
|
|
246
252
|
options={browserOptions}
|
|
247
253
|
value={selectedBrowser}
|
|
248
254
|
onChange={handleBrowserChangeWithCascade}
|
|
255
|
+
getPopupContainer={getDropdownPopupContainer}
|
|
249
256
|
/>
|
|
250
257
|
</CapColumn>
|
|
251
258
|
</CapRow>
|