Skip to content

Commit 0c91682

Browse files
adc103AlecCollinscesarParra
authored
- Fix Function Clicking (Before it would reset the component when any clicked) (#219)
- Add validation logic (Mostly copied from normal playground) Co-authored-by: AlecCollins <Alec.Collins@purelightpower.com> Co-authored-by: Cesar Parra <cesar.parra@gmail.com>
1 parent c7bcc85 commit 0c91682

File tree

3 files changed

+230
-4
lines changed

3 files changed

+230
-4
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,12 @@ pre {
161161
.border-gray-200 {
162162
border-color: var(--lwc-colorBorder);
163163
}
164+
165+
.compact-input {
166+
font-size: 0.875rem;
167+
}
168+
169+
.compact-input input {
170+
height: 2.25rem;
171+
font-size: 0.875rem;
172+
}

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

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,31 @@
22
<lightning-card>
33
<div class="slds-grid slds-grid_align-spread slds-grid_vertical-align-center">
44
<div class="slds-col slds-text-body_small slds-text-title_bold pl-2">{title}</div>
5-
<div class="slds-col">
6-
<lightning-button variant="brand-outline" label="Save"
7-
onclick={handleExpressionSaved}
8-
></lightning-button>
5+
<div class="slds-col slds-grid slds-grid_align-end slds-gutters_x-small">
6+
<template lwc:if={enableValidation}>
7+
<div class="slds-col" style="width: 300px;">
8+
<lightning-input type="text"
9+
value={recordId}
10+
onchange={handleRecordIdChange}
11+
placeholder="Record Id for validation (optional)"
12+
variant="label-hidden"
13+
class="compact-input"
14+
></lightning-input>
15+
</div>
16+
</template>
17+
<template lwc:if={showValidationButton}>
18+
<div class="slds-col">
19+
<lightning-button variant="brand" label="Validate"
20+
onclick={handleValidate}
21+
icon-name="utility:play"
22+
></lightning-button>
23+
</div>
24+
</template>
25+
<div class="slds-col">
26+
<lightning-button variant="brand-outline" label="Save"
27+
onclick={handleExpressionSaved}
28+
></lightning-button>
29+
</div>
930
</div>
1031
</div>
1132
<div class="pt-1">
@@ -43,6 +64,7 @@
4364
href="#"
4465
data-name={value.name}
4566
onmouseenter={handleMouseEnter}
67+
onclick={handleFunctionClick}
4668
>
4769
{value.name}
4870
</a>
@@ -76,5 +98,36 @@
7698
</div>
7799
</div>
78100
</div>
101+
<template lwc:if={showResults}>
102+
<hr/>
103+
<div class="pt-1">
104+
<lightning-tabset>
105+
<lightning-tab label="Result">
106+
<template for:each={result.payload} for:item="payload">
107+
<div key={payload.message}>
108+
<lightning-badge label={payload.type}></lightning-badge>
109+
<pre class={resultColor}>
110+
<lightning-formatted-rich-text value={payload.message}></lightning-formatted-rich-text>
111+
</pre>
112+
<hr/>
113+
</div>
114+
</template>
115+
</lightning-tab>
116+
<lightning-tab label="Diagnostics">
117+
<ul>
118+
<li><strong>CPU Time (ms):</strong> {diagnostics.cpuTime}</li>
119+
<li><strong>DML Statements:</strong> {diagnostics.dmlStatements}</li>
120+
<li><strong>Queries:</strong> {diagnostics.queries}</li>
121+
<li><strong>Query Rows:</strong> {diagnostics.queryRows}</li>
122+
</ul>
123+
</lightning-tab>
124+
<lightning-tab label="AST">
125+
<pre class={resultColor}>
126+
<lightning-formatted-rich-text value={ast}></lightning-formatted-rich-text>
127+
</pre>
128+
</lightning-tab>
129+
</lightning-tabset>
130+
</div>
131+
</template>
79132
</lightning-card>
80133
</template>

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

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { LightningElement, api } from 'lwc';
22
import { getFunctionsAndOperators } from 'c/functions';
33
import monaco from '@salesforce/resourceUrl/monaco';
44
import getFunctions from "@salesforce/apex/PlaygroundController.getFunctionNames";
5+
import validate from '@salesforce/apex/PlaygroundController.validate';
6+
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
57

68
export default class MiniEditor extends LightningElement {
79
@api
@@ -27,11 +29,26 @@ export default class MiniEditor extends LightningElement {
2729
@api
2830
defaultExpression = '';
2931

32+
@api
33+
enableValidation = false;
34+
35+
@api
36+
recordId = '';
37+
3038
iframeUrl = `${monaco}/main.html`;
3139

3240
categories = [];
3341
expression = '';
3442
lastHoveredFunction = null;
43+
result = {};
44+
diagnostics = {
45+
cpuTime: "Unavailable",
46+
dmlStatements: "Unavailable",
47+
queries: "Unavailable",
48+
queryRows: "Unavailable",
49+
};
50+
ast = "";
51+
showResults = false;
3552

3653
async connectedCallback() {
3754
this.categories = await getFunctionsAndOperators();
@@ -50,6 +67,14 @@ export default class MiniEditor extends LightningElement {
5067
return this.variant === 'editor';
5168
}
5269

70+
get showValidationButton() {
71+
return this.enableValidation && (this.variant === 'editor' || this.variant === 'textarea');
72+
}
73+
74+
get resultColor() {
75+
return this.result.type === 'error' ? 'slds-text-color_error' : 'slds-text-color_default';
76+
}
77+
5378
async iframeLoaded() {
5479
const functionKeywords = await getFunctions();
5580
this.iframeWindow.postMessage({
@@ -132,4 +157,143 @@ export default class MiniEditor extends LightningElement {
132157
}
133158
}
134159
}
160+
161+
handleFunctionClick(event) {
162+
event.preventDefault();
163+
const functionName = event.target.dataset.name;
164+
if (!functionName) {
165+
return;
166+
}
167+
168+
// Find the function to insert
169+
for (const category of this.categories) {
170+
const foundValue = category.values.find((value) => value.name === functionName);
171+
if (foundValue) {
172+
const functionToInsert = foundValue.autoCompleteValue || foundValue.name;
173+
174+
if (this.variant === 'editor') {
175+
// For Monaco editor, send message to iframe
176+
this.iframeWindow.postMessage({
177+
name: 'append',
178+
payload: functionToInsert
179+
});
180+
} else if (this.variant === 'textarea' || this.variant === 'input') {
181+
// For textarea and input, insert at cursor position
182+
const inputElement = this.template.querySelector(`${this.variant}[name="expression"]`);
183+
if (inputElement) {
184+
const start = inputElement.selectionStart;
185+
const end = inputElement.selectionEnd;
186+
const text = inputElement.value;
187+
const before = text.substring(0, start);
188+
const after = text.substring(end, text.length);
189+
190+
this.expression = before + functionToInsert + after;
191+
inputElement.value = this.expression;
192+
193+
// Set cursor position after inserted text
194+
setTimeout(() => {
195+
const newPosition = start + functionToInsert.length;
196+
inputElement.setSelectionRange(newPosition, newPosition);
197+
inputElement.focus();
198+
}, 0);
199+
}
200+
}
201+
break;
202+
}
203+
}
204+
}
205+
206+
handleRecordIdChange(event) {
207+
this.recordId = event.target.value;
208+
}
209+
210+
async handleValidate() {
211+
if (!this.expression) {
212+
return;
213+
}
214+
215+
if (this.variant === 'editor') {
216+
this.iframeWindow.postMessage({
217+
name: 'clear_markers'
218+
});
219+
}
220+
221+
try {
222+
const result = await validate({ expr: this.expression, recordId: this.recordId });
223+
if (result.error) {
224+
this.result = {
225+
type: "error",
226+
payload: [{ type: 'error', message: result.error.message }]
227+
};
228+
229+
if (this.variant === 'editor') {
230+
this.iframeWindow.postMessage({
231+
name: 'evaluation_error',
232+
payload: result.error
233+
});
234+
}
235+
} else {
236+
const payload = result.result ?? null;
237+
const toPrint = result.toPrint.map((item) => item ?? null);
238+
const allResults = [...toPrint, payload];
239+
this.result = {
240+
type: "success",
241+
payload: allResults.map((current, i) => ({
242+
type: i === allResults.length - 1 ? "result" : "printed",
243+
message: this._syntaxHighlight(JSON.stringify(current, null, 4))
244+
}))
245+
};
246+
}
247+
248+
this._setDiagnostics(result.diagnostics ?? {});
249+
this.ast = result.ast ?
250+
this._syntaxHighlight(JSON.stringify(result.ast, null, 4)) :
251+
"";
252+
this.showResults = true;
253+
} catch (e) {
254+
const event = new ShowToastEvent({
255+
title: 'Unknown error',
256+
variant: 'error',
257+
message: e.body?.message || 'An unknown error occurred',
258+
});
259+
this.dispatchEvent(event);
260+
261+
this.result = {
262+
type: "error",
263+
payload: [{ type: 'error', message: 'An unknown error occurred while evaluating the expression.' }]
264+
};
265+
this.showResults = true;
266+
}
267+
}
268+
269+
_setDiagnostics(diagnostics) {
270+
this.diagnostics = Object.keys(diagnostics).reduce((acc, key) => {
271+
acc[key] = diagnostics[key] ?? "Unavailable";
272+
return acc;
273+
}, {
274+
cpuTime: "Unavailable",
275+
dmlStatements: "Unavailable",
276+
queries: "Unavailable",
277+
queryRows: "Unavailable",
278+
});
279+
}
280+
281+
_syntaxHighlight(json) {
282+
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
283+
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
284+
let style = 'color: #c2410c;';
285+
if (/^"/.test(match)) {
286+
if (/:$/.test(match)) {
287+
style = 'color: #b91c1c;';
288+
} else {
289+
style = 'color: #0f766e;';
290+
}
291+
} else if (/true|false/.test(match)) {
292+
style = 'color: #4338ca;';
293+
} else if (/null/.test(match)) {
294+
style = 'color: #0e7490;';
295+
}
296+
return '<span style="' + style + '">' + match + '</span>';
297+
});
298+
}
135299
}

0 commit comments

Comments
 (0)