1
0
Fork 0
mirror of synced 2024-10-02 10:08:09 +13:00

Merge branch 'master' of github.com:Budibase/budibase into feature/sql-query-aliasing

This commit is contained in:
mike12345567 2024-02-05 10:04:07 +00:00
commit f73f78c67c
53 changed files with 628 additions and 370 deletions

View file

@ -45,6 +45,16 @@
"no-prototype-builtins": "off", "no-prototype-builtins": "off",
"local-rules/no-budibase-imports": "error" "local-rules/no-budibase-imports": "error"
} }
},
{
"files": [
"packages/builder/**/*",
"packages/client/**/*",
"packages/frontend-core/**/*"
],
"rules": {
"no-console": ["error", { "allow": ["warn", "error", "debug"] } ]
}
} }
], ],
"rules": { "rules": {

View file

@ -1,5 +1,5 @@
{ {
"version": "2.16.0", "version": "2.17.3",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

@ -1 +1 @@
Subproject commit 485ec16a9eed48c548a5f1239772139f3319f028 Subproject commit cc12291732ee902dc832bc7d93cf2086ffdf0cff

View file

@ -21,7 +21,7 @@
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@budibase/nano": "10.1.4", "@budibase/nano": "10.1.5",
"@budibase/pouchdb-replication-stream": "1.2.10", "@budibase/pouchdb-replication-stream": "1.2.10",
"@budibase/shared-core": "0.0.0", "@budibase/shared-core": "0.0.0",
"@budibase/types": "0.0.0", "@budibase/types": "0.0.0",

View file

@ -23,7 +23,7 @@ const getCloudfrontSignParams = () => {
return { return {
keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID!, keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID!,
privateKeyString: getPrivateKey(), privateKeyString: getPrivateKey(),
expireTime: new Date().getTime() + 1000 * 60 * 60, // 1 hour expireTime: new Date().getTime() + 1000 * 60 * 60 * 24, // 1 day
} }
} }

View file

@ -7,7 +7,7 @@ import tar from "tar-fs"
import zlib from "zlib" import zlib from "zlib"
import { promisify } from "util" import { promisify } from "util"
import { join } from "path" import { join } from "path"
import fs from "fs" import fs, { ReadStream } from "fs"
import env from "../environment" import env from "../environment"
import { budibaseTempDir } from "./utils" import { budibaseTempDir } from "./utils"
import { v4 } from "uuid" import { v4 } from "uuid"
@ -184,7 +184,7 @@ export async function upload({
export async function streamUpload( export async function streamUpload(
bucketName: string, bucketName: string,
filename: string, filename: string,
stream: any, stream: ReadStream | ReadableStream,
extra = {} extra = {}
) { ) {
const objectStore = ObjectStore(bucketName) const objectStore = ObjectStore(bucketName)

View file

@ -364,7 +364,6 @@ const getContextBindings = (asset, componentId) => {
* Generates a set of bindings for a given component context * Generates a set of bindings for a given component context
*/ */
const generateComponentContextBindings = (asset, componentContext) => { const generateComponentContextBindings = (asset, componentContext) => {
console.log("Hello ")
const { component, definition, contexts } = componentContext const { component, definition, contexts } = componentContext
if (!component || !definition || !contexts?.length) { if (!component || !definition || !contexts?.length) {
return [] return []

View file

@ -158,6 +158,7 @@ export const getFrontendStore = () => {
...INITIAL_FRONTEND_STATE.features, ...INITIAL_FRONTEND_STATE.features,
...application.features, ...application.features,
}, },
automations: application.automations || {},
icon: application.icon || {}, icon: application.icon || {},
initialised: true, initialised: true,
})) }))

View file

@ -21,7 +21,7 @@ export const createBuilderWebsocket = appId => {
}) })
}) })
socket.on("connect_error", err => { 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", () => { socket.on("disconnect", () => {
userStore.actions.reset() userStore.actions.reset()

View file

@ -15,7 +15,6 @@
Icon, Icon,
Checkbox, Checkbox,
DatePicker, DatePicker,
Detail,
} from "@budibase/bbui" } from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, selectedAutomation } from "builderStore" import { automationStore, selectedAutomation } from "builderStore"
@ -33,6 +32,8 @@
import Editor from "components/integration/QueryEditor.svelte" import Editor from "components/integration/QueryEditor.svelte"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import CodeEditor from "components/common/CodeEditor/CodeEditor.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 { import {
bindingsToCompletions, bindingsToCompletions,
hbAutocomplete, hbAutocomplete,
@ -56,7 +57,7 @@
let drawer let drawer
let fillWidth = true let fillWidth = true
let inputData let inputData
let codeBindingOpen = false let insertAtPos, getCaretPosition
$: filters = lookForFilters(schemaProperties) || [] $: filters = lookForFilters(schemaProperties) || []
$: tempFilters = filters $: tempFilters = filters
$: stepId = block.stepId $: stepId = block.stepId
@ -75,6 +76,10 @@
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW $: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
$: codeMode = $: codeMode =
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
disableWrapping: true,
})
$: editingJs = codeMode === EditorModes.JS
$: stepCompletions = $: stepCompletions =
codeMode === EditorModes.Handlebars codeMode === EditorModes.Handlebars
@ -157,6 +162,7 @@
let bindings = [] let bindings = []
let loopBlockCount = 0 let loopBlockCount = 0
const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => { const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => {
if (!name) return
const runtimeBinding = determineRuntimeBinding(name, idx, isLoopBlock) const runtimeBinding = determineRuntimeBinding(name, idx, isLoopBlock)
const categoryName = determineCategoryName(idx, isLoopBlock, bindingName) const categoryName = determineCategoryName(idx, isLoopBlock, bindingName)
@ -291,7 +297,6 @@
loopBlockCount++ loopBlockCount++
continue continue
} }
Object.entries(schema).forEach(([name, value]) => Object.entries(schema).forEach(([name, value]) =>
addBinding(name, value, icon, idx, isLoopBlock, bindingName) addBinding(name, value, icon, idx, isLoopBlock, bindingName)
) )
@ -539,39 +544,51 @@
/> />
{:else if value.customType === "code"} {:else if value.customType === "code"}
<CodeEditorModal> <CodeEditorModal>
{#if codeMode == EditorModes.JS} <div class:js-editor={editingJs}>
<ActionButton <div class:js-code={editingJs} style="width: 100%">
on:click={() => (codeBindingOpen = !codeBindingOpen)} <CodeEditor
quiet value={inputData[key]}
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"} on:change={e => {
> // need to pass without the value inside
<Detail size="S">Bindings</Detail> onChange({ detail: e.detail }, key)
</ActionButton> inputData[key] = e.detail
{#if codeBindingOpen} }}
<pre>{JSON.stringify(bindings, null, 2)}</pre> completions={stepCompletions}
{/if} mode={codeMode}
{/if} autocompleteEnabled={codeMode !== EditorModes.JS}
<CodeEditor bind:getCaretPosition
value={inputData[key]} bind:insertAtPos
on:change={e => { height={500}
// need to pass without the value inside />
onChange({ detail: e.detail }, key) <div class="messaging">
inputData[key] = e.detail {#if codeMode === EditorModes.Handlebars}
}} <Icon name="FlashOn" />
completions={stepCompletions} <div class="messaging-wrap">
mode={codeMode} <div>
autocompleteEnabled={codeMode != EditorModes.JS} Add available bindings by typing <strong>
height={500} &#125;&#125;
/> </strong>
<div class="messaging"> </div>
{#if codeMode == EditorModes.Handlebars} </div>
<Icon name="FlashOn" /> {/if}
<div class="messaging-wrap"> </div>
<div> </div>
Add available bindings by typing <strong> {#if editingJs}
&#125;&#125; <div class="js-binding-picker">
</strong> <BindingPicker
</div> {bindings}
allowHelpers={false}
addBinding={binding =>
bindingsHelpers.onSelectBinding(
inputData[key],
binding,
{
js: true,
dontDecode: true,
}
)}
mode="javascript"
/>
</div> </div>
{/if} {/if}
</div> </div>
@ -658,4 +675,20 @@
.test :global(.drawer) { .test :global(.drawer) {
width: 10000px !important; 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);
}
</style> </style>

View file

@ -54,6 +54,7 @@
export let placeholder = null export let placeholder = null
export let autocompleteEnabled = true export let autocompleteEnabled = true
export let autofocus = false export let autofocus = false
export let jsBindingWrapping = true
// Export a function to expose caret position // Export a function to expose caret position
export const getCaretPosition = () => { export const getCaretPosition = () => {
@ -187,7 +188,7 @@
) )
complete.push( complete.push(
EditorView.inputHandler.of((view, from, to, insert) => { EditorView.inputHandler.of((view, from, to, insert) => {
if (insert === "$") { if (jsBindingWrapping && insert === "$") {
let { text } = view.state.doc.lineAt(from) let { text } = view.state.doc.lineAt(from)
const left = from ? text.substring(0, from) : "" const left = from ? text.substring(0, from) : ""

View file

@ -286,13 +286,14 @@ export const hbInsert = (value, from, to, text) => {
return parsedInsert return parsedInsert
} }
export function jsInsert(value, from, to, text, { helper } = {}) { export function jsInsert(value, from, to, text, { helper, disableWrapping }) {
let parsedInsert = "" let parsedInsert = ""
const left = from ? value.substring(0, from) : "" const left = from ? value.substring(0, from) : ""
const right = to ? value.substring(to) : "" const right = to ? value.substring(to) : ""
if (disableWrapping) {
if (helper) { parsedInsert = text
} else if (helper) {
parsedInsert = `helpers.${text}()` parsedInsert = `helpers.${text}()`
} else if (!left.includes('$("') || !right.includes('")')) { } else if (!left.includes('$("') || !right.includes('")')) {
parsedInsert = `$("${text}")` parsedInsert = `$("${text}")`
@ -312,7 +313,7 @@ export const insertBinding = (view, from, to, text, mode) => {
} else if (mode.name == "handlebars") { } else if (mode.name == "handlebars") {
parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text) parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text)
} else { } else {
console.log("Unsupported") console.warn("Unsupported")
return return
} }

View file

@ -29,10 +29,9 @@
hbAutocomplete, hbAutocomplete,
EditorModes, EditorModes,
bindingsToCompletions, bindingsToCompletions,
hbInsert,
jsInsert,
} from "../CodeEditor" } from "../CodeEditor"
import BindingPicker from "./BindingPicker.svelte" import BindingPicker from "./BindingPicker.svelte"
import { BindingHelpers } from "./utils"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -60,8 +59,10 @@
let targetMode = null let targetMode = null
$: usingJS = mode === "JavaScript" $: usingJS = mode === "JavaScript"
$: editorMode = mode == "JavaScript" ? EditorModes.JS : EditorModes.Handlebars $: editorMode =
mode === "JavaScript" ? EditorModes.JS : EditorModes.Handlebars
$: bindingCompletions = bindingsToCompletions(bindings, editorMode) $: bindingCompletions = bindingsToCompletions(bindings, editorMode)
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
const updateValue = val => { const updateValue = val => {
valid = isValid(readableToRuntimeBinding(bindings, val)) valid = isValid(readableToRuntimeBinding(bindings, val))
@ -70,31 +71,13 @@
} }
} }
// Adds a JS/HBS helper to the expression
const onSelectHelper = (helper, js) => { const onSelectHelper = (helper, js) => {
const pos = getCaretPosition() bindingHelpers.onSelectHelper(js ? jsValue : hbsValue, helper, { js })
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 })
}
} }
// Adds a data binding to the expression
const onSelectBinding = (binding, { forceJS } = {}) => { const onSelectBinding = (binding, { forceJS } = {}) => {
const { start, end } = getCaretPosition() const js = usingJS || forceJS
if (usingJS || forceJS) { bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, { js })
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 onChangeMode = e => { const onChangeMode = e => {

View file

@ -9,6 +9,7 @@
export let bindings export let bindings
export let mode export let mode
export let allowHelpers export let allowHelpers
export let noPaddingTop = false
let search = "" let search = ""
let popover let popover
@ -47,9 +48,10 @@
}) })
$: filteredHelpers = helpers?.filter(helper => { $: filteredHelpers = helpers?.filter(helper => {
return ( return (
!search || (!search ||
helper.label.match(searchRgx) || helper.label.match(searchRgx) ||
helper.description.match(searchRgx) helper.description.match(searchRgx)) &&
(mode.name !== "javascript" || helper.allowsJs)
) )
}) })

View file

@ -1,38 +1,41 @@
export function addHBSBinding(value, caretPos, binding) { import { decodeJSBinding } from "@budibase/string-templates"
binding = typeof binding === "string" ? binding : binding.path import { hbInsert, jsInsert } from "components/common/CodeEditor"
value = value == null ? "" : value
const left = caretPos?.start ? value.substring(0, caretPos.start) : "" export class BindingHelpers {
const right = caretPos?.end ? value.substring(caretPos.end) : "" constructor(getCaretPosition, insertAtPos, { disableWrapping } = {}) {
if (!left.includes("{{") || !right.includes("}}")) { this.getCaretPosition = getCaretPosition
binding = `{{ ${binding} }}` 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 } = {}) { // Adds a JS/HBS helper to the expression
binding = typeof binding === "string" ? binding : binding.path onSelectHelper(value, helper, { js, dontDecode }) {
value = value == null ? "" : value const pos = this.getCaretPosition()
if (!helper) { const { start, end } = pos
binding = `$("${binding}")` if (js) {
} else { const jsVal = dontDecode ? value : decodeJSBinding(value)
binding = `helpers.${binding}()` 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 = // Adds a data binding to the expression
value.substring(0, caretPos.start) + onSelectBinding(value, binding, { js, dontDecode }) {
binding + const { start, end } = this.getCaretPosition()
value.substring(caretPos.end, value.length) if (js) {
} else { const jsVal = dontDecode ? value : decodeJSBinding(value)
value += binding 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
} }

View file

@ -67,7 +67,7 @@
})) }))
navigateStep(target) navigateStep(target)
} else { } else {
console.log("Could not retrieve step") console.warn("Could not retrieve step")
} }
} else { } else {
if (typeof tourStep.onComplete === "function") { if (typeof tourStep.onComplete === "function") {

View file

@ -3,11 +3,11 @@ import { get } from "svelte/store"
const registerNode = async (node, tourStepKey) => { const registerNode = async (node, tourStepKey) => {
if (!node) { if (!node) {
console.log("Tour Handler - an anchor node is required") console.warn("Tour Handler - an anchor node is required")
} }
if (!get(store).tourKey) { if (!get(store).tourKey) {
console.log("Tour Handler - No active tour ", tourStepKey, node) console.error("Tour Handler - No active tour ", tourStepKey, node)
return return
} }

View file

@ -45,7 +45,7 @@ const endUserOnboarding = async ({ skipped = false } = {}) => {
onboarding: false, onboarding: false,
})) }))
} catch (e) { } catch (e) {
console.log("Onboarding failed", e) console.error("Onboarding failed", e)
return false return false
} }
return true return true

View file

@ -1,4 +1,4 @@
import { getManifest } from "@budibase/string-templates" import { getManifest, helpersToRemoveForJs } from "@budibase/string-templates"
export function handlebarsCompletions() { export function handlebarsCompletions() {
const manifest = getManifest() const manifest = getManifest()
@ -11,6 +11,9 @@ export function handlebarsCompletions() {
label: helperName, label: helperName,
displayText: helperName, displayText: helperName,
description: helperConfig.description, description: helperConfig.description,
allowsJs:
!helperConfig.requiresBlock &&
!helpersToRemoveForJs.includes(helperName),
})) }))
) )
} }

