@capillarytech/creatives-library 8.0.316-alpha.0 → 8.0.316-alpha.2
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/utils/common.js +6 -0
- package/v2Containers/WebPush/Create/components/MessageSection.js +45 -22
- package/v2Containers/WebPush/Create/components/MessageSection.test.js +54 -0
- package/v2Containers/WebPush/Create/components/__snapshots__/MessageSection.test.js.snap +23 -19
- package/v2Containers/WebPush/Create/hooks/useAiraTriggerPosition.js +80 -0
- package/v2Containers/WebPush/Create/hooks/useAiraTriggerPosition.test.js +210 -0
- package/v2Containers/WebPush/Create/index.js +3 -0
- package/v2Containers/WebPush/Create/index.scss +5 -0
package/package.json
CHANGED
package/utils/common.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
REGISTRATION_CUSTOM_FIELD,
|
|
17
17
|
JP_LOCALE_HIDE_FEATURE,
|
|
18
18
|
ENABLE_CUSTOMER_BARCODE_TAG,
|
|
19
|
+
AI_CONTENT_BOT_DISABLED,
|
|
19
20
|
BADGES_UI_ENABLED,
|
|
20
21
|
BADGES_ENROLL,
|
|
21
22
|
EMAIL_UNSUBSCRIBE_TAG_MANDATORY,
|
|
@@ -126,6 +127,11 @@ export const hasCustomerBarcodeFeatureEnabled = Auth.hasFeatureAccess.bind(
|
|
|
126
127
|
ENABLE_CUSTOMER_BARCODE_TAG,
|
|
127
128
|
);
|
|
128
129
|
|
|
130
|
+
export const isAiContentBotDisabled = Auth.hasFeatureAccess.bind(
|
|
131
|
+
null,
|
|
132
|
+
AI_CONTENT_BOT_DISABLED,
|
|
133
|
+
);
|
|
134
|
+
|
|
129
135
|
// Note: The "EMAIL_UNSUBSCRIBE_TAG_MANDATORY" feature flag determines if the Unsubscribe tag in email is optional.
|
|
130
136
|
// When this flag is enabled for an org, the Unsubscribe tag is NOT mandatory in the email flow.
|
|
131
137
|
// This is as per the requirement in the tech doc:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useRef } 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';
|
|
@@ -8,7 +8,9 @@ import CapError from '@capillarytech/cap-ui-library/CapError';
|
|
|
8
8
|
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
9
9
|
import CapEmojiPicker from '@capillarytech/cap-ui-library/CapEmojiPicker';
|
|
10
10
|
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
11
|
+
import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
|
|
11
12
|
import { MESSAGE_MAX_LENGTH, SHOW_CHARACTER_COUNT } from '../../constants';
|
|
13
|
+
import { useAiraTriggerPosition } from '../hooks/useAiraTriggerPosition';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* MessageSection component - Message textarea with tags, emoji picker, and character count
|
|
@@ -23,7 +25,15 @@ export const MessageSection = ({
|
|
|
23
25
|
messageCountRef,
|
|
24
26
|
messageTextAreaRef,
|
|
25
27
|
handleMessageTextAreaRef,
|
|
28
|
+
isAiContentBotDisabled,
|
|
26
29
|
}) => {
|
|
30
|
+
const messageInputWrapperRef = useRef(null);
|
|
31
|
+
const airaRootStyle = useAiraTriggerPosition({
|
|
32
|
+
wrapperRef: messageInputWrapperRef,
|
|
33
|
+
inputRef: messageTextAreaRef,
|
|
34
|
+
error,
|
|
35
|
+
});
|
|
36
|
+
|
|
27
37
|
const renderCharacterCount = () => {
|
|
28
38
|
if (!SHOW_CHARACTER_COUNT) return null;
|
|
29
39
|
|
|
@@ -50,29 +60,40 @@ export const MessageSection = ({
|
|
|
50
60
|
<CapHeading type="h4" className="webpush-message">
|
|
51
61
|
<FormattedMessage {...messages.message} />
|
|
52
62
|
</CapHeading>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
onChange={onChange}
|
|
56
|
-
textAreaRef={messageTextAreaRef}
|
|
57
|
-
>
|
|
58
|
-
<CapInput.TextArea
|
|
59
|
-
id="webpush-message-input"
|
|
63
|
+
<div className="webpush-message-input-wrapper" ref={messageInputWrapperRef}>
|
|
64
|
+
<CapEmojiPicker.Wrapper
|
|
60
65
|
value={value}
|
|
61
66
|
onChange={onChange}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
textAreaRef={messageTextAreaRef}
|
|
68
|
+
>
|
|
69
|
+
<CapInput.TextArea
|
|
70
|
+
id="webpush-message-input"
|
|
71
|
+
value={value}
|
|
72
|
+
onChange={onChange}
|
|
73
|
+
placeholder={formatMessage(messages.messagePlaceholder)}
|
|
74
|
+
size="default"
|
|
75
|
+
isRequired
|
|
76
|
+
autosize={{ minRows: 3, maxRows: 5 }}
|
|
77
|
+
setInputRef={handleMessageTextAreaRef}
|
|
78
|
+
errorMessage={
|
|
79
|
+
error && (
|
|
80
|
+
<CapError className="webpush-template-message-error">
|
|
81
|
+
{error}
|
|
82
|
+
</CapError>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
/>
|
|
86
|
+
</CapEmojiPicker.Wrapper>
|
|
87
|
+
{!isAiContentBotDisabled && (
|
|
88
|
+
<CapAskAira.ContentGenerationBot
|
|
89
|
+
text={value || ''}
|
|
90
|
+
setText={(text) => onChange(text)}
|
|
91
|
+
iconPlacement="float-br"
|
|
92
|
+
iconSize="1.6rem"
|
|
93
|
+
rootStyle={airaRootStyle}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
76
97
|
{renderCharacterCount()}
|
|
77
98
|
</CapRow>
|
|
78
99
|
<CapDivider className="webpush-message-divider" />
|
|
@@ -90,6 +111,7 @@ MessageSection.propTypes = {
|
|
|
90
111
|
messageCountRef: PropTypes.object,
|
|
91
112
|
messageTextAreaRef: PropTypes.object,
|
|
92
113
|
handleMessageTextAreaRef: PropTypes.func.isRequired,
|
|
114
|
+
isAiContentBotDisabled: PropTypes.bool,
|
|
93
115
|
};
|
|
94
116
|
|
|
95
117
|
MessageSection.defaultProps = {
|
|
@@ -97,6 +119,7 @@ MessageSection.defaultProps = {
|
|
|
97
119
|
tagList: null,
|
|
98
120
|
messageCountRef: null,
|
|
99
121
|
messageTextAreaRef: null,
|
|
122
|
+
isAiContentBotDisabled: false,
|
|
100
123
|
};
|
|
101
124
|
|
|
102
125
|
export default MessageSection;
|
|
@@ -8,6 +8,7 @@ import CapError from '@capillarytech/cap-ui-library/CapError';
|
|
|
8
8
|
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
9
9
|
import CapEmojiPicker from '@capillarytech/cap-ui-library/CapEmojiPicker';
|
|
10
10
|
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
11
|
+
import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
|
|
11
12
|
import { MESSAGE_MAX_LENGTH, SHOW_CHARACTER_COUNT } from '../../constants';
|
|
12
13
|
|
|
13
14
|
describe('MessageSection', () => {
|
|
@@ -45,6 +46,7 @@ describe('MessageSection', () => {
|
|
|
45
46
|
messageCountRef: null,
|
|
46
47
|
messageTextAreaRef: null,
|
|
47
48
|
handleMessageTextAreaRef: mockHandleMessageTextAreaRef,
|
|
49
|
+
isAiContentBotDisabled: true,
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
beforeEach(() => {
|
|
@@ -76,6 +78,32 @@ describe('MessageSection', () => {
|
|
|
76
78
|
expect(emojiPicker.prop('value')).toBe('Test Message');
|
|
77
79
|
});
|
|
78
80
|
|
|
81
|
+
it('should not render aiRA button when feature is disabled', () => {
|
|
82
|
+
const wrapper = mountWithIntl(<MessageSection {...defaultProps} />);
|
|
83
|
+
expect(wrapper.find(CapAskAira.ContentGenerationBot).exists()).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should render aiRA button when feature is enabled', () => {
|
|
87
|
+
const wrapper = mountWithIntl(
|
|
88
|
+
<MessageSection {...defaultProps} isAiContentBotDisabled={false} />
|
|
89
|
+
);
|
|
90
|
+
const airaBot = wrapper.find(CapAskAira.ContentGenerationBot);
|
|
91
|
+
expect(airaBot.exists()).toBe(true);
|
|
92
|
+
expect(airaBot.prop('text')).toBe('Test Message');
|
|
93
|
+
expect(airaBot.prop('rootStyle')).toMatchObject({
|
|
94
|
+
right: '2.5rem',
|
|
95
|
+
left: 'auto',
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should render aiRA and emoji controls together', () => {
|
|
100
|
+
const wrapper = mountWithIntl(
|
|
101
|
+
<MessageSection {...defaultProps} isAiContentBotDisabled={false} />
|
|
102
|
+
);
|
|
103
|
+
expect(wrapper.find(CapAskAira.ContentGenerationBot).exists()).toBe(true);
|
|
104
|
+
expect(wrapper.find(CapEmojiPicker.Wrapper).exists()).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
79
107
|
it('should render CapInput.TextArea with correct props', () => {
|
|
80
108
|
const wrapper = mountWithIntl(<MessageSection {...defaultProps} />);
|
|
81
109
|
const textArea = wrapper.find(CapInput.TextArea);
|
|
@@ -145,6 +173,23 @@ describe('MessageSection', () => {
|
|
|
145
173
|
const textArea = wrapper.find(CapInput.TextArea);
|
|
146
174
|
expect(textArea.prop('errorMessage')).toBeDefined();
|
|
147
175
|
});
|
|
176
|
+
|
|
177
|
+
it('should keep aiRA visible and anchored when error is present', () => {
|
|
178
|
+
const errorMessage = 'Message is required';
|
|
179
|
+
const wrapper = mountWithIntl(
|
|
180
|
+
<MessageSection
|
|
181
|
+
{...defaultProps}
|
|
182
|
+
error={errorMessage}
|
|
183
|
+
isAiContentBotDisabled={false}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
const airaBot = wrapper.find(CapAskAira.ContentGenerationBot);
|
|
187
|
+
expect(airaBot.exists()).toBe(true);
|
|
188
|
+
expect(airaBot.prop('rootStyle')).toMatchObject({
|
|
189
|
+
right: '2.5rem',
|
|
190
|
+
left: 'auto',
|
|
191
|
+
});
|
|
192
|
+
});
|
|
148
193
|
});
|
|
149
194
|
|
|
150
195
|
describe('Character Count', () => {
|
|
@@ -216,6 +261,15 @@ describe('MessageSection', () => {
|
|
|
216
261
|
emojiPicker.prop('onChange')('New Message');
|
|
217
262
|
expect(mockOnChange).toHaveBeenCalledWith('New Message');
|
|
218
263
|
});
|
|
264
|
+
|
|
265
|
+
it('should call onChange when aiRA returns generated text', () => {
|
|
266
|
+
const wrapper = mountWithIntl(
|
|
267
|
+
<MessageSection {...defaultProps} isAiContentBotDisabled={false} />
|
|
268
|
+
);
|
|
269
|
+
const airaBot = wrapper.find(CapAskAira.ContentGenerationBot);
|
|
270
|
+
airaBot.prop('setText')('Generated by aiRA');
|
|
271
|
+
expect(mockOnChange).toHaveBeenCalledWith('Generated by aiRA');
|
|
272
|
+
});
|
|
219
273
|
});
|
|
220
274
|
|
|
221
275
|
describe('Default Props', () => {
|
|
@@ -18,28 +18,32 @@ exports[`MessageSection Rendering should render correctly with default props 1`]
|
|
|
18
18
|
values={Object {}}
|
|
19
19
|
/>
|
|
20
20
|
</CapHeading>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
textAreaRef={null}
|
|
24
|
-
value="Test Message"
|
|
21
|
+
<div
|
|
22
|
+
className="webpush-message-input-wrapper"
|
|
25
23
|
>
|
|
26
|
-
<
|
|
27
|
-
autosize={
|
|
28
|
-
Object {
|
|
29
|
-
"maxRows": 5,
|
|
30
|
-
"minRows": 3,
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
errorMessage=""
|
|
34
|
-
id="webpush-message-input"
|
|
35
|
-
isRequired={true}
|
|
36
|
-
labelPosition="top"
|
|
24
|
+
<InjectIntl(Wrapper)
|
|
37
25
|
onChange={[MockFunction]}
|
|
38
|
-
|
|
39
|
-
size="default"
|
|
26
|
+
textAreaRef={null}
|
|
40
27
|
value="Test Message"
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
>
|
|
29
|
+
<_Class
|
|
30
|
+
autosize={
|
|
31
|
+
Object {
|
|
32
|
+
"maxRows": 5,
|
|
33
|
+
"minRows": 3,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
errorMessage=""
|
|
37
|
+
id="webpush-message-input"
|
|
38
|
+
isRequired={true}
|
|
39
|
+
labelPosition="top"
|
|
40
|
+
onChange={[MockFunction]}
|
|
41
|
+
setInputRef={[MockFunction]}
|
|
42
|
+
size="default"
|
|
43
|
+
value="Test Message"
|
|
44
|
+
/>
|
|
45
|
+
</InjectIntl(Wrapper)>
|
|
46
|
+
</div>
|
|
43
47
|
<CapLabel
|
|
44
48
|
className="webpush-character-count"
|
|
45
49
|
type="label2"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useLayoutEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BOTTOM = '0.83rem';
|
|
4
|
+
|
|
5
|
+
export const getBottomOffset = (wrapperElement, inputElement, hasError) => {
|
|
6
|
+
if (!wrapperElement || !inputElement) {
|
|
7
|
+
return DEFAULT_BOTTOM;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!hasError) {
|
|
11
|
+
return DEFAULT_BOTTOM;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const inputRect = inputElement.getBoundingClientRect();
|
|
15
|
+
const wrapperRect = wrapperElement.getBoundingClientRect();
|
|
16
|
+
const inputBottomFromWrapperTop = inputRect.bottom - wrapperRect.top;
|
|
17
|
+
const distanceFromWrapperBottom = wrapperRect.height - inputBottomFromWrapperTop;
|
|
18
|
+
|
|
19
|
+
return `${Math.max(distanceFromWrapperBottom + 4, 4)}px`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const useAiraTriggerPosition = ({
|
|
23
|
+
wrapperRef,
|
|
24
|
+
inputRef,
|
|
25
|
+
rightOffset = '2.5rem',
|
|
26
|
+
error,
|
|
27
|
+
}) => {
|
|
28
|
+
const [bottomOffset, setBottomOffset] = useState(DEFAULT_BOTTOM);
|
|
29
|
+
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
const wrapperElement = wrapperRef?.current;
|
|
32
|
+
if (!wrapperElement) {
|
|
33
|
+
setBottomOffset(DEFAULT_BOTTOM);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const updatePosition = () => {
|
|
38
|
+
const inputElement = inputRef?.current;
|
|
39
|
+
const hasError = Boolean(
|
|
40
|
+
error
|
|
41
|
+
|| wrapperElement.querySelector('.error-message')
|
|
42
|
+
|| wrapperElement.querySelector('.webpush-template-message-error'),
|
|
43
|
+
);
|
|
44
|
+
const nextOffset = getBottomOffset(wrapperElement, inputElement, hasError);
|
|
45
|
+
setBottomOffset((prevOffset) => (
|
|
46
|
+
prevOffset === nextOffset ? prevOffset : nextOffset
|
|
47
|
+
));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
updatePosition();
|
|
51
|
+
|
|
52
|
+
let resizeObserver = null;
|
|
53
|
+
|
|
54
|
+
if (window.ResizeObserver) {
|
|
55
|
+
resizeObserver = new window.ResizeObserver(() => {
|
|
56
|
+
updatePosition();
|
|
57
|
+
});
|
|
58
|
+
resizeObserver.observe(wrapperElement);
|
|
59
|
+
if (inputRef?.current) {
|
|
60
|
+
resizeObserver.observe(inputRef.current);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
if (resizeObserver) {
|
|
66
|
+
resizeObserver.disconnect();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}, [wrapperRef, inputRef, error]);
|
|
70
|
+
|
|
71
|
+
return useMemo(
|
|
72
|
+
() => ({
|
|
73
|
+
bottom: bottomOffset,
|
|
74
|
+
right: rightOffset,
|
|
75
|
+
left: 'auto',
|
|
76
|
+
}),
|
|
77
|
+
[bottomOffset, rightOffset],
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { getBottomOffset, useAiraTriggerPosition } from './useAiraTriggerPosition';
|
|
3
|
+
|
|
4
|
+
describe('useAiraTriggerPosition', () => {
|
|
5
|
+
let resizeObserverCallback;
|
|
6
|
+
let observeMock;
|
|
7
|
+
let disconnectMock;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
observeMock = jest.fn();
|
|
11
|
+
disconnectMock = jest.fn();
|
|
12
|
+
resizeObserverCallback = null;
|
|
13
|
+
|
|
14
|
+
global.ResizeObserver = jest.fn((cb) => {
|
|
15
|
+
resizeObserverCallback = cb;
|
|
16
|
+
return {
|
|
17
|
+
observe: observeMock,
|
|
18
|
+
disconnect: disconnectMock,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
global.ResizeObserver = jest.fn((cb) => {
|
|
26
|
+
resizeObserverCallback = cb;
|
|
27
|
+
return {
|
|
28
|
+
observe: observeMock,
|
|
29
|
+
disconnect: disconnectMock,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const createRefs = () => {
|
|
35
|
+
const wrapperElement = document.createElement('div');
|
|
36
|
+
const inputElement = document.createElement('textarea');
|
|
37
|
+
wrapperElement.appendChild(inputElement);
|
|
38
|
+
|
|
39
|
+
const wrapperRef = { current: wrapperElement };
|
|
40
|
+
const inputRef = { current: inputElement };
|
|
41
|
+
|
|
42
|
+
return { wrapperElement, inputElement, wrapperRef, inputRef };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
it('should return default style when there is no error', () => {
|
|
46
|
+
const { wrapperRef, inputRef } = createRefs();
|
|
47
|
+
|
|
48
|
+
const { result } = renderHook(() => useAiraTriggerPosition({
|
|
49
|
+
wrapperRef,
|
|
50
|
+
inputRef,
|
|
51
|
+
rightOffset: '2.5rem',
|
|
52
|
+
value: 'hello',
|
|
53
|
+
error: '',
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
expect(result.current).toEqual({
|
|
57
|
+
bottom: '0.83rem',
|
|
58
|
+
right: '2.5rem',
|
|
59
|
+
left: 'auto',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should compute bottom offset when error is present', () => {
|
|
64
|
+
const { wrapperElement, inputElement, wrapperRef, inputRef } = createRefs();
|
|
65
|
+
|
|
66
|
+
const errorElement = document.createElement('div');
|
|
67
|
+
errorElement.className = 'webpush-template-message-error';
|
|
68
|
+
wrapperElement.appendChild(errorElement);
|
|
69
|
+
|
|
70
|
+
wrapperElement.getBoundingClientRect = jest.fn(() => ({
|
|
71
|
+
top: 0,
|
|
72
|
+
bottom: 160,
|
|
73
|
+
height: 160,
|
|
74
|
+
left: 0,
|
|
75
|
+
right: 100,
|
|
76
|
+
width: 100,
|
|
77
|
+
}));
|
|
78
|
+
inputElement.getBoundingClientRect = jest.fn(() => ({
|
|
79
|
+
top: 20,
|
|
80
|
+
bottom: 100,
|
|
81
|
+
height: 80,
|
|
82
|
+
left: 0,
|
|
83
|
+
right: 100,
|
|
84
|
+
width: 100,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const { result } = renderHook(() => useAiraTriggerPosition({
|
|
88
|
+
wrapperRef,
|
|
89
|
+
inputRef,
|
|
90
|
+
rightOffset: '2.5rem',
|
|
91
|
+
value: 'hello',
|
|
92
|
+
error: 'Required',
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
expect(result.current).toEqual({
|
|
96
|
+
bottom: '64px',
|
|
97
|
+
right: '2.5rem',
|
|
98
|
+
left: 'auto',
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should observe wrapper and input for layout changes', () => {
|
|
103
|
+
const { wrapperElement, inputElement, wrapperRef, inputRef } = createRefs();
|
|
104
|
+
|
|
105
|
+
renderHook(() => useAiraTriggerPosition({
|
|
106
|
+
wrapperRef,
|
|
107
|
+
inputRef,
|
|
108
|
+
rightOffset: '2.5rem',
|
|
109
|
+
value: '',
|
|
110
|
+
error: '',
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
expect(observeMock).toHaveBeenCalledWith(wrapperElement);
|
|
114
|
+
expect(observeMock).toHaveBeenCalledWith(inputElement);
|
|
115
|
+
expect(typeof resizeObserverCallback).toBe('function');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should invoke updatePosition when ResizeObserver fires', () => {
|
|
119
|
+
const { wrapperElement, inputElement, wrapperRef, inputRef } = createRefs();
|
|
120
|
+
|
|
121
|
+
renderHook(() => useAiraTriggerPosition({
|
|
122
|
+
wrapperRef,
|
|
123
|
+
inputRef,
|
|
124
|
+
rightOffset: '2.5rem',
|
|
125
|
+
error: '',
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
expect(resizeObserverCallback).toBeTruthy();
|
|
129
|
+
resizeObserverCallback();
|
|
130
|
+
expect(observeMock).toHaveBeenCalledWith(wrapperElement);
|
|
131
|
+
expect(observeMock).toHaveBeenCalledWith(inputElement);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should not observe input when inputRef has no current element', () => {
|
|
135
|
+
const { wrapperElement, wrapperRef } = createRefs();
|
|
136
|
+
|
|
137
|
+
renderHook(() => useAiraTriggerPosition({
|
|
138
|
+
wrapperRef,
|
|
139
|
+
inputRef: { current: null },
|
|
140
|
+
rightOffset: '2.5rem',
|
|
141
|
+
error: '',
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
expect(observeMock).toHaveBeenCalledWith(wrapperElement);
|
|
145
|
+
expect(observeMock).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should use default right offset when omitted', () => {
|
|
149
|
+
const { wrapperRef, inputRef } = createRefs();
|
|
150
|
+
|
|
151
|
+
const { result } = renderHook(() => useAiraTriggerPosition({
|
|
152
|
+
wrapperRef,
|
|
153
|
+
inputRef,
|
|
154
|
+
error: '',
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
expect(result.current.right).toBe('2.5rem');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should use default bottom when wrapper ref is not attached', () => {
|
|
161
|
+
const wrapperRef = { current: null };
|
|
162
|
+
const inputRef = { current: null };
|
|
163
|
+
|
|
164
|
+
const { result } = renderHook(() => useAiraTriggerPosition({
|
|
165
|
+
wrapperRef,
|
|
166
|
+
inputRef,
|
|
167
|
+
rightOffset: '2.5rem',
|
|
168
|
+
error: '',
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
expect(result.current).toEqual({
|
|
172
|
+
bottom: '0.83rem',
|
|
173
|
+
right: '2.5rem',
|
|
174
|
+
left: 'auto',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should skip disconnect when ResizeObserver is unavailable', () => {
|
|
179
|
+
const saved = global.ResizeObserver;
|
|
180
|
+
delete global.ResizeObserver;
|
|
181
|
+
|
|
182
|
+
const { wrapperRef, inputRef } = createRefs();
|
|
183
|
+
|
|
184
|
+
const { unmount } = renderHook(() => useAiraTriggerPosition({
|
|
185
|
+
wrapperRef,
|
|
186
|
+
inputRef,
|
|
187
|
+
rightOffset: '2.5rem',
|
|
188
|
+
error: '',
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
expect(disconnectMock).not.toHaveBeenCalled();
|
|
192
|
+
unmount();
|
|
193
|
+
expect(disconnectMock).not.toHaveBeenCalled();
|
|
194
|
+
|
|
195
|
+
global.ResizeObserver = saved;
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('getBottomOffset', () => {
|
|
200
|
+
it('returns default when wrapper element is missing', () => {
|
|
201
|
+
const input = document.createElement('textarea');
|
|
202
|
+
expect(getBottomOffset(null, input, true)).toBe('0.83rem');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('returns default when input element is missing', () => {
|
|
206
|
+
const wrapper = document.createElement('div');
|
|
207
|
+
expect(getBottomOffset(wrapper, null, true)).toBe('0.83rem');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
} from '../../Cap/selectors';
|
|
73
73
|
import './index.scss';
|
|
74
74
|
import { WEBPUSH } from '../../CreativesContainer/constants';
|
|
75
|
+
import { isAiContentBotDisabled } from '../../../utils/common';
|
|
75
76
|
|
|
76
77
|
// Memoized TagList wrapper components for better performance
|
|
77
78
|
const MemoizedTagList = memo(({
|
|
@@ -156,6 +157,7 @@ const WebPushCreate = ({
|
|
|
156
157
|
restrictPersonalization = false,
|
|
157
158
|
}) => {
|
|
158
159
|
const { formatMessage } = intl;
|
|
160
|
+
const aiContentBotDisabled = isAiContentBotDisabled();
|
|
159
161
|
|
|
160
162
|
// Form state - kept in main component for now
|
|
161
163
|
const [templateName, setTemplateName] = useState('');
|
|
@@ -997,6 +999,7 @@ const WebPushCreate = ({
|
|
|
997
999
|
messageCountRef={messageCountRef}
|
|
998
1000
|
messageTextAreaRef={messageTextAreaRef}
|
|
999
1001
|
handleMessageTextAreaRef={handleMessageTextAreaRef}
|
|
1002
|
+
isAiContentBotDisabled={aiContentBotDisabled}
|
|
1000
1003
|
/>
|
|
1001
1004
|
<MediaSection
|
|
1002
1005
|
mediaType={mediaType}
|