Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions expression-src/main/editor/lwc/miniEditor/miniEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,12 @@ pre {
.border-gray-200 {
border-color: var(--lwc-colorBorder);
}

.compact-input {
font-size: 0.875rem;
}

.compact-input input {
height: 2.25rem;
font-size: 0.875rem;
}
61 changes: 57 additions & 4 deletions expression-src/main/editor/lwc/miniEditor/miniEditor.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,31 @@
<lightning-card>
<div class="slds-grid slds-grid_align-spread slds-grid_vertical-align-center">
<div class="slds-col slds-text-body_small slds-text-title_bold pl-2">{title}</div>
<div class="slds-col">
<lightning-button variant="brand-outline" label="Save"
onclick={handleExpressionSaved}
></lightning-button>
<div class="slds-col slds-grid slds-grid_align-end slds-gutters_x-small">
<template lwc:if={enableValidation}>
<div class="slds-col" style="width: 300px;">
<lightning-input type="text"
value={recordId}
onchange={handleRecordIdChange}
placeholder="Record Id for validation (optional)"
variant="label-hidden"
class="compact-input"
></lightning-input>
</div>
</template>
<template lwc:if={showValidationButton}>
<div class="slds-col">
<lightning-button variant="brand" label="Validate"
onclick={handleValidate}
icon-name="utility:play"
></lightning-button>
</div>
</template>
<div class="slds-col">
<lightning-button variant="brand-outline" label="Save"
onclick={handleExpressionSaved}
></lightning-button>
</div>
</div>
</div>
<div class="pt-1">
Expand Down Expand Up @@ -43,6 +64,7 @@
href="#"
data-name={value.name}
onmouseenter={handleMouseEnter}
onclick={handleFunctionClick}
>
{value.name}
</a>
Expand Down Expand Up @@ -76,5 +98,36 @@
</div>
</div>
</div>
<template lwc:if={showResults}>
<hr/>
<div class="pt-1">
<lightning-tabset>
<lightning-tab label="Result">
<template for:each={result.payload} for:item="payload">
<div key={payload.message}>
<lightning-badge label={payload.type}></lightning-badge>
<pre class={resultColor}>
<lightning-formatted-rich-text value={payload.message}></lightning-formatted-rich-text>
</pre>
<hr/>
</div>
</template>
</lightning-tab>
<lightning-tab label="Diagnostics">
<ul>
<li><strong>CPU Time (ms):</strong> {diagnostics.cpuTime}</li>
<li><strong>DML Statements:</strong> {diagnostics.dmlStatements}</li>
<li><strong>Queries:</strong> {diagnostics.queries}</li>
<li><strong>Query Rows:</strong> {diagnostics.queryRows}</li>
</ul>
</lightning-tab>
<lightning-tab label="AST">
<pre class={resultColor}>
<lightning-formatted-rich-text value={ast}></lightning-formatted-rich-text>
</pre>
</lightning-tab>
</lightning-tabset>
</div>
</template>
</lightning-card>
</template>
164 changes: 164 additions & 0 deletions expression-src/main/editor/lwc/miniEditor/miniEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { LightningElement, api } from 'lwc';
import { getFunctionsAndOperators } from 'c/functions';
import monaco from '@salesforce/resourceUrl/monaco';
import getFunctions from "@salesforce/apex/PlaygroundController.getFunctionNames";
import validate from '@salesforce/apex/PlaygroundController.validate';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class MiniEditor extends LightningElement {
@api
Expand All @@ -27,11 +29,26 @@ export default class MiniEditor extends LightningElement {
@api
defaultExpression = '';

@api
enableValidation = false;

@api
recordId = '';

iframeUrl = `${monaco}/main.html`;

categories = [];
expression = '';
lastHoveredFunction = null;
result = {};
diagnostics = {
cpuTime: "Unavailable",
dmlStatements: "Unavailable",
queries: "Unavailable",
queryRows: "Unavailable",
};
ast = "";
showResults = false;

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

get showValidationButton() {
return this.enableValidation && (this.variant === 'editor' || this.variant === 'textarea');
}

get resultColor() {
return this.result.type === 'error' ? 'slds-text-color_error' : 'slds-text-color_default';
}

async iframeLoaded() {
const functionKeywords = await getFunctions();
this.iframeWindow.postMessage({
Expand Down Expand Up @@ -132,4 +157,143 @@ export default class MiniEditor extends LightningElement {
}
}
}

handleFunctionClick(event) {
event.preventDefault();
const functionName = event.target.dataset.name;
if (!functionName) {
return;
}

// Find the function to insert
for (const category of this.categories) {
const foundValue = category.values.find((value) => value.name === functionName);
if (foundValue) {
const functionToInsert = foundValue.autoCompleteValue || foundValue.name;

if (this.variant === 'editor') {
// For Monaco editor, send message to iframe
this.iframeWindow.postMessage({
name: 'append',
payload: functionToInsert
});
} else if (this.variant === 'textarea' || this.variant === 'input') {
// For textarea and input, insert at cursor position
const inputElement = this.template.querySelector(`${this.variant}[name="expression"]`);
if (inputElement) {
const start = inputElement.selectionStart;
const end = inputElement.selectionEnd;
const text = inputElement.value;
const before = text.substring(0, start);
const after = text.substring(end, text.length);

this.expression = before + functionToInsert + after;
inputElement.value = this.expression;

// Set cursor position after inserted text
setTimeout(() => {
const newPosition = start + functionToInsert.length;
inputElement.setSelectionRange(newPosition, newPosition);
inputElement.focus();
}, 0);
}
}
break;
}
}
}

handleRecordIdChange(event) {
this.recordId = event.target.value;
}

async handleValidate() {
if (!this.expression) {
return;
}

if (this.variant === 'editor') {
this.iframeWindow.postMessage({
name: 'clear_markers'
});
}

try {
const result = await validate({ expr: this.expression, recordId: this.recordId });
if (result.error) {
this.result = {
type: "error",
payload: [{ type: 'error', message: result.error.message }]
};

if (this.variant === 'editor') {
this.iframeWindow.postMessage({
name: 'evaluation_error',
payload: result.error
});
}
} else {
const payload = result.result ?? null;
const toPrint = result.toPrint.map((item) => item ?? null);
const allResults = [...toPrint, payload];
this.result = {
type: "success",
payload: allResults.map((current, i) => ({
type: i === allResults.length - 1 ? "result" : "printed",
message: this._syntaxHighlight(JSON.stringify(current, null, 4))
}))
};
}

this._setDiagnostics(result.diagnostics ?? {});
this.ast = result.ast ?
this._syntaxHighlight(JSON.stringify(result.ast, null, 4)) :
"";
this.showResults = true;
} catch (e) {
const event = new ShowToastEvent({
title: 'Unknown error',
variant: 'error',
message: e.body?.message || 'An unknown error occurred',
});
this.dispatchEvent(event);

this.result = {
type: "error",
payload: [{ type: 'error', message: 'An unknown error occurred while evaluating the expression.' }]
};
this.showResults = true;
}
}

_setDiagnostics(diagnostics) {
this.diagnostics = Object.keys(diagnostics).reduce((acc, key) => {
acc[key] = diagnostics[key] ?? "Unavailable";
return acc;
}, {
cpuTime: "Unavailable",
dmlStatements: "Unavailable",
queries: "Unavailable",
queryRows: "Unavailable",
});
}

_syntaxHighlight(json) {
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
let style = 'color: #c2410c;';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
style = 'color: #b91c1c;';
} else {
style = 'color: #0f766e;';
}
} else if (/true|false/.test(match)) {
style = 'color: #4338ca;';
} else if (/null/.test(match)) {
style = 'color: #0e7490;';
}
return '<span style="' + style + '">' + match + '</span>';
});
}
}