Skip to content

Commit 9b846c6

Browse files
authored
fix(front): ensure promotion works with special characters in target names (#659)
## 📝 Description Fixes issue where the promote action was failing for promotion targets with special characters in the name. Related [issue](renderedtext/tasks#8779). ## ✅ Checklist - [x] I have tested this change - [x] ~This change requires documentation update~ N/A
1 parent 79c682f commit 9b846c6

File tree

8 files changed

+660
-12
lines changed

8 files changed

+660
-12
lines changed

front/assets/js/utils.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,19 @@ export class Utils {
6060
static endOfWeek(date) {
6161
return moment(date).endOf('isoWeek');
6262
}
63+
64+
// Escapes CSS attribute values
65+
// Uses CSS.escape() when available, with fallback for older browsers
66+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape
67+
static escapeCSSAttributeValue(value) {
68+
if (!value) return value;
69+
70+
// Use native CSS.escape() if available
71+
if (typeof CSS !== 'undefined' && CSS.escape) {
72+
return CSS.escape(value);
73+
}
74+
75+
// Fallback for older browsers: escape quotes and backslashes
76+
return value.replace(/(["'\\])/g, '\\$1');
77+
}
6378
}

front/assets/js/utils.spec.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,97 @@ describe("Utils", () => {
2121
expect(Utils.toSeconds(36000000000)).to.equal(36)
2222
});
2323
});
24+
describe("escapeCSSAttributeValue", () => {
25+
it("escapes single quotes", () => {
26+
const input = "Publish 'my-package' to Production";
27+
const escaped = Utils.escapeCSSAttributeValue(input);
28+
const expected = "Publish \\'my-package\\' to Production";
29+
expect(escaped).to.equal(expected);
30+
});
31+
32+
it("escapes double quotes", () => {
33+
const input = 'Deploy "production" build';
34+
const escaped = Utils.escapeCSSAttributeValue(input);
35+
const expected = 'Deploy \\"production\\" build';
36+
expect(escaped).to.equal(expected);
37+
});
38+
39+
it("escapes backslashes", () => {
40+
const input = "Path\\to\\file";
41+
const escaped = Utils.escapeCSSAttributeValue(input);
42+
const expected = "Path\\\\to\\\\file";
43+
expect(escaped).to.equal(expected);
44+
});
45+
46+
it("escapes special characters", () => {
47+
const input = "test[data]:value.class#id";
48+
const escaped = Utils.escapeCSSAttributeValue(input);
49+
// When CSS.escape is available, it escapes brackets, colons, etc.
50+
// Fallback only escapes quotes and backslashes
51+
if (typeof CSS !== 'undefined' && CSS.escape) {
52+
expect(escaped).to.not.equal(input);
53+
} else {
54+
expect(escaped).to.equal(input);
55+
}
56+
});
57+
58+
it("handles simple strings", () => {
59+
const input = "Simple-promotion_name123";
60+
const escaped = Utils.escapeCSSAttributeValue(input);
61+
// Simple alphanumeric strings with hyphens/underscores shouldn't need escaping
62+
expect(escaped).to.equal(input);
63+
});
64+
65+
it("handles empty string", () => {
66+
expect(Utils.escapeCSSAttributeValue("")).to.equal("");
67+
});
68+
69+
it("handles null/undefined", () => {
70+
expect(Utils.escapeCSSAttributeValue(null)).to.equal(null);
71+
expect(Utils.escapeCSSAttributeValue(undefined)).to.equal(undefined);
72+
});
73+
74+
it("escapes complex strings with brackets and quotes", () => {
75+
const input = "test'][arbitrary-selector][data-x='some-value";
76+
const escaped = Utils.escapeCSSAttributeValue(input);
77+
const expected = "test\\'][arbitrary-selector][data-x=\\'some-value";
78+
expect(escaped).to.equal(expected);
79+
});
80+
81+
it("can be used in DOM selectors", () => {
82+
const input = "Publish 'my-package' to Production";
83+
const escaped = Utils.escapeCSSAttributeValue(input);
84+
const expected = "Publish \\'my-package\\' to Production";
85+
expect(escaped).to.equal(expected);
86+
87+
expect(() => {
88+
document.querySelector(`[data-promotion-target="${escaped}"]`);
89+
}).to.not.throw();
90+
});
91+
92+
it("handles unicode characters (emoji)", () => {
93+
const input = "Deploy 🚀 to production";
94+
const escaped = Utils.escapeCSSAttributeValue(input);
95+
expect(escaped).to.equal(input);
96+
});
97+
98+
it("handles unicode characters (accented letters)", () => {
99+
const input = "Déploiement en français";
100+
const escaped = Utils.escapeCSSAttributeValue(input);
101+
expect(escaped).to.equal(input);
102+
});
103+
104+
it("handles unicode characters (Chinese)", () => {
105+
const input = "部署到生产环境";
106+
const escaped = Utils.escapeCSSAttributeValue(input);
107+
expect(escaped).to.equal(input);
108+
});
109+
110+
it("handles mixed unicode and special characters", () => {
111+
const input = "Deploy 'app' 🚀 to Prod";
112+
const escaped = Utils.escapeCSSAttributeValue(input);
113+
const expected = "Deploy \\'app\\' 🚀 to Prod";
114+
expect(escaped).to.equal(expected);
115+
});
116+
});
24117
})

