diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 16c6c37bbd..e986179cfc 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -68,83 +68,6 @@ jobs: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} 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: needs: [release-images] runs-on: ubuntu-latest diff --git a/lerna.json b/lerna.json index 0a3d923d6c..ee2681c548 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 479a54bd94..6ff40ab4f3 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.18-alpha.6", + "@budibase/types": "2.3.18-alpha.9", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 6cfeb44a7b..9771c2aa43 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.18-alpha.6", + "@budibase/string-templates": "2.3.18-alpha.9", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index 8290acd7cc..452a8c74a1 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -67,6 +67,9 @@ color: var(--spectrum-alias-icon-color-selected-hover) !important; cursor: pointer; } + svg.hoverable:active { + color: var(--spectrum-global-color-blue-400) !important; + } svg.disabled { color: var(--spectrum-global-color-gray-500) !important; diff --git a/packages/bbui/src/InlineAlert/InlineAlert.svelte b/packages/bbui/src/InlineAlert/InlineAlert.svelte index 57e7296234..bd873042b3 100644 --- a/packages/bbui/src/InlineAlert/InlineAlert.svelte +++ b/packages/bbui/src/InlineAlert/InlineAlert.svelte @@ -57,5 +57,7 @@ --spectrum-semantic-negative-icon-color: #e34850; min-width: 100px; margin: 0; + border-color: var(--spectrum-global-color-gray-400); + border-width: 1px; } diff --git a/packages/bbui/src/Label/Label.svelte b/packages/bbui/src/Label/Label.svelte index ee6d9adf76..261ca946ea 100644 --- a/packages/bbui/src/Label/Label.svelte +++ b/packages/bbui/src/Label/Label.svelte @@ -21,7 +21,7 @@ label { padding: 0; white-space: nowrap; - color: var(--spectrum-global-color-gray-600); + color: var(--spectrum-global-color-gray-700); } .muted { diff --git a/packages/bbui/src/Modal/Modal.svelte b/packages/bbui/src/Modal/Modal.svelte index 47420444a2..45081356c1 100644 --- a/packages/bbui/src/Modal/Modal.svelte +++ b/packages/bbui/src/Modal/Modal.svelte @@ -1,7 +1,7 @@ - + onMount(() => { + document.addEventListener("keydown", handleKey) + return () => { + document.removeEventListener("keydown", handleKey) + } + }) + {#if inline} {#if visible} diff --git a/packages/bbui/src/helpers.js b/packages/bbui/src/helpers.js index b02783e0bd..f2246fbb49 100644 --- a/packages/bbui/src/helpers.js +++ b/packages/bbui/src/helpers.js @@ -104,6 +104,9 @@ export const deepSet = (obj, key, value) => { * @param obj the object to clone */ export const cloneDeep = obj => { + if (!obj) { + return obj + } return JSON.parse(JSON.stringify(obj)) } diff --git a/packages/builder/package.json b/packages/builder/package.json index 77b09fdbf3..a4b2c0fc89 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.6", - "@budibase/client": "2.3.18-alpha.6", - "@budibase/frontend-core": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", + "@budibase/bbui": "2.3.18-alpha.9", + "@budibase/client": "2.3.18-alpha.9", + "@budibase/frontend-core": "2.3.18-alpha.9", + "@budibase/string-templates": "2.3.18-alpha.9", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", @@ -72,6 +72,7 @@ "codemirror": "^5.59.0", "dayjs": "^1.11.2", "downloadjs": "1.4.7", + "fast-json-patch": "^3.1.1", "lodash": "4.17.21", "posthog-js": "^1.36.0", "remixicon": "2.5.0", diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 69bca7eac3..d15cdb6e98 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -5,12 +5,47 @@ import { getThemeStore } from "./store/theme" import { derived } from "svelte/store" import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" +import { createHistoryStore } from "builderStore/store/history" +import { get } from "svelte/store" export const store = getFrontendStore() export const automationStore = getAutomationStore() export const themeStore = getThemeStore() 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 => { return $store.screens.find(screen => screen._id === $store.selectedScreenId) }) @@ -71,3 +106,13 @@ export const selectedComponentPath = derived( ).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 + ) +}) diff --git a/packages/builder/src/builderStore/store/automation/Automation.js b/packages/builder/src/builderStore/store/automation/Automation.js deleted file mode 100644 index af0c03cb5a..0000000000 --- a/packages/builder/src/builderStore/store/automation/Automation.js +++ /dev/null @@ -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, - } - } -} diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index af102ab694..dc1e2a2cc1 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -1,16 +1,18 @@ -import { writable } from "svelte/store" +import { writable, get } from "svelte/store" import { API } from "api" -import Automation from "./Automation" import { cloneDeep } from "lodash/fp" +import { generate } from "shortid" +import { selectedAutomation } from "builderStore" const initialAutomationState = { automations: [], + testResults: null, showTestPanel: false, blockDefinitions: { TRIGGER: [], ACTION: [], }, - selectedAutomation: null, + selectedAutomationId: null, } export const getAutomationStore = () => { @@ -37,49 +39,41 @@ const automationActions = store => ({ API.getAutomationDefinitions(), ]) store.update(state => { - let selected = state.selectedAutomation?.automation state.automations = responses[0] + state.automations.sort((a, b) => { + return a.name < b.name ? -1 : 1 + }) state.blockDefinitions = { TRIGGER: responses[1].trigger, 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 }) }, - create: async ({ name }) => { + create: async (name, trigger) => { const automation = { name, type: "automation", definition: { steps: [], + trigger, }, } - const response = await API.createAutomation(automation) - store.update(state => { - state.automations = [...state.automations, response.automation] - store.actions.select(response.automation) - return state - }) + const response = await store.actions.save(automation) + await store.actions.fetch() + store.actions.select(response._id) + return response }, duplicate: async automation => { - const response = await API.createAutomation({ + const response = await store.actions.save({ ...automation, name: `${automation.name} - copy`, _id: undefined, _ref: undefined, }) - store.update(state => { - state.automations = [...state.automations, response.automation] - store.actions.select(response.automation) - return state - }) + await store.actions.fetch() + store.actions.select(response._id) + return response }, save: async automation => { const response = await API.updateAutomation(automation) @@ -90,11 +84,13 @@ const automationActions = store => ({ ) if (existingIdx !== -1) { state.automations.splice(existingIdx, 1, updatedAutomation) - state.automations = [...state.automations] - store.actions.select(updatedAutomation) return state + } else { + state.automations = [...state.automations, updatedAutomation] } + return state }) + return response.automation }, delete: async automation => { await API.deleteAutomation({ @@ -102,34 +98,83 @@ const automationActions = store => ({ automationRev: automation?._rev, }) store.update(state => { - const existingIdx = state.automations.findIndex( - existing => existing._id === automation?._id + // Remove the automation + state.automations = state.automations.filter( + x => x._id !== automation._id ) - state.automations.splice(existingIdx, 1) - state.automations = [...state.automations] - state.selectedAutomation = null - state.selectedBlock = null + // Select a new automation if required + if (automation._id === state.selectedAutomationId) { + store.actions.select(state.automations[0]?._id) + } 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) => { - store.update(state => { - state.selectedAutomation.testResults = null - return state - }) const result = await API.testAutomation({ automationId: automation?._id, testData, }) + if (!result?.trigger && !result?.steps?.length) { + throw "Something went wrong testing your automation" + } store.update(state => { - state.selectedAutomation.testResults = result + state.testResults = result 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 => { - state.selectedAutomation = new Automation(cloneDeep(automation)) - state.selectedBlock = null + state.selectedAutomationId = id + state.testResults = null + state.showTestPanel = false return state }) }, @@ -147,48 +192,57 @@ const automationActions = store => ({ appId, }) }, - addTestDataToAutomation: data => { - store.update(state => { - state.selectedAutomation.addTestData(data) - return state - }) + addTestDataToAutomation: async data => { + let newAutomation = cloneDeep(get(selectedAutomation)) + newAutomation.testData = { + ...newAutomation.testData, + ...data, + } + await store.actions.save(newAutomation) }, - addBlockToAutomation: (block, blockIdx) => { - store.update(state => { - state.selectedBlock = state.selectedAutomation.addBlock( - cloneDeep(block), - blockIdx - ) - return state - }) + constructBlock(type, stepId, blockDefinition) { + return { + ...blockDefinition, + inputs: blockDefinition.inputs || {}, + stepId, + type, + id: generate(), + } }, - toggleFieldControl: value => { - store.update(state => { - state.selectedBlock.rowControl = value - return state - }) + addBlockToAutomation: async (block, blockIdx) => { + const automation = get(selectedAutomation) + let newAutomation = cloneDeep(automation) + if (!automation) { + return + } + newAutomation.definition.steps.splice(blockIdx, 0, block) + await store.actions.save(newAutomation) }, - deleteAutomationBlock: block => { - store.update(state => { - const idx = - state.selectedAutomation.automation.definition.steps.findIndex( - x => x.id === block.id - ) - state.selectedAutomation.deleteBlock(block.id) + /** + * "rowControl" appears to be the name of the flag used to determine whether + * a certain automation block uses values or bindings as inputs + */ + toggleRowControl: async (block, rowControl) => { + const newBlock = { ...block, rowControl } + 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 - const steps = state.selectedAutomation.automation.definition.steps - let nextSelectedBlock - if (steps[idx] != null) { - nextSelectedBlock = steps[idx] - } else if (steps[idx - 1] != null) { - nextSelectedBlock = steps[idx - 1] - } else { - nextSelectedBlock = - state.selectedAutomation.automation.definition.trigger || null - } - state.selectedBlock = nextSelectedBlock - return state - }) + // Delete trigger if required + if (newAutomation.definition.trigger?.id === block.id) { + delete newAutomation.definition.trigger + } else { + // Otherwise remove step + newAutomation.definition.steps = newAutomation.definition.steps.filter( + step => step.id !== block.id + ) + } + await store.actions.save(newAutomation) }, }) diff --git a/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js b/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js deleted file mode 100644 index 8378310c2e..0000000000 --- a/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js +++ /dev/null @@ -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 {{time}} 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 - ) - }) -}) diff --git a/packages/builder/src/builderStore/store/automation/tests/testAutomation.js b/packages/builder/src/builderStore/store/automation/tests/testAutomation.js deleted file mode 100644 index 3fafbaf1d0..0000000000 --- a/packages/builder/src/builderStore/store/automation/tests/testAutomation.js +++ /dev/null @@ -1,78 +0,0 @@ -export default { - name: "Test automation", - definition: { - steps: [ - { - id: "ANBDINAPS", - description: "Send an email.", - tagline: "Send email to {{to}}", - 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 {{table.name}}", - 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", -} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 56b8a599f0..d58a2d5b9e 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -1,6 +1,11 @@ import { get, writable } from "svelte/store" import { cloneDeep } from "lodash/fp" -import { selectedScreen, selectedComponent } from "builderStore" +import { + selectedScreen, + selectedComponent, + screenHistoryStore, + automationHistoryStore, +} from "builderStore" import { datasources, integrations, @@ -122,6 +127,8 @@ export const getFrontendStore = () => { navigation: application.navigation || {}, usedPlugins: application.usedPlugins || [], })) + screenHistoryStore.reset() + automationHistoryStore.reset() // Initialise backend stores database.set(application.instance) @@ -179,10 +186,7 @@ export const getFrontendStore = () => { } // Check screen isn't already selected - if ( - state.selectedScreenId === screen._id && - state.selectedComponentId === screen.props?._id - ) { + if (state.selectedScreenId === screen._id) { return } @@ -256,7 +260,7 @@ export const getFrontendStore = () => { } }, save: async screen => { - /* + /* Temporarily disabled to accomodate migration issues. store.actions.screens.validate(screen) */ @@ -347,6 +351,7 @@ export const getFrontendStore = () => { return state }) + return null }, updateSetting: async (screen, name, value) => { if (!screen || !name) { diff --git a/packages/builder/src/builderStore/store/history.js b/packages/builder/src/builderStore/store/history.js new file mode 100644 index 0000000000..0f21085c6a --- /dev/null +++ b/packages/builder/src/builderStore/store/history.js @@ -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} + */ + 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} + */ + 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, + } +} diff --git a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte index e852ee1a0d..b80ba45086 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte @@ -1,10 +1,10 @@ -{#if automation} - +{#if $selectedAutomation} + {#key $selectedAutomation._id} + + {/key} {/if} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index caf8835b86..f30b49eb39 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -5,7 +5,6 @@ Detail, Body, Icon, - Tooltip, notifications, } from "@budibase/bbui" import { automationStore } from "builderStore" @@ -13,7 +12,6 @@ import { externalActions } from "./ExternalActions" export let blockIdx - export let blockComplete const disabled = { SEND_EMAIL_SMTP: { @@ -50,15 +48,12 @@ async function addBlockToAutomation() { try { - const newBlock = $automationStore.selectedAutomation.constructBlock( + const newBlock = automationStore.actions.constructBlock( "ACTION", actionVal.stepId, actionVal ) - automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1) - await automationStore.actions.save( - $automationStore.selectedAutomation?.automation - ) + await automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1) } catch (error) { notifications.error("Error saving automation") } @@ -66,20 +61,14 @@ { - blockComplete = true - addBlockToAutomation() - }} + onConfirm={addBlockToAutomation} > - Select an app or event. - - - Apps - + + Apps {#each Object.entries(external) as [idx, action]} - {idx.charAt(0).toUpperCase() + idx.slice(1)} + + {idx.charAt(0).toUpperCase() + idx.slice(1)} + + {/each} + + Actions - {#each Object.entries(internal) as [idx, action]} - {#if disabled[idx] && disabled[idx].disabled} - - selectAction(action)} - > - - - - {action.name} - - - - {:else} - selectAction(action)} - > - - - - {action.name} - + {@const isDisabled = disabled[idx] && disabled[idx].disabled} + selectAction(action)} + > + + + {action.name} + {#if isDisabled} + + {/if} - {/if} + {/each} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 4b01616b54..63a3478ef3 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -1,5 +1,5 @@ - + {automation.name} - - - - - - - + + + + { testDataModal.show() @@ -62,15 +60,13 @@ icon="MultipleCheck" size="M">Run test - - { - $automationStore.showTestPanel = true - }} - size="M">Test Details - + { + $automationStore.showTestPanel = true + }} + size="M">Test Details @@ -80,7 +76,7 @@ {#if block.stepId !== ActionStepID.LOOP} @@ -105,6 +101,9 @@ diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index d6e5fcb36d..7484a60502 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -1,5 +1,5 @@ {}}> - {#if loopingSelected} + {#if loopBlock} { @@ -174,13 +142,8 @@ - { - onSelect(block) - }} - > - + {}}> + @@ -198,9 +161,7 @@ $automationStore.blockDefinitions.ACTION.LOOP.schema.inputs .properties )} - block={$automationStore.selectedAutomation?.automation.definition.steps.find( - x => x.blockToLoop === block.id - )} + block={loopBlock} {webhookModal} /> @@ -209,22 +170,28 @@ {/if} {/if} - - {#if !blockComplete} + (open = !open)} + /> + {#if open} {#if !isTrigger} - {#if !loopingSelected} - addLooping()} icon="Reuse" - >Add Looping + {#if !loopBlock} + addLooping()} icon="Reuse"> + Add Looping + {/if} {#if showBindingPicker} {#if lastStep} - testDataModal.show()} cta - >Finish and test automation + testDataModal.show()} cta> + Finish and test automation + {/if} {/if} - + diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte index bc40b7bf85..c09474a370 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte @@ -2,21 +2,22 @@ import { automationStore } from "builderStore" import { Icon, Body, Detail, StatusLight } from "@budibase/bbui" import { externalActions } from "./ExternalActions" + import { createEventDispatcher } from "svelte" export let block - export let blockComplete + export let open export let showTestStatus = false - export let showParameters = {} export let testResult export let isTrigger export let idx + const dispatch = createEventDispatcher() + $: { if (!testResult) { - testResult = - $automationStore.selectedAutomation?.testResults?.steps.filter(step => - block.id ? step.id === block.id : step.stepId === block.stepId - )[0] + testResult = $automationStore.testResults?.steps?.filter(step => + block.id ? step.id === block.id : step.stepId === block.stepId + )?.[0] } } $: isTrigger = isTrigger || block.type === "TRIGGER" @@ -45,13 +46,7 @@ - { - blockComplete = !blockComplete - showParameters[block.id] = blockComplete - }} - class="splitHeader" - > + dispatch("toggle")} class="splitHeader"> {#if externalActions[block.stepId]} - + diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index 3beb6cd79b..cd170142e4 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -7,7 +7,7 @@ Label, notifications, } from "@budibase/bbui" - import { automationStore } from "builderStore" + import { automationStore, selectedAutomation } from "builderStore" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import { cloneDeep } from "lodash/fp" @@ -17,9 +17,7 @@ $: { // clone the trigger so we're not mutating the reference - trigger = cloneDeep( - $automationStore.selectedAutomation.automation.definition.trigger - ) + trigger = cloneDeep($selectedAutomation.definition.trigger) // get the outputs so we can define the fields let schema = Object.entries(trigger.schema?.outputs?.properties || {}) @@ -32,7 +30,7 @@ } // 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 $: isError = !trigger.schema.outputs.required.every( @@ -51,10 +49,7 @@ const testAutomation = async () => { try { - await automationStore.actions.test( - $automationStore.selectedAutomation?.automation, - testData - ) + await automationStore.actions.test($selectedAutomation, testData) $automationStore.showTestPanel = true } catch (error) { notifications.error("Error testing automation") @@ -70,8 +65,8 @@ onConfirm={testAutomation} cancelText="Cancel" > - + + JSON parseTestJSON(e)} /> diff --git a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte index bd46c70df5..d74eab3622 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte @@ -7,7 +7,7 @@ export let testResults export let width = "400px" - let showParameters + let openBlocks = {} let blocks function prepTestResults(results) { @@ -48,14 +48,15 @@ {#if block.stepId !== ActionStepID.LOOP} (openBlocks[block.id] = !openBlocks[block.id])} isTrigger={idx === 0} - {idx} testResult={filteredResults?.[idx]} + showTestStatus + {block} + {idx} /> - {#if showParameters && showParameters[block.id]} + {#if openBlocks[block.id]} {#if filteredResults?.[idx]?.outputs.iterations} diff --git a/packages/builder/src/components/automation/AutomationBuilder/TestPanel.svelte b/packages/builder/src/components/automation/AutomationBuilder/TestPanel.svelte index 4f09edf7c2..c2626fb3c2 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/TestPanel.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/TestPanel.svelte @@ -2,26 +2,8 @@ import { Icon, Divider } from "@budibase/bbui" import TestDisplay from "./TestDisplay.svelte" import { automationStore } from "builderStore" - import { ActionStepID } from "constants/backend/automations" 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 @@ -42,7 +24,7 @@ - + diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte index dc442c0fae..85e6a5faa3 100644 --- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte @@ -1,36 +1,20 @@ - - - - - Add automation - - - - - - - - + + + Add automation + + + - + + + diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte index 3549bc6de8..5fb27eaaf3 100644 --- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte @@ -1,6 +1,4 @@ - (touched = true)} /> + (touched = true)} + updateOnChange={false} + /> {#if touched && !value} Please specify a CRON expression {/if} diff --git a/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte b/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte index 59bf818b71..86eadd66a1 100644 --- a/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte +++ b/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte @@ -1,17 +1,18 @@ + + + + + + + diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte index 6a23ba8cbd..2c5f07240e 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte @@ -42,29 +42,22 @@ return } try { - await automationStore.actions.create({ - name: parameters.newAutomationName, - }) - const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP - const newBlock = $automationStore.selectedAutomation.constructBlock( + let trigger = automationStore.actions.constructBlock( "TRIGGER", "APP", - appActionDefinition + $automationStore.blockDefinitions.TRIGGER.APP ) - - newBlock.inputs = { + trigger.inputs = { fields: Object.keys(parameters.fields ?? {}).reduce((fields, key) => { fields[key] = "string" return fields }, {}), } - - automationStore.actions.addBlockToAutomation(newBlock) - await automationStore.actions.save( - $automationStore.selectedAutomation?.automation + const automation = await automationStore.actions.create( + parameters.newAutomationName, + trigger ) - parameters.automationId = - $automationStore.selectedAutomation.automation._id + parameters.automationId = automation._id delete parameters.newAutomationName } catch (error) { notifications.error("Error creating automation") diff --git a/packages/builder/src/pages/builder/admin/index.svelte b/packages/builder/src/pages/builder/admin/index.svelte index cc1a70b3bc..dc87054b0c 100644 --- a/packages/builder/src/pages/builder/admin/index.svelte +++ b/packages/builder/src/pages/builder/admin/index.svelte @@ -149,6 +149,7 @@ 0 || submitted} on:click={save} > diff --git a/packages/builder/src/pages/builder/app/[application]/automate/[automation]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/automate/[automationId]/_layout.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/automate/[automation]/_layout.svelte rename to packages/builder/src/pages/builder/app/[application]/automate/[automationId]/_layout.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/automate/[automation]/index.svelte b/packages/builder/src/pages/builder/app/[application]/automate/[automationId]/index.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/automate/[automation]/index.svelte rename to packages/builder/src/pages/builder/app/[application]/automate/[automationId]/index.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/automate/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/automate/_layout.svelte index b1d01d58cf..74dfe671ab 100644 --- a/packages/builder/src/pages/builder/app/[application]/automate/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/automate/_layout.svelte @@ -1,30 +1,40 @@ - - - + - {#if automation} + {#if $automationStore.automations?.length} {:else} @@ -40,9 +50,9 @@ You have no automations Let's fix that. Call the bots! - modal.show()} size="M" cta - >Create automation + modal.show()} size="M" cta> + Create automation + @@ -51,7 +61,7 @@ {#if $automationStore.showTestPanel} - + {/if} @@ -71,22 +81,8 @@ grid-template-columns: 260px minmax(510px, 1fr) fit-content(500px); 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 { position: relative; - padding-top: var(--spacing-l); display: flex; flex-direction: column; justify-content: flex-start; diff --git a/packages/builder/src/pages/builder/app/[application]/automate/index.svelte b/packages/builder/src/pages/builder/app/[application]/automate/index.svelte index b861e82dd1..ea2c853139 100644 --- a/packages/builder/src/pages/builder/app/[application]/automate/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/automate/index.svelte @@ -1,17 +1,10 @@ diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte index e97e3ee15c..785b221239 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte @@ -1,9 +1,11 @@ @@ -22,6 +24,9 @@ /> + {#if $isActive("./screens") || $isActive("./components")} + + {/if} {#if $store.clientFeatures.devicePreview} {/if} @@ -52,6 +57,7 @@ align-items: flex-start; gap: var(--spacing-l); margin: 0 2px; + z-index: 1; } .header-left, .header-right { @@ -59,7 +65,7 @@ flex-direction: row; justify-content: flex-start; align-items: center; - gap: var(--spacing-l); + gap: var(--spacing-xl); } .header-left { flex: 1 1 auto; diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentKeyHandler.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentKeyHandler.svelte index f83e5d6194..956e1f7a3b 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentKeyHandler.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentKeyHandler.svelte @@ -12,30 +12,30 @@ let componentToEject const keyHandlers = { - ["^ArrowUp"]: async component => { + ["Ctrl+ArrowUp"]: async component => { await store.actions.components.moveUp(component) }, - ["^ArrowDown"]: async component => { + ["Ctrl+ArrowDown"]: async component => { await store.actions.components.moveDown(component) }, - ["^c"]: component => { + ["Ctrl+c"]: component => { store.actions.components.copy(component, false) }, - ["^x"]: component => { + ["Ctrl+x"]: component => { store.actions.components.copy(component, true) }, - ["^v"]: async component => { + ["Ctrl+v"]: async component => { await store.actions.components.paste(component, "inside") }, - ["^d"]: async component => { + ["Ctrl+d"]: async component => { store.actions.components.copy(component) await store.actions.components.paste(component, "below") }, - ["^e"]: component => { + ["Ctrl+e"]: component => { componentToEject = component confirmEjectDialog.show() }, - ["^Enter"]: () => { + ["Ctrl+Enter"]: () => { $goto("./new") }, ["Delete"]: component => { @@ -53,14 +53,19 @@ store.actions.components.selectNext() }, ["Escape"]: () => { - if (!$isActive("/new")) { - return false + if ($isActive("./new")) { + $goto("./") } - $goto("./") }, } - const handleKeyAction = async (event, component, key, ctrlKey = false) => { + const handleKeyAction = async ({ + event, + component, + key, + ctrlKey = false, + shiftKey = false, + }) => { if (!component || !key) { return false } @@ -69,9 +74,12 @@ if (key === "Backspace") { key = "Delete" } - // Prefix key with a caret for ctrl modifier + // Prefix keys for modifiers + if (shiftKey) { + key = "Shift+" + key + } if (ctrlKey) { - key = "^" + key + key = "Ctrl+" + key } const handler = keyHandlers[key] if (!handler) { @@ -97,19 +105,26 @@ return } // Key events are always for the selected component - return await handleKeyAction( - e, - $selectedComponent, - e.key, - e.ctrlKey || e.metaKey - ) + return await handleKeyAction({ + event: e, + component: $selectedComponent, + key: e.key, + ctrlKey: e.ctrlKey || e.metaKey, + shiftKey: e.shiftKey, + }) } const handleComponentMenu = async e => { // 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) - return await handleKeyAction(null, component, key, ctrlKey) + return await handleKeyAction({ + event: null, + component, + key, + ctrlKey, + shiftKey, + }) } onMount(() => { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/NewScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/NewScreenModal.svelte index b5cd28c702..e3a974b344 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/NewScreenModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/NewScreenModal.svelte @@ -1,11 +1,13 @@ diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/blankScreenPreview.png b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/blankScreenPreview.png new file mode 100644 index 0000000000..0511e2f4fe Binary files /dev/null and b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/blankScreenPreview.png differ diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/listScreenPreview.png b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/listScreenPreview.png new file mode 100644 index 0000000000..b98210d90c Binary files /dev/null and b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/listScreenPreview.png differ diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/index.svelte index d1a5df68e1..6236721e1a 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/index.svelte @@ -5,6 +5,8 @@ -{#key $selectedScreen?._id} - -{/key} +{#if $selectedScreen} + {#key $selectedScreen._id} + + {/key} +{/if} diff --git a/packages/builder/yarn.lock b/packages/builder/yarn.lock index 20fc29b0e3..7035ca1765 100644 --- a/packages/builder/yarn.lock +++ b/packages/builder/yarn.lock @@ -3178,6 +3178,11 @@ fast-glob@^3.0.3: merge2 "^1.3.0" 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: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" diff --git a/packages/cli/package.json b/packages/cli/package.json index 49f7a6ab6b..c5a8eb7dc6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", - "@budibase/types": "2.3.18-alpha.6", + "@budibase/backend-core": "2.3.18-alpha.9", + "@budibase/string-templates": "2.3.18-alpha.9", + "@budibase/types": "2.3.18-alpha.9", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index b848ae691e..0872267c03 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.6", - "@budibase/frontend-core": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", + "@budibase/bbui": "2.3.18-alpha.9", + "@budibase/frontend-core": "2.3.18-alpha.9", + "@budibase/string-templates": "2.3.18-alpha.9", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 8328865966..ea74808a4d 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.18-alpha.6", + "@budibase/bbui": "2.3.18-alpha.9", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a44a61274d..0bb9a12dae 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index c183a808b8..147e3b17b6 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.18-alpha.6", - "@budibase/client": "2.3.18-alpha.6", - "@budibase/pro": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", - "@budibase/types": "2.3.18-alpha.6", + "@budibase/backend-core": "2.3.18-alpha.9", + "@budibase/client": "2.3.18-alpha.9", + "@budibase/pro": "2.3.18-alpha.9", + "@budibase/string-templates": "2.3.18-alpha.9", + "@budibase/types": "2.3.18-alpha.9", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index 0e6624d125..8ccd3fb3d0 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -65,10 +65,14 @@ export async function create(ctx: BBContext) { // call through to update if already exists 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 = cleanAutomationInputs(automation) @@ -126,6 +130,13 @@ export async function update(ctx: BBContext) { const db = context.getAppDB() let automation = ctx.request.body 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) automation = cleanAutomationInputs(automation) automation = await checkForWebhooks({ diff --git a/packages/server/src/api/routes/automation.ts b/packages/server/src/api/routes/automation.ts index dc0f171584..cb8b6042ae 100644 --- a/packages/server/src/api/routes/automation.ts +++ b/packages/server/src/api/routes/automation.ts @@ -38,7 +38,7 @@ router "/api/automations", bodyResource("_id"), authorized(permissions.BUILDER), - automationValidator(true), + automationValidator(false), controller.update ) .post( diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 04d1b7759f..0ef2527e7e 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.6.tgz#e6b3304e96b9469f3ca0f4fcfda8cf234c37e2d7" - integrity sha512-To0kFbB9nZ6p0UO4ScS4PJ0gbqI1PrMWRXJLTv/6GU3PxnsqvH1tbpcleLMz2zeE04e5xdwt6W1oPViELom2gg== +"@budibase/backend-core@2.3.18-alpha.9": + version "2.3.18-alpha.9" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.9.tgz#13ecee562b25e4f5297dfcc1fc47e32a52729c72" + integrity sha512-1mvBGAGwwj2aYlu1xx3FjGJRYSG4btYdljv5dkjvfCxl1wZoYrxQDe8FOzU0AOqtLyBgk9ANkkF185i1lhGXqg== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.6" + "@budibase/types" "2.3.18-alpha.9" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.6.tgz#7c5c221da7da79af79605a00aacac5c60f209bf6" - integrity sha512-YWPxmZn+z3tm5GZ+2UZSkOAhamlue/dmki+FCML5pIp3dCw8KsXnpzYXHRc1F8yXMTmA/8KBb/YkjQ2WK3Rk7A== +"@budibase/pro@2.3.18-alpha.9": + version "2.3.18-alpha.9" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.9.tgz#e930299011d88114df201df744a6bf470f39127e" + integrity sha512-QEapO6kYeMt5h12E48SnEuFNmXTmN0PFkHP5FTgA7MH1JPgaVsAcbNjeEZ+aOLOtQ1e6jRbuB5Ue0JxWMfujOg== dependencies: - "@budibase/backend-core" "2.3.18-alpha.6" - "@budibase/types" "2.3.18-alpha.6" + "@budibase/backend-core" "2.3.18-alpha.9" + "@budibase/types" "2.3.18-alpha.9" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.6.tgz#9438ee64008668bbcb3d688b189cc649e03dfd60" - integrity sha512-16YtXwSODS8UDhdxCP2piGDWELP05EZuPbwLsOUFLX3Gt0+Wwkme+XWw4pTPE+GoK/mTVkDxzSc4cvuXWtfxxA== +"@budibase/types@2.3.18-alpha.9": + version "2.3.18-alpha.9" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.9.tgz#6e5b684e83c8054bfb3d3da01d8ce7c4fab1269a" + integrity sha512-dgtm1IgzNun3em7fTkZzUHSn7YmypiohlTKGBDu4No94Br3K6ysiI1hc4eSEuzakL+TAuvmVgjRKpw2yzumeBQ== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 82b97419ef..0ca02a80b6 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index f1a9adfa2f..739d21126f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 2854f35441..cd0e6139c5 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.9", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.6", - "@budibase/pro": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", - "@budibase/types": "2.3.18-alpha.6", + "@budibase/backend-core": "2.3.18-alpha.9", + "@budibase/pro": "2.3.18-alpha.9", + "@budibase/string-templates": "2.3.18-alpha.9", + "@budibase/types": "2.3.18-alpha.9", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 7d296fe01b..2063169772 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.6.tgz#e6b3304e96b9469f3ca0f4fcfda8cf234c37e2d7" - integrity sha512-To0kFbB9nZ6p0UO4ScS4PJ0gbqI1PrMWRXJLTv/6GU3PxnsqvH1tbpcleLMz2zeE04e5xdwt6W1oPViELom2gg== +"@budibase/backend-core@2.3.18-alpha.9": + version "2.3.18-alpha.9" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.9.tgz#13ecee562b25e4f5297dfcc1fc47e32a52729c72" + integrity sha512-1mvBGAGwwj2aYlu1xx3FjGJRYSG4btYdljv5dkjvfCxl1wZoYrxQDe8FOzU0AOqtLyBgk9ANkkF185i1lhGXqg== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.6" + "@budibase/types" "2.3.18-alpha.9" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.6.tgz#7c5c221da7da79af79605a00aacac5c60f209bf6" - integrity sha512-YWPxmZn+z3tm5GZ+2UZSkOAhamlue/dmki+FCML5pIp3dCw8KsXnpzYXHRc1F8yXMTmA/8KBb/YkjQ2WK3Rk7A== +"@budibase/pro@2.3.18-alpha.9": + version "2.3.18-alpha.9" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.9.tgz#e930299011d88114df201df744a6bf470f39127e" + integrity sha512-QEapO6kYeMt5h12E48SnEuFNmXTmN0PFkHP5FTgA7MH1JPgaVsAcbNjeEZ+aOLOtQ1e6jRbuB5Ue0JxWMfujOg== dependencies: - "@budibase/backend-core" "2.3.18-alpha.6" - "@budibase/types" "2.3.18-alpha.6" + "@budibase/backend-core" "2.3.18-alpha.9" + "@budibase/types" "2.3.18-alpha.9" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.6.tgz#9438ee64008668bbcb3d688b189cc649e03dfd60" - integrity sha512-16YtXwSODS8UDhdxCP2piGDWELP05EZuPbwLsOUFLX3Gt0+Wwkme+XWw4pTPE+GoK/mTVkDxzSc4cvuXWtfxxA== +"@budibase/types@2.3.18-alpha.9": + version "2.3.18-alpha.9" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.9.tgz#6e5b684e83c8054bfb3d3da01d8ce7c4fab1269a" + integrity sha512-dgtm1IgzNun3em7fTkZzUHSn7YmypiohlTKGBDu4No94Br3K6ysiI1hc4eSEuzakL+TAuvmVgjRKpw2yzumeBQ== "@cspotcode/source-map-support@^0.8.0": version "0.8.1"