Skip to content

Commit 04caeae

Browse files
authored
fix(designer-ui): Retain tokens inserted into HTML editor if they contain } (#4198)
* Remove dupe logic from `appendStringSegment` * Add regression tests for `convertStringToSegments` * Add first pass token parsing changes for `convertStringToSegments` * Make `nodeMap` a required argument * Improve token-to-segment logic & add tests * Run commit hooks * Make tests more readable * Add support for quoted strings * Fix case when quotes were used outside of `@{}`
1 parent 2d43902 commit 04caeae

File tree

8 files changed

+290
-78
lines changed

8 files changed

+290
-78
lines changed

libs/designer-ui/src/lib/arrayeditor/util/serializecollapsedarray.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ export const parseSimpleItems = (
7474
const stringValueCasted = prettifyJsonString(convertSegmentsToString(castedArraySegments, nodeMap));
7575
const stringValueUncasted = prettifyJsonString(convertSegmentsToString(uncastedArraySegments, nodeMap));
7676
return {
77-
castedValue: convertStringToSegments(stringValueCasted, /*tokensEnabled*/ false, nodeMap),
78-
uncastedValue: convertStringToSegments(stringValueUncasted, /*tokensEnabled*/ true, nodeMap),
77+
castedValue: convertStringToSegments(stringValueCasted, nodeMap, { tokensEnabled: false }),
78+
uncastedValue: convertStringToSegments(stringValueUncasted, nodeMap, { tokensEnabled: true }),
7979
};
8080
} catch (e) {
8181
return { uncastedValue: uncastedArraySegments, castedValue: castedArraySegments };
@@ -102,7 +102,7 @@ export const parseComplexItems = (
102102
uncastedArrayVal.push(convertComplexItemsToArray(itemSchema, items, nodeMap, /*suppress casting*/ true, castParameter));
103103
});
104104
return {
105-
castedValue: convertStringToSegments(JSON.stringify(castedArrayVal, null, 4), /*tokensEnabled*/ false, nodeMap),
106-
uncastedValue: convertStringToSegments(JSON.stringify(uncastedArrayVal, null, 4), /*tokensEnabled*/ true, nodeMap),
105+
castedValue: convertStringToSegments(JSON.stringify(castedArrayVal, null, 4), nodeMap, { tokensEnabled: false }),
106+
uncastedValue: convertStringToSegments(JSON.stringify(uncastedArrayVal, null, 4), nodeMap, { tokensEnabled: true }),
107107
};
108108
};

libs/designer-ui/src/lib/arrayeditor/util/util.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ export const validationAndSerializeSimpleArray = (
159159
returnItems.push({
160160
value: convertStringToSegments(
161161
valueType === constants.SWAGGER.TYPE.STRING ? (value as string) : JSON.stringify(value, null, 4),
162-
/*tokensEnabled*/ true,
163-
nodeMap
162+
nodeMap,
163+
{ tokensEnabled: true }
164164
),
165165
key: guid(),
166166
});
@@ -232,7 +232,7 @@ const convertObjectToComplexArrayItemArray = (
232232
key: itemSchema.key,
233233
title: handleTitle(itemSchema.key, itemSchema.title),
234234
description: itemSchema.description ?? '',
235-
value: convertStringToSegments(obj, /*tokensEnabled*/ true, nodeMap),
235+
value: convertStringToSegments(obj, nodeMap, { tokensEnabled: true }),
236236
},
237237
];
238238
}
@@ -267,7 +267,7 @@ const convertObjectToComplexArrayItemArray = (
267267
key: itemSchemaProperty.key,
268268
title: handleTitle(itemSchema.key, itemSchemaProperty.title),
269269
description: itemSchemaProperty.description ?? '',
270-
value: convertStringToSegments(value, true, nodeMap),
270+
value: convertStringToSegments(value, nodeMap, { tokensEnabled: true }),
271271
});
272272
}
273273
});

