diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js index d9d894c33e..9348706399 100644 --- a/eslint-local-rules/index.js +++ b/eslint-local-rules/index.js @@ -41,11 +41,12 @@ module.exports = { if ( /^@budibase\/[^/]+\/.*$/.test(importPath) && importPath !== "@budibase/backend-core/tests" && - importPath !== "@budibase/string-templates/test/utils" + importPath !== "@budibase/string-templates/test/utils" && + importPath !== "@budibase/client/manifest.json" ) { context.report({ node, - message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`, + message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests, @budibase/string-templates/test/utils and @budibase/client/manifest.json.`, }) } }, diff --git a/lerna.json b/lerna.json index d033c24518..13040cb50c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.46", + "version": "3.3.1", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/packages/bbui/src/bbui.css b/packages/bbui/src/bbui.css index dd0588818e..810c5ff2c0 100644 --- a/packages/bbui/src/bbui.css +++ b/packages/bbui/src/bbui.css @@ -45,6 +45,11 @@ --purple: #806fde; --purple-dark: #130080; + --error-bg: rgba(226, 109, 105, 0.3); + --warning-bg: rgba(255, 210, 106, 0.3); + --error-content: rgba(226, 109, 105, 0.6); + --warning-content: rgba(255, 210, 106, 0.6); + --rounded-small: 4px; --rounded-medium: 8px; --rounded-large: 16px; diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 16183ea59a..e713d9bc85 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -293,7 +293,7 @@ type: RowSelector, props: { row: inputData["oldRow"] || { - tableId: inputData["row"].tableId, + tableId: inputData["row"]?.tableId, }, meta: { fields: inputData["meta"]?.oldFields || {}, diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 98df69bc06..ffb477012c 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -12,7 +12,7 @@ decodeJSBinding, encodeJSBinding, processObjectSync, - processStringSync, + processStringWithLogsSync, } from "@budibase/string-templates" import { readableToRuntimeBinding } from "@/dataBinding" import CodeEditor from "../CodeEditor/CodeEditor.svelte" @@ -41,6 +41,7 @@ InsertAtPositionFn, JSONValue, } from "@budibase/types" + import type { Log } from "@budibase/string-templates" import type { CompletionContext } from "@codemirror/autocomplete" const dispatch = createEventDispatcher() @@ -66,6 +67,7 @@ let insertAtPos: InsertAtPositionFn | undefined let targetMode: BindingMode | null = null let expressionResult: string | undefined + let expressionLogs: Log[] | undefined let expressionError: string | undefined let evaluating = false @@ -157,7 +159,7 @@ (expression: string | null, context: any, snippets: Snippet[]) => { try { expressionError = undefined - expressionResult = processStringSync( + const output = processStringWithLogsSync( expression || "", { ...context, @@ -167,6 +169,8 @@ noThrow: false, } ) + expressionResult = output.result + expressionLogs = output.logs } catch (err: any) { expressionResult = undefined expressionError = err @@ -421,6 +425,7 @@ diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index c8bf5529ad..c47840ea83 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -4,11 +4,13 @@ import { Helpers } from "@budibase/bbui" import { fade } from "svelte/transition" import { UserScriptError } from "@budibase/string-templates" + import type { Log } from "@budibase/string-templates" import type { JSONValue } from "@budibase/types" // this can be essentially any primitive response from the JS function export let expressionResult: JSONValue | undefined = undefined export let expressionError: string | undefined = undefined + export let expressionLogs: Log[] = [] export let evaluating = false export let expression: string | null = null @@ -16,6 +18,11 @@ $: empty = expression == null || expression?.trim() === "" $: success = !error && !empty $: highlightedResult = highlight(expressionResult) + $: highlightedLogs = expressionLogs.map(l => ({ + log: highlight(l.log.join(", ")), + line: l.line, + type: l.type, + })) const formatError = (err: any) => { if (err.code === UserScriptError.code) { @@ -25,14 +32,14 @@ } // json can be any primitive type - const highlight = (json?: any | null) => { + const highlight = (json?: JSONValue | null) => { if (json == null) { return "" } // Attempt to parse and then stringify, in case this is valid result try { - json = JSON.stringify(JSON.parse(json), null, 2) + json = JSON.stringify(JSON.parse(json as any), null, 2) } catch (err) { // couldn't parse/stringify, just treat it as the raw input } @@ -61,7 +68,7 @@
{#if error} - +
Error
{#if evaluating}
@@ -90,8 +97,36 @@ {:else if error} {formatError(expressionError)} {:else} - - {@html highlightedResult} +
+ {#each highlightedLogs as logLine} +
+
+ {#if logLine.type === "error"} + + {:else if logLine.type === "warn"} + + {/if} + + {@html logLine.log} +
+ {#if logLine.line} + :{logLine.line} + {/if} +
+ {/each} +
+ + {@html highlightedResult} +
+
{/if}
@@ -130,20 +165,37 @@ height: 100%; z-index: 1; position: absolute; - opacity: 10%; } .header.error::before { - background: var(--spectrum-global-color-red-400); + background: var(--error-bg); } .body { flex: 1 1 auto; padding: var(--spacing-m) var(--spacing-l); font-family: var(--font-mono); font-size: 12px; - overflow-y: scroll; + overflow-y: auto; overflow-x: hidden; - white-space: pre-wrap; + white-space: pre-line; word-wrap: break-word; height: 0; } + .output-lines { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + } + .line { + border-bottom: var(--border-light); + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: end; + padding: var(--spacing-s); + } + .icon-log { + display: flex; + gap: var(--spacing-s); + align-items: start; + } diff --git a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte index f6d7cfc2c3..4ea8c63087 100644 --- a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte +++ b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte @@ -1,4 +1,5 @@ {#if dividerState} @@ -21,15 +37,16 @@ {#each dataSet as data}
  • onSelect(data)} > - {data.datasourceName ? `${data.datasourceName} - ` : ""}{data.label} + {data.datasourceName && displayDatasourceName + ? `${data.datasourceName} - ` + : ""}{data.label} format.table(table, $datasources.list)) - .sort((a, b) => { - // sort tables alphabetically, grouped by datasource - const dsA = a.datasourceName ?? "" - const dsB = b.datasourceName ?? "" - - const dsComparison = dsA.localeCompare(dsB) - if (dsComparison !== 0) { - return dsComparison - } - return a.label.localeCompare(b.label) - }) + $: tables = sortAndFormat.tables($tablesStore.list, $datasources.list) $: viewsV1 = $viewsStore.list.map(view => ({ ...view, label: view.name, type: "view", })) - $: viewsV2 = $viewsV2Store.list.map(format.viewV2) + $: viewsV2 = sortAndFormat.viewsV2($viewsV2Store.list, $datasources.list) $: views = [...(viewsV1 || []), ...(viewsV2 || [])] $: queries = $queriesStore.list .filter(q => showAllQueries || q.queryVerb === "read" || q.readable) @@ -93,67 +86,9 @@ value: `{{ literal ${safe(provider._id)} }}`, type: "provider", })) - $: links = bindings - // Get only link bindings - .filter(x => x.fieldSchema?.type === "link") - // Filter out bindings provided by forms - .filter(x => !x.component?.endsWith("/form")) - .map(binding => { - const { providerId, readableBinding, fieldSchema } = binding || {} - const { name, tableId } = fieldSchema || {} - const safeProviderId = safe(providerId) - return { - providerId, - label: readableBinding, - fieldName: name, - tableId, - type: "link", - // These properties will be enriched by the client library and provide - // details of the parent row of the relationship field, from context - rowId: `{{ ${safeProviderId}.${safe("_id")} }}`, - rowTableId: `{{ ${safeProviderId}.${safe("tableId")} }}`, - } - }) - $: fields = bindings - .filter( - x => - x.fieldSchema?.type === "attachment" || - (x.fieldSchema?.type === "array" && x.tableId) - ) - .map(binding => { - const { providerId, readableBinding, runtimeBinding } = binding - const { name, type, tableId } = binding.fieldSchema - return { - providerId, - label: readableBinding, - fieldName: name, - fieldType: type, - tableId, - type: "field", - value: `{{ literal ${runtimeBinding} }}`, - } - }) - $: jsonArrays = bindings - .filter( - x => - x.fieldSchema?.type === "jsonarray" || - (x.fieldSchema?.type === "json" && x.fieldSchema?.subtype === "array") - ) - .map(binding => { - const { providerId, readableBinding, runtimeBinding, tableId } = binding - const { name, type, prefixKeys, subtype } = binding.fieldSchema - return { - providerId, - label: readableBinding, - fieldName: name, - fieldType: type, - tableId, - prefixKeys, - type: type === "jsonarray" ? "jsonarray" : "queryarray", - subtype, - value: `{{ literal ${runtimeBinding} }}`, - } - }) + $: links = extractRelationships(bindings) + $: fields = extractFields(bindings) + $: jsonArrays = extractJSONArrayFields(bindings) $: custom = { type: "custom", label: "JSON / CSV", @@ -303,6 +238,7 @@ dataSet={views} {value} onSelect={handleSelected} + identifiers={["tableId", "name"]} /> {/if} {#if queries?.length} @@ -312,6 +248,7 @@ dataSet={queries} {value} onSelect={handleSelected} + identifiers={["_id"]} /> {/if} {#if links?.length} @@ -321,6 +258,7 @@ dataSet={links} {value} onSelect={handleSelected} + identifiers={["tableId", "fieldName"]} /> {/if} {#if fields?.length} @@ -330,6 +268,7 @@ dataSet={fields} {value} onSelect={handleSelected} + identifiers={["providerId", "tableId", "fieldName"]} /> {/if} {#if jsonArrays?.length} @@ -339,6 +278,7 @@ dataSet={jsonArrays} {value} onSelect={handleSelected} + identifiers={["providerId", "tableId", "fieldName"]} /> {/if} {#if showDataProviders && dataProviders?.length} @@ -348,6 +288,7 @@ dataSet={dataProviders} {value} onSelect={handleSelected} + identifiers={["providerId"]} /> {/if} - import { Select } from "@budibase/bbui" + import { Popover, Select } from "@budibase/bbui" import { createEventDispatcher, onMount } from "svelte" - import { tables as tablesStore, viewsV2 } from "@/stores/builder" - import { tableSelect as format } from "@/helpers/data/format" + import { + tables as tableStore, + datasources as datasourceStore, + viewsV2 as viewsV2Store, + } from "@/stores/builder" + import DataSourceCategory from "./DataSourceSelect/DataSourceCategory.svelte" + import { sortAndFormat } from "@/helpers/data/format" export let value + let anchorRight, dropdownRight + const dispatch = createEventDispatcher() - $: tables = $tablesStore.list.map(format.table) - $: views = $viewsV2.list.map(format.viewV2) + $: tables = sortAndFormat.tables($tableStore.list, $datasourceStore.list) + $: views = sortAndFormat.viewsV2($viewsV2Store.list, $datasourceStore.list) $: options = [...(tables || []), ...(views || [])] + $: text = value?.label ?? "Choose an option" + const onChange = e => { dispatch( "change", - options.find(x => x.resourceId === e.detail) + options.find(x => x.resourceId === e.resourceId) ) + dropdownRight.hide() } onMount(() => { @@ -29,10 +39,47 @@ }) -
    +