Skip to content

Commit 6448f13

Browse files
authored
Mini editor monaco support (#203)
* Enhance mini editor with Monaco support * Releasing new package version * Releasing new package version * The Transform function inherits the parent environment to have access to any contextual fields
1 parent 807372c commit 6448f13

File tree

10 files changed

+120
-49
lines changed

10 files changed

+120
-49
lines changed

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/public/packages.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"packageId": "04tRb0000036iJBIAY",
2+
"packageId": "04tRb0000037T8PIAU",
33
"componentPackageId": "04tRb0000012Mv8IAE"
44
}

expression-src/main/editor/lwc/miniEditor/miniEditor.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ pre {
8585
min-width: 12rem;
8686
}
8787

88+
.w-48 {
89+
width: 12rem;
90+
}
91+
8892
.max-h-72 {
8993
max-height: 18rem;
9094
}

expression-src/main/editor/lwc/miniEditor/miniEditor.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,29 @@
99
</div>
1010
</div>
1111
<div class="pt-1">
12-
<template lwc:if={displayAsTextArea}>
12+
<template lwc:if={displayAsTextareaVariant}>
1313
<textarea name="expression"
1414
onkeyup={handleExpressionChange}
1515
class="unstyled-input p-2 mono" placeholder={placeholder} value={expression}>
1616
{expression}
1717
</textarea>
1818
</template>
19-
<template lwc:else>
19+
<template lwc:if={displayAsInput}>
2020
<input type="text" name="expression"
2121
onkeyup={handleExpressionChange}
2222
class="unstyled-input p-2 mono" placeholder={placeholder} value={expression}/>
2323
</template>
24+
<template lwc:if={displayAsEditor}>
25+
<iframe src={iframeUrl}
26+
scrolling="no" style="width: 100%; height: 100%; overflow: hidden; border: none"
27+
onload={iframeLoaded}
28+
></iframe>
29+
</template>
2430
</div>
2531
<hr/>
2632
<div class="mb-1">
2733
<div class="flex">
28-
<div class="pl-2 pt-2 pr-4 min-w-48 max-h-72 overflow-y-scroll">
34+
<div class="pl-2 pt-2 pr-4 w-48 min-w-48 max-h-72 overflow-y-scroll">
2935
<ul class="list-none px-0">
3036
<template for:each={displayFunctions} for:item="category">
3137
<li class="truncate pt-1" key={category.category}>

expression-src/main/editor/lwc/miniEditor/miniEditor.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { LightningElement, api } from 'lwc';
22
import { getFunctionsAndOperators } from 'c/functions';
3+
import monaco from '@salesforce/resourceUrl/monaco';
4+
import getFunctions from "@salesforce/apex/PlaygroundController.getFunctionNames";
35