View file

@ -52,7 +52,7 @@ export const syncURLToState = options => {
let cachedPage = get(routify.page) let cachedPage = get(routify.page)
let previousParamsHash = null let previousParamsHash = null
let debug = false let debug = false
const log = (...params) => debug && console.log(`[${urlParam}]`, ...params) const log = (...params) => debug && console.debug(`[${urlParam}]`, ...params)
// Navigate to a certain URL // Navigate to a certain URL
const gotoUrl = (url, params) => { const gotoUrl = (url, params) => {

View file

@ -107,7 +107,7 @@
return return
} }
if (!prodAppId) { if (!prodAppId) {
console.log("Application id required") console.error("Application id required")
return return
} }
await usersFetch.update({ await usersFetch.update({

View file

@ -66,7 +66,7 @@
try { try {
await store.actions.screens.updateSetting(get(selectedScreen), key, value) await store.actions.screens.updateSetting(get(selectedScreen), key, value)
} catch (error) { } catch (error) {
console.log(error) console.error(error)
notifications.error("Error saving screen settings") notifications.error("Error saving screen settings")
} }
} }

View file

@ -71,7 +71,7 @@
$goto(`./${screenId}`) $goto(`./${screenId}`)
store.actions.screens.select(screenId) store.actions.screens.select(screenId)
} catch (error) { } catch (error) {
console.log(error) console.error(error)
notifications.error("Error creating screens") notifications.error("Error creating screens")
} }
} }