front/assets/js/workflow_view/switch.js

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import $ from "jquery"
33
import { TriggerEvent } from "./trigger_event"
44
import { Pollman } from "../pollman"
55
import { TargetParams } from "./target_params"
6+
import { Utils } from "../utils"
67

78
export var Switch = {
89
init: function() {
@@ -40,7 +41,8 @@ export var Switch = {
4041
Switch.askToConfirmPromotion(switchId, promotionTarget);
4142

4243
// Focus on first input or select in the promotion box
43-
const promotionForm = $(`[data-promotion-target="${promotionTarget}"]`);
44+
const escapedPromotionTarget = Utils.escapeCSSAttributeValue(promotionTarget);
45+
const promotionForm = $(`[data-promotion-target="${escapedPromotionTarget}"]`);
4446
const firstInput = promotionForm.find('input, select').first();
4547
if (firstInput.length) {
4648
if (firstInput[0].tomselect) {
@@ -82,8 +84,9 @@ export var Switch = {
8284
Pollman.start();
8385
alert("Something went wrong. Please try again.");
8486

85-
$(`[switch='${parentPromotionSwitch}'] [data-promotion-target='${parentPromotionTarget}'][promote-button]`).removeAttr("disabled");
86-
$(`[switch='${parentPromotionSwitch}'] [data-promotion-target='${parentPromotionTarget}'][promote-button]`).removeClass("btn-working");
87+
let escapedTarget = Utils.escapeCSSAttributeValue(parentPromotionTarget);
88+
$(`[switch='${parentPromotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).removeAttr("disabled");
89+
$(`[switch='${parentPromotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).removeClass("btn-working");
8790
});
8891
})
8992
},
@@ -96,9 +99,10 @@ export var Switch = {
9699

97100
let promotionTarget = Switch.parentPromotionTarget(target)
98101
let promotionSwitch = Switch.parentSwitch(target)
102+
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);
99103

100-
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-confirmation]`).hide();
101-
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-button]`).show();
104+
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-confirmation]`).hide();
105+
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).show();
102106

103107
Pollman.pollNow();
104108
Pollman.start();
@@ -138,18 +142,21 @@ export var Switch = {
138142

139143
askToConfirmPromotion: function(promotionSwitch, promotionTarget) {
140144
Switch.hidePromotionBoxElements(promotionSwitch, promotionTarget);
141-
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-confirmation]`).show();
145+
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);
146+
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-confirmation]`).show();
142147
},
143148

144149
hidePromotionBoxElements: function(promotionSwitch, promotionTarget) {
145-
$(`[switch='${promotionSwitch}'] [promotion-box][data-promotion-target='${promotionTarget}']`).children().hide();
150+
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);
151+
$(`[switch='${promotionSwitch}'] [promotion-box][data-promotion-target='${escapedTarget}']`).children().hide();
146152
},
147153

148154
showPromotingInProgress(promotionSwitch, promotionTarget) {
149-
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-confirmation]`).hide();
150-
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-button]`).show();
151-
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-button]`).attr("disabled", "");
152-
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-button]`).addClass("btn-working");
155+
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);
156+
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-confirmation]`).hide();
157+
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).show();
158+
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).attr("disabled", "");
159+
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).addClass("btn-working");
153160
Switch.afterResize(promotionSwitch);
154161
},
155162

@@ -167,7 +174,8 @@ export var Switch = {
167174
},
168175

169176
latestTriggerEvent: function(promotionSwitch, promotionTarget) {
170-
return $(`[switch='${promotionSwitch}'] [trigger-event][data-promotion-target='${promotionTarget}']`).first();
177+
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);
178+
return $(`[switch='${promotionSwitch}'] [trigger-event][data-promotion-target='${escapedTarget}']`).first();
171179
},
172180

173181
isProcessed: function(triggerEvent) {

0 commit comments

Comments
 (0)