46
export default class MiniEditor extends LightningElement {
57
@api
@@ -8,12 +10,25 @@ export default class MiniEditor extends LightningElement {
810
@api
911
placeholder = "Your Expression";
1012

13+
/**
14+
* Deprecated: Use variant instead.
15+
* @type {boolean}
16+
*/
1117
@api
1218
displayAsTextArea = false;
1319

20+
/**
21+
*
22+
* @type {"input" | "textarea" | "editor"}
23+
*/
24+
@api
25+
variant = "editor";
26+
1427
@api
1528
defaultExpression = '';
1629

30+
iframeUrl = `${monaco}/main.html`;
31+
1732
categories = [];
1833
expression = '';
1934
lastHoveredFunction = null;
@@ -23,6 +38,38 @@ export default class MiniEditor extends LightningElement {
2338
this.expression = this.defaultExpression;
2439
}
2540

41+
get displayAsInput() {
42+
return this.variant === 'input';
43+
}
44+
45+
get displayAsTextareaVariant() {
46+
return this.variant === 'textarea';
47+
}
48+
49+
get displayAsEditor() {
50+
return this.variant === 'editor';
51+
}
52+
53+
async iframeLoaded() {
54+
const functionKeywords = await getFunctions();
55+
this.iframeWindow.postMessage({
56+
name: 'initialize',
57+
keywords: functionKeywords,
58+
initialValue: this.expression
59+
});
60+
61+
window.addEventListener('message', (event) => {
62+
const {name, payload} = event.data;
63+
if (name === 'content_change') {
64+
this.expression = payload;
65+
}
66+
}, false);
67+
}
68+
69+
get iframeWindow() {
70+
return this.template.querySelector('iframe').contentWindow;
71+
}
72+
2673
get displayFunctions() {
2774
const data = this.categories.map((category) => {
2875
return {

expression-src/main/editor/staticresources/monaco/main.html

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<!DOCTYPE html>
22
<html>
33
<head>
4-
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
5-
<link
6-
rel="stylesheet"
7-
data-name="vs/editor/editor.main"
8-
href="min/vs/editor/editor.main.css"
9-
/>
4+
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
5+
<link
6+
rel="stylesheet"
7+
data-name="vs/editor/editor.main"
8+
href="min/vs/editor/editor.main.css"
9+
/>
1010
</head>
1111
<body style="margin: 0;">
1212
<div id="container" style="height: 100vh;"></div>
@@ -23,7 +23,7 @@
2323

2424
function onMessage(event) {
2525
if (event.data.name === 'initialize') {
26-
initialize(event.data.keywords);
26+
initialize(event.data.keywords, event.data.initialValue);
2727
}
2828
if (event.data.name === 'evaluation_error') {
2929
markError(event.data.payload);
@@ -36,7 +36,7 @@
3636
}
3737
}
3838

39-
function initialize(functionKeywords) {
39+
function initialize(functionKeywords, initialValue) {
4040
monaco.languages.register({id: 'expression'});
4141
let keywords = functionKeywords;
4242

@@ -147,7 +147,7 @@
147147
});
148148

149149
window.editor = monaco.editor.create(document.getElementById('container'), {
150-
value: [''].join('\n'),
150+
value: initialValue ?? [''].join('\n'),
151151
language: 'expression',
152152
wordWrap: "wordWrapColumn",
153153
wordWrapColumn: 100,

expression-src/main/src/interpreter/std-lib/DataFunctions.cls

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public with sharing class DataFunctions {
3030
private class TransformFn extends StandardFunction {
3131
public override Object call(List<Expr> arguments) {
3232
Object sourceObj = evaluate(arguments.get(0));
33-
Environment env = new Environment();
33+
Environment env = new Environment(this.interpreter.getEnvironment());
3434
env.define('$source', sourceObj);
3535
Interpreter interpreter = new Interpreter(env);
3636
return interpreter.interpret(arguments.get(1));
@@ -127,80 +127,80 @@ public with sharing class DataFunctions {
127127
public class ParseJsonFn extends StandardFunction {
128128
public override Object call(List<Expr> arguments) {
129129
Object jsonInput = evaluate(arguments.get(0));
130-
130+
131131
if (!(jsonInput instanceof String)) {
132132
throw new FunctionExecutionException(
133133
'Error executing "PARSEJSON" function: the argument must evaluate to a string value.'
134134
);
135135
}
136-
136+
137137
String jsonStr = (String) jsonInput;
138-
138+
139139
try {
140140
Object deserializedObj = JSON.deserializeUntyped(jsonStr);
141-
141+
142142
if (!(deserializedObj instanceof Map<String, Object>)) {
143143
Map<Object, Object> wrappedMap = new Map<Object, Object>();
144144
wrappedMap.put('value', deserializedObj);
145145
return wrappedMap;
146146
}
147-
148-
Map<String, Object> typedMap = (Map<String, Object>)deserializedObj;
147+
148+
Map<String, Object> typedMap = (Map<String, Object>) deserializedObj;
149149
Map<Object, Object> resultMap = new Map<Object, Object>();
150-
150+
151151
for (String key : typedMap.keySet()) {
152152
Object value = typedMap.get(key);
153153
if (value instanceof Map<String, Object>) {
154-
value = convertToObjectMap((Map<String, Object>)value);
154+
value = convertToObjectMap((Map<String, Object>) value);
155155
} else if (value instanceof List<Object>) {
156-
value = convertListContents((List<Object>)value);
156+
value = convertListContents((List<Object>) value);
157157
}
158158
resultMap.put(key, value);
159159
}
160-
160+
161161
return resultMap;
162162
} catch (Exception e) {
163163
throw new FunctionExecutionException(
164164
'Error executing "PARSEJSON" function: Invalid JSON format. ' + e.getMessage()
165165
);
166166
}
167167
}
168-
168+
169169
private Map<Object, Object> convertToObjectMap(Map<String, Object> typedMap) {
170170
Map<Object, Object> resultMap = new Map<Object, Object>();
171-
171+
172172
for (String key : typedMap.keySet()) {
173173
Object value = typedMap.get(key);
174174
// Recursively convert nested maps
175175
if (value instanceof Map<String, Object>) {
176-
value = convertToObjectMap((Map<String, Object>)value);
176+
value = convertToObjectMap((Map<String, Object>) value);
177177
} else if (value instanceof List<Object>) {
178-
value = convertListContents((List<Object>)value);
178+
value = convertListContents((List<Object>) value);
179179
}
180180
resultMap.put(key, value);
181181
}
182-
182+
183183
return resultMap;
184184
}
185-
185+
186186
private List<Object> convertListContents(List<Object> typedList) {
187187
List<Object> resultList = new List<Object>();
188-
188+
189189
for (Object item : typedList) {
190190
if (item instanceof Map<String, Object>) {
191-
resultList.add(convertToObjectMap((Map<String, Object>)item));
191+
resultList.add(convertToObjectMap((Map<String, Object>) item));
192192
} else if (item instanceof List<Object>) {
193-
resultList.add(convertListContents((List<Object>)item));
193+
resultList.add(convertListContents((List<Object>) item));
194194
} else {
195195
resultList.add(item);
196196
}
197197
}
198-
198+
199199
return resultList;
200200
}
201201

202202
public override Arity getArity() {
203203
return Arity.exactly(1);
204204
}
205205
}
206-
}
206+
}

expression-src/spec/language/std-functions/DataFunctionsTest.cls

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ private class DataFunctionsTest {
77
Assert.areEqual('A', result);
88
}
99

10+
@IsTest
11+
private static void transformCanAccessTheParentEnvironment() {
12+
Account someRecord = new Account(Name = 'Test Account');
13+
14+
String formula = '"Test Account" -> TRANSFORM($source = Name)';
15+
Object result = Evaluator.run(formula, someRecord);
16+
17+
Assert.isTrue((Boolean) result, 'The transform function should return true when the source matches the expected value.');
18+
}
19+
1020
@IsTest
1121
private static void letFunctionAllowsForVariablesToBeDefined() {
1222
String formula = 'LET({"$a": 1, "$b": 2}, $a + $b)';
@@ -20,56 +30,56 @@ private class DataFunctionsTest {
2030
Object result = Evaluator.run(formula);
2131
Assert.isNotNull(result);
2232
}
23-
33+
2434
@IsTest
2535
private static void parseJsonWithAccount() {
2636
Account acc = new Account(
2737
Name = 'Test Account',
2838
Description = '{"name":"John","age":30}'
2939
);
3040
insert acc;
31-
41+
3242
String formula = 'PARSEJSON(Description)';
3343
Object result = Evaluator.run(formula, acc.Id);
34-
44+
3545
Map<Object, Object> resultMap = (Map<Object, Object>) result;
3646
Assert.areEqual('John', resultMap.get('name'));
3747
Assert.areEqual(30, resultMap.get('age'));
3848
}
39-
49+
4050
@IsTest
4151
private static void parseJsonWithNestedObject() {
4252
Account acc = new Account(
4353
Name = 'Test Account',
4454
Description = '{"name":"John","age":30,"address":{"city":"New York","zip":10001}}'
4555
);
4656
insert acc;
47-
57+
4858
String formula = 'PARSEJSON(Description)';
4959
Object result = Evaluator.run(formula, acc.Id);
50-
60+
5161
Map<Object, Object> resultMap = (Map<Object, Object>) result;
5262
Map<Object, Object> address = (Map<Object, Object>) resultMap.get('address');
5363
Assert.areEqual('New York', address.get('city'));
5464
Assert.areEqual(10001, address.get('zip'));
5565
}
56-
66+
5767
@IsTest
5868
private static void parseJsonWithList() {
5969
Account acc = new Account(
6070
Name = 'Test Account',
6171
Description = '{"name":"John","age":30,"skills":["Apex","JavaScript","HTML"]}'
6272
);
6373
insert acc;
64-
74+
6575
String formula = 'PARSEJSON(Description)';
6676
Object result = Evaluator.run(formula, acc.Id);
67-
77+
6878
Map<Object, Object> resultMap = (Map<Object, Object>) result;
6979
List<Object> skills = (List<Object>) resultMap.get('skills');
7080
Assert.areEqual('Apex', skills[0]);
7181
Assert.areEqual('JavaScript', skills[1]);
7282
Assert.areEqual('HTML', skills[2]);
7383
}
7484

75-
}
85+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"start:dev:components": "npm run scratch:create:communities && npm run source:push && npm run permset:assign",
1111
"start:dev:example:conf": "npm run start:dev:components && sf project deploy start -d unpackaged/examples/expression-conf/main && sf data import tree -p unpackaged/examples/expression-conf/data/data-plan.json",
1212
"prepackage:create:expression": "node scripts/prepare-for-packaging.mjs",
13-
"package:create:expression": "sf package version create -x -c -p 0HoHu000000TNMAKA4 -w 30",
13+
"package:create:expression": "sf package version create -x -c -p 0HoHu000000TNMAKA4 -w 60",
1414
"postpackage:create:expression": "node scripts/post-packaging.mjs",
1515
"package:create:expression:components": "npm run package:pre && sf package version create -x -c -p 0HoHu000000TNMFKA4 -w 30 && npm run package:post",
1616
"generate:library:docs": "node scripts/generate-library-docs.mjs",

0 commit comments

Comments
 (0)