@capillarytech/creatives-library 9.0.3 → 9.0.5

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": "9.0.3",
4
+ "version": "9.0.5",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -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
- this.textArea.focus();
49
- const textAreaRef = this.textArea.textAreaRef;
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
- this.textArea.focus();
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
- setInputRef={this.setInputRef}
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
+ });
@@ -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
- setInputRef={this.setInputRef}
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
- this.textArea.focus();
533
- const textAreaRef = this.textArea.textAreaRef;
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
- this.textArea.focus();
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">
@@ -146,7 +152,7 @@ MessageSection.propTypes = {
146
152
  tagList: PropTypes.node,
147
153
  messageCountRef: PropTypes.object,
148
154
  messageTextAreaRef: PropTypes.object,
149
- handleMessageTextAreaRef: PropTypes.func.isRequired,
155
+ handleMessageTextAreaRef: PropTypes.func,
150
156
  isAiContentBotDisabled: PropTypes.bool,
151
157
  };
152
158
 
@@ -155,6 +161,7 @@ MessageSection.defaultProps = {
155
161
  tagList: null,
156
162
  messageCountRef: null,
157
163
  messageTextAreaRef: null,
164
+ handleMessageTextAreaRef: () => {},
158
165
  isAiContentBotDisabled: false,
159
166
  };
160
167
 
@@ -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 call handleMessageTextAreaRef with setInputRef', () => {
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')).toBe(mockHandleMessageTextAreaRef);
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', () => {
@@ -47,7 +47,6 @@ exports[`MessageSection Rendering should render correctly with default props 1`]
47
47
  id="webpush-message-input"
48
48
  isRequired={true}
49
49
  onChange={[MockFunction]}
50
- setInputRef={[MockFunction]}
51
50
  size="default"
52
51
  value="Test Message"
53
52
  />
@@ -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>
@@ -700,6 +700,147 @@ describe('PreviewControls', () => {
700
700
  });
701
701
  });
702
702
 
