@capillarytech/creatives-library 8.0.318 → 8.0.320
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/constants/unified.js +15 -0
- package/package.json +1 -1
- package/services/api.js +6 -0
- package/services/tests/api.test.js +7 -0
- package/utils/common.js +6 -1
- package/utils/templateVarUtils.js +172 -0
- package/utils/tests/templateVarUtils.test.js +160 -0
- package/v2Components/CapTagList/index.js +10 -0
- package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
- package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
- package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
- package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
- package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
- package/v2Components/CommonTestAndPreview/constants.js +38 -0
- package/v2Components/CommonTestAndPreview/index.js +693 -155
- package/v2Components/CommonTestAndPreview/messages.js +41 -3
- package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
- package/v2Components/CommonTestAndPreview/sagas.js +15 -6
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
- package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
- package/v2Components/FormBuilder/index.js +7 -1
- package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
- package/v2Components/SmsFallback/constants.js +73 -0
- package/v2Components/SmsFallback/index.js +956 -0
- package/v2Components/SmsFallback/index.scss +265 -0
- package/v2Components/SmsFallback/messages.js +78 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
- package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
- package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
- package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
- package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
- package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
- package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
- package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
- package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
- package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
- package/v2Components/VarSegmentMessageEditor/index.js +125 -0
- package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
- package/v2Containers/CommunicationFlow/constants.js +200 -0
- package/v2Containers/CommunicationFlow/index.js +102 -0
- package/v2Containers/CommunicationFlow/messages.js +346 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
- package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
- package/v2Containers/CreativesContainer/constants.js +12 -0
- package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
- package/v2Containers/CreativesContainer/index.js +289 -99
- package/v2Containers/CreativesContainer/index.scss +51 -1
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
- package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
- package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
- package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
- package/v2Containers/Rcs/constants.js +32 -1
- package/v2Containers/Rcs/index.js +950 -873
- package/v2Containers/Rcs/index.scss +85 -6
- package/v2Containers/Rcs/messages.js +10 -1
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
- package/v2Containers/Rcs/tests/index.test.js +41 -38
- package/v2Containers/Rcs/tests/mockData.js +38 -0
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
- package/v2Containers/Rcs/tests/utils.test.js +379 -1
- package/v2Containers/Rcs/utils.js +358 -10
- package/v2Containers/Sms/Create/index.js +81 -36
- package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
- package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
- package/v2Containers/SmsTrai/Create/index.js +9 -4
- package/v2Containers/SmsTrai/Edit/constants.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +609 -128
- package/v2Containers/SmsTrai/Edit/index.scss +121 -0
- package/v2Containers/SmsTrai/Edit/messages.js +9 -4
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
- package/v2Containers/SmsWrapper/index.js +37 -8
- package/v2Containers/TagList/index.js +6 -0
- package/v2Containers/Templates/TemplatesActionBar.js +101 -0
- package/v2Containers/Templates/_templates.scss +61 -2
- package/v2Containers/Templates/actions.js +11 -0
- package/v2Containers/Templates/constants.js +2 -0
- package/v2Containers/Templates/index.js +90 -40
- package/v2Containers/Templates/sagas.js +57 -12
- package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
- package/v2Containers/Templates/tests/sagas.test.js +193 -12
- package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
- package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
- package/v2Containers/TemplatesV2/index.js +86 -23
- package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
- package/v2Containers/Whatsapp/index.js +3 -20
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Object} options
|
|
5
|
+
* @param {(params: { page: number, search: string, reset: boolean }) => Promise<{ templates: Array, totalCount: number }>} options.fetchTemplates
|
|
6
|
+
* @param {number} [options.perPage=25]
|
|
7
|
+
*/
|
|
8
|
+
export function useLocalTemplateList({ fetchTemplates, perPage = 25 }) {
|
|
9
|
+
const [listData, setListData] = useState({ templates: [], totalCount: 0 });
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const [page, setPage] = useState(1);
|
|
12
|
+
const [search, setSearchState] = useState('');
|
|
13
|
+
const searchRef = useRef('');
|
|
14
|
+
/** Drops stale responses when a newer fetch starts (search / reset while a request is in flight). */
|
|
15
|
+
const fetchGenerationRef = useRef(0);
|
|
16
|
+
const setSearch = useCallback((value) => {
|
|
17
|
+
const term = typeof value === 'string' ? value : '';
|
|
18
|
+
searchRef.current = term;
|
|
19
|
+
setSearchState(term);
|
|
20
|
+
}, []);
|
|
21
|
+
const lastFetchFullPageRef = useRef(false);
|
|
22
|
+
|
|
23
|
+
const { templates = [], totalCount = 0 } = listData ?? {};
|
|
24
|
+
const hasKnownTotal = (totalCount ?? 0) > 0;
|
|
25
|
+
const hasMoreByTotal = (totalCount ?? 0) > (templates?.length ?? 0);
|
|
26
|
+
const hasMoreByFullPage =
|
|
27
|
+
!hasKnownTotal && lastFetchFullPageRef.current && (templates?.length ?? 0) > 0;
|
|
28
|
+
const canLoadMore = (hasMoreByTotal || hasMoreByFullPage) && !loading;
|
|
29
|
+
|
|
30
|
+
const runFetch = useCallback(
|
|
31
|
+
async ({ page: p = 1, reset = true, search: searchTerm } = {}) => {
|
|
32
|
+
const term = searchTerm !== undefined ? searchTerm : searchRef.current;
|
|
33
|
+
const gen = ++fetchGenerationRef.current;
|
|
34
|
+
setLoading(true);
|
|
35
|
+
try {
|
|
36
|
+
const result = await fetchTemplates({ page: p, search: term, reset });
|
|
37
|
+
if (gen !== fetchGenerationRef.current) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const nextTemplates = result?.templates ?? [];
|
|
41
|
+
const nextTotalCount = result?.totalCount ?? 0;
|
|
42
|
+
lastFetchFullPageRef.current = nextTemplates.length >= perPage;
|
|
43
|
+
setListData((prev) => ({
|
|
44
|
+
templates: reset ? nextTemplates : [...(prev.templates || []), ...nextTemplates],
|
|
45
|
+
totalCount: nextTotalCount > 0 ? nextTotalCount : (reset ? 0 : prev.totalCount),
|
|
46
|
+
}));
|
|
47
|
+
setPage(p);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (gen !== fetchGenerationRef.current) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
lastFetchFullPageRef.current = false;
|
|
53
|
+
if (reset) {
|
|
54
|
+
setListData({ templates: [], totalCount: 0 });
|
|
55
|
+
setPage(1);
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
if (gen === fetchGenerationRef.current) {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[fetchTemplates, perPage]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const loadMore = useCallback(() => {
|
|
67
|
+
if (!canLoadMore) return;
|
|
68
|
+
runFetch({ page: page + 1, reset: false, search: searchRef.current });
|
|
69
|
+
}, [canLoadMore, page, runFetch]);
|
|
70
|
+
|
|
71
|
+
const reset = useCallback(
|
|
72
|
+
(searchTerm) => {
|
|
73
|
+
const term = searchTerm !== undefined ? searchTerm : searchRef.current;
|
|
74
|
+
setSearch(term);
|
|
75
|
+
lastFetchFullPageRef.current = false;
|
|
76
|
+
runFetch({ page: 1, reset: true, search: term });
|
|
77
|
+
},
|
|
78
|
+
[runFetch, setSearch]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
templates,
|
|
83
|
+
totalCount,
|
|
84
|
+
loading,
|
|
85
|
+
page,
|
|
86
|
+
search,
|
|
87
|
+
setSearch,
|
|
88
|
+
loadMore,
|
|
89
|
+
reset,
|
|
90
|
+
canLoadMore,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -18,7 +18,7 @@ import injectReducer from '../../utils/injectReducer';
|
|
|
18
18
|
import injectSaga from '../../utils/injectSaga';
|
|
19
19
|
|
|
20
20
|
import CommonTestAndPreview from '../CommonTestAndPreview';
|
|
21
|
-
import { CHANNELS } from '../CommonTestAndPreview/constants';
|
|
21
|
+
import { CHANNELS, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../CommonTestAndPreview/constants';
|
|
22
22
|
import * as commonTestAndPreviewActions from '../CommonTestAndPreview/actions';
|
|
23
23
|
import { commonTestAndPreviewSaga } from '../CommonTestAndPreview/sagas';
|
|
24
24
|
import commonTestAndPreviewReducer from '../CommonTestAndPreview/reducer';
|
|
@@ -78,6 +78,12 @@ TestAndPreviewSlidebox.propTypes = {
|
|
|
78
78
|
content: PropTypes.string,
|
|
79
79
|
beeInstance: PropTypes.object,
|
|
80
80
|
currentTab: PropTypes.number,
|
|
81
|
+
smsFallbackContent: PropTypes.shape({
|
|
82
|
+
templateContent: PropTypes.string,
|
|
83
|
+
senderId: PropTypes.string,
|
|
84
|
+
templateName: PropTypes.string,
|
|
85
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: PropTypes.object,
|
|
86
|
+
}),
|
|
81
87
|
// Redux props are passed through
|
|
82
88
|
actions: PropTypes.object.isRequired,
|
|
83
89
|
extractedTags: PropTypes.array.isRequired,
|
|
@@ -113,6 +119,7 @@ TestAndPreviewSlidebox.defaultProps = {
|
|
|
113
119
|
wecrmAccounts: [],
|
|
114
120
|
isLoadingSenderDetails: false,
|
|
115
121
|
orgUnitId: -1,
|
|
122
|
+
smsFallbackContent: null,
|
|
116
123
|
};
|
|
117
124
|
|
|
118
125
|
const mapStateToProps = createStructuredSelector({
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
GET_PREFILLED_VALUES_SUCCESS,
|
|
35
35
|
GET_PREFILLED_VALUES_FAILURE,
|
|
36
36
|
} from './constants';
|
|
37
|
+
import { extractPreviewFromLiquidResponse } from '../CommonTestAndPreview/previewApiUtils';
|
|
37
38
|
|
|
38
39
|
// Search Customers Saga
|
|
39
40
|
export function* searchCustomersSaga(action) {
|
|
@@ -80,11 +81,12 @@ export function* updatePreviewSaga(action) {
|
|
|
80
81
|
const customValues = action.payload.resolvedTags;
|
|
81
82
|
|
|
82
83
|
const response = yield call(Api.updateEmailPreview, action.payload);
|
|
83
|
-
|
|
84
|
+
const previewPayload = extractPreviewFromLiquidResponse(response);
|
|
85
|
+
if (previewPayload) {
|
|
84
86
|
yield put({
|
|
85
87
|
type: UPDATE_PREVIEW_SUCCESS,
|
|
86
88
|
payload: {
|
|
87
|
-
previewData:
|
|
89
|
+
previewData: previewPayload,
|
|
88
90
|
customValues, // Pass custom values to be preserved
|
|
89
91
|
},
|
|
90
92
|
});
|
|
@@ -221,8 +223,13 @@ export function* createMessageMetaSaga(action) {
|
|
|
221
223
|
export function* getPrefilledValuesSaga(action) {
|
|
222
224
|
try {
|
|
223
225
|
const response = yield call(Api.updateEmailPreview, action.payload);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
const body =
|
|
227
|
+
response?.data !== undefined && response?.data !== null
|
|
228
|
+
? response.data
|
|
229
|
+
: response;
|
|
230
|
+
const resolvedTagValues = body?.resolvedTagValues;
|
|
231
|
+
if (resolvedTagValues != null) {
|
|
232
|
+
yield put({ type: GET_PREFILLED_VALUES_SUCCESS, payload: { values: resolvedTagValues } });
|
|
226
233
|
} else {
|
|
227
234
|
yield put({ type: GET_PREFILLED_VALUES_FAILURE, payload: { error: response.error || 'Failed to fetch prefilled values' } });
|
|
228
235
|
}
|
|
@@ -136,7 +136,9 @@ describe('TestAndPreviewSlidebox Sagas', () => {
|
|
|
136
136
|
describe('updatePreviewSaga', () => {
|
|
137
137
|
it('should handle successful preview update', () => {
|
|
138
138
|
const mockResponse = {
|
|
139
|
-
data:
|
|
139
|
+
data: {
|
|
140
|
+
resolvedBody: 'Test Preview Data',
|
|
141
|
+
},
|
|
140
142
|
};
|
|
141
143
|
const customValues = { test: 'value' };
|
|
142
144
|
return expectSaga(sagas.updatePreviewSaga, {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared message editor that renders template text with {{var}} and/or DLT `{#var#}` segments as
|
|
3
|
+
* variable inputs and static text as headings.
|
|
4
|
+
* Reused by RCS (title/description), SmsTrai Edit (SMS fallback), and WhatsApp (edit message/header).
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import PropTypes from 'prop-types';
|
|
8
|
+
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
9
|
+
import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
|
|
10
|
+
import CapInput from '@capillarytech/cap-ui-library/CapInput';
|
|
11
|
+
import {
|
|
12
|
+
splitTemplateVarString,
|
|
13
|
+
DEFAULT_MUSTACHE_VAR_REGEX,
|
|
14
|
+
isAnyTemplateVarToken,
|
|
15
|
+
} from '../../utils/templateVarUtils';
|
|
16
|
+
|
|
17
|
+
import './index.scss';
|
|
18
|
+
import { VAR_SEGMENT_PLACEHOLDER_PREFIX } from './constants';
|
|
19
|
+
|
|
20
|
+
const { TextArea } = CapInput;
|
|
21
|
+
|
|
22
|
+
export function VarSegmentMessageEditor({
|
|
23
|
+
templateString = '',
|
|
24
|
+
valueMap = {},
|
|
25
|
+
onChange,
|
|
26
|
+
onFocus,
|
|
27
|
+
placeholderPrefix = VAR_SEGMENT_PLACEHOLDER_PREFIX,
|
|
28
|
+
getPlaceholder,
|
|
29
|
+
wrapperClassName = 'rcs_text_area_wrapper',
|
|
30
|
+
rowClassName = 'rcs-edit-template-message-input',
|
|
31
|
+
headingClassName = 'rcs-edit-template-message-split',
|
|
32
|
+
varRegex,
|
|
33
|
+
readOnly = false,
|
|
34
|
+
disabled = false,
|
|
35
|
+
footerContent,
|
|
36
|
+
renderVarFooter,
|
|
37
|
+
}) {
|
|
38
|
+
const segments = splitTemplateVarString(templateString, varRegex || DEFAULT_MUSTACHE_VAR_REGEX);
|
|
39
|
+
if (!segments?.length) return null;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={wrapperClassName}>
|
|
43
|
+
<CapRow className={rowClassName}>
|
|
44
|
+
{segments.map((segmentToken, segmentIndex) => {
|
|
45
|
+
const isVar =
|
|
46
|
+
typeof segmentToken === 'string' && isAnyTemplateVarToken(segmentToken);
|
|
47
|
+
if (isVar) {
|
|
48
|
+
const varSegmentFieldId = `${segmentToken}_${segmentIndex}`;
|
|
49
|
+
const slotValueFromMap = valueMap?.[varSegmentFieldId];
|
|
50
|
+
// Missing key: show empty (not the raw {{…}} token) so cleared slots and incomplete maps
|
|
51
|
+
// cannot resurrect the token; placeholder still guides the user.
|
|
52
|
+
const value =
|
|
53
|
+
slotValueFromMap !== undefined && slotValueFromMap !== null ? slotValueFromMap : '';
|
|
54
|
+
if (readOnly) {
|
|
55
|
+
return (
|
|
56
|
+
<CapHeading
|
|
57
|
+
key={varSegmentFieldId}
|
|
58
|
+
type="h4"
|
|
59
|
+
className={`${headingClassName} var-segment-message-editor__read-only-value`.trim()}
|
|
60
|
+
>
|
|
61
|
+
{value}
|
|
62
|
+
</CapHeading>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const fromGet = getPlaceholder && getPlaceholder(segmentToken, segmentIndex);
|
|
66
|
+
const placeholder =
|
|
67
|
+
fromGet !== undefined && fromGet !== null && fromGet !== ''
|
|
68
|
+
? fromGet
|
|
69
|
+
: `${placeholderPrefix}${segmentToken}`;
|
|
70
|
+
return (
|
|
71
|
+
<div key={varSegmentFieldId} className="var-segment-message-editor__var-slot">
|
|
72
|
+
<TextArea
|
|
73
|
+
id={varSegmentFieldId}
|
|
74
|
+
placeholder={placeholder}
|
|
75
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
76
|
+
value={value}
|
|
77
|
+
onFocus={() => onFocus && onFocus(varSegmentFieldId)}
|
|
78
|
+
onChange={(e) =>
|
|
79
|
+
onChange && onChange(varSegmentFieldId, e?.target?.value ?? '')}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
/>
|
|
82
|
+
{renderVarFooter
|
|
83
|
+
? renderVarFooter(segmentToken, segmentIndex, varSegmentFieldId)
|
|
84
|
+
: null}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (segmentToken) {
|
|
89
|
+
return (
|
|
90
|
+
<CapHeading
|
|
91
|
+
key={`static_${segmentIndex}_${segmentToken}`}
|
|
92
|
+
type="h4"
|
|
93
|
+
className={headingClassName}
|
|
94
|
+
>
|
|
95
|
+
{segmentToken}
|
|
96
|
+
</CapHeading>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
})}
|
|
101
|
+
</CapRow>
|
|
102
|
+
{footerContent}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
VarSegmentMessageEditor.propTypes = {
|
|
108
|
+
templateString: PropTypes.string,
|
|
109
|
+
valueMap: PropTypes.object,
|
|
110
|
+
onChange: PropTypes.func,
|
|
111
|
+
onFocus: PropTypes.func,
|
|
112
|
+
placeholderPrefix: PropTypes.string,
|
|
113
|
+
getPlaceholder: PropTypes.func,
|
|
114
|
+
wrapperClassName: PropTypes.string,
|
|
115
|
+
rowClassName: PropTypes.string,
|
|
116
|
+
headingClassName: PropTypes.string,
|
|
117
|
+
varRegex: PropTypes.object,
|
|
118
|
+
readOnly: PropTypes.bool,
|
|
119
|
+
disabled: PropTypes.bool,
|
|
120
|
+
footerContent: PropTypes.node,
|
|
121
|
+
/** Optional hint below a variable field (e.g. DLT `{#var#}` max length). */
|
|
122
|
+
renderVarFooter: PropTypes.func,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default VarSegmentMessageEditor;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
@import '~@capillarytech/cap-ui-library/styles/_variables';
|
|
2
|
+
|
|
3
|
+
/* Same look as RCS edit message block: background, spacing, text color */
|
|
4
|
+
.rcs_text_area_wrapper {
|
|
5
|
+
.rcs-edit-template-message-input {
|
|
6
|
+
background-color: $CAP_G10;
|
|
7
|
+
padding: $CAP_SPACE_12 $CAP_SPACE_16 $CAP_SPACE_16;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.rcs-edit-template-message-split {
|
|
11
|
+
margin: 0 0 $CAP_SPACE_04 0;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
text-overflow: ellipsis;
|
|
14
|
+
color: $FONT_COLOR_04;
|
|
15
|
+
font-weight: 500;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Variable chips: match RCS edit (white field, light border, 4px radius) */
|
|
19
|
+
.rcs-edit-template-message-input .ant-input,
|
|
20
|
+
.rcs-edit-template-message-input textarea.ant-input {
|
|
21
|
+
margin: 0 0 0.125rem 0;
|
|
22
|
+
border-radius: 0.25rem;
|
|
23
|
+
border: 0.0625rem solid $CAP_G07;
|
|
24
|
+
background-color: $CAP_WHITE;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Small gap between tag border and the next line (static text) */
|
|
29
|
+
.rcs-edit-template-message-input :not(:first-child) {
|
|
30
|
+
margin-top: $CAP_SPACE_08;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.rcs-edit-template-message-input > *:last-child {
|
|
34
|
+
margin-bottom: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.var-segment-message-editor__var-slot {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
width: 100%;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.var-segment-message-editor__read-only-value {
|
|
45
|
+
margin: 0;
|
|
46
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommunicationFlow - Orchestrator Component
|
|
3
|
+
*
|
|
4
|
+
* Manages the step flow, validation, and data aggregation.
|
|
5
|
+
* Renders steps based on config and calls callbacks with aggregated data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, {
|
|
9
|
+
useState, useCallback, useMemo, useEffect,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import PropTypes from 'prop-types';
|
|
12
|
+
import { injectIntl } from 'react-intl';
|
|
13
|
+
import { compose } from 'redux';
|
|
14
|
+
import { connect } from 'react-redux';
|
|
15
|
+
import { createStructuredSelector } from 'reselect';
|
|
16
|
+
// import { CouponsCapContainer } from '@capillarytech/cap-coupons'; // Commented - cap-coupons flows disabled
|
|
17
|
+
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
18
|
+
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
19
|
+
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
20
|
+
// import injectSaga from '../../utils/injectSaga'; // cap-coupons flows disabled
|
|
21
|
+
// import injectReducer from '../../utils/injectReducer';
|
|
22
|
+
import { makeSelectAuthenticated } from '../Cap/selectors';
|
|
23
|
+
import DynamicControlsStep from './steps/DynamicControlsStep';
|
|
24
|
+
import MessageTypeStep from './steps/MessageTypeStep';
|
|
25
|
+
import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
|
|
26
|
+
import ChannelSelectionStep from './steps/ChannelSelectionStep';
|
|
27
|
+
import {
|
|
28
|
+
STEPS,
|
|
29
|
+
CHANNEL_PRIORITY,
|
|
30
|
+
AB_TEST,
|
|
31
|
+
} from './constants';
|
|
32
|
+
import { getEnabledSteps } from './utils/getEnabledSteps';
|
|
33
|
+
import messages from './messages';
|
|
34
|
+
import './CommunicationFlow.scss';
|
|
35
|
+
|
|
36
|
+
// Inject couponsCap reducer and saga - commented out (cap-coupons flows disabled)
|
|
37
|
+
// const withCouponsReducer = injectReducer({
|
|
38
|
+
// key: 'couponsCap',
|
|
39
|
+
// reducer: CouponsCapContainer.couponsCapReducer,
|
|
40
|
+
// });
|
|
41
|
+
// const withCouponsSaga = injectSaga({
|
|
42
|
+
// key: 'couponsCapSaga',
|
|
43
|
+
// saga: CouponsCapContainer.couponsCapSaga,
|
|
44
|
+
// });
|
|
45
|
+
|
|
46
|
+
const CommunicationFlow = ({
|
|
47
|
+
config,
|
|
48
|
+
initialData,
|
|
49
|
+
onSave,
|
|
50
|
+
onCancel, // eslint-disable-line
|
|
51
|
+
onChange,
|
|
52
|
+
intl,
|
|
53
|
+
capData, // From Redux - contains user/org info needed by CouponsWrapper
|
|
54
|
+
}) => {
|
|
55
|
+
const { formatMessage } = intl || {};
|
|
56
|
+
const { messageTypeData = {}, communicationStrategyData = {}, contentTemplateData = {} } = config?.features || {};
|
|
57
|
+
// Initialize step data from initialData or defaults
|
|
58
|
+
const [stepData, setStepData] = useState(() => {
|
|
59
|
+
const defaultMessageType = messageTypeData.defaultOption?.value || null;
|
|
60
|
+
return {
|
|
61
|
+
messageType: initialData?.messageType || defaultMessageType,
|
|
62
|
+
communicationStrategy: initialData?.communicationStrategy || null,
|
|
63
|
+
channel: initialData?.channel || config.channel || null,
|
|
64
|
+
channels: initialData?.channels || [],
|
|
65
|
+
selectedOfferDetails: initialData?.selectedOfferDetails || [],
|
|
66
|
+
contentItems: initialData?.contentItems || [],
|
|
67
|
+
deliverySetting: initialData?.deliverySetting || {},
|
|
68
|
+
dynamicControls: initialData?.dynamicControls || {},
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
const [validationErrors, setValidationErrors] = useState({});
|
|
72
|
+
|
|
73
|
+
// Memoize enabled steps
|
|
74
|
+
const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get aggregated data from all steps
|
|
78
|
+
*/
|
|
79
|
+
const getAggregatedData = useCallback(() => {
|
|
80
|
+
const { communicationStrategy } = stepData;
|
|
81
|
+
const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(communicationStrategy);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
messageType: stepData.messageType,
|
|
85
|
+
communicationStrategy: stepData.communicationStrategy,
|
|
86
|
+
channel: isMultiChannel ? null : stepData.channel,
|
|
87
|
+
channels: isMultiChannel ? stepData.channels : [],
|
|
88
|
+
selectedOfferDetails: stepData.selectedOfferDetails,
|
|
89
|
+
contentItems: stepData.contentItems || [],
|
|
90
|
+
deliverySetting: stepData.deliverySetting,
|
|
91
|
+
dynamicControls: stepData.dynamicControls,
|
|
92
|
+
};
|
|
93
|
+
}, [stepData]);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle step data change
|
|
97
|
+
*/
|
|
98
|
+
const handleStepChange = useCallback((step, data) => {
|
|
99
|
+
setStepData((prevStepData) => ({
|
|
100
|
+
...prevStepData,
|
|
101
|
+
...data,
|
|
102
|
+
}));
|
|
103
|
+
setValidationErrors((prevErrors) => ({
|
|
104
|
+
...prevErrors,
|
|
105
|
+
[step]: null, // Clear validation error for this step
|
|
106
|
+
}));
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
// Call onChange callback when stepData changes
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (onChange) {
|
|
112
|
+
onChange(getAggregatedData());
|
|
113
|
+
}
|
|
114
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
|
+
}, [stepData]);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Render all enabled steps
|
|
119
|
+
*/
|
|
120
|
+
const renderSteps = useCallback(() => enabledSteps.map((step) => {
|
|
121
|
+
const commonProps = {
|
|
122
|
+
value: stepData,
|
|
123
|
+
onChange: (data) => handleStepChange(step, data),
|
|
124
|
+
error: validationErrors[step],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
switch (step) {
|
|
128
|
+
case STEPS.MESSAGE_TYPE:
|
|
129
|
+
return (
|
|
130
|
+
<CapRow key={step}>
|
|
131
|
+
<MessageTypeStep
|
|
132
|
+
{...commonProps}
|
|
133
|
+
value={stepData.messageType}
|
|
134
|
+
options={messageTypeData.options}
|
|
135
|
+
defaultOption={messageTypeData.defaultOption}
|
|
136
|
+
onChange={(messageType) => handleStepChange(step, { messageType })}
|
|
137
|
+
/>
|
|
138
|
+
<CapDivider />
|
|
139
|
+
</CapRow>
|
|
140
|
+
);
|
|
141
|
+
case STEPS.COMMUNICATION_STRATEGY:
|
|
142
|
+
return (
|
|
143
|
+
<CapRow key={step}>
|
|
144
|
+
<CommunicationStrategyStep
|
|
145
|
+
{...commonProps}
|
|
146
|
+
value={stepData.communicationStrategy}
|
|
147
|
+
// messageType={stepData.messageType}
|
|
148
|
+
options={communicationStrategyData.options}
|
|
149
|
+
disabled={communicationStrategyData.disabled}
|
|
150
|
+
onChange={(communicationStrategy) => handleStepChange(step, { communicationStrategy })}
|
|
151
|
+
/>
|
|
152
|
+
<CapDivider />
|
|
153
|
+
</CapRow>
|
|
154
|
+
);
|
|
155
|
+
case STEPS.CHANNEL_SELECTION:
|
|
156
|
+
// Only show ChannelSelectionStep if communication strategy is selected
|
|
157
|
+
if (!stepData.communicationStrategy) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return (
|
|
161
|
+
<CapRow key={step}>
|
|
162
|
+
<ChannelSelectionStep
|
|
163
|
+
{...commonProps}
|
|
164
|
+
value={stepData}
|
|
165
|
+
channels={contentTemplateData.channels}
|
|
166
|
+
onChange={(data) => handleStepChange(step, data)}
|
|
167
|
+
channelsToHide={contentTemplateData.channelsToHide}
|
|
168
|
+
channelsToDisable={contentTemplateData.channelsToDisable}
|
|
169
|
+
creativesMode={config.mode || 'create'}
|
|
170
|
+
selectedOfferDetails={stepData.selectedOfferDetails}
|
|
171
|
+
incentivesData={config.features?.incentivesData}
|
|
172
|
+
deliverySettingsData={config.features?.deliverySettingsData}
|
|
173
|
+
config={config}
|
|
174
|
+
capData={capData}
|
|
175
|
+
/>
|
|
176
|
+
<CapDivider />
|
|
177
|
+
</CapRow>
|
|
178
|
+
);
|
|
179
|
+
// This will be added back in when coupons/points are integrated so keeping it commented out for now
|
|
180
|
+
// case STEPS.INCENTIVES:
|
|
181
|
+
// return (
|
|
182
|
+
// <IncentivesStep
|
|
183
|
+
// key={step}
|
|
184
|
+
// {...commonProps}
|
|
185
|
+
// value={stepData.selectedOfferDetails}
|
|
186
|
+
// onChange={(selectedOfferDetails) => handleStepChange(step, { selectedOfferDetails })}
|
|
187
|
+
// />
|
|
188
|
+
// );
|
|
189
|
+
case STEPS.DYNAMIC_CONTROLS:
|
|
190
|
+
// Only show DynamicControlsStep if communication strategy is selected
|
|
191
|
+
if (!stepData.communicationStrategy) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return (
|
|
195
|
+
<DynamicControlsStep
|
|
196
|
+
key={step}
|
|
197
|
+
{...commonProps}
|
|
198
|
+
value={{ dynamicControls: stepData.dynamicControls }}
|
|
199
|
+
controls={config.features?.dynamicControlsData?.controls || []}
|
|
200
|
+
onChange={(data) => handleStepChange(step, { dynamicControls: data.dynamicControls })}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
default:
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}), [enabledSteps, stepData, validationErrors, config, handleStepChange, messageTypeData, communicationStrategyData, contentTemplateData]);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<CapRow className="communication-flow-container">
|
|
210
|
+
{renderSteps()}
|
|
211
|
+
{onSave && (
|
|
212
|
+
<CapRow className="communication-flow-container__footer">
|
|
213
|
+
<CapButton type="primary" onClick={() => onSave(getAggregatedData())}>
|
|
214
|
+
{formatMessage(messages.save)}
|
|
215
|
+
</CapButton>
|
|
216
|
+
</CapRow>
|
|
217
|
+
)}
|
|
218
|
+
</CapRow>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
CommunicationFlow.propTypes = {
|
|
223
|
+
config: PropTypes.shape({
|
|
224
|
+
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona']).isRequired,
|
|
225
|
+
channel: PropTypes.string,
|
|
226
|
+
mode: PropTypes.oneOf(['create', 'edit', 'preview']).isRequired,
|
|
227
|
+
channelsToHide: PropTypes.arrayOf(PropTypes.string),
|
|
228
|
+
channelsToDisable: PropTypes.arrayOf(PropTypes.string),
|
|
229
|
+
features: PropTypes.shape({
|
|
230
|
+
enableMessageType: PropTypes.bool,
|
|
231
|
+
enableCommunicationStrategy: PropTypes.bool,
|
|
232
|
+
messageTypeData: PropTypes.shape({
|
|
233
|
+
options: PropTypes.arrayOf(PropTypes.shape({
|
|
234
|
+
value: PropTypes.string.isRequired,
|
|
235
|
+
label: PropTypes.node.isRequired,
|
|
236
|
+
})),
|
|
237
|
+
defaultOption: PropTypes.shape({
|
|
238
|
+
value: PropTypes.string.isRequired,
|
|
239
|
+
label: PropTypes.node.isRequired,
|
|
240
|
+
}),
|
|
241
|
+
required: PropTypes.bool,
|
|
242
|
+
}),
|
|
243
|
+
communicationStrategyData: PropTypes.shape({
|
|
244
|
+
options: PropTypes.arrayOf(PropTypes.shape({
|
|
245
|
+
value: PropTypes.string.isRequired,
|
|
246
|
+
label: PropTypes.node.isRequired,
|
|
247
|
+
})),
|
|
248
|
+
defaultOption: PropTypes.shape({
|
|
249
|
+
value: PropTypes.string.isRequired,
|
|
250
|
+
label: PropTypes.node.isRequired,
|
|
251
|
+
}),
|
|
252
|
+
required: PropTypes.bool,
|
|
253
|
+
disabled: PropTypes.bool,
|
|
254
|
+
}),
|
|
255
|
+
contentTemplateData: PropTypes.shape({
|
|
256
|
+
required: PropTypes.bool,
|
|
257
|
+
channels: PropTypes.object,
|
|
258
|
+
}),
|
|
259
|
+
enableIncentives: PropTypes.bool,
|
|
260
|
+
enableDeliverySettings: PropTypes.bool,
|
|
261
|
+
enableOtherSettings: PropTypes.bool,
|
|
262
|
+
incentivesTypes: PropTypes.arrayOf(PropTypes.oneOf(['coupons', 'points', 'promotions', 'giftVouchers', 'badges'])),
|
|
263
|
+
}),
|
|
264
|
+
context: PropTypes.object,
|
|
265
|
+
}).isRequired,
|
|
266
|
+
initialData: PropTypes.object,
|
|
267
|
+
onSave: PropTypes.func.isRequired,
|
|
268
|
+
onCancel: PropTypes.func.isRequired,
|
|
269
|
+
onChange: PropTypes.func,
|
|
270
|
+
intl: PropTypes.object.isRequired,
|
|
271
|
+
capData: PropTypes.object,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
CommunicationFlow.defaultProps = {
|
|
275
|
+
initialData: null,
|
|
276
|
+
onChange: null,
|
|
277
|
+
capData: {},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const mapStateToProps = createStructuredSelector({
|
|
281
|
+
capData: makeSelectAuthenticated(),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const withConnect = connect(mapStateToProps);
|
|
285
|
+
|
|
286
|
+
export default compose(
|
|
287
|
+
// withCouponsReducer, // cap-coupons flows disabled
|
|
288
|
+
// withCouponsSaga, // cap-coupons flows disabled
|
|
289
|
+
withConnect,
|
|
290
|
+
injectIntl,
|
|
291
|
+
)(CommunicationFlow);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
@import '~@capillarytech/cap-ui-library/styles/_variables.scss';
|
|
2
|
+
|
|
3
|
+
// .slidebox-content-container is a DOM ancestor of .communication-flow-container
|
|
4
|
+
// (the outer CapSlideBox wraps this component), so it cannot be scoped as a
|
|
5
|
+
// descendant selector — it must remain at root level to match correctly.
|
|
6
|
+
.slidebox-content-container {
|
|
7
|
+
padding: 0 $CAP_SPACE_48 !important; // 48px left and right, 0 top and bottom
|
|
8
|
+
.slidebox-footer {
|
|
9
|
+
padding: 0 $CAP_SPACE_48 !important;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.communication-flow-container {
|
|
14
|
+
.step-divider {
|
|
15
|
+
margin: $CAP_SPACE_32 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.heading-style {
|
|
19
|
+
margin-bottom: $CAP_SPACE_08;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&__footer {
|
|
23
|
+
padding: $CAP_SPACE_16 0 $CAP_SPACE_08;
|
|
24
|
+
}
|
|
25
|
+
}
|