import { writable, get, derived } from "svelte/store" import { API } from "api" import { cloneDeep } from "lodash/fp" import { generate } from "shortid" import { createHistoryStore } from "stores/builder/history" import { notifications } from "@budibase/bbui" import { updateReferencesInObject } from "dataBinding" const initialAutomationState = { automations: [], testResults: null, showTestPanel: false, blockDefinitions: { TRIGGER: [], ACTION: [], }, selectedAutomationId: null, } // If this functions, remove the actions elements export const createAutomationStore = () => { const store = writable(initialAutomationState) store.actions = automationActions(store) // Setup history for automations const history = createHistoryStore({ getDoc: store.actions.getDefinition, selectDoc: store.actions.select, }) store.actions.save = history.wrapSaveDoc(store.actions.save) store.actions.delete = history.wrapDeleteDoc(store.actions.delete) return { store, history } } const updateStepReferences = (steps, modifiedIndex, action) => { steps.forEach(step => { updateReferencesInObject({ obj: step.inputs, modifiedIndex, action, label: "steps", }) }) } const automationActions = store => ({ definitions: async () => { const response = await API.getAutomationDefinitions() store.update(state => { state.blockDefinitions = { TRIGGER: response.trigger, ACTION: response.action, } return state }) return response }, fetch: async () => { const responses = await Promise.all([ API.getAutomations(), API.getAutomationDefinitions(), ]) store.update(state => { 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, } return state }) }, create: async (name, trigger) => { const automation = { name, type: "automation", definition: { steps: [], trigger, }, disabled: false, } const response = await store.actions.save(automation) await store.actions.fetch() store.actions.select(response._id) return response }, duplicate: async automation => { const response = await store.actions.save({ ...automation, name: `${automation.name} - copy`, _id: undefined, _ref: undefined, }) await store.actions.fetch() store.actions.select(response._id) return response }, save: async automation => { const response = await API.updateAutomation(automation) store.update(state => { const updatedAutomation = response.automation const existingIdx = state.automations.findIndex( existing => existing._id === automation._id ) if (existingIdx !== -1) { state.automations.splice(existingIdx, 1, updatedAutomation) return state } else { state.automations = [...state.automations, updatedAutomation] } return state }) return response.automation }, delete: async automation => { await API.deleteAutomation({ automationId: automation?._id, automationRev: automation?._rev, }) store.update(state => { // Remove the automation state.automations = state.automations.filter( x => x._id !== automation._id ) // Select a new automation if required if (automation._id === state.selectedAutomationId) { store.actions.select(state.automations[0]?._id) } return state }) await store.actions.fetch() }, toggleDisabled: async automationId => { let automation try { automation = store.actions.getDefinition(automationId) if (!automation) { return } automation.disabled = !automation.disabled await store.actions.save(automation) notifications.success( `Automation ${ automation.disabled ? "enabled" : "disabled" } successfully` ) } catch (error) { notifications.error( `Error ${ automation && automation.disabled ? "enabling" : "disabling" } automation` ) } }, 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) => { let result try { result = await API.testAutomation({ automationId: automation?._id, testData, }) } catch (err) { const message = err.message || err.status || JSON.stringify(err) throw `Automation test failed - ${message}` } if (!result?.trigger && !result?.steps?.length) { if (result?.err?.code === "usage_limit_exceeded") { throw "You have exceeded your automation quota" } throw "Something went wrong testing your automation" } store.update(state => { state.testResults = result return state }) }, 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.selectedAutomationId = id state.testResults = null state.showTestPanel = false return state }) }, getLogs: async ({ automationId, startDate, status, page } = {}) => { return await API.getAutomationLogs({ automationId, startDate, status, page, }) }, clearLogErrors: async ({ automationId, appId } = {}) => { return await API.clearAutomationLogErrors({ automationId, appId, }) }, addTestDataToAutomation: async data => { let newAutomation = cloneDeep(get(selectedAutomation)) newAutomation.testData = { ...newAutomation.testData, ...data, } await store.actions.save(newAutomation) }, constructBlock(type, stepId, blockDefinition) { return { ...blockDefinition, inputs: blockDefinition.inputs || {}, stepId, type, id: generate(), } }, addBlockToAutomation: async (block, blockIdx) => { const automation = get(selectedAutomation) let newAutomation = cloneDeep(automation) if (!automation) { return } try { updateStepReferences(newAutomation.definition.steps, blockIdx, "add") } catch (e) { notifications.error("Error adding automation block") } newAutomation.definition.steps.splice(blockIdx, 0, block) await store.actions.save(newAutomation) }, saveAutomationName: async (blockId, name) => { const automation = get(selectedAutomation) let newAutomation = cloneDeep(automation) if (!automation) { return } newAutomation.definition.stepNames = { ...newAutomation.definition.stepNames, [blockId]: name.trim(), } await store.actions.save(newAutomation) }, deleteAutomationName: async blockId => { const automation = get(selectedAutomation) let newAutomation = cloneDeep(automation) if (!automation) { return } delete newAutomation.definition.stepNames[blockId] await store.actions.save(newAutomation) }, deleteAutomationBlock: async (block, blockIdx) => { const automation = get(selectedAutomation) let newAutomation = cloneDeep(automation) // 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 ) delete newAutomation.definition.stepNames?.[block.id] } try { updateStepReferences(newAutomation.definition.steps, blockIdx, "delete") } catch (e) { notifications.error("Error deleting automation block") } await store.actions.save(newAutomation) }, replace: async (automationId, automation) => { if (!automation) { store.update(state => { // Remove the automation state.automations = state.automations.filter( x => x._id !== automationId ) // Select a new automation if required if (automationId === state.selectedAutomationId) { store.actions.select(state.automations[0]?._id) } return state }) } else { const index = get(store).automations.findIndex( x => x._id === automation._id ) if (index === -1) { // Automation addition store.update(state => ({ ...state, automations: [...state.automations, automation], })) } else { // Automation update store.update(state => { state.automations[index] = automation return state }) } } }, }) const automations = createAutomationStore() export const automationStore = automations.store export const automationHistoryStore = automations.history // Derived automation state export const selectedAutomation = derived(automationStore, $automationStore => { if (!$automationStore.selectedAutomationId) { return null } return $automationStore.automations?.find( x => x._id === $automationStore.selectedAutomationId ) })