Merge remote-tracking branch 'origin/master' into feature/monolith-js-refactor
This commit is contained in:
commit
03f90500e5
|
@ -77,7 +77,7 @@ mkdir -p ${DATA_DIR}/minio
|
||||||
chown -R couchdb:couchdb ${DATA_DIR}/couch
|
chown -R couchdb:couchdb ${DATA_DIR}/couch
|
||||||
redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 &
|
redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 &
|
||||||
/bbcouch-runner.sh &
|
/bbcouch-runner.sh &
|
||||||
minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 &
|
/minio/minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 &
|
||||||
/etc/init.d/nginx restart
|
/etc/init.d/nginx restart
|
||||||
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
|
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
|
||||||
# Add monthly cron job to renew certbot certificate
|
# Add monthly cron job to renew certbot certificate
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.12.9",
|
"version": "2.12.12",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -30,7 +30,6 @@ export * as timers from "./timers"
|
||||||
export { default as env } from "./environment"
|
export { default as env } from "./environment"
|
||||||
export * as blacklist from "./blacklist"
|
export * as blacklist from "./blacklist"
|
||||||
export * as docUpdates from "./docUpdates"
|
export * as docUpdates from "./docUpdates"
|
||||||
export * from "./utils/Duration"
|
|
||||||
export { SearchParams } from "./db"
|
export { SearchParams } from "./db"
|
||||||
// Add context to tenancy for backwards compatibility
|
// Add context to tenancy for backwards compatibility
|
||||||
// only do this for external usages to prevent internal
|
// only do this for external usages to prevent internal
|
||||||
|
|
|
@ -18,8 +18,12 @@ export const ObjectStoreBuckets = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const bbTmp = join(tmpdir(), ".budibase")
|
const bbTmp = join(tmpdir(), ".budibase")
|
||||||
if (!fs.existsSync(bbTmp)) {
|
try {
|
||||||
fs.mkdirSync(bbTmp)
|
fs.mkdirSync(bbTmp)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.code !== "EEXIST") {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function budibaseTempDir() {
|
export function budibaseTempDir() {
|
||||||
|
|
|
@ -36,7 +36,7 @@ class InMemoryQueue {
|
||||||
* @param opts This is not used by the in memory queue as there is no real use
|
* @param opts This is not used by the in memory queue as there is no real use
|
||||||
* case when in memory, but is the same API as Bull
|
* case when in memory, but is the same API as Bull
|
||||||
*/
|
*/
|
||||||
constructor(name: string, opts?: any) {
|
constructor(name: string, opts = null) {
|
||||||
this._name = name
|
this._name = name
|
||||||
this._opts = opts
|
this._opts = opts
|
||||||
this._messages = []
|
this._messages = []
|
||||||
|
|
|
@ -2,18 +2,11 @@ import env from "../environment"
|
||||||
import { getRedisOptions } from "../redis/utils"
|
import { getRedisOptions } from "../redis/utils"
|
||||||
import { JobQueue } from "./constants"
|
import { JobQueue } from "./constants"
|
||||||
import InMemoryQueue from "./inMemoryQueue"
|
import InMemoryQueue from "./inMemoryQueue"
|
||||||
import BullQueue, { QueueOptions } from "bull"
|
import BullQueue from "bull"
|
||||||
import { addListeners, StalledFn } from "./listeners"
|
import { addListeners, StalledFn } from "./listeners"
|
||||||
import { Duration } from "../utils"
|
|
||||||
import * as timers from "../timers"
|
import * as timers from "../timers"
|
||||||
import * as Redis from "ioredis"
|
|
||||||
|
|
||||||
// the queue lock is held for 5 minutes
|
const CLEANUP_PERIOD_MS = 60 * 1000
|
||||||
const QUEUE_LOCK_MS = Duration.fromMinutes(5).toMs()
|
|
||||||
// queue lock is refreshed every 30 seconds
|
|
||||||
const QUEUE_LOCK_RENEW_INTERNAL_MS = Duration.fromSeconds(30).toMs()
|
|
||||||
// cleanup the queue every 60 seconds
|
|
||||||
const CLEANUP_PERIOD_MS = Duration.fromSeconds(60).toMs()
|
|
||||||
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
|
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
|
||||||
let cleanupInterval: NodeJS.Timeout
|
let cleanupInterval: NodeJS.Timeout
|
||||||
|
|
||||||
|
@ -28,14 +21,7 @@ export function createQueue<T>(
|
||||||
opts: { removeStalledCb?: StalledFn } = {}
|
opts: { removeStalledCb?: StalledFn } = {}
|
||||||
): BullQueue.Queue<T> {
|
): BullQueue.Queue<T> {
|
||||||
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
|
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
|
||||||
const queueConfig: QueueOptions = {
|
const queueConfig: any = redisProtocolUrl || { redis: redisOpts }
|
||||||
redis: redisProtocolUrl! || (redisOpts as Redis.RedisOptions),
|
|
||||||
settings: {
|
|
||||||
maxStalledCount: 0,
|
|
||||||
lockDuration: QUEUE_LOCK_MS,
|
|
||||||
lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
let queue: any
|
let queue: any
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
queue = new BullQueue(jobQueue, queueConfig)
|
queue = new BullQueue(jobQueue, queueConfig)
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
export enum DurationType {
|
|
||||||
MILLISECONDS = "milliseconds",
|
|
||||||
SECONDS = "seconds",
|
|
||||||
MINUTES = "minutes",
|
|
||||||
HOURS = "hours",
|
|
||||||
DAYS = "days",
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversion: Record<DurationType, number> = {
|
|
||||||
milliseconds: 1,
|
|
||||||
seconds: 1000,
|
|
||||||
minutes: 60 * 1000,
|
|
||||||
hours: 60 * 60 * 1000,
|
|
||||||
days: 24 * 60 * 60 * 1000,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Duration {
|
|
||||||
static convert(from: DurationType, to: DurationType, duration: number) {
|
|
||||||
const milliseconds = duration * conversion[from]
|
|
||||||
return milliseconds / conversion[to]
|
|
||||||
}
|
|
||||||
|
|
||||||
static from(from: DurationType, duration: number) {
|
|
||||||
return {
|
|
||||||
to: (to: DurationType) => {
|
|
||||||
return Duration.convert(from, to, duration)
|
|
||||||
},
|
|
||||||
toMs: () => {
|
|
||||||
return Duration.convert(from, DurationType.MILLISECONDS, duration)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromSeconds(duration: number) {
|
|
||||||
return Duration.from(DurationType.SECONDS, duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromMinutes(duration: number) {
|
|
||||||
return Duration.from(DurationType.MINUTES, duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromHours(duration: number) {
|
|
||||||
return Duration.from(DurationType.HOURS, duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromDays(duration: number) {
|
|
||||||
return Duration.from(DurationType.DAYS, duration)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from "./hashing"
|
export * from "./hashing"
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
export * from "./stringUtils"
|
export * from "./stringUtils"
|
||||||
export * from "./Duration"
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { Duration, DurationType } from "../Duration"
|
|
||||||
|
|
||||||
describe("duration", () => {
|
|
||||||
it("should convert minutes to milliseconds", () => {
|
|
||||||
expect(Duration.fromMinutes(5).toMs()).toBe(300000)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should convert seconds to milliseconds", () => {
|
|
||||||
expect(Duration.fromSeconds(30).toMs()).toBe(30000)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should convert days to milliseconds", () => {
|
|
||||||
expect(Duration.fromDays(1).toMs()).toBe(86400000)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should convert minutes to days", () => {
|
|
||||||
expect(Duration.fromMinutes(1440).to(DurationType.DAYS)).toBe(1)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -6,3 +6,4 @@ release/
|
||||||
dist/
|
dist/
|
||||||
routify
|
routify
|
||||||
.routify/
|
.routify/
|
||||||
|
svelte.config.js
|
|
@ -9,13 +9,7 @@
|
||||||
import TestDataModal from "./TestDataModal.svelte"
|
import TestDataModal from "./TestDataModal.svelte"
|
||||||
import { flip } from "svelte/animate"
|
import { flip } from "svelte/animate"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
import {
|
import { Icon, notifications, Modal } from "@budibase/bbui"
|
||||||
Heading,
|
|
||||||
Icon,
|
|
||||||
ActionButton,
|
|
||||||
notifications,
|
|
||||||
Modal,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { ActionStepID } from "constants/backend/automations"
|
import { ActionStepID } from "constants/backend/automations"
|
||||||
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||||
|
|
||||||
|
@ -23,9 +17,8 @@
|
||||||
|
|
||||||
let testDataModal
|
let testDataModal
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
let scrolling = false
|
||||||
$: blocks = getBlocks(automation)
|
$: blocks = getBlocks(automation).filter(x => x.stepId !== ActionStepID.LOOP)
|
||||||
|
|
||||||
const getBlocks = automation => {
|
const getBlocks = automation => {
|
||||||
let blocks = []
|
let blocks = []
|
||||||
if (automation.definition.trigger) {
|
if (automation.definition.trigger) {
|
||||||
|
@ -35,58 +28,72 @@
|
||||||
return blocks
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAutomation() {
|
const deleteAutomation = async () => {
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.delete($selectedAutomation)
|
await automationStore.actions.delete($selectedAutomation)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error deleting automation")
|
notifications.error("Error deleting automation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleScroll = e => {
|
||||||
|
if (e.target.scrollTop >= 30) {
|
||||||
|
scrolling = true
|
||||||
|
} else if (e.target.scrollTop) {
|
||||||
|
// Set scrolling back to false if scrolled back to less than 100px
|
||||||
|
scrolling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="canvas">
|
<div class="header" class:scrolling>
|
||||||
<div class="header">
|
<div class="header-left">
|
||||||
<Heading size="S">{automation.name}</Heading>
|
<UndoRedoControl store={automationHistoryStore} />
|
||||||
<div class="controls">
|
</div>
|
||||||
<UndoRedoControl store={automationHistoryStore} />
|
<div class="controls">
|
||||||
|
<div class="buttons">
|
||||||
|
<Icon hoverable size="M" name="Play" />
|
||||||
|
<div
|
||||||
|
on:click={() => {
|
||||||
|
testDataModal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Run test
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
<Icon
|
<Icon
|
||||||
on:click={confirmDeleteDialog.show}
|
disabled={!$automationStore.testResults}
|
||||||
hoverable
|
hoverable
|
||||||
size="M"
|
size="M"
|
||||||
name="DeleteOutline"
|
name="Multiple"
|
||||||
/>
|
/>
|
||||||
<div class="buttons">
|
<div
|
||||||
<ActionButton
|
class:disabled={!$automationStore.testResults}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
testDataModal.show()
|
$automationStore.showTestPanel = true
|
||||||
}}
|
}}
|
||||||
icon="MultipleCheck"
|
>
|
||||||
size="M">Run test</ActionButton
|
Test details
|
||||||
>
|
|
||||||
<ActionButton
|
|
||||||
disabled={!$automationStore.testResults}
|
|
||||||
on:click={() => {
|
|
||||||
$automationStore.showTestPanel = true
|
|
||||||
}}
|
|
||||||
size="M">Test Details</ActionButton
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="canvas" on:scroll={handleScroll}>
|
||||||
{#each blocks as block, idx (block.id)}
|
<div class="content">
|
||||||
<div
|
{#each blocks as block, idx (block.id)}
|
||||||
class="block"
|
<div
|
||||||
animate:flip={{ duration: 500 }}
|
class="block"
|
||||||
in:fly={{ x: 500, duration: 500 }}
|
animate:flip={{ duration: 500 }}
|
||||||
out:fly|local={{ x: 500, duration: 500 }}
|
in:fly={{ x: 500, duration: 500 }}
|
||||||
>
|
out:fly|local={{ x: 500, duration: 500 }}
|
||||||
{#if block.stepId !== ActionStepID.LOOP}
|
>
|
||||||
<FlowItem {testDataModal} {block} {idx} />
|
{#if block.stepId !== ActionStepID.LOOP}
|
||||||
{/if}
|
<FlowItem {testDataModal} {block} {idx} />
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
|
@ -106,6 +113,12 @@
|
||||||
<style>
|
<style>
|
||||||
.canvas {
|
.canvas {
|
||||||
padding: var(--spacing-l) var(--spacing-xl);
|
padding: var(--spacing-l) var(--spacing-xl);
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left :global(div) {
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
/* Fix for firefox not respecting bottom padding in scrolling containers */
|
/* Fix for firefox not respecting bottom padding in scrolling containers */
|
||||||
.canvas > *:last-child {
|
.canvas > *:last-child {
|
||||||
|
@ -120,23 +133,45 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: inline-block;
|
flex-grow: 1;
|
||||||
text-align: left;
|
padding: 23px 23px 80px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header.scrolling {
|
||||||
|
background: var(--background);
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
border-left: var(--border-light);
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-left: var(--spacing-l);
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
flex: 0 0 48px;
|
||||||
|
padding-right: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
.controls,
|
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.buttons {
|
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--spectrum-global-color-gray-500) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,21 +7,17 @@
|
||||||
Detail,
|
Detail,
|
||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
ActionButton,
|
|
||||||
notifications,
|
notifications,
|
||||||
Label,
|
Label,
|
||||||
|
AbsTooltip,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import ActionModal from "./ActionModal.svelte"
|
import ActionModal from "./ActionModal.svelte"
|
||||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||||
import {
|
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
|
||||||
ActionStepID,
|
import { permissions } from "stores/backend"
|
||||||
TriggerStepID,
|
|
||||||
Features,
|
|
||||||
} from "constants/backend/automations"
|
|
||||||
import { permissions } from "stores/builder"
|
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testDataModal
|
export let testDataModal
|
||||||
|
@ -86,7 +82,7 @@
|
||||||
if (loopBlock) {
|
if (loopBlock) {
|
||||||
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||||
}
|
}
|
||||||
await automationStore.actions.deleteAutomationBlock(block)
|
await automationStore.actions.deleteAutomationBlock(block, blockIdx)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving automation")
|
notifications.error("Error saving automation")
|
||||||
}
|
}
|
||||||
|
@ -129,6 +125,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="blockTitle">
|
<div class="blockTitle">
|
||||||
|
<AbsTooltip type="negative" text="Remove looping">
|
||||||
|
<Icon on:click={removeLooping} hoverable name="DeleteOutline" />
|
||||||
|
</AbsTooltip>
|
||||||
|
|
||||||
<div style="margin-left: 10px;" on:click={() => {}}>
|
<div style="margin-left: 10px;" on:click={() => {}}>
|
||||||
<Icon hoverable name={showLooping ? "ChevronDown" : "ChevronUp"} />
|
<Icon hoverable name={showLooping ? "ChevronDown" : "ChevronUp"} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -139,9 +139,6 @@
|
||||||
<Divider noMargin />
|
<Divider noMargin />
|
||||||
{#if !showLooping}
|
{#if !showLooping}
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<div class="block-options">
|
|
||||||
<ActionButton on:click={() => removeLooping()} icon="DeleteOutline" />
|
|
||||||
</div>
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<AutomationBlockSetup
|
<AutomationBlockSetup
|
||||||
schemaProperties={Object.entries(
|
schemaProperties={Object.entries(
|
||||||
|
@ -162,31 +159,19 @@
|
||||||
{block}
|
{block}
|
||||||
{testDataModal}
|
{testDataModal}
|
||||||
{idx}
|
{idx}
|
||||||
|
{addLooping}
|
||||||
|
{deleteStep}
|
||||||
on:toggle={() => (open = !open)}
|
on:toggle={() => (open = !open)}
|
||||||
/>
|
/>
|
||||||
{#if open}
|
{#if open}
|
||||||
<Divider noMargin />
|
<Divider noMargin />
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
{#if !isTrigger}
|
|
||||||
<div>
|
|
||||||
<div class="block-options">
|
|
||||||
{#if !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)}
|
|
||||||
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
|
||||||
Add Looping
|
|
||||||
</ActionButton>
|
|
||||||
{/if}
|
|
||||||
<ActionButton
|
|
||||||
on:click={() => deleteStep()}
|
|
||||||
icon="DeleteOutline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if isAppAction}
|
{#if isAppAction}
|
||||||
<Label>Role</Label>
|
<div>
|
||||||
<RoleSelect bind:value={role} />
|
<Label>Role</Label>
|
||||||
|
<RoleSelect bind:value={role} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<AutomationBlockSetup
|
<AutomationBlockSetup
|
||||||
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
||||||
|
@ -270,5 +255,6 @@
|
||||||
.blockTitle {
|
.blockTitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { automationStore } from "stores/builder"
|
import { automationStore, selectedAutomation } from "stores/builder"
|
||||||
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui"
|
import { Icon, Body, AbsTooltip, StatusLight } from "@budibase/bbui"
|
||||||
import { externalActions } from "./ExternalActions"
|
import { externalActions } from "./ExternalActions"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { Features } from "constants/backend/automations"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let open
|
export let open
|
||||||
|
@ -10,9 +11,20 @@
|
||||||
export let testResult
|
export let testResult
|
||||||
export let isTrigger
|
export let isTrigger
|
||||||
export let idx
|
export let idx
|
||||||
|
export let addLooping
|
||||||
|
export let deleteStep
|
||||||
|
|
||||||
|
let validRegex = /^[A-Za-z0-9_\s]+$/
|
||||||
|
let typing = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
$: stepNames = $selectedAutomation.definition.stepNames
|
||||||
|
$: automationName = stepNames?.[block.id] || block?.name || ""
|
||||||
|
$: automationNameError = getAutomationNameError(automationName)
|
||||||
|
$: status = updateStatus(testResult, isTrigger)
|
||||||
|
$: isHeaderTrigger = isTrigger || block.type === "TRIGGER"
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (!testResult) {
|
if (!testResult) {
|
||||||
testResult = $automationStore.testResults?.steps?.filter(step =>
|
testResult = $automationStore.testResults?.steps?.filter(step =>
|
||||||
|
@ -20,8 +32,9 @@
|
||||||
)?.[0]
|
)?.[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: isTrigger = isTrigger || block.type === "TRIGGER"
|
$: loopBlock = $selectedAutomation.definition.steps.find(
|
||||||
$: status = updateStatus(testResult, isTrigger)
|
x => x.blockToLoop === block?.id
|
||||||
|
)
|
||||||
|
|
||||||
async function onSelect(block) {
|
async function onSelect(block) {
|
||||||
await automationStore.update(state => {
|
await automationStore.update(state => {
|
||||||
|
@ -43,10 +56,49 @@
|
||||||
return { negative: true, message: "Error" }
|
return { negative: true, message: "Error" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAutomationNameError = name => {
|
||||||
|
if (stepNames) {
|
||||||
|
for (const [key, value] of Object.entries(stepNames)) {
|
||||||
|
if (name === value && key !== block.id) {
|
||||||
|
return "This name already exists, please enter a unique name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name !== block.name && name?.length > 0) {
|
||||||
|
let invalidRoleName = !validRegex.test(name)
|
||||||
|
if (invalidRoleName) {
|
||||||
|
return "Please enter a role name consisting of only alphanumeric symbols and underscores"
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTyping = async () => {
|
||||||
|
typing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveName = async () => {
|
||||||
|
if (automationNameError || block.name === automationName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (automationName.length === 0) {
|
||||||
|
await automationStore.actions.deleteAutomationName(block.id)
|
||||||
|
} else {
|
||||||
|
await automationStore.actions.saveAutomationName(block.id, automationName)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="blockSection">
|
<div
|
||||||
<div on:click={() => dispatch("toggle")} class="splitHeader">
|
class:typing={typing && !automationNameError}
|
||||||
|
class:typing-error={automationNameError}
|
||||||
|
class="blockSection"
|
||||||
|
>
|
||||||
|
<div class="splitHeader">
|
||||||
<div class="center-items">
|
<div class="center-items">
|
||||||
{#if externalActions[block.stepId]}
|
{#if externalActions[block.stepId]}
|
||||||
<img
|
<img
|
||||||
|
@ -67,40 +119,104 @@
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="iconAlign">
|
<div class="iconAlign">
|
||||||
{#if isTrigger}
|
{#if isHeaderTrigger}
|
||||||
<Body size="XS"><b>Trigger</b></Body>
|
<Body size="XS"><b>Trigger</b></Body>
|
||||||
<Body size="XS">When this happens:</Body>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Body size="XS"><b>Step {idx}</b></Body>
|
<div style="margin-left: 2px;">
|
||||||
<Body size="XS">Do this:</Body>
|
<Body size="XS"><b>Step {idx}</b></Body>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
<input
|
||||||
|
placeholder="Enter some text"
|
||||||
|
name="name"
|
||||||
|
autocomplete="off"
|
||||||
|
value={automationName}
|
||||||
|
on:input={e => {
|
||||||
|
automationName = e.target.value.trim()
|
||||||
|
}}
|
||||||
|
on:click={startTyping}
|
||||||
|
on:blur={async () => {
|
||||||
|
typing = false
|
||||||
|
if (automationNameError) {
|
||||||
|
automationName = stepNames[block.id] || block?.name
|
||||||
|
} else {
|
||||||
|
await saveName()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="blockTitle">
|
<div class="blockTitle">
|
||||||
{#if showTestStatus && testResult}
|
{#if showTestStatus && testResult}
|
||||||
<div style="float: right;">
|
<div class="status-container">
|
||||||
<StatusLight
|
<div style="float:right;">
|
||||||
positive={status?.positive}
|
<StatusLight
|
||||||
yellow={status?.yellow}
|
positive={status?.positive}
|
||||||
negative={status?.negative}
|
yellow={status?.yellow}
|
||||||
><Body size="XS">{status?.message}</Body></StatusLight
|
negative={status?.negative}
|
||||||
>
|
>
|
||||||
|
<Body size="XS">{status?.message}</Body>
|
||||||
|
</StatusLight>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
on:click={() => dispatch("toggle")}
|
||||||
|
hoverable
|
||||||
|
name={open ? "ChevronUp" : "ChevronDown"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
style="margin-left: 10px; margin-bottom: var(--spacing-xs);"
|
class="context-actions"
|
||||||
|
class:hide-context-actions={typing}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
onSelect(block)
|
onSelect(block)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon hoverable name={open ? "ChevronUp" : "ChevronDown"} />
|
{#if !showTestStatus}
|
||||||
|
{#if !isHeaderTrigger && !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)}
|
||||||
|
<AbsTooltip type="info" text="Add looping">
|
||||||
|
<Icon on:click={addLooping} hoverable name="RotateCW" />
|
||||||
|
</AbsTooltip>
|
||||||
|
{/if}
|
||||||
|
<AbsTooltip type="negative" text="Delete step">
|
||||||
|
<Icon on:click={deleteStep} hoverable name="DeleteOutline" />
|
||||||
|
</AbsTooltip>
|
||||||
|
{/if}
|
||||||
|
{#if !showTestStatus}
|
||||||
|
<Icon
|
||||||
|
on:click={() => dispatch("toggle")}
|
||||||
|
hoverable
|
||||||
|
name={open ? "ChevronUp" : "ChevronDown"}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if automationNameError}
|
||||||
|
<div class="error-container">
|
||||||
|
<AbsTooltip type="negative" text={automationNameError}>
|
||||||
|
<div class="error-icon">
|
||||||
|
<Icon size="S" name="Alert" />
|
||||||
|
</div>
|
||||||
|
</AbsTooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.status-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
/* You can also add padding or margin to adjust the spacing between the text and the chevron if needed. */
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
.center-items {
|
.center-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -117,10 +233,55 @@
|
||||||
|
|
||||||
.blockSection {
|
.blockSection {
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blockTitle {
|
.blockTitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
}
|
||||||
|
|
||||||
|
.hide-context-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: var(--ink);
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-size: var(--spectrum-alias-font-size-default);
|
||||||
|
width: 230px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide arrows for number fields */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing {
|
||||||
|
border: 1px solid var(--spectrum-global-color-static-blue-500);
|
||||||
|
border-radius: 4px 4px 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-error {
|
||||||
|
border: 1px solid var(--spectrum-global-color-static-red-500);
|
||||||
|
border-radius: 4px 4px 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon :global(.spectrum-Icon) {
|
||||||
|
fill: var(--spectrum-global-color-red-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Add test data"
|
title="Add test data"
|
||||||
confirmText="Test"
|
confirmText="Test"
|
||||||
|
size="M"
|
||||||
showConfirmButton={true}
|
showConfirmButton={true}
|
||||||
disabled={isError}
|
disabled={isError}
|
||||||
onConfirm={testAutomation}
|
onConfirm={testAutomation}
|
||||||
|
|
|
@ -57,7 +57,6 @@
|
||||||
let fillWidth = true
|
let fillWidth = true
|
||||||
let inputData
|
let inputData
|
||||||
let codeBindingOpen = false
|
let codeBindingOpen = false
|
||||||
|
|
||||||
$: filters = lookForFilters(schemaProperties) || []
|
$: filters = lookForFilters(schemaProperties) || []
|
||||||
$: tempFilters = filters
|
$: tempFilters = filters
|
||||||
$: stepId = block.stepId
|
$: stepId = block.stepId
|
||||||
|
@ -154,7 +153,7 @@
|
||||||
}
|
}
|
||||||
let blockIdx = allSteps.findIndex(step => step.id === block.id)
|
let blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||||
|
|
||||||
// Extract all outputs from all previous steps as available bindins
|
// Extract all outputs from all previous steps as available bindingsx§x
|
||||||
let bindings = []
|
let bindings = []
|
||||||
let loopBlockCount = 0
|
let loopBlockCount = 0
|
||||||
for (let idx = 0; idx < blockIdx; idx++) {
|
for (let idx = 0; idx < blockIdx; idx++) {
|
||||||
|
@ -182,20 +181,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const outputs = Object.entries(schema)
|
const outputs = Object.entries(schema)
|
||||||
|
|
||||||
let bindingIcon = ""
|
let bindingIcon = ""
|
||||||
let bindindingRank = 0
|
let bindingRank = 0
|
||||||
|
|
||||||
if (idx === 0) {
|
if (idx === 0) {
|
||||||
bindingIcon = automation.trigger.icon
|
bindingIcon = automation.trigger.icon
|
||||||
} else if (isLoopBlock) {
|
} else if (isLoopBlock) {
|
||||||
bindingIcon = "Reuse"
|
bindingIcon = "Reuse"
|
||||||
bindindingRank = idx + 1
|
bindingRank = idx + 1
|
||||||
} else {
|
} else {
|
||||||
bindingIcon = allSteps[idx].icon
|
bindingIcon = allSteps[idx].icon
|
||||||
bindindingRank = idx - loopBlockCount
|
bindingRank = idx - loopBlockCount
|
||||||
}
|
}
|
||||||
|
let bindingName =
|
||||||
|
automation.stepNames?.[allSteps[idx - loopBlockCount].id]
|
||||||
bindings = bindings.concat(
|
bindings = bindings.concat(
|
||||||
outputs.map(([name, value]) => {
|
outputs.map(([name, value]) => {
|
||||||
let runtimeName = isLoopBlock
|
let runtimeName = isLoopBlock
|
||||||
|
@ -204,14 +202,20 @@
|
||||||
? `steps[${idx - loopBlockCount}].${name}`
|
? `steps[${idx - loopBlockCount}].${name}`
|
||||||
: `steps.${idx - loopBlockCount}.${name}`
|
: `steps.${idx - loopBlockCount}.${name}`
|
||||||
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
||||||
const categoryName =
|
|
||||||
idx === 0
|
let categoryName
|
||||||
? "Trigger outputs"
|
if (idx === 0) {
|
||||||
: isLoopBlock
|
categoryName = "Trigger outputs"
|
||||||
? "Loop Outputs"
|
} else if (isLoopBlock) {
|
||||||
: `Step ${idx - loopBlockCount} outputs`
|
categoryName = "Loop Outputs"
|
||||||
|
} else if (bindingName) {
|
||||||
|
categoryName = `${bindingName} outputs`
|
||||||
|
} else {
|
||||||
|
categoryName = `Step ${idx - loopBlockCount} outputs`
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
readableBinding: runtime,
|
readableBinding: bindingName ? `${bindingName}.${name}` : runtime,
|
||||||
runtimeBinding: runtime,
|
runtimeBinding: runtime,
|
||||||
type: value.type,
|
type: value.type,
|
||||||
description: value.description,
|
description: value.description,
|
||||||
|
@ -220,7 +224,7 @@
|
||||||
display: {
|
display: {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
name: name,
|
name: name,
|
||||||
rank: bindindingRank,
|
rank: bindingRank,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -276,6 +280,16 @@
|
||||||
return !dependsOn || !!inputData[dependsOn]
|
return !dependsOn || !!inputData[dependsOn]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldRenderField(value) {
|
||||||
|
return (
|
||||||
|
value.customType !== "row" &&
|
||||||
|
value.customType !== "code" &&
|
||||||
|
value.customType !== "queryParams" &&
|
||||||
|
value.customType !== "cron" &&
|
||||||
|
value.customType !== "triggerSchema"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await environment.loadVariables()
|
await environment.loadVariables()
|
||||||
|
@ -288,245 +302,248 @@
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each schemaProperties as [key, value]}
|
{#each schemaProperties as [key, value]}
|
||||||
{#if canShowField(key, value)}
|
{#if canShowField(key, value)}
|
||||||
<div class="block-field">
|
<div class:block-field={shouldRenderField(value)}>
|
||||||
{#if key !== "fields" && value.type !== "boolean"}
|
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
|
||||||
<Label
|
<Label
|
||||||
tooltip={value.title === "Binding / Value"
|
tooltip={value.title === "Binding / Value"
|
||||||
? "If using the String input type, please use a comma or newline separated string"
|
? "If using the String input type, please use a comma or newline separated string"
|
||||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
<div class:field-width={shouldRenderField(value)}>
|
||||||
<Select
|
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||||
on:change={e => onChange(e, key)}
|
<Select
|
||||||
value={inputData[key]}
|
|
||||||
placeholder={false}
|
|
||||||
options={value.enum}
|
|
||||||
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 => {
|
|
||||||
onChange(e, key)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else if value.type === "boolean"}
|
|
||||||
<div style="margin-top: 10px">
|
|
||||||
<Checkbox
|
|
||||||
text={value.title}
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else if value.type === "date"}
|
|
||||||
<DrawerBindableSlot
|
|
||||||
fillWidth
|
|
||||||
title={value.title}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type={"date"}
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
{bindings}
|
|
||||||
allowJS={true}
|
|
||||||
updateOnChange={false}
|
|
||||||
drawerLeft="260px"
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
placeholder={false}
|
||||||
|
options={value.enum}
|
||||||
|
getOptionLabel={(x, idx) =>
|
||||||
|
value.pretty ? value.pretty[idx] : x}
|
||||||
/>
|
/>
|
||||||
</DrawerBindableSlot>
|
{:else if value.type === "json"}
|
||||||
{:else if value.customType === "column"}
|
<Editor
|
||||||
<Select
|
editorHeight="250"
|
||||||
on:change={e => onChange(e, key)}
|
editorWidth="448"
|
||||||
value={inputData[key]}
|
mode="json"
|
||||||
options={Object.keys(table?.schema || {})}
|
value={inputData[key]?.value}
|
||||||
/>
|
on:change={e => {
|
||||||
{:else if value.customType === "filters"}
|
onChange(e, key)
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
}}
|
||||||
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
|
|
||||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<FilterDrawer
|
|
||||||
slot="body"
|
|
||||||
{filters}
|
|
||||||
{bindings}
|
|
||||||
{schemaFields}
|
|
||||||
datasource={{ type: "table", tableId }}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
fillWidth
|
|
||||||
on:change={e => (tempFilters = e.detail)}
|
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
{:else if value.type === "boolean"}
|
||||||
{:else if value.customType === "password"}
|
<div style="margin-top: 10px">
|
||||||
<Input
|
<Checkbox
|
||||||
type="password"
|
text={value.title}
|
||||||
on:change={e => onChange(e, key)}
|
value={inputData[key]}
|
||||||
value={inputData[key]}
|
on:change={e => onChange(e, key)}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "email"}
|
</div>
|
||||||
{#if isTestModal}
|
{:else if value.type === "date"}
|
||||||
<ModalBindableInput
|
<DrawerBindableSlot
|
||||||
title={value.title}
|
|
||||||
value={inputData[key]}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type="email"
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
{bindings}
|
|
||||||
fillWidth
|
|
||||||
updateOnChange={false}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<DrawerBindableInput
|
|
||||||
fillWidth
|
fillWidth
|
||||||
title={value.title}
|
title={value.title}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type="email"
|
type={"date"}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
allowJS={false}
|
allowJS={true}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
drawerLeft="260px"
|
drawerLeft="260px"
|
||||||
/>
|
>
|
||||||
{/if}
|
<DatePicker
|
||||||
{:else if value.customType === "query"}
|
value={inputData[key]}
|
||||||
<QuerySelector
|
on:change={e => onChange(e, key)}
|
||||||
on:change={e => onChange(e, key)}
|
/>
|
||||||
value={inputData[key]}
|
</DrawerBindableSlot>
|
||||||
/>
|
{:else if value.customType === "column"}
|
||||||
{:else if value.customType === "cron"}
|
<Select
|
||||||
<CronBuilder
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "queryParams"}
|
|
||||||
<QueryParamSelector
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
{bindings}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "table"}
|
|
||||||
<TableSelector
|
|
||||||
{isTrigger}
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "row"}
|
|
||||||
<RowSelector
|
|
||||||
value={inputData[key]}
|
|
||||||
meta={inputData["meta"] || {}}
|
|
||||||
on:change={e => {
|
|
||||||
if (e.detail?.key) {
|
|
||||||
onChange(e, e.detail.key)
|
|
||||||
} else {
|
|
||||||
onChange(e, key)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{bindings}
|
|
||||||
{isTestModal}
|
|
||||||
{isUpdateRow}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "webhookUrl"}
|
|
||||||
<WebhookDisplay
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "fields"}
|
|
||||||
<FieldSelector
|
|
||||||
{block}
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
{bindings}
|
|
||||||
{isTestModal}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "triggerSchema"}
|
|
||||||
<SchemaSetup
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "code"}
|
|
||||||
<CodeEditorModal>
|
|
||||||
{#if codeMode == EditorModes.JS}
|
|
||||||
<ActionButton
|
|
||||||
on:click={() => (codeBindingOpen = !codeBindingOpen)}
|
|
||||||
quiet
|
|
||||||
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
|
|
||||||
>
|
|
||||||
<Detail size="S">Bindings</Detail>
|
|
||||||
</ActionButton>
|
|
||||||
{#if codeBindingOpen}
|
|
||||||
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
<CodeEditor
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => {
|
|
||||||
// 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}
|
|
||||||
/>
|
|
||||||
<div class="messaging">
|
|
||||||
{#if codeMode == EditorModes.Handlebars}
|
|
||||||
<Icon name="FlashOn" />
|
|
||||||
<div class="messaging-wrap">
|
|
||||||
<div>
|
|
||||||
Add available bindings by typing <strong>
|
|
||||||
}}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</CodeEditorModal>
|
|
||||||
{:else if value.customType === "loopOption"}
|
|
||||||
<Select
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
autoWidth
|
|
||||||
value={inputData[key]}
|
|
||||||
options={["Array", "String"]}
|
|
||||||
defaultValue={"Array"}
|
|
||||||
/>
|
|
||||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
|
||||||
{#if isTestModal}
|
|
||||||
<ModalBindableInput
|
|
||||||
title={value.title}
|
|
||||||
value={inputData[key]}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type={value.customType}
|
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
value={inputData[key]}
|
||||||
updateOnChange={false}
|
options={Object.keys(table?.schema || {})}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else if value.customType === "filters"}
|
||||||
<div class="test">
|
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||||
|
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
|
||||||
|
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<FilterDrawer
|
||||||
|
slot="body"
|
||||||
|
{filters}
|
||||||
|
{bindings}
|
||||||
|
{schemaFields}
|
||||||
|
datasource={{ type: "table", tableId }}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
fillWidth
|
||||||
|
on:change={e => (tempFilters = e.detail)}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
{:else if value.customType === "password"}
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "email"}
|
||||||
|
{#if isTestModal}
|
||||||
|
<ModalBindableInput
|
||||||
|
title={value.title}
|
||||||
|
value={inputData[key]}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type="email"
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
fillWidth
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
fillWidth={true}
|
fillWidth
|
||||||
title={value.title}
|
title={value.title}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={value.customType}
|
type="email"
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
allowJS={false}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
placeholder={value.customType === "queryLimit"
|
|
||||||
? queryLimit
|
|
||||||
: ""}
|
|
||||||
drawerLeft="260px"
|
drawerLeft="260px"
|
||||||
/>
|
/>
|
||||||
</div>
|
{/if}
|
||||||
|
{:else if value.customType === "query"}
|
||||||
|
<QuerySelector
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "cron"}
|
||||||
|
<CronBuilder
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "queryParams"}
|
||||||
|
<QueryParamSelector
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "table"}
|
||||||
|
<TableSelector
|
||||||
|
{isTrigger}
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "row"}
|
||||||
|
<RowSelector
|
||||||
|
value={inputData[key]}
|
||||||
|
meta={inputData["meta"] || {}}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail?.key) {
|
||||||
|
onChange(e, e.detail.key)
|
||||||
|
} else {
|
||||||
|
onChange(e, key)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{bindings}
|
||||||
|
{isTestModal}
|
||||||
|
{isUpdateRow}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "webhookUrl"}
|
||||||
|
<WebhookDisplay
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "fields"}
|
||||||
|
<FieldSelector
|
||||||
|
{block}
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
{isTestModal}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "triggerSchema"}
|
||||||
|
<SchemaSetup
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "code"}
|
||||||
|
<CodeEditorModal>
|
||||||
|
{#if codeMode == EditorModes.JS}
|
||||||
|
<ActionButton
|
||||||
|
on:click={() => (codeBindingOpen = !codeBindingOpen)}
|
||||||
|
quiet
|
||||||
|
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
|
||||||
|
>
|
||||||
|
<Detail size="S">Bindings</Detail>
|
||||||
|
</ActionButton>
|
||||||
|
{#if codeBindingOpen}
|
||||||
|
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
<CodeEditor
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => {
|
||||||
|
// 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}
|
||||||
|
/>
|
||||||
|
<div class="messaging">
|
||||||
|
{#if codeMode == EditorModes.Handlebars}
|
||||||
|
<Icon name="FlashOn" />
|
||||||
|
<div class="messaging-wrap">
|
||||||
|
<div>
|
||||||
|
Add available bindings by typing <strong>
|
||||||
|
}}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</CodeEditorModal>
|
||||||
|
{:else if value.customType === "loopOption"}
|
||||||
|
<Select
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
autoWidth
|
||||||
|
value={inputData[key]}
|
||||||
|
options={["Array", "String"]}
|
||||||
|
defaultValue={"Array"}
|
||||||
|
/>
|
||||||
|
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||||
|
{#if isTestModal}
|
||||||
|
<ModalBindableInput
|
||||||
|
title={value.title}
|
||||||
|
value={inputData[key]}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={value.customType}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="test">
|
||||||
|
<DrawerBindableInput
|
||||||
|
fillWidth={true}
|
||||||
|
title={value.title}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={value.customType}
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
updateOnChange={false}
|
||||||
|
placeholder={value.customType === "queryLimit"
|
||||||
|
? queryLimit
|
||||||
|
: ""}
|
||||||
|
drawerLeft="260px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -540,6 +557,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.field-width {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
.messaging {
|
.messaging {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -554,8 +575,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-field {
|
.block-field {
|
||||||
display: grid;
|
display: flex; /* Use Flexbox */
|
||||||
grid-gap: 5px;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row; /* Arrange label and field side by side */
|
||||||
|
align-items: center; /* Align vertically in the center */
|
||||||
|
gap: 10px; /* Add some space between label and field */
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test :global(.drawer) {
|
.test :global(.drawer) {
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Button primary on:click={show}>Edit Code</Button>
|
<div class="center">
|
||||||
|
<Button primary on:click={show}>Edit Code</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container :global(section > header) {
|
.container :global(section > header) {
|
||||||
|
@ -33,4 +35,9 @@
|
||||||
.container :global(textarea) {
|
.container :global(textarea) {
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { queries } from "stores/builder"
|
import { queries } from "stores/builder"
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
|
@ -27,41 +27,55 @@
|
||||||
$: if (value?.queryId == null) value = { queryId: "" }
|
$: if (value?.queryId == null) value = { queryId: "" }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="block-field">
|
<div class="schema-fields">
|
||||||
<Select
|
<Label>Query</Label>
|
||||||
label="Query"
|
<div class="field-width">
|
||||||
on:change={onChangeQuery}
|
<Select
|
||||||
value={value.queryId}
|
on:change={onChangeQuery}
|
||||||
options={$queries.list}
|
value={value.queryId}
|
||||||
getOptionValue={query => query._id}
|
options={$queries.list}
|
||||||
getOptionLabel={query => query.name}
|
getOptionValue={query => query._id}
|
||||||
/>
|
getOptionLabel={query => query.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if parameters.length}
|
{#if parameters.length}
|
||||||
<div class="schema-fields">
|
<div class="schema-fields">
|
||||||
{#each parameters as field}
|
{#each parameters as field}
|
||||||
<DrawerBindableInput
|
<Label>{field.name}</Label>
|
||||||
panel={AutomationBindingPanel}
|
<div class="field-width">
|
||||||
extraThin
|
<DrawerBindableInput
|
||||||
value={value[field.name]}
|
panel={AutomationBindingPanel}
|
||||||
on:change={e => onChange(e, field)}
|
extraThin
|
||||||
label={field.name}
|
value={value[field.name]}
|
||||||
type="string"
|
on:change={e => onChange(e, field)}
|
||||||
{bindings}
|
type="string"
|
||||||
fillWidth={true}
|
{bindings}
|
||||||
updateOnChange={false}
|
fillWidth={true}
|
||||||
/>
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.schema-fields {
|
.field-width {
|
||||||
display: grid;
|
width: 320px;
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.schema-fields {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.schema-fields :global(label) {
|
.schema-fields :global(label) {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { tables } from "stores/builder"
|
import { tables } from "stores/builder"
|
||||||
import { Select, Checkbox } from "@budibase/bbui"
|
import { Select, Checkbox, Label } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
import { TableNames } from "constants"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -99,41 +100,25 @@
|
||||||
$: if (value?.tableId == null) value = { tableId: "" }
|
$: if (value?.tableId == null) value = { tableId: "" }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
<div class="schema-fields">
|
||||||
on:change={onChangeTable}
|
<Label>Table</Label>
|
||||||
value={value.tableId}
|
<div class="field-width">
|
||||||
options={$tables.list}
|
<Select
|
||||||
getOptionLabel={table => table.name}
|
on:change={onChangeTable}
|
||||||
getOptionValue={table => table._id}
|
value={value.tableId}
|
||||||
/>
|
options={$tables.list.filter(table => table._id !== TableNames.USERS)}
|
||||||
|
getOptionLabel={table => table.name}
|
||||||
|
getOptionValue={table => table._id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{#if schemaFields.length}
|
{#if schemaFields.length}
|
||||||
<div class="schema-fields">
|
{#each schemaFields as [field, schema]}
|
||||||
{#each schemaFields as [field, schema]}
|
<div class="schema-fields">
|
||||||
{#if !schema.autocolumn && schema.type !== "attachment"}
|
<Label>{field}</Label>
|
||||||
{#if isTestModal}
|
<div class="field-width">
|
||||||
<RowSelectorTypes
|
{#if !schema.autocolumn && schema.type !== "attachment"}
|
||||||
{isTestModal}
|
{#if isTestModal}
|
||||||
{field}
|
|
||||||
{schema}
|
|
||||||
bindings={parsedBindings}
|
|
||||||
{value}
|
|
||||||
{onChange}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<DrawerBindableSlot
|
|
||||||
fillWidth
|
|
||||||
title={value.title}
|
|
||||||
label={field}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type={schema.type}
|
|
||||||
{schema}
|
|
||||||
value={value[field]}
|
|
||||||
on:change={e => onChange(e, field)}
|
|
||||||
{bindings}
|
|
||||||
allowJS={true}
|
|
||||||
updateOnChange={false}
|
|
||||||
drawerLeft="260px"
|
|
||||||
>
|
|
||||||
<RowSelectorTypes
|
<RowSelectorTypes
|
||||||
{isTestModal}
|
{isTestModal}
|
||||||
{field}
|
{field}
|
||||||
|
@ -142,28 +127,61 @@
|
||||||
{value}
|
{value}
|
||||||
{onChange}
|
{onChange}
|
||||||
/>
|
/>
|
||||||
</DrawerBindableSlot>
|
{:else}
|
||||||
|
<DrawerBindableSlot
|
||||||
|
fillWidth
|
||||||
|
title={value.title}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={schema.type}
|
||||||
|
{schema}
|
||||||
|
value={value[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
{bindings}
|
||||||
|
allowJS={true}
|
||||||
|
updateOnChange={false}
|
||||||
|
drawerLeft="260px"
|
||||||
|
>
|
||||||
|
<RowSelectorTypes
|
||||||
|
{isTestModal}
|
||||||
|
{field}
|
||||||
|
{schema}
|
||||||
|
bindings={parsedBindings}
|
||||||
|
{value}
|
||||||
|
{onChange}
|
||||||
|
/>
|
||||||
|
</DrawerBindableSlot>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
{#if isUpdateRow && schema.type === "link"}
|
{#if isUpdateRow && schema.type === "link"}
|
||||||
<div class="checkbox-field">
|
<div class="checkbox-field">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={meta.fields?.[field]?.clearRelationships}
|
value={meta.fields?.[field]?.clearRelationships}
|
||||||
text={"Clear relationships if empty?"}
|
text={"Clear relationships if empty?"}
|
||||||
size={"S"}
|
size={"S"}
|
||||||
on:change={e => onChangeSetting(e, field)}
|
on:change={e => onChangeSetting(e, field)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.field-width {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
.schema-fields {
|
.schema-fields {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-gap: var(--spacing-s);
|
justify-content: space-between;
|
||||||
margin-top: var(--spacing-s);
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.schema-fields :global(label) {
|
.schema-fields :global(label) {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Select, DatePicker, Multiselect, TextArea } from "@budibase/bbui"
|
||||||
Select,
|
|
||||||
DatePicker,
|
|
||||||
Multiselect,
|
|
||||||
TextArea,
|
|
||||||
Label,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
||||||
|
@ -33,20 +27,14 @@
|
||||||
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
label={field}
|
|
||||||
value={value[field]}
|
value={value[field]}
|
||||||
options={schema.constraints.inclusion}
|
options={schema.constraints.inclusion}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "datetime"}
|
{:else if schema.type === "datetime"}
|
||||||
<DatePicker
|
<DatePicker value={value[field]} on:change={e => onChange(e, field)} />
|
||||||
label={field}
|
|
||||||
value={value[field]}
|
|
||||||
on:change={e => onChange(e, field)}
|
|
||||||
/>
|
|
||||||
{:else if schema.type === "boolean"}
|
{:else if schema.type === "boolean"}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
label={field}
|
|
||||||
value={value[field]}
|
value={value[field]}
|
||||||
options={[
|
options={[
|
||||||
{ label: "True", value: "true" },
|
{ label: "True", value: "true" },
|
||||||
|
@ -56,19 +44,13 @@
|
||||||
{:else if schema.type === "array"}
|
{:else if schema.type === "array"}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
bind:value={value[field]}
|
bind:value={value[field]}
|
||||||
label={field}
|
|
||||||
options={schema.constraints.inclusion}
|
options={schema.constraints.inclusion}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "longform"}
|
{:else if schema.type === "longform"}
|
||||||
<TextArea
|
<TextArea bind:value={value[field]} on:change={e => onChange(e, field)} />
|
||||||
label={field}
|
|
||||||
bind:value={value[field]}
|
|
||||||
on:change={e => onChange(e, field)}
|
|
||||||
/>
|
|
||||||
{:else if schema.type === "json"}
|
{:else if schema.type === "json"}
|
||||||
<span>
|
<span>
|
||||||
<Label>{field}</Label>
|
|
||||||
<Editor
|
<Editor
|
||||||
editorHeight="150"
|
editorHeight="150"
|
||||||
mode="json"
|
mode="json"
|
||||||
|
@ -92,7 +74,6 @@
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
value={value[field]}
|
value={value[field]}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
label={field}
|
|
||||||
type="string"
|
type="string"
|
||||||
bindings={parsedBindings}
|
bindings={parsedBindings}
|
||||||
fillWidth={true}
|
fillWidth={true}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<Select
|
<Select
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
bind:value
|
bind:value
|
||||||
options={filteredTables}
|
options={filteredTables.filter(table => table._id !== TableNames.USERS)}
|
||||||
getOptionLabel={table => table.name}
|
getOptionLabel={table => table.name}
|
||||||
getOptionValue={table => table._id}
|
getOptionValue={table => table._id}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -206,12 +206,12 @@
|
||||||
.text-area-slot-icon {
|
.text-area-slot-icon {
|
||||||
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||||
border-bottom-right-radius: 0px !important;
|
border-bottom-right-radius: 0px !important;
|
||||||
top: 26px !important;
|
top: 1px !important;
|
||||||
}
|
}
|
||||||
.json-slot-icon {
|
.json-slot-icon {
|
||||||
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||||
border-bottom-right-radius: 0px !important;
|
border-bottom-right-radius: 0px !important;
|
||||||
top: 23px !important;
|
top: 1px !important;
|
||||||
right: 0px !important;
|
right: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-l);
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.centered {
|
.centered {
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { API } from "api"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { createHistoryStore } from "stores/builder/history"
|
import { createHistoryStore } from "stores/builder/history"
|
||||||
|
import { selectedAutomation } from "stores/builder"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
const initialAutomationState = {
|
const initialAutomationState = {
|
||||||
automations: [],
|
automations: [],
|
||||||
|
@ -32,6 +34,37 @@ export const createAutomationStore = () => {
|
||||||
return { store, history }
|
return { store, history }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateReferencesInObject = (obj, modifiedIndex, action) => {
|
||||||
|
const regex = /{{\s*steps\.(\d+)\./g
|
||||||
|
for (const key in obj) {
|
||||||
|
if (typeof obj[key] === "string") {
|
||||||
|
let matches
|
||||||
|
while ((matches = regex.exec(obj[key])) !== null) {
|
||||||
|
const referencedStep = parseInt(matches[1])
|
||||||
|
if (action === "add" && referencedStep >= modifiedIndex) {
|
||||||
|
obj[key] = obj[key].replace(
|
||||||
|
`{{ steps.${referencedStep}.`,
|
||||||
|
`{{ steps.${referencedStep + 1}.`
|
||||||
|
)
|
||||||
|
} else if (action === "delete" && referencedStep > modifiedIndex) {
|
||||||
|
obj[key] = obj[key].replace(
|
||||||
|
`{{ steps.${referencedStep}.`,
|
||||||
|
`{{ steps.${referencedStep - 1}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||||
|
updateReferencesInObject(obj[key], modifiedIndex, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStepReferences = (steps, modifiedIndex, action) => {
|
||||||
|
steps.forEach(step => {
|
||||||
|
updateReferencesInObject(step.inputs, modifiedIndex, action)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const automationActions = store => ({
|
const automationActions = store => ({
|
||||||
definitions: async () => {
|
definitions: async () => {
|
||||||
const response = await API.getAutomationDefinitions()
|
const response = await API.getAutomationDefinitions()
|
||||||
|
@ -229,10 +262,40 @@ const automationActions = store => ({
|
||||||
if (!automation) {
|
if (!automation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateStepReferences(newAutomation.definition.steps, blockIdx, "add")
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error("Error adding automation block")
|
||||||
|
}
|
||||||
newAutomation.definition.steps.splice(blockIdx, 0, block)
|
newAutomation.definition.steps.splice(blockIdx, 0, block)
|
||||||
await store.actions.save(newAutomation)
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
deleteAutomationBlock: async block => {
|
saveAutomationName: async (blockId, name) => {
|
||||||
|
const automation = get(selectedAutomation)
|
||||||
|
let newAutomation = cloneDeep(automation)
|
||||||
|
if (!automation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newAutomation.definition.stepNames = {
|
||||||
|
...newAutomation.definition.stepNames,
|
||||||
|
[blockId]: name.trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.actions.save(newAutomation)
|
||||||
|
},
|
||||||
|
deleteAutomationName: async blockId => {
|
||||||
|
const automation = get(selectedAutomation)
|
||||||
|
let newAutomation = cloneDeep(automation)
|
||||||
|
if (!automation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete newAutomation.definition.stepNames[blockId]
|
||||||
|
|
||||||
|
await store.actions.save(newAutomation)
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteAutomationBlock: async (block, blockIdx) => {
|
||||||
const automation = get(selectedAutomation)
|
const automation = get(selectedAutomation)
|
||||||
let newAutomation = cloneDeep(automation)
|
let newAutomation = cloneDeep(automation)
|
||||||
|
|
||||||
|
@ -244,7 +307,14 @@ const automationActions = store => ({
|
||||||
newAutomation.definition.steps = newAutomation.definition.steps.filter(
|
newAutomation.definition.steps = newAutomation.definition.steps.filter(
|
||||||
step => step.id !== block.id
|
step => step.id !== block.id
|
||||||
)
|
)
|
||||||
|
delete newAutomation.definition.stepNames?.[block.id]
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
updateStepReferences(newAutomation.definition.steps, blockIdx, "delete")
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error("Error deleting automation block")
|
||||||
|
}
|
||||||
|
|
||||||
await store.actions.save(newAutomation)
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
replace: async (automationId, automation) => {
|
replace: async (automationId, automation) => {
|
||||||
|
|
|
@ -49,7 +49,12 @@ describe.each([
|
||||||
let table: Table
|
let table: Table
|
||||||
let tableId: string
|
let tableId: string
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(async () => {
|
||||||
|
if (dsProvider) {
|
||||||
|
await dsProvider.stopContainer()
|
||||||
|
}
|
||||||
|
setup.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
|
@ -521,20 +526,17 @@ describe.each([
|
||||||
const rowUsage = await getRowUsage()
|
const rowUsage = await getRowUsage()
|
||||||
const queryUsage = await getQueryUsage()
|
const queryUsage = await getQueryUsage()
|
||||||
|
|
||||||
const res = await config.api.row.patch(table._id!, {
|
const row = await config.api.row.patch(table._id!, {
|
||||||
_id: existing._id!,
|
_id: existing._id!,
|
||||||
_rev: existing._rev!,
|
_rev: existing._rev!,
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
name: "Updated Name",
|
name: "Updated Name",
|
||||||
})
|
})
|
||||||
|
|
||||||
expect((res as any).res.statusMessage).toEqual(
|
expect(row.name).toEqual("Updated Name")
|
||||||
`${table.name} updated successfully.`
|
expect(row.description).toEqual(existing.description)
|
||||||
)
|
|
||||||
expect(res.body.name).toEqual("Updated Name")
|
|
||||||
expect(res.body.description).toEqual(existing.description)
|
|
||||||
|
|
||||||
const savedRow = await loadRow(res.body._id, table._id!)
|
const savedRow = await loadRow(row._id!, table._id!)
|
||||||
|
|
||||||
expect(savedRow.body.description).toEqual(existing.description)
|
expect(savedRow.body.description).toEqual(existing.description)
|
||||||
expect(savedRow.body.name).toEqual("Updated Name")
|
expect(savedRow.body.name).toEqual("Updated Name")
|
||||||
|
|
|
@ -492,6 +492,67 @@ describe("/tables", () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should succeed when the row is created from the other side of the relationship", async () => {
|
||||||
|
// We found a bug just after releasing this feature where if the row was created from the
|
||||||
|
// users table, not the table linking to it, the migration would succeed but lose the data.
|
||||||
|
// This happened because the order of the documents in the link was reversed.
|
||||||
|
const table = await config.api.table.create({
|
||||||
|
name: "table",
|
||||||
|
type: "table",
|
||||||
|
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
sourceType: TableSourceType.INTERNAL,
|
||||||
|
schema: {
|
||||||
|
"user relationship": {
|
||||||
|
type: FieldType.LINK,
|
||||||
|
fieldName: "test",
|
||||||
|
name: "user relationship",
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
relationshipType: RelationshipType.MANY_TO_ONE,
|
||||||
|
tableId: InternalTable.USER_METADATA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let testRow = await config.api.row.save(table._id!, {})
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
users.map(u =>
|
||||||
|
config.api.row.patch(InternalTable.USER_METADATA, {
|
||||||
|
tableId: InternalTable.USER_METADATA,
|
||||||
|
_rev: u._rev!,
|
||||||
|
_id: u._id!,
|
||||||
|
test: [testRow],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await config.api.table.migrate(table._id!, {
|
||||||
|
oldColumn: table.schema["user relationship"],
|
||||||
|
newColumn: {
|
||||||
|
name: "user column",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: FieldSubtype.USERS,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const migratedTable = await config.api.table.get(table._id!)
|
||||||
|
expect(migratedTable.schema["user column"]).toBeDefined()
|
||||||
|
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||||
|
|
||||||
|
const resp = await config.api.row.get(table._id!, testRow._id!)
|
||||||
|
const migratedRow = resp.body as Row
|
||||||
|
|
||||||
|
expect(migratedRow["user column"]).toBeDefined()
|
||||||
|
expect(migratedRow["user relationship"]).not.toBeDefined()
|
||||||
|
expect(migratedRow["user column"]).toHaveLength(3)
|
||||||
|
expect(migratedRow["user column"].map((u: Row) => u._id)).toEqual(
|
||||||
|
expect.arrayContaining(users.map(u => u._id))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("should successfully migrate a many-to-many user relationship to a users column", async () => {
|
it("should successfully migrate a many-to-many user relationship to a users column", async () => {
|
||||||
const table = await config.api.table.create({
|
const table = await config.api.table.create({
|
||||||
name: "table",
|
name: "table",
|
||||||
|
|
|
@ -55,7 +55,13 @@ export class RowAPI extends TestAPI {
|
||||||
.send(row)
|
.send(row)
|
||||||
.set(this.config.defaultHeaders())
|
.set(this.config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(expectStatus)
|
if (resp.status !== expectStatus) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected status ${expectStatus} but got ${
|
||||||
|
resp.status
|
||||||
|
}, body: ${JSON.stringify(resp.body)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
return resp.body as Row
|
return resp.body as Row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,13 +83,20 @@ export class RowAPI extends TestAPI {
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
row: PatchRowRequest,
|
row: PatchRowRequest,
|
||||||
{ expectStatus } = { expectStatus: 200 }
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
) => {
|
): Promise<Row> => {
|
||||||
return this.request
|
let resp = await this.request
|
||||||
.patch(`/api/${sourceId}/rows`)
|
.patch(`/api/${sourceId}/rows`)
|
||||||
.send(row)
|
.send(row)
|
||||||
.set(this.config.defaultHeaders())
|
.set(this.config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(expectStatus)
|
if (resp.status !== expectStatus) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected status ${expectStatus} but got ${
|
||||||
|
resp.status
|
||||||
|
}, body: ${JSON.stringify(resp.body)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return resp.body as Row
|
||||||
}
|
}
|
||||||
|
|
||||||
delete = async (
|
delete = async (
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
export enum FeatureFlag {
|
export enum FeatureFlag {
|
||||||
LICENSING = "LICENSING",
|
LICENSING = "LICENSING",
|
||||||
// Feature IDs in Posthog
|
PER_CREATOR_PER_USER_PRICE = "PER_CREATOR_PER_USER_PRICE",
|
||||||
PER_CREATOR_PER_USER_PRICE = "18873",
|
PER_CREATOR_PER_USER_PRICE_ALERT = "PER_CREATOR_PER_USER_PRICE_ALERT",
|
||||||
PER_CREATOR_PER_USER_PRICE_ALERT = "18530",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TenantFeatureFlags {
|
export interface TenantFeatureFlags {
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
_Describe the problem or feature in addition to a link to the relevant github issues._
|
_Describe the problem or feature in addition to a link to the relevant github issues._
|
||||||
|
|
||||||
Addresses:
|
### Addresses:
|
||||||
|
|
||||||
- `<Enter the Link to the issue(s) this PR addresses>`
|
- `<Enter the Link to the issue(s) this PR addresses>`
|
||||||
- ...more if required
|
- ...more if required
|
||||||
|
|
||||||
## App Export
|
## App Export
|
||||||
|
|
||||||
- If possible, attach an app export file along with your request template to make QA testing easier, with minimal setup.
|
- If possible, attach an app export file along with your request template to make QA testing easier, with minimal setup.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
_If a UI facing feature, a short video of the happy path, and some screenshots of the new functionality._
|
_If a UI facing feature, a short video of the happy path, and some screenshots of the new functionality._
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
if [[ $TARGETARCH == arm* ]] ;
|
if [[ $TARGETARCH == arm* ]] ;
|
||||||
then
|
then
|
||||||
echo "INSTALLING ARM64 MINIO"
|
echo "INSTALLING ARM64 MINIO"
|
||||||
wget wget https://dl.min.io/server/minio/release/linux-arm64/archive/minio.deb -O minio.deb
|
wget https://dl.min.io/server/minio/release/linux-arm64/minio
|
||||||
else
|
else
|
||||||
echo "INSTALLING AMD64 MINIO"
|
echo "INSTALLING AMD64 MINIO"
|
||||||
wget wget https://dl.min.io/server/minio/release/linux-amd64/archive/minio.deb -O minio.deb
|
wget https://dl.min.io/server/minio/release/linux-amd64/minio
|
||||||
fi
|
fi
|
||||||
dpkg -i minio.deb
|
chmod +x minio
|
Loading…
Reference in New Issue