@capillarytech/creatives-library 8.0.349 → 8.0.351-alpha.0

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.
@@ -52,6 +52,7 @@ export const EXTENDED_TAG = 'ExtendedTagMessage';
52
52
  export const BADGES_UI_ENABLED = 'BADGES_UI_ENABLED';
53
53
  export const JP_LOCALE_HIDE_FEATURE = 'JP_LOCALE_HIDE_FEATURE';
54
54
  export const ENABLE_WECHAT = 'ENABLE_WECHAT';
55
+ export const ENABLE_CREATIVES_ARCHIVAL = 'ENABLE_CREATIVES_ARCHIVAL';
55
56
  export const ENABLE_WEBPUSH = 'ENABLE_WEBPUSH';
56
57
  export const ENABLE_CUSTOMER_BARCODE_TAG = 'ENABLE_CUSTOMER_BARCODE_TAG';
57
58
  export const EMAIL_UNSUBSCRIBE_TAG_MANDATORY = 'EMAIL_UNSUBSCRIBE_TAG_MANDATORY';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.349",
4
+ "version": "8.0.351-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  ENABLE_NEW_MPUSH,
28
28
  ENABLE_NEW_EDITOR_FLOW_INAPP,
29
29
  SUPPORT_ENGAGEMENT_MODULE,
30
+ ENABLE_CREATIVES_ARCHIVAL,
30
31
  } from '../constants/unified';
31
32
  import { apiMessageFormatHandler } from './commonUtils';
32
33
 
@@ -122,6 +123,11 @@ export const hasWechatFeatureEnabled = Auth.hasFeatureAccess.bind(
122
123
  ENABLE_WECHAT,
123
124
  );
124
125
 
