Merge remote-tracking branch 'origin/master' into feature/monolith-js-refactor

This commit is contained in:
Dean 2023-11-06 11:29:10 +00:00
commit 03f90500e5
29 changed files with 838 additions and 548 deletions

View File

@ -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

View File

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

View File

@ -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

View File

@ -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() {

View File

@ -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 = []

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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"

View File

@ -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)
})
})

View File

@ -6,3 +6,4 @@ release/
dist/ dist/
routify routify
.routify/ .routify/
svelte.config.js

View File

@ -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,45 +28,58 @@
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>
<div class="controls">
<UndoRedoControl store={automationHistoryStore} /> <UndoRedoControl store={automationHistoryStore} />
<Icon </div>
on:click={confirmDeleteDialog.show} <div class="controls">
hoverable
size="M"
name="DeleteOutline"
/>
<div class="buttons"> <div class="buttons">
<ActionButton <Icon hoverable size="M" name="Play" />
<div
on:click={() => { on:click={() => {
testDataModal.show() testDataModal.show()
}} }}
icon="MultipleCheck"
size="M">Run test</ActionButton
> >
<ActionButton Run test
</div>
</div>
<div class="buttons">
<Icon
disabled={!$automationStore.testResults} disabled={!$automationStore.testResults}
hoverable
size="M"
name="Multiple"
/>
<div
class:disabled={!$automationStore.testResults}
on:click={() => { on:click={() => {
$automationStore.showTestPanel = true $automationStore.showTestPanel = true
}} }}
size="M">Test Details</ActionButton
> >
Test details
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="canvas" on:scroll={handleScroll}>
<div class="content"> <div class="content">
{#each blocks as block, idx (block.id)} {#each blocks as block, idx (block.id)}
<div <div
@ -88,6 +94,7 @@
</div> </div>
{/each} {/each}
</div> </div>
</div>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
okText="Delete Automation" okText="Delete Automation"
@ -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>

View File

@ -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}
<div>
<Label>Role</Label> <Label>Role</Label>
<RoleSelect bind:value={role} /> <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>

View File

@ -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}
<div style="margin-left: 2px;">
<Body size="XS"><b>Step {idx}</b></Body> <Body size="XS"><b>Step {idx}</b></Body>
<Body size="XS">Do this:</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 class="status-container">
<div style="float:right;"> <div style="float:right;">
<StatusLight <StatusLight
positive={status?.positive} positive={status?.positive}
yellow={status?.yellow} yellow={status?.yellow}
negative={status?.negative} negative={status?.negative}
><Body size="XS">{status?.message}</Body></StatusLight
> >
<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>

View File

@ -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}

View File

@ -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,21 +302,23 @@
<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}
<div class:field-width={shouldRenderField(value)}>
{#if value.type === "string" && value.enum && canShowField(key, value)} {#if value.type === "string" && value.enum && canShowField(key, value)}
<Select <Select
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
value={inputData[key]} value={inputData[key]}
placeholder={false} placeholder={false}
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"} {:else if value.type === "json"}
<Editor <Editor
@ -528,6 +544,7 @@
{/if} {/if}
{/if} {/if}
</div> </div>
</div>
{/if} {/if}
{/each} {/each}
</div> </div>
@ -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) {

View File

@ -23,7 +23,9 @@
</div> </div>
</ModalContent> </ModalContent>
</Modal> </Modal>
<div class="center">
<Button primary on:click={show}>Edit Code</Button> <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>

View File

@ -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,9 +27,10 @@
$: if (value?.queryId == null) value = { queryId: "" } $: if (value?.queryId == null) value = { queryId: "" }
</script> </script>
<div class="block-field"> <div class="schema-fields">
<Label>Query</Label>
<div class="field-width">
<Select <Select
label="Query"
on:change={onChangeQuery} on:change={onChangeQuery}
value={value.queryId} value={value.queryId}
options={$queries.list} options={$queries.list}
@ -37,31 +38,44 @@
getOptionLabel={query => query.name} 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}
<Label>{field.name}</Label>
<div class="field-width">
<DrawerBindableInput <DrawerBindableInput
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
extraThin extraThin
value={value[field.name]} value={value[field.name]}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
label={field.name}
type="string" type="string"
{bindings} {bindings}
fillWidth={true} fillWidth={true}
updateOnChange={false} 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;
} }

View File

@ -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,16 +100,23 @@
$: if (value?.tableId == null) value = { tableId: "" } $: if (value?.tableId == null) value = { tableId: "" }
</script> </script>
<div class="schema-fields">
<Label>Table</Label>
<div class="field-width">
<Select <Select
on:change={onChangeTable} on:change={onChangeTable}
value={value.tableId} value={value.tableId}
options={$tables.list} options={$tables.list.filter(table => table._id !== TableNames.USERS)}
getOptionLabel={table => table.name} getOptionLabel={table => table.name}
getOptionValue={table => table._id} 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">
<Label>{field}</Label>
<div class="field-width">
{#if !schema.autocolumn && schema.type !== "attachment"} {#if !schema.autocolumn && schema.type !== "attachment"}
{#if isTestModal} {#if isTestModal}
<RowSelectorTypes <RowSelectorTypes
@ -123,7 +131,6 @@
<DrawerBindableSlot <DrawerBindableSlot
fillWidth fillWidth
title={value.title} title={value.title}
label={field}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
type={schema.type} type={schema.type}
{schema} {schema}
@ -145,6 +152,7 @@
</DrawerBindableSlot> </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
@ -155,15 +163,25 @@
/> />
</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;

View File

@ -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}

View File

@ -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}
/> />

View File

@ -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;
} }

View File

@ -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 {

View File

@ -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) => {

View File

@ -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")

View File

@ -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",

View File

@ -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 (

View File

@ -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 {

View File

@ -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._

View File

@ -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