View file

@ -36,15 +36,12 @@
let status = null let status = null
let timeRange = null let timeRange = null
let loaded = false let loaded = false
$: app = $apps.find(app => $store.appId?.includes(app.appId))
$: app = $apps.find(app => app.devId === $store.appId?.includes(app.appId))
$: licensePlan = $auth.user?.license?.plan $: licensePlan = $auth.user?.license?.plan
$: page = $pageInfo.page $: page = $pageInfo.page
$: fetchLogs(automationId, status, page, timeRange) $: fetchLogs(automationId, status, page, timeRange)
$: isCloud = $admin.cloud $: isCloud = $admin.cloud
$: chainAutomations = app?.automations?.chainAutomations ?? !isCloud $: chainAutomations = app?.automations?.chainAutomations ?? !isCloud
const timeOptions = [ const timeOptions = [
{ value: "90-d", label: "Past 90 days" }, { value: "90-d", label: "Past 90 days" },
{ value: "30-d", label: "Past 30 days" }, { value: "30-d", label: "Past 30 days" },

View file

@ -13,6 +13,7 @@
import CreateRestoreModal from "./CreateRestoreModal.svelte" import CreateRestoreModal from "./CreateRestoreModal.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { isOnlyUser } from "builderStore" import { isOnlyUser } from "builderStore"
import { BackupType } from "constants/backend/backups"
export let row export let row
@ -42,12 +43,11 @@
</script> </script>
<div class="cell"> <div class="cell">
<ActionMenu align="right"> {#if row.type !== BackupType.RESTORE}
<div slot="control"> <ActionMenu align="right">
<Icon size="M" hoverable name="MoreSmallList" /> <div slot="control">
</div> <Icon size="M" hoverable name="MoreSmallList" />
</div>
{#if row.type !== "restore"}
<AbsTooltip <AbsTooltip
position={TooltipPosition.Left} position={TooltipPosition.Left}
text="Unavailable - another user is editing this app" text="Unavailable - another user is editing this app"
@ -62,8 +62,8 @@
</AbsTooltip> </AbsTooltip>
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem> <MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem> <MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
{/if} </ActionMenu>
</ActionMenu> {/if}
</div> </div>
<Modal bind:this={restoreBackupModal}> <Modal bind:this={restoreBackupModal}>

View file

@ -31,7 +31,7 @@
async function login() { async function login() {
form.validate() form.validate()
if (Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
console.log("errors", errors) console.error("errors", errors)
return return
} }
try { try {

View file

@ -6742,6 +6742,17 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "select", "type": "select",
"label": "Layout", "label": "Layout",

View file

@ -307,7 +307,7 @@
// Reset view // Reset view
resetView() resetView()
} catch (e) { } catch (e) {
console.log("There was a problem with the map", e) console.error("There was a problem with the map", e)
} }
} }

View file

@ -61,7 +61,7 @@
resolve({ initialised: true }) resolve({ initialised: true })
}) })
.catch(err => { .catch(err => {
console.log("There was a problem scanning the image", err) console.error("There was a problem scanning the image", err)
resolve({ initialised: false }) resolve({ initialised: false })
}) })
}) })

View file

@ -28,6 +28,7 @@ import { fetchDatasourceSchema } from "./utils/schema.js"
import { getAPIKey } from "./utils/api.js" import { getAPIKey } from "./utils/api.js"
import { enrichButtonActions } from "./utils/buttonActions.js" import { enrichButtonActions } from "./utils/buttonActions.js"
import { processStringSync, makePropSafe } from "@budibase/string-templates" import { processStringSync, makePropSafe } from "@budibase/string-templates"
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
export default { export default {
API, API,
@ -54,6 +55,8 @@ export default {
linkable, linkable,
getAction, getAction,
fetchDatasourceSchema, fetchDatasourceSchema,
fetchData,
LuceneUtils,
ContextScopes, ContextScopes,
getAPIKey, getAPIKey,
enrichButtonActions, enrichButtonActions,

View file

@ -14,7 +14,7 @@ const createOrgStore = () => {
const settingsConfigDoc = await API.getTenantConfig(tenantId) const settingsConfigDoc = await API.getTenantConfig(tenantId)
set({ logoUrl: settingsConfigDoc.config.logoUrl }) set({ logoUrl: settingsConfigDoc.config.logoUrl })
} catch (e) { } catch (e) {
console.log("Could not init org ", e) console.error("Could not init org ", e)
} }
} }

