From 47a0f6e9cfa3678f2024819f3b8e620c344dab8f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 13 Feb 2025 13:12:15 +0100 Subject: [PATCH 01/23] Unify --- .../common/bindings/BindingPanel.svelte | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 4bd37bf72c..e583ffcfdd 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -70,6 +70,7 @@ let expressionLogs: Log[] | undefined let expressionError: string | undefined let evaluating = false + let completions: BindingCompletion[] = [] $: useSnippets = allowSnippets && !$licensing.isFreePlan $: editorModeOptions = getModeOptions(allowHBS, allowJS) @@ -90,11 +91,20 @@ $: requestEval(runtimeExpression, context, snippets) $: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode) $: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos) - $: hbsCompletions = getHBSCompletions(bindingCompletions) - $: jsCompletions = getJSCompletions(bindingCompletions, snippets, { - useHelpers: allowHelpers, - useSnippets, - }) + + $: { + if (mode === BindingMode.Text) { + completions = getHBSCompletions(bindingCompletions) + } else if (mode === BindingMode.JavaScript) { + completions = getJSCompletions(bindingCompletions, snippets, { + useHelpers: allowHelpers, + useSnippets, + }) + } else { + completions = [] + } + } + $: { // Ensure a valid side panel option is always selected if (sidePanel && !sidePanelOptions.includes(sidePanel)) { @@ -365,13 +375,13 @@ {/if}
{#if mode === BindingMode.Text} - {#key hbsCompletions} + {#key completions} {/key} {:else if mode === BindingMode.JavaScript} - {#key jsCompletions} + {#key completions} Date: Thu, 13 Feb 2025 13:54:15 +0100 Subject: [PATCH 02/23] Capture options --- .../common/bindings/BindingPanel.svelte | 85 +++++++++---------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index e583ffcfdd..935c6d48bc 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -58,7 +58,7 @@ export let placeholder = null export let showTabBar = true - let mode: BindingMode | null + let mode: BindingMode let sidePanel: SidePanel | null let initialValueJS = value?.startsWith?.("{{ js ") let jsValue: string | null = initialValueJS ? value : null @@ -71,6 +71,7 @@ let expressionError: string | undefined let evaluating = false let completions: BindingCompletion[] = [] + let autocompleteOptions: BindingCompletionOption[] = [] $: useSnippets = allowSnippets && !$licensing.isFreePlan $: editorModeOptions = getModeOptions(allowHBS, allowJS) @@ -89,22 +90,44 @@ | null $: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value) $: requestEval(runtimeExpression, context, snippets) - $: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode) $: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos) - $: { - if (mode === BindingMode.Text) { - completions = getHBSCompletions(bindingCompletions) - } else if (mode === BindingMode.JavaScript) { - completions = getJSCompletions(bindingCompletions, snippets, { - useHelpers: allowHelpers, - useSnippets, - }) - } else { - completions = [] + function getOptions( + mode: typeof editorMode, + bindings: EnrichedBinding[] + ): [BindingCompletionOption[], BindingCompletion[]] { + const autocompleteOptions = [] + const completions = [] + + const bindingOptions = bindingsToCompletions(bindings, editorMode) + const helperOptions = getHelperCompletions(mode) + + if (mode.name === "handlebars") { + autocompleteOptions.push(...bindingOptions) + autocompleteOptions.push(...helperOptions) + + completions.push(hbAutocomplete(autocompleteOptions)) + } else if (mode.name === "javascript") { + if (bindingOptions.length) { + completions.push(jsAutocomplete(bindingOptions)) + } + + if (allowHelpers) { + completions.push(jsHelperAutocomplete(helperOptions)) + } + if (useSnippets && snippets?.length) { + completions.push(snippetAutoComplete(snippets)) + } } + + return [autocompleteOptions, completions] } + $: [autocompleteOptions, completions] = getOptions( + editorMode, + enrichedBindings + ) + $: { // Ensure a valid side panel option is always selected if (sidePanel && !sidePanelOptions.includes(sidePanel)) { @@ -112,38 +135,6 @@ } } - const getHBSCompletions = (bindingCompletions: BindingCompletionOption[]) => { - return [ - hbAutocomplete([ - ...bindingCompletions, - ...getHelperCompletions(EditorModes.Handlebars), - ]), - ] - } - - const getJSCompletions = ( - bindingCompletions: BindingCompletionOption[], - snippets: Snippet[] | null, - config: { - useHelpers: boolean - useSnippets: boolean - } - ) => { - const completions: BindingCompletion[] = [] - if (bindingCompletions.length) { - completions.push(jsAutocomplete([...bindingCompletions])) - } - if (config.useHelpers) { - completions.push( - jsHelperAutocomplete([...getHelperCompletions(EditorModes.JS)]) - ) - } - if (config.useSnippets && snippets) { - completions.push(snippetAutoComplete(snippets)) - } - return completions - } - const getModeOptions = (allowHBS: boolean, allowJS: boolean) => { let options = [] if (allowHBS) { @@ -223,7 +214,7 @@ bindings: EnrichedBinding[], context: any, snippets: Snippet[] | null - ) => { + ): EnrichedBinding[] => { // Create a single big array to enrich in one go const bindingStrings = bindings.map(binding => { if (binding.runtimeBinding.startsWith('trim "')) { @@ -300,7 +291,7 @@ jsValue = null hbsValue = null updateValue(null) - mode = targetMode + mode = targetMode! targetMode = null } @@ -400,7 +391,7 @@ autofocus={autofocusEditor} placeholder={placeholder || "Add bindings by typing $ or use the menu on the right"} - jsBindingWrapping={bindingCompletions.length > 0} + jsBindingWrapping={completions.length > 0} /> {/key} {/if} From 29a84758d77db158c04737af1b90c982d0961ae6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 13 Feb 2025 14:22:18 +0100 Subject: [PATCH 03/23] Validate --- .../common/CodeEditor/CodeEditor.svelte | 123 +++++++++++++++++- .../src/components/common/CodeEditor/index.ts | 1 + .../common/bindings/BindingPanel.svelte | 1 + packages/builder/src/types/bindings.ts | 4 +- packages/types/src/ui/bindings/helper.ts | 1 + 5 files changed, 126 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 2acde47539..646c8bb92b 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -2,6 +2,7 @@ import { Label } from "@budibase/bbui" import { onMount, createEventDispatcher, onDestroy } from "svelte" import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates" + import Handlebars from "handlebars" import { autocompletion, @@ -40,16 +41,18 @@ indentMore, indentLess, } from "@codemirror/commands" + import { setDiagnostics } from "@codemirror/lint" + import type { Diagnostic } from "@codemirror/lint" import { Compartment, EditorState } from "@codemirror/state" import { javascript } from "@codemirror/lang-javascript" import { EditorModes } from "./" import { themeStore } from "@/stores/portal" import type { EditorMode } from "@budibase/types" - import type { BindingCompletion } from "@/types" + import type { BindingCompletion, BindingCompletionOption } from "@/types" export let label: string | undefined = undefined - // TODO: work out what best type fits this export let completions: BindingCompletion[] = [] + export let options: BindingCompletionOption[] = [] export let mode: EditorMode = EditorModes.Handlebars export let value: string | null = "" export let placeholder: string | null = null @@ -245,6 +248,95 @@ ] } + async function validateHbsTemplate( + editor: EditorView, + template: string, + helpers: Record + ): Promise { + const diagnostics: Diagnostic[] = [] + + try { + const ast = Handlebars.parse(template) + + function traverseNodes(nodes: any[]) { + nodes.forEach(node => { + if ( + node.type === "MustacheStatement" && + node.path.type === "PathExpression" + ) { + const helperName = node.path.original + + const from = + editor.state.doc.line(node.loc.start.line).from + + node.loc.start.column + const to = + editor.state.doc.line(node.loc.end.line).from + + node.loc.end.column + + if (!(helperName in helpers)) { + diagnostics.push({ + from, + to, + severity: "warning", + message: `"${helperName}" handler does not exist.`, + }) + return + } + + const expectedParams = helpers[helperName] + + if (expectedParams) { + const providedParams = node.params + if (providedParams.length !== expectedParams.length) { + diagnostics.push({ + from, + to, + severity: "error", + message: `Helper "${helperName}" expects ${ + expectedParams.length + } parameters (${expectedParams.join(", ")}), but got ${ + providedParams.length + }.`, + }) + } + } + } + + if (node.program) { + traverseNodes(node.program.body) + } + }) + } + + traverseNodes(ast.body) + } catch (e: any) { + diagnostics.push({ + from: 0, + to: template.length, + severity: "error", + message: `Syntax error: ${e.message}`, + }) + } + + return diagnostics + } + + // function getCompletions(): ((_: CompletionContext) => any)[] { + // switch (mode.name) { + // case "handlebars": + // return [hbAutocomplete([...completions])] + + // case "javascript": + // return [jsAutocomplete([...completions])] + + // case "text/html": + // return [] + + // default: + // throw utils.unreachable(mode) + // } + // } + // None of this is reactive, but it never has been, so we just assume most // config flags aren't changed at runtime // TODO: work out type for base @@ -255,6 +347,7 @@ complete.push( autocompletion({ override: [...completions], + // override: [...completions.map(c => c.completionDelegate)], closeOnBlur: true, icons: false, optionClass: completion => @@ -340,6 +433,31 @@ return complete } + function validate( + value: string | null, + editor: EditorView, + mode: EditorMode, + options: BindingCompletionOption[] + ) { + if (!value) { + return + } + + const expectedHelpers: Record = {} + + for (const option of options) { + expectedHelpers[option.label] = option.args || [] + } + + if (mode === EditorModes.Handlebars) { + validateHbsTemplate(editor, value, expectedHelpers).then(diagnostics => { + editor?.dispatch(setDiagnostics(editor.state, diagnostics)) + }) + } + } + + $: validate(value, editor, mode, options) + const initEditor = () => { const baseExtensions = buildBaseExtensions() @@ -366,7 +484,6 @@
{/if} -
diff --git a/packages/builder/src/components/common/CodeEditor/index.ts b/packages/builder/src/components/common/CodeEditor/index.ts index 5529484665..4db4d610b8 100644 --- a/packages/builder/src/components/common/CodeEditor/index.ts +++ b/packages/builder/src/components/common/CodeEditor/index.ts @@ -83,6 +83,7 @@ const helpersToCompletion = ( const helper = helpers[helperName] return { label: helperName, + args: helper.args, info: () => buildHelperInfoNode(helper), type: "helper", section: helperSection, diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 935c6d48bc..2de5a89bdc 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -373,6 +373,7 @@ bind:getCaretPosition bind:insertAtPos {completions} + options={autocompleteOptions} autofocus={autofocusEditor} placeholder={placeholder || "Add bindings by typing {{ or use the menu on the right"} diff --git a/packages/builder/src/types/bindings.ts b/packages/builder/src/types/bindings.ts index 00571f1d8b..7dc9c4f5ca 100644 --- a/packages/builder/src/types/bindings.ts +++ b/packages/builder/src/types/bindings.ts @@ -5,4 +5,6 @@ export type BindingCompletion = (context: CompletionContext) => { options: Completion[] } | null -export type BindingCompletionOption = Completion +export interface BindingCompletionOption extends Completion { + args?: any[] +} diff --git a/packages/types/src/ui/bindings/helper.ts b/packages/types/src/ui/bindings/helper.ts index 5ebd305958..5de040cf3d 100644 --- a/packages/types/src/ui/bindings/helper.ts +++ b/packages/types/src/ui/bindings/helper.ts @@ -1,4 +1,5 @@ export interface Helper { example: string description: string + args: any[] } From fb848c370b679a53f034d0c9b9d9b1c879b9a3d5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 13 Feb 2025 14:23:06 +0100 Subject: [PATCH 04/23] Type --- .../common/CodeEditor/CodeEditor.svelte | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 646c8bb92b..f96b8d10f3 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -43,7 +43,7 @@ } from "@codemirror/commands" import { setDiagnostics } from "@codemirror/lint" import type { Diagnostic } from "@codemirror/lint" - import { Compartment, EditorState } from "@codemirror/state" + import { Compartment, EditorState, Extension } from "@codemirror/state" import { javascript } from "@codemirror/lang-javascript" import { EditorModes } from "./" import { themeStore } from "@/stores/portal" @@ -321,26 +321,10 @@ return diagnostics } - // function getCompletions(): ((_: CompletionContext) => any)[] { - // switch (mode.name) { - // case "handlebars": - // return [hbAutocomplete([...completions])] - - // case "javascript": - // return [jsAutocomplete([...completions])] - - // case "text/html": - // return [] - - // default: - // throw utils.unreachable(mode) - // } - // } - // None of this is reactive, but it never has been, so we just assume most // config flags aren't changed at runtime // TODO: work out type for base - const buildExtensions = (base: any[]) => { + const buildExtensions = (base: Extension[]) => { let complete = [...base] if (autocompleteEnabled) { From dc2f7d53b4e08c5735ab0749c862f8c7b571b77c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 14 Feb 2025 10:38:28 +0100 Subject: [PATCH 05/23] Unify and clean code --- .../common/CodeEditor/CodeEditor.svelte | 3 +- .../src/components/common/CodeEditor/index.ts | 22 +++++++-- .../common/bindings/BindingPanel.svelte | 48 +++++-------------- 3 files changed, 30 insertions(+), 43 deletions(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index f96b8d10f3..6381773d24 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -43,7 +43,8 @@ } from "@codemirror/commands" import { setDiagnostics } from "@codemirror/lint" import type { Diagnostic } from "@codemirror/lint" - import { Compartment, EditorState, Extension } from "@codemirror/state" + import { Compartment, EditorState } from "@codemirror/state" + import type { Extension } from "@codemirror/state" import { javascript } from "@codemirror/lang-javascript" import { EditorModes } from "./" import { themeStore } from "@/stores/portal" diff --git a/packages/builder/src/components/common/CodeEditor/index.ts b/packages/builder/src/components/common/CodeEditor/index.ts index 4db4d610b8..8c81f03213 100644 --- a/packages/builder/src/components/common/CodeEditor/index.ts +++ b/packages/builder/src/components/common/CodeEditor/index.ts @@ -137,9 +137,13 @@ export const hbAutocomplete = ( baseCompletions: BindingCompletionOption[] ): BindingCompletion => { function coreCompletion(context: CompletionContext) { - let bindingStart = context.matchBefore(EditorModes.Handlebars.match) + if (!baseCompletions.length) { + return null + } - let options = baseCompletions || [] + const bindingStart = context.matchBefore(EditorModes.Handlebars.match) + + const options = baseCompletions if (!bindingStart) { return null @@ -150,7 +154,7 @@ export const hbAutocomplete = ( return null } const query = bindingStart.text.replace(match[0], "") - let filtered = bindingFilter(options, query) + const filtered = bindingFilter(options, query) return { from: bindingStart.from + match[0].length, @@ -170,8 +174,12 @@ export const jsAutocomplete = ( baseCompletions: BindingCompletionOption[] ): BindingCompletion => { function coreCompletion(context: CompletionContext) { - let jsBinding = wrappedAutocompleteMatch(context) - let options = baseCompletions || [] + if (!baseCompletions.length) { + return null + } + + const jsBinding = wrappedAutocompleteMatch(context) + const options = baseCompletions if (jsBinding) { // Accommodate spaces @@ -210,6 +218,10 @@ function setAutocomplete( options: BindingCompletionOption[] ): BindingCompletion { return function (context: CompletionContext) { + if (!options.length) { + return null + } + if (wrappedAutocompleteMatch(context)) { return null } diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 2de5a89bdc..3d99204926 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -70,8 +70,6 @@ let expressionLogs: Log[] | undefined let expressionError: string | undefined let evaluating = false - let completions: BindingCompletion[] = [] - let autocompleteOptions: BindingCompletionOption[] = [] $: useSnippets = allowSnippets && !$licensing.isFreePlan $: editorModeOptions = getModeOptions(allowHBS, allowJS) @@ -92,41 +90,18 @@ $: requestEval(runtimeExpression, context, snippets) $: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos) - function getOptions( - mode: typeof editorMode, - bindings: EnrichedBinding[] - ): [BindingCompletionOption[], BindingCompletion[]] { - const autocompleteOptions = [] - const completions = [] + $: bindingOptions = bindingsToCompletions(bindings, editorMode) + $: helperOptions = allowHelpers ? getHelperCompletions(editorMode) : [] + $: snippetsOptions = + usingJS && useSnippets && snippets?.length ? snippets : [] - const bindingOptions = bindingsToCompletions(bindings, editorMode) - const helperOptions = getHelperCompletions(mode) - - if (mode.name === "handlebars") { - autocompleteOptions.push(...bindingOptions) - autocompleteOptions.push(...helperOptions) - - completions.push(hbAutocomplete(autocompleteOptions)) - } else if (mode.name === "javascript") { - if (bindingOptions.length) { - completions.push(jsAutocomplete(bindingOptions)) - } - - if (allowHelpers) { - completions.push(jsHelperAutocomplete(helperOptions)) - } - if (useSnippets && snippets?.length) { - completions.push(snippetAutoComplete(snippets)) - } - } - - return [autocompleteOptions, completions] - } - - $: [autocompleteOptions, completions] = getOptions( - editorMode, - enrichedBindings - ) + $: completions = !usingJS + ? [hbAutocomplete([...bindingOptions, ...helperOptions])] + : [ + jsAutocomplete(bindingOptions), + jsHelperAutocomplete(helperOptions), + snippetAutoComplete(snippetsOptions), + ] $: { // Ensure a valid side panel option is always selected @@ -373,7 +348,6 @@ bind:getCaretPosition bind:insertAtPos {completions} - options={autocompleteOptions} autofocus={autofocusEditor} placeholder={placeholder || "Add bindings by typing {{ or use the menu on the right"} From ec2d78829ebfbfd7d4e2435774a4705240762a36 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 14 Feb 2025 11:09:28 +0100 Subject: [PATCH 06/23] Validate fields --- .../common/CodeEditor/CodeEditor.svelte | 50 ++++++++----------- .../common/bindings/BindingPanel.svelte | 12 ++++- packages/builder/src/types/bindings.ts | 7 +++ 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 6381773d24..1571b9ad22 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -49,11 +49,11 @@ import { EditorModes } from "./" import { themeStore } from "@/stores/portal" import type { EditorMode } from "@budibase/types" - import type { BindingCompletion, BindingCompletionOption } from "@/types" + import type { BindingCompletion, CodeValidator } from "@/types" export let label: string | undefined = undefined export let completions: BindingCompletion[] = [] - export let options: BindingCompletionOption[] = [] + export let validations: CodeValidator | null = null export let mode: EditorMode = EditorModes.Handlebars export let value: string | null = "" export let placeholder: string | null = null @@ -252,7 +252,7 @@ async function validateHbsTemplate( editor: EditorView, template: string, - helpers: Record + validations: CodeValidator ): Promise { const diagnostics: Diagnostic[] = [] @@ -274,7 +274,7 @@ editor.state.doc.line(node.loc.end.line).from + node.loc.end.column - if (!(helperName in helpers)) { + if (!(helperName in validations)) { diagnostics.push({ from, to, @@ -284,22 +284,20 @@ return } - const expectedParams = helpers[helperName] + const config = validations[helperName] - if (expectedParams) { - const providedParams = node.params - if (providedParams.length !== expectedParams.length) { - diagnostics.push({ - from, - to, - severity: "error", - message: `Helper "${helperName}" expects ${ - expectedParams.length - } parameters (${expectedParams.join(", ")}), but got ${ - providedParams.length - }.`, - }) - } + const providedParams = node.params + if (providedParams.length !== config.arguments.length) { + diagnostics.push({ + from, + to, + severity: "error", + message: `Helper "${helperName}" expects ${ + config.arguments.length + } parameters (${config.arguments.join(", ")}), but got ${ + providedParams.length + }.`, + }) } } @@ -422,26 +420,20 @@ value: string | null, editor: EditorView, mode: EditorMode, - options: BindingCompletionOption[] + validations: CodeValidator | null ) { - if (!value) { + if (!value || !validations) { return } - const expectedHelpers: Record = {} - - for (const option of options) { - expectedHelpers[option.label] = option.args || [] - } - if (mode === EditorModes.Handlebars) { - validateHbsTemplate(editor, value, expectedHelpers).then(diagnostics => { + validateHbsTemplate(editor, value, validations).then(diagnostics => { editor?.dispatch(setDiagnostics(editor.state, diagnostics)) }) } } - $: validate(value, editor, mode, options) + $: validate(value, editor, mode, validations) const initEditor = () => { const baseExtensions = buildBaseExtensions() diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 3d99204926..4f124140cd 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -42,7 +42,7 @@ JSONValue, } from "@budibase/types" import type { Log } from "@budibase/string-templates" - import type { BindingCompletion, BindingCompletionOption } from "@/types" + import type { CodeValidator } from "@/types" const dispatch = createEventDispatcher() @@ -103,6 +103,15 @@ snippetAutoComplete(snippetsOptions), ] + $: validations = { + ...bindingOptions.reduce((validations, option) => { + validations[option.label] = { + arguments: [], + } + return validations + }, {}), + } + $: { // Ensure a valid side panel option is always selected if (sidePanel && !sidePanelOptions.includes(sidePanel)) { @@ -348,6 +357,7 @@ bind:getCaretPosition bind:insertAtPos {completions} + {validations} autofocus={autofocusEditor} placeholder={placeholder || "Add bindings by typing {{ or use the menu on the right"} diff --git a/packages/builder/src/types/bindings.ts b/packages/builder/src/types/bindings.ts index 7dc9c4f5ca..3e62149ed4 100644 --- a/packages/builder/src/types/bindings.ts +++ b/packages/builder/src/types/bindings.ts @@ -8,3 +8,10 @@ export type BindingCompletion = (context: CompletionContext) => { export interface BindingCompletionOption extends Completion { args?: any[] } + +export type CodeValidator = Record< + string, + { + arguments: any[] + } +> From 901df11e8c5a82c80f4709268db359dc23828a89 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 14 Feb 2025 11:12:57 +0100 Subject: [PATCH 07/23] Validate helpers --- .../src/components/common/CodeEditor/CodeEditor.svelte | 10 ++++++---- .../src/components/common/bindings/BindingPanel.svelte | 6 ++++++ packages/builder/src/types/bindings.ts | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 1571b9ad22..74126c1c70 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -284,17 +284,19 @@ return } - const config = validations[helperName] + const { arguments: expectedArguments = [] } = + validations[helperName] const providedParams = node.params - if (providedParams.length !== config.arguments.length) { + + if (providedParams.length !== expectedArguments.length) { diagnostics.push({ from, to, severity: "error", message: `Helper "${helperName}" expects ${ - config.arguments.length - } parameters (${config.arguments.join(", ")}), but got ${ + expectedArguments.length + } parameters (${expectedArguments.join(", ")}), but got ${ providedParams.length }.`, }) diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 4f124140cd..a9e7c1ffdf 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -110,6 +110,12 @@ } return validations }, {}), + ...helperOptions.reduce((validations, option) => { + validations[option.label] = { + arguments: option.args, + } + return validations + }, {}), } $: { diff --git a/packages/builder/src/types/bindings.ts b/packages/builder/src/types/bindings.ts index 3e62149ed4..3879a18c7d 100644 --- a/packages/builder/src/types/bindings.ts +++ b/packages/builder/src/types/bindings.ts @@ -12,6 +12,6 @@ export interface BindingCompletionOption extends Completion { export type CodeValidator = Record< string, { - arguments: any[] + arguments?: any[] } > From cd44cefa3f45ed44f81db4d99b7d7f68fc1423a0 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 14 Feb 2025 11:23:54 +0100 Subject: [PATCH 08/23] Fix error selecting formula column type --- .../components/backend/DataTable/modals/CreateEditColumn.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 4af1dcc0ee..a10f493de0 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -386,7 +386,7 @@ editableColumn.relationshipType = RelationshipType.MANY_TO_MANY } else if (editableColumn.type === FieldType.FORMULA) { editableColumn.formulaType = "dynamic" - editableColumn.responseType = field.responseType || FIELDS.STRING.type + editableColumn.responseType = field?.responseType || FIELDS.STRING.type } } From bc015eba1a56f218cde245ebbeb9ce69cc72b774 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 14 Feb 2025 12:07:55 +0100 Subject: [PATCH 09/23] Typing --- .../common/CodeEditor/CodeEditor.svelte | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 74126c1c70..e340fe2c65 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -249,6 +249,18 @@ ] } + function isMustacheStatement( + node: hbs.AST.Statement + ): node is hbs.AST.MustacheStatement { + return node.type === "MustacheStatement" + } + + function isBlockStatement( + node: hbs.AST.Statement + ): node is hbs.AST.BlockStatement { + return node.type === "BlockStatement" + } + async function validateHbsTemplate( editor: EditorView, template: string, @@ -259,13 +271,13 @@ try { const ast = Handlebars.parse(template) - function traverseNodes(nodes: any[]) { + function traverseNodes(nodes: hbs.AST.Statement[]) { nodes.forEach(node => { if ( - node.type === "MustacheStatement" && + isMustacheStatement(node) && node.path.type === "PathExpression" ) { - const helperName = node.path.original + const helperName = (node.path as hbs.AST.PathExpression).original const from = editor.state.doc.line(node.loc.start.line).from + @@ -303,7 +315,7 @@ } } - if (node.program) { + if (isBlockStatement(node)) { traverseNodes(node.program.body) } }) @@ -332,7 +344,6 @@ complete.push( autocompletion({ override: [...completions], - // override: [...completions.map(c => c.completionDelegate)], closeOnBlur: true, icons: false, optionClass: completion => From f2e8888c94c73ed7461463359f36e0d6ed7edf87 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 14 Feb 2025 12:22:49 +0100 Subject: [PATCH 10/23] Validate body --- .../components/common/CodeEditor/CodeEditor.svelte | 12 +++++++++++- .../src/components/common/CodeEditor/index.ts | 1 + .../components/common/bindings/BindingPanel.svelte | 1 + packages/builder/src/types/bindings.ts | 2 ++ packages/types/src/ui/bindings/helper.ts | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index e340fe2c65..79a5605840 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -296,9 +296,19 @@ return } - const { arguments: expectedArguments = [] } = + const { arguments: expectedArguments = [], requiresBlock } = validations[helperName] + if (requiresBlock && !isBlockStatement(node)) { + diagnostics.push({ + from, + to, + severity: "error", + message: `Helper "${helperName}" requires a body.`, + }) + return + } + const providedParams = node.params if (providedParams.length !== expectedArguments.length) { diff --git a/packages/builder/src/components/common/CodeEditor/index.ts b/packages/builder/src/components/common/CodeEditor/index.ts index 8c81f03213..924b4e1b53 100644 --- a/packages/builder/src/components/common/CodeEditor/index.ts +++ b/packages/builder/src/components/common/CodeEditor/index.ts @@ -84,6 +84,7 @@ const helpersToCompletion = ( return { label: helperName, args: helper.args, + requiresBlock: helper.requiresBlock, info: () => buildHelperInfoNode(helper), type: "helper", section: helperSection, diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index a9e7c1ffdf..2c35acdf2d 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -113,6 +113,7 @@ ...helperOptions.reduce((validations, option) => { validations[option.label] = { arguments: option.args, + requiresBlock: option.requiresBlock, } return validations }, {}), diff --git a/packages/builder/src/types/bindings.ts b/packages/builder/src/types/bindings.ts index 3879a18c7d..6ae5ddab88 100644 --- a/packages/builder/src/types/bindings.ts +++ b/packages/builder/src/types/bindings.ts @@ -7,11 +7,13 @@ export type BindingCompletion = (context: CompletionContext) => { export interface BindingCompletionOption extends Completion { args?: any[] + requiresBlock?: boolean } export type CodeValidator = Record< string, { arguments?: any[] + requiresBlock?: boolean } > diff --git a/packages/types/src/ui/bindings/helper.ts b/packages/types/src/ui/bindings/helper.ts index 5de040cf3d..e772180264 100644 --- a/packages/types/src/ui/bindings/helper.ts +++ b/packages/types/src/ui/bindings/helper.ts @@ -2,4 +2,5 @@ export interface Helper { example: string description: string args: any[] + requiresBlock?: boolean } From fa534017694c8f5bbbe76d032f007f29530efde4 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 14 Feb 2025 12:26:46 +0100 Subject: [PATCH 11/23] Improve message --- .../builder/src/components/common/CodeEditor/CodeEditor.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 79a5605840..170c5bf0e1 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -304,7 +304,7 @@ from, to, severity: "error", - message: `Helper "${helperName}" requires a body.`, + message: `Helper "${helperName}" requires a body:\n{{#${helperName} ...}} [body] {{/${helperName}}}`, }) return } From c94909cfcde708760f02bccce10e26fea386f89a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 14 Feb 2025 12:47:01 +0100 Subject: [PATCH 12/23] Prevent body checks --- .../common/CodeEditor/CodeEditor.svelte | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 170c5bf0e1..69360cea2f 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -271,7 +271,13 @@ try { const ast = Handlebars.parse(template) - function traverseNodes(nodes: hbs.AST.Statement[]) { + function traverseNodes( + nodes: hbs.AST.Statement[], + options?: { + ignoreMissing?: boolean + } + ) { + const ignoreMissing = options?.ignoreMissing || false nodes.forEach(node => { if ( isMustacheStatement(node) && @@ -287,12 +293,14 @@ node.loc.end.column if (!(helperName in validations)) { - diagnostics.push({ - from, - to, - severity: "warning", - message: `"${helperName}" handler does not exist.`, - }) + if (!ignoreMissing) { + diagnostics.push({ + from, + to, + severity: "warning", + message: `"${helperName}" handler does not exist.`, + }) + } return } @@ -326,7 +334,7 @@ } if (isBlockStatement(node)) { - traverseNodes(node.program.body) + traverseNodes(node.program.body, { ignoreMissing: true }) } }) } From a2646a13e6f573980f99819e70bf55a768390353 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 14 Feb 2025 12:55:05 +0100 Subject: [PATCH 13/23] Fix eslint --- .../builder/src/components/common/CodeEditor/CodeEditor.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 69360cea2f..d3548e37d2 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -1,4 +1,5 @@