Merge remote-tracking branch 'origin/develop' into feature/user-side-panel-ux-updates
This commit is contained in:
commit
84d6af52f4
19 changed files with 414 additions and 59 deletions
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.6.8-alpha.7",
|
"version": "2.6.8-alpha.10",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/backend-core",
|
"packages/backend-core",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import * as db from "../../db"
|
||||||
import { Header } from "../../constants"
|
import { Header } from "../../constants"
|
||||||
import { newid } from "../../utils"
|
import { newid } from "../../utils"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
|
import { BBContext } from "@budibase/types"
|
||||||
|
|
||||||
describe("utils", () => {
|
describe("utils", () => {
|
||||||
const config = new DBTestConfiguration()
|
const config = new DBTestConfiguration()
|
||||||
|
@ -106,4 +107,85 @@ describe("utils", () => {
|
||||||
expect(actual).toBe(undefined)
|
expect(actual).toBe(undefined)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("isServingBuilder", () => {
|
||||||
|
let ctx: BBContext
|
||||||
|
|
||||||
|
const expectResult = (result: boolean) =>
|
||||||
|
expect(utils.isServingBuilder(ctx)).toBe(result)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx = structures.koa.newContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true if current path is in builder", async () => {
|
||||||
|
ctx.path = "/builder/app/app_"
|
||||||
|
expectResult(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if current path doesn't have '/' suffix", async () => {
|
||||||
|
ctx.path = "/builder/app"
|
||||||
|
expectResult(false)
|
||||||
|
|
||||||
|
ctx.path = "/xx"
|
||||||
|
expectResult(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isServingBuilderPreview", () => {
|
||||||
|
let ctx: BBContext
|
||||||
|
|
||||||
|
const expectResult = (result: boolean) =>
|
||||||
|
expect(utils.isServingBuilderPreview(ctx)).toBe(result)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx = structures.koa.newContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true if current path is in builder preview", async () => {
|
||||||
|
ctx.path = "/app/preview/xx"
|
||||||
|
expectResult(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if current path is not in builder preview", async () => {
|
||||||
|
ctx.path = "/builder"
|
||||||
|
expectResult(false)
|
||||||
|
|
||||||
|
ctx.path = "/xx"
|
||||||
|
expectResult(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isPublicAPIRequest", () => {
|
||||||
|
let ctx: BBContext
|
||||||
|
|
||||||
|
const expectResult = (result: boolean) =>
|
||||||
|
expect(utils.isPublicApiRequest(ctx)).toBe(result)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx = structures.koa.newContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true if current path remains to public API", async () => {
|
||||||
|
ctx.path = "/api/public/v1/invoices"
|
||||||
|
expectResult(true)
|
||||||
|
|
||||||
|
ctx.path = "/api/public/v1"
|
||||||
|
expectResult(true)
|
||||||
|
|
||||||
|
ctx.path = "/api/public/v2"
|
||||||
|
expectResult(true)
|
||||||
|
|
||||||
|
ctx.path = "/api/public/v21"
|
||||||
|
expectResult(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if current path doesn't remain to public API", async () => {
|
||||||
|
ctx.path = "/api/public"
|
||||||
|
expectResult(false)
|
||||||
|
|
||||||
|
ctx.path = "/xx"
|
||||||
|
expectResult(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import { getAllApps, queryGlobalView } from "../db"
|
import { getAllApps } from "../db"
|
||||||
import {
|
import { Header, MAX_VALID_DATE, DocumentType, SEPARATOR } from "../constants"
|
||||||
Header,
|
|
||||||
MAX_VALID_DATE,
|
|
||||||
DocumentType,
|
|
||||||
SEPARATOR,
|
|
||||||
ViewName,
|
|
||||||
} from "../constants"
|
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import * as tenancy from "../tenancy"
|
import * as tenancy from "../tenancy"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
|
@ -23,7 +17,9 @@ const APP_PREFIX = DocumentType.APP + SEPARATOR
|
||||||
const PROD_APP_PREFIX = "/app/"
|
const PROD_APP_PREFIX = "/app/"
|
||||||
|
|
||||||
const BUILDER_PREVIEW_PATH = "/app/preview"
|
const BUILDER_PREVIEW_PATH = "/app/preview"
|
||||||
const BUILDER_REFERER_PREFIX = "/builder/app/"
|
const BUILDER_PREFIX = "/builder"
|
||||||
|
const BUILDER_APP_PREFIX = `${BUILDER_PREFIX}/app/`
|
||||||
|
const PUBLIC_API_PREFIX = "/api/public/v"
|
||||||
|
|
||||||
function confirmAppId(possibleAppId: string | undefined) {
|
function confirmAppId(possibleAppId: string | undefined) {
|
||||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||||
|
@ -69,6 +65,18 @@ export function isServingApp(ctx: Ctx) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isServingBuilder(ctx: Ctx): boolean {
|
||||||
|
return ctx.path.startsWith(BUILDER_APP_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isServingBuilderPreview(ctx: Ctx): boolean {
|
||||||
|
return ctx.path.startsWith(BUILDER_PREVIEW_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPublicApiRequest(ctx: Ctx): boolean {
|
||||||
|
return ctx.path.startsWith(PUBLIC_API_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a request tries to find the appId, which can be located in various places
|
* Given a request tries to find the appId, which can be located in various places
|
||||||
* @param {object} ctx The main request body to look through.
|
* @param {object} ctx The main request body to look through.
|
||||||
|
@ -110,7 +118,7 @@ export async function getAppIdFromCtx(ctx: Ctx) {
|
||||||
// make sure this is performed after prod app url resolution, in case the
|
// make sure this is performed after prod app url resolution, in case the
|
||||||
// referer header is present from a builder redirect
|
// referer header is present from a builder redirect
|
||||||
const referer = ctx.request.headers.referer
|
const referer = ctx.request.headers.referer
|
||||||
if (!appId && referer?.includes(BUILDER_REFERER_PREFIX)) {
|
if (!appId && referer?.includes(BUILDER_APP_PREFIX)) {
|
||||||
const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
|
const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
|
||||||
appId = confirmAppId(refererId)
|
appId = confirmAppId(refererId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,14 @@
|
||||||
export let ignoreTimezones = false
|
export let ignoreTimezones = false
|
||||||
export let time24hr = false
|
export let time24hr = false
|
||||||
export let range = false
|
export let range = false
|
||||||
|
export let flatpickr
|
||||||
|
export let useKeyboardShortcuts = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flatpickrId = `${uuid()}-wrapper`
|
const flatpickrId = `${uuid()}-wrapper`
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
let flatpickr, flatpickrOptions
|
let flatpickrOptions
|
||||||
|
|
||||||
// Another classic flatpickr issue. Errors were randomly being thrown due to
|
// Another classic flatpickr issue. Errors were randomly being thrown due to
|
||||||
// flatpickr internal code. Making sure that "destroy" is a valid function
|
// flatpickr internal code. Making sure that "destroy" is a valid function
|
||||||
|
@ -59,6 +63,8 @@
|
||||||
dispatch("change", timestamp.toISOString())
|
dispatch("change", timestamp.toISOString())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onOpen: () => dispatch("open"),
|
||||||
|
onClose: () => dispatch("close"),
|
||||||
}
|
}
|
||||||
|
|
||||||
$: redrawOptions = {
|
$: redrawOptions = {
|
||||||
|
@ -113,12 +119,16 @@
|
||||||
|
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
open = true
|
open = true
|
||||||
|
if (useKeyboardShortcuts) {
|
||||||
document.addEventListener("keyup", clearDateOnBackspace)
|
document.addEventListener("keyup", clearDateOnBackspace)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
open = false
|
open = false
|
||||||
|
if (useKeyboardShortcuts) {
|
||||||
document.removeEventListener("keyup", clearDateOnBackspace)
|
document.removeEventListener("keyup", clearDateOnBackspace)
|
||||||
|
}
|
||||||
|
|
||||||
// Manually blur all input fields since flatpickr creates a second
|
// Manually blur all input fields since flatpickr creates a second
|
||||||
// duplicate input field.
|
// duplicate input field.
|
||||||
|
|
|
@ -61,11 +61,63 @@
|
||||||
$: isTrigger = block?.type === "TRIGGER"
|
$: isTrigger = block?.type === "TRIGGER"
|
||||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO - Remove after November 2023
|
||||||
|
* *******************************
|
||||||
|
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||||
|
* and the new JSON body.
|
||||||
|
*/
|
||||||
|
let deprecatedSchemaProperties
|
||||||
|
$: {
|
||||||
|
if (block?.stepId === "integromat" || block?.stepId === "zapier") {
|
||||||
|
deprecatedSchemaProperties = schemaProperties.filter(
|
||||||
|
prop => !prop[0].startsWith("value")
|
||||||
|
)
|
||||||
|
if (!deprecatedSchemaProperties.map(entry => entry[0]).includes("body")) {
|
||||||
|
deprecatedSchemaProperties.push([
|
||||||
|
"body",
|
||||||
|
{
|
||||||
|
title: "Payload",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deprecatedSchemaProperties = schemaProperties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/****************************************************/
|
||||||
|
|
||||||
const getInputData = (testData, blockInputs) => {
|
const getInputData = (testData, blockInputs) => {
|
||||||
let newInputData = testData || blockInputs
|
let newInputData = testData || blockInputs
|
||||||
if (block.event === "app:trigger" && !newInputData?.fields) {
|
if (block.event === "app:trigger" && !newInputData?.fields) {
|
||||||
newInputData = cloneDeep(blockInputs)
|
newInputData = cloneDeep(blockInputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO - Remove after November 2023
|
||||||
|
* *******************************
|
||||||
|
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||||
|
* and the new JSON body.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
(block?.stepId === "integromat" || block?.stepId === "zapier") &&
|
||||||
|
!newInputData?.body?.value
|
||||||
|
) {
|
||||||
|
let deprecatedValues = {
|
||||||
|
...newInputData,
|
||||||
|
}
|
||||||
|
delete deprecatedValues.url
|
||||||
|
delete deprecatedValues.body
|
||||||
|
newInputData = {
|
||||||
|
url: newInputData.url,
|
||||||
|
body: {
|
||||||
|
value: JSON.stringify(deprecatedValues),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**********************************/
|
||||||
|
|
||||||
inputData = newInputData
|
inputData = newInputData
|
||||||
setDefaultEnumValues()
|
setDefaultEnumValues()
|
||||||
}
|
}
|
||||||
|
@ -239,7 +291,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each schemaProperties as [key, value]}
|
{#each deprecatedSchemaProperties as [key, value]}
|
||||||
<div class="block-field">
|
<div class="block-field">
|
||||||
{#if key !== "fields"}
|
{#if key !== "fields"}
|
||||||
<Label
|
<Label
|
||||||
|
@ -256,6 +308,28 @@
|
||||||
options={value.enum}
|
options={value.enum}
|
||||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||||
/>
|
/>
|
||||||
|
{:else if value.type === "json"}
|
||||||
|
<Editor
|
||||||
|
editorHeight="250"
|
||||||
|
editorWidth="448"
|
||||||
|
mode="json"
|
||||||
|
value={inputData[key]?.value}
|
||||||
|
on:change={e => {
|
||||||
|
/**
|
||||||
|
* TODO - Remove after November 2023
|
||||||
|
* *******************************
|
||||||
|
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||||
|
* and the new JSON body.
|
||||||
|
*/
|
||||||
|
delete inputData.value1
|
||||||
|
delete inputData.value2
|
||||||
|
delete inputData.value3
|
||||||
|
delete inputData.value4
|
||||||
|
delete inputData.value5
|
||||||
|
/***********************/
|
||||||
|
onChange(e, key)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{:else if value.customType === "column"}
|
{:else if value.customType === "column"}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
export let tab = true
|
export let tab = true
|
||||||
export let mode
|
export let mode
|
||||||
export let editorHeight = 500
|
export let editorHeight = 500
|
||||||
|
export let editorWidth = 640
|
||||||
// export let parameters = []
|
// export let parameters = []
|
||||||
|
|
||||||
let width
|
let width
|
||||||
|
@ -169,7 +170,9 @@
|
||||||
{#if label}
|
{#if label}
|
||||||
<Label small>{label}</Label>
|
<Label small>{label}</Label>
|
||||||
{/if}
|
{/if}
|
||||||
<div style={`--code-mirror-height: ${editorHeight}px`}>
|
<div
|
||||||
|
style={`--code-mirror-height: ${editorHeight}px; --code-mirror-width: ${editorWidth}px;`}
|
||||||
|
>
|
||||||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -183,6 +186,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.CodeMirror) {
|
div :global(.CodeMirror) {
|
||||||
|
width: var(--code-mirror-width) !important;
|
||||||
height: var(--code-mirror-height) !important;
|
height: var(--code-mirror-height) !important;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
$: readonly =
|
$: readonly =
|
||||||
column.schema.autocolumn ||
|
column.schema.autocolumn ||
|
||||||
column.schema.disabled ||
|
column.schema.disabled ||
|
||||||
|
column.schema.type === "formula" ||
|
||||||
(!$config.allowEditRows && row._id)
|
(!$config.allowEditRows && row._id)
|
||||||
|
|
||||||
// Register this cell API if the row is focused
|
// Register this cell API if the row is focused
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { CoreDatePicker, Icon } from "@budibase/bbui"
|
import { CoreDatePicker, Icon } from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let schema
|
export let schema
|
||||||
export let onChange
|
export let onChange
|
||||||
export let focused = false
|
export let focused = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
|
export let api
|
||||||
|
|
||||||
|
let flatpickr
|
||||||
|
let isOpen
|
||||||
|
|
||||||
// adding the 0- will turn a string like 00:00:00 into a valid ISO
|
// adding the 0- will turn a string like 00:00:00 into a valid ISO
|
||||||
// date, but will make actual ISO dates invalid
|
// date, but will make actual ISO dates invalid
|
||||||
|
@ -19,6 +24,26 @@
|
||||||
? "MMM D YYYY"
|
? "MMM D YYYY"
|
||||||
: "MMM D YYYY, HH:mm"
|
: "MMM D YYYY, HH:mm"
|
||||||
$: editable = focused && !readonly
|
$: editable = focused && !readonly
|
||||||
|
|
||||||
|
// Ensure we close flatpickr when unselected
|
||||||
|
$: {
|
||||||
|
if (!focused) {
|
||||||
|
flatpickr?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = () => {
|
||||||
|
return isOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
api = {
|
||||||
|
onKeyDown,
|
||||||
|
focus: () => flatpickr?.open(),
|
||||||
|
blur: () => flatpickr?.close(),
|
||||||
|
isActive: () => isOpen,
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -42,6 +67,10 @@
|
||||||
{timeOnly}
|
{timeOnly}
|
||||||
time24hr
|
time24hr
|
||||||
ignoreTimezones={schema.ignoreTimezones}
|
ignoreTimezones={schema.ignoreTimezones}
|
||||||
|
bind:flatpickr
|
||||||
|
on:open={() => (isOpen = true)}
|
||||||
|
on:close={() => (isOpen = false)}
|
||||||
|
useKeyboardShortcuts={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { clickOutside, Menu, MenuItem, notifications } from "@budibase/bbui"
|
import {
|
||||||
|
clickOutside,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Helpers,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import { NewRowID } from "../lib/constants"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
focusedRow,
|
focusedRow,
|
||||||
|
@ -14,9 +21,11 @@
|
||||||
clipboard,
|
clipboard,
|
||||||
dispatch,
|
dispatch,
|
||||||
focusedCellAPI,
|
focusedCellAPI,
|
||||||
|
focusedRowId,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
$: style = makeStyle($menu)
|
$: style = makeStyle($menu)
|
||||||
|
$: isNewRow = $focusedRowId === NewRowID
|
||||||
|
|
||||||
const makeStyle = menu => {
|
const makeStyle = menu => {
|
||||||
return `left:${menu.left}px; top:${menu.top}px;`
|
return `left:${menu.left}px; top:${menu.top}px;`
|
||||||
|
@ -36,6 +45,11 @@
|
||||||
$focusedCellId = `${newRow._id}-${column}`
|
$focusedCellId = `${newRow._id}-${column}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyToClipboard = async value => {
|
||||||
|
await Helpers.copyToClipboard(value)
|
||||||
|
notifications.success("Copied to clipboard")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $menu.visible}
|
{#if $menu.visible}
|
||||||
|
@ -58,22 +72,38 @@
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Maximize"
|
icon="Maximize"
|
||||||
disabled={!$config.allowEditRows}
|
disabled={isNewRow || !$config.allowEditRows}
|
||||||
on:click={() => dispatch("edit-row", $focusedRow)}
|
on:click={() => dispatch("edit-row", $focusedRow)}
|
||||||
on:click={menu.actions.close}
|
on:click={menu.actions.close}
|
||||||
>
|
>
|
||||||
Edit row in modal
|
Edit row in modal
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon="Copy"
|
||||||
|
disabled={isNewRow || !$focusedRow?._id}
|
||||||
|
on:click={() => copyToClipboard($focusedRow?._id)}
|
||||||
|
on:click={menu.actions.close}
|
||||||
|
>
|
||||||
|
Copy row _id
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon="Copy"
|
||||||
|
disabled={isNewRow || !$focusedRow?._rev}
|
||||||
|
on:click={() => copyToClipboard($focusedRow?._rev)}
|
||||||
|
on:click={menu.actions.close}
|
||||||
|
>
|
||||||
|
Copy row _rev
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Duplicate"
|
icon="Duplicate"
|
||||||
disabled={!$config.allowAddRows}
|
disabled={isNewRow || !$config.allowAddRows}
|
||||||
on:click={duplicate}
|
on:click={duplicate}
|
||||||
>
|
>
|
||||||
Duplicate row
|
Duplicate row
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Delete"
|
icon="Delete"
|
||||||
disabled={!$config.allowDeleteRows}
|
disabled={isNewRow || !$config.allowDeleteRows}
|
||||||
on:click={deleteRow}
|
on:click={deleteRow}
|
||||||
>
|
>
|
||||||
Delete row
|
Delete row
|
||||||
|
|
|
@ -338,15 +338,11 @@ export const deriveStores = context => {
|
||||||
...state,
|
...state,
|
||||||
[rowId]: true,
|
[rowId]: true,
|
||||||
}))
|
}))
|
||||||
const newRow = { ...row, ...get(rowChangeCache)[rowId] }
|
const saved = await API.saveRow({ ...row, ...get(rowChangeCache)[rowId] })
|
||||||
const saved = await API.saveRow(newRow)
|
|
||||||
|
|
||||||
// Update state after a successful change
|
// Update state after a successful change
|
||||||
rows.update(state => {
|
rows.update(state => {
|
||||||
state[index] = {
|
state[index] = saved
|
||||||
...newRow,
|
|
||||||
_rev: saved._rev,
|
|
||||||
}
|
|
||||||
return state.slice()
|
return state.slice()
|
||||||
})
|
})
|
||||||
rowChangeCache.update(state => {
|
rowChangeCache.update(state => {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit c57a98d246a50a43905d8572a88c901ec598390c
|
Subproject commit 14345384f7a6755d1e2de327104741e0f208f55d
|
|
@ -26,6 +26,10 @@ export const definition: AutomationStepSchema = {
|
||||||
type: AutomationIOType.STRING,
|
type: AutomationIOType.STRING,
|
||||||
title: "Webhook URL",
|
title: "Webhook URL",
|
||||||
},
|
},
|
||||||
|
body: {
|
||||||
|
type: AutomationIOType.JSON,
|
||||||
|
title: "Payload",
|
||||||
|
},
|
||||||
value1: {
|
value1: {
|
||||||
type: AutomationIOType.STRING,
|
type: AutomationIOType.STRING,
|
||||||
title: "Input Value 1",
|
title: "Input Value 1",
|
||||||
|
@ -70,7 +74,19 @@ export const definition: AutomationStepSchema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run({ inputs }: AutomationStepInput) {
|
export async function run({ inputs }: AutomationStepInput) {
|
||||||
const { url, value1, value2, value3, value4, value5 } = inputs
|
//TODO - Remove deprecated values 1,2,3,4,5 after November 2023
|
||||||
|
const { url, value1, value2, value3, value4, value5, body } = inputs
|
||||||
|
|
||||||
|
let payload = {}
|
||||||
|
try {
|
||||||
|
payload = body?.value ? JSON.parse(body?.value) : {}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
httpStatus: 400,
|
||||||
|
response: "Invalid payload JSON",
|
||||||
|
success: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!url?.trim()?.length) {
|
if (!url?.trim()?.length) {
|
||||||
return {
|
return {
|
||||||
|
@ -89,6 +105,7 @@ export async function run({ inputs }: AutomationStepInput) {
|
||||||
value3,
|
value3,
|
||||||
value4,
|
value4,
|
||||||
value5,
|
value5,
|
||||||
|
...payload,
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
|
@ -24,6 +24,10 @@ export const definition: AutomationStepSchema = {
|
||||||
type: AutomationIOType.STRING,
|
type: AutomationIOType.STRING,
|
||||||
title: "Webhook URL",
|
title: "Webhook URL",
|
||||||
},
|
},
|
||||||
|
body: {
|
||||||
|
type: AutomationIOType.JSON,
|
||||||
|
title: "Payload",
|
||||||
|
},
|
||||||
value1: {
|
value1: {
|
||||||
type: AutomationIOType.STRING,
|
type: AutomationIOType.STRING,
|
||||||
title: "Payload Value 1",
|
title: "Payload Value 1",
|
||||||
|
@ -63,7 +67,19 @@ export const definition: AutomationStepSchema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run({ inputs }: AutomationStepInput) {
|
export async function run({ inputs }: AutomationStepInput) {
|
||||||
const { url, value1, value2, value3, value4, value5 } = inputs
|
//TODO - Remove deprecated values 1,2,3,4,5 after November 2023
|
||||||
|
const { url, value1, value2, value3, value4, value5, body } = inputs
|
||||||
|
|
||||||
|
let payload = {}
|
||||||
|
try {
|
||||||
|
payload = body?.value ? JSON.parse(body?.value) : {}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
httpStatus: 400,
|
||||||
|
response: "Invalid payload JSON",
|
||||||
|
success: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!url?.trim()?.length) {
|
if (!url?.trim()?.length) {
|
||||||
return {
|
return {
|
||||||
|
@ -85,6 +101,7 @@ export async function run({ inputs }: AutomationStepInput) {
|
||||||
value3,
|
value3,
|
||||||
value4,
|
value4,
|
||||||
value5,
|
value5,
|
||||||
|
...payload,
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
54
packages/server/src/automations/tests/make.spec.ts
Normal file
54
packages/server/src/automations/tests/make.spec.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { getConfig, afterAll, runStep, actions } from "./utilities"
|
||||||
|
|
||||||
|
describe("test the outgoing webhook action", () => {
|
||||||
|
let config = getConfig()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll()
|
||||||
|
|
||||||
|
it("should be able to run the action", async () => {
|
||||||
|
const res = await runStep(actions.integromat.stepId, {
|
||||||
|
value1: "test",
|
||||||
|
url: "http://www.test.com",
|
||||||
|
})
|
||||||
|
expect(res.response.url).toEqual("http://www.test.com")
|
||||||
|
expect(res.response.method).toEqual("post")
|
||||||
|
expect(res.success).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add the payload props when a JSON string is provided", async () => {
|
||||||
|
const payload = `{"value1":1,"value2":2,"value3":3,"value4":4,"value5":5,"name":"Adam","age":9}`
|
||||||
|
const res = await runStep(actions.integromat.stepId, {
|
||||||
|
value1: "ONE",
|
||||||
|
value2: "TWO",
|
||||||
|
value3: "THREE",
|
||||||
|
value4: "FOUR",
|
||||||
|
value5: "FIVE",
|
||||||
|
body: {
|
||||||
|
value: payload,
|
||||||
|
},
|
||||||
|
url: "http://www.test.com",
|
||||||
|
})
|
||||||
|
expect(res.response.url).toEqual("http://www.test.com")
|
||||||
|
expect(res.response.method).toEqual("post")
|
||||||
|
expect(res.response.body).toEqual(payload)
|
||||||
|
expect(res.success).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||||
|
const payload = `{ value1 1 }`
|
||||||
|
const res = await runStep(actions.integromat.stepId, {
|
||||||
|
value1: "ONE",
|
||||||
|
body: {
|
||||||
|
value: payload,
|
||||||
|
},
|
||||||
|
url: "http://www.test.com",
|
||||||
|
})
|
||||||
|
expect(res.httpStatus).toEqual(400)
|
||||||
|
expect(res.response).toEqual("Invalid payload JSON")
|
||||||
|
expect(res.success).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,27 +0,0 @@
|
||||||
const setup = require("./utilities")
|
|
||||||
const fetch = require("node-fetch")
|
|
||||||
|
|
||||||
jest.mock("node-fetch")
|
|
||||||
|
|
||||||
describe("test the outgoing webhook action", () => {
|
|
||||||
let inputs
|
|
||||||
let config = setup.getConfig()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
inputs = {
|
|
||||||
value1: "test",
|
|
||||||
url: "http://www.test.com",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
|
||||||
|
|
||||||
it("should be able to run the action", async () => {
|
|
||||||
const res = await setup.runStep(setup.actions.zapier.stepId, inputs)
|
|
||||||
expect(res.response.url).toEqual("http://www.test.com")
|
|
||||||
expect(res.response.method).toEqual("post")
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
56
packages/server/src/automations/tests/zapier.spec.ts
Normal file
56
packages/server/src/automations/tests/zapier.spec.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { getConfig, afterAll, runStep, actions } from "./utilities"
|
||||||
|
|
||||||
|
describe("test the outgoing webhook action", () => {
|
||||||
|
let config = getConfig()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll()
|
||||||
|
|
||||||
|
it("should be able to run the action", async () => {
|
||||||
|
const res = await runStep(actions.zapier.stepId, {
|
||||||
|
value1: "test",
|
||||||
|
url: "http://www.test.com",
|
||||||
|
})
|
||||||
|
expect(res.response.url).toEqual("http://www.test.com")
|
||||||
|
expect(res.response.method).toEqual("post")
|
||||||
|
expect(res.success).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add the payload props when a JSON string is provided", async () => {
|
||||||
|
const payload = `{ "value1": 1, "value2": 2, "value3": 3, "value4": 4, "value5": 5, "name": "Adam", "age": 9 }`
|
||||||
|
const res = await runStep(actions.zapier.stepId, {
|
||||||
|
value1: "ONE",
|
||||||
|
value2: "TWO",
|
||||||
|
value3: "THREE",
|
||||||
|
value4: "FOUR",
|
||||||
|
value5: "FIVE",
|
||||||
|
body: {
|
||||||
|
value: payload,
|
||||||
|
},
|
||||||
|
url: "http://www.test.com",
|
||||||
|
})
|
||||||
|
expect(res.response.url).toEqual("http://www.test.com")
|
||||||
|
expect(res.response.method).toEqual("post")
|
||||||
|
expect(res.response.body).toEqual(
|
||||||
|
`{"platform":"budibase","value1":1,"value2":2,"value3":3,"value4":4,"value5":5,"name":"Adam","age":9}`
|
||||||
|
)
|
||||||
|
expect(res.success).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||||
|
const payload = `{ value1 1 }`
|
||||||
|
const res = await runStep(actions.zapier.stepId, {
|
||||||
|
value1: "ONE",
|
||||||
|
body: {
|
||||||
|
value: payload,
|
||||||
|
},
|
||||||
|
url: "http://www.test.com",
|
||||||
|
})
|
||||||
|
expect(res.httpStatus).toEqual(400)
|
||||||
|
expect(res.response).toEqual("Invalid payload JSON")
|
||||||
|
expect(res.success).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
|
@ -7,6 +7,7 @@ export enum AutomationIOType {
|
||||||
BOOLEAN = "boolean",
|
BOOLEAN = "boolean",
|
||||||
NUMBER = "number",
|
NUMBER = "number",
|
||||||
ARRAY = "array",
|
ARRAY = "array",
|
||||||
|
JSON = "json",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AutomationCustomIOType {
|
export enum AutomationCustomIOType {
|
||||||
|
|
|
@ -20,6 +20,12 @@ export default class LicenseAPI {
|
||||||
internal: true,
|
internal: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not update license for accountId=${accountId}: ${response.status}`
|
||||||
|
)
|
||||||
|
}
|
||||||
return [response, json]
|
return [response, json]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,6 @@ describe("Internal API - App Specific Roles & Permissions", () => {
|
||||||
await config.beforeAll()
|
await config.beforeAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await config.afterAll()
|
|
||||||
})
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await config.afterAll()
|
await config.afterAll()
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue