Merge remote-tracking branch 'origin/develop' into feature/app-user-onboarding-ux
This commit is contained in:
commit
c135a029f9
67 changed files with 1504 additions and 1013 deletions
77
.github/workflows/release-develop.yml
vendored
77
.github/workflows/release-develop.yml
vendored
|
@ -68,83 +68,6 @@ jobs:
|
||||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
|
|
||||||
deploy-to-release-env:
|
|
||||||
needs: [release-images]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Get the current budibase release version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
|
||||||
with:
|
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
aws-region: eu-west-1
|
|
||||||
|
|
||||||
- name: Pull values.yaml from budibase-infra
|
|
||||||
run: |
|
|
||||||
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
|
||||||
-H 'Accept: application/vnd.github.v3.raw' \
|
|
||||||
-o values.release.yaml \
|
|
||||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-release/values.yaml
|
|
||||||
wc -l values.release.yaml
|
|
||||||
|
|
||||||
- name: Deploy to Release Environment
|
|
||||||
uses: glopezep/helm@v1.7.1
|
|
||||||
with:
|
|
||||||
release: budibase-release
|
|
||||||
namespace: budibase
|
|
||||||
chart: charts/budibase
|
|
||||||
token: ${{ github.token }}
|
|
||||||
helm: helm3
|
|
||||||
values: |
|
|
||||||
globals:
|
|
||||||
appVersion: develop
|
|
||||||
ingress:
|
|
||||||
enabled: true
|
|
||||||
nginx: true
|
|
||||||
value-files: >-
|
|
||||||
[
|
|
||||||
"values.release.yaml"
|
|
||||||
]
|
|
||||||
env:
|
|
||||||
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
|
|
||||||
|
|
||||||
- name: Re roll app-service
|
|
||||||
uses: actions-hub/kubectl@master
|
|
||||||
env:
|
|
||||||
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
|
|
||||||
with:
|
|
||||||
args: rollout restart deployment app-service -n budibase
|
|
||||||
|
|
||||||
- name: Re roll proxy-service
|
|
||||||
uses: actions-hub/kubectl@master
|
|
||||||
env:
|
|
||||||
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
|
|
||||||
with:
|
|
||||||
args: rollout restart deployment proxy-service -n budibase
|
|
||||||
|
|
||||||
- name: Re roll worker-service
|
|
||||||
uses: actions-hub/kubectl@master
|
|
||||||
env:
|
|
||||||
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
|
|
||||||
with:
|
|
||||||
args: rollout restart deployment worker-service -n budibase
|
|
||||||
|
|
||||||
- name: Discord Webhook Action
|
|
||||||
uses: tsickert/discord-webhook@v4.0.0
|
|
||||||
with:
|
|
||||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
|
||||||
content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env."
|
|
||||||
embed-title: ${{ env.RELEASE_VERSION }}
|
|
||||||
|
|
||||||
release-helm-chart:
|
release-helm-chart:
|
||||||
needs: [release-images]
|
needs: [release-images]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
nodejs 14.19.3
|
nodejs 14.19.3
|
||||||
python 3.11.1
|
python 3.10.0
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/nano": "10.1.1",
|
"@budibase/nano": "10.1.1",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||||
"@budibase/types": "2.3.18-alpha.6",
|
"@budibase/types": "2.3.18-alpha.12",
|
||||||
"@shopify/jest-koa-mocks": "5.0.1",
|
"@shopify/jest-koa-mocks": "5.0.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-cloudfront-sign": "2.2.0",
|
"aws-cloudfront-sign": "2.2.0",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.6",
|
"@budibase/string-templates": "2.3.18-alpha.12",
|
||||||
"@spectrum-css/accordion": "3.0.24",
|
"@spectrum-css/accordion": "3.0.24",
|
||||||
"@spectrum-css/actionbutton": "1.0.1",
|
"@spectrum-css/actionbutton": "1.0.1",
|
||||||
"@spectrum-css/actiongroup": "1.0.1",
|
"@spectrum-css/actiongroup": "1.0.1",
|
||||||
|
|
|
@ -67,6 +67,9 @@
|
||||||
color: var(--spectrum-alias-icon-color-selected-hover) !important;
|
color: var(--spectrum-alias-icon-color-selected-hover) !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
svg.hoverable:active {
|
||||||
|
color: var(--spectrum-global-color-blue-400) !important;
|
||||||
|
}
|
||||||
|
|
||||||
svg.disabled {
|
svg.disabled {
|
||||||
color: var(--spectrum-global-color-gray-500) !important;
|
color: var(--spectrum-global-color-gray-500) !important;
|
||||||
|
|
|
@ -57,5 +57,7 @@
|
||||||
--spectrum-semantic-negative-icon-color: #e34850;
|
--spectrum-semantic-negative-icon-color: #e34850;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
border-color: var(--spectrum-global-color-gray-400);
|
||||||
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
label {
|
label {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/modal/dist/index-vars.css"
|
import "@spectrum-css/modal/dist/index-vars.css"
|
||||||
import "@spectrum-css/underlay/dist/index-vars.css"
|
import "@spectrum-css/underlay/dist/index-vars.css"
|
||||||
import { createEventDispatcher, setContext, tick } from "svelte"
|
import { createEventDispatcher, setContext, tick, onMount } from "svelte"
|
||||||
import { fade, fly } from "svelte/transition"
|
import { fade, fly } from "svelte/transition"
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
import Context from "../context"
|
import Context from "../context"
|
||||||
|
@ -62,9 +62,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext(Context.Modal, { show, hide, cancel })
|
setContext(Context.Modal, { show, hide, cancel })
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKey} />
|
onMount(() => {
|
||||||
|
document.addEventListener("keydown", handleKey)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
{#if inline}
|
{#if inline}
|
||||||
{#if visible}
|
{#if visible}
|
||||||
|
|
|
@ -104,6 +104,9 @@ export const deepSet = (obj, key, value) => {
|
||||||
* @param obj the object to clone
|
* @param obj the object to clone
|
||||||
*/
|
*/
|
||||||
export const cloneDeep = obj => {
|
export const cloneDeep = obj => {
|
||||||
|
if (!obj) {
|
||||||
|
return obj
|
||||||
|
}
|
||||||
return JSON.parse(JSON.stringify(obj))
|
return JSON.parse(JSON.stringify(obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -58,10 +58,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.3.18-alpha.6",
|
"@budibase/bbui": "2.3.18-alpha.12",
|
||||||
"@budibase/client": "2.3.18-alpha.6",
|
"@budibase/client": "2.3.18-alpha.12",
|
||||||
"@budibase/frontend-core": "2.3.18-alpha.6",
|
"@budibase/frontend-core": "2.3.18-alpha.12",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.6",
|
"@budibase/string-templates": "2.3.18-alpha.12",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||||
|
@ -72,6 +72,7 @@
|
||||||
"codemirror": "^5.59.0",
|
"codemirror": "^5.59.0",
|
||||||
"dayjs": "^1.11.2",
|
"dayjs": "^1.11.2",
|
||||||
"downloadjs": "1.4.7",
|
"downloadjs": "1.4.7",
|
||||||
|
"fast-json-patch": "^3.1.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"posthog-js": "^1.36.0",
|
"posthog-js": "^1.36.0",
|
||||||
"remixicon": "2.5.0",
|
"remixicon": "2.5.0",
|
||||||
|
|
|
@ -5,12 +5,47 @@ import { getThemeStore } from "./store/theme"
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
import { createHistoryStore } from "builderStore/store/history"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
export const themeStore = getThemeStore()
|
export const themeStore = getThemeStore()
|
||||||
export const temporalStore = getTemporalStore()
|
export const temporalStore = getTemporalStore()
|
||||||
|
|
||||||
|
// Setup history for screens
|
||||||
|
export const screenHistoryStore = createHistoryStore({
|
||||||
|
getDoc: id => get(store).screens?.find(screen => screen._id === id),
|
||||||
|
selectDoc: store.actions.screens.select,
|
||||||
|
afterAction: () => {
|
||||||
|
// Ensure a valid component is selected
|
||||||
|
if (!get(selectedComponent)) {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
selectedComponentId: get(selectedScreen)?.props._id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
store.actions.screens.save = screenHistoryStore.wrapSaveDoc(
|
||||||
|
store.actions.screens.save
|
||||||
|
)
|
||||||
|
store.actions.screens.delete = screenHistoryStore.wrapDeleteDoc(
|
||||||
|
store.actions.screens.delete
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup history for automations
|
||||||
|
export const automationHistoryStore = createHistoryStore({
|
||||||
|
getDoc: automationStore.actions.getDefinition,
|
||||||
|
selectDoc: automationStore.actions.select,
|
||||||
|
})
|
||||||
|
automationStore.actions.save = automationHistoryStore.wrapSaveDoc(
|
||||||
|
automationStore.actions.save
|
||||||
|
)
|
||||||
|
automationStore.actions.delete = automationHistoryStore.wrapDeleteDoc(
|
||||||
|
automationStore.actions.delete
|
||||||
|
)
|
||||||
|
|
||||||
export const selectedScreen = derived(store, $store => {
|
export const selectedScreen = derived(store, $store => {
|
||||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
||||||
})
|
})
|
||||||
|
@ -71,3 +106,13 @@ export const selectedComponentPath = derived(
|
||||||
).map(component => component._id)
|
).map(component => component._id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Derived automation state
|
||||||
|
export const selectedAutomation = derived(automationStore, $automationStore => {
|
||||||
|
if (!$automationStore.selectedAutomationId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return $automationStore.automations?.find(
|
||||||
|
x => x._id === $automationStore.selectedAutomationId
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { generate } from "shortid"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class responsible for the traversing of the automation definition.
|
|
||||||
* Automation definitions are stored in linked lists.
|
|
||||||
*/
|
|
||||||
export default class Automation {
|
|
||||||
constructor(automation) {
|
|
||||||
this.automation = automation
|
|
||||||
}
|
|
||||||
|
|
||||||
hasTrigger() {
|
|
||||||
return this.automation.definition.trigger
|
|
||||||
}
|
|
||||||
|
|
||||||
addTestData(data) {
|
|
||||||
this.automation.testData = { ...this.automation.testData, ...data }
|
|
||||||
}
|
|
||||||
|
|
||||||
addBlock(block, idx) {
|
|
||||||
// Make sure to add trigger if doesn't exist
|
|
||||||
if (!this.hasTrigger() && block.type === "TRIGGER") {
|
|
||||||
const trigger = { id: generate(), ...block }
|
|
||||||
this.automation.definition.trigger = trigger
|
|
||||||
return trigger
|
|
||||||
}
|
|
||||||
|
|
||||||
const newBlock = { id: generate(), ...block }
|
|
||||||
this.automation.definition.steps.splice(idx, 0, newBlock)
|
|
||||||
return newBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBlock(updatedBlock, id) {
|
|
||||||
const { steps, trigger } = this.automation.definition
|
|
||||||
|
|
||||||
if (trigger && trigger.id === id) {
|
|
||||||
this.automation.definition.trigger = updatedBlock
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepIdx = steps.findIndex(step => step.id === id)
|
|
||||||
if (stepIdx < 0) throw new Error("Block not found.")
|
|
||||||
steps.splice(stepIdx, 1, updatedBlock)
|
|
||||||
this.automation.definition.steps = steps
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteBlock(id) {
|
|
||||||
const { steps, trigger } = this.automation.definition
|
|
||||||
|
|
||||||
if (trigger && trigger.id === id) {
|
|
||||||
this.automation.definition.trigger = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepIdx = steps.findIndex(step => step.id === id)
|
|
||||||
if (stepIdx < 0) throw new Error("Block not found.")
|
|
||||||
steps.splice(stepIdx, 1)
|
|
||||||
this.automation.definition.steps = steps
|
|
||||||
}
|
|
||||||
|
|
||||||
constructBlock(type, stepId, blockDefinition) {
|
|
||||||
return {
|
|
||||||
...blockDefinition,
|
|
||||||
inputs: blockDefinition.inputs || {},
|
|
||||||
stepId,
|
|
||||||
type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import Automation from "./Automation"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { generate } from "shortid"
|
||||||
|
import { selectedAutomation } from "builderStore"
|
||||||
|
|
||||||
const initialAutomationState = {
|
const initialAutomationState = {
|
||||||
automations: [],
|
automations: [],
|
||||||
|
testResults: null,
|
||||||
showTestPanel: false,
|
showTestPanel: false,
|
||||||
blockDefinitions: {
|
blockDefinitions: {
|
||||||
TRIGGER: [],
|
TRIGGER: [],
|
||||||
ACTION: [],
|
ACTION: [],
|
||||||
},
|
},
|
||||||
selectedAutomation: null,
|
selectedAutomationId: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAutomationStore = () => {
|
export const getAutomationStore = () => {
|
||||||
|
@ -37,49 +39,41 @@ const automationActions = store => ({
|
||||||
API.getAutomationDefinitions(),
|
API.getAutomationDefinitions(),
|
||||||
])
|
])
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
let selected = state.selectedAutomation?.automation
|
|
||||||
state.automations = responses[0]
|
state.automations = responses[0]
|
||||||
|
state.automations.sort((a, b) => {
|
||||||
|
return a.name < b.name ? -1 : 1
|
||||||
|
})
|
||||||
state.blockDefinitions = {
|
state.blockDefinitions = {
|
||||||
TRIGGER: responses[1].trigger,
|
TRIGGER: responses[1].trigger,
|
||||||
ACTION: responses[1].action,
|
ACTION: responses[1].action,
|
||||||
}
|
}
|
||||||
// If previously selected find the new obj and select it
|
|
||||||
if (selected) {
|
|
||||||
selected = responses[0].filter(
|
|
||||||
automation => automation._id === selected._id
|
|
||||||
)
|
|
||||||
state.selectedAutomation = new Automation(selected[0])
|
|
||||||
}
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
create: async ({ name }) => {
|
create: async (name, trigger) => {
|
||||||
const automation = {
|
const automation = {
|
||||||
name,
|
name,
|
||||||
type: "automation",
|
type: "automation",
|
||||||
definition: {
|
definition: {
|
||||||
steps: [],
|
steps: [],
|
||||||
|
trigger,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const response = await API.createAutomation(automation)
|
const response = await store.actions.save(automation)
|
||||||
store.update(state => {
|
await store.actions.fetch()
|
||||||
state.automations = [...state.automations, response.automation]
|
store.actions.select(response._id)
|
||||||
store.actions.select(response.automation)
|
return response
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
duplicate: async automation => {
|
duplicate: async automation => {
|
||||||
const response = await API.createAutomation({
|
const response = await store.actions.save({
|
||||||
...automation,
|
...automation,
|
||||||
name: `${automation.name} - copy`,
|
name: `${automation.name} - copy`,
|
||||||
_id: undefined,
|
_id: undefined,
|
||||||
_ref: undefined,
|
_ref: undefined,
|
||||||
})
|
})
|
||||||
store.update(state => {
|
await store.actions.fetch()
|
||||||
state.automations = [...state.automations, response.automation]
|
store.actions.select(response._id)
|
||||||
store.actions.select(response.automation)
|
return response
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
save: async automation => {
|
save: async automation => {
|
||||||
const response = await API.updateAutomation(automation)
|
const response = await API.updateAutomation(automation)
|
||||||
|
@ -90,11 +84,13 @@ const automationActions = store => ({
|
||||||
)
|
)
|
||||||
if (existingIdx !== -1) {
|
if (existingIdx !== -1) {
|
||||||
state.automations.splice(existingIdx, 1, updatedAutomation)
|
state.automations.splice(existingIdx, 1, updatedAutomation)
|
||||||
state.automations = [...state.automations]
|
|
||||||
store.actions.select(updatedAutomation)
|
|
||||||
return state
|
return state
|
||||||
|
} else {
|
||||||
|
state.automations = [...state.automations, updatedAutomation]
|
||||||
}
|
}
|
||||||
|
return state
|
||||||
})
|
})
|
||||||
|
return response.automation
|
||||||
},
|
},
|
||||||
delete: async automation => {
|
delete: async automation => {
|
||||||
await API.deleteAutomation({
|
await API.deleteAutomation({
|
||||||
|
@ -102,34 +98,83 @@ const automationActions = store => ({
|
||||||
automationRev: automation?._rev,
|
automationRev: automation?._rev,
|
||||||
})
|
})
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const existingIdx = state.automations.findIndex(
|
// Remove the automation
|
||||||
existing => existing._id === automation?._id
|
state.automations = state.automations.filter(
|
||||||
|
x => x._id !== automation._id
|
||||||
)
|
)
|
||||||
state.automations.splice(existingIdx, 1)
|
// Select a new automation if required
|
||||||
state.automations = [...state.automations]
|
if (automation._id === state.selectedAutomationId) {
|
||||||
state.selectedAutomation = null
|
store.actions.select(state.automations[0]?._id)
|
||||||
state.selectedBlock = null
|
}
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
await store.actions.fetch()
|
||||||
|
},
|
||||||
|
updateBlockInputs: async (block, data) => {
|
||||||
|
// Create new modified block
|
||||||
|
let newBlock = {
|
||||||
|
...block,
|
||||||
|
inputs: {
|
||||||
|
...block.inputs,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any nullish or empty string values
|
||||||
|
Object.keys(newBlock.inputs).forEach(key => {
|
||||||
|
const val = newBlock.inputs[key]
|
||||||
|
if (val == null || val === "") {
|
||||||
|
delete newBlock.inputs[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create new modified automation
|
||||||
|
const automation = get(selectedAutomation)
|
||||||
|
const newAutomation = store.actions.getUpdatedDefinition(
|
||||||
|
automation,
|
||||||
|
newBlock
|
||||||
|
)
|
||||||
|
|
||||||
|
// Don't save if no changes were made
|
||||||
|
if (JSON.stringify(newAutomation) === JSON.stringify(automation)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
test: async (automation, testData) => {
|
test: async (automation, testData) => {
|
||||||
store.update(state => {
|
|
||||||
state.selectedAutomation.testResults = null
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
const result = await API.testAutomation({
|
const result = await API.testAutomation({
|
||||||
automationId: automation?._id,
|
automationId: automation?._id,
|
||||||
testData,
|
testData,
|
||||||
})
|
})
|
||||||
|
if (!result?.trigger && !result?.steps?.length) {
|
||||||
|
throw "Something went wrong testing your automation"
|
||||||
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedAutomation.testResults = result
|
state.testResults = result
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
select: automation => {
|
getDefinition: id => {
|
||||||
|
return get(store).automations?.find(x => x._id === id)
|
||||||
|
},
|
||||||
|
getUpdatedDefinition: (automation, block) => {
|
||||||
|
let newAutomation = cloneDeep(automation)
|
||||||
|
if (automation.definition.trigger?.id === block.id) {
|
||||||
|
newAutomation.definition.trigger = block
|
||||||
|
} else {
|
||||||
|
const idx = automation.definition.steps.findIndex(x => x.id === block.id)
|
||||||
|
newAutomation.definition.steps.splice(idx, 1, block)
|
||||||
|
}
|
||||||
|
return newAutomation
|
||||||
|
},
|
||||||
|
select: id => {
|
||||||
|
if (!id || id === get(store).selectedAutomationId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedAutomation = new Automation(cloneDeep(automation))
|
state.selectedAutomationId = id
|
||||||
state.selectedBlock = null
|
state.testResults = null
|
||||||
|
state.showTestPanel = false
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -147,48 +192,57 @@ const automationActions = store => ({
|
||||||
appId,
|
appId,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addTestDataToAutomation: data => {
|
addTestDataToAutomation: async data => {
|
||||||
store.update(state => {
|
let newAutomation = cloneDeep(get(selectedAutomation))
|
||||||
state.selectedAutomation.addTestData(data)
|
newAutomation.testData = {
|
||||||
return state
|
...newAutomation.testData,
|
||||||
})
|
...data,
|
||||||
|
}
|
||||||
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
addBlockToAutomation: (block, blockIdx) => {
|
constructBlock(type, stepId, blockDefinition) {
|
||||||
store.update(state => {
|
return {
|
||||||
state.selectedBlock = state.selectedAutomation.addBlock(
|
...blockDefinition,
|
||||||
cloneDeep(block),
|
inputs: blockDefinition.inputs || {},
|
||||||
blockIdx
|
stepId,
|
||||||
)
|
type,
|
||||||
return state
|
id: generate(),
|
||||||
})
|
}
|
||||||
},
|
},
|
||||||
toggleFieldControl: value => {
|
addBlockToAutomation: async (block, blockIdx) => {
|
||||||
store.update(state => {
|
const automation = get(selectedAutomation)
|
||||||
state.selectedBlock.rowControl = value
|
let newAutomation = cloneDeep(automation)
|
||||||
return state
|
if (!automation) {
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
newAutomation.definition.steps.splice(blockIdx, 0, block)
|
||||||
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
deleteAutomationBlock: block => {
|
/**
|
||||||
store.update(state => {
|
* "rowControl" appears to be the name of the flag used to determine whether
|
||||||
const idx =
|
* a certain automation block uses values or bindings as inputs
|
||||||
state.selectedAutomation.automation.definition.steps.findIndex(
|
*/
|
||||||
x => x.id === block.id
|
toggleRowControl: async (block, rowControl) => {
|
||||||
)
|
const newBlock = { ...block, rowControl }
|
||||||
state.selectedAutomation.deleteBlock(block.id)
|
const newAutomation = store.actions.getUpdatedDefinition(
|
||||||
|
get(selectedAutomation),
|
||||||
|
newBlock
|
||||||
|
)
|
||||||
|
await store.actions.save(newAutomation)
|
||||||
|
},
|
||||||
|
deleteAutomationBlock: async block => {
|
||||||
|
const automation = get(selectedAutomation)
|
||||||
|
let newAutomation = cloneDeep(automation)
|
||||||
|
|
||||||
// Select next closest step
|
// Delete trigger if required
|
||||||
const steps = state.selectedAutomation.automation.definition.steps
|
if (newAutomation.definition.trigger?.id === block.id) {
|
||||||
let nextSelectedBlock
|
delete newAutomation.definition.trigger
|
||||||
if (steps[idx] != null) {
|
} else {
|
||||||
nextSelectedBlock = steps[idx]
|
// Otherwise remove step
|
||||||
} else if (steps[idx - 1] != null) {
|
newAutomation.definition.steps = newAutomation.definition.steps.filter(
|
||||||
nextSelectedBlock = steps[idx - 1]
|
step => step.id !== block.id
|
||||||
} else {
|
)
|
||||||
nextSelectedBlock =
|
}
|
||||||
state.selectedAutomation.automation.definition.trigger || null
|
await store.actions.save(newAutomation)
|
||||||
}
|
|
||||||
state.selectedBlock = nextSelectedBlock
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import Automation from "../Automation"
|
|
||||||
import TEST_AUTOMATION from "./testAutomation"
|
|
||||||
|
|
||||||
const TEST_BLOCK = {
|
|
||||||
id: "AUXJQGZY7",
|
|
||||||
name: "Delay",
|
|
||||||
icon: "ri-time-fill",
|
|
||||||
tagline: "Delay for <b>{{time}}</b> milliseconds",
|
|
||||||
description: "Delay the automation until an amount of time has passed.",
|
|
||||||
params: { time: "number" },
|
|
||||||
type: "LOGIC",
|
|
||||||
args: { time: "5000" },
|
|
||||||
stepId: "DELAY",
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Automation Data Object", () => {
|
|
||||||
let automation
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
automation = new Automation({ ...TEST_AUTOMATION })
|
|
||||||
})
|
|
||||||
|
|
||||||
it("adds a automation block to the automation", () => {
|
|
||||||
automation.addBlock(TEST_BLOCK)
|
|
||||||
expect(automation.automation.definition)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("updates a automation block with new attributes", () => {
|
|
||||||
const firstBlock = automation.automation.definition.steps[0]
|
|
||||||
const updatedBlock = {
|
|
||||||
...firstBlock,
|
|
||||||
name: "UPDATED",
|
|
||||||
}
|
|
||||||
automation.updateBlock(updatedBlock, firstBlock.id)
|
|
||||||
expect(automation.automation.definition.steps[0]).toEqual(updatedBlock)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("deletes a automation block successfully", () => {
|
|
||||||
const { steps } = automation.automation.definition
|
|
||||||
const originalLength = steps.length
|
|
||||||
|
|
||||||
const lastBlock = steps[steps.length - 1]
|
|
||||||
automation.deleteBlock(lastBlock.id)
|
|
||||||
expect(automation.automation.definition.steps.length).toBeLessThan(
|
|
||||||
originalLength
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,78 +0,0 @@
|
||||||
export default {
|
|
||||||
name: "Test automation",
|
|
||||||
definition: {
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
id: "ANBDINAPS",
|
|
||||||
description: "Send an email.",
|
|
||||||
tagline: "Send email to <b>{{to}}</b>",
|
|
||||||
icon: "ri-mail-open-fill",
|
|
||||||
name: "Send Email",
|
|
||||||
params: {
|
|
||||||
to: "string",
|
|
||||||
from: "string",
|
|
||||||
subject: "longText",
|
|
||||||
text: "longText",
|
|
||||||
},
|
|
||||||
type: "ACTION",
|
|
||||||
args: {
|
|
||||||
text: "A user was created!",
|
|
||||||
subject: "New Budibase User",
|
|
||||||
from: "budimaster@budibase.com",
|
|
||||||
to: "test@test.com",
|
|
||||||
},
|
|
||||||
stepId: "SEND_EMAIL",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
trigger: {
|
|
||||||
id: "iRzYMOqND",
|
|
||||||
name: "Row Saved",
|
|
||||||
event: "row:save",
|
|
||||||
icon: "ri-save-line",
|
|
||||||
tagline: "Row is added to <b>{{table.name}}</b>",
|
|
||||||
description: "Fired when a row is saved to your database.",
|
|
||||||
params: { table: "table" },
|
|
||||||
type: "TRIGGER",
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
type: "table",
|
|
||||||
views: {},
|
|
||||||
name: "users",
|
|
||||||
schema: {
|
|
||||||
name: {
|
|
||||||
type: "string",
|
|
||||||
constraints: {
|
|
||||||
type: "string",
|
|
||||||
length: { maximum: 123 },
|
|
||||||
presence: { allowEmpty: false },
|
|
||||||
},
|
|
||||||
name: "name",
|
|
||||||
},
|
|
||||||
age: {
|
|
||||||
type: "number",
|
|
||||||
constraints: {
|
|
||||||
type: "number",
|
|
||||||
presence: { allowEmpty: false },
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: "",
|
|
||||||
lessThanOrEqualTo: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: "age",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_id: "c6b4e610cd984b588837bca27188a451",
|
|
||||||
_rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stepId: "ROW_SAVED",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "automation",
|
|
||||||
ok: true,
|
|
||||||
id: "b384f861f4754e1693835324a7fcca62",
|
|
||||||
rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37",
|
|
||||||
live: false,
|
|
||||||
_id: "b384f861f4754e1693835324a7fcca62",
|
|
||||||
_rev: "108-4116829ec375e0481d0ecab9e83a2caf",
|
|
||||||
}
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { get, writable } from "svelte/store"
|
import { get, writable } from "svelte/store"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { selectedScreen, selectedComponent } from "builderStore"
|
import {
|
||||||
|
selectedScreen,
|
||||||
|
selectedComponent,
|
||||||
|
screenHistoryStore,
|
||||||
|
automationHistoryStore,
|
||||||
|
} from "builderStore"
|
||||||
import {
|
import {
|
||||||
datasources,
|
datasources,
|
||||||
integrations,
|
integrations,
|
||||||
|
@ -124,6 +129,8 @@ export const getFrontendStore = () => {
|
||||||
navigation: application.navigation || {},
|
navigation: application.navigation || {},
|
||||||
usedPlugins: application.usedPlugins || [],
|
usedPlugins: application.usedPlugins || [],
|
||||||
}))
|
}))
|
||||||
|
screenHistoryStore.reset()
|
||||||
|
automationHistoryStore.reset()
|
||||||
|
|
||||||
// Initialise backend stores
|
// Initialise backend stores
|
||||||
database.set(application.instance)
|
database.set(application.instance)
|
||||||
|
@ -181,10 +188,7 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check screen isn't already selected
|
// Check screen isn't already selected
|
||||||
if (
|
if (state.selectedScreenId === screen._id) {
|
||||||
state.selectedScreenId === screen._id &&
|
|
||||||
state.selectedComponentId === screen.props?._id
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,7 +262,7 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
save: async screen => {
|
save: async screen => {
|
||||||
/*
|
/*
|
||||||
Temporarily disabled to accomodate migration issues.
|
Temporarily disabled to accomodate migration issues.
|
||||||
store.actions.screens.validate(screen)
|
store.actions.screens.validate(screen)
|
||||||
*/
|
*/
|
||||||
|
@ -349,6 +353,7 @@ export const getFrontendStore = () => {
|
||||||
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
updateSetting: async (screen, name, value) => {
|
updateSetting: async (screen, name, value) => {
|
||||||
if (!screen || !name) {
|
if (!screen || !name) {
|
||||||
|
|
319
packages/builder/src/builderStore/store/history.js
Normal file
319
packages/builder/src/builderStore/store/history.js
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
import * as jsonpatch from "fast-json-patch/index.mjs"
|
||||||
|
import { writable, derived, get } from "svelte/store"
|
||||||
|
|
||||||
|
const Operations = {
|
||||||
|
Add: "Add",
|
||||||
|
Delete: "Delete",
|
||||||
|
Change: "Change",
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
history: [],
|
||||||
|
position: 0,
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createHistoryStore = ({
|
||||||
|
getDoc,
|
||||||
|
selectDoc,
|
||||||
|
beforeAction,
|
||||||
|
afterAction,
|
||||||
|
}) => {
|
||||||
|
// Use a derived store to check if we are able to undo or redo any operations
|
||||||
|
const store = writable(initialState)
|
||||||
|
const derivedStore = derived(store, $store => {
|
||||||
|
return {
|
||||||
|
...$store,
|
||||||
|
canUndo: $store.position > 0,
|
||||||
|
canRedo: $store.position < $store.history.length,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wrapped versions of essential functions which we call ourselves when using
|
||||||
|
// undo and redo
|
||||||
|
let saveFn
|
||||||
|
let deleteFn
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal util to set the loading flag
|
||||||
|
*/
|
||||||
|
const startLoading = () => {
|
||||||
|
store.update(state => {
|
||||||
|
state.loading = true
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal util to unset the loading flag
|
||||||
|
*/
|
||||||
|
const stopLoading = () => {
|
||||||
|
store.update(state => {
|
||||||
|
state.loading = false
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets history state
|
||||||
|
*/
|
||||||
|
const reset = () => {
|
||||||
|
store.set(initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or updates an operation in history.
|
||||||
|
* For internal use only.
|
||||||
|
* @param operation the operation to save
|
||||||
|
*/
|
||||||
|
const saveOperation = operation => {
|
||||||
|
store.update(state => {
|
||||||
|
// Update history
|
||||||
|
let history = state.history
|
||||||
|
let position = state.position
|
||||||
|
if (!operation.id) {
|
||||||
|
// Every time a new operation occurs we discard any redo potential
|
||||||
|
operation.id = Math.random()
|
||||||
|
history = [...history.slice(0, state.position), operation]
|
||||||
|
position += 1
|
||||||
|
} else {
|
||||||
|
// If this is a redo/undo of an existing operation, just update history
|
||||||
|
// to replace the doc object as revisions may have changed
|
||||||
|
const idx = history.findIndex(op => op.id === operation.id)
|
||||||
|
history[idx].doc = operation.doc
|
||||||
|
}
|
||||||
|
return { history, position }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the save function, which asynchronously updates a doc.
|
||||||
|
* The returned function is an enriched version of the real save function so
|
||||||
|
* that we can control history.
|
||||||
|
* @param fn the save function
|
||||||
|
* @returns {function} a wrapped version of the save function
|
||||||
|
*/
|
||||||
|
const wrapSaveDoc = fn => {
|
||||||
|
saveFn = async (doc, operationId) => {
|
||||||
|
// Only works on a single doc at a time
|
||||||
|
if (!doc || Array.isArray(doc)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startLoading()
|
||||||
|
try {
|
||||||
|
const oldDoc = getDoc(doc._id)
|
||||||
|
const newDoc = jsonpatch.deepClone(await fn(doc))
|
||||||
|
|
||||||
|
// Store the change
|
||||||
|
if (!oldDoc) {
|
||||||
|
// If no old doc, this is an add operation
|
||||||
|
saveOperation({
|
||||||
|
type: Operations.Add,
|
||||||
|
doc: newDoc,
|
||||||
|
id: operationId,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Otherwise this is a change operation
|
||||||
|
saveOperation({
|
||||||
|
type: Operations.Change,
|
||||||
|
forwardPatch: jsonpatch.compare(oldDoc, doc),
|
||||||
|
backwardsPatch: jsonpatch.compare(doc, oldDoc),
|
||||||
|
doc: newDoc,
|
||||||
|
id: operationId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
stopLoading()
|
||||||
|
return newDoc
|
||||||
|
} catch (error) {
|
||||||
|
// We want to allow errors to propagate up to normal handlers, but we
|
||||||
|
// want to stop loading first
|
||||||
|
stopLoading()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return saveFn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the delete function, which asynchronously deletes a doc.
|
||||||
|
* The returned function is an enriched version of the real delete function so
|
||||||
|
* that we can control history.
|
||||||
|
* @param fn the delete function
|
||||||
|
* @returns {function} a wrapped version of the delete function
|
||||||
|
*/
|
||||||
|
const wrapDeleteDoc = fn => {
|
||||||
|
deleteFn = async (doc, operationId) => {
|
||||||
|
// Only works on a single doc at a time
|
||||||
|
if (!doc || Array.isArray(doc)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startLoading()
|
||||||
|
try {
|
||||||
|
const oldDoc = jsonpatch.deepClone(doc)
|
||||||
|
await fn(doc)
|
||||||
|
saveOperation({
|
||||||
|
type: Operations.Delete,
|
||||||
|
doc: oldDoc,
|
||||||
|
id: operationId,
|
||||||
|
})
|
||||||
|
stopLoading()
|
||||||
|
} catch (error) {
|
||||||
|
// We want to allow errors to propagate up to normal handlers, but we
|
||||||
|
// want to stop loading first
|
||||||
|
stopLoading()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deleteFn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously undoes the previous operation.
|
||||||
|
* Optionally selects the changed document so that changes are visible.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const undo = async () => {
|
||||||
|
// Sanity checks
|
||||||
|
const { canUndo, history, position, loading } = get(derivedStore)
|
||||||
|
if (!canUndo || loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const operation = history[position - 1]
|
||||||
|
if (!operation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startLoading()
|
||||||
|
|
||||||
|
// Before hook
|
||||||
|
await beforeAction?.(operation)
|
||||||
|
|
||||||
|
// Update state immediately to prevent further clicks and to prevent bad
|
||||||
|
// history in the event of an update failing
|
||||||
|
store.update(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
position: state.position - 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Undo the operation
|
||||||
|
try {
|
||||||
|
// Undo ADD
|
||||||
|
if (operation.type === Operations.Add) {
|
||||||
|
// Try to get the latest doc version to delete
|
||||||
|
const latestDoc = getDoc(operation.doc._id)
|
||||||
|
const doc = latestDoc || operation.doc
|
||||||
|
await deleteFn(doc, operation.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo DELETE
|
||||||
|
else if (operation.type === Operations.Delete) {
|
||||||
|
// Delete the _rev from the deleted doc so that we can save it as a new
|
||||||
|
// doc again without conflicts
|
||||||
|
let doc = jsonpatch.deepClone(operation.doc)
|
||||||
|
delete doc._rev
|
||||||
|
const created = await saveFn(doc, operation.id)
|
||||||
|
selectDoc?.(created?._id || doc._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo CHANGE
|
||||||
|
else {
|
||||||
|
// Get the current doc and apply the backwards patch on top of it
|
||||||
|
let doc = jsonpatch.deepClone(getDoc(operation.doc._id))
|
||||||
|
if (doc) {
|
||||||
|
jsonpatch.applyPatch(
|
||||||
|
doc,
|
||||||
|
jsonpatch.deepClone(operation.backwardsPatch)
|
||||||
|
)
|
||||||
|
await saveFn(doc, operation.id)
|
||||||
|
selectDoc?.(doc._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopLoading()
|
||||||
|
} catch (error) {
|
||||||
|
stopLoading()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// After hook
|
||||||
|
await afterAction?.(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously redoes the previous undo.
|
||||||
|
* Optionally selects the changed document so that changes are visible.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const redo = async () => {
|
||||||
|
// Sanity checks
|
||||||
|
const { canRedo, history, position, loading } = get(derivedStore)
|
||||||
|
if (!canRedo || loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const operation = history[position]
|
||||||
|
if (!operation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startLoading()
|
||||||
|
|
||||||
|
// Before hook
|
||||||
|
await beforeAction?.(operation)
|
||||||
|
|
||||||
|
// Update state immediately to prevent further clicks and to prevent bad
|
||||||
|
// history in the event of an update failing
|
||||||
|
store.update(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
position: state.position + 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Redo the operation
|
||||||
|
try {
|
||||||
|
// Redo ADD
|
||||||
|
if (operation.type === Operations.Add) {
|
||||||
|
// Delete the _rev from the deleted doc so that we can save it as a new
|
||||||
|
// doc again without conflicts
|
||||||
|
let doc = jsonpatch.deepClone(operation.doc)
|
||||||
|
delete doc._rev
|
||||||
|
const created = await saveFn(doc, operation.id)
|
||||||
|
selectDoc?.(created?._id || doc._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redo DELETE
|
||||||
|
else if (operation.type === Operations.Delete) {
|
||||||
|
// Try to get the latest doc version to delete
|
||||||
|
const latestDoc = getDoc(operation.doc._id)
|
||||||
|
const doc = latestDoc || operation.doc
|
||||||
|
await deleteFn(doc, operation.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redo CHANGE
|
||||||
|
else {
|
||||||
|
// Get the current doc and apply the forwards patch on top of it
|
||||||
|
let doc = jsonpatch.deepClone(getDoc(operation.doc._id))
|
||||||
|
if (doc) {
|
||||||
|
jsonpatch.applyPatch(doc, jsonpatch.deepClone(operation.forwardPatch))
|
||||||
|
await saveFn(doc, operation.id)
|
||||||
|
selectDoc?.(doc._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopLoading()
|
||||||
|
} catch (error) {
|
||||||
|
stopLoading()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// After hook
|
||||||
|
await afterAction?.(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: derivedStore.subscribe,
|
||||||
|
wrapSaveDoc,
|
||||||
|
wrapDeleteDoc,
|
||||||
|
reset,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { automationStore } from "builderStore"
|
import { selectedAutomation } from "builderStore"
|
||||||
import Flowchart from "./FlowChart/FlowChart.svelte"
|
import Flowchart from "./FlowChart/FlowChart.svelte"
|
||||||
|
|
||||||
$: automation = $automationStore.selectedAutomation?.automation
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if automation}
|
{#if $selectedAutomation}
|
||||||
<Flowchart {automation} />
|
{#key $selectedAutomation._id}
|
||||||
|
<Flowchart automation={$selectedAutomation} />
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
Detail,
|
Detail,
|
||||||
Body,
|
Body,
|
||||||
Icon,
|
Icon,
|
||||||
Tooltip,
|
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
|
@ -13,7 +12,6 @@
|
||||||
import { externalActions } from "./ExternalActions"
|
import { externalActions } from "./ExternalActions"
|
||||||
|
|
||||||
export let blockIdx
|
export let blockIdx
|
||||||
export let blockComplete
|
|
||||||
|
|
||||||
const disabled = {
|
const disabled = {
|
||||||
SEND_EMAIL_SMTP: {
|
SEND_EMAIL_SMTP: {
|
||||||
|
@ -50,15 +48,12 @@
|
||||||
|
|
||||||
async function addBlockToAutomation() {
|
async function addBlockToAutomation() {
|
||||||
try {
|
try {
|
||||||
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
const newBlock = automationStore.actions.constructBlock(
|
||||||
"ACTION",
|
"ACTION",
|
||||||
actionVal.stepId,
|
actionVal.stepId,
|
||||||
actionVal
|
actionVal
|
||||||
)
|
)
|
||||||
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
|
await automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving automation")
|
notifications.error("Error saving automation")
|
||||||
}
|
}
|
||||||
|
@ -66,20 +61,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Create Automation"
|
title="Add automation step"
|
||||||
confirmText="Save"
|
confirmText="Save"
|
||||||
size="M"
|
size="M"
|
||||||
disabled={!selectedAction}
|
disabled={!selectedAction}
|
||||||
onConfirm={() => {
|
onConfirm={addBlockToAutomation}
|
||||||
blockComplete = true
|
|
||||||
addBlockToAutomation()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Body size="XS">Select an app or event.</Body>
|
<Layout noPadding gap="XS">
|
||||||
|
<Detail size="S">Apps</Detail>
|
||||||
<Layout noPadding>
|
|
||||||
<Body size="S">Apps</Body>
|
|
||||||
|
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each Object.entries(external) as [idx, action]}
|
{#each Object.entries(external) as [idx, action]}
|
||||||
<div
|
<div
|
||||||
|
@ -95,64 +84,45 @@
|
||||||
alt="zapier"
|
alt="zapier"
|
||||||
/>
|
/>
|
||||||
<span class="icon-spacing">
|
<span class="icon-spacing">
|
||||||
<Body size="XS">{idx.charAt(0).toUpperCase() + idx.slice(1)}</Body
|
<Body size="XS">
|
||||||
></span
|
{idx.charAt(0).toUpperCase() + idx.slice(1)}
|
||||||
>
|
</Body>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
<Detail size="S">Actions</Detail>
|
<Detail size="S">Actions</Detail>
|
||||||
|
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each Object.entries(internal) as [idx, action]}
|
{#each Object.entries(internal) as [idx, action]}
|
||||||
{#if disabled[idx] && disabled[idx].disabled}
|
{@const isDisabled = disabled[idx] && disabled[idx].disabled}
|
||||||
<Tooltip text={disabled[idx].message} direction="bottom">
|
<div
|
||||||
<div
|
class="item"
|
||||||
class="item"
|
class:disabled={isDisabled}
|
||||||
class:selected={selectedAction === action.name}
|
class:selected={selectedAction === action.name}
|
||||||
class:disabled={true}
|
on:click={isDisabled ? null : () => selectAction(action)}
|
||||||
on:click={() => selectAction(action)}
|
>
|
||||||
>
|
<div class="item-body">
|
||||||
<div class="item-body">
|
<Icon name={action.icon} />
|
||||||
<Icon name={action.icon} />
|
<Body size="XS">{action.name}</Body>
|
||||||
<span class="icon-spacing">
|
{#if isDisabled}
|
||||||
<Body size="XS">{action.name}</Body></span
|
<Icon name="Help" tooltip={disabled[idx].message} />
|
||||||
>
|
{/if}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="item"
|
|
||||||
class:selected={selectedAction === action.name}
|
|
||||||
on:click={() => selectAction(action)}
|
|
||||||
>
|
|
||||||
<div class="item-body">
|
|
||||||
<Icon name={action.icon} />
|
|
||||||
<span class="icon-spacing">
|
|
||||||
<Body size="XS">{action.name}</Body></span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.icon-spacing {
|
|
||||||
margin-left: var(--spacing-m);
|
|
||||||
}
|
|
||||||
.item-body {
|
.item-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.item-list {
|
.item-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -171,8 +141,15 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
.item:hover,
|
.item:not(.disabled):hover,
|
||||||
.selected {
|
.selected {
|
||||||
background: var(--spectrum-alias-background-color-tertiary);
|
background: var(--spectrum-alias-background-color-tertiary);
|
||||||
}
|
}
|
||||||
|
.disabled {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
color: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
.disabled :global(.spectrum-Body) {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import FlowItem from "./FlowItem.svelte"
|
import FlowItem from "./FlowItem.svelte"
|
||||||
import TestDataModal from "./TestDataModal.svelte"
|
import TestDataModal from "./TestDataModal.svelte"
|
||||||
|
@ -13,27 +13,28 @@
|
||||||
Modal,
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { ActionStepID } from "constants/backend/automations"
|
import { ActionStepID } from "constants/backend/automations"
|
||||||
|
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||||
|
import { automationHistoryStore } from "builderStore"
|
||||||
|
|
||||||
export let automation
|
export let automation
|
||||||
|
|
||||||
let testDataModal
|
let testDataModal
|
||||||
let blocks
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
|
||||||
$: {
|
$: blocks = getBlocks(automation)
|
||||||
blocks = []
|
|
||||||
if (automation) {
|
const getBlocks = automation => {
|
||||||
if (automation.definition.trigger) {
|
let blocks = []
|
||||||
blocks.push(automation.definition.trigger)
|
if (automation.definition.trigger) {
|
||||||
}
|
blocks.push(automation.definition.trigger)
|
||||||
blocks = blocks.concat(automation.definition.steps || [])
|
|
||||||
}
|
}
|
||||||
|
blocks = blocks.concat(automation.definition.steps || [])
|
||||||
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAutomation() {
|
async function deleteAutomation() {
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.delete(
|
await automationStore.actions.delete($selectedAutomation)
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error deleting automation")
|
notifications.error("Error deleting automation")
|
||||||
}
|
}
|
||||||
|
@ -41,20 +42,17 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="canvas">
|
<div class="canvas">
|
||||||
<div style="float: left; padding-left: var(--spacing-xl);">
|
<div class="header">
|
||||||
<Heading size="S">{automation.name}</Heading>
|
<Heading size="S">{automation.name}</Heading>
|
||||||
</div>
|
<div class="controls">
|
||||||
<div style="float: right; padding-right: var(--spacing-xl);" class="title">
|
<UndoRedoControl store={automationHistoryStore} />
|
||||||
<div class="subtitle">
|
<Icon
|
||||||
<div style="display:flex; align-items: center;">
|
on:click={confirmDeleteDialog.show}
|
||||||
<div class="icon">
|
hoverable
|
||||||
<Icon
|
size="M"
|
||||||
on:click={confirmDeleteDialog.show}
|
name="DeleteOutline"
|
||||||
hoverable
|
/>
|
||||||
size="M"
|
<div class="buttons">
|
||||||
name="DeleteOutline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
testDataModal.show()
|
testDataModal.show()
|
||||||
|
@ -62,15 +60,13 @@
|
||||||
icon="MultipleCheck"
|
icon="MultipleCheck"
|
||||||
size="M">Run test</ActionButton
|
size="M">Run test</ActionButton
|
||||||
>
|
>
|
||||||
<div style="padding-left: var(--spacing-m);">
|
<ActionButton
|
||||||
<ActionButton
|
disabled={!$automationStore.testResults}
|
||||||
disabled={!$automationStore.selectedAutomation?.testResults}
|
on:click={() => {
|
||||||
on:click={() => {
|
$automationStore.showTestPanel = true
|
||||||
$automationStore.showTestPanel = true
|
}}
|
||||||
}}
|
size="M">Test Details</ActionButton
|
||||||
size="M">Test Details</ActionButton
|
>
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,7 +76,7 @@
|
||||||
<div
|
<div
|
||||||
class="block"
|
class="block"
|
||||||
animate:flip={{ duration: 500 }}
|
animate:flip={{ duration: 500 }}
|
||||||
in:fly|local={{ x: 500, duration: 500 }}
|
in:fly={{ x: 500, duration: 500 }}
|
||||||
out:fly|local={{ x: 500, duration: 500 }}
|
out:fly|local={{ x: 500, duration: 500 }}
|
||||||
>
|
>
|
||||||
{#if block.stepId !== ActionStepID.LOOP}
|
{#if block.stepId !== ActionStepID.LOOP}
|
||||||
|
@ -105,6 +101,9 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.canvas {
|
||||||
|
padding: var(--spacing-l) var(--spacing-xl);
|
||||||
|
}
|
||||||
/* 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 {
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
|
@ -122,18 +121,19 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.header {
|
||||||
padding-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
padding-bottom: var(--spacing-xl);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.icon {
|
.controls,
|
||||||
cursor: pointer;
|
.buttons {
|
||||||
padding-right: var(--spacing-m);
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Divider,
|
Divider,
|
||||||
|
@ -23,36 +23,26 @@
|
||||||
export let block
|
export let block
|
||||||
export let testDataModal
|
export let testDataModal
|
||||||
export let idx
|
export let idx
|
||||||
|
|
||||||
let selected
|
let selected
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let actionModal
|
let actionModal
|
||||||
let blockComplete
|
let open = true
|
||||||
let showLooping = false
|
let showLooping = false
|
||||||
let role
|
let role
|
||||||
|
|
||||||
$: automationId = $automationStore.selectedAutomation?.automation._id
|
$: automationId = $selectedAutomation?._id
|
||||||
$: showBindingPicker =
|
$: showBindingPicker =
|
||||||
block.stepId === ActionStepID.CREATE_ROW ||
|
block.stepId === ActionStepID.CREATE_ROW ||
|
||||||
block.stepId === ActionStepID.UPDATE_ROW
|
block.stepId === ActionStepID.UPDATE_ROW
|
||||||
|
|
||||||
$: isTrigger = block.type === "TRIGGER"
|
$: isTrigger = block.type === "TRIGGER"
|
||||||
|
$: steps = $selectedAutomation?.definition?.steps ?? []
|
||||||
$: selected = $automationStore.selectedBlock?.id === block.id
|
|
||||||
$: steps =
|
|
||||||
$automationStore.selectedAutomation?.automation?.definition?.steps ?? []
|
|
||||||
|
|
||||||
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
||||||
$: lastStep = !isTrigger && blockIdx + 1 === steps.length
|
$: lastStep = !isTrigger && blockIdx + 1 === steps.length
|
||||||
|
$: totalBlocks = $selectedAutomation?.definition?.steps.length + 1
|
||||||
$: totalBlocks =
|
$: loopBlock = $selectedAutomation?.definition.steps.find(
|
||||||
$automationStore.selectedAutomation?.automation?.definition?.steps.length +
|
x => x.blockToLoop === block.id
|
||||||
1
|
)
|
||||||
|
|
||||||
$: loopingSelected =
|
|
||||||
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
|
||||||
x => x.blockToLoop === block.id
|
|
||||||
)
|
|
||||||
|
|
||||||
$: isAppAction = block?.stepId === TriggerStepID.APP
|
$: isAppAction = block?.stepId === TriggerStepID.APP
|
||||||
$: isAppAction && setPermissions(role)
|
$: isAppAction && setPermissions(role)
|
||||||
$: isAppAction && getPermissions(automationId)
|
$: isAppAction && getPermissions(automationId)
|
||||||
|
@ -81,76 +71,54 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeLooping() {
|
async function removeLooping() {
|
||||||
loopingSelected = false
|
let loopBlock = $selectedAutomation?.definition.steps.find(
|
||||||
let loopBlock =
|
x => x.blockToLoop === block.id
|
||||||
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
|
||||||
x => x.blockToLoop === block.id
|
|
||||||
)
|
|
||||||
automationStore.actions.deleteAutomationBlock(loopBlock)
|
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
)
|
||||||
|
try {
|
||||||
|
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving automation")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteStep() {
|
async function deleteStep() {
|
||||||
let loopBlock =
|
let loopBlock = $selectedAutomation?.definition.steps.find(
|
||||||
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
x => x.blockToLoop === block.id
|
||||||
x => x.blockToLoop === block.id
|
)
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (loopBlock) {
|
if (loopBlock) {
|
||||||
automationStore.actions.deleteAutomationBlock(loopBlock)
|
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||||
}
|
}
|
||||||
automationStore.actions.deleteAutomationBlock(block)
|
await automationStore.actions.deleteAutomationBlock(block)
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving notification")
|
notifications.error("Error saving automation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function toggleFieldControl(evt) {
|
|
||||||
onSelect(block)
|
/**
|
||||||
let rowControl
|
* "rowControl" appears to be the name of the flag used to determine whether
|
||||||
if (evt.detail === "Use values") {
|
* a certain automation block uses values or bindings as inputs
|
||||||
rowControl = false
|
*/
|
||||||
} else {
|
function toggleRowControl(evt) {
|
||||||
rowControl = true
|
const rowControl = evt.detail !== "Use values"
|
||||||
}
|
automationStore.actions.toggleRowControl(block, rowControl)
|
||||||
automationStore.actions.toggleFieldControl(rowControl)
|
|
||||||
automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addLooping() {
|
async function addLooping() {
|
||||||
loopingSelected = true
|
|
||||||
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
||||||
|
const loopBlock = automationStore.actions.constructBlock(
|
||||||
const loopBlock = $automationStore.selectedAutomation.constructBlock(
|
|
||||||
"ACTION",
|
"ACTION",
|
||||||
"LOOP",
|
"LOOP",
|
||||||
loopDefinition
|
loopDefinition
|
||||||
)
|
)
|
||||||
loopBlock.blockToLoop = block.id
|
loopBlock.blockToLoop = block.id
|
||||||
block.loopBlock = loopBlock.id
|
await automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
|
||||||
automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
|
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSelect(block) {
|
|
||||||
await automationStore.update(state => {
|
|
||||||
state.selectedBlock = block
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
|
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
|
||||||
{#if loopingSelected}
|
{#if loopBlock}
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<div
|
<div
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -174,13 +142,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="blockTitle">
|
<div class="blockTitle">
|
||||||
<div
|
<div style="margin-left: 10px;" on:click={() => {}}>
|
||||||
style="margin-left: 10px;"
|
<Icon hoverable name={showLooping ? "ChevronDown" : "ChevronUp"} />
|
||||||
on:click={() => {
|
|
||||||
onSelect(block)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name={showLooping ? "ChevronUp" : "ChevronDown"} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -198,9 +161,7 @@
|
||||||
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
||||||
.properties
|
.properties
|
||||||
)}
|
)}
|
||||||
block={$automationStore.selectedAutomation?.automation.definition.steps.find(
|
block={loopBlock}
|
||||||
x => x.blockToLoop === block.id
|
|
||||||
)}
|
|
||||||
{webhookModal}
|
{webhookModal}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -209,22 +170,28 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<FlowItemHeader bind:blockComplete {block} {testDataModal} {idx} />
|
<FlowItemHeader
|
||||||
{#if !blockComplete}
|
{open}
|
||||||
|
{block}
|
||||||
|
{testDataModal}
|
||||||
|
{idx}
|
||||||
|
on:toggle={() => (open = !open)}
|
||||||
|
/>
|
||||||
|
{#if open}
|
||||||
<Divider noMargin />
|
<Divider noMargin />
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
{#if !isTrigger}
|
{#if !isTrigger}
|
||||||
<div>
|
<div>
|
||||||
<div class="block-options">
|
<div class="block-options">
|
||||||
{#if !loopingSelected}
|
{#if !loopBlock}
|
||||||
<ActionButton on:click={() => addLooping()} icon="Reuse"
|
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
||||||
>Add Looping</ActionButton
|
Add Looping
|
||||||
>
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showBindingPicker}
|
{#if showBindingPicker}
|
||||||
<Select
|
<Select
|
||||||
on:change={toggleFieldControl}
|
on:change={toggleRowControl}
|
||||||
defaultValue="Use values"
|
defaultValue="Use values"
|
||||||
autoWidth
|
autoWidth
|
||||||
value={block.rowControl ? "Use bindings" : "Use values"}
|
value={block.rowControl ? "Use bindings" : "Use values"}
|
||||||
|
@ -250,16 +217,16 @@
|
||||||
{webhookModal}
|
{webhookModal}
|
||||||
/>
|
/>
|
||||||
{#if lastStep}
|
{#if lastStep}
|
||||||
<Button on:click={() => testDataModal.show()} cta
|
<Button on:click={() => testDataModal.show()} cta>
|
||||||
>Finish and test automation</Button
|
Finish and test automation
|
||||||
>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Modal bind:this={actionModal} width="30%">
|
<Modal bind:this={actionModal} width="30%">
|
||||||
<ActionModal {blockIdx} bind:blockComplete />
|
<ActionModal {blockIdx} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={webhookModal} width="30%">
|
<Modal bind:this={webhookModal} width="30%">
|
||||||
|
|
|
@ -2,21 +2,22 @@
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui"
|
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui"
|
||||||
import { externalActions } from "./ExternalActions"
|
import { externalActions } from "./ExternalActions"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let blockComplete
|
export let open
|
||||||
export let showTestStatus = false
|
export let showTestStatus = false
|
||||||
export let showParameters = {}
|
|
||||||
export let testResult
|
export let testResult
|
||||||
export let isTrigger
|
export let isTrigger
|
||||||
export let idx
|
export let idx
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (!testResult) {
|
if (!testResult) {
|
||||||
testResult =
|
testResult = $automationStore.testResults?.steps?.filter(step =>
|
||||||
$automationStore.selectedAutomation?.testResults?.steps.filter(step =>
|
block.id ? step.id === block.id : step.stepId === block.stepId
|
||||||
block.id ? step.id === block.id : step.stepId === block.stepId
|
)?.[0]
|
||||||
)[0]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: isTrigger = isTrigger || block.type === "TRIGGER"
|
$: isTrigger = isTrigger || block.type === "TRIGGER"
|
||||||
|
@ -45,13 +46,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<div
|
<div on:click={() => dispatch("toggle")} class="splitHeader">
|
||||||
on:click={() => {
|
|
||||||
blockComplete = !blockComplete
|
|
||||||
showParameters[block.id] = blockComplete
|
|
||||||
}}
|
|
||||||
class="splitHeader"
|
|
||||||
>
|
|
||||||
<div class="center-items">
|
<div class="center-items">
|
||||||
{#if externalActions[block.stepId]}
|
{#if externalActions[block.stepId]}
|
||||||
<img
|
<img
|
||||||
|
@ -99,7 +94,7 @@
|
||||||
onSelect(block)
|
onSelect(block)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon hoverable name={blockComplete ? "ChevronUp" : "ChevronDown"} />
|
<Icon hoverable name={open ? "ChevronUp" : "ChevronDown"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
Label,
|
Label,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
@ -17,9 +17,7 @@
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// clone the trigger so we're not mutating the reference
|
// clone the trigger so we're not mutating the reference
|
||||||
trigger = cloneDeep(
|
trigger = cloneDeep($selectedAutomation.definition.trigger)
|
||||||
$automationStore.selectedAutomation.automation.definition.trigger
|
|
||||||
)
|
|
||||||
|
|
||||||
// get the outputs so we can define the fields
|
// get the outputs so we can define the fields
|
||||||
let schema = Object.entries(trigger.schema?.outputs?.properties || {})
|
let schema = Object.entries(trigger.schema?.outputs?.properties || {})
|
||||||
|
@ -32,7 +30,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to see if there is existing test data in the store
|
// check to see if there is existing test data in the store
|
||||||
$: testData = $automationStore.selectedAutomation.automation.testData || {}
|
$: testData = $selectedAutomation.testData || {}
|
||||||
|
|
||||||
// Check the schema to see if required fields have been entered
|
// Check the schema to see if required fields have been entered
|
||||||
$: isError = !trigger.schema.outputs.required.every(
|
$: isError = !trigger.schema.outputs.required.every(
|
||||||
|
@ -51,10 +49,7 @@
|
||||||
|
|
||||||
const testAutomation = async () => {
|
const testAutomation = async () => {
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.test(
|
await automationStore.actions.test($selectedAutomation, testData)
|
||||||
$automationStore.selectedAutomation?.automation,
|
|
||||||
testData
|
|
||||||
)
|
|
||||||
$automationStore.showTestPanel = true
|
$automationStore.showTestPanel = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error testing automation")
|
notifications.error("Error testing automation")
|
||||||
|
@ -70,8 +65,8 @@
|
||||||
onConfirm={testAutomation}
|
onConfirm={testAutomation}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
>
|
>
|
||||||
<Tabs selected="Form" quiet
|
<Tabs selected="Form" quiet>
|
||||||
><Tab icon="Form" title="Form">
|
<Tab icon="Form" title="Form">
|
||||||
<div class="tab-content-padding">
|
<div class="tab-content-padding">
|
||||||
<AutomationBlockSetup
|
<AutomationBlockSetup
|
||||||
{testData}
|
{testData}
|
||||||
|
@ -86,11 +81,7 @@
|
||||||
<Label>JSON</Label>
|
<Label>JSON</Label>
|
||||||
<div class="text-area-container">
|
<div class="text-area-container">
|
||||||
<TextArea
|
<TextArea
|
||||||
value={JSON.stringify(
|
value={JSON.stringify($selectedAutomation.testData, null, 2)}
|
||||||
$automationStore.selectedAutomation.automation.testData,
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}
|
|
||||||
error={failedParse}
|
error={failedParse}
|
||||||
on:change={e => parseTestJSON(e)}
|
on:change={e => parseTestJSON(e)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
export let testResults
|
export let testResults
|
||||||
export let width = "400px"
|
export let width = "400px"
|
||||||
|
|
||||||
let showParameters
|
let openBlocks = {}
|
||||||
let blocks
|
let blocks
|
||||||
|
|
||||||
function prepTestResults(results) {
|
function prepTestResults(results) {
|
||||||
|
@ -48,14 +48,15 @@
|
||||||
<div class="block" style={width ? `width: ${width}` : ""}>
|
<div class="block" style={width ? `width: ${width}` : ""}>
|
||||||
{#if block.stepId !== ActionStepID.LOOP}
|
{#if block.stepId !== ActionStepID.LOOP}
|
||||||
<FlowItemHeader
|
<FlowItemHeader
|
||||||
showTestStatus={true}
|
open={!!openBlocks[block.id]}
|
||||||
bind:showParameters
|
on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])}
|
||||||
{block}
|
|
||||||
isTrigger={idx === 0}
|
isTrigger={idx === 0}
|
||||||
{idx}
|
|
||||||
testResult={filteredResults?.[idx]}
|
testResult={filteredResults?.[idx]}
|
||||||
|
showTestStatus
|
||||||
|
{block}
|
||||||
|
{idx}
|
||||||
/>
|
/>
|
||||||
{#if showParameters && showParameters[block.id]}
|
{#if openBlocks[block.id]}
|
||||||
<Divider noMargin />
|
<Divider noMargin />
|
||||||
{#if filteredResults?.[idx]?.outputs.iterations}
|
{#if filteredResults?.[idx]?.outputs.iterations}
|
||||||
<div style="display: flex; padding: 10px 10px 0px 12px;">
|
<div style="display: flex; padding: 10px 10px 0px 12px;">
|
||||||
|
|
|
@ -2,26 +2,8 @@
|
||||||
import { Icon, Divider } from "@budibase/bbui"
|
import { Icon, Divider } from "@budibase/bbui"
|
||||||
import TestDisplay from "./TestDisplay.svelte"
|
import TestDisplay from "./TestDisplay.svelte"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { ActionStepID } from "constants/backend/automations"
|
|
||||||
|
|
||||||
export let automation
|
export let automation
|
||||||
|
|
||||||
let blocks, testResults
|
|
||||||
|
|
||||||
$: {
|
|
||||||
blocks = []
|
|
||||||
if (automation) {
|
|
||||||
if (automation.definition.trigger) {
|
|
||||||
blocks.push(automation.definition.trigger)
|
|
||||||
}
|
|
||||||
blocks = blocks
|
|
||||||
.concat(automation.definition.steps || [])
|
|
||||||
.filter(x => x.stepId !== ActionStepID.LOOP)
|
|
||||||
} else if ($automationStore.selectedAutomation) {
|
|
||||||
automation = $automationStore.selectedAutomation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$: testResults = $automationStore.selectedAutomation?.testResults
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@ -42,7 +24,7 @@
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<TestDisplay {automation} {testResults} />
|
<TestDisplay {automation} testResults={$automationStore.testResults} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.title {
|
.title {
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import { automationStore } from "builderStore"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
|
$: selectedAutomationId = $selectedAutomation?._id
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -16,9 +15,8 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function selectAutomation(automation) {
|
function selectAutomation(id) {
|
||||||
automationStore.actions.select(automation)
|
automationStore.actions.select(id)
|
||||||
$goto(`./${automation._id}`)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -29,7 +27,7 @@
|
||||||
icon="ShareAndroid"
|
icon="ShareAndroid"
|
||||||
text={automation.name}
|
text={automation.name}
|
||||||
selected={automation._id === selectedAutomationId}
|
selected={automation._id === selectedAutomationId}
|
||||||
on:click={() => selectAutomation(automation)}
|
on:click={() => selectAutomation(automation._id)}
|
||||||
>
|
>
|
||||||
<EditAutomationPopover {automation} />
|
<EditAutomationPopover {automation} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
@ -42,5 +40,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
margin: 0 calc(-1 * var(--spacing-xl));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,36 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import AutomationList from "./AutomationList.svelte"
|
import AutomationList from "./AutomationList.svelte"
|
||||||
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
||||||
import { Modal, Tabs, Tab, Button, Layout } from "@budibase/bbui"
|
import { Modal, Button, Layout } from "@budibase/bbui"
|
||||||
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
|
||||||
export let modal
|
export let modal
|
||||||
export let webhookModal
|
export let webhookModal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="nav">
|
<Panel title="Automations" borderRight>
|
||||||
<Tabs selected="Automations">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<Tab title="Automations">
|
<Button cta on:click={modal.show}>Add automation</Button>
|
||||||
<Layout paddingX="L" paddingY="L" gap="S">
|
<AutomationList />
|
||||||
<Button cta wide on:click={modal.show}>Add automation</Button>
|
</Layout>
|
||||||
</Layout>
|
</Panel>
|
||||||
<AutomationList />
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<CreateAutomationModal {webhookModal} />
|
|
||||||
</Modal>
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<Modal bind:this={modal}>
|
||||||
.nav {
|
<CreateAutomationModal {webhookModal} />
|
||||||
overflow-y: auto;
|
</Modal>
|
||||||
background: var(--background);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
position: relative;
|
|
||||||
border-right: var(--border-light);
|
|
||||||
padding-bottom: 60px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import { database } from "stores/backend"
|
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
|
@ -10,48 +8,37 @@
|
||||||
Layout,
|
Layout,
|
||||||
Body,
|
Body,
|
||||||
Icon,
|
Icon,
|
||||||
|
Label,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { TriggerStepID } from "constants/backend/automations"
|
import { TriggerStepID } from "constants/backend/automations"
|
||||||
|
|
||||||
|
export let webhookModal
|
||||||
|
|
||||||
let name
|
let name
|
||||||
let selectedTrigger
|
let selectedTrigger
|
||||||
let nameTouched = false
|
let nameTouched = false
|
||||||
let triggerVal
|
let triggerVal
|
||||||
export let webhookModal
|
|
||||||
|
|
||||||
$: instanceId = $database._id
|
|
||||||
$: nameError =
|
$: nameError =
|
||||||
nameTouched && !name ? "Please specify a name for the automation." : null
|
nameTouched && !name ? "Please specify a name for the automation." : null
|
||||||
|
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)
|
||||||
|
|
||||||
async function createAutomation() {
|
async function createAutomation() {
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.create({
|
const trigger = automationStore.actions.constructBlock(
|
||||||
name,
|
|
||||||
instanceId,
|
|
||||||
})
|
|
||||||
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
|
||||||
"TRIGGER",
|
"TRIGGER",
|
||||||
triggerVal.stepId,
|
triggerVal.stepId,
|
||||||
triggerVal
|
triggerVal
|
||||||
)
|
)
|
||||||
|
await automationStore.actions.create(name, trigger)
|
||||||
automationStore.actions.addBlockToAutomation(newBlock)
|
|
||||||
if (triggerVal.stepId === TriggerStepID.WEBHOOK) {
|
if (triggerVal.stepId === TriggerStepID.WEBHOOK) {
|
||||||
webhookModal.show
|
webhookModal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
|
|
||||||
notifications.success(`Automation ${name} created`)
|
notifications.success(`Automation ${name} created`)
|
||||||
|
|
||||||
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error creating automation")
|
notifications.error("Error creating automation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)
|
|
||||||
|
|
||||||
const selectTrigger = trigger => {
|
const selectTrigger = trigger => {
|
||||||
triggerVal = trigger
|
triggerVal = trigger
|
||||||
|
@ -70,9 +57,9 @@
|
||||||
header="You must publish your app to activate your automations."
|
header="You must publish your app to activate your automations."
|
||||||
message="To test your automation before publishing, you can use the 'Run Test' functionality on the next screen."
|
message="To test your automation before publishing, you can use the 'Run Test' functionality on the next screen."
|
||||||
/>
|
/>
|
||||||
<Body size="XS"
|
<Body size="S">
|
||||||
>Please name your automation, then select a trigger. Every automation must
|
Please name your automation, then select a trigger.<br />
|
||||||
start with a trigger.
|
Every automation must start with a trigger.
|
||||||
</Body>
|
</Body>
|
||||||
<Input
|
<Input
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
|
@ -81,9 +68,8 @@
|
||||||
label="Name"
|
label="Name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Layout noPadding>
|
<Layout noPadding gap="XS">
|
||||||
<Body size="S">Triggers</Body>
|
<Label size="S">Trigger</Label>
|
||||||
|
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each triggers as [idx, trigger]}
|
{#each triggers as [idx, trigger]}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { ActionMenu, MenuItem, notifications, Icon } from "@budibase/bbui"
|
import { ActionMenu, MenuItem, notifications, Icon } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
@ -14,7 +13,6 @@
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.delete(automation)
|
await automationStore.actions.delete(automation)
|
||||||
notifications.success("Automation deleted successfully")
|
notifications.success("Automation deleted successfully")
|
||||||
$goto("../automate")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error deleting automation")
|
notifications.error("Error deleting automation")
|
||||||
}
|
}
|
||||||
|
@ -24,7 +22,6 @@
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.duplicate(automation)
|
await automationStore.actions.duplicate(automation)
|
||||||
notifications.success("Automation has been duplicated successfully")
|
notifications.success("Automation has been duplicated successfully")
|
||||||
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error duplicating automation")
|
notifications.error("Error duplicating automation")
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import { Icon, Input, ModalContent, Modal } from "@budibase/bbui"
|
import { Icon, Input, ModalContent, Modal } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let automation
|
||||||
|
export let onCancel = undefined
|
||||||
|
|
||||||
let name
|
let name
|
||||||
let error = ""
|
let error = ""
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
export let automation
|
|
||||||
export let onCancel = undefined
|
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
name = automation?.name
|
name = automation?.name
|
||||||
modal.show()
|
modal.show()
|
||||||
|
|
|
@ -15,8 +15,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import { automationStore } from "builderStore"
|
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { environment, licensing } from "stores/portal"
|
import { environment, licensing } from "stores/portal"
|
||||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||||
|
@ -50,22 +49,8 @@
|
||||||
$: filters = lookForFilters(schemaProperties) || []
|
$: filters = lookForFilters(schemaProperties) || []
|
||||||
$: tempFilters = filters
|
$: tempFilters = filters
|
||||||
$: stepId = block.stepId
|
$: stepId = block.stepId
|
||||||
$: bindings = getAvailableBindings(
|
$: bindings = getAvailableBindings(block, $selectedAutomation?.definition)
|
||||||
block || $automationStore.selectedBlock,
|
|
||||||
$automationStore.selectedAutomation?.automation?.definition
|
|
||||||
)
|
|
||||||
|
|
||||||
$: getInputData(testData, block.inputs)
|
$: getInputData(testData, block.inputs)
|
||||||
const getInputData = (testData, blockInputs) => {
|
|
||||||
let newInputData = testData || blockInputs
|
|
||||||
|
|
||||||
if (block.event === "app:trigger" && !newInputData?.fields) {
|
|
||||||
newInputData = cloneDeep(blockInputs)
|
|
||||||
}
|
|
||||||
|
|
||||||
inputData = newInputData
|
|
||||||
}
|
|
||||||
|
|
||||||
$: tableId = inputData ? inputData.tableId : null
|
$: tableId = inputData ? inputData.tableId : null
|
||||||
$: table = tableId
|
$: table = tableId
|
||||||
? $tables.list.find(table => table._id === inputData.tableId)
|
? $tables.list.find(table => table._id === inputData.tableId)
|
||||||
|
@ -76,39 +61,48 @@
|
||||||
$: isTrigger = block?.type === "TRIGGER"
|
$: isTrigger = block?.type === "TRIGGER"
|
||||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||||
|
|
||||||
|
const getInputData = (testData, blockInputs) => {
|
||||||
|
let newInputData = testData || blockInputs
|
||||||
|
if (block.event === "app:trigger" && !newInputData?.fields) {
|
||||||
|
newInputData = cloneDeep(blockInputs)
|
||||||
|
}
|
||||||
|
inputData = newInputData
|
||||||
|
}
|
||||||
|
|
||||||
const onChange = Utils.sequential(async (e, key) => {
|
const onChange = Utils.sequential(async (e, key) => {
|
||||||
|
// We need to cache the schema as part of the definition because it is
|
||||||
|
// used in the server to detect relationships. It would be far better to
|
||||||
|
// instead fetch the schema in the backend at runtime.
|
||||||
|
let schema
|
||||||
if (e.detail?.tableId) {
|
if (e.detail?.tableId) {
|
||||||
const tableSchema = getSchemaForTable(e.detail.tableId, {
|
schema = getSchemaForTable(e.detail.tableId, {
|
||||||
searchableSchema: true,
|
searchableSchema: true,
|
||||||
}).schema
|
}).schema
|
||||||
if (isTestModal) {
|
|
||||||
testData.schema = tableSchema
|
|
||||||
} else {
|
|
||||||
block.inputs.schema = tableSchema
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isTestModal) {
|
if (isTestModal) {
|
||||||
|
let newTestData = { schema }
|
||||||
|
|
||||||
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
|
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
|
||||||
if (stepId === TriggerStepID.WEBHOOK) {
|
if (stepId === TriggerStepID.WEBHOOK) {
|
||||||
automationStore.actions.addTestDataToAutomation({
|
newTestData = {
|
||||||
|
...newTestData,
|
||||||
body: {
|
body: {
|
||||||
[key]: e.detail,
|
[key]: e.detail,
|
||||||
...$automationStore.selectedAutomation.automation.testData?.body,
|
...$selectedAutomation.testData?.body,
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
automationStore.actions.addTestDataToAutomation({
|
newTestData = {
|
||||||
|
...newTestData,
|
||||||
[key]: e.detail,
|
[key]: e.detail,
|
||||||
})
|
}
|
||||||
testData[key] = e.detail
|
await automationStore.actions.addTestDataToAutomation(newTestData)
|
||||||
} else {
|
} else {
|
||||||
block.inputs[key] = e.detail
|
const data = { schema, [key]: e.detail }
|
||||||
|
await automationStore.actions.updateBlockInputs(block, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving automation")
|
notifications.error("Error saving automation")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,11 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
if (e.detail === value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
value = e.detail
|
value = e.detail
|
||||||
dispatch("change", e.detail)
|
dispatch("change", e.detail)
|
||||||
}
|
}
|
||||||
|
@ -43,7 +47,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="block-field">
|
<div class="block-field">
|
||||||
<Input on:change={onChange} {value} on:blur={() => (touched = true)} />
|
<Input
|
||||||
|
on:change={onChange}
|
||||||
|
{value}
|
||||||
|
on:blur={() => (touched = true)}
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
{#if touched && !value}
|
{#if touched && !value}
|
||||||
<Label><div class="error">Please specify a CRON expression</div></Label>
|
<Label><div class="error">Please specify a CRON expression</div></Label>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, notifications } from "@budibase/bbui"
|
import { Icon, notifications } from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import WebhookDisplay from "./WebhookDisplay.svelte"
|
import WebhookDisplay from "./WebhookDisplay.svelte"
|
||||||
import { ModalContent } from "@budibase/bbui"
|
import { ModalContent } from "@budibase/bbui"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
|
||||||
const POLL_RATE_MS = 2500
|
const POLL_RATE_MS = 2500
|
||||||
|
|
||||||
let interval
|
let interval
|
||||||
let finished = false
|
let finished = false
|
||||||
let schemaURL
|
let schemaURL
|
||||||
let propCount = 0
|
let propCount = 0
|
||||||
|
|
||||||
$: automation = $automationStore.selectedAutomation?.automation
|
$: automation = $selectedAutomation
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!automation?.definition?.trigger?.inputs.schemaUrl) {
|
if (!automation?.definition?.trigger?.inputs.schemaUrl) {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let store
|
||||||
|
|
||||||
|
const handleKeyPress = e => {
|
||||||
|
if (!(e.ctrlKey || e.metaKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.shiftKey && e.key === "Z") {
|
||||||
|
store.redo()
|
||||||
|
} else if (e.key === "z") {
|
||||||
|
store.undo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener("keydown", handleKeyPress)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyPress)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="undo-redo">
|
||||||
|
<Icon
|
||||||
|
name="Undo"
|
||||||
|
hoverable
|
||||||
|
on:click={store.undo}
|
||||||
|
disabled={!$store.canUndo}
|
||||||
|
tooltip="Undo latest change"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="Redo"
|
||||||
|
hoverable
|
||||||
|
on:click={store.redo}
|
||||||
|
disabled={!$store.canRedo}
|
||||||
|
tooltip="Redo latest undo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.undo-redo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
padding-right: var(--spacing-xl);
|
||||||
|
border-right: var(--border-light);
|
||||||
|
}
|
||||||
|
.undo-redo :global(svg) {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -42,29 +42,22 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.create({
|
let trigger = automationStore.actions.constructBlock(
|
||||||
name: parameters.newAutomationName,
|
|
||||||
})
|
|
||||||
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
|
|
||||||
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
|
||||||
"TRIGGER",
|
"TRIGGER",
|
||||||
"APP",
|
"APP",
|
||||||
appActionDefinition
|
$automationStore.blockDefinitions.TRIGGER.APP
|
||||||
)
|
)
|
||||||
|
trigger.inputs = {
|
||||||
newBlock.inputs = {
|
|
||||||
fields: Object.keys(parameters.fields ?? {}).reduce((fields, key) => {
|
fields: Object.keys(parameters.fields ?? {}).reduce((fields, key) => {
|
||||||
fields[key] = "string"
|
fields[key] = "string"
|
||||||
return fields
|
return fields
|
||||||
}, {}),
|
}, {}),
|
||||||
}
|
}
|
||||||
|
const automation = await automationStore.actions.create(
|
||||||
automationStore.actions.addBlockToAutomation(newBlock)
|
parameters.newAutomationName,
|
||||||
await automationStore.actions.save(
|
trigger
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
)
|
||||||
parameters.automationId =
|
parameters.automationId = automation._id
|
||||||
$automationStore.selectedAutomation.automation._id
|
|
||||||
delete parameters.newAutomationName
|
delete parameters.newAutomationName
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error creating automation")
|
notifications.error("Error creating automation")
|
||||||
|
|
|
@ -28,10 +28,10 @@
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
const { name, url } = $values
|
const { url } = $values
|
||||||
|
|
||||||
validation.check({
|
validation.check({
|
||||||
name,
|
...$values,
|
||||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -95,9 +95,9 @@
|
||||||
appValidation.url(validation, { apps: applications })
|
appValidation.url(validation, { apps: applications })
|
||||||
appValidation.file(validation, { template })
|
appValidation.file(validation, { template })
|
||||||
// init validation
|
// init validation
|
||||||
const { name, url } = $values
|
const { url } = $values
|
||||||
validation.check({
|
validation.check({
|
||||||
name,
|
...$values,
|
||||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,10 @@
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
const { name, url } = $values
|
const { url } = $values
|
||||||
|
|
||||||
validation.check({
|
validation.check({
|
||||||
name,
|
...$values,
|
||||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -37,9 +37,9 @@
|
||||||
appValidation.name(validation, { apps: applications, currentApp: app })
|
appValidation.name(validation, { apps: applications, currentApp: app })
|
||||||
appValidation.url(validation, { apps: applications, currentApp: app })
|
appValidation.url(validation, { apps: applications, currentApp: app })
|
||||||
// init validation
|
// init validation
|
||||||
const { name, url } = $values
|
const { url } = $values
|
||||||
validation.check({
|
validation.check({
|
||||||
name,
|
...$values,
|
||||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,6 +149,7 @@
|
||||||
<Layout gap="XS" noPadding justifyItems="center">
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
<Button
|
<Button
|
||||||
cta
|
cta
|
||||||
|
size="L"
|
||||||
disabled={Object.keys(errors).length > 0 || submitted}
|
disabled={Object.keys(errors).length > 0 || submitted}
|
||||||
on:click={save}
|
on:click={save}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,30 +1,40 @@
|
||||||
<script>
|
<script>
|
||||||
import { Heading, Body, Layout, Button, Modal } from "@budibase/bbui"
|
import { Heading, Body, Layout, Button, Modal } from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
|
import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
|
||||||
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
|
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
|
||||||
import { onMount } from "svelte"
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
|
import * as routify from "@roxi/routify"
|
||||||
|
|
||||||
$: automation =
|
// Keep URL and state in sync for selected screen ID
|
||||||
$automationStore.selectedAutomation?.automation ||
|
const stopSyncing = syncURLToState({
|
||||||
$automationStore.automations[0]
|
urlParam: "automationId",
|
||||||
|
stateKey: "selectedAutomationId",
|
||||||
|
validate: id => $automationStore.automations.some(x => x._id === id),
|
||||||
|
fallbackUrl: "./index",
|
||||||
|
store: automationStore,
|
||||||
|
up: automationStore.actions.select,
|
||||||
|
routify,
|
||||||
|
})
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
let webhookModal
|
let webhookModal
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
$automationStore.showTestPanel = false
|
$automationStore.showTestPanel = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onDestroy(stopSyncing)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- routify:options index=3 -->
|
<!-- routify:options index=3 -->
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="nav">
|
<AutomationPanel {modal} {webhookModal} />
|
||||||
<AutomationPanel {modal} {webhookModal} />
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#if automation}
|
{#if $automationStore.automations?.length}
|
||||||
<slot />
|
<slot />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="centered">
|
<div class="centered">
|
||||||
|
@ -40,9 +50,9 @@
|
||||||
</svg>
|
</svg>
|
||||||
<Heading size="M">You have no automations</Heading>
|
<Heading size="M">You have no automations</Heading>
|
||||||
<Body size="M">Let's fix that. Call the bots!</Body>
|
<Body size="M">Let's fix that. Call the bots!</Body>
|
||||||
<Button on:click={() => modal.show()} size="M" cta
|
<Button on:click={() => modal.show()} size="M" cta>
|
||||||
>Create automation</Button
|
Create automation
|
||||||
>
|
</Button>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,7 +61,7 @@
|
||||||
|
|
||||||
{#if $automationStore.showTestPanel}
|
{#if $automationStore.showTestPanel}
|
||||||
<div class="setup">
|
<div class="setup">
|
||||||
<TestPanel {automation} />
|
<TestPanel automation={$selectedAutomation} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
@ -71,22 +81,8 @@
|
||||||
grid-template-columns: 260px minmax(510px, 1fr) fit-content(500px);
|
grid-template-columns: 260px minmax(510px, 1fr) fit-content(500px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
border-right: var(--border-light);
|
|
||||||
background-color: var(--background);
|
|
||||||
padding-bottom: 60px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: var(--spacing-l);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect, leftover } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
|
|
||||||
onMount(async () => {
|
$: {
|
||||||
// navigate to first automation in list, if not already selected
|
if ($automationStore.automations?.length) {
|
||||||
if (
|
|
||||||
!$leftover &&
|
|
||||||
$automationStore.automations.length > 0 &&
|
|
||||||
(!$automationStore.selectedAutomation ||
|
|
||||||
!$automationStore.selectedAutomation?.automation?._id)
|
|
||||||
) {
|
|
||||||
$redirect(`./${$automationStore.automations[0]._id}`)
|
$redirect(`./${$automationStore.automations[0]._id}`)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
|
import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
|
||||||
import AppPreview from "./AppPreview.svelte"
|
import AppPreview from "./AppPreview.svelte"
|
||||||
import { store, sortedScreens } from "builderStore"
|
import { store, sortedScreens, screenHistoryStore } from "builderStore"
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||||
|
import { isActive } from "@roxi/routify"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-panel">
|
<div class="app-panel">
|
||||||
|
@ -22,6 +24,9 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
{#if $isActive("./screens") || $isActive("./components")}
|
||||||
|
<UndoRedoControl store={screenHistoryStore} />
|
||||||
|
{/if}
|
||||||
{#if $store.clientFeatures.devicePreview}
|
{#if $store.clientFeatures.devicePreview}
|
||||||
<DevicePreviewSelect />
|
<DevicePreviewSelect />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -52,6 +57,7 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.header-left,
|
.header-left,
|
||||||
.header-right {
|
.header-right {
|
||||||
|
@ -59,7 +65,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
.header-left {
|
.header-left {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
|
@ -12,30 +12,30 @@
|
||||||
let componentToEject
|
let componentToEject
|
||||||
|
|
||||||
const keyHandlers = {
|
const keyHandlers = {
|
||||||
["^ArrowUp"]: async component => {
|
["Ctrl+ArrowUp"]: async component => {
|
||||||
await store.actions.components.moveUp(component)
|
await store.actions.components.moveUp(component)
|
||||||
},
|
},
|
||||||
["^ArrowDown"]: async component => {
|
["Ctrl+ArrowDown"]: async component => {
|
||||||
await store.actions.components.moveDown(component)
|
await store.actions.components.moveDown(component)
|
||||||
},
|
},
|
||||||
["^c"]: component => {
|
["Ctrl+c"]: component => {
|
||||||
store.actions.components.copy(component, false)
|
store.actions.components.copy(component, false)
|
||||||
},
|
},
|
||||||
["^x"]: component => {
|
["Ctrl+x"]: component => {
|
||||||
store.actions.components.copy(component, true)
|
store.actions.components.copy(component, true)
|
||||||
},
|
},
|
||||||
["^v"]: async component => {
|
["Ctrl+v"]: async component => {
|
||||||
await store.actions.components.paste(component, "inside")
|
await store.actions.components.paste(component, "inside")
|
||||||
},
|
},
|
||||||
["^d"]: async component => {
|
["Ctrl+d"]: async component => {
|
||||||
store.actions.components.copy(component)
|
store.actions.components.copy(component)
|
||||||
await store.actions.components.paste(component, "below")
|
await store.actions.components.paste(component, "below")
|
||||||
},
|
},
|
||||||
["^e"]: component => {
|
["Ctrl+e"]: component => {
|
||||||
componentToEject = component
|
componentToEject = component
|
||||||
confirmEjectDialog.show()
|
confirmEjectDialog.show()
|
||||||
},
|
},
|
||||||
["^Enter"]: () => {
|
["Ctrl+Enter"]: () => {
|
||||||
$goto("./new")
|
$goto("./new")
|
||||||
},
|
},
|
||||||
["Delete"]: component => {
|
["Delete"]: component => {
|
||||||
|
@ -53,14 +53,19 @@
|
||||||
store.actions.components.selectNext()
|
store.actions.components.selectNext()
|
||||||
},
|
},
|
||||||
["Escape"]: () => {
|
["Escape"]: () => {
|
||||||
if (!$isActive("/new")) {
|
if ($isActive("./new")) {
|
||||||
return false
|
$goto("./")
|
||||||
}
|
}
|
||||||
$goto("./")
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyAction = async (event, component, key, ctrlKey = false) => {
|
const handleKeyAction = async ({
|
||||||
|
event,
|
||||||
|
component,
|
||||||
|
key,
|
||||||
|
ctrlKey = false,
|
||||||
|
shiftKey = false,
|
||||||
|
}) => {
|
||||||
if (!component || !key) {
|
if (!component || !key) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -69,9 +74,12 @@
|
||||||
if (key === "Backspace") {
|
if (key === "Backspace") {
|
||||||
key = "Delete"
|
key = "Delete"
|
||||||
}
|
}
|
||||||
// Prefix key with a caret for ctrl modifier
|
// Prefix keys for modifiers
|
||||||
|
if (shiftKey) {
|
||||||
|
key = "Shift+" + key
|
||||||
|
}
|
||||||
if (ctrlKey) {
|
if (ctrlKey) {
|
||||||
key = "^" + key
|
key = "Ctrl+" + key
|
||||||
}
|
}
|
||||||
const handler = keyHandlers[key]
|
const handler = keyHandlers[key]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
|
@ -97,19 +105,26 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Key events are always for the selected component
|
// Key events are always for the selected component
|
||||||
return await handleKeyAction(
|
return await handleKeyAction({
|
||||||
e,
|
event: e,
|
||||||
$selectedComponent,
|
component: $selectedComponent,
|
||||||
e.key,
|
key: e.key,
|
||||||
e.ctrlKey || e.metaKey
|
ctrlKey: e.ctrlKey || e.metaKey,
|
||||||
)
|
shiftKey: e.shiftKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleComponentMenu = async e => {
|
const handleComponentMenu = async e => {
|
||||||
// Menu events can be for any component
|
// Menu events can be for any component
|
||||||
const { id, key, ctrlKey } = e.detail
|
const { id, key, ctrlKey, shiftKey } = e.detail
|
||||||
const component = findComponent($selectedScreen.props, id)
|
const component = findComponent($selectedScreen.props, id)
|
||||||
return await handleKeyAction(null, component, key, ctrlKey)
|
return await handleKeyAction({
|
||||||
|
event: null,
|
||||||
|
component,
|
||||||
|
key,
|
||||||
|
ctrlKey,
|
||||||
|
shiftKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { ModalContent, Body, Layout, Icon, Heading } from "@budibase/bbui"
|
import { ModalContent, Body, Layout, Icon, Heading } from "@budibase/bbui"
|
||||||
|
import blankScreenPreview from "./blankScreenPreview.png"
|
||||||
|
import listScreenPreview from "./listScreenPreview.png"
|
||||||
|
|
||||||
export let onConfirm
|
export let onConfirm
|
||||||
export let onCancel
|
export let onCancel
|
||||||
|
|
||||||
let autoCreateModeKey = "autoCreate"
|
let listScreenModeKey = "autoCreate"
|
||||||
let blankScreenModeKey = "blankScreen"
|
let blankScreenModeKey = "blankScreen"
|
||||||
|
|
||||||
let selectedScreenMode
|
let selectedScreenMode
|
||||||
|
@ -23,61 +25,77 @@
|
||||||
onConfirm={confirmScreenSelection}
|
onConfirm={confirmScreenSelection}
|
||||||
{onCancel}
|
{onCancel}
|
||||||
disabled={!selectedScreenMode}
|
disabled={!selectedScreenMode}
|
||||||
size="L"
|
size="M"
|
||||||
>
|
>
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div
|
<div
|
||||||
class="screen-type item"
|
class="screen-type item blankView"
|
||||||
class:selected={selectedScreenMode == blankScreenModeKey}
|
class:selected={selectedScreenMode == blankScreenModeKey}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedScreenMode = blankScreenModeKey
|
selectedScreenMode = blankScreenModeKey
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="content screen-type-wrap">
|
<div class="content screen-type-wrap">
|
||||||
<Icon name="WebPage" />
|
<img
|
||||||
|
alt="blank screen preview"
|
||||||
|
class="preview"
|
||||||
|
src={blankScreenPreview}
|
||||||
|
/>
|
||||||
<div class="screen-type-text">
|
<div class="screen-type-text">
|
||||||
<Heading size="XS">Blank screen</Heading>
|
<Heading size="XS">Blank screen</Heading>
|
||||||
<Body size="S">Add a blank screen</Body>
|
<Body size="S">Add an empty blank screen</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style="color: var(--spectrum-global-color-green-600); float: right"
|
style="color: var(--spectrum-global-color-green-600); float: right"
|
||||||
>
|
>
|
||||||
{#if selectedScreenMode == blankScreenModeKey}
|
<div
|
||||||
<div class="checkmark-spacing">
|
class={`checkmark-spacing ${
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
selectedScreenMode == blankScreenModeKey ? "visible" : ""
|
||||||
</div>
|
}`}
|
||||||
{/if}
|
>
|
||||||
|
<Icon size="S" name="CheckmarkCircle" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="listViewTitle">
|
||||||
|
<Heading size="XS">Quickly create a screen from your data</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="screen-type item"
|
class="screen-type item"
|
||||||
class:selected={selectedScreenMode == autoCreateModeKey}
|
class:selected={selectedScreenMode == listScreenModeKey}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedScreenMode = autoCreateModeKey
|
selectedScreenMode = listScreenModeKey
|
||||||
}}
|
}}
|
||||||
class:disabled={!$tables.list.filter(table => table._id !== "ta_users")
|
class:disabled={!$tables.list.filter(table => table._id !== "ta_users")
|
||||||
.length}
|
.length}
|
||||||
>
|
>
|
||||||
<div class="content screen-type-wrap">
|
<div class="content screen-type-wrap">
|
||||||
<Icon name="WebPages" />
|
<img
|
||||||
|
alt="list screen preview"
|
||||||
|
class="preview"
|
||||||
|
src={listScreenPreview}
|
||||||
|
/>
|
||||||
<div class="screen-type-text">
|
<div class="screen-type-text">
|
||||||
<Heading size="XS">Autogenerated screens</Heading>
|
<Heading size="XS">List view</Heading>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Add autogenerated screens with CRUD functionality to get a working
|
Create, edit and view your data in a list view screen with side
|
||||||
app quickly! (Requires a datasource)
|
panel
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style="color: var(--spectrum-global-color-green-600); float: right"
|
style="color: var(--spectrum-global-color-green-600); float: right"
|
||||||
>
|
>
|
||||||
{#if selectedScreenMode == autoCreateModeKey}
|
<div
|
||||||
<div class="checkmark-spacing">
|
class={`checkmark-spacing ${
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
selectedScreenMode == listScreenModeKey ? "visible" : ""
|
||||||
</div>
|
}`}
|
||||||
{/if}
|
>
|
||||||
|
<Icon size="S" name="CheckmarkCircle" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -85,9 +103,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.screen-type.item {
|
|
||||||
padding: var(--spectrum-alias-item-padding-xl);
|
|
||||||
}
|
|
||||||
.screen-type-wrap {
|
.screen-type-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -99,6 +114,7 @@
|
||||||
}
|
}
|
||||||
.checkmark-spacing {
|
.checkmark-spacing {
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-m);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
letter-spacing: 0px;
|
letter-spacing: 0px;
|
||||||
|
@ -106,7 +122,6 @@
|
||||||
.item {
|
.item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||||
padding: var(--spectrum-alias-item-padding-s);
|
|
||||||
background: var(--spectrum-alias-background-color-secondary);
|
background: var(--spectrum-alias-background-color-secondary);
|
||||||
transition: 0.3s all;
|
transition: 0.3s all;
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
@ -132,4 +147,19 @@
|
||||||
.screen-type-wrap :global(.spectrum-Heading) {
|
.screen-type-wrap :global(.spectrum-Heading) {
|
||||||
padding-bottom: var(--spectrum-alias-item-padding-s);
|
padding-bottom: var(--spectrum-alias-item-padding-s);
|
||||||
}
|
}
|
||||||
|
.preview {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listViewTitle {
|
||||||
|
margin-top: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blankView {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
let errors = {}
|
let errors = {}
|
||||||
|
|
||||||
const routeTaken = url => {
|
const routeTaken = url => {
|
||||||
const roleId = get(selectedScreen)?.routing.roleId || "BASIC"
|
const roleId = get(selectedScreen).routing.roleId || "BASIC"
|
||||||
return get(store).screens.some(
|
return get(store).screens.some(
|
||||||
screen =>
|
screen =>
|
||||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleTaken = roleId => {
|
const roleTaken = roleId => {
|
||||||
const url = get(selectedScreen)?.routing.route
|
const url = get(selectedScreen).routing.route
|
||||||
return get(store).screens.some(
|
return get(store).screens.some(
|
||||||
screen =>
|
screen =>
|
||||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
return sanitizeUrl(val)
|
return sanitizeUrl(val)
|
||||||
},
|
},
|
||||||
validate: route => {
|
validate: route => {
|
||||||
const existingRoute = get(selectedScreen)?.routing.route
|
const existingRoute = get(selectedScreen).routing.route
|
||||||
if (route !== existingRoute && routeTaken(route)) {
|
if (route !== existingRoute && routeTaken(route)) {
|
||||||
return "That URL is already in use for this role"
|
return "That URL is already in use for this role"
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
label: "Access",
|
label: "Access",
|
||||||
control: RoleSelect,
|
control: RoleSelect,
|
||||||
validate: role => {
|
validate: role => {
|
||||||
const existingRole = get(selectedScreen)?.routing.roleId
|
const existingRole = get(selectedScreen).routing.roleId
|
||||||
if (role !== existingRole && roleTaken(role)) {
|
if (role !== existingRole && roleTaken(role)) {
|
||||||
return "That role is already in use for this URL"
|
return "That role is already in use for this URL"
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel
|
<Panel
|
||||||
title={$selectedScreen?.routing.route}
|
title={$selectedScreen.routing.route}
|
||||||
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
||||||
borderLeft
|
borderLeft
|
||||||
>
|
>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
|
@ -5,6 +5,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScreenListPanel />
|
<ScreenListPanel />
|
||||||
{#key $selectedScreen?._id}
|
{#if $selectedScreen}
|
||||||
<ScreenSettingsPanel />
|
{#key $selectedScreen._id}
|
||||||
{/key}
|
<ScreenSettingsPanel />
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
|
|
|
@ -3178,6 +3178,11 @@ fast-glob@^3.0.3:
|
||||||
merge2 "^1.3.0"
|
merge2 "^1.3.0"
|
||||||
micromatch "^4.0.4"
|
micromatch "^4.0.4"
|
||||||
|
|
||||||
|
fast-json-patch@^3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947"
|
||||||
|
integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==
|
||||||
|
|
||||||
fast-json-stable-stringify@^2.0.0:
|
fast-json-stable-stringify@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -26,9 +26,9 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.3.18-alpha.6",
|
"@budibase/backend-core": "2.3.18-alpha.12",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.6",
|
"@budibase/string-templates": "2.3.18-alpha.12",
|
||||||
"@budibase/types": "2.3.18-alpha.6",
|
"@budibase/types": "2.3.18-alpha.12",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.3.18-alpha.6",
|
"@budibase/bbui": "2.3.18-alpha.12",
|
||||||
"@budibase/frontend-core": "2.3.18-alpha.6",
|
"@budibase/frontend-core": "2.3.18-alpha.12",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.6",
|
"@budibase/string-templates": "2.3.18-alpha.12",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.3.18-alpha.6",
|
"@budibase/bbui": "2.3.18-alpha.12",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/sdk",
|
"name": "@budibase/sdk",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"description": "Budibase Public API SDK",
|
"description": "Budibase Public API SDK",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -43,11 +43,11 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "10.0.3",
|
"@apidevtools/swagger-parser": "10.0.3",
|
||||||
"@budibase/backend-core": "2.3.18-alpha.6",
|
"@budibase/backend-core": "2.3.18-alpha.12",
|
||||||
"@budibase/client": "2.3.18-alpha.6",
|
"@budibase/client": "2.3.18-alpha.12",
|
||||||
"@budibase/pro": "2.3.18-alpha.6",
|
"@budibase/pro": "2.3.18-alpha.12",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.6",
|
"@budibase/string-templates": "2.3.18-alpha.12",
|
||||||
"@budibase/types": "2.3.18-alpha.6",
|
"@budibase/types": "2.3.18-alpha.12",
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -65,10 +65,14 @@ export async function create(ctx: BBContext) {
|
||||||
|
|
||||||
// call through to update if already exists
|
// call through to update if already exists
|
||||||
if (automation._id && automation._rev) {
|
if (automation._id && automation._rev) {
|
||||||
return update(ctx)
|
await update(ctx)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
automation._id = generateAutomationID()
|
// Respect existing IDs if recreating a deleted automation
|
||||||
|
if (!automation._id) {
|
||||||
|
automation._id = generateAutomationID()
|
||||||
|
}
|
||||||
|
|
||||||
automation.type = "automation"
|
automation.type = "automation"
|
||||||
automation = cleanAutomationInputs(automation)
|
automation = cleanAutomationInputs(automation)
|
||||||
|
@ -126,6 +130,13 @@ export async function update(ctx: BBContext) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let automation = ctx.request.body
|
let automation = ctx.request.body
|
||||||
automation.appId = ctx.appId
|
automation.appId = ctx.appId
|
||||||
|
|
||||||
|
// Call through to create if it doesn't exist
|
||||||
|
if (!automation._id || !automation._rev) {
|
||||||
|
await create(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const oldAutomation = await db.get(automation._id)
|
const oldAutomation = await db.get(automation._id)
|
||||||
automation = cleanAutomationInputs(automation)
|
automation = cleanAutomationInputs(automation)
|
||||||
automation = await checkForWebhooks({
|
automation = await checkForWebhooks({
|
||||||
|
|
|
@ -142,7 +142,11 @@ function cleanupConfig(config: RunConfig, table: Table): RunConfig {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateIdForRow(row: Row | undefined, table: Table): string {
|
function generateIdForRow(
|
||||||
|
row: Row | undefined,
|
||||||
|
table: Table,
|
||||||
|
isLinked: boolean = false
|
||||||
|
): string {
|
||||||
const primary = table.primary
|
const primary = table.primary
|
||||||
if (!row || !primary) {
|
if (!row || !primary) {
|
||||||
return ""
|
return ""
|
||||||
|
@ -150,8 +154,12 @@ function generateIdForRow(row: Row | undefined, table: Table): string {
|
||||||
// build id array
|
// build id array
|
||||||
let idParts = []
|
let idParts = []
|
||||||
for (let field of primary) {
|
for (let field of primary) {
|
||||||
// need to handle table name + field or just field, depending on if relationships used
|
let fieldValue = extractFieldValue({
|
||||||
const fieldValue = row[`${table.name}.${field}`] || row[field]
|
row,
|
||||||
|
tableName: table.name,
|
||||||
|
fieldName: field,
|
||||||
|
isLinked,
|
||||||
|
})
|
||||||
if (fieldValue) {
|
if (fieldValue) {
|
||||||
idParts.push(fieldValue)
|
idParts.push(fieldValue)
|
||||||
}
|
}
|
||||||
|
@ -174,18 +182,52 @@ function getEndpoint(tableId: string | undefined, operation: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function basicProcessing(row: Row, table: Table): Row {
|
// need to handle table name + field or just field, depending on if relationships used
|
||||||
|
function extractFieldValue({
|
||||||
|
row,
|
||||||
|
tableName,
|
||||||
|
fieldName,
|
||||||
|
isLinked,
|
||||||
|
}: {
|
||||||
|
row: Row
|
||||||
|
tableName: string
|
||||||
|
fieldName: string
|
||||||
|
isLinked: boolean
|
||||||
|
}) {
|
||||||
|
let value = row[`${tableName}.${fieldName}`]
|
||||||
|
if (value == null && !isLinked) {
|
||||||
|
value = row[fieldName]
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function basicProcessing({
|
||||||
|
row,
|
||||||
|
table,
|
||||||
|
isLinked,
|
||||||
|
}: {
|
||||||
|
row: Row
|
||||||
|
table: Table
|
||||||
|
isLinked: boolean
|
||||||
|
}): Row {
|
||||||
const thisRow: Row = {}
|
const thisRow: Row = {}
|
||||||
// filter the row down to what is actually the row (not joined)
|
// filter the row down to what is actually the row (not joined)
|
||||||
for (let fieldName of Object.keys(table.schema)) {
|
for (let field of Object.values(table.schema)) {
|
||||||
const pathValue = row[`${table.name}.${fieldName}`]
|
const fieldName = field.name
|
||||||
const value = pathValue != null ? pathValue : row[fieldName]
|
|
||||||
|
const value = extractFieldValue({
|
||||||
|
row,
|
||||||
|
tableName: table.name,
|
||||||
|
fieldName,
|
||||||
|
isLinked,
|
||||||
|
})
|
||||||
|
|
||||||
// all responses include "select col as table.col" so that overlaps are handled
|
// all responses include "select col as table.col" so that overlaps are handled
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
thisRow[fieldName] = value
|
thisRow[fieldName] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
thisRow._id = generateIdForRow(row, table)
|
thisRow._id = generateIdForRow(row, table, isLinked)
|
||||||
thisRow.tableId = table._id
|
thisRow.tableId = table._id
|
||||||
thisRow._rev = "rev"
|
thisRow._rev = "rev"
|
||||||
return processFormulas(table, thisRow)
|
return processFormulas(table, thisRow)
|
||||||
|
@ -293,7 +335,7 @@ export class ExternalRequest {
|
||||||
// we're not inserting a doc, will be a bunch of update calls
|
// we're not inserting a doc, will be a bunch of update calls
|
||||||
const otherKey: string = field.throughFrom || linkTablePrimary
|
const otherKey: string = field.throughFrom || linkTablePrimary
|
||||||
const thisKey: string = field.throughTo || tablePrimary
|
const thisKey: string = field.throughTo || tablePrimary
|
||||||
row[key].map((relationship: any) => {
|
row[key].forEach((relationship: any) => {
|
||||||
manyRelationships.push({
|
manyRelationships.push({
|
||||||
tableId: field.through || field.tableId,
|
tableId: field.through || field.tableId,
|
||||||
isUpdate: false,
|
isUpdate: false,
|
||||||
|
@ -309,7 +351,7 @@ export class ExternalRequest {
|
||||||
const thisKey: string = "id"
|
const thisKey: string = "id"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const otherKey: string = field.fieldName
|
const otherKey: string = field.fieldName
|
||||||
row[key].map((relationship: any) => {
|
row[key].forEach((relationship: any) => {
|
||||||
manyRelationships.push({
|
manyRelationships.push({
|
||||||
tableId: field.tableId,
|
tableId: field.tableId,
|
||||||
isUpdate: true,
|
isUpdate: true,
|
||||||
|
@ -379,7 +421,8 @@ export class ExternalRequest {
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let linked = basicProcessing(row, linkedTable)
|
|
||||||
|
let linked = basicProcessing({ row, table: linkedTable, isLinked: true })
|
||||||
if (!linked._id) {
|
if (!linked._id) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -427,7 +470,10 @@ export class ExternalRequest {
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const thisRow = fixArrayTypes(basicProcessing(row, table), table)
|
const thisRow = fixArrayTypes(
|
||||||
|
basicProcessing({ row, table, isLinked: false }),
|
||||||
|
table
|
||||||
|
)
|
||||||
if (thisRow._id == null) {
|
if (thisRow._id == null) {
|
||||||
throw "Unable to generate row ID for SQL rows"
|
throw "Unable to generate row ID for SQL rows"
|
||||||
}
|
}
|
||||||
|
@ -567,19 +613,41 @@ export class ExternalRequest {
|
||||||
const { key, tableId, isUpdate, id, ...rest } = relationship
|
const { key, tableId, isUpdate, id, ...rest } = relationship
|
||||||
const body: { [key: string]: any } = processObjectSync(rest, row, {})
|
const body: { [key: string]: any } = processObjectSync(rest, row, {})
|
||||||
const linkTable = this.getTable(tableId)
|
const linkTable = this.getTable(tableId)
|
||||||
// @ts-ignore
|
const relationshipPrimary = linkTable?.primary || []
|
||||||
const linkPrimary = linkTable?.primary[0]
|
const linkPrimary = relationshipPrimary[0]
|
||||||
if (!linkTable || !linkPrimary) {
|
if (!linkTable || !linkPrimary) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const linkSecondary = relationshipPrimary[1]
|
||||||
|
|
||||||
const rows = related[key]?.rows || []
|
const rows = related[key]?.rows || []
|
||||||
const found = rows.find(
|
|
||||||
(row: { [key: string]: any }) =>
|
function relationshipMatchPredicate({
|
||||||
|
row,
|
||||||
|
linkPrimary,
|
||||||
|
linkSecondary,
|
||||||
|
}: {
|
||||||
|
row: { [key: string]: any }
|
||||||
|
linkPrimary: string
|
||||||
|
linkSecondary?: string
|
||||||
|
}) {
|
||||||
|
const matchesPrimaryLink =
|
||||||
row[linkPrimary] === relationship.id ||
|
row[linkPrimary] === relationship.id ||
|
||||||
row[linkPrimary] === body?.[linkPrimary]
|
row[linkPrimary] === body?.[linkPrimary]
|
||||||
|
if (!matchesPrimaryLink || !linkSecondary) {
|
||||||
|
return matchesPrimaryLink
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesSecondayLink = row[linkSecondary] === body?.[linkSecondary]
|
||||||
|
return matchesPrimaryLink && matchesSecondayLink
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRelationship = rows.find((row: { [key: string]: any }) =>
|
||||||
|
relationshipMatchPredicate({ row, linkPrimary, linkSecondary })
|
||||||
)
|
)
|
||||||
const operation = isUpdate ? Operation.UPDATE : Operation.CREATE
|
const operation = isUpdate ? Operation.UPDATE : Operation.CREATE
|
||||||
if (!found) {
|
if (!existingRelationship) {
|
||||||
promises.push(
|
promises.push(
|
||||||
getDatasourceAndQuery({
|
getDatasourceAndQuery({
|
||||||
endpoint: getEndpoint(tableId, operation),
|
endpoint: getEndpoint(tableId, operation),
|
||||||
|
@ -590,7 +658,7 @@ export class ExternalRequest {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// remove the relationship from cache so it isn't adjusted again
|
// remove the relationship from cache so it isn't adjusted again
|
||||||
rows.splice(rows.indexOf(found), 1)
|
rows.splice(rows.indexOf(existingRelationship), 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// finally cleanup anything that needs to be removed
|
// finally cleanup anything that needs to be removed
|
||||||
|
@ -629,10 +697,7 @@ export class ExternalRequest {
|
||||||
* Creating the specific list of fields that we desire, and excluding the ones that are no use to us
|
* Creating the specific list of fields that we desire, and excluding the ones that are no use to us
|
||||||
* is more performant and has the added benefit of protecting against this scenario.
|
* is more performant and has the added benefit of protecting against this scenario.
|
||||||
*/
|
*/
|
||||||
buildFields(
|
buildFields(table: Table, includeRelations: boolean) {
|
||||||
table: Table,
|
|
||||||
includeRelations: IncludeRelationship = IncludeRelationship.INCLUDE
|
|
||||||
) {
|
|
||||||
function extractRealFields(table: Table, existing: string[] = []) {
|
function extractRealFields(table: Table, existing: string[] = []) {
|
||||||
return Object.entries(table.schema)
|
return Object.entries(table.schema)
|
||||||
.filter(
|
.filter(
|
||||||
|
@ -691,6 +756,10 @@ export class ExternalRequest {
|
||||||
}
|
}
|
||||||
filters = buildFilters(id, filters || {}, table)
|
filters = buildFilters(id, filters || {}, table)
|
||||||
const relationships = this.buildRelationships(table)
|
const relationships = this.buildRelationships(table)
|
||||||
|
|
||||||
|
const includeSqlRelationships =
|
||||||
|
config.includeSqlRelationships === IncludeRelationship.INCLUDE
|
||||||
|
|
||||||
// clean up row on ingress using schema
|
// clean up row on ingress using schema
|
||||||
const processed = this.inputProcessing(row, table)
|
const processed = this.inputProcessing(row, table)
|
||||||
row = processed.row
|
row = processed.row
|
||||||
|
@ -708,9 +777,7 @@ export class ExternalRequest {
|
||||||
},
|
},
|
||||||
resource: {
|
resource: {
|
||||||
// have to specify the fields to avoid column overlap (for SQL)
|
// have to specify the fields to avoid column overlap (for SQL)
|
||||||
fields: isSql
|
fields: isSql ? this.buildFields(table, includeSqlRelationships) : [],
|
||||||
? this.buildFields(table, config.includeSqlRelationships)
|
|
||||||
: [],
|
|
||||||
},
|
},
|
||||||
filters,
|
filters,
|
||||||
sort,
|
sort,
|
||||||
|
@ -725,6 +792,7 @@ export class ExternalRequest {
|
||||||
table,
|
table,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// can't really use response right now
|
// can't really use response right now
|
||||||
const response = await getDatasourceAndQuery(json)
|
const response = await getDatasourceAndQuery(json)
|
||||||
// handle many to many relationships now if we know the ID (could be auto increment)
|
// handle many to many relationships now if we know the ID (could be auto increment)
|
||||||
|
|
|
@ -58,7 +58,7 @@ export async function patch(ctx: BBContext) {
|
||||||
return handleRequest(Operation.UPDATE, tableId, {
|
return handleRequest(Operation.UPDATE, tableId, {
|
||||||
id: breakRowIdField(id),
|
id: breakRowIdField(id),
|
||||||
row: inputs,
|
row: inputs,
|
||||||
includeSqlRelationships: IncludeRelationship.EXCLUDE,
|
includeSqlRelationships: IncludeRelationship.INCLUDE,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ router
|
||||||
"/api/automations",
|
"/api/automations",
|
||||||
bodyResource("_id"),
|
bodyResource("_id"),
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
automationValidator(true),
|
automationValidator(false),
|
||||||
controller.update
|
controller.update
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
|
|
|
@ -27,7 +27,9 @@ describe("row api - postgres", () => {
|
||||||
let makeRequest: MakeRequestResponse,
|
let makeRequest: MakeRequestResponse,
|
||||||
postgresDatasource: Datasource,
|
postgresDatasource: Datasource,
|
||||||
primaryPostgresTable: Table,
|
primaryPostgresTable: Table,
|
||||||
auxPostgresTable: Table
|
oneToManyRelationshipInfo: ForeignTableInfo,
|
||||||
|
manyToOneRelationshipInfo: ForeignTableInfo,
|
||||||
|
manyToManyRelationshipInfo: ForeignTableInfo
|
||||||
|
|
||||||
let host: string
|
let host: string
|
||||||
let port: number
|
let port: number
|
||||||
|
@ -67,37 +69,58 @@ describe("row api - postgres", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
auxPostgresTable = await config.createTable({
|
async function createAuxTable(prefix: string) {
|
||||||
name: generator.word({ length: 10 }),
|
return await config.createTable({
|
||||||
type: "external",
|
name: `${prefix}_${generator.word({ length: 6 })}`,
|
||||||
primary: ["id"],
|
type: "external",
|
||||||
schema: {
|
primary: ["id"],
|
||||||
id: {
|
primaryDisplay: "title",
|
||||||
name: "id",
|
schema: {
|
||||||
type: FieldType.AUTO,
|
id: {
|
||||||
constraints: {
|
name: "id",
|
||||||
presence: true,
|
type: FieldType.AUTO,
|
||||||
|
autocolumn: true,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
name: "title",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: {
|
sourceId: postgresDatasource._id,
|
||||||
name: "title",
|
})
|
||||||
type: FieldType.STRING,
|
}
|
||||||
constraints: {
|
|
||||||
presence: true,
|
oneToManyRelationshipInfo = {
|
||||||
},
|
table: await createAuxTable("o2m"),
|
||||||
},
|
fieldName: "oneToManyRelation",
|
||||||
},
|
relationshipType: RelationshipTypes.ONE_TO_MANY,
|
||||||
sourceId: postgresDatasource._id,
|
}
|
||||||
})
|
manyToOneRelationshipInfo = {
|
||||||
|
table: await createAuxTable("m2o"),
|
||||||
|
fieldName: "manyToOneRelation",
|
||||||
|
relationshipType: RelationshipTypes.MANY_TO_ONE,
|
||||||
|
}
|
||||||
|
manyToManyRelationshipInfo = {
|
||||||
|
table: await createAuxTable("m2m"),
|
||||||
|
fieldName: "manyToManyRelation",
|
||||||
|
relationshipType: RelationshipTypes.MANY_TO_MANY,
|
||||||
|
}
|
||||||
|
|
||||||
primaryPostgresTable = await config.createTable({
|
primaryPostgresTable = await config.createTable({
|
||||||
name: generator.word({ length: 10 }),
|
name: `p_${generator.word({ length: 6 })}`,
|
||||||
type: "external",
|
type: "external",
|
||||||
primary: ["id"],
|
primary: ["id"],
|
||||||
schema: {
|
schema: {
|
||||||
id: {
|
id: {
|
||||||
name: "id",
|
name: "id",
|
||||||
type: FieldType.AUTO,
|
type: FieldType.AUTO,
|
||||||
|
autocolumn: true,
|
||||||
constraints: {
|
constraints: {
|
||||||
presence: true,
|
presence: true,
|
||||||
},
|
},
|
||||||
|
@ -117,25 +140,48 @@ describe("row api - postgres", () => {
|
||||||
name: "value",
|
name: "value",
|
||||||
type: FieldType.NUMBER,
|
type: FieldType.NUMBER,
|
||||||
},
|
},
|
||||||
linkedField: {
|
oneToManyRelation: {
|
||||||
type: FieldType.LINK,
|
type: FieldType.LINK,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array",
|
type: "array",
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
fieldName: "foreignField",
|
fieldName: oneToManyRelationshipInfo.fieldName,
|
||||||
name: "linkedField",
|
name: "oneToManyRelation",
|
||||||
relationshipType: RelationshipTypes.ONE_TO_MANY,
|
relationshipType: RelationshipTypes.ONE_TO_MANY,
|
||||||
tableId: auxPostgresTable._id,
|
tableId: oneToManyRelationshipInfo.table._id,
|
||||||
|
main: true,
|
||||||
|
},
|
||||||
|
manyToOneRelation: {
|
||||||
|
type: FieldType.LINK,
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
fieldName: manyToOneRelationshipInfo.fieldName,
|
||||||
|
name: "manyToOneRelation",
|
||||||
|
relationshipType: RelationshipTypes.MANY_TO_ONE,
|
||||||
|
tableId: manyToOneRelationshipInfo.table._id,
|
||||||
|
main: true,
|
||||||
|
},
|
||||||
|
manyToManyRelation: {
|
||||||
|
type: FieldType.LINK,
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
fieldName: manyToManyRelationshipInfo.fieldName,
|
||||||
|
name: "manyToManyRelation",
|
||||||
|
relationshipType: RelationshipTypes.MANY_TO_MANY,
|
||||||
|
tableId: manyToManyRelationshipInfo.table._id,
|
||||||
|
main: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sourceId: postgresDatasource._id,
|
sourceId: postgresDatasource._id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(config.end)
|
||||||
await config.end()
|
|
||||||
})
|
|
||||||
|
|
||||||
function generateRandomPrimaryRowData() {
|
function generateRandomPrimaryRowData() {
|
||||||
return {
|
return {
|
||||||
|
@ -151,22 +197,99 @@ describe("row api - postgres", () => {
|
||||||
value: number
|
value: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ForeignTableInfo = {
|
||||||
|
table: Table
|
||||||
|
fieldName: string
|
||||||
|
relationshipType: RelationshipTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForeignRowsInfo = {
|
||||||
|
row: Row
|
||||||
|
relationshipType: RelationshipTypes
|
||||||
|
}
|
||||||
|
|
||||||
async function createPrimaryRow(opts: {
|
async function createPrimaryRow(opts: {
|
||||||
rowData: PrimaryRowData
|
rowData: PrimaryRowData
|
||||||
createForeignRow?: boolean
|
createForeignRows?: {
|
||||||
|
createOneToMany?: boolean
|
||||||
|
createManyToOne?: number
|
||||||
|
createManyToMany?: number
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
let { rowData } = opts
|
let { rowData } = opts as any
|
||||||
let foreignRow: Row | undefined
|
let foreignRows: ForeignRowsInfo[] = []
|
||||||
if (opts?.createForeignRow) {
|
|
||||||
foreignRow = await config.createRow({
|
async function createForeignRow(tableInfo: ForeignTableInfo) {
|
||||||
tableId: auxPostgresTable._id,
|
const foreignKey = `fk_${tableInfo.table.name}_${tableInfo.fieldName}`
|
||||||
|
|
||||||
|
const foreignRow = await config.createRow({
|
||||||
|
tableId: tableInfo.table._id,
|
||||||
title: generator.name(),
|
title: generator.name(),
|
||||||
})
|
})
|
||||||
|
|
||||||
rowData = {
|
rowData = {
|
||||||
...rowData,
|
...rowData,
|
||||||
[`fk_${auxPostgresTable.name}_foreignField`]: foreignRow.id,
|
[foreignKey]: foreignRow.id,
|
||||||
}
|
}
|
||||||
|
foreignRows.push({
|
||||||
|
row: foreignRow,
|
||||||
|
|
||||||
|
relationshipType: tableInfo.relationshipType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.createForeignRows?.createOneToMany) {
|
||||||
|
const foreignKey = `fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`
|
||||||
|
|
||||||
|
const foreignRow = await config.createRow({
|
||||||
|
tableId: oneToManyRelationshipInfo.table._id,
|
||||||
|
title: generator.name(),
|
||||||
|
})
|
||||||
|
|
||||||
|
rowData = {
|
||||||
|
...rowData,
|
||||||
|
[foreignKey]: foreignRow.id,
|
||||||
|
}
|
||||||
|
foreignRows.push({
|
||||||
|
row: foreignRow,
|
||||||
|
relationshipType: oneToManyRelationshipInfo.relationshipType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < (opts?.createForeignRows?.createManyToOne || 0); i++) {
|
||||||
|
const foreignRow = await config.createRow({
|
||||||
|
tableId: manyToOneRelationshipInfo.table._id,
|
||||||
|
title: generator.name(),
|
||||||
|
})
|
||||||
|
|
||||||
|
rowData = {
|
||||||
|
...rowData,
|
||||||
|
[manyToOneRelationshipInfo.fieldName]:
|
||||||
|
rowData[manyToOneRelationshipInfo.fieldName] || [],
|
||||||
|
}
|
||||||
|
rowData[manyToOneRelationshipInfo.fieldName].push(foreignRow._id)
|
||||||
|
foreignRows.push({
|
||||||
|
row: foreignRow,
|
||||||
|
relationshipType: RelationshipTypes.MANY_TO_ONE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < (opts?.createForeignRows?.createManyToMany || 0); i++) {
|
||||||
|
const foreignRow = await config.createRow({
|
||||||
|
tableId: manyToManyRelationshipInfo.table._id,
|
||||||
|
title: generator.name(),
|
||||||
|
})
|
||||||
|
|
||||||
|
rowData = {
|
||||||
|
...rowData,
|
||||||
|
[manyToManyRelationshipInfo.fieldName]:
|
||||||
|
rowData[manyToManyRelationshipInfo.fieldName] || [],
|
||||||
|
}
|
||||||
|
rowData[manyToManyRelationshipInfo.fieldName].push(foreignRow._id)
|
||||||
|
foreignRows.push({
|
||||||
|
row: foreignRow,
|
||||||
|
relationshipType: RelationshipTypes.MANY_TO_MANY,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = await config.createRow({
|
const row = await config.createRow({
|
||||||
|
@ -174,7 +297,7 @@ describe("row api - postgres", () => {
|
||||||
...rowData,
|
...rowData,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { row, foreignRow }
|
return { row, foreignRows }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createDefaultPgTable() {
|
async function createDefaultPgTable() {
|
||||||
|
@ -198,7 +321,9 @@ describe("row api - postgres", () => {
|
||||||
async function populatePrimaryRows(
|
async function populatePrimaryRows(
|
||||||
count: number,
|
count: number,
|
||||||
opts?: {
|
opts?: {
|
||||||
createForeignRow?: boolean
|
createOneToMany?: boolean
|
||||||
|
createManyToOne?: number
|
||||||
|
createManyToMany?: number
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
|
@ -210,7 +335,7 @@ describe("row api - postgres", () => {
|
||||||
rowData,
|
rowData,
|
||||||
...(await createPrimaryRow({
|
...(await createPrimaryRow({
|
||||||
rowData,
|
rowData,
|
||||||
createForeignRow: opts?.createForeignRow,
|
createForeignRows: opts,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -295,7 +420,7 @@ describe("row api - postgres", () => {
|
||||||
describe("given than a row exists", () => {
|
describe("given than a row exists", () => {
|
||||||
let row: Row
|
let row: Row
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
let rowResponse = _.sample(await populatePrimaryRows(10))!
|
let rowResponse = _.sample(await populatePrimaryRows(1))!
|
||||||
row = rowResponse.row
|
row = rowResponse.row
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -403,7 +528,7 @@ describe("row api - postgres", () => {
|
||||||
let rows: { row: Row; rowData: PrimaryRowData }[]
|
let rows: { row: Row; rowData: PrimaryRowData }[]
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
rows = await populatePrimaryRows(10)
|
rows = await populatePrimaryRows(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("a single row can be retrieved successfully", async () => {
|
it("a single row can be retrieved successfully", async () => {
|
||||||
|
@ -419,34 +544,136 @@ describe("row api - postgres", () => {
|
||||||
|
|
||||||
describe("given a row with relation data", () => {
|
describe("given a row with relation data", () => {
|
||||||
let row: Row
|
let row: Row
|
||||||
let foreignRow: Row
|
let rowData: {
|
||||||
beforeEach(async () => {
|
name: string
|
||||||
let [createdRow] = await populatePrimaryRows(1, {
|
description: string
|
||||||
createForeignRow: true,
|
value: number
|
||||||
|
}
|
||||||
|
let foreignRows: ForeignRowsInfo[]
|
||||||
|
|
||||||
|
describe("with all relationship types", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
let [createdRow] = await populatePrimaryRows(1, {
|
||||||
|
createOneToMany: true,
|
||||||
|
createManyToOne: 3,
|
||||||
|
createManyToMany: 2,
|
||||||
|
})
|
||||||
|
row = createdRow.row
|
||||||
|
rowData = createdRow.rowData
|
||||||
|
foreignRows = createdRow.foreignRows
|
||||||
|
})
|
||||||
|
|
||||||
|
it("only one to many foreign keys are retrieved", async () => {
|
||||||
|
const res = await getRow(primaryPostgresTable._id, row.id)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
|
||||||
|
const one2ManyForeignRows = foreignRows.filter(
|
||||||
|
x => x.relationshipType === RelationshipTypes.ONE_TO_MANY
|
||||||
|
)
|
||||||
|
expect(one2ManyForeignRows).toHaveLength(1)
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
...rowData,
|
||||||
|
id: row.id,
|
||||||
|
tableId: row.tableId,
|
||||||
|
_id: expect.any(String),
|
||||||
|
_rev: expect.any(String),
|
||||||
|
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
|
||||||
|
one2ManyForeignRows[0].row.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
|
||||||
})
|
})
|
||||||
row = createdRow.row
|
|
||||||
foreignRow = createdRow.foreignRow!
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("only foreign keys are retrieved", async () => {
|
describe("with only one to many", () => {
|
||||||
const res = await getRow(primaryPostgresTable._id, row.id)
|
beforeEach(async () => {
|
||||||
|
let [createdRow] = await populatePrimaryRows(1, {
|
||||||
expect(res.status).toBe(200)
|
createOneToMany: true,
|
||||||
|
})
|
||||||
expect(res.body).toEqual({
|
row = createdRow.row
|
||||||
...row,
|
rowData = createdRow.rowData
|
||||||
_id: expect.any(String),
|
foreignRows = createdRow.foreignRows
|
||||||
_rev: expect.any(String),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(res.body.foreignField).toBeUndefined()
|
it("only one to many foreign keys are retrieved", async () => {
|
||||||
|
const res = await getRow(primaryPostgresTable._id, row.id)
|
||||||
|
|
||||||
expect(
|
expect(res.status).toBe(200)
|
||||||
res.body[`fk_${auxPostgresTable.name}_foreignField`]
|
|
||||||
).toBeDefined()
|
expect(foreignRows).toHaveLength(1)
|
||||||
expect(res.body[`fk_${auxPostgresTable.name}_foreignField`]).toBe(
|
|
||||||
foreignRow.id
|
expect(res.body).toEqual({
|
||||||
)
|
...rowData,
|
||||||
|
id: row.id,
|
||||||
|
tableId: row.tableId,
|
||||||
|
_id: expect.any(String),
|
||||||
|
_rev: expect.any(String),
|
||||||
|
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
|
||||||
|
foreignRows[0].row.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("with only many to one", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
let [createdRow] = await populatePrimaryRows(1, {
|
||||||
|
createManyToOne: 3,
|
||||||
|
})
|
||||||
|
row = createdRow.row
|
||||||
|
rowData = createdRow.rowData
|
||||||
|
foreignRows = createdRow.foreignRows
|
||||||
|
})
|
||||||
|
|
||||||
|
it("only one to many foreign keys are retrieved", async () => {
|
||||||
|
const res = await getRow(primaryPostgresTable._id, row.id)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
|
||||||
|
expect(foreignRows).toHaveLength(3)
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
...rowData,
|
||||||
|
id: row.id,
|
||||||
|
tableId: row.tableId,
|
||||||
|
_id: expect.any(String),
|
||||||
|
_rev: expect.any(String),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("with only many to many", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
let [createdRow] = await populatePrimaryRows(1, {
|
||||||
|
createManyToMany: 2,
|
||||||
|
})
|
||||||
|
row = createdRow.row
|
||||||
|
rowData = createdRow.rowData
|
||||||
|
foreignRows = createdRow.foreignRows
|
||||||
|
})
|
||||||
|
|
||||||
|
it("only one to many foreign keys are retrieved", async () => {
|
||||||
|
const res = await getRow(primaryPostgresTable._id, row.id)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
|
||||||
|
expect(foreignRows).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
...rowData,
|
||||||
|
id: row.id,
|
||||||
|
tableId: row.tableId,
|
||||||
|
_id: expect.any(String),
|
||||||
|
_rev: expect.any(String),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -667,31 +894,74 @@ describe("row api - postgres", () => {
|
||||||
const getAll = (tableId: string | undefined, rowId: string | undefined) =>
|
const getAll = (tableId: string | undefined, rowId: string | undefined) =>
|
||||||
makeRequest("get", `/api/${tableId}/${rowId}/enrich`)
|
makeRequest("get", `/api/${tableId}/${rowId}/enrich`)
|
||||||
describe("given a row with relation data", () => {
|
describe("given a row with relation data", () => {
|
||||||
let row: Row, foreignRow: Row | undefined
|
let row: Row, rowData: PrimaryRowData, foreignRows: ForeignRowsInfo[]
|
||||||
|
|
||||||
beforeEach(async () => {
|
describe("with all relationship types", () => {
|
||||||
const rowsInfo = await createPrimaryRow({
|
beforeEach(async () => {
|
||||||
rowData: generateRandomPrimaryRowData(),
|
rowData = generateRandomPrimaryRowData()
|
||||||
createForeignRow: true,
|
const rowsInfo = await createPrimaryRow({
|
||||||
|
rowData,
|
||||||
|
createForeignRows: {
|
||||||
|
createOneToMany: true,
|
||||||
|
createManyToOne: 3,
|
||||||
|
createManyToMany: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
row = rowsInfo.row
|
||||||
|
foreignRows = rowsInfo.foreignRows
|
||||||
})
|
})
|
||||||
|
|
||||||
row = rowsInfo.row
|
it("enrich populates the foreign fields", async () => {
|
||||||
foreignRow = rowsInfo.foreignRow
|
const res = await getAll(primaryPostgresTable._id, row.id)
|
||||||
})
|
|
||||||
|
|
||||||
it("enrich populates the foreign field", async () => {
|
expect(res.status).toBe(200)
|
||||||
const res = await getAll(primaryPostgresTable._id, row.id)
|
|
||||||
|
|
||||||
expect(res.status).toBe(200)
|
const foreignRowsByType = _.groupBy(
|
||||||
|
foreignRows,
|
||||||
expect(foreignRow).toBeDefined()
|
x => x.relationshipType
|
||||||
expect(res.body).toEqual({
|
)
|
||||||
...row,
|
expect(res.body).toEqual({
|
||||||
linkedField: [
|
...rowData,
|
||||||
{
|
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
|
||||||
...foreignRow,
|
foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row.id,
|
||||||
},
|
[oneToManyRelationshipInfo.fieldName]: [
|
||||||
],
|
{
|
||||||
|
...foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row,
|
||||||
|
_id: expect.any(String),
|
||||||
|
_rev: expect.any(String),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[manyToOneRelationshipInfo.fieldName]: [
|
||||||
|
{
|
||||||
|
...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][0].row,
|
||||||
|
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
|
||||||
|
row.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][1].row,
|
||||||
|
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
|
||||||
|
row.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][2].row,
|
||||||
|
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
|
||||||
|
row.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[manyToManyRelationshipInfo.fieldName]: [
|
||||||
|
{
|
||||||
|
...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][0].row,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][1].row,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: row.id,
|
||||||
|
tableId: row.tableId,
|
||||||
|
_id: expect.any(String),
|
||||||
|
_rev: expect.any(String),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -714,7 +984,7 @@ describe("row api - postgres", () => {
|
||||||
const rowsCount = 6
|
const rowsCount = 6
|
||||||
let rows: {
|
let rows: {
|
||||||
row: Row
|
row: Row
|
||||||
foreignRow: Row | undefined
|
foreignRows: ForeignRowsInfo[]
|
||||||
rowData: PrimaryRowData
|
rowData: PrimaryRowData
|
||||||
}[]
|
}[]
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
|
@ -415,9 +415,7 @@ class InternalBuilder {
|
||||||
if (opts.disableReturning) {
|
if (opts.disableReturning) {
|
||||||
return query.insert(parsedBody)
|
return query.insert(parsedBody)
|
||||||
} else {
|
} else {
|
||||||
return query
|
return query.insert(parsedBody).returning("*")
|
||||||
.insert(parsedBody)
|
|
||||||
.returning(generateSelectStatement(json, knex))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -502,9 +500,7 @@ class InternalBuilder {
|
||||||
if (opts.disableReturning) {
|
if (opts.disableReturning) {
|
||||||
return query.update(parsedBody)
|
return query.update(parsedBody)
|
||||||
} else {
|
} else {
|
||||||
return query
|
return query.update(parsedBody).returning("*")
|
||||||
.update(parsedBody)
|
|
||||||
.returning(generateSelectStatement(json, knex))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1278,14 +1278,14 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/backend-core@2.3.18-alpha.6":
|
"@budibase/backend-core@2.3.18-alpha.12":
|
||||||
version "2.3.18-alpha.6"
|
version "2.3.18-alpha.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.6.tgz#e6b3304e96b9469f3ca0f4fcfda8cf234c37e2d7"
|
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.12.tgz#ad1b16be64b78b596af2b5f75647c32e8f6f101a"
|
||||||
integrity sha512-To0kFbB9nZ6p0UO4ScS4PJ0gbqI1PrMWRXJLTv/6GU3PxnsqvH1tbpcleLMz2zeE04e5xdwt6W1oPViELom2gg==
|
integrity sha512-E1NEO+/sNkkRqn/xk9XQmFBO9/dl27w9EB0QGztti/16JV9NgxyDQCJIdGwlD08s1y/lUwOKk0TkSZJs+CTYDw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/nano" "10.1.1"
|
"@budibase/nano" "10.1.1"
|
||||||
"@budibase/pouchdb-replication-stream" "1.2.10"
|
"@budibase/pouchdb-replication-stream" "1.2.10"
|
||||||
"@budibase/types" "2.3.18-alpha.6"
|
"@budibase/types" "2.3.18-alpha.12"
|
||||||
"@shopify/jest-koa-mocks" "5.0.1"
|
"@shopify/jest-koa-mocks" "5.0.1"
|
||||||
"@techpass/passport-openidconnect" "0.3.2"
|
"@techpass/passport-openidconnect" "0.3.2"
|
||||||
aws-cloudfront-sign "2.2.0"
|
aws-cloudfront-sign "2.2.0"
|
||||||
|
@ -1392,13 +1392,13 @@
|
||||||
pouchdb-promise "^6.0.4"
|
pouchdb-promise "^6.0.4"
|
||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
|
|
||||||
"@budibase/pro@2.3.18-alpha.6":
|
"@budibase/pro@2.3.18-alpha.12":
|
||||||
version "2.3.18-alpha.6"
|
version "2.3.18-alpha.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.6.tgz#7c5c221da7da79af79605a00aacac5c60f209bf6"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.12.tgz#be552b3a9f5850e746081540d6586aae69147bec"
|
||||||
integrity sha512-YWPxmZn+z3tm5GZ+2UZSkOAhamlue/dmki+FCML5pIp3dCw8KsXnpzYXHRc1F8yXMTmA/8KBb/YkjQ2WK3Rk7A==
|
integrity sha512-M3b0njzSi47KH6uaQfYPoA2KWrjPiwcU3ONyaVWXHIktVrIKtYaFwOLBr/dmWGfMrL2297SSqg7V4DTaLyAhnw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/backend-core" "2.3.18-alpha.6"
|
"@budibase/backend-core" "2.3.18-alpha.12"
|
||||||
"@budibase/types" "2.3.18-alpha.6"
|
"@budibase/types" "2.3.18-alpha.12"
|
||||||
"@koa/router" "8.0.8"
|
"@koa/router" "8.0.8"
|
||||||
bull "4.10.1"
|
bull "4.10.1"
|
||||||
joi "17.6.0"
|
joi "17.6.0"
|
||||||
|
@ -1424,10 +1424,10 @@
|
||||||
svelte-apexcharts "^1.0.2"
|
svelte-apexcharts "^1.0.2"
|
||||||
svelte-flatpickr "^3.1.0"
|
svelte-flatpickr "^3.1.0"
|
||||||
|
|
||||||
"@budibase/types@2.3.18-alpha.6":
|
"@budibase/types@2.3.18-alpha.12":
|
||||||
version "2.3.18-alpha.6"
|
version "2.3.18-alpha.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.6.tgz#9438ee64008668bbcb3d688b189cc649e03dfd60"
|
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.12.tgz#a63eb978ccc7e55c209b3e9d71f9aecf7facc0d1"
|
||||||
integrity sha512-16YtXwSODS8UDhdxCP2piGDWELP05EZuPbwLsOUFLX3Gt0+Wwkme+XWw4pTPE+GoK/mTVkDxzSc4cvuXWtfxxA==
|
integrity sha512-27o2BmI/HXIR3frZ8FtqHgAe1hd8jPIzgPaEhKrQiYJ/opUVccqupx9ld75Hyk9E6cdXu0UF0/+LxPpUmMugag==
|
||||||
|
|
||||||
"@bull-board/api@3.7.0":
|
"@bull-board/api@3.7.0":
|
||||||
version "3.7.0"
|
version "3.7.0"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.cjs",
|
"main": "src/index.cjs",
|
||||||
"module": "dist/bundle.mjs",
|
"module": "dist/bundle.mjs",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/types",
|
"name": "@budibase/types",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"description": "Budibase types",
|
"description": "Budibase types",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "2.3.18-alpha.6",
|
"version": "2.3.18-alpha.12",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -36,10 +36,10 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.3.18-alpha.6",
|
"@budibase/backend-core": "2.3.18-alpha.12",
|
||||||
"@budibase/pro": "2.3.18-alpha.6",
|
"@budibase/pro": "2.3.18-alpha.12",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.6",
|
"@budibase/string-templates": "2.3.18-alpha.12",
|
||||||
"@budibase/types": "2.3.18-alpha.6",
|
"@budibase/types": "2.3.18-alpha.12",
|
||||||
"@koa/router": "8.0.8",
|
"@koa/router": "8.0.8",
|
||||||
"@sentry/node": "6.17.7",
|
"@sentry/node": "6.17.7",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
|
|
|
@ -475,14 +475,14 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/backend-core@2.3.18-alpha.6":
|
"@budibase/backend-core@2.3.18-alpha.12":
|
||||||
version "2.3.18-alpha.6"
|
version "2.3.18-alpha.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.6.tgz#e6b3304e96b9469f3ca0f4fcfda8cf234c37e2d7"
|
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.12.tgz#ad1b16be64b78b596af2b5f75647c32e8f6f101a"
|
||||||
integrity sha512-To0kFbB9nZ6p0UO4ScS4PJ0gbqI1PrMWRXJLTv/6GU3PxnsqvH1tbpcleLMz2zeE04e5xdwt6W1oPViELom2gg==
|
integrity sha512-E1NEO+/sNkkRqn/xk9XQmFBO9/dl27w9EB0QGztti/16JV9NgxyDQCJIdGwlD08s1y/lUwOKk0TkSZJs+CTYDw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/nano" "10.1.1"
|
"@budibase/nano" "10.1.1"
|
||||||
"@budibase/pouchdb-replication-stream" "1.2.10"
|
"@budibase/pouchdb-replication-stream" "1.2.10"
|
||||||
"@budibase/types" "2.3.18-alpha.6"
|
"@budibase/types" "2.3.18-alpha.12"
|
||||||
"@shopify/jest-koa-mocks" "5.0.1"
|
"@shopify/jest-koa-mocks" "5.0.1"
|
||||||
"@techpass/passport-openidconnect" "0.3.2"
|
"@techpass/passport-openidconnect" "0.3.2"
|
||||||
aws-cloudfront-sign "2.2.0"
|
aws-cloudfront-sign "2.2.0"
|
||||||
|
@ -539,13 +539,13 @@
|
||||||
pouchdb-promise "^6.0.4"
|
pouchdb-promise "^6.0.4"
|
||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
|
|
||||||
"@budibase/pro@2.3.18-alpha.6":
|
"@budibase/pro@2.3.18-alpha.12":
|
||||||
version "2.3.18-alpha.6"
|
version "2.3.18-alpha.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.6.tgz#7c5c221da7da79af79605a00aacac5c60f209bf6"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.12.tgz#be552b3a9f5850e746081540d6586aae69147bec"
|
||||||
integrity sha512-YWPxmZn+z3tm5GZ+2UZSkOAhamlue/dmki+FCML5pIp3dCw8KsXnpzYXHRc1F8yXMTmA/8KBb/YkjQ2WK3Rk7A==
|
integrity sha512-M3b0njzSi47KH6uaQfYPoA2KWrjPiwcU3ONyaVWXHIktVrIKtYaFwOLBr/dmWGfMrL2297SSqg7V4DTaLyAhnw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/backend-core" "2.3.18-alpha.6"
|
"@budibase/backend-core" "2.3.18-alpha.12"
|
||||||
"@budibase/types" "2.3.18-alpha.6"
|
"@budibase/types" "2.3.18-alpha.12"
|
||||||
"@koa/router" "8.0.8"
|
"@koa/router" "8.0.8"
|
||||||
bull "4.10.1"
|
bull "4.10.1"
|
||||||
joi "17.6.0"
|
joi "17.6.0"
|
||||||
|
@ -553,10 +553,10 @@
|
||||||
lru-cache "^7.14.1"
|
lru-cache "^7.14.1"
|
||||||
node-fetch "^2.6.1"
|
node-fetch "^2.6.1"
|
||||||
|
|
||||||
"@budibase/types@2.3.18-alpha.6":
|
"@budibase/types@2.3.18-alpha.12":
|
||||||
version "2.3.18-alpha.6"
|
version "2.3.18-alpha.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.6.tgz#9438ee64008668bbcb3d688b189cc649e03dfd60"
|
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.12.tgz#a63eb978ccc7e55c209b3e9d71f9aecf7facc0d1"
|
||||||
integrity sha512-16YtXwSODS8UDhdxCP2piGDWELP05EZuPbwLsOUFLX3Gt0+Wwkme+XWw4pTPE+GoK/mTVkDxzSc4cvuXWtfxxA==
|
integrity sha512-27o2BmI/HXIR3frZ8FtqHgAe1hd8jPIzgPaEhKrQiYJ/opUVccqupx9ld75Hyk9E6cdXu0UF0/+LxPpUmMugag==
|
||||||
|
|
||||||
"@cspotcode/source-map-support@^0.8.0":
|
"@cspotcode/source-map-support@^0.8.0":
|
||||||
version "0.8.1"
|
version "0.8.1"
|
||||||
|
|
Loading…
Reference in a new issue