703
+ describe('getPopupContainer (getDropdownPopupContainer)', () => {
704
+ const getResolver = (wrapper) =>
705
+ wrapper.find(CapSelect.CapCustomSelect).first().prop('getPopupContainer');
706
+
707
+ let appended;
708
+
709
+ beforeEach(() => {
710
+ appended = [];
711
+ });
712
+
713
+ afterEach(() => {
714
+ // Keep jsdom clean between assertions so containers from one test don't leak
715
+ appended.forEach((node) => {
716
+ if (node && node.parentNode) {
717
+ node.parentNode.removeChild(node);
718
+ }
719
+ });
720
+ appended = [];
721
+ });
722
+
723
+ const mountInBody = (node) => {
724
+ document.body.appendChild(node);
725
+ appended.push(node);
726
+ return node;
727
+ };
728
+
729
+ it('should pass getPopupContainer to every rendered select', () => {
730
+ const wrapper = mountWithIntl(<PreviewControls {...defaultProps} showStateDropdown={true} />);
731
+ const selects = wrapper.find(CapSelect.CapCustomSelect);
732
+ expect(selects.length).toBe(3);
733
+ selects.forEach((select) => {
734
+ expect(typeof select.prop('getPopupContainer')).toBe('function');
735
+ });
736
+ });
737
+
738
+ it('should return the closest .ant-modal-container ancestor when one exists', () => {
739
+ const wrapper = mountWithIntl(<PreviewControls {...defaultProps} />);
740
+ const getPopupContainer = getResolver(wrapper);
741
+
742
+ const modalContainer = document.createElement('div');
743
+ modalContainer.className = 'ant-modal-container';
744
+ const triggerNode = document.createElement('span');
745
+ modalContainer.appendChild(triggerNode);
746
+ mountInBody(modalContainer);
747
+
748
+ expect(getPopupContainer(triggerNode)).toBe(modalContainer);
749
+ });
750
+
751
+ it('should return the closest .ant-modal-content ancestor when one exists', () => {
752
+ const wrapper = mountWithIntl(<PreviewControls {...defaultProps} />);
753
+ const getPopupContainer = getResolver(wrapper);
754
+
755
+ const modalContent = document.createElement('div');
756
+ modalContent.className = 'ant-modal-content';
757
+ const triggerNode = document.createElement('span');
758
+ modalContent.appendChild(triggerNode);
759
+ mountInBody(modalContent);
760
+
761
+ expect(getPopupContainer(triggerNode)).toBe(modalContent);
762
+ });
763
+
764
+ it('should resolve through deeply nested descendants up to the modal ancestor', () => {
765
+ const wrapper = mountWithIntl(<PreviewControls {...defaultProps} />);
766
+ const getPopupContainer = getResolver(wrapper);
767
+
768
+ const modalContainer = document.createElement('div');
769
+ modalContainer.className = 'ant-modal-container';
770
+ const middle = document.createElement('div');
771
+ const triggerNode = document.createElement('span');
772
+ middle.appendChild(triggerNode);
773
+ modalContainer.appendChild(middle);
774
+ mountInBody(modalContainer);
775
+
776
+ expect(getPopupContainer(triggerNode)).toBe(modalContainer);
777
+ });
778
+
779
+ it('should match the trigger node itself when it is the modal container', () => {
780
+ const wrapper = mountWithIntl(<PreviewControls {...defaultProps} />);
781
+ const getPopupContainer = getResolver(wrapper);
782
+
783
+ const modalContainer = document.createElement('div');
784
+ modalContainer.className = 'ant-modal-content';
785
+ mountInBody(modalContainer);
786
+
787
+ // closest() includes the element itself, so a self-match should be returned
788
+ expect(getPopupContainer(modalContainer)).toBe(modalContainer);
789
+ });
790
+
791
+ it('should pick the nearest modal ancestor when modals are nested', () => {
792
+ const wrapper = mountWithIntl(<PreviewControls {...defaultProps} />);
793
+ const getPopupContainer = getResolver(wrapper);
794
+
795
+ const outerModal = document.createElement('div');
796
+ outerModal.className = 'ant-modal-container';
797
+ const innerModal = document.createElement('div');
798
+ innerModal.className = 'ant-modal-content';
799
+ const triggerNode = document.createElement('span');
800
+ innerModal.appendChild(triggerNode);
801
+ outerModal.appendChild(innerModal);
802
+ mountInBody(outerModal);
803
+
804
+ expect(getPopupContainer(triggerNode)).toBe(innerModal);
805
+ });
806
+
807
+ it('should fall back to document.body when the trigger is not inside a modal', () => {
808
+ const wrapper = mountWithIntl(<PreviewControls {...defaultProps} />);
809
+ const getPopupContainer = getResolver(wrapper);
810
+
811
+ const wrapperDiv = document.createElement('div');
812
+ wrapperDiv.className = 'some-unrelated-container';
813
+ const triggerNode = document.createElement('span');
814
+ wrapperDiv.appendChild(triggerNode);
815
+ mountInBody(wrapperDiv);
816
+
817
+ expect(getPopupContainer(triggerNode)).toBe(document.body);
818
+ });
819
+
820
+ it('should fall back to document.body for a detached (unattached) trigger node', () => {
821
+ const wrapper = mountWithIntl(<PreviewControls {...defaultProps} />);
822
+ const getPopupContainer = getResolver(wrapper);
823
+
824
+ const detachedNode = document.createElement('span');
825
+ expect(getPopupContainer(detachedNode)).toBe(document.body);
826
+ });
827
+
828
+ it('should resolve the same way for the compact layout selects', () => {
829
+ const wrapper = mountWithIntl(
830
+ <PreviewControls {...defaultProps} layoutMode={LAYOUT_MODE.COMPACT} showStateDropdown={true} />,
831
+ );
832
+ const getPopupContainer = getResolver(wrapper);
833
+
834
+ const modalContainer = document.createElement('div');
835
+ modalContainer.className = 'ant-modal-container';
836
+ const triggerNode = document.createElement('span');
837
+ modalContainer.appendChild(triggerNode);
838
+ mountInBody(modalContainer);
839
+
840
+ expect(getPopupContainer(triggerNode)).toBe(modalContainer);
841
+ });
842
+ });
843
+
703
844
  describe('FormattedMessage Integration', () => {
704
845
  it('should render FormattedMessage for operatingSystem label', () => {
705
846
  const wrapper = mountWithIntl(<PreviewControls {...defaultProps} />);
@@ -21,6 +21,7 @@ exports[`PreviewControls Basic Rendering should render correctly with default pr
21
21
  />
22
22
  </CapLabel>
23
23
  <ComponentWithLabel(InjectIntl(CapCustomSelect))
24
+ getPopupContainer={[Function]}
24
25
  onChange={[Function]}
25
26
  options={
26
27
  Array [
@@ -70,6 +71,7 @@ exports[`PreviewControls Basic Rendering should render correctly with default pr
70
71
  />
71
72
  </CapLabel>
72
73
  <ComponentWithLabel(InjectIntl(CapCustomSelect))
74
+ getPopupContainer={[Function]}
73
75
  onChange={[Function]}
74
76
  options={
75
77
  Array [
@@ -119,6 +121,7 @@ exports[`PreviewControls Basic Rendering should render correctly with default pr
119
121
  />
120
122
  </CapLabel>
121
123
  <ComponentWithLabel(InjectIntl(CapCustomSelect))
124
+ getPopupContainer={[Function]}
122
125
  onChange={[MockFunction]}
123
126
  options={
124
127
  Array [