126
+ export const hasCreativesArchivalEnabled = Auth.hasFeatureAccess.bind(
127
+ null,
128
+ ENABLE_CREATIVES_ARCHIVAL,
129
+ );
130
+
125
131
  export const hasWebPushFeatureEnabled = Auth.hasFeatureAccess.bind(
126
132
  null,
127
133
  ENABLE_WEBPUSH,
@@ -18,7 +18,7 @@ import CapRow from '@capillarytech/cap-ui-library/CapRow';
18
18
  import { ANDROID, IOS } from '../constants';
19
19
  import messages from '../messages';
20
20
  import { getWhatsappQuickReply, getWhatsappCarouselButtonView } from '../../../v2Containers/Whatsapp/utils';
21
- import { QUICK_REPLY, PHONE_NUMBER, WHATSAPP_CATEGORIES } from '../../../v2Containers/Whatsapp/constants';
21
+ import { QUICK_REPLY, PHONE_NUMBER, WHATSAPP_CATEGORIES, TEMPLATE_VARIABLE_REGEX } from '../../../v2Containers/Whatsapp/constants';
22
22
  import videoPlay from '../../../assets/videoPlay.svg';
23
23
  import whatsappImageEmptyPreview from '../../TemplatePreview/assets/images/empty_image_preview.svg';
24
24
  import whatsappVideoEmptyPreview from '../../TemplatePreview/assets/images/empty_video_preview.svg';
@@ -27,6 +27,8 @@ import whatsappVideoEmptyPreview from '../../TemplatePreview/assets/images/empty
27
27
  const whatsappMobileAndroid = require('../../../assets/whatsapp_android.png');
28
28
  const whatsappMobileIos = require('../../../assets/whatsapp_ios.png');
29
29
 
30
+ const { CapLabelInline } = CapLabel;
31
+
30
32
  const WhatsAppPreviewContent = ({
31
33
  content,
32
34
  device,
@@ -214,11 +216,21 @@ const WhatsAppPreviewContent = ({
214
216
 
215
217
  {/* Image Preview */}
216
218
  {content?.whatsappImageSrc && (
217
- <CapImage
218
- src={content.whatsappImageSrc}
219
- className="whatsapp-image"
220
- alt={formatMessage(messages.previewGenerated)}
221
- />
219
+ TEMPLATE_VARIABLE_REGEX.test(content.whatsappImageSrc) ? (
220
+ <CapRow className="whatsapp-image-url-placeholder">
221
+ <CapIcon type="picture" size="s" className="whatsapp-image-url-placeholder-icon" />
222
+ <CapLabelInline type="label2" className="whatsapp-image-url-placeholder-text">
223
+ {content.whatsappImageSrc}
224
+ </CapLabelInline>
225
+ </CapRow>
226
+ ) : (
227
+ <CapImage
228
+ src={content.whatsappImageSrc}
229
+ className="whatsapp-image"
230
+ alt={formatMessage(messages.previewGenerated)}
231
+ onError={(e) => { e.target.src = whatsappImageEmptyPreview; }}
232
+ />
233
+ )
222
234
  )}
223
235
 
224
236
  {/* Video Preview */}
@@ -940,6 +940,33 @@
940
940
  }
941
941
  }
942
942
 
943
+ .whatsapp-image-url-placeholder {
944
+ display: flex;
945
+ flex-direction: column;
946
+ align-items: center;
947
+ justify-content: center;
948
+ gap: $CAP_SPACE_08;
949
+ width: 100%;
950
+ min-height: 5rem;
951
+ max-height: 8.786rem;
952
+ margin-bottom: $CAP_SPACE_04;
953
+ background-color: $CAP_G09;
954
+ border-radius: $CAP_SPACE_04;
955
+ padding: $CAP_SPACE_08;
956
+
957
+ .whatsapp-image-url-placeholder-icon {
958
+ color: $CAP_G05;
959
+ font-size: 1.5rem;
960
+ }
961
+
962
+ .whatsapp-image-url-placeholder-text {
963
+ font-size: $FONT_SIZE_S;
964
+ color: $CAP_G04;
965
+ text-align: center;
966
+ word-break: break-all;
967
+ }
968
+ }
969
+
943
970
  .whatsapp-image {
944
971
  width: 100%;
945
972
  max-height: 8.786rem;
@@ -26,6 +26,7 @@ jest.mock('../../../../v2Containers/Whatsapp/constants', () => ({
26
26
  WHATSAPP_CATEGORIES: {
27
27
  authentication: 'authentication',
28
28
  },
29
+ TEMPLATE_VARIABLE_REGEX: /\{\{.*?\}\}/,
29
30
  }));
30
31
 
31
32
  // Convert messages object to format expected by IntlProvider
@@ -238,6 +239,53 @@ describe('WhatsAppPreviewContent', () => {
238
239
  expect(image.getAttribute('src')).toBe('https://image.url');
239
240
  });
240
241
 
242
+ it('should render placeholder when whatsappImageSrc is a template variable', () => {
243
+ const props = {
244
+ ...defaultProps,
245
+ content: {
246
+ templateMsg: 'Message',
247
+ whatsappImageSrc: '{{imageUrl}}',
248
+ },
249
+ };
250
+
251
+ const { container } = render(
252
+ <TestWrapper>
253
+ <ComponentToRender {...props} />
254
+ </TestWrapper>
255
+ );
256
+
257
+ expect(container.querySelector('.whatsapp-image-url-placeholder')).toBeTruthy();
258
+ expect(container.querySelector('.whatsapp-image-url-placeholder-icon')).toBeTruthy();
259
+ expect(container.querySelector('.whatsapp-image-url-placeholder-text').textContent).toBe('{{imageUrl}}');
260
+ expect(container.querySelector('img.whatsapp-image')).toBeFalsy();
261
+ });
262
+
263
+ it('should fall back to empty preview image when image fails to load', () => {
264
+ const props = {
265
+ ...defaultProps,
266
+ content: {
267
+ templateMsg: 'Message',
268
+ whatsappImageSrc: 'https://broken.url/image.jpg',
269
+ },
270
+ };
271
+
272
+ const { container } = render(
273
+ <TestWrapper>
274
+ <ComponentToRender {...props} />
275
+ </TestWrapper>
276
+ );
277
+
278
+ const image = container.querySelector('img.whatsapp-image');
279
+ expect(image).toBeTruthy();
280
+ expect(image.getAttribute('src')).toBe('https://broken.url/image.jpg');
281
+
282
+ // Trigger onError to swap src to fallback
283
+ const errorEvent = new Event('error');
284
+ image.dispatchEvent(errorEvent);
285
+
286
+ expect(image.getAttribute('src')).not.toBe('https://broken.url/image.jpg');
287
+ });
288
+
241
289
  it('should render video preview when whatsappVideoPreviewImg is present', () => {
242
290
  const props = {
243
291
  ...defaultProps,
@@ -816,6 +816,27 @@
816
816
  }
817
817
  }
818
818
 
819
+ .whatsapp-image-url-placeholder {
820
+ display: flex;
821
+ flex-direction: column;
822
+ align-items: center;
823
+ justify-content: center;
824
+ background-color: $CAP_G09;
825
+ border-radius: $CAP_SPACE_04;
826
+ width: 100%;
827
+ min-height: 5rem;
828
+ max-height: 8.786rem;
829
+ margin-bottom: $CAP_SPACE_04;
830
+ .whatsapp-image-url-placeholder-text {
831
+ font-size: 0.786rem;
832
+ color: $CAP_G04;
833
+ margin-top: $CAP_SPACE_04;
834
+ text-align: center;
835
+ word-break: break-all;
836
+ padding: 0 $CAP_SPACE_04;
837
+ }
838
+ }
839
+
819
840
  .msg-container-carousel {
820
841
  display: flex;
821
842
  flex-direction: column;
@@ -48,7 +48,7 @@ import { TEMPLATE, IMAGE_CAROUSEL, IMAGE, STICKER, TEXT, IMAGE_MAP, VIDEO } from
48
48
  import CapFacebookPreview from '../../v2Containers/CapFacebookPreview';
49
49
  import WhatsappStatusContainer from '../WhatsappStatusContainer';
50
50
  import { getWhatsappQuickReply, getWhatsappCarouselButtonView } from '../../v2Containers/Whatsapp/utils';
51
- import { QUICK_REPLY, WHATSAPP_CATEGORIES, PHONE_NUMBER } from '../../v2Containers/Whatsapp/constants';
51
+ import { QUICK_REPLY, WHATSAPP_CATEGORIES, PHONE_NUMBER, TEMPLATE_VARIABLE_REGEX } from '../../v2Containers/Whatsapp/constants';
52
52
  import { RCS_BUTTON_TYPES, LEFT, HORIZONTAL, VERTICAL, RIGHT} from '../../v2Containers/Rcs/constants';
53
53
  import { ANDROID, INAPP_MESSAGE_LAYOUT_TYPES } from '../../v2Containers/InApp/constants';
54
54
  import { CAROUSEL } from '../../v2Containers/MobilePushNew/constants';
@@ -66,6 +66,8 @@ const lineVideoPlaceholder = require('../../assets/rich-video-placeholder.svg');
66
66
  const androidPushMessagePhone = require('./assets/images/Android_With_date_and_time.svg');
67
67
  const iPhonePushMessagePhone = require('./assets/images/iOS_With_date_and_time.svg');
68
68
 
69
+ const { CapLabelInline } = CapLabel;
70
+
69
71
  export class TemplatePreview extends React.Component { // eslint-disable-line react/prefer-stateless-function
70
72
  onPreviewContentClicked = (channel) => {
71
73
  const IOSContent = (this.props.templateDataRaw && this.props.templateDataRaw.versions.base.IOS) ||
@@ -1194,11 +1196,21 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
1194
1196
  {content?.showUrlPreview
1195
1197
  && renderUrlPreview(content?.metaTagsDetails)}
1196
1198
  {content?.whatsappImageSrc && (
1197
- <CapImage
1198
- src={content.whatsappImageSrc}
1199
- className="whatsapp-image"
1200
- alt={formatMessage(messages.previewGenerated)}
1201
- />
1199
+ TEMPLATE_VARIABLE_REGEX.test(content.whatsappImageSrc) ? (
1200
+ <CapRow className="whatsapp-image-url-placeholder">
1201
+ <CapIcon type="picture" size="s" className="whatsapp-image-url-placeholder-icon" />
1202
+ <CapLabelInline type="label2" className="whatsapp-image-url-placeholder-text">
1203
+ {content.whatsappImageSrc}
1204
+ </CapLabelInline>
1205
+ </CapRow>
1206
+ ) : (
1207
+ <CapImage
1208
+ src={content.whatsappImageSrc}
1209
+ className="whatsapp-image"
1210
+ alt={formatMessage(messages.previewGenerated)}
1211
+ onError={(e) => { e.target.src = whatsappImageEmptyPreview; }}
1212
+ />
1213
+ )
1202
1214
  )}
1203
1215
  {content?.whatsappVideoPreviewImg && (
1204
1216
  <CapTooltip
@@ -43,6 +43,7 @@ exports[`Test Templates container Should render correct preview component for wh
43
43
  <CapImage
44
44
  alt="Preview is being generated"
45
45
  className="whatsapp-image"
46
+ onError={[Function]}
46
47
  src="https://crm-nightly-new-fileservice.s3.amazonaws.com/intouch_creative_assets/c9edc114-923b-4ac7-82d0-d6682213.jpg"
47
48
  />
48
49
  You have received {{1}} points
@@ -50,7 +50,7 @@ import { RCS_STATUSES } from '../Rcs/constants';
50
50
  import { CREATIVE } from '../Facebook/constants';
51
51
  import { LOYALTY } from '../App/constants';
52
52
  import {
53
- WHATSAPP_STATUSES, WHATSAPP_MEDIA_TYPES, PHONE_NUMBER, URL,
53
+ WHATSAPP_STATUSES, WHATSAPP_MEDIA_TYPES, PHONE_NUMBER, URL, ADD_IMAGE_URL_PREVIEW_MARKER,
54
54
  } from '../Whatsapp/constants';
55
55
  import { updateImagesInHtml } from '../../utils/cdnTransformation';
56
56
 
@@ -260,7 +260,7 @@ export class Creatives extends React.Component {
260
260
  };
261
261
 
262
262
  onEditTemplate = () => {
263
- if (this.props.templateData?.isArchived) {
263
+ if (commonUtil.hasCreativesArchivalEnabled() && this.props.templateData?.isArchived) {
264
264
  CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
265
265
  return;
266
266
  }
@@ -688,6 +688,8 @@ export class Creatives extends React.Component {
688
688
  switch (mediaType) {
689
689
  case (WHATSAPP_MEDIA_TYPES.IMAGE):
690
690
  mediaParams.imageUrl = url;
691
+ // Detect isAddImageUrl from previewUrl marker (previewUrl is unused for IMAGE type)
692
+ mediaParams.isAddImageUrl = previewUrl === ADD_IMAGE_URL_PREVIEW_MARKER;
691
693
  break;
692
694
  case (WHATSAPP_MEDIA_TYPES.VIDEO):
693
695
  mediaParams.videoUrl = url;
@@ -1146,6 +1148,7 @@ export class Creatives extends React.Component {
1146
1148
  headerTemplate = '',
1147
1149
  } = {},
1148
1150
  isPreviewUrl = false,
1151
+ isAddImageUrl = false,
1149
1152
  carouselData = [],
1150
1153
  } = cloneDeep(versions.base.content.whatsapp);
1151
1154
 
@@ -1182,6 +1185,11 @@ export class Creatives extends React.Component {
1182
1185
  switch (mediaType) {
1183
1186
  case (WHATSAPP_MEDIA_TYPES.IMAGE):
1184
1187
  whatsappMedia.url = imageUrl;
1188
+ // previewUrl is unused for IMAGE type — reuse it to persist isAddImageUrl flag
1189
+ // without adding any new field that the backend would reject
1190
+ if (isAddImageUrl) {
1191
+ whatsappMedia.previewUrl = ADD_IMAGE_URL_PREVIEW_MARKER;
1192
+ }
1185
1193
  break;
1186
1194
  case (WHATSAPP_MEDIA_TYPES.VIDEO):
1187
1195
  whatsappMedia.url = videoUrl;
@@ -2140,7 +2148,7 @@ export class Creatives extends React.Component {
2140
2148
  onSave={this.saveMessage}
2141
2149
  onDiscard={this.discardMessage}
2142
2150
  onEditTemplate={this.onEditTemplate}
2143
- isTemplateArchived={!!(this.props.templateData && this.props.templateData.isArchived)}
2151
+ isTemplateArchived={!!(commonUtil.hasCreativesArchivalEnabled() && this.props.templateData && this.props.templateData.isArchived)}
2144
2152
  slidBoxContent={slidBoxContent}
2145
2153
  onCreateNextStep={this.onCreateNextStep}
2146
2154
  currentChannel={currentChannel.toUpperCase()}
@@ -799,6 +799,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
799
799
  }
800
800
  const formData = _.cloneDeep(this.state.formData);
801
801
  formData[currentTab - 1][field.id] = myField.value;
802
+ if (formData.base) {
803
+ formData.base[field.id] = myField.value;
804
+ }
802
805
  this.setState({formData}, () => this.onTemplateContentChange());
803
806
  }
804
807
 
@@ -1216,6 +1216,7 @@
1216
1216
  justify-content: space-between;
1217
1217
  align-items: center;
1218
1218
  gap: $CAP_SPACE_08;
1219
+ margin-right: $CAP_SPACE_32;
1219
1220
  }
1220
1221
 
1221
1222
  .template-listing-more-btn {
@@ -1938,9 +1938,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1938
1938
  // Archive eligibility per template: Zalo never; WhatsApp/RCS not when pending/awaiting
1939
1939
  const cardWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
1940
1940
  const cardRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
1941
- const isCardArchiveEligible = this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
1942
- const isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
1943
- const isAnyArchiveInProgress = !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
1941
+ const isArchivalEnabled = commonUtil.hasCreativesArchivalEnabled();
1942
+ const isCardArchiveEligible = isArchivalEnabled && this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
1943
+ const isArchivedMode = isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
1944
+ const isAnyArchiveInProgress = isArchivalEnabled && !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
1944
1945
  const templateData = {
1945
1946
  key: `${currentChannel}-card-${template?.name}`,
1946
1947
  title: (
@@ -2077,7 +2078,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2077
2078
  </CapMenu.Item>
2078
2079
  )}
2079
2080
  {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2080
- {(() => {
2081
+ {commonUtil.hasCreativesArchivalEnabled() && (() => {
2081
2082
  const channelUp = this.state.channel.toUpperCase();
2082
2083
  if (!this.isChannelArchiveEligible(channelUp, status, rcsStatus)) return null;
2083
2084
  return !template?.isArchived ? (
@@ -2626,11 +2627,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2626
2627
 
2627
2628
  const noLoaderAndSearchText = isEmpty(this.state.searchText) && !isLoading;
2628
2629
 
2629
- const isArchivedModeLocal = get(this.props, 'Templates.isArchivedMode', false);
2630
+ const isArchivalEnabledLocal = commonUtil.hasCreativesArchivalEnabled();
2631
+ const isArchivedModeLocal = isArchivalEnabledLocal && get(this.props, 'Templates.isArchivedMode', false);
2630
2632
  const selectedIdsLocal = get(this.props, 'Templates.selectedTemplateIds', []);
2631
2633
  const selectedIdsArrayLocal = selectedIdsLocal && typeof selectedIdsLocal.toJS === 'function' ? selectedIdsLocal.toJS() : (Array.isArray(selectedIdsLocal) ? selectedIdsLocal : []);
2632
2634
  const selectedCountLocal = selectedIdsArrayLocal.length;
2633
- const hasSelectionLocal = this.props.isFullMode && selectedCountLocal > 0;
2635
+ const hasSelectionLocal = isArchivalEnabledLocal && this.props.isFullMode && selectedCountLocal > 0;
2634
2636
 
2635
2637
  return (<div>
2636
2638
  {[WECHAT, MOBILE_PUSH, WEBPUSH, INAPP, WHATSAPP, ZALO, RCS].includes(currentChannel) && this.showAccountName()}
@@ -3840,7 +3842,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3840
3842
  <span className="template-name" style={{ fontWeight: `${this.state.channel.toLowerCase() === 'wechat' ? '400' : '600'}` }}>
3841
3843
  { template && template.versions && template.versions.history && template.versions.history.length > 1 && this.state.channel.toLowerCase() !== 'mobilepush' && <i style={{fontSize: '16px', margin: '0 8px 0 0', verticalAlign: 'middle'}} className="material-icons">filter_none</i>}
3842
3844
  {template?.name}
3843
- {template.isArchived && <CapColoredTag tagColor={CAP_G08} tagTextColor={CAP_G05} className="archived-tag">{this.props.intl.formatMessage(messages.archivedTag)}</CapColoredTag>}
3845
+ {commonUtil.hasCreativesArchivalEnabled() && template.isArchived && <CapColoredTag tagColor={CAP_G08} tagTextColor={CAP_G05} className="archived-tag">{this.props.intl.formatMessage(messages.archivedTag)}</CapColoredTag>}
3844
3846
  </span>
3845
3847
  {this.props.location.query.type !== EMBEDDED && <CapPopover
3846
3848
  trigger="click"
@@ -3851,7 +3853,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3851
3853
  {this.props.intl.formatMessage(messages.duplicateButton)}
3852
3854
  </CapButton>
3853
3855
  </div>}
3854
- {this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
3856
+ {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
3855
3857
  <CapButton
3856
3858
  type="link"
3857
3859
  onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
@@ -3861,7 +3863,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3861
3863
  <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.archiveButton)}</CapLabel.CapLabelInline>
3862
3864
  </CapButton>
3863
3865
  </div>}
3864
- {this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
3866
+ {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
3865
3867
  <CapButton
3866
3868
  type="link"
3867
3869
  onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
@@ -4478,10 +4480,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4478
4480
  if (([WHATSAPP_LOWERCASE, ZALO_LOWERCASE, RCS_LOWERCASE].includes(this.state?.channel?.toLocaleLowerCase()) && isEmpty(this.state?.hostName))) {
4479
4481
  isfilterContentVisisble = false;
4480
4482
  }
4481
- const _isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
4483
+ const _isArchivalEnabled = commonUtil.hasCreativesArchivalEnabled();
4484
+ const _isArchivedMode = _isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
4482
4485
  const _renderSelectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
4483
4486
  const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4484
- const _renderHasSelection = this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4487
+ const _renderHasSelection = _isArchivalEnabled && this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4485
4488
 
4486
4489
  const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4487
4490
  {isfilterContentVisisble && <CapInput.Search
@@ -4650,8 +4653,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4650
4653
  )
4651
4654
  : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4652
4655
  )}
4653
- {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active */}
4654
- {!_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
4656
+ {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active, archive flag enabled */}
4657
+ {commonUtil.hasCreativesArchivalEnabled() && !_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
4655
4658
  <CapDropdown
4656
4659
  trigger={['click']}
4657
4660
  overlay={
@@ -4720,8 +4723,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4720
4723
  }
4721
4724
  />
4722
4725
 
4723
- {/* Archived mode header with back arrow (full mode only) */}
4724
- {this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
4726
+ {/* Archived mode header with back arrow (full mode only, archive flag enabled) */}
4727
+ {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
4725
4728
  <CapRow type="flex" align="middle" className="archived-mode-header">
4726
4729
  <CapIcon
4727
4730
  type="back"
@@ -831,37 +831,6 @@ exports[`Test Templates container Should render temlates when whatsapp templates
831
831
  >
832
832
  Create new
833
833
  </CapButton>
834
- <CapDropdown
835
- overlay={
836
- <CapMenu>
837
- <withItemHOC
838
- onClick={[Function]}
839
- >
840
- <FormattedMessage
841
- defaultMessage="Archived templates"
842
- id="creatives.containersV2.Templates.archivedTemplates"
843
- values={Object {}}
844
- />
845
- </withItemHOC>
846
- </CapMenu>
847
- }
848
- placement="bottomRight"
849
- trigger={
850
- Array [
851
- "click",
852
- ]
853
- }
854
- >
855
- <CapButton
856
- className="template-listing-more-btn"
857
- isAddBtn={false}
858
- type="flat"
859
- >
860
- <CapIcon
861
- type="more"
862
- />
863
- </CapButton>
864
- </CapDropdown>
865
834
  </div>
866
835
  </div>
867
836
  </div>
@@ -1282,37 +1251,6 @@ exports[`Test Templates container Test max templates exceeded 1`] = `
1282
1251
  >
1283
1252
  Create new
1284
1253
  </CapButton>
1285
- <CapDropdown
1286
- overlay={
1287
- <CapMenu>
1288
- <withItemHOC
1289
- onClick={[Function]}
1290
- >
1291
- <FormattedMessage
1292
- defaultMessage="Archived templates"
1293
- id="creatives.containersV2.Templates.archivedTemplates"
1294
- values={Object {}}
1295
- />
1296
- </withItemHOC>
1297
- </CapMenu>
1298
- }
1299
- placement="bottomRight"
1300
- trigger={
1301
- Array [
1302
- "click",
1303
- ]
1304
- }
1305
- >
1306
- <CapButton
1307
- className="template-listing-more-btn"
1308
- isAddBtn={false}
1309
- type="flat"
1310
- >
1311
- <CapIcon
1312
- type="more"
1313
- />
1314
- </CapButton>
1315
- </CapDropdown>
1316
1254
  </div>
1317
1255
  </div>
1318
1256
  </div>
@@ -1786,37 +1724,6 @@ exports[`Test Templates container Test max templates not exceeded 1`] = `
1786
1724
  >
1787
1725
  Create new
1788
1726
  </CapButton>
1789
- <CapDropdown
1790
- overlay={
1791
- <CapMenu>
1792
- <withItemHOC
1793
- onClick={[Function]}
1794
- >
1795
- <FormattedMessage
1796
- defaultMessage="Archived templates"
1797
- id="creatives.containersV2.Templates.archivedTemplates"
1798
- values={Object {}}
1799
- />
1800
- </withItemHOC>
1801
- </CapMenu>
1802
- }
1803
- placement="bottomRight"
1804
- trigger={
1805
- Array [
1806
- "click",
1807
- ]
1808
- }
1809
- >
1810
- <CapButton
1811
- className="template-listing-more-btn"
1812
- isAddBtn={false}
1813
- type="flat"
1814
- >
1815
- <CapIcon
1816
- type="more"
1817
- />
1818
- </CapButton>
1819
- </CapDropdown>
1820
1727
  </div>
1821
1728
  </div>
1822
1729
  </div>
@@ -2290,37 +2197,6 @@ exports[`Test Templates container Test max templates warning 1`] = `
2290
2197
  >
2291
2198
  Create new
2292
2199
  </CapButton>
2293
- <CapDropdown
2294
- overlay={
2295
- <CapMenu>
2296
- <withItemHOC
2297
- onClick={[Function]}
2298
- >
2299
- <FormattedMessage
2300
- defaultMessage="Archived templates"
2301
- id="creatives.containersV2.Templates.archivedTemplates"
2302
- values={Object {}}
2303
- />
2304
- </withItemHOC>
2305
- </CapMenu>
2306
- }
2307
- placement="bottomRight"
2308
- trigger={
2309
- Array [
2310
- "click",
2311
- ]
2312
- }
2313
- >
2314
- <CapButton
2315
- className="template-listing-more-btn"
2316
- isAddBtn={false}
2317
- type="flat"
2318
- >
2319
- <CapIcon
2320
- type="more"
2321
- />
2322
- </CapButton>
2323
- </CapDropdown>
2324
2200
  </div>
2325
2201
  </div>
2326
2202
  </div>
@@ -319,6 +319,14 @@ export const WHATSAPP_MEDIA_TYPES = {
319
319
  DOCUMENT: 'DOCUMENT',
320
320
  CAROUSEL: 'CAROUSEL',
321
321
  };
322
+
323
+ export const WHATSAPP_IMAGE_SOURCE = {
324
+ UPLOAD: 'UPLOAD_IMAGE',
325
+ URL: 'ADD_IMAGE_URL',
326
+ };
327
+
328
+ export const ADD_IMAGE_URL_PREVIEW_MARKER = '__add_image_url__';
329
+ export const TEMPLATE_VARIABLE_REGEX = /\{\{.*?\}\}/;
322
330
  export const NONE = 'NONE';
323
331
  export const CTA = 'CTA';
324
332
  export const QUICK_REPLY = 'QUICK_REPLY';
@@ -104,7 +104,8 @@ import {
104
104
  VIDEO,
105
105
  URL,
106
106
  REQUEST,
107
- HOST_KARIX
107
+ HOST_KARIX,
108
+ WHATSAPP_IMAGE_SOURCE,
108
109
  } from './constants';
109
110
  import { transformAssetData, transformToVendorFormat, VENDOR_TYPES, resolveCarouselVideoUpdateFields } from '../../utils/vendorDataTransformers';
110
111
  import { validateCarouselCards } from '../../utils/commonUtils';
@@ -252,12 +253,15 @@ export const Whatsapp = (props) => {
252
253
  const [headerTextAreaId, setHeaderTextAreaId] = useState('');
253
254
  const [isHeaderTagValidationError, updateIsHeaderTagValidationError] =
254
255
  useState(false);
256
+ const [imageURLCursorPosition, setImageURLCursorPosition] = useState(0);
255
257
  const [showUrlPreview, setShowUrlPreview] = useState(false);
256
258
  const [previewUrl, setPreviewUrl] = useState('');
257
259
  const [carouselMediaType, setCarouselMediaType] = useState('image');
258
260
  const [carouselData, setCarouselData] = useState(CAROUSEL_INITIAL_DATA);
259
261
  const [activeIndex, setActiveIndex] = useState(defaultActiveIndex);
260
262
  const [carouselValidateTag, setCarouselValidateTag] = useState(false);
263
+ const [mediaTypeSelection, setMediaTypeSelection] = useState(WHATSAPP_IMAGE_SOURCE.UPLOAD);
264
+ const [imageURL, setImageURL] = useState('');
261
265
  // TestAndPreviewSlidebox state
262
266
  const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
263
267
  const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
@@ -364,6 +368,7 @@ export const Whatsapp = (props) => {
364
368
  whatsappMedia: { header = '', footer = '', headerVarMapped = [] } = {},
365
369
  isPreviewUrl = false,
366
370
  carouselData : editCarouselData = [],
371
+ isAddImageUrl = false,
367
372
  } = editContent;
368
373
  setTemplateCategory(category);
369
374
  setTemplateStatus(status);
@@ -378,6 +383,14 @@ export const Whatsapp = (props) => {
378
383
  });
379
384
  setWhatsappDocSource(documentUrl);
380
385
  setWhatsappDocParams(whatsappDocParams);
386
+ if (mediaType === WHATSAPP_MEDIA_TYPES.IMAGE) {
387
+ if (isAddImageUrl) {
388
+ setMediaTypeSelection(WHATSAPP_IMAGE_SOURCE.URL);
389
+ setImageURL(imageUrl);
390
+ } else {
391
+ setMediaTypeSelection(WHATSAPP_IMAGE_SOURCE.UPLOAD);
392
+ }
393
+ }
381
394
  if (host === HOST_HAPTIC) {
382
395
  updateAssetList(transformToVendorFormat({
383
396
  ...assetList,
@@ -800,6 +813,15 @@ export const Whatsapp = (props) => {
800
813
  });
801
814
  }
802
815
 
816
+ const onImageURLTagSelect = (data) => {
817
+ const currentURL = imageURL || '';
818
+ const tag = `{{${data}}}`;
819
+ const newURL = currentURL.slice(0, imageURLCursorPosition) + tag + currentURL.slice(imageURLCursorPosition);
820
+ setImageURL(newURL);
821
+ setWhatsappImageSrc(newURL);
822
+ setImageURLCursorPosition(imageURLCursorPosition + tag.length);
823
+ };
824
+
803
825
  //setting the id of currently selected text area, is used onTagSelect
804
826
  const setTextAreaId = ({ target: { id } }, type, carouselIndex) => {
805
827
  if (type === HEADER_TEXT) {
@@ -1151,6 +1173,9 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
1151
1173
  );
1152
1174
  } else {
1153
1175
  setWhatsappImageSrc(filePath);
1176
+ if (mediaTypeSelection !== WHATSAPP_IMAGE_SOURCE.URL) {
1177
+ setMediaTypeSelection(WHATSAPP_IMAGE_SOURCE.UPLOAD);
1178
+ }
1154
1179
  if (isHaptic) {
1155
1180
  setHapticFileHandle(fileHandle);
1156
1181
  } else {
@@ -1159,7 +1184,7 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
1159
1184
  }
1160
1185
  actions.clearWhatsappAsset(0);
1161
1186
  },
1162
- [whatsappImageSrc, isMediaTypeCarousel, activeIndex, carouselData, isHaptic],
1187
+ [whatsappImageSrc, isMediaTypeCarousel, activeIndex, carouselData, isHaptic, mediaTypeSelection],
1163
1188
  );
1164
1189
 
1165
1190
  const updateOnWhatsappImageReUpload = useCallback(() => {
@@ -1626,8 +1651,10 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
1626
1651
  const { whatsappVideoSrc = '', whatsappVideoPreviewImg = '' } = whatsappVideoSrcAndPreview;
1627
1652
  switch (templateMediaType) {
1628
1653
  case WHATSAPP_MEDIA_TYPES.IMAGE:
1654
+ const isImageUrlMode = mediaTypeSelection === WHATSAPP_IMAGE_SOURCE.URL;
1629
1655
  mediaParams = {
1630
1656
  imageUrl: getCdnUrl({ url: whatsappImageSrc, channelName: WHATSAPP }),
1657
+ ...(isImageUrlMode && { isAddImageUrl: true }),
1631
1658
  };
1632
1659
  break;
1633
1660
  case WHATSAPP_MEDIA_TYPES.VIDEO:
@@ -1646,7 +1673,6 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
1646
1673
  break;
1647
1674
  }
1648
1675
 
1649
- // Add the correct file handle based on host
1650
1676
  if (isHaptic && hapticFileHandle) {
1651
1677
  mediaParams.hapticFileHandle = hapticFileHandle;
1652
1678
  } else if (!isHaptic && karixFileHandle) {
@@ -1758,7 +1784,21 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
1758
1784
  type: WHATSAPP,
1759
1785
  });
1760
1786
  };
1761
- const isMediatypeValid =()=> ((isMediaTypeImage && whatsappImageSrc === "") || (isMediaTypeVideo && whatsappVideoSrcAndPreview?.whatsappVideoSrc === "") || (isMediaTypeDoc && whatsappDocSource === ""));
1787
+ const isMediatypeValid =()=> {
1788
+ if (isMediaTypeImage) {
1789
+ if (mediaTypeSelection === WHATSAPP_IMAGE_SOURCE.URL) {
1790
+ return isEditFlow ? !imageURL?.trim() : !whatsappImageSrc?.trim();
1791
+ }
1792
+ return !whatsappImageSrc?.trim();
1793
+ }
1794
+ if (isMediaTypeVideo) {
1795
+ return !whatsappVideoSrcAndPreview?.whatsappVideoSrc?.trim();
1796
+ }
1797
+ if (isMediaTypeDoc) {
1798
+ return !whatsappDocSource?.trim();
1799
+ }
1800
+ return false;
1801
+ };
1762
1802
 
1763
1803
  const isDisableDone = () => {
1764
1804
  // Snapshot removed
@@ -1835,9 +1875,107 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
1835
1875
  return false;
1836
1876
  };
1837
1877
 
1878
+ const renderImageURLComponent = () => (
1879
+ <>
1880
+ {templateStatus === WHATSAPP_STATUSES.approved && !isAuthenticationTemplate && (
1881
+ <CapRow className="whatsapp-render-heading">
1882
+ <CapHeader
1883
+ title={
1884
+ <CapHeading type="h4">
1885
+ {formatMessage(messages.mediaImage)}
1886
+ </CapHeading>
1887
+ }
1888
+ suffix={
1889
+ <TagList
1890
+ label={formatMessage(globalMessages.addLabels)}
1891
+ onTagSelect={onImageURLTagSelect}
1892
+ location={location}
1893
+ tags={tags || []}
1894
+ onContextChange={handleOnTagsContextChange}
1895
+ injectedTags={injectedTags || {}}
1896
+ selectedOfferDetails={selectedOfferDetails}
1897
+ eventContextTags={eventContextTags}
1898
+ />
1899
+ }
1900
+ />
1901
+ </CapRow>
1902
+ )}
1903
+ <CapRow className="whatsapp-image-url-container">
1904
+ <CapInput
1905
+ id="whatsapp-image-url-input"
1906
+ placeholder={formatMessage(messages.imageUrlPlaceholder)}
1907
+ value={imageURL}
1908
+ disabled={isEditFlow && isFullMode}
1909
+ onChange={e => {
1910
+ setImageURL(e.target.value);
1911
+ setWhatsappImageSrc(e.target.value);
1912
+ setImageURLCursorPosition(e.target.selectionStart || e.target.value.length);
1913
+ }}
1914
+ onFocus={(e) => {
1915
+ setImageURLCursorPosition(e.target.selectionStart || e.target.value.length);
1916
+ }}
1917
+ onSelect={(e) => {
1918
+ setImageURLCursorPosition(e.target.selectionStart || e.target.value.length);
1919
+ }}
1920
+ />
1921
+ </CapRow>
1922
+ </>
1923
+ );
1924
+
1925
+ const handleMediaChange = (e) => {
1926
+ if (templateStatus === WHATSAPP_STATUSES.approved || isEditFlow) {
1927
+ return;
1928
+ }
1929
+ setMediaTypeSelection(e.target.value);
1930
+ if (e.target.value === WHATSAPP_IMAGE_SOURCE.UPLOAD) {
1931
+ setImageURL('');
1932
+ } else if (e.target.value === WHATSAPP_IMAGE_SOURCE.URL) {
1933
+ setWhatsappImageSrc('');
1934
+ if (isHaptic) {
1935
+ setHapticFileHandle('');
1936
+ } else {
1937
+ setKarixFileHandle('');
1938
+ }
1939
+ }
1940
+ };
1941
+
1838
1942
  const renderMediaComponent = () => (
1839
1943
  <>
1840
- {isMediaTypeImage && renderImageComponent()}
1944
+ {isMediaTypeImage && (
1945
+ <CapRow className="whatsapp-dynamic-url-selection-container">
1946
+ <CapHeading type="h4" className="whatsapp-dynamic-url-selection-heading">
1947
+ {formatMessage(messages.mediaImage)}
1948
+ </CapHeading>
1949
+ <CapRadioGroup
1950
+ options={[
1951
+ {
1952
+ value: WHATSAPP_IMAGE_SOURCE.UPLOAD,
1953
+ label: formatMessage(messages.imageSourceUpload),
1954
+ },
1955
+ {
1956
+ value: WHATSAPP_IMAGE_SOURCE.URL,
1957
+ label: (
1958
+ <CapLabel.CapLabelInline type="label2">
1959
+ {formatMessage(messages.imageSourceUrl)}{' '}
1960
+ <CapTooltip
1961
+ placement="top"
1962
+ title={formatMessage(messages.imageSourceUrlTooltip)}
1963
+ >
1964
+ <CapIcon type="info-circle" size="s" style={{ cursor: 'pointer' }} />
1965
+ </CapTooltip>
1966
+ </CapLabel.CapLabelInline>
1967
+ ),
1968
+ },
1969
+ ]}
1970
+ value={mediaTypeSelection}
1971
+ onChange={handleMediaChange}
1972
+ className="whatsapp-media-radio"
1973
+ disabled={templateStatus === WHATSAPP_STATUSES.approved || isEditFlow}
1974
+ />
1975
+ </CapRow>
1976
+ )}
1977
+ {isMediaTypeImage && (mediaTypeSelection === WHATSAPP_IMAGE_SOURCE.UPLOAD || !isEditFlow) && renderImageComponent()}
1978
+ {isMediaTypeImage && mediaTypeSelection === WHATSAPP_IMAGE_SOURCE.URL && isEditFlow && renderImageURLComponent()}
1841
1979
  {isMediaTypeVideo && renderVideoComonent()}
1842
1980
  {isMediaTypeDoc && renderDocumentComponent()}
1843
1981
  </>
@@ -9,6 +9,14 @@
9
9
  margin-top: $CAP_SPACE_16;
10
10
  }
11
11
 
12
+ .whatsapp-dynamic-url-selection-container {
13
+ margin-bottom: $CAP_SPACE_08;
14
+ margin-top: $CAP_SPACE_32;
15
+ .whatsapp-dynamic-url-selection-heading {
16
+ margin-bottom: $CAP_SPACE_08;
17
+ }
18
+ }
19
+
12
20
  .whatsapp-render-heading {
13
21
  margin-top: $CAP_SPACE_16;
14
22
  margin-bottom: $CAP_SPACE_06;
@@ -138,6 +138,22 @@ export default defineMessages({
138
138
  id: `${prefix}.mediaCarousel`,
139
139
  defaultMessage: 'Carousel',
140
140
  },
141
+ imageSourceUpload: {
142
+ id: `${prefix}.imageSourceUpload`,
143
+ defaultMessage: 'Upload image',
144
+ },
145
+ imageSourceUrl: {
146
+ id: `${prefix}.imageSourceUrl`,
147
+ defaultMessage: 'Dynamic image',
148
+ },
149
+ imageSourceUrlTooltip: {
150
+ id: `${prefix}.imageSourceUrlTooltip`,
151
+ defaultMessage: 'Post template approval, the image placeholder is replaceable with image URL',
152
+ },
153
+ imageUrlPlaceholder: {
154
+ id: `${prefix}.imageUrlPlaceholder`,
155
+ defaultMessage: 'Enter image URL',
156
+ },
141
157
  disabledFeatureTooltip: {
142
158
  id: `${prefix}.disabledFeatureTooltip`,
143
159
  defaultMessage: 'Not yet enabled. Coming soon!',