libs/designer-ui/src/lib/authentication/util.ts

+14-14
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ export const serializeAuthentication = (
364364
editorString: string,
365365
setCurrentProps: (items: AuthProps) => void,
366366
setOption: (s: AuthenticationType) => void,
367-
nodeMap?: Map<string, ValueSegment>
367+
nodeMap: Map<string, ValueSegment>
368368
): boolean => {
369369
let jsonEditor = Object.create(null);
370370
try {
@@ -376,44 +376,44 @@ export const serializeAuthentication = (
376376
switch (jsonEditor.type) {
377377
case AuthenticationType.BASIC:
378378
returnItems.basic = {
379-
basicUsername: convertStringToSegments(jsonEditor.username, true, nodeMap),
380-
basicPassword: convertStringToSegments(jsonEditor.password, true, nodeMap),
379+
basicUsername: convertStringToSegments(jsonEditor.username, nodeMap, { tokensEnabled: true }),
380+
basicPassword: convertStringToSegments(jsonEditor.password, nodeMap, { tokensEnabled: true }),
381381
};
382382
break;
383383
case AuthenticationType.CERTIFICATE:
384384
returnItems.clientCertificate = {
385-
clientCertificatePfx: convertStringToSegments(jsonEditor.pfx, true, nodeMap),
386-
clientCertificatePassword: convertStringToSegments(jsonEditor.password, true, nodeMap),
385+
clientCertificatePfx: convertStringToSegments(jsonEditor.pfx, nodeMap, { tokensEnabled: true }),
386+
clientCertificatePassword: convertStringToSegments(jsonEditor.password, nodeMap, { tokensEnabled: true }),
387387
};
388388
break;
389389
case AuthenticationType.RAW:
390390
returnItems.raw = {
391-
rawValue: convertStringToSegments(jsonEditor.value, true, nodeMap),
391+
rawValue: convertStringToSegments(jsonEditor.value, nodeMap, { tokensEnabled: true }),
392392
};
393393
break;
394394
case AuthenticationType.MSI:
395395
returnItems.msi = {
396396
msiIdentity: jsonEditor.identity,
397-
msiAudience: convertStringToSegments(jsonEditor.audience, true, nodeMap),
397+
msiAudience: convertStringToSegments(jsonEditor.audience, nodeMap, { tokensEnabled: true }),
398398
};
399399
break;
400400
case AuthenticationType.OAUTH:
401401
returnItems.aadOAuth = {
402-
oauthTenant: convertStringToSegments(jsonEditor.tenant, true, nodeMap),
403-
oauthAudience: convertStringToSegments(jsonEditor.audience, true, nodeMap),
404-
oauthClientId: convertStringToSegments(jsonEditor.clientId, true, nodeMap),
402+
oauthTenant: convertStringToSegments(jsonEditor.tenant, nodeMap, { tokensEnabled: true }),
403+
oauthAudience: convertStringToSegments(jsonEditor.audience, nodeMap, { tokensEnabled: true }),
404+
oauthClientId: convertStringToSegments(jsonEditor.clientId, nodeMap, { tokensEnabled: true }),
405405
};
406406
if (jsonEditor.authority) {
407-
returnItems.aadOAuth.oauthAuthority = convertStringToSegments(jsonEditor.authority, true, nodeMap);
407+
returnItems.aadOAuth.oauthAuthority = convertStringToSegments(jsonEditor.authority, nodeMap, { tokensEnabled: true });
408408
}
409409
if (jsonEditor.secret) {
410410
returnItems.aadOAuth.oauthType = AuthenticationOAuthType.SECRET;
411-
returnItems.aadOAuth.oauthTypeSecret = convertStringToSegments(jsonEditor.secret, true, nodeMap);
411+
returnItems.aadOAuth.oauthTypeSecret = convertStringToSegments(jsonEditor.secret, nodeMap, { tokensEnabled: true });
412412
}
413413
if (jsonEditor.pfx && jsonEditor.password) {
414414
returnItems.aadOAuth.oauthType = AuthenticationOAuthType.CERTIFICATE;
415-
returnItems.aadOAuth.oauthTypeCertificatePfx = convertStringToSegments(jsonEditor.pfx, true, nodeMap);
416-
returnItems.aadOAuth.oauthTypeCertificatePassword = convertStringToSegments(jsonEditor.password, true, nodeMap);
415+
returnItems.aadOAuth.oauthTypeCertificatePfx = convertStringToSegments(jsonEditor.pfx, nodeMap, { tokensEnabled: true });
416+
returnItems.aadOAuth.oauthTypeCertificatePassword = convertStringToSegments(jsonEditor.password, nodeMap, { tokensEnabled: true });
417417
}
418418
break;
419419
default:

libs/designer-ui/src/lib/dictionary/util/serializecollapseddictionary.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export const serializeDictionary = (
2727
const newValue = valueType === constants.SWAGGER.TYPE.STRING ? (value as string) : removeQuotes(JSON.stringify(value));
2828
returnItems.push({
2929
id: guid(),
30-
key: convertStringToSegments(newKey, true, nodeMap),
31-
value: convertStringToSegments(newValue, true, nodeMap),
30+
key: convertStringToSegments(newKey, nodeMap, { tokensEnabled: true }),
31+
value: convertStringToSegments(newValue, nodeMap, { tokensEnabled: true }),
3232
});
3333
}
3434
setItems(returnItems);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { ValueSegmentType } from '../../../models/parameter';
2+
import { convertStringToSegments } from '../editorToSegment';
3+
import type { ValueSegment } from '@microsoft/designer-client-services-logic-apps';
4+
5+
type SimplifiedValueSegment = Omit<ValueSegment, 'id'>;
6+
7+
describe('lib/editor/base/utils/editorToSegment', () => {
8+
describe('convertStringToSegments', () => {
9+
const getInitializeVariableAbcToken = (): SimplifiedValueSegment => ({
10+
type: 'token',
11+
token: {
12+
key: 'variables:abc',
13+
name: 'abc',
14+
type: 'string',
15+
title: 'abc',
16+
brandColor: '#770BD6',
17+
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAzMiAzMiIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMzIgMzIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+DQogPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSIjNzcwQkQ2Ii8+DQogPGcgZmlsbD0iI2ZmZiI+DQogIDxwYXRoIGQ9Ik02Ljc2MywxMy42ODV2LTMuMjA4QzYuNzYzLDguNzQ4LDcuNzYyLDgsMTAsOHYxLjA3Yy0xLDAtMiwwLjMyNS0yLDEuNDA3djMuMTg4ICAgIEM4LDE0LjgzNiw2LjUxMiwxNiw1LjUxMiwxNkM2LjUxMiwxNiw4LDE3LjE2NCw4LDE4LjMzNVYyMS41YzAsMS4wODIsMSwxLjQyOSwyLDEuNDI5VjI0Yy0yLjIzOCwwLTMuMjM4LTAuNzcyLTMuMjM4LTIuNXYtMy4xNjUgICAgYzAtMS4xNDktMC44OTMtMS41MjktMS43NjMtMS41ODV2LTEuNUM1Ljg3LDE1LjE5NCw2Ljc2MywxNC44MzQsNi43NjMsMTMuNjg1eiIvPg0KICA8cGF0aCBkPSJtMjUuMjM4IDEzLjY4NXYtMy4yMDhjMC0xLjcyOS0xLTIuNDc3LTMuMjM4LTIuNDc3djEuMDdjMSAwIDIgMC4zMjUgMiAxLjQwN3YzLjE4OGMwIDEuMTcxIDEuNDg4IDIuMzM1IDIuNDg4IDIuMzM1LTEgMC0yLjQ4OCAxLjE2NC0yLjQ4OCAyLjMzNXYzLjE2NWMwIDEuMDgyLTEgMS40MjktMiAxLjQyOXYxLjA3MWMyLjIzOCAwIDMuMjM4LTAuNzcyIDMuMjM4LTIuNXYtMy4xNjVjMC0xLjE0OSAwLjg5My0xLjUyOSAxLjc2Mi0xLjU4NXYtMS41Yy0wLjg3LTAuMDU2LTEuNzYyLTAuNDE2LTEuNzYyLTEuNTY1eiIvPg0KICA8cGF0aCBkPSJtMTUuODE1IDE2LjUxMmwtMC4yNDItMC42NDFjLTAuMTc3LTAuNDUzLTAuMjczLTAuNjk4LTAuMjg5LTAuNzM0bC0wLjM3NS0wLjgzNmMtMC4yNjYtMC41OTktMC41MjEtMC44OTgtMC43NjYtMC44OTgtMC4zNyAwLTAuNjYyIDAuMzQ3LTAuODc1IDEuMDM5LTAuMTU2LTAuMDU3LTAuMjM0LTAuMTQxLTAuMjM0LTAuMjUgMC0wLjMyMyAwLjE4OC0wLjY5MiAwLjU2Mi0xLjEwOSAwLjM3NS0wLjQxNyAwLjcxLTAuNjI1IDEuMDA3LTAuNjI1IDAuNTgzIDAgMS4xODYgMC44MzkgMS44MTEgMi41MTZsMC4xNjEgMC40MTQgMC4xOC0wLjI4OWMxLjEwOC0xLjc2IDIuMDQ0LTIuNjQxIDIuODA0LTIuNjQxIDAuMTk4IDAgMC40MyAwLjA1OCAwLjY5NSAwLjE3MmwtMC45NDYgMC45OTJjLTAuMTI1LTAuMDM2LTAuMjE0LTAuMDU1LTAuMjY2LTAuMDU1LTAuNTczIDAtMS4yNTYgMC42NTktMi4wNDggMS45NzdsLTAuMjI3IDAuMzc5IDAuMTc5IDAuNDhjMC42ODQgMS44OTEgMS4yNDkgMi44MzYgMS42OTQgMi44MzYgMC40MDggMCAwLjcyLTAuMjkyIDAuOTM1LTAuODc1IDAuMTQ2IDAuMDk0IDAuMjE5IDAuMTkgMC4yMTkgMC4yODkgMCAwLjI2MS0wLjIwOCAwLjU3My0wLjYyNSAwLjkzOHMtMC43NzYgMC41NDctMS4wNzggMC41NDdjLTAuNjA0IDAtMS4yMjEtMC44NTItMS44NTEtMi41NTVsLTAuMjE5LTAuNTc4LTAuMjI3IDAuMzk4Yy0xLjA2MiAxLjgyMy0yLjA3OCAyLjczNC0zLjA0NyAyLjczNC0wLjM2NSAwLTAuNjc1LTAuMDkxLTAuOTMtMC4yNzFsMC45MDYtMC44ODVjMC4xNTYgMC4xNTYgMC4zMzggMC4yMzQgMC41NDcgMC4yMzQgMC41ODggMCAxLjI1LTAuNTk2IDEuOTg0LTEuNzg2bDAuNDA2LTAuNjU4IDAuMTU1LTAuMjU5eiIvPg0KICA8ZWxsaXBzZSB0cmFuc2Zvcm09Im1hdHJpeCguMDUzNiAtLjk5ODYgLjk5ODYgLjA1MzYgNS40OTI1IDMyLjI0NSkiIGN4PSIxOS43NTciIGN5PSIxMy4yMjUiIHJ4PSIuNzc4IiByeT0iLjc3OCIvPg0KICA8ZWxsaXBzZSB0cmFuc2Zvcm09Im1hdHJpeCguMDUzNiAtLjk5ODYgLjk5ODYgLjA1MzYgLTcuNTgzOSAzMC42MjkpIiBjeD0iMTIuMzY2IiBjeT0iMTkuMzE1IiByeD0iLjc3OCIgcnk9Ii43NzgiLz4NCiA8L2c+DQo8L3N2Zz4NCg==',
18+
tokenType: 'variable',
19+
value: "variables('abc')",
20+
},
21+
value: "variables('abc')",
22+
});
23+
const getInitializeVariableOpenBraceToken = (): SimplifiedValueSegment => ({
24+
...getInitializeVariableAbcToken(),
25+
token: {
26+
...getInitializeVariableAbcToken().token,
27+
key: 'variables:@{',
28+
value: "variables('@{')",
29+
},
30+
value: "variables('@{')",
31+
});
32+
const getCreateNewFolderToken = (): SimplifiedValueSegment => ({
33+
type: 'token',
34+
token: {
35+
key: 'body.$.{Link}',
36+
name: '{Link}',
37+
type: 'string',
38+
title: 'Link to item',
39+
brandColor: '#036C70',
40+
icon: 'https://connectoricons-prod.azureedge.net/releases/v1.0.1676/1.0.1676.3617/sharepointonline/icon.png',
41+
description:
42+
'Link that can be used to get to the file or list item. Only people with permissions to the item will be able to open the link.',
43+
tokenType: 'outputs',
44+
actionName: 'Create_new_folder',
45+
required: false,
46+
isSecure: false,
47+
source: 'body',
48+
schema: {
49+
title: 'Link to item',
50+
description:
51+
'Link that can be used to get to the file or list item. Only people with permissions to the item will be able to open the link.',
52+
type: 'string',
53+
'x-ms-permission': 'read-only',
54+
'x-ms-sort': 'none',
55+
},
56+
value: "body('Create_new_folder')?['{Link}']",
57+
},
58+
value: "body('Create_new_folder')?['{Link}']",
59+
});
60+
61+
it('does not parse segments into tokens if tokensEnabled is not set', () => {
62+
const nodeMap = new Map<string, ValueSegment>();
63+
nodeMap.set(`@{variables('abc')}`, { id: '', ...getInitializeVariableAbcToken() });
64+
nodeMap.set(`@{body('Create_new_folder')?['{Link}']}`, { id: '', ...getCreateNewFolderToken() });
65+
66+
const input = `Text before @{variables('abc')} text after`;
67+
68+
const segments = convertStringToSegments(input, nodeMap);
69+
const simplifiedSegments = segments.map(
70+
(segment): SimplifiedValueSegment => ({
71+
// Remove IDs for easier comparison.
72+
token: segment.token,
73+
type: segment.type,
74+
value: segment.value,
75+
})
76+
);
77+
78+
expect(simplifiedSegments).toEqual([{ type: ValueSegmentType.LITERAL, value: input }]);
79+
});
80+
81+
it.each<[string, SimplifiedValueSegment[]]>([
82+
['plain text', [{ type: ValueSegmentType.LITERAL, value: 'plain text' }]],
83+
['@{no closing brace', [{ type: ValueSegmentType.LITERAL, value: '@{no closing brace' }]],
84+
[`Text before @{variables('abc')}`, [{ type: ValueSegmentType.LITERAL, value: 'Text before ' }, getInitializeVariableAbcToken()]],
85+
[
86+
`Text before @{variables('abc')} text after`,
87+
[
88+
{ type: ValueSegmentType.LITERAL, value: 'Text before ' },
89+
getInitializeVariableAbcToken(),
90+
{ type: ValueSegmentType.LITERAL, value: ' text after' },
91+
],
92+
],
93+
[
94+
`Text quoted '@{variables('abc')}'`,
95+
[
96+
{ type: ValueSegmentType.LITERAL, value: `Text quoted '` },
97+
getInitializeVariableAbcToken(),
98+
{ type: ValueSegmentType.LITERAL, value: `'` },
99+
],
100+
],
101+
[
102+
`Text before @{body('Create_new_folder')?['{Link}']} text after`,
103+
[
104+
{ type: ValueSegmentType.LITERAL, value: 'Text before ' },
105+
getCreateNewFolderToken(),
106+
{ type: ValueSegmentType.LITERAL, value: ' text after' },
107+
],
108+
],
109+
[
110+
`t1 @{variables('@{')} t2`,
111+
[
112+
{ type: ValueSegmentType.LITERAL, value: 't1 ' },
113+
getInitializeVariableOpenBraceToken(),
114+
{ type: ValueSegmentType.LITERAL, value: ' t2' },
115+
],
116+
],
117+
[`t1 @{variables('@{')`, [{ type: ValueSegmentType.LITERAL, value: `t1 @{variables('@{')` }]],
118+
[
119+
`t1 @{not a token t2 @{variables('abc')} t3`,
120+
[
121+
{ type: ValueSegmentType.LITERAL, value: 't1 @{not a token t2 ' },
122+
getInitializeVariableAbcToken(),
123+
{ type: ValueSegmentType.LITERAL, value: ' t3' },
124+
],
125+
],
126+
[
127+
`t1 @{not a token} t2 @{variables('abc')} t3`,
128+
[
129+
{ type: ValueSegmentType.LITERAL, value: 't1 @{not a token} t2 ' },
130+
getInitializeVariableAbcToken(),
131+
{ type: ValueSegmentType.LITERAL, value: ' t3' },
132+
],
133+
],
134+
])('parses segments out of %p with appropriate node map', (input, expectedTokens) => {
135+
const nodeMap = new Map<string, ValueSegment>();
136+
nodeMap.set(`@{variables('abc')}`, { id: '', ...getInitializeVariableAbcToken() });
137+
nodeMap.set(`@{variables('@{')}`, { id: '', ...getInitializeVariableOpenBraceToken() });
138+
nodeMap.set(`@{body('Create_new_folder')?['{Link}']}`, { id: '', ...getCreateNewFolderToken() });
139+
140+
const segments = convertStringToSegments(input, nodeMap, { tokensEnabled: true });
141+
const simplifiedSegments = segments.map(
142+
(segment): SimplifiedValueSegment => ({
143+
// Remove IDs for easier comparison.
144+
token: segment.token,
145+
type: segment.type,
146+
value: segment.value,
147+
})
148+
);
149+
150+
expect(simplifiedSegments).toEqual(expectedTokens);
151+
});
152+
153+
it.each<[string, SimplifiedValueSegment[]]>([
154+
['plain text', [{ type: ValueSegmentType.LITERAL, value: 'plain text' }]],
155+
[
156+
`Text before @{variables('abc')} text after`,
157+
[{ type: ValueSegmentType.LITERAL, value: `Text before @{variables('abc')} text after` }],
158+
],
159+
[
160+
`Text before @{body('Create_new_folder')?['{Link}']} text after`,
161+
[{ type: ValueSegmentType.LITERAL, value: `Text before @{body('Create_new_folder')?['{Link}']} text after` }],
162+
],
163+
])('parses segments out of %p with an empty node map', (input, expectedTokens) => {
164+
const nodeMap = new Map<string, ValueSegment>();
165+
166+
const segments = convertStringToSegments(input, nodeMap, { tokensEnabled: true });
167+
const simplifiedSegments = segments.map(
168+
(segment): SimplifiedValueSegment => ({
169+
// Remove IDs for easier comparison.
170+
token: segment.token,
171+
type: segment.type,
172+
value: segment.value,
173+
})
174+
);
175+
176+
expect(simplifiedSegments).toEqual(expectedTokens);
177+
});
178+
});
179+
});

0 commit comments

Comments
 (0)