View file

@ -211,29 +211,27 @@ const deleteRowHandler = async action => {
const triggerAutomationHandler = async action => { const triggerAutomationHandler = async action => {
const { fields, notificationOverride, timeout } = action.parameters const { fields, notificationOverride, timeout } = action.parameters
if (fields) { try {
try { const result = await API.triggerAutomation({
const result = await API.triggerAutomation({ automationId: action.parameters.automationId,
automationId: action.parameters.automationId, fields,
fields, timeout,
timeout, })
})
// Value will exist if automation is synchronous, so return it.
if (result.value) {
if (!notificationOverride) {
notificationStore.actions.success("Automation ran successfully")
}
return { result }
}
// Value will exist if automation is synchronous, so return it.
if (result.value) {
if (!notificationOverride) { if (!notificationOverride) {
notificationStore.actions.success("Automation triggered") notificationStore.actions.success("Automation ran successfully")
} }
} catch (error) { return { result }
// Abort next actions
return false
} }
if (!notificationOverride) {
notificationStore.actions.success("Automation triggered")
}
} catch (error) {
// Abort next actions
return false
} }
} }
const navigationHandler = action => { const navigationHandler = action => {

View file

@ -29,7 +29,7 @@ export const createGridWebsocket = context => {
connectToDatasource(get(datasource)) connectToDatasource(get(datasource))
}) })
socket.on("connect_error", err => { socket.on("connect_error", err => {
console.log("Failed to connect to grid websocket:", err.message) console.error("Failed to connect to grid websocket:", err.message)
}) })
// User events // User events

@ -1 +1 @@
Subproject commit eb9565f568cfef14b336b14eee753119acfdd43b Subproject commit aaf7101cd1493215155cc8f83124c70d53eb1be4

View file

@ -1,7 +1,13 @@
import fs from "fs"
import { join } from "path"
module AwsMock { module AwsMock {
const aws: any = {} const aws: any = {}
const response = (body: any) => () => ({ promise: () => body }) const response = (body: any, extra?: any) => () => ({
promise: () => body,
...extra,
})
function DocumentClient() { function DocumentClient() {
// @ts-ignore // @ts-ignore
@ -73,9 +79,18 @@ module AwsMock {
// @ts-ignore // @ts-ignore
this.getObject = jest.fn( this.getObject = jest.fn(
response({ response(
Body: "", {
}) Body: "",
},
{
createReadStream: jest
.fn()
.mockReturnValue(
fs.createReadStream(join(__dirname, "aws-sdk.ts"))
),
}
)
) )
} }

View file

@ -445,6 +445,9 @@ export async function update(ctx: UserCtx) {
name: app.name, name: app.name,
url: app.url, url: app.url,
icon: app.icon, icon: app.icon,
automations: {
chainAutomations: app.automations?.chainAutomations,
},
}) })
} }

View file

@ -165,8 +165,9 @@ export async function importApp(
const isTar = template.file && template?.file?.type?.endsWith("gzip") const isTar = template.file && template?.file?.type?.endsWith("gzip")
const isDirectory = const isDirectory =
template.file && fs.lstatSync(template.file.path).isDirectory() template.file && fs.lstatSync(template.file.path).isDirectory()
let tmpPath: string | undefined = undefined
if (template.file && (isTar || isDirectory)) { if (template.file && (isTar || isDirectory)) {
const tmpPath = isTar ? await untarFile(template.file) : template.file.path tmpPath = isTar ? await untarFile(template.file) : template.file.path
if (isTar && template.file.password) { if (isTar && template.file.password) {
await decryptFiles(tmpPath, template.file.password) await decryptFiles(tmpPath, template.file.password)
} }
@ -208,5 +209,9 @@ export async function importApp(
} }
await updateAttachmentColumns(prodAppId, db) await updateAttachmentColumns(prodAppId, db)
await updateAutomations(prodAppId, db) await updateAutomations(prodAppId, db)
// clear up afterward
if (tmpPath) {
fs.rmSync(tmpPath, { recursive: true, force: true })
}
return ok return ok
} }

View file

