@capillarytech/creatives-library 8.0.353 → 8.0.354
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/index.html +1 -0
- package/package.json +1 -1
- package/utils/cdnTransformation.js +63 -3
- package/utils/tests/cdnTransformation.test.js +111 -0
- package/v2Components/FormBuilder/index.js +52 -162
- package/v2Containers/CreativesContainer/index.js +24 -60
- package/v2Containers/Templates/sagas.js +6 -1
- package/v2Containers/Templates/tests/sagas.test.js +23 -6
package/index.html
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
<!-- End Google Tag Manager -->
|
|
24
24
|
<script>try{Typekit.load({ async: true });}catch(e){console.log(e)}</script>
|
|
25
25
|
<title>Capillary - Creatives</title>
|
|
26
|
+
<script>try{window.APP_ENV=__ENV_OBJECT__;}catch(e){window.APP_ENV={};}</script>
|
|
26
27
|
</head>
|
|
27
28
|
<body>
|
|
28
29
|
<noscript>
|
package/package.json
CHANGED
|
@@ -422,14 +422,74 @@ export function removeAllCdnLocalStorageItems() {
|
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
/**
|
|
425
|
-
*
|
|
426
|
-
*
|
|
425
|
+
* Safe JSON.parse — returns fallback if value is empty or malformed.
|
|
426
|
+
* Used to parse the JSON-string env vars (qualityCfg, mapping, s3CdnMap) injected via window.APP_ENV.
|
|
427
|
+
*/
|
|
428
|
+
function safeJsonParse(value, fallback) {
|
|
429
|
+
if (!value) return fallback;
|
|
430
|
+
try {
|
|
431
|
+
return JSON.parse(value);
|
|
432
|
+
} catch (e) {
|
|
433
|
+
return fallback;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Reads CDN config from window.APP_ENV (injected at container startup by entrypoint.sh)
|
|
439
|
+
* and populates localStorage via saveCdnConfigs — same shape as the legacy API response.
|
|
440
|
+
*
|
|
441
|
+
* Returns true if env config was found and applied, false otherwise (saga falls back to API).
|
|
442
|
+
*
|
|
443
|
+
* In deployed containers, window.APP_ENV is populated → no API call. In local dev
|
|
444
|
+
* (`npm start`), window.APP_ENV is empty → returns false → saga falls back to the API
|
|
445
|
+
* (legacy path, retained for the rollout window).
|
|
446
|
+
*
|
|
447
|
+
* Closes the VAPT info-disclosure surface (CAP-183204) by eliminating the
|
|
448
|
+
* /getCdnTransformationConfig API response in production.
|
|
449
|
+
*/
|
|
450
|
+
export function initCdnConfigFromEnv() {
|
|
451
|
+
try {
|
|
452
|
+
const env = (typeof window !== 'undefined' && window.APP_ENV) || {};
|
|
453
|
+
|
|
454
|
+
// All four fields are required to construct a CDN URL. If any is missing,
|
|
455
|
+
// partial config would silently make every getCdnUrl call fall through to the
|
|
456
|
+
// raw S3 URL. Return false so the saga falls back to the API (which may still
|
|
457
|
+
// have the values during the cluster-by-cluster rollout window).
|
|
458
|
+
if (!env.CDN_HOSTNAME
|
|
459
|
+
|| !env.CDN_IMG_TRANSFORMATION_URL_SUFFIX
|
|
460
|
+
|| !env.CREATIVE_ASSETS_BUCKET_PATH
|
|
461
|
+
|| !env.S3_CDN_MAP) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
saveCdnConfigs({
|
|
466
|
+
hostname: env.CDN_HOSTNAME,
|
|
467
|
+
transformationUrlSuffix: env.CDN_IMG_TRANSFORMATION_URL_SUFFIX,
|
|
468
|
+
bucketPath: env.CREATIVE_ASSETS_BUCKET_PATH,
|
|
469
|
+
qualityCfg: safeJsonParse(env.CDN_IMG_QUALITY_CFG, {}),
|
|
470
|
+
overrideEmailQuality: env.OVERRIDE_IMAGE_QUALITY === 'true',
|
|
471
|
+
overrideEmailQualityMapping: safeJsonParse(env.IMAGE_QUALITY_MAPPING, []),
|
|
472
|
+
s3CdnMap: safeJsonParse(env.S3_CDN_MAP, {}),
|
|
473
|
+
});
|
|
474
|
+
return true;
|
|
475
|
+
} catch (e) {
|
|
476
|
+
Bugsnag.leaveBreadcrumb('initCdnConfigFromEnv failed to apply window.APP_ENV');
|
|
477
|
+
Bugsnag.notify(e, (event) => {
|
|
478
|
+
event.severity = 'error';
|
|
479
|
+
});
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
*
|
|
486
|
+
* @param {*} configsResponse
|
|
427
487
|
* This util function saves the getCdnConfigs response into the local storage.
|
|
428
488
|
* The following response items are mapped into the following keys in local storage:
|
|
429
489
|
* hostname -> CREATIVES_CDN_BASE_URL
|
|
430
490
|
* qualityCfg ->CREATIVES_CDN_QUALITY_CONFIG
|
|
431
491
|
* transformationUrlSuffix -> CREATIVES_CDN_TRANSFORMATION_URL_SUFFIX
|
|
432
|
-
*
|
|
492
|
+
*
|
|
433
493
|
* 1. If configsReponse is empty. All above mentioned keys are deleted from localstorage.
|
|
434
494
|
* 2. If any one of the above keys is missing. The respective keys are deleted from localstorage.
|
|
435
495
|
* 3. Else it is saved into the localstorage.
|
|
@@ -546,6 +546,117 @@ describe("cdnTransformationTests", () => {
|
|
|
546
546
|
});
|
|
547
547
|
});
|
|
548
548
|
|
|
549
|
+
describe("initCdnConfigFromEnv()", () => {
|
|
550
|
+
afterEach(() => {
|
|
551
|
+
delete window.APP_ENV;
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("returns false when window.APP_ENV is undefined", () => {
|
|
555
|
+
delete window.APP_ENV;
|
|
556
|
+
expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const completeEnv = {
|
|
560
|
+
CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
|
|
561
|
+
CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
|
|
562
|
+
CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
|
|
563
|
+
OVERRIDE_IMAGE_QUALITY: "false",
|
|
564
|
+
CDN_IMG_QUALITY_CFG: "{}",
|
|
565
|
+
IMAGE_QUALITY_MAPPING: "[]",
|
|
566
|
+
S3_CDN_MAP: '{"host.s3.amazonaws.com":{"cdn_host":"cdn.example.com","bucket_path":"x"}}',
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
it.each([
|
|
570
|
+
["CDN_HOSTNAME"],
|
|
571
|
+
["CDN_IMG_TRANSFORMATION_URL_SUFFIX"],
|
|
572
|
+
["CREATIVE_ASSETS_BUCKET_PATH"],
|
|
573
|
+
["S3_CDN_MAP"],
|
|
574
|
+
])("returns false when required field %s is missing", (missingKey) => {
|
|
575
|
+
window.APP_ENV = { ...completeEnv };
|
|
576
|
+
delete window.APP_ENV[missingKey];
|
|
577
|
+
expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it.each([
|
|
581
|
+
["CDN_HOSTNAME"],
|
|
582
|
+
["CDN_IMG_TRANSFORMATION_URL_SUFFIX"],
|
|
583
|
+
["CREATIVE_ASSETS_BUCKET_PATH"],
|
|
584
|
+
["S3_CDN_MAP"],
|
|
585
|
+
])("returns false when required field %s is empty string", (emptyKey) => {
|
|
586
|
+
window.APP_ENV = { ...completeEnv, [emptyKey]: "" };
|
|
587
|
+
expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("populates localStorage and returns true for a complete env", () => {
|
|
591
|
+
window.APP_ENV = {
|
|
592
|
+
CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
|
|
593
|
+
CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
|
|
594
|
+
CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
|
|
595
|
+
OVERRIDE_IMAGE_QUALITY: "true",
|
|
596
|
+
CDN_IMG_QUALITY_CFG: '{"EMAIL":65,"DEFAULT":70}',
|
|
597
|
+
IMAGE_QUALITY_MAPPING: '[[30,90],[80,85]]',
|
|
598
|
+
S3_CDN_MAP: '{"host.s3.amazonaws.com":{"cdn_host":"cdn.example.com","bucket_path":"x"}}',
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
expect(cdnUtils.initCdnConfigFromEnv()).toBe(true);
|
|
602
|
+
expect(localStorage.getItem("CREATIVES_CDN_BASE_URL")).toBe("https://storage.crm.n.content-cdn.io");
|
|
603
|
+
expect(localStorage.getItem("CREATIVES_CDN_TRANSFORMATION_URL_SUFFIX")).toBe("cdn-cgi/image");
|
|
604
|
+
expect(localStorage.getItem("CREATIVES_S3_BUCKET_PATH")).toBe("intouch_creative_assets");
|
|
605
|
+
expect(localStorage.getItem("CREATIVES_CDN_QUALITY_CONFIG")).toBe('{"EMAIL":65,"DEFAULT":70}');
|
|
606
|
+
expect(localStorage.getItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY")).toBe("true");
|
|
607
|
+
expect(localStorage.getItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY_MAPPING")).toBe("[[30,90],[80,85]]");
|
|
608
|
+
expect(JSON.parse(localStorage.getItem("S3_CDN_MAP"))).toEqual({
|
|
609
|
+
"host.s3.amazonaws.com": { cdn_host: "cdn.example.com", bucket_path: "x" },
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("uses fallback values for malformed JSON env vars instead of throwing", () => {
|
|
614
|
+
window.APP_ENV = {
|
|
615
|
+
CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
|
|
616
|
+
CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
|
|
617
|
+
CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
|
|
618
|
+
OVERRIDE_IMAGE_QUALITY: "false",
|
|
619
|
+
CDN_IMG_QUALITY_CFG: "not-valid-json{{",
|
|
620
|
+
IMAGE_QUALITY_MAPPING: "also-broken",
|
|
621
|
+
S3_CDN_MAP: "}{",
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
expect(cdnUtils.initCdnConfigFromEnv()).toBe(true);
|
|
625
|
+
expect(localStorage.getItem("CREATIVES_CDN_BASE_URL")).toBe("https://storage.crm.n.content-cdn.io");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("treats OVERRIDE_IMAGE_QUALITY values other than the string \"true\" as false", () => {
|
|
629
|
+
window.APP_ENV = {
|
|
630
|
+
CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
|
|
631
|
+
CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
|
|
632
|
+
CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
|
|
633
|
+
OVERRIDE_IMAGE_QUALITY: "no",
|
|
634
|
+
CDN_IMG_QUALITY_CFG: "{}",
|
|
635
|
+
IMAGE_QUALITY_MAPPING: "[]",
|
|
636
|
+
S3_CDN_MAP: "{}",
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
cdnUtils.initCdnConfigFromEnv();
|
|
640
|
+
expect(localStorage.getItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY")).toBe(undefined);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("returns false and notifies Bugsnag when window.APP_ENV access throws", () => {
|
|
644
|
+
// Force the `window.APP_ENV` read inside initCdnConfigFromEnv to throw,
|
|
645
|
+
// which exercises the function's outer catch block.
|
|
646
|
+
Object.defineProperty(window, "APP_ENV", {
|
|
647
|
+
get() { throw new Error("APP_ENV access boom"); },
|
|
648
|
+
configurable: true,
|
|
649
|
+
});
|
|
650
|
+
const breadcrumbSpy = jest.spyOn(Bugsnag, "leaveBreadcrumb");
|
|
651
|
+
|
|
652
|
+
expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
|
|
653
|
+
expect(breadcrumbSpy).toHaveBeenCalledWith(
|
|
654
|
+
"initCdnConfigFromEnv failed to apply window.APP_ENV"
|
|
655
|
+
);
|
|
656
|
+
expect(bugsnagSpy).toHaveBeenCalled();
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
549
660
|
describe("getEmailImageOverrideQuality()",()=>{
|
|
550
661
|
it("Should return quality of last element in array if file size is greater than max provided in array",()=>{
|
|
551
662
|
localStorage.setItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY", "true");
|
|
@@ -79,69 +79,6 @@ const errorMessageForTags = {
|
|
|
79
79
|
TAG_BRACKET_COUNT_MISMATCH_ERROR: 'tagBracketCountMismatchError'
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
// Isolated input for EMAIL template-name: only this tiny component re-renders on each keystroke.
|
|
83
|
-
// formData is updated only on blur (onCommit), eliminating all re-renders during typing.
|
|
84
|
-
class HighFreqInput extends React.Component {
|
|
85
|
-
constructor(props) {
|
|
86
|
-
super(props);
|
|
87
|
-
this.state = { localValue: props.value || '' };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
componentDidUpdate(prevProps) {
|
|
91
|
-
if (prevProps.value !== this.props.value && this.state.localValue !== this.props.value) {
|
|
92
|
-
this.setState({ localValue: this.props.value || '' });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
handleChange = (e) => {
|
|
97
|
-
this.setState({ localValue: e.target.value });
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
handleBlur = (e) => {
|
|
101
|
-
this.props.onCommit(this.state.localValue);
|
|
102
|
-
if (this.props.onBlur) this.props.onBlur(e);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
render() {
|
|
106
|
-
const { value: _v, onCommit: _oc, onBlur: _ob, ...rest } = this.props;
|
|
107
|
-
return <CapInput {...rest} value={this.state.localValue} onChange={this.handleChange} onBlur={this.handleBlur} />;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Isolated wrapper for EMAIL template-subject: blur-only commit, same as HighFreqInput.
|
|
112
|
-
class HighFreqTagInput extends React.Component {
|
|
113
|
-
constructor(props) {
|
|
114
|
-
super(props);
|
|
115
|
-
this.state = { localInputValue: props.inputValue || '' };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
componentDidUpdate(prevProps) {
|
|
119
|
-
if (prevProps.inputValue !== this.props.inputValue && this.state.localInputValue !== this.props.inputValue) {
|
|
120
|
-
this.setState({ localInputValue: this.props.inputValue || '' });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
handleInputChange = (e) => {
|
|
125
|
-
this.setState({ localInputValue: e.target.value });
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
handleBlur = () => {
|
|
129
|
-
this.props.onCommit(this.state.localInputValue);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
render() {
|
|
133
|
-
const { inputValue: _iv, onCommit: _oc, inputOnChange: _ic, ...rest } = this.props;
|
|
134
|
-
return (
|
|
135
|
-
<CapTagListWithInput
|
|
136
|
-
{...rest}
|
|
137
|
-
inputValue={this.state.localInputValue}
|
|
138
|
-
inputOnChange={this.handleInputChange}
|
|
139
|
-
inputProps={{ ...(this.props.inputProps || {}), onBlur: this.handleBlur }}
|
|
140
|
-
/>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
82
|
class FormBuilder extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
|
146
83
|
constructor(props) {
|
|
147
84
|
super(props);
|
|
@@ -415,7 +352,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
415
352
|
this.setState({tabCount: nextProps.tabCount});
|
|
416
353
|
}
|
|
417
354
|
if (nextProps.startValidation && nextProps.startValidation !== false && this.props.startValidation !== nextProps.startValidation) {
|
|
418
|
-
if (this.debouncedUpdateFormData) this.debouncedUpdateFormData.flush();
|
|
419
355
|
this.setState({checkValidation: true});
|
|
420
356
|
this.validateForm(null, null, true, true, () => {
|
|
421
357
|
//triggering the saveFormData or onSubmit when validation sets isFormValid to TRUE
|
|
@@ -3474,25 +3410,26 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
3474
3410
|
? formatMessage(messages.personalizationTagsErrorMessage)
|
|
3475
3411
|
: (errorType === TAG_BRACKET_COUNT_MISMATCH_ERROR ? formatMessage(globalMessages.unbalanacedCurlyBraces) : (val.errorMessage && ifError ? val.errorMessage : ''));
|
|
3476
3412
|
if (styling === 'semantic') {
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3413
|
+
columns.push(
|
|
3414
|
+
<CapColumn key={val.id} span={val.width} offset={val.offset} style={val.style || {}}>
|
|
3415
|
+
<CapInput
|
|
3416
|
+
id={val.id}
|
|
3417
|
+
errorMessage={errorMessageText}
|
|
3418
|
+
label={val.label}
|
|
3419
|
+
inductiveText={val.inductiveText}
|
|
3420
|
+
className={`input-primary chart-name-input${ifError ? ' error' : ''}`}
|
|
3421
|
+
// fluid={val.fluid}
|
|
3422
|
+
style={val.style ? val.style : {}}
|
|
3423
|
+
placeholder={val.placeholder}
|
|
3424
|
+
onChange={(e) => this.updateFormData(e.target.value, val)}
|
|
3425
|
+
onBlur={(e) => this.handleFieldBlur(e, val)}
|
|
3426
|
+
value={value || ""}
|
|
3427
|
+
defaultValue={isVersionEnable ? this.state.formData[`${this.state.currentTab - 1}`][val.id] : this.state.formData[val.id]}
|
|
3428
|
+
disabled={val.disabled}
|
|
3429
|
+
size={val.size || "default"}
|
|
3430
|
+
/>
|
|
3431
|
+
{this.props.schema?.channel === EMAIL &&
|
|
3432
|
+
!aiContentBotDisabled && (
|
|
3496
3433
|
<CapAskAira.ContentGenerationBot
|
|
3497
3434
|
text={value || ""}
|
|
3498
3435
|
setText={this.handleSetText.bind(this, val)}
|
|
@@ -3501,48 +3438,12 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
3501
3438
|
rootStyle={{
|
|
3502
3439
|
bottom: "0.2rem",
|
|
3503
3440
|
right: "0.2rem",
|
|
3504
|
-
left: "auto",
|
|
3441
|
+
left: "auto",
|
|
3505
3442
|
}}
|
|
3506
3443
|
/>
|
|
3507
3444
|
)}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
} else {
|
|
3511
|
-
columns.push(
|
|
3512
|
-
<CapColumn key={val.id} span={val.width} offset={val.offset} style={val.style || {}}>
|
|
3513
|
-
<CapInput
|
|
3514
|
-
id={val.id}
|
|
3515
|
-
errorMessage={errorMessageText}
|
|
3516
|
-
label={val.label}
|
|
3517
|
-
inductiveText={val.inductiveText}
|
|
3518
|
-
className={`input-primary chart-name-input${ifError ? ' error' : ''}`}
|
|
3519
|
-
// fluid={val.fluid}
|
|
3520
|
-
style={val.style ? val.style : {}}
|
|
3521
|
-
placeholder={val.placeholder}
|
|
3522
|
-
onChange={(e) => this.updateFormData(e.target.value, val)}
|
|
3523
|
-
onBlur={(e) => this.handleFieldBlur(e, val)}
|
|
3524
|
-
value={value || ""}
|
|
3525
|
-
defaultValue={isVersionEnable ? this.state.formData[`${this.state.currentTab - 1}`][val.id] : this.state.formData[val.id]}
|
|
3526
|
-
disabled={val.disabled}
|
|
3527
|
-
size={val.size || "default"}
|
|
3528
|
-
/>
|
|
3529
|
-
{this.props.schema?.channel === EMAIL &&
|
|
3530
|
-
!aiContentBotDisabled && (
|
|
3531
|
-
<CapAskAira.ContentGenerationBot
|
|
3532
|
-
text={value || ""}
|
|
3533
|
-
setText={this.handleSetText.bind(this, val)}
|
|
3534
|
-
iconPlacement="float-br"
|
|
3535
|
-
iconSize="1.6rem"
|
|
3536
|
-
rootStyle={{
|
|
3537
|
-
bottom: "0.2rem",
|
|
3538
|
-
right: "0.2rem",
|
|
3539
|
-
left: "auto",
|
|
3540
|
-
}}
|
|
3541
|
-
/>
|
|
3542
|
-
)}
|
|
3543
|
-
</CapColumn>
|
|
3544
|
-
);
|
|
3545
|
-
}
|
|
3445
|
+
</CapColumn>
|
|
3446
|
+
);
|
|
3546
3447
|
}
|
|
3547
3448
|
break;
|
|
3548
3449
|
|
|
@@ -3783,48 +3684,37 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
3783
3684
|
isBEEAppEnableForCapTagList === false ||
|
|
3784
3685
|
channelForCapTagList !== 'EMAIL'
|
|
3785
3686
|
) {
|
|
3786
|
-
const isEmailStandaloneSubject = val.standalone && channelForCapTagList === EMAIL && val.id === 'template-subject';
|
|
3787
|
-
const tagListProps = {
|
|
3788
|
-
key: `input-${val.id}`,
|
|
3789
|
-
inputId: val.id,
|
|
3790
|
-
inputValue: this.state.formData[val.id] || '',
|
|
3791
|
-
inputPlaceholder: val.placeholder || '',
|
|
3792
|
-
inputErrorMessage: val.errorMessage && ifError ? val.errorMessage : '',
|
|
3793
|
-
inputRequired: val.required || false,
|
|
3794
|
-
inputDisabled: val.disabled || false,
|
|
3795
|
-
headingText: val.label || '',
|
|
3796
|
-
headingStyle: val.headingStyle || { marginTop: '3%', marginRight: '79%' },
|
|
3797
|
-
headingType: "h4",
|
|
3798
|
-
onTagSelect: (data) => this.callChildEvent(data, val, 'onTagSelect'),
|
|
3799
|
-
onContextChange: this.props.onContextChange,
|
|
3800
|
-
location: this.props.location,
|
|
3801
|
-
tags: this.props.tags ? this.props.tags : [],
|
|
3802
|
-
injectedTags: this.props.injectedTags ? this.props.injectedTags : {},
|
|
3803
|
-
className: val.className ? val.className : '',
|
|
3804
|
-
userLocale: this.state.translationLang,
|
|
3805
|
-
selectedOfferDetails: this.props.selectedOfferDetails,
|
|
3806
|
-
eventContextTags: this.props?.eventContextTags,
|
|
3807
|
-
waitEventContextTags: this.props?.waitEventContextTags,
|
|
3808
|
-
moduleFilterEnabled: moduleFilterEnabledForCapTagList,
|
|
3809
|
-
containerStyle: val.style || {},
|
|
3810
|
-
inputProps: val.inputProps || {},
|
|
3811
|
-
showInput: val.showInput !== false,
|
|
3812
|
-
showTagList: val.showTagList !== false,
|
|
3813
|
-
restrictPersonalization: this.props.restrictPersonalization,
|
|
3814
|
-
};
|
|
3815
3687
|
columns.push(
|
|
3816
3688
|
<CapColumn key={`input-${val.id}`} offset={val.offset} span={val.width ? val.width : ''} style={val.style ? val.style : {marginBottom: '16px'}}>
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3689
|
+
<CapTagListWithInput
|
|
3690
|
+
key={`input-${val.id}`}
|
|
3691
|
+
inputId={val.id}
|
|
3692
|
+
inputValue={this.state.formData[val.id] || ''}
|
|
3693
|
+
inputOnChange={(e) => this.updateFormData(e.target.value, val)}
|
|
3694
|
+
inputPlaceholder={val.placeholder || ''}
|
|
3695
|
+
inputErrorMessage={val.errorMessage && ifError ? val.errorMessage : ''}
|
|
3696
|
+
inputRequired={val.required || false}
|
|
3697
|
+
inputDisabled={val.disabled || false}
|
|
3698
|
+
headingText={val.label || ''}
|
|
3699
|
+
headingStyle={val.headingStyle || { marginTop: '3%', marginRight: '79%' }}
|
|
3700
|
+
headingType="h4"
|
|
3701
|
+
onTagSelect={(data) => this.callChildEvent(data, val, 'onTagSelect')}
|
|
3702
|
+
onContextChange={this.props.onContextChange}
|
|
3703
|
+
location={this.props.location}
|
|
3704
|
+
tags={this.props.tags ? this.props.tags : []}
|
|
3705
|
+
injectedTags={this.props.injectedTags ? this.props.injectedTags : {}}
|
|
3706
|
+
className={val.className ? val.className : ''}
|
|
3707
|
+
userLocale={this.state.translationLang}
|
|
3708
|
+
selectedOfferDetails={this.props.selectedOfferDetails}
|
|
3709
|
+
eventContextTags={this.props?.eventContextTags}
|
|
3710
|
+
waitEventContextTags={this.props?.waitEventContextTags}
|
|
3711
|
+
moduleFilterEnabled={moduleFilterEnabledForCapTagList}
|
|
3712
|
+
containerStyle={val.style || {}}
|
|
3713
|
+
inputProps={val.inputProps || {}}
|
|
3714
|
+
showInput={val.showInput !== false}
|
|
3715
|
+
showTagList={val.showTagList !== false}
|
|
3716
|
+
restrictPersonalization={this.props.restrictPersonalization}
|
|
3717
|
+
/>
|
|
3828
3718
|
</CapColumn>
|
|
3829
3719
|
);
|
|
3830
3720
|
}
|
|
@@ -1,48 +1,4 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
3
|
-
// Isolated input for the email template name field.
|
|
4
|
-
// Manages its own value in local state so keystrokes only re-render this
|
|
5
|
-
// small component, not the entire CreativesContainer → Email → FormBuilder tree.
|
|
6
|
-
class TemplateNameInputField extends React.Component {
|
|
7
|
-
constructor(props) {
|
|
8
|
-
super(props);
|
|
9
|
-
this.state = { localValue: props.initialValue || '' };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
componentDidUpdate(prevProps) {
|
|
13
|
-
// Sync from props only when the external value changed AND the user hasn't
|
|
14
|
-
// diverged from the previous prop value. This handles async data-load in edit
|
|
15
|
-
// mode without overwriting what the user is actively typing.
|
|
16
|
-
if (
|
|
17
|
-
prevProps.initialValue !== this.props.initialValue &&
|
|
18
|
-
this.state.localValue === (prevProps.initialValue || '')
|
|
19
|
-
) {
|
|
20
|
-
this.setState({ localValue: this.props.initialValue || '' });
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
handleChange = (ev) => {
|
|
25
|
-
const { value } = ev.currentTarget;
|
|
26
|
-
this.setState({ localValue: value });
|
|
27
|
-
if (this.props.onChange) this.props.onChange(value);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
handleBlur = () => {
|
|
31
|
-
if (this.props.onBlur) this.props.onBlur(this.state.localValue);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
render() {
|
|
35
|
-
const { onChange: _onChange, initialValue: _initialValue, onBlur: _ob, ...rest } = this.props;
|
|
36
|
-
return (
|
|
37
|
-
<CapInput
|
|
38
|
-
{...rest}
|
|
39
|
-
value={this.state.localValue}
|
|
40
|
-
onChange={this.handleChange}
|
|
41
|
-
onBlur={this.handleBlur}
|
|
42
|
-
/>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
2
|
import PropTypes from 'prop-types';
|
|
47
3
|
import {
|
|
48
4
|
CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
|
|
@@ -1797,22 +1753,30 @@ export class Creatives extends React.Component {
|
|
|
1797
1753
|
} />
|
|
1798
1754
|
)
|
|
1799
1755
|
|
|
1800
|
-
templateNameComponentInput = ({ formData, onFormDataChange, name }) =>
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1756
|
+
templateNameComponentInput = ({ formData, onFormDataChange, name }) => {
|
|
1757
|
+
// Use local state for immediate UI feedback, fallback to prop value
|
|
1758
|
+
const displayValue = this.state.localTemplateName !== '' ? this.state.localTemplateName : name;
|
|
1759
|
+
|
|
1760
|
+
return (
|
|
1761
|
+
<CapInput
|
|
1762
|
+
value={displayValue}
|
|
1763
|
+
suffix={<span />}
|
|
1764
|
+
onBlur={() => {
|
|
1765
|
+
this.setState({
|
|
1766
|
+
isEditName: false,
|
|
1767
|
+
localTemplateName: '', // Clear local state on blur
|
|
1768
|
+
}, () => {
|
|
1769
|
+
this.showTemplateName({ formData, onFormDataChange });
|
|
1770
|
+
});
|
|
1771
|
+
}}
|
|
1772
|
+
onChange={(ev) => {
|
|
1773
|
+
const { value } = ev.currentTarget;
|
|
1774
|
+
// Use optimized update for better performance
|
|
1775
|
+
this.updateTemplateNameImmediately(value, formData, onFormDataChange);
|
|
1776
|
+
}}
|
|
1777
|
+
/>
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1816
1780
|
|
|
1817
1781
|
showTemplateName = ({ formData, onFormDataChange }) => { //gets called from email/index after template data is fetched
|
|
1818
1782
|
const {
|
|
@@ -6,7 +6,7 @@ import { CapNotification } from '@capillarytech/cap-ui-library';
|
|
|
6
6
|
// import { schema, normalize } from 'normalizr';
|
|
7
7
|
import * as Api from '../../services/api';
|
|
8
8
|
import * as types from './constants';
|
|
9
|
-
import { saveCdnConfigs, removeAllCdnLocalStorageItems } from '../../utils/cdnTransformation';
|
|
9
|
+
import { saveCdnConfigs, removeAllCdnLocalStorageItems, initCdnConfigFromEnv } from '../../utils/cdnTransformation';
|
|
10
10
|
import { COPY_OF } from '../../constants/unified';
|
|
11
11
|
import { ZALO_TEMPLATE_INFO_REQUEST } from '../Zalo/constants';
|
|
12
12
|
import { getTemplateInfoById } from '../Zalo/saga';
|
|
@@ -107,6 +107,11 @@ export function* getOrgLevelCampaignSettings() {
|
|
|
107
107
|
|
|
108
108
|
export function* getCdnTransformationConfig() {
|
|
109
109
|
try {
|
|
110
|
+
// VAPT CAP-183204: prefer env vars injected via window.APP_ENV — avoids the
|
|
111
|
+
// API response that leaked CDN/S3 infrastructure details. Fallback to API
|
|
112
|
+
// keeps clusters that haven't received the env vars yet working during rollout.
|
|
113
|
+
if (initCdnConfigFromEnv()) return;
|
|
114
|
+
|
|
110
115
|
const res = yield call(Api.getCdnTransformationConfig);
|
|
111
116
|
if (res?.success && res?.status?.code === 200) {
|
|
112
117
|
const cdnConfigs = res?.response;
|
|
@@ -54,10 +54,25 @@ jest.mock('@capillarytech/cap-ui-library', () => ({
|
|
|
54
54
|
}));
|
|
55
55
|
|
|
56
56
|
describe('getCdnTransformationConfig saga', () => {
|
|
57
|
-
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
delete window.APP_ENV;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("short-circuits to env config and skips the API call when window.APP_ENV is set", async () => {
|
|
62
|
+
const initSpy = jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(true);
|
|
63
|
+
const apiSpy = jest.spyOn(api, "getCdnTransformationConfig");
|
|
64
|
+
|
|
65
|
+
await expectSaga(getCdnTransformationConfig).run();
|
|
66
|
+
|
|
67
|
+
expect(initSpy).toHaveBeenCalled();
|
|
68
|
+
expect(apiSpy).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("handle valid response from api", async () => {
|
|
72
|
+
jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
|
|
58
73
|
const saveCdnConfigsSpy = jest.spyOn(cdnUtils, "saveCdnConfigs");
|
|
59
74
|
|
|
60
|
-
expectSaga(getCdnTransformationConfig)
|
|
75
|
+
await expectSaga(getCdnTransformationConfig)
|
|
61
76
|
.provide([
|
|
62
77
|
[
|
|
63
78
|
call(api.getCdnTransformationConfig),
|
|
@@ -68,13 +83,14 @@ describe('getCdnTransformationConfig saga', () => {
|
|
|
68
83
|
expect(saveCdnConfigsSpy).toHaveBeenCalled();
|
|
69
84
|
});
|
|
70
85
|
|
|
71
|
-
it("handle error response from api", () => {
|
|
86
|
+
it("handle error response from api", async () => {
|
|
87
|
+
jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
|
|
72
88
|
const removeAllCdnLocalStorageItemsSpy = jest.spyOn(
|
|
73
89
|
cdnUtils,
|
|
74
90
|
"removeAllCdnLocalStorageItems"
|
|
75
91
|
);
|
|
76
92
|
|
|
77
|
-
expectSaga(getCdnTransformationConfig)
|
|
93
|
+
await expectSaga(getCdnTransformationConfig)
|
|
78
94
|
.provide([
|
|
79
95
|
[
|
|
80
96
|
call(api.getCdnTransformationConfig),
|
|
@@ -85,7 +101,8 @@ describe('getCdnTransformationConfig saga', () => {
|
|
|
85
101
|
expect(removeAllCdnLocalStorageItemsSpy).toHaveBeenCalled();
|
|
86
102
|
});
|
|
87
103
|
|
|
88
|
-
it("remove localStorage items when an error is thrown", () => {
|
|
104
|
+
it("remove localStorage items when an error is thrown", async () => {
|
|
105
|
+
jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
|
|
89
106
|
const saveCdnConfigsSpy = jest.spyOn(cdnUtils, "saveCdnConfigs").mockImplementation(()=>{
|
|
90
107
|
throw new Error()
|
|
91
108
|
});
|
|
@@ -94,7 +111,7 @@ describe('getCdnTransformationConfig saga', () => {
|
|
|
94
111
|
"removeAllCdnLocalStorageItems"
|
|
95
112
|
);
|
|
96
113
|
|
|
97
|
-
expectSaga(getCdnTransformationConfig)
|
|
114
|
+
await expectSaga(getCdnTransformationConfig)
|
|
98
115
|
.provide([
|
|
99
116
|
[
|
|
100
117
|
call(api.getCdnTransformationConfig),
|