diff --git a/.eslintrc.json b/.eslintrc.json index 79f7e56712..917443014b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,6 +45,16 @@ "no-prototype-builtins": "off", "local-rules/no-budibase-imports": "error" } + }, + { + "files": [ + "packages/builder/**/*", + "packages/client/**/*", + "packages/frontend-core/**/*" + ], + "rules": { + "no-console": ["error", { "allow": ["warn", "error", "debug"] } ] + } } ], "rules": { diff --git a/lerna.json b/lerna.json index f91c51d4bb..9adb9c5ddf 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.16.0", + "version": "2.17.8", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/account-portal b/packages/account-portal index 485ec16a9e..cc12291732 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 485ec16a9eed48c548a5f1239772139f3319f028 +Subproject commit cc12291732ee902dc832bc7d93cf2086ffdf0cff diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index d6325e1de9..85644488f5 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -21,7 +21,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/nano": "10.1.4", + "@budibase/nano": "10.1.5", "@budibase/pouchdb-replication-stream": "1.2.10", "@budibase/shared-core": "0.0.0", "@budibase/types": "0.0.0", diff --git a/packages/backend-core/src/objectStore/cloudfront.ts b/packages/backend-core/src/objectStore/cloudfront.ts index 866fe9e880..3bca97d11e 100644 --- a/packages/backend-core/src/objectStore/cloudfront.ts +++ b/packages/backend-core/src/objectStore/cloudfront.ts @@ -23,7 +23,7 @@ const getCloudfrontSignParams = () => { return { keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID!, privateKeyString: getPrivateKey(), - expireTime: new Date().getTime() + 1000 * 60 * 60, // 1 hour + expireTime: new Date().getTime() + 1000 * 60 * 60 * 24, // 1 day } } diff --git a/packages/backend-core/src/objectStore/objectStore.ts b/packages/backend-core/src/objectStore/objectStore.ts index 57ead0e809..8d18fb97fd 100644 --- a/packages/backend-core/src/objectStore/objectStore.ts +++ b/packages/backend-core/src/objectStore/objectStore.ts @@ -7,7 +7,7 @@ import tar from "tar-fs" import zlib from "zlib" import { promisify } from "util" import { join } from "path" -import fs from "fs" +import fs, { ReadStream } from "fs" import env from "../environment" import { budibaseTempDir } from "./utils" import { v4 } from "uuid" @@ -184,7 +184,7 @@ export async function upload({ export async function streamUpload( bucketName: string, filename: string, - stream: any, + stream: ReadStream | ReadableStream, extra = {} ) { const objectStore = ObjectStore(bucketName) @@ -255,7 +255,8 @@ export async function listAllObjects(bucketName: string, path: string) { objects = objects.concat(response.Contents) } isTruncated = !!response.IsTruncated - } while (isTruncated) + token = response.NextContinuationToken + } while (isTruncated && token) return objects } diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index b95dace5b2..0bcb25a35f 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -2,7 +2,7 @@ import env from "../environment" import { getRedisOptions } from "../redis/utils" import { JobQueue } from "./constants" import InMemoryQueue from "./inMemoryQueue" -import BullQueue, { QueueOptions } from "bull" +import BullQueue, { QueueOptions, JobOptions } from "bull" import { addListeners, StalledFn } from "./listeners" import { Duration } from "../utils" import * as timers from "../timers" @@ -24,17 +24,24 @@ async function cleanup() { export function createQueue( jobQueue: JobQueue, - opts: { removeStalledCb?: StalledFn } = {} + opts: { + removeStalledCb?: StalledFn + maxStalledCount?: number + jobOptions?: JobOptions + } = {} ): BullQueue.Queue { const redisOpts = getRedisOptions() const queueConfig: QueueOptions = { redis: redisOpts, settings: { - maxStalledCount: 0, + maxStalledCount: opts.maxStalledCount ? opts.maxStalledCount : 0, lockDuration: QUEUE_LOCK_MS, lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS, }, } + if (opts.jobOptions) { + queueConfig.defaultJobOptions = opts.jobOptions + } let queue: any if (!env.isTest()) { queue = new BullQueue(jobQueue, queueConfig) diff --git a/packages/builder/src/builderStore/componentUtils.js b/packages/builder/src/builderStore/componentUtils.js index 733ce0948e..88656b896f 100644 --- a/packages/builder/src/builderStore/componentUtils.js +++ b/packages/builder/src/builderStore/componentUtils.js @@ -7,6 +7,9 @@ import { findHBSBlocks, } from "@budibase/string-templates" import { capitalise } from "helpers" +import { Constants } from "@budibase/frontend-core" + +const { ContextScopes } = Constants /** * Recursively searches for a specific component ID @@ -263,11 +266,59 @@ export const getComponentName = component => { if (component == null) { return "" } - const components = get(store)?.components || {} const componentDefinition = components[component._component] || {} - const name = - componentDefinition.friendlyName || componentDefinition.name || "" - - return name + return componentDefinition.friendlyName || componentDefinition.name || "" +} + +/** + * Recurses through the component tree and builds a tree of contexts provided + * by components. + */ +export const buildContextTree = ( + rootComponent, + tree = { root: [] }, + currentBranch = "root" +) => { + // Sanity check + if (!rootComponent) { + return tree + } + + // Process this component's contexts + const def = store.actions.components.getDefinition(rootComponent._component) + if (def?.context) { + tree[currentBranch].push(rootComponent._id) + const contexts = Array.isArray(def.context) ? def.context : [def.context] + + // If we provide local context, start a new branch for our children + if (contexts.some(context => context.scope === ContextScopes.Local)) { + currentBranch = rootComponent._id + tree[rootComponent._id] = [] + } + } + + // Process children + if (rootComponent._children) { + rootComponent._children.forEach(child => { + buildContextTree(child, tree, currentBranch) + }) + } + + return tree +} + +/** + * Generates a lookup map of which context branch all components in a component + * tree are inside. + */ +export const buildContextTreeLookupMap = rootComponent => { + const tree = buildContextTree(rootComponent) + let map = {} + Object.entries(tree).forEach(([branch, ids]) => { + ids.forEach(id => { + map[id] = branch + }) + }) + return map } diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index cc3851c318..edea3b9ec7 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -1,6 +1,7 @@ import { cloneDeep } from "lodash/fp" import { get } from "svelte/store" import { + buildContextTreeLookupMap, findAllComponents, findAllMatchingComponents, findComponent, @@ -20,11 +21,13 @@ import { encodeJSBinding, } from "@budibase/string-templates" import { TableNames } from "../constants" -import { JSONUtils } from "@budibase/frontend-core" +import { JSONUtils, Constants } from "@budibase/frontend-core" import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json" import { environment, licensing } from "stores/portal" import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils" +const { ContextScopes } = Constants + // Regex to match all instances of template strings const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g @@ -214,20 +217,27 @@ export const getComponentContexts = ( return [] } let map = {} + const componentPath = findComponentPath(asset.props, componentId) + const componentPathIds = componentPath.map(component => component._id) + const contextTreeLookupMap = buildContextTreeLookupMap(asset.props) // Processes all contexts exposed by a component const processContexts = scope => component => { - const def = store.actions.components.getDefinition(component._component) + // Sanity check + const def = store.actions.components.getDefinition(component?._component) if (!def?.context) { return } - if (!map[component._id]) { - map[component._id] = { - component, - definition: def, - contexts: [], - } + + // Filter out global contexts not in the same branch. + // Global contexts are only valid if their branch root is an ancestor of + // this component. + const branch = contextTreeLookupMap[component._id] + if (branch !== "root" && !componentPathIds.includes(branch)) { + return } + + // Process all contexts provided by this component const contexts = Array.isArray(def.context) ? def.context : [def.context] contexts.forEach(context => { // Ensure type matches @@ -235,7 +245,7 @@ export const getComponentContexts = ( return } // Ensure scope matches - let contextScope = context.scope || "global" + let contextScope = context.scope || ContextScopes.Global if (contextScope !== scope) { return } @@ -243,17 +253,23 @@ export const getComponentContexts = ( if (!isContextCompatibleWithComponent(context, component)) { return } + if (!map[component._id]) { + map[component._id] = { + component, + definition: def, + contexts: [], + } + } map[component._id].contexts.push(context) }) } // Process all global contexts const allComponents = findAllComponents(asset.props) - allComponents.forEach(processContexts("global")) + allComponents.forEach(processContexts(ContextScopes.Global)) - // Process all local contexts - const localComponents = findComponentPath(asset.props, componentId) - localComponents.forEach(processContexts("local")) + // Process all local contexts in the immediate tree + componentPath.forEach(processContexts(ContextScopes.Local)) // Exclude self if required if (!options?.includeSelf) { @@ -364,7 +380,6 @@ const getContextBindings = (asset, componentId) => { * Generates a set of bindings for a given component context */ const generateComponentContextBindings = (asset, componentContext) => { - console.log("Hello ") const { component, definition, contexts } = componentContext if (!component || !definition || !contexts?.length) { return [] diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 4d43869f8e..55208bb97e 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -158,6 +158,7 @@ export const getFrontendStore = () => { ...INITIAL_FRONTEND_STATE.features, ...application.features, }, + automations: application.automations || {}, icon: application.icon || {}, initialised: true, })) diff --git a/packages/builder/src/builderStore/websocket.js b/packages/builder/src/builderStore/websocket.js index ca3f49e9a3..4482e3ae1e 100644 --- a/packages/builder/src/builderStore/websocket.js +++ b/packages/builder/src/builderStore/websocket.js @@ -21,7 +21,7 @@ export const createBuilderWebsocket = appId => { }) }) socket.on("connect_error", err => { - console.log("Failed to connect to builder websocket:", err.message) + console.error("Failed to connect to builder websocket:", err.message) }) socket.on("disconnect", () => { userStore.actions.reset() diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index af54e4d2da..1536d83a82 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -15,7 +15,6 @@ Icon, Checkbox, DatePicker, - Detail, } from "@budibase/bbui" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import { automationStore, selectedAutomation } from "builderStore" @@ -33,6 +32,8 @@ import Editor from "components/integration/QueryEditor.svelte" import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte" + import BindingPicker from "components/common/bindings/BindingPicker.svelte" + import { BindingHelpers } from "components/common/bindings/utils" import { bindingsToCompletions, hbAutocomplete, @@ -56,7 +57,7 @@ let drawer let fillWidth = true let inputData - let codeBindingOpen = false + let insertAtPos, getCaretPosition $: filters = lookForFilters(schemaProperties) || [] $: tempFilters = filters $: stepId = block.stepId @@ -75,6 +76,10 @@ $: isUpdateRow = stepId === ActionStepID.UPDATE_ROW $: codeMode = stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS + $: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, { + disableWrapping: true, + }) + $: editingJs = codeMode === EditorModes.JS $: stepCompletions = codeMode === EditorModes.Handlebars @@ -157,6 +162,7 @@ let bindings = [] let loopBlockCount = 0 const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => { + if (!name) return const runtimeBinding = determineRuntimeBinding(name, idx, isLoopBlock) const categoryName = determineCategoryName(idx, isLoopBlock, bindingName) @@ -291,7 +297,6 @@ loopBlockCount++ continue } - Object.entries(schema).forEach(([name, value]) => addBinding(name, value, icon, idx, isLoopBlock, bindingName) ) @@ -539,39 +544,51 @@ /> {:else if value.customType === "code"} - {#if codeMode == EditorModes.JS} - (codeBindingOpen = !codeBindingOpen)} - quiet - icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"} - > - Bindings - - {#if codeBindingOpen} -
{JSON.stringify(bindings, null, 2)}
- {/if} - {/if} - { - // need to pass without the value inside - onChange({ detail: e.detail }, key) - inputData[key] = e.detail - }} - completions={stepCompletions} - mode={codeMode} - autocompleteEnabled={codeMode != EditorModes.JS} - height={500} - /> -
- {#if codeMode == EditorModes.Handlebars} - -
-
- Add available bindings by typing - }} - -
+
+
+ { + // need to pass without the value inside + onChange({ detail: e.detail }, key) + inputData[key] = e.detail + }} + completions={stepCompletions} + mode={codeMode} + autocompleteEnabled={codeMode !== EditorModes.JS} + bind:getCaretPosition + bind:insertAtPos + height={500} + /> +
+ {#if codeMode === EditorModes.Handlebars} + +
+
+ Add available bindings by typing + }} + +
+
+ {/if} +
+
+ {#if editingJs} +
+ + bindingsHelpers.onSelectBinding( + inputData[key], + binding, + { + js: true, + dontDecode: true, + } + )} + mode="javascript" + />
{/if}
@@ -658,4 +675,20 @@ .test :global(.drawer) { width: 10000px !important; } + + .js-editor { + display: flex; + flex-direction: row; + flex-grow: 1; + width: 100%; + } + + .js-code { + flex: 7; + } + + .js-binding-picker { + flex: 3; + margin-top: calc((var(--spacing-xl) * -1) + 1px); + } diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index a39634f9a3..e906ef445d 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -54,6 +54,7 @@ export let placeholder = null export let autocompleteEnabled = true export let autofocus = false + export let jsBindingWrapping = true // Export a function to expose caret position export const getCaretPosition = () => { @@ -187,7 +188,7 @@ ) complete.push( EditorView.inputHandler.of((view, from, to, insert) => { - if (insert === "$") { + if (jsBindingWrapping && insert === "$") { let { text } = view.state.doc.lineAt(from) const left = from ? text.substring(0, from) : "" diff --git a/packages/builder/src/components/common/CodeEditor/index.js b/packages/builder/src/components/common/CodeEditor/index.js index 7987deff52..0d71a475f0 100644 --- a/packages/builder/src/components/common/CodeEditor/index.js +++ b/packages/builder/src/components/common/CodeEditor/index.js @@ -286,13 +286,20 @@ export const hbInsert = (value, from, to, text) => { return parsedInsert } -export function jsInsert(value, from, to, text, { helper } = {}) { +export function jsInsert( + value, + from, + to, + text, + { helper, disableWrapping } = {} +) { let parsedInsert = "" const left = from ? value.substring(0, from) : "" const right = to ? value.substring(to) : "" - - if (helper) { + if (disableWrapping) { + parsedInsert = text + } else if (helper) { parsedInsert = `helpers.${text}()` } else if (!left.includes('$("') || !right.includes('")')) { parsedInsert = `$("${text}")` @@ -312,7 +319,7 @@ export const insertBinding = (view, from, to, text, mode) => { } else if (mode.name == "handlebars") { parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text) } else { - console.log("Unsupported") + console.warn("Unsupported") return } diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 548a71b483..0ef34a36ad 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -29,10 +29,9 @@ hbAutocomplete, EditorModes, bindingsToCompletions, - hbInsert, - jsInsert, } from "../CodeEditor" import BindingPicker from "./BindingPicker.svelte" + import { BindingHelpers } from "./utils" const dispatch = createEventDispatcher() @@ -60,8 +59,10 @@ let targetMode = null $: usingJS = mode === "JavaScript" - $: editorMode = mode == "JavaScript" ? EditorModes.JS : EditorModes.Handlebars + $: editorMode = + mode === "JavaScript" ? EditorModes.JS : EditorModes.Handlebars $: bindingCompletions = bindingsToCompletions(bindings, editorMode) + $: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos) const updateValue = val => { valid = isValid(readableToRuntimeBinding(bindings, val)) @@ -70,31 +71,13 @@ } } - // Adds a JS/HBS helper to the expression const onSelectHelper = (helper, js) => { - const pos = getCaretPosition() - const { start, end } = pos - if (js) { - let js = decodeJSBinding(jsValue) - const insertVal = jsInsert(js, start, end, helper.text, { helper: true }) - insertAtPos({ start, end, value: insertVal }) - } else { - const insertVal = hbInsert(hbsValue, start, end, helper.text) - insertAtPos({ start, end, value: insertVal }) - } + bindingHelpers.onSelectHelper(js ? jsValue : hbsValue, helper, { js }) } - // Adds a data binding to the expression const onSelectBinding = (binding, { forceJS } = {}) => { - const { start, end } = getCaretPosition() - if (usingJS || forceJS) { - let js = decodeJSBinding(jsValue) - const insertVal = jsInsert(js, start, end, binding.readableBinding) - insertAtPos({ start, end, value: insertVal }) - } else { - const insertVal = hbInsert(hbsValue, start, end, binding.readableBinding) - insertAtPos({ start, end, value: insertVal }) - } + const js = usingJS || forceJS + bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, { js }) } const onChangeMode = e => { diff --git a/packages/builder/src/components/common/bindings/BindingPicker.svelte b/packages/builder/src/components/common/bindings/BindingPicker.svelte index 93d9d62021..9daed00324 100644 --- a/packages/builder/src/components/common/bindings/BindingPicker.svelte +++ b/packages/builder/src/components/common/bindings/BindingPicker.svelte @@ -9,6 +9,7 @@ export let bindings export let mode export let allowHelpers + export let noPaddingTop = false let search = "" let popover @@ -47,9 +48,10 @@ }) $: filteredHelpers = helpers?.filter(helper => { return ( - !search || - helper.label.match(searchRgx) || - helper.description.match(searchRgx) + (!search || + helper.label.match(searchRgx) || + helper.description.match(searchRgx)) && + (mode.name !== "javascript" || helper.allowsJs) ) }) diff --git a/packages/builder/src/components/common/bindings/utils.js b/packages/builder/src/components/common/bindings/utils.js index 8d414ffed3..a086cd0394 100644 --- a/packages/builder/src/components/common/bindings/utils.js +++ b/packages/builder/src/components/common/bindings/utils.js @@ -1,38 +1,41 @@ -export function addHBSBinding(value, caretPos, binding) { - binding = typeof binding === "string" ? binding : binding.path - value = value == null ? "" : value +import { decodeJSBinding } from "@budibase/string-templates" +import { hbInsert, jsInsert } from "components/common/CodeEditor" - const left = caretPos?.start ? value.substring(0, caretPos.start) : "" - const right = caretPos?.end ? value.substring(caretPos.end) : "" - if (!left.includes("{{") || !right.includes("}}")) { - binding = `{{ ${binding} }}` +export class BindingHelpers { + constructor(getCaretPosition, insertAtPos, { disableWrapping } = {}) { + this.getCaretPosition = getCaretPosition + this.insertAtPos = insertAtPos + this.disableWrapping = disableWrapping } - if (caretPos.start) { - value = - value.substring(0, caretPos.start) + - binding + - value.substring(caretPos.end, value.length) - } else { - value += binding - } - return value -} -export function addJSBinding(value, caretPos, binding, { helper } = {}) { - binding = typeof binding === "string" ? binding : binding.path - value = value == null ? "" : value - if (!helper) { - binding = `$("${binding}")` - } else { - binding = `helpers.${binding}()` + // Adds a JS/HBS helper to the expression + onSelectHelper(value, helper, { js, dontDecode }) { + const pos = this.getCaretPosition() + const { start, end } = pos + if (js) { + const jsVal = dontDecode ? value : decodeJSBinding(value) + const insertVal = jsInsert(jsVal, start, end, helper.text, { + helper: true, + }) + this.insertAtPos({ start, end, value: insertVal }) + } else { + const insertVal = hbInsert(value, start, end, helper.text) + this.insertAtPos({ start, end, value: insertVal }) + } } - if (caretPos.start) { - value = - value.substring(0, caretPos.start) + - binding + - value.substring(caretPos.end, value.length) - } else { - value += binding + + // Adds a data binding to the expression + onSelectBinding(value, binding, { js, dontDecode }) { + const { start, end } = this.getCaretPosition() + if (js) { + const jsVal = dontDecode ? value : decodeJSBinding(value) + const insertVal = jsInsert(jsVal, start, end, binding.readableBinding, { + disableWrapping: this.disableWrapping, + }) + this.insertAtPos({ start, end, value: insertVal }) + } else { + const insertVal = hbInsert(value, start, end, binding.readableBinding) + this.insertAtPos({ start, end, value: insertVal }) + } } - return value } diff --git a/packages/builder/src/components/integration/QueryViewer.svelte b/packages/builder/src/components/integration/QueryViewer.svelte index 59a3289731..42ebcb7693 100644 --- a/packages/builder/src/components/integration/QueryViewer.svelte +++ b/packages/builder/src/components/integration/QueryViewer.svelte @@ -93,7 +93,13 @@ notifications.success("Query executed successfully") } catch (error) { - notifications.error(`Query Error: ${error.message}`) + if (typeof error.message === "string") { + notifications.error(`Query Error: ${error.message}`) + } else if (typeof error.message?.code === "string") { + notifications.error(`Query Error: ${error.message.code}`) + } else { + notifications.error(`Query Error: ${JSON.stringify(error.message)}`) + } if (!suppressErrors) { throw error diff --git a/packages/builder/src/components/portal/onboarding/TourPopover.svelte b/packages/builder/src/components/portal/onboarding/TourPopover.svelte index d959a6ef95..cc56715284 100644 --- a/packages/builder/src/components/portal/onboarding/TourPopover.svelte +++ b/packages/builder/src/components/portal/onboarding/TourPopover.svelte @@ -67,7 +67,7 @@ })) navigateStep(target) } else { - console.log("Could not retrieve step") + console.warn("Could not retrieve step") } } else { if (typeof tourStep.onComplete === "function") { diff --git a/packages/builder/src/components/portal/onboarding/tourHandler.js b/packages/builder/src/components/portal/onboarding/tourHandler.js index d4a564f23a..5b44bdc96f 100644 --- a/packages/builder/src/components/portal/onboarding/tourHandler.js +++ b/packages/builder/src/components/portal/onboarding/tourHandler.js @@ -3,11 +3,11 @@ import { get } from "svelte/store" const registerNode = async (node, tourStepKey) => { if (!node) { - console.log("Tour Handler - an anchor node is required") + console.warn("Tour Handler - an anchor node is required") } if (!get(store).tourKey) { - console.log("Tour Handler - No active tour ", tourStepKey, node) + console.error("Tour Handler - No active tour ", tourStepKey, node) return } diff --git a/packages/builder/src/components/portal/onboarding/tours.js b/packages/builder/src/components/portal/onboarding/tours.js index e82026da94..55fd4c4a9b 100644 --- a/packages/builder/src/components/portal/onboarding/tours.js +++ b/packages/builder/src/components/portal/onboarding/tours.js @@ -45,7 +45,7 @@ const endUserOnboarding = async ({ skipped = false } = {}) => { onboarding: false, })) } catch (e) { - console.log("Onboarding failed", e) + console.error("Onboarding failed", e) return false } return true diff --git a/packages/builder/src/constants/completions.js b/packages/builder/src/constants/completions.js index 32de934324..e539a8084a 100644 --- a/packages/builder/src/constants/completions.js +++ b/packages/builder/src/constants/completions.js @@ -1,4 +1,4 @@ -import { getManifest } from "@budibase/string-templates" +import { getManifest, helpersToRemoveForJs } from "@budibase/string-templates" export function handlebarsCompletions() { const manifest = getManifest() @@ -11,6 +11,9 @@ export function handlebarsCompletions() { label: helperName, displayText: helperName, description: helperConfig.description, + allowsJs: + !helperConfig.requiresBlock && + !helpersToRemoveForJs.includes(helperName), })) ) } diff --git a/packages/builder/src/helpers/urlStateSync.js b/packages/builder/src/helpers/urlStateSync.js index 2408dde2f1..9337393f06 100644 --- a/packages/builder/src/helpers/urlStateSync.js +++ b/packages/builder/src/helpers/urlStateSync.js @@ -52,7 +52,7 @@ export const syncURLToState = options => { let cachedPage = get(routify.page) let previousParamsHash = null let debug = false - const log = (...params) => debug && console.log(`[${urlParam}]`, ...params) + const log = (...params) => debug && console.debug(`[${urlParam}]`, ...params) // Navigate to a certain URL const gotoUrl = (url, params) => { diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte index 33116094eb..cd00b36186 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -107,7 +107,7 @@ return } if (!prodAppId) { - console.log("Application id required") + console.error("Application id required") return } await usersFetch.update({ diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[datasourceId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[datasourceId]/index.svelte index 090cffeb7e..b69201865f 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[datasourceId]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[datasourceId]/index.svelte @@ -12,12 +12,16 @@ import PromptQueryModal from "./_components/PromptQueryModal.svelte" import SettingsPanel from "./_components/panels/Settings.svelte" import { helpers } from "@budibase/shared-core" + import { admin } from "stores/portal" + import { IntegrationTypes } from "constants/backend" let selectedPanel = null let panelOptions = [] $: datasource = $datasources.selected + $: isCloud = $admin.cloud + $: isPostgres = datasource?.source === IntegrationTypes.POSTGRES $: getOptions(datasource) const getOptions = datasource => { @@ -41,7 +45,13 @@ } // always the last option for SQL if (helpers.isSQL(datasource)) { - panelOptions.push("Settings") + if (isCloud && isPostgres) { + // We don't show the settings panel for Postgres on Budicloud because + // it requires pg_dump to work and we don't want to enable shell injection + // attacks. + } else { + panelOptions.push("Settings") + } } } diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte index 383026c4f8..f9f994ce67 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte @@ -15,10 +15,15 @@ Checkbox, notifications, Select, + Combobox, } from "@budibase/bbui" import { selectedScreen, store } from "builderStore" import { DefaultAppTheme } from "constants" + $: screenRouteOptions = $store.screens + .map(screen => screen.routing?.route) + .filter(x => x != null) + const updateShowNavigation = async e => { await store.actions.screens.updateSetting( get(selectedScreen), @@ -107,23 +112,6 @@ on:change={e => update("navWidth", e.detail)} /> {/if} -
- -
- update("hideLogo", !e.detail)} - /> - {#if !$store.navigation.hideLogo} -
- -
- update("logoUrl", e.detail)} - updateOnChange={false} - /> - {/if}
@@ -160,6 +148,47 @@ />
+ +
+
+
+ Logo +
+
+
+ +
+ update("hideLogo", !e.detail)} + /> + {#if !$store.navigation.hideLogo} +
+ +
+ update("logoUrl", e.detail)} + updateOnChange={false} + /> +
+ +
+ update("logoLinkUrl", e.detail)} + options={screenRouteOptions} + /> +
+ +
+ update("openLogoLinkInNewTab", !!e.detail)} + /> + {/if} +
+
{/if} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte index fb9ee2c8a5..b2bcd3daa4 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte @@ -66,7 +66,7 @@ try { await store.actions.screens.updateSetting(get(selectedScreen), key, value) } catch (error) { - console.log(error) + console.error(error) notifications.error("Error saving screen settings") } } diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte index ab8ccecf6e..2a2459949d 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte @@ -78,7 +78,7 @@ //$goto(`./${screenId}`) //store.actions.screens.select(screenId) } catch (error) { - console.log(error) + console.error(error) notifications.error("Error creating screens") } } diff --git a/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte b/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte index fde392e31d..9c45eb7eb1 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte @@ -36,15 +36,12 @@ let status = null let timeRange = null let loaded = false - - $: app = $apps.find(app => app.devId === $store.appId?.includes(app.appId)) + $: app = $apps.find(app => $store.appId?.includes(app.appId)) $: licensePlan = $auth.user?.license?.plan $: page = $pageInfo.page $: fetchLogs(automationId, status, page, timeRange) $: isCloud = $admin.cloud - $: chainAutomations = app?.automations?.chainAutomations ?? !isCloud - const timeOptions = [ { value: "90-d", label: "Past 90 days" }, { value: "30-d", label: "Past 30 days" }, diff --git a/packages/builder/src/pages/builder/app/[application]/settings/backups/_components/ActionsRenderer.svelte b/packages/builder/src/pages/builder/app/[application]/settings/backups/_components/ActionsRenderer.svelte index 93f0f518ad..ffdf14fdce 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/backups/_components/ActionsRenderer.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/backups/_components/ActionsRenderer.svelte @@ -13,6 +13,7 @@ import CreateRestoreModal from "./CreateRestoreModal.svelte" import { createEventDispatcher } from "svelte" import { isOnlyUser } from "builderStore" + import { BackupType } from "constants/backend/backups" export let row @@ -42,12 +43,11 @@
- -
- -
- - {#if row.type !== "restore"} + {#if row.type !== BackupType.RESTORE} + +
+ +
Delete Download - {/if} -
+
+ {/if}
diff --git a/packages/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index 0ba7e6448b..7bb2a3ad49 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -31,7 +31,7 @@ async function login() { form.validate() if (Object.keys(errors).length > 0) { - console.log("errors", errors) + console.error("errors", errors) return } try { diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 5bbf465766..64d9366423 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -4720,7 +4720,8 @@ } ], "context": { - "type": "schema" + "type": "schema", + "scope": "local" } }, "daterangepicker": { @@ -6742,6 +6743,17 @@ "key": "disabled", "defaultValue": false }, + { + "type": "boolean", + "label": "Read only", + "key": "readonly", + "defaultValue": false, + "dependsOn": { + "setting": "disabled", + "value": true, + "invert": true + } + }, { "type": "select", "label": "Layout", diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index bdab0dd9ab..2ca57a3dde 100644 --- a/packages/client/src/components/app/Layout.svelte +++ b/packages/client/src/components/app/Layout.svelte @@ -33,6 +33,8 @@ export let navTextColor export let navWidth export let pageWidth + export let logoLinkUrl + export let openLogoLinkInNewTab export let embedded = false @@ -150,6 +152,16 @@ } return style } + + const getSanitizedUrl = (url, openInNewTab) => { + if (!isInternal(url)) { + return ensureExternal(url) + } + if (openInNewTab) { + return `#${url}` + } + return url + }