@ -1,4 +1,4 @@
import fetch from "node-fetch" import { Response, default as fetch } from "node-fetch"
import env from "../environment" import env from "../environment"
import { checkSlashesInUrl } from "./index" import { checkSlashesInUrl } from "./index"
import { import {
@ -40,25 +40,21 @@ export function request(ctx?: Ctx, request?: any) {
} }
async function checkResponse( async function checkResponse(
response: any, response: Response,
errorMsg: string, errorMsg: string,
{ ctx }: { ctx?: Ctx } = {} { ctx }: { ctx?: Ctx } = {}
) { ) {
if (response.status !== 200) { if (response.status >= 300) {
let error let responseErrorMessage
try { if (response.headers.get("content-type")?.includes("json")) {
error = await response.json() const error = await response.json()
if (!error.message) { responseErrorMessage = error.message ?? JSON.stringify(error)
error = JSON.stringify(error) } else {
} responseErrorMessage = await response.text()
} catch (err) {
error = await response.text()
} }
const msg = `Unable to ${errorMsg} - ${ const msg = `Unable to ${errorMsg} - ${responseErrorMessage}`
error.message ? error.message : error
}`
if (ctx) { if (ctx) {
ctx.throw(400, msg) ctx.throw(msg, response.status)
} else { } else {
throw msg throw msg
} }

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,7 @@
"manifest": "node ./scripts/gen-collection-info.js" "manifest": "node ./scripts/gen-collection-info.js"
}, },
"dependencies": { "dependencies": {
"@budibase/handlebars-helpers": "^0.13.0", "@budibase/handlebars-helpers": "^0.13.1",
"dayjs": "^1.10.8", "dayjs": "^1.10.8",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",

View file

@ -10,8 +10,8 @@ const marked = require("marked")
* https://github.com/budibase/handlebars-helpers * https://github.com/budibase/handlebars-helpers
*/ */
const { join } = require("path") const { join } = require("path")
const path = require("path")
const DIRECTORY = join(__dirname, "..", "..", "..")
const COLLECTIONS = [ const COLLECTIONS = [
"math", "math",
"array", "array",
@ -115,6 +115,8 @@ function getCommentInfo(file, func) {
docs.example = docs.example.replace("product", "multiply") docs.example = docs.example.replace("product", "multiply")
} }
docs.description = blocks[0].trim() docs.description = blocks[0].trim()
docs.acceptsBlock = docs.tags.some(el => el.title === "block")
docs.acceptsInline = docs.tags.some(el => el.title === "inline")
return docs return docs
} }
@ -127,7 +129,7 @@ function run() {
const foundNames = [] const foundNames = []
for (let collection of COLLECTIONS) { for (let collection of COLLECTIONS) {
const collectionFile = fs.readFileSync( const collectionFile = fs.readFileSync(
`${DIRECTORY}/node_modules/${HELPER_LIBRARY}/lib/${collection}.js`, `${path.dirname(require.resolve(HELPER_LIBRARY))}/lib/${collection}.js`,
"utf8" "utf8"
) )
const collectionInfo = {} const collectionInfo = {}
@ -159,6 +161,7 @@ function run() {
numArgs: args.length, numArgs: args.length,
example: jsDocInfo.example || undefined, example: jsDocInfo.example || undefined,
description: jsDocInfo.description, description: jsDocInfo.description,
requiresBlock: jsDocInfo.acceptsBlock && !jsDocInfo.acceptsInline,
}) })
} }
outputJSON[collection] = collectionInfo outputJSON[collection] = collectionInfo

View file

@ -1,4 +1,4 @@
const { getHelperList } = require("../helpers") const { getJsHelperList } = require("../helpers")
function getLayers(fullBlock) { function getLayers(fullBlock) {
let layers = [] let layers = []
@ -109,7 +109,7 @@ module.exports.convertHBSBlock = (block, blockNumber) => {
const layers = getLayers(block) const layers = getLayers(block)
let value = null let value = null
const list = getHelperList() const list = getJsHelperList()
for (let layer of layers) { for (let layer of layers) {
const parts = splitBySpace(layer) const parts = splitBySpace(layer)
if (value || parts.length > 1 || list[parts[0]]) { if (value || parts.length > 1 || list[parts[0]]) {

View file

@ -115,7 +115,7 @@ module.exports.duration = (str, pattern, format) => {
setLocale(config.str, config.pattern) setLocale(config.str, config.pattern)
const duration = dayjs.duration(config.str, config.pattern) const duration = dayjs.duration(config.str, config.pattern)
if (!isOptions(format)) { if (format && !isOptions(format)) {
return duration.format(format) return duration.format(format)
} else { } else {
return duration.humanize() return duration.humanize()

View file

@ -7,7 +7,7 @@ const {
HelperFunctionBuiltin, HelperFunctionBuiltin,
LITERAL_MARKER, LITERAL_MARKER,
} = require("./constants") } = require("./constants")
const { getHelperList } = require("./list") const { getJsHelperList } = require("./list")
const HTML_SWAPS = { const HTML_SWAPS = {
"<": "&lt;", "<": "&lt;",
@ -97,4 +97,4 @@ module.exports.unregisterAll = handlebars => {
externalHandlebars.unregisterAll(handlebars) externalHandlebars.unregisterAll(handlebars)
} }
module.exports.getHelperList = getHelperList module.exports.getJsHelperList = getJsHelperList

View file

@ -1,7 +1,7 @@
const { atob } = require("../utilities") const { atob } = require("../utilities")
const cloneDeep = require("lodash.clonedeep") const cloneDeep = require("lodash.clonedeep")
const { LITERAL_MARKER } = require("../helpers/constants") const { LITERAL_MARKER } = require("../helpers/constants")
const { getHelperList } = require("./list") const { getJsHelperList } = require("./list")
// The method of executing JS scripts depends on the bundle being built. // The method of executing JS scripts depends on the bundle being built.
// This setter is used in the entrypoint (either index.cjs or index.mjs). // This setter is used in the entrypoint (either index.cjs or index.mjs).
@ -49,7 +49,7 @@ module.exports.processJS = (handlebars, context) => {
// app context. // app context.
const sandboxContext = { const sandboxContext = {
$: path => getContextValue(path, cloneDeep(context)), $: path => getContextValue(path, cloneDeep(context)),
helpers: getHelperList(), helpers: getJsHelperList(),
} }
// Create a sandbox with our context and run the JS // Create a sandbox with our context and run the JS

View file

@ -3,7 +3,10 @@ const helperList = require("@budibase/handlebars-helpers")
let helpers = undefined let helpers = undefined
module.exports.getHelperList = () => { const helpersToRemoveForJs = ["sortBy"]
module.exports.helpersToRemoveForJs = helpersToRemoveForJs
module.exports.getJsHelperList = () => {
if (helpers) { if (helpers) {
return helpers return helpers
} }
@ -15,12 +18,17 @@ module.exports.getHelperList = () => {
} }
for (let collection of constructed) { for (let collection of constructed) {
for (let [key, func] of Object.entries(collection)) { for (let [key, func] of Object.entries(collection)) {
helpers[key] = func // Handlebars injects the hbs options to the helpers by default. We are adding an empty {} as a last parameter to simulate it
helpers[key] = (...props) => func(...props, {})
} }
} }
for (let key of Object.keys(externalHandlebars.addedHelpers)) { for (let key of Object.keys(externalHandlebars.addedHelpers)) {
helpers[key] = externalHandlebars.addedHelpers[key] helpers[key] = externalHandlebars.addedHelpers[key]
} }
for (const toRemove of helpersToRemoveForJs) {
delete helpers[toRemove]
}
Object.freeze(helpers) Object.freeze(helpers)
return helpers return helpers
} }

View file

@ -20,6 +20,7 @@ module.exports.findHBSBlocks = templates.findHBSBlocks
module.exports.convertToJS = templates.convertToJS module.exports.convertToJS = templates.convertToJS
module.exports.setJSRunner = templates.setJSRunner module.exports.setJSRunner = templates.setJSRunner
module.exports.FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX module.exports.FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX
module.exports.helpersToRemoveForJs = templates.helpersToRemoveForJs
if (!process.env.NO_JS) { if (!process.env.NO_JS) {
const { VM } = require("vm2") const { VM } = require("vm2")

View file

@ -10,6 +10,7 @@ const {
} = require("./utilities") } = require("./utilities")
const { convertHBSBlock } = require("./conversion") const { convertHBSBlock } = require("./conversion")
const javascript = require("./helpers/javascript") const javascript = require("./helpers/javascript")
const { helpersToRemoveForJs } = require("./helpers/list")
const hbsInstance = handlebars.create() const hbsInstance = handlebars.create()
registerAll(hbsInstance) registerAll(hbsInstance)
@ -394,3 +395,4 @@ module.exports.convertToJS = hbs => {
} }
module.exports.FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX module.exports.FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX
module.exports.helpersToRemoveForJs = helpersToRemoveForJs

View file

@ -21,6 +21,7 @@ export const findHBSBlocks = templates.findHBSBlocks
export const convertToJS = templates.convertToJS export const convertToJS = templates.convertToJS
export const setJSRunner = templates.setJSRunner export const setJSRunner = templates.setJSRunner
export const FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX export const FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX
export const helpersToRemoveForJs = templates.helpersToRemoveForJs
if (process && !process.env.NO_JS) { if (process && !process.env.NO_JS) {
/** /**

View file

@ -16,21 +16,55 @@ jest.mock("@budibase/handlebars-helpers/lib/uuid", () => {
}) })
const fs = require("fs") const fs = require("fs")
const { processString } = require("../src/index.cjs") const {
processString,
convertToJS,
processStringSync,
encodeJSBinding,
} = require("../src/index.cjs")
const tk = require("timekeeper") const tk = require("timekeeper")
const { getJsHelperList } = require("../src/helpers")
tk.freeze("2021-01-21T12:00:00") tk.freeze("2021-01-21T12:00:00")
const processJS = (js, context) => {
return processStringSync(encodeJSBinding(js), context)
}
const manifest = JSON.parse( const manifest = JSON.parse(
fs.readFileSync(require.resolve("../manifest.json"), "utf8") fs.readFileSync(require.resolve("../manifest.json"), "utf8")
) )
const collections = Object.keys(manifest) const collections = Object.keys(manifest)
const examples = collections.reduce((acc, collection) => { const examples = collections.reduce((acc, collection) => {
const functions = Object.keys(manifest[collection]).filter( const functions = Object.entries(manifest[collection])
fnc => manifest[collection][fnc].example .filter(([_, details]) => details.example)
) .map(([name, details]) => {
if (functions.length) { const example = details.example
let [hbs, js] = example.split("->").map(x => x.trim())
if (!js) {
// The function has no return value
return
}
// Trim 's
js = js.replace(/^\'|\'$/g, "")
if ((parsedExpected = tryParseJson(js))) {
if (Array.isArray(parsedExpected)) {
if (typeof parsedExpected[0] === "object") {
js = JSON.stringify(parsedExpected)
} else {
js = parsedExpected.join(",")
}
}
}
const requiresHbsBody = details.requiresBlock
return [name, { hbs, js, requiresHbsBody }]
})
.filter(x => !!x)
if (Object.keys(functions).length) {
acc[collection] = functions acc[collection] = functions
} }
return acc return acc
@ -55,11 +89,7 @@ function tryParseJson(str) {
describe("manifest", () => { describe("manifest", () => {
describe("examples are valid", () => { describe("examples are valid", () => {
describe.each(Object.keys(examples))("%s", collection => { describe.each(Object.keys(examples))("%s", collection => {
it.each(examples[collection])("%s", async func => { it.each(examples[collection])("%s", async (_, { hbs, js }) => {
const example = manifest[collection][func].example
let [hbs, js] = example.split("->").map(x => x.trim())
const context = { const context = {
double: i => i * 2, double: i => i * 2,
isString: x => typeof x === "string", isString: x => typeof x === "string",
@ -71,23 +101,40 @@ describe("manifest", () => {
context[`array${i}`] = JSON.parse(arrayString.replace(/\'/g, '"')) context[`array${i}`] = JSON.parse(arrayString.replace(/\'/g, '"'))
}) })
if (js === undefined) { let result = await processString(hbs, context)
// The function has no return value result = result.replace(/&nbsp;/g, " ")
return expect(result).toEqual(js)
})
})
})
describe("can be parsed and run as js", () => {
const jsHelpers = getJsHelperList()
const jsExamples = Object.keys(examples).reduce((acc, v) => {
acc[v] = examples[v].filter(([key]) => jsHelpers[key])
return acc
}, {})
describe.each(Object.keys(jsExamples))("%s", collection => {
it.each(
jsExamples[collection].filter(
([_, { requiresHbsBody }]) => !requiresHbsBody
)
)("%s", async (_, { hbs, js }) => {
const context = {
double: i => i * 2,
isString: x => typeof x === "string",
} }
let result = await processString(hbs, context) const arrays = hbs.match(/\[[^/\]]+\]/)
// Trim 's arrays?.forEach((arrayString, i) => {
js = js.replace(/^\'|\'$/g, "") hbs = hbs.replace(new RegExp(escapeRegExp(arrayString)), `array${i}`)
if ((parsedExpected = tryParseJson(js))) { context[`array${i}`] = JSON.parse(arrayString.replace(/\'/g, '"'))
if (Array.isArray(parsedExpected)) { })
if (typeof parsedExpected[0] === "object") {
js = JSON.stringify(parsedExpected) let convertedJs = convertToJS(hbs)
} else {
js = parsedExpected.join(",") let result = processJS(convertedJs, context)
}
}
}
result = result.replace(/&nbsp;/g, " ") result = result.replace(/&nbsp;/g, " ")
expect(result).toEqual(js) expect(result).toEqual(js)
}) })

View file

@ -15,7 +15,7 @@
}, },
"jest": {}, "jest": {},
"devDependencies": { "devDependencies": {
"@budibase/nano": "10.1.4", "@budibase/nano": "10.1.5",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/pouchdb": "6.4.0", "@types/pouchdb": "6.4.0",
"@types/redlock": "4.0.3", "@types/redlock": "4.0.3",

View file

@ -63,7 +63,7 @@
"koa-useragent": "^4.1.0", "koa-useragent": "^4.1.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"node-fetch": "2.6.7", "node-fetch": "2.6.7",
"nodemailer": "6.7.2", "nodemailer": "6.9.9",
"passport-google-oauth": "2.0.0", "passport-google-oauth": "2.0.0",
"passport-local": "1.0.0", "passport-local": "1.0.0",
"pouchdb": "7.3.0", "pouchdb": "7.3.0",

View file

@ -2038,10 +2038,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/handlebars-helpers@^0.13.0": "@budibase/handlebars-helpers@^0.13.1":
version "0.13.0" version "0.13.1"
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.13.0.tgz#224333d14e3900b7dacf48286af1e624a9fd62ea" resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.13.1.tgz#d02e73c0df8305cd675e70dc37f8427eb0842080"
integrity sha512-g8+sFrMNxsIDnK+MmdUICTVGr6ReUFtnPp9hJX0VZwz1pN3Ynolpk/Qbu6rEWAvoU1sEqY1mXr9uo/+kEfeGbQ== integrity sha512-v4RbXhr3igvK3i2pj5cNltu/4NMxdPIzcUt/o0RoInhesNH1VSLRdweSFr6/Y34fsCR5jHZ6vltdcz2RgrTKgw==
dependencies: dependencies:
get-object "^0.2.0" get-object "^0.2.0"
get-value "^3.0.1" get-value "^3.0.1"
@ -2057,10 +2057,10 @@
to-gfm-code-block "^0.1.1" to-gfm-code-block "^0.1.1"
uuid "^9.0.1" uuid "^9.0.1"
"@budibase/nano@10.1.4": "@budibase/nano@10.1.5":
version "10.1.4" version "10.1.5"
resolved "https://registry.yarnpkg.com/@budibase/nano/-/nano-10.1.4.tgz#5c2670d0b4c12d736ddd6581c57d47c0aa45efad" resolved "https://registry.yarnpkg.com/@budibase/nano/-/nano-10.1.5.tgz#eeaded7bfc707ecabf8fde604425b865a90c06ec"
integrity sha512-J+IVaAljGideDvJss/AUxXA1599HEIUJo5c0LLlmc1KMA3GZWZjyX+w2fxAw3qF7hqFvX+qAStQgdcD3+/GPMA== integrity sha512-q1eKIsYKo+iK17zsJYd3VBl+5ufQMPpHYLec0wVsid8wnJVrTQk7RNpBlBUn/EDgXM7t8XNNHlERqHu+CxJu8Q==
dependencies: dependencies:
"@types/tough-cookie" "^4.0.2" "@types/tough-cookie" "^4.0.2"
axios "^1.1.3" axios "^1.1.3"