Merge branch 'master' into BUDI-9016/extract-componenterrors-from-client
This commit is contained in:
commit
8f7e9dc0fd
|
@ -19,7 +19,7 @@
|
||||||
$: success = !error && !empty
|
$: success = !error && !empty
|
||||||
$: highlightedResult = highlight(expressionResult)
|
$: highlightedResult = highlight(expressionResult)
|
||||||
$: highlightedLogs = expressionLogs.map(l => ({
|
$: highlightedLogs = expressionLogs.map(l => ({
|
||||||
log: highlight(l.log.join(", ")),
|
log: l.log.map(part => highlight(part)).join(", "),
|
||||||
line: l.line,
|
line: l.line,
|
||||||
type: l.type,
|
type: l.type,
|
||||||
}))
|
}))
|
||||||
|
@ -95,7 +95,9 @@
|
||||||
{#if empty}
|
{#if empty}
|
||||||
Your expression will be evaluated here
|
Your expression will be evaluated here
|
||||||
{:else if error}
|
{:else if error}
|
||||||
{formatError(expressionError)}
|
<div class="error-msg">
|
||||||
|
{formatError(expressionError)}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="output-lines">
|
<div class="output-lines">
|
||||||
{#each highlightedLogs as logLine}
|
{#each highlightedLogs as logLine}
|
||||||
|
@ -118,13 +120,17 @@
|
||||||
<span>{@html logLine.log}</span>
|
<span>{@html logLine.log}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if logLine.line}
|
{#if logLine.line}
|
||||||
<span style="color: var(--blue)">:{logLine.line}</span>
|
<span style="color: var(--blue); overflow-wrap: normal;"
|
||||||
|
>:{logLine.line}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="line">
|
<div class="line">
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
<div>
|
||||||
{@html highlightedResult}
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
|
{@html highlightedResult}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -169,29 +175,33 @@
|
||||||
.header.error::before {
|
.header.error::before {
|
||||||
background: var(--error-bg);
|
background: var(--error-bg);
|
||||||
}
|
}
|
||||||
|
.error-msg {
|
||||||
|
padding-top: var(--spacing-m);
|
||||||
|
}
|
||||||
.body {
|
.body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: var(--spacing-m) var(--spacing-l);
|
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
margin: 0 var(--spacing-m);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
white-space: pre-line;
|
word-wrap: anywhere;
|
||||||
word-wrap: break-word;
|
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
.output-lines {
|
.output-lines {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
}
|
||||||
.line {
|
.line {
|
||||||
border-bottom: var(--border-light);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-m) 0;
|
||||||
|
word-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.line:not(:first-of-type) {
|
||||||
|
border-top: var(--border-light);
|
||||||
}
|
}
|
||||||
.icon-log {
|
.icon-log {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { derived, get } from "svelte/store"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { createHistoryStore } from "@/stores/builder/history"
|
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
||||||
import { licensing } from "@/stores/portal"
|
import { licensing } from "@/stores/portal"
|
||||||
import { tables, appStore } from "@/stores/builder"
|
import { tables, appStore } from "@/stores/builder"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
@ -1428,7 +1428,7 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
class AutomationStore extends BudiStore<AutomationState> {
|
class AutomationStore extends BudiStore<AutomationState> {
|
||||||
history: any
|
history: HistoryStore<Automation>
|
||||||
actions: ReturnType<typeof automationActions>
|
actions: ReturnType<typeof automationActions>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -1437,8 +1437,6 @@ class AutomationStore extends BudiStore<AutomationState> {
|
||||||
this.history = createHistoryStore({
|
this.history = createHistoryStore({
|
||||||
getDoc: this.actions.getDefinition.bind(this),
|
getDoc: this.actions.getDefinition.bind(this),
|
||||||
selectDoc: this.actions.select.bind(this),
|
selectDoc: this.actions.select.bind(this),
|
||||||
beforeAction: () => {},
|
|
||||||
afterAction: () => {},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Then wrap save and delete with history
|
// Then wrap save and delete with history
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { selectedScreen as selectedScreenStore } from "./screens"
|
import { selectedScreen as selectedScreenStore } from "./screens"
|
||||||
import { findComponentPath } from "@/helpers/components"
|
import { findComponentPath } from "@/helpers/components"
|
||||||
import { Screen, Component } from "@budibase/types"
|
import { Component, Screen } from "@budibase/types"
|
||||||
import { BudiStore, PersistenceType } from "@/stores/BudiStore"
|
import { BudiStore, PersistenceType } from "@/stores/BudiStore"
|
||||||
|
|
||||||
interface OpenNodesState {
|
interface OpenNodesState {
|
||||||
|
|
|
@ -30,9 +30,18 @@ import {
|
||||||
} from "@/constants/backend"
|
} from "@/constants/backend"
|
||||||
import { BudiStore } from "../BudiStore"
|
import { BudiStore } from "../BudiStore"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import { Component, FieldType, Screen, Table } from "@budibase/types"
|
import {
|
||||||
|
Component as ComponentType,
|
||||||
|
FieldType,
|
||||||
|
Screen,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/types"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
|
|
||||||
|
interface Component extends ComponentType {
|
||||||
|
_id: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ComponentState {
|
export interface ComponentState {
|
||||||
components: Record<string, ComponentDefinition>
|
components: Record<string, ComponentDefinition>
|
||||||
customComponents: string[]
|
customComponents: string[]
|
||||||
|
@ -254,7 +263,10 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
* @param {object} opts
|
* @param {object} opts
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
enrichEmptySettings(component: Component, opts: any) {
|
enrichEmptySettings(
|
||||||
|
component: Component,
|
||||||
|
opts: { screen?: Screen; parent?: Component; useDefaultValues?: boolean }
|
||||||
|
) {
|
||||||
if (!component?._component) {
|
if (!component?._component) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -364,7 +376,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
getSchemaForDatasource(screen, dataSource, {})
|
getSchemaForDatasource(screen, dataSource, {})
|
||||||
|
|
||||||
// Finds fields by types from the schema of the configured datasource
|
// Finds fields by types from the schema of the configured datasource
|
||||||
const findFieldTypes = (fieldTypes: any) => {
|
const findFieldTypes = (fieldTypes: FieldType | FieldType[]) => {
|
||||||
if (!Array.isArray(fieldTypes)) {
|
if (!Array.isArray(fieldTypes)) {
|
||||||
fieldTypes = [fieldTypes]
|
fieldTypes = [fieldTypes]
|
||||||
}
|
}
|
||||||
|
@ -439,7 +451,11 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
* @param {object} parent
|
* @param {object} parent
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
createInstance(componentName: string, presetProps: any, parent: any) {
|
createInstance(
|
||||||
|
componentName: string,
|
||||||
|
presetProps: any,
|
||||||
|
parent: any
|
||||||
|
): Component | null {
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
throw "A valid screen must be selected"
|
throw "A valid screen must be selected"
|
||||||
|
@ -451,7 +467,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate basic component structure
|
// Generate basic component structure
|
||||||
let instance = {
|
let instance: Component = {
|
||||||
_id: Helpers.uuid(),
|
_id: Helpers.uuid(),
|
||||||
_component: definition.component,
|
_component: definition.component,
|
||||||
_styles: {
|
_styles: {
|
||||||
|
@ -478,7 +494,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom post processing for creation only
|
// Custom post processing for creation only
|
||||||
let extras: any = {}
|
let extras: Partial<Component> = {}
|
||||||
if (definition.hasChildren) {
|
if (definition.hasChildren) {
|
||||||
extras._children = []
|
extras._children = []
|
||||||
}
|
}
|
||||||
|
@ -487,7 +503,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
if (componentName.endsWith("/formstep")) {
|
if (componentName.endsWith("/formstep")) {
|
||||||
const parentForm = findClosestMatchingComponent(
|
const parentForm = findClosestMatchingComponent(
|
||||||
screen.props,
|
screen.props,
|
||||||
get(selectedComponent)._id,
|
get(selectedComponent)?._id,
|
||||||
(component: Component) => component._component.endsWith("/form")
|
(component: Component) => component._component.endsWith("/form")
|
||||||
)
|
)
|
||||||
const formSteps = findAllMatchingComponents(
|
const formSteps = findAllMatchingComponents(
|
||||||
|
@ -515,7 +531,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
async create(
|
async create(
|
||||||
componentName: string,
|
componentName: string,
|
||||||
presetProps: any,
|
presetProps: any,
|
||||||
parent: any,
|
parent: Component,
|
||||||
index: number
|
index: number
|
||||||
) {
|
) {
|
||||||
const state = get(this.store)
|
const state = get(this.store)
|
||||||
|
@ -772,7 +788,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
if (!cut) {
|
if (!cut) {
|
||||||
componentToPaste = makeComponentUnique(componentToPaste)
|
componentToPaste = makeComponentUnique(componentToPaste)
|
||||||
}
|
}
|
||||||
newComponentId = componentToPaste._id!
|
newComponentId = componentToPaste._id
|
||||||
|
|
||||||
// Strip grid position metadata if pasting into a new screen, but keep
|
// Strip grid position metadata if pasting into a new screen, but keep
|
||||||
// alignment metadata
|
// alignment metadata
|
||||||
|
@ -915,7 +931,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
|
|
||||||
// If we have children, select first child, and the node is not collapsed
|
// If we have children, select first child, and the node is not collapsed
|
||||||
if (
|
if (
|
||||||
component._children?.length &&
|
component?._children?.length &&
|
||||||
(state.selectedComponentId === navComponentId ||
|
(state.selectedComponentId === navComponentId ||
|
||||||
componentTreeNodesStore.isNodeExpanded(component._id))
|
componentTreeNodesStore.isNodeExpanded(component._id))
|
||||||
) {
|
) {
|
||||||
|
@ -1339,12 +1355,15 @@ export const componentStore = new ComponentStore()
|
||||||
|
|
||||||
export const selectedComponent = derived(
|
export const selectedComponent = derived(
|
||||||
[componentStore, selectedScreen],
|
[componentStore, selectedScreen],
|
||||||
([$store, $selectedScreen]) => {
|
([$store, $selectedScreen]): Component | null => {
|
||||||
if (
|
if (
|
||||||
$selectedScreen &&
|
$selectedScreen &&
|
||||||
$store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`)
|
$store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`)
|
||||||
) {
|
) {
|
||||||
return $selectedScreen?.props
|
return {
|
||||||
|
...$selectedScreen.props,
|
||||||
|
_id: $selectedScreen.props._id!,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!$selectedScreen || !$store.selectedComponentId) {
|
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -1,10 +1,25 @@
|
||||||
import * as jsonpatch from "fast-json-patch/index.mjs"
|
import { Document } from "@budibase/types"
|
||||||
import { writable, derived, get } from "svelte/store"
|
import * as jsonpatch from "fast-json-patch"
|
||||||
|
import { writable, derived, get, Readable } from "svelte/store"
|
||||||
|
|
||||||
export const Operations = {
|
export const enum Operations {
|
||||||
Add: "Add",
|
Add = "Add",
|
||||||
Delete: "Delete",
|
Delete = "Delete",
|
||||||
Change: "Change",
|
Change = "Change",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Operator<T extends Document> {
|
||||||
|
id?: number
|
||||||
|
type: Operations
|
||||||
|
doc: T
|
||||||
|
forwardPatch?: jsonpatch.Operation[]
|
||||||
|
backwardsPatch?: jsonpatch.Operation[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryState<T extends Document> {
|
||||||
|
history: Operator<T>[]
|
||||||
|
position: number
|
||||||
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialState = {
|
export const initialState = {
|
||||||
|
@ -13,14 +28,38 @@ export const initialState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createHistoryStore = ({
|
export interface HistoryStore<T extends Document>
|
||||||
|
extends Readable<
|
||||||
|
HistoryState<T> & {
|
||||||
|
canUndo: boolean
|
||||||
|
canRedo: boolean
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
wrapSaveDoc: (
|
||||||
|
fn: (doc: T) => Promise<T>
|
||||||
|
) => (doc: T, operationId?: number) => Promise<T>
|
||||||
|
wrapDeleteDoc: (
|
||||||
|
fn: (doc: T) => Promise<void>
|
||||||
|
) => (doc: T, operationId?: number) => Promise<void>
|
||||||
|
|
||||||
|
reset: () => void
|
||||||
|
undo: () => Promise<void>
|
||||||
|
redo: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createHistoryStore = <T extends Document>({
|
||||||
getDoc,
|
getDoc,
|
||||||
selectDoc,
|
selectDoc,
|
||||||
beforeAction,
|
beforeAction,
|
||||||
afterAction,
|
afterAction,
|
||||||
}) => {
|
}: {
|
||||||
|
getDoc: (id: string) => T | undefined
|
||||||
|
selectDoc: (id: string) => void
|
||||||
|
beforeAction?: (operation?: Operator<T>) => void
|
||||||
|
afterAction?: (operation?: Operator<T>) => void
|
||||||
|
}): HistoryStore<T> => {
|
||||||
// Use a derived store to check if we are able to undo or redo any operations
|
// Use a derived store to check if we are able to undo or redo any operations
|
||||||
const store = writable(initialState)
|
const store = writable<HistoryState<T>>(initialState)
|
||||||
const derivedStore = derived(store, $store => {
|
const derivedStore = derived(store, $store => {
|
||||||
return {
|
return {
|
||||||
...$store,
|
...$store,
|
||||||
|
@ -31,8 +70,8 @@ export const createHistoryStore = ({
|
||||||
|
|
||||||
// Wrapped versions of essential functions which we call ourselves when using
|
// Wrapped versions of essential functions which we call ourselves when using
|
||||||
// undo and redo
|
// undo and redo
|
||||||
let saveFn
|
let saveFn: (doc: T, operationId?: number) => Promise<T>
|
||||||
let deleteFn
|
let deleteFn: (doc: T, operationId?: number) => Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal util to set the loading flag
|
* Internal util to set the loading flag
|
||||||
|
@ -66,7 +105,7 @@ export const createHistoryStore = ({
|
||||||
* For internal use only.
|
* For internal use only.
|
||||||
* @param operation the operation to save
|
* @param operation the operation to save
|
||||||
*/
|
*/
|
||||||
const saveOperation = operation => {
|
const saveOperation = (operation: Operator<T>) => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// Update history
|
// Update history
|
||||||
let history = state.history
|
let history = state.history
|
||||||
|
@ -93,15 +132,15 @@ export const createHistoryStore = ({
|
||||||
* @param fn the save function
|
* @param fn the save function
|
||||||
* @returns {function} a wrapped version of the save function
|
* @returns {function} a wrapped version of the save function
|
||||||
*/
|
*/
|
||||||
const wrapSaveDoc = fn => {
|
const wrapSaveDoc = (fn: (doc: T) => Promise<T>) => {
|
||||||
saveFn = async (doc, operationId) => {
|
saveFn = async (doc: T, operationId?: number) => {
|
||||||
// Only works on a single doc at a time
|
// Only works on a single doc at a time
|
||||||
if (!doc || Array.isArray(doc)) {
|
if (!doc || Array.isArray(doc)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
startLoading()
|
startLoading()
|
||||||
try {
|
try {
|
||||||
const oldDoc = getDoc(doc._id)
|
const oldDoc = getDoc(doc._id!)
|
||||||
const newDoc = jsonpatch.deepClone(await fn(doc))
|
const newDoc = jsonpatch.deepClone(await fn(doc))
|
||||||
|
|
||||||
// Store the change
|
// Store the change
|
||||||
|
@ -141,8 +180,8 @@ export const createHistoryStore = ({
|
||||||
* @param fn the delete function
|
* @param fn the delete function
|
||||||
* @returns {function} a wrapped version of the delete function
|
* @returns {function} a wrapped version of the delete function
|
||||||
*/
|
*/
|
||||||
const wrapDeleteDoc = fn => {
|
const wrapDeleteDoc = (fn: (doc: T) => Promise<void>) => {
|
||||||
deleteFn = async (doc, operationId) => {
|
deleteFn = async (doc: T, operationId?: number) => {
|
||||||
// Only works on a single doc at a time
|
// Only works on a single doc at a time
|
||||||
if (!doc || Array.isArray(doc)) {
|
if (!doc || Array.isArray(doc)) {
|
||||||
return
|
return
|
||||||
|
@ -201,7 +240,7 @@ export const createHistoryStore = ({
|
||||||
// Undo ADD
|
// Undo ADD
|
||||||
if (operation.type === Operations.Add) {
|
if (operation.type === Operations.Add) {
|
||||||
// Try to get the latest doc version to delete
|
// Try to get the latest doc version to delete
|
||||||
const latestDoc = getDoc(operation.doc._id)
|
const latestDoc = getDoc(operation.doc._id!)
|
||||||
const doc = latestDoc || operation.doc
|
const doc = latestDoc || operation.doc
|
||||||
await deleteFn(doc, operation.id)
|
await deleteFn(doc, operation.id)
|
||||||
}
|
}
|
||||||
|
@ -219,7 +258,7 @@ export const createHistoryStore = ({
|
||||||
// Undo CHANGE
|
// Undo CHANGE
|
||||||
else {
|
else {
|
||||||
// Get the current doc and apply the backwards patch on top of it
|
// Get the current doc and apply the backwards patch on top of it
|
||||||
let doc = jsonpatch.deepClone(getDoc(operation.doc._id))
|
let doc = jsonpatch.deepClone(getDoc(operation.doc._id!))
|
||||||
if (doc) {
|
if (doc) {
|
||||||
jsonpatch.applyPatch(
|
jsonpatch.applyPatch(
|
||||||
doc,
|
doc,
|
||||||
|
@ -283,7 +322,7 @@ export const createHistoryStore = ({
|
||||||
// Redo DELETE
|
// Redo DELETE
|
||||||
else if (operation.type === Operations.Delete) {
|
else if (operation.type === Operations.Delete) {
|
||||||
// Try to get the latest doc version to delete
|
// Try to get the latest doc version to delete
|
||||||
const latestDoc = getDoc(operation.doc._id)
|
const latestDoc = getDoc(operation.doc._id!)
|
||||||
const doc = latestDoc || operation.doc
|
const doc = latestDoc || operation.doc
|
||||||
await deleteFn(doc, operation.id)
|
await deleteFn(doc, operation.id)
|
||||||
}
|
}
|
||||||
|
@ -291,7 +330,7 @@ export const createHistoryStore = ({
|
||||||
// Redo CHANGE
|
// Redo CHANGE
|
||||||
else {
|
else {
|
||||||
// Get the current doc and apply the forwards patch on top of it
|
// Get the current doc and apply the forwards patch on top of it
|
||||||
let doc = jsonpatch.deepClone(getDoc(operation.doc._id))
|
let doc = jsonpatch.deepClone(getDoc(operation.doc._id!))
|
||||||
if (doc) {
|
if (doc) {
|
||||||
jsonpatch.applyPatch(doc, jsonpatch.deepClone(operation.forwardPatch))
|
jsonpatch.applyPatch(doc, jsonpatch.deepClone(operation.forwardPatch))
|
||||||
await saveFn(doc, operation.id)
|
await saveFn(doc, operation.id)
|
|
@ -10,7 +10,7 @@ import {
|
||||||
navigationStore,
|
navigationStore,
|
||||||
selectedComponent,
|
selectedComponent,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import { createHistoryStore } from "@/stores/builder/history"
|
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { BudiStore } from "../BudiStore"
|
import { BudiStore } from "../BudiStore"
|
||||||
import {
|
import {
|
||||||
|
@ -33,9 +33,9 @@ export const initialScreenState: ScreenState = {
|
||||||
|
|
||||||
// Review the nulls
|
// Review the nulls
|
||||||
export class ScreenStore extends BudiStore<ScreenState> {
|
export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
history: any
|
history: HistoryStore<Screen>
|
||||||
delete: any
|
delete: (screens: Screen) => Promise<void>
|
||||||
save: any
|
save: (screen: Screen) => Promise<Screen>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(initialScreenState)
|
super(initialScreenState)
|
||||||
|
@ -58,13 +58,12 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
getDoc: (id: string) =>
|
getDoc: (id: string) =>
|
||||||
get(this.store).screens?.find(screen => screen._id === id),
|
get(this.store).screens?.find(screen => screen._id === id),
|
||||||
selectDoc: this.select,
|
selectDoc: this.select,
|
||||||
beforeAction: () => {},
|
|
||||||
afterAction: () => {
|
afterAction: () => {
|
||||||
// Ensure a valid component is selected
|
// Ensure a valid component is selected
|
||||||
if (!get(selectedComponent)) {
|
if (!get(selectedComponent)) {
|
||||||
this.update(state => ({
|
componentStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
selectedComponentId: get(selectedScreen)?.props._id,
|
selectedComponentId: get(selectedScreen)?._id,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -281,7 +280,10 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
* supports deeply mutating the current doc rather than just appending data.
|
* supports deeply mutating the current doc rather than just appending data.
|
||||||
*/
|
*/
|
||||||
sequentialScreenPatch = Utils.sequential(
|
sequentialScreenPatch = Utils.sequential(
|
||||||
async (patchFn: (screen: Screen) => any, screenId: string) => {
|
async (
|
||||||
|
patchFn: (screen: Screen) => boolean,
|
||||||
|
screenId: string
|
||||||
|
): Promise<Screen | void> => {
|
||||||
const state = get(this.store)
|
const state = get(this.store)
|
||||||
const screen = state.screens.find(screen => screen._id === screenId)
|
const screen = state.screens.find(screen => screen._id === screenId)
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
|
@ -362,10 +364,10 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
* Any deleted screens will then have their routes/links purged
|
* Any deleted screens will then have their routes/links purged
|
||||||
*
|
*
|
||||||
* Wrapped by {@link delete}
|
* Wrapped by {@link delete}
|
||||||
* @param {Screen | Screen[]} screens
|
* @param {Screen } screens
|
||||||
*/
|
*/
|
||||||
async deleteScreen(screens: Screen | Screen[]) {
|
async deleteScreen(screen: Screen) {
|
||||||
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
const screensToDelete = [screen]
|
||||||
// Build array of promises to speed up bulk deletions
|
// Build array of promises to speed up bulk deletions
|
||||||
let promises: Promise<DeleteScreenResponse>[] = []
|
let promises: Promise<DeleteScreenResponse>[] = []
|
||||||
let deleteUrls: string[] = []
|
let deleteUrls: string[] = []
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
|
import { SearchFilterGroup, UISearchFilter } from "@budibase/types"
|
||||||
|
|
||||||
export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
export const sleep = (ms: number) =>
|
||||||
|
new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to wrap an async function and ensure all invocations happen
|
* Utility to wrap an async function and ensure all invocations happen
|
||||||
|
@ -10,12 +12,18 @@ export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
* @param fn the async function to run
|
* @param fn the async function to run
|
||||||
* @return {Function} a sequential version of the function
|
* @return {Function} a sequential version of the function
|
||||||
*/
|
*/
|
||||||
export const sequential = fn => {
|
export const sequential = <
|
||||||
let queue = []
|
TReturn,
|
||||||
return (...params) => {
|
TFunction extends (...args: any[]) => Promise<TReturn>
|
||||||
return new Promise((resolve, reject) => {
|
>(
|
||||||
|
fn: TFunction
|
||||||
|
): TFunction => {
|
||||||
|
let queue: (() => Promise<void>)[] = []
|
||||||
|
const result = (...params: Parameters<TFunction>) => {
|
||||||
|
return new Promise<TReturn>((resolve, reject) => {
|
||||||
queue.push(async () => {
|
queue.push(async () => {
|
||||||
let data, error
|
let data: TReturn | undefined
|
||||||
|
let error: unknown
|
||||||
try {
|
try {
|
||||||
data = await fn(...params)
|
data = await fn(...params)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -28,7 +36,7 @@ export const sequential = fn => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
} else {
|
} else {
|
||||||
resolve(data)
|
resolve(data!)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (queue.length === 1) {
|
if (queue.length === 1) {
|
||||||
|
@ -36,6 +44,7 @@ export const sequential = fn => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return result as TFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,9 +54,9 @@ export const sequential = fn => {
|
||||||
* @param minDelay the minimum delay between invocations
|
* @param minDelay the minimum delay between invocations
|
||||||
* @returns a debounced version of the callback
|
* @returns a debounced version of the callback
|
||||||
*/
|
*/
|
||||||
export const debounce = (callback, minDelay = 1000) => {
|
export const debounce = (callback: Function, minDelay = 1000) => {
|
||||||
let timeout
|
let timeout: ReturnType<typeof setTimeout>
|
||||||
return async (...params) => {
|
return async (...params: any[]) => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
|
@ -70,11 +79,11 @@ export const debounce = (callback, minDelay = 1000) => {
|
||||||
* @param minDelay
|
* @param minDelay
|
||||||
* @returns {Function} a throttled version function
|
* @returns {Function} a throttled version function
|
||||||
*/
|
*/
|
||||||
export const throttle = (callback, minDelay = 1000) => {
|
export const throttle = (callback: Function, minDelay = 1000) => {
|
||||||
let lastParams
|
let lastParams: any[]
|
||||||
let stalled = false
|
let stalled = false
|
||||||
let pending = false
|
let pending = false
|
||||||
const invoke = (...params) => {
|
const invoke = (...params: any[]) => {
|
||||||
lastParams = params
|
lastParams = params
|
||||||
if (stalled) {
|
if (stalled) {
|
||||||
pending = true
|
pending = true
|
||||||
|
@ -98,10 +107,10 @@ export const throttle = (callback, minDelay = 1000) => {
|
||||||
* @param callback the function to run
|
* @param callback the function to run
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
export const domDebounce = callback => {
|
export const domDebounce = (callback: Function) => {
|
||||||
let active = false
|
let active = false
|
||||||
let lastParams
|
let lastParams: any[]
|
||||||
return (...params) => {
|
return (...params: any[]) => {
|
||||||
lastParams = params
|
lastParams = params
|
||||||
if (!active) {
|
if (!active) {
|
||||||
active = true
|
active = true
|
||||||
|
@ -119,7 +128,17 @@ export const domDebounce = callback => {
|
||||||
*
|
*
|
||||||
* @param {any} props
|
* @param {any} props
|
||||||
* */
|
* */
|
||||||
export const buildFormBlockButtonConfig = props => {
|
export const buildFormBlockButtonConfig = (props?: {
|
||||||
|
_id?: string
|
||||||
|
actionType?: string
|
||||||
|
dataSource?: { resourceId: string }
|
||||||
|
notificationOverride?: boolean
|
||||||
|
actionUrl?: string
|
||||||
|
showDeleteButton?: boolean
|
||||||
|
deleteButtonLabel?: string
|
||||||
|
showSaveButton?: boolean
|
||||||
|
saveButtonLabel?: string
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
_id,
|
_id,
|
||||||
actionType,
|
actionType,
|
||||||
|
@ -227,7 +246,11 @@ export const buildFormBlockButtonConfig = props => {
|
||||||
|
|
||||||
const defaultButtons = []
|
const defaultButtons = []
|
||||||
|
|
||||||
if (["Update", "Create"].includes(actionType) && showSaveButton !== false) {
|
if (
|
||||||
|
actionType &&
|
||||||
|
["Update", "Create"].includes(actionType) &&
|
||||||
|
showSaveButton !== false
|
||||||
|
) {
|
||||||
defaultButtons.push({
|
defaultButtons.push({
|
||||||
text: saveText || "Save",
|
text: saveText || "Save",
|
||||||
_id: Helpers.uuid(),
|
_id: Helpers.uuid(),
|
||||||
|
@ -251,7 +274,13 @@ export const buildFormBlockButtonConfig = props => {
|
||||||
return defaultButtons
|
return defaultButtons
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildMultiStepFormBlockDefaultProps = props => {
|
export const buildMultiStepFormBlockDefaultProps = (props?: {
|
||||||
|
_id: string
|
||||||
|
stepCount: number
|
||||||
|
currentStep: number
|
||||||
|
actionType: string
|
||||||
|
dataSource: { resourceId: string }
|
||||||
|
}) => {
|
||||||
const { _id, stepCount, currentStep, actionType, dataSource } = props || {}
|
const { _id, stepCount, currentStep, actionType, dataSource } = props || {}
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
|
@ -361,7 +390,7 @@ export const buildMultiStepFormBlockDefaultProps = props => {
|
||||||
* @param {Object} filter UI filter
|
* @param {Object} filter UI filter
|
||||||
* @returns {Object} parsed filter
|
* @returns {Object} parsed filter
|
||||||
*/
|
*/
|
||||||
export function parseFilter(filter) {
|
export function parseFilter(filter: UISearchFilter) {
|
||||||
if (!filter?.groups) {
|
if (!filter?.groups) {
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
|
@ -369,13 +398,13 @@ export function parseFilter(filter) {
|
||||||
const update = cloneDeep(filter)
|
const update = cloneDeep(filter)
|
||||||
|
|
||||||
update.groups = update.groups
|
update.groups = update.groups
|
||||||
.map(group => {
|
?.map(group => {
|
||||||
group.filters = group.filters.filter(filter => {
|
group.filters = group.filters?.filter((filter: any) => {
|
||||||
return filter.field && filter.operator
|
return filter.field && filter.operator
|
||||||
})
|
})
|
||||||
return group.filters.length ? group : null
|
return group.filters?.length ? group : null
|
||||||
})
|
})
|
||||||
.filter(group => group)
|
.filter((group): group is SearchFilterGroup => !!group)
|
||||||
|
|
||||||
return update
|
return update
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
checkBuilderEndpoint,
|
checkBuilderEndpoint,
|
||||||
getAllTableRows,
|
getAllTableRows,
|
||||||
clearAllAutomations,
|
|
||||||
testAutomation,
|
testAutomation,
|
||||||
} from "./utilities/TestFunctions"
|
} from "./utilities/TestFunctions"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
|
@ -12,9 +11,9 @@ import {
|
||||||
import { configs, context, events } from "@budibase/backend-core"
|
import { configs, context, events } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import {
|
import {
|
||||||
Automation,
|
|
||||||
ConfigType,
|
ConfigType,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
isDidNotTriggerResponse,
|
||||||
SettingsConfig,
|
SettingsConfig,
|
||||||
Table,
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -22,11 +21,13 @@ import { mocks } from "@budibase/backend-core/tests"
|
||||||
import { removeDeprecated } from "../../../automations/utils"
|
import { removeDeprecated } from "../../../automations/utils"
|
||||||
import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder"
|
||||||
import { automations } from "@budibase/shared-core"
|
import { automations } from "@budibase/shared-core"
|
||||||
|
import { basicTable } from "../../../tests/utilities/structures"
|
||||||
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
|
|
||||||
const FilterConditions = automations.steps.filter.FilterConditions
|
const FilterConditions = automations.steps.filter.FilterConditions
|
||||||
|
|
||||||
const MAX_RETRIES = 4
|
const MAX_RETRIES = 4
|
||||||
let {
|
const {
|
||||||
basicAutomation,
|
basicAutomation,
|
||||||
newAutomation,
|
newAutomation,
|
||||||
automationTrigger,
|
automationTrigger,
|
||||||
|
@ -37,10 +38,11 @@ let {
|
||||||
} = setup.structures
|
} = setup.structures
|
||||||
|
|
||||||
describe("/automations", () => {
|
describe("/automations", () => {
|
||||||
let request = setup.getRequest()
|
const config = new TestConfiguration()
|
||||||
let config = setup.getConfig()
|
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(() => {
|
||||||
|
config.end()
|
||||||
|
})
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
|
@ -52,40 +54,26 @@ describe("/automations", () => {
|
||||||
|
|
||||||
describe("get definitions", () => {
|
describe("get definitions", () => {
|
||||||
it("returns a list of definitions for actions", async () => {
|
it("returns a list of definitions for actions", async () => {
|
||||||
const res = await request
|
const res = await config.api.automation.getActions()
|
||||||
.get(`/api/automations/action/list`)
|
expect(Object.keys(res).length).not.toEqual(0)
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns a list of definitions for triggerInfo", async () => {
|
it("returns a list of definitions for triggerInfo", async () => {
|
||||||
const res = await request
|
const res = await config.api.automation.getTriggers()
|
||||||
.get(`/api/automations/trigger/list`)
|
expect(Object.keys(res).length).not.toEqual(0)
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns all of the definitions in one", async () => {
|
it("returns all of the definitions in one", async () => {
|
||||||
const res = await request
|
const { action, trigger } = await config.api.automation.getDefinitions()
|
||||||
.get(`/api/automations/definitions/list`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
let definitionsLength = Object.keys(
|
let definitionsLength = Object.keys(
|
||||||
removeDeprecated(BUILTIN_ACTION_DEFINITIONS)
|
removeDeprecated(BUILTIN_ACTION_DEFINITIONS)
|
||||||
).length
|
).length
|
||||||
|
|
||||||
expect(Object.keys(res.body.action).length).toBeGreaterThanOrEqual(
|
expect(Object.keys(action).length).toBeGreaterThanOrEqual(
|
||||||
definitionsLength
|
definitionsLength
|
||||||
)
|
)
|
||||||
expect(Object.keys(res.body.trigger).length).toEqual(
|
expect(Object.keys(trigger).length).toEqual(
|
||||||
Object.keys(removeDeprecated(TRIGGER_DEFINITIONS)).length
|
Object.keys(removeDeprecated(TRIGGER_DEFINITIONS)).length
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -93,38 +81,27 @@ describe("/automations", () => {
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("creates an automation with no steps", async () => {
|
it("creates an automation with no steps", async () => {
|
||||||
const automation = newAutomation()
|
const { message, automation } = await config.api.automation.post(
|
||||||
automation.definition.steps = []
|
newAutomation({ steps: [] })
|
||||||
|
)
|
||||||
|
|
||||||
const res = await request
|
expect(message).toEqual("Automation created successfully")
|
||||||
.post(`/api/automations`)
|
expect(automation.name).toEqual("My Automation")
|
||||||
.set(config.defaultHeaders())
|
expect(automation._id).not.toEqual(null)
|
||||||
.send(automation)
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.message).toEqual("Automation created successfully")
|
|
||||||
expect(res.body.automation.name).toEqual("My Automation")
|
|
||||||
expect(res.body.automation._id).not.toEqual(null)
|
|
||||||
expect(events.automation.created).toHaveBeenCalledTimes(1)
|
expect(events.automation.created).toHaveBeenCalledTimes(1)
|
||||||
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates an automation with steps", async () => {
|
it("creates an automation with steps", async () => {
|
||||||
const automation = newAutomation()
|
|
||||||
automation.definition.steps.push(automationStep())
|
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
const res = await request
|
const { message, automation } = await config.api.automation.post(
|
||||||
.post(`/api/automations`)
|
newAutomation({ steps: [automationStep(), automationStep()] })
|
||||||
.set(config.defaultHeaders())
|
)
|
||||||
.send(automation)
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.message).toEqual("Automation created successfully")
|
expect(message).toEqual("Automation created successfully")
|
||||||
expect(res.body.automation.name).toEqual("My Automation")
|
expect(automation.name).toEqual("My Automation")
|
||||||
expect(res.body.automation._id).not.toEqual(null)
|
expect(automation._id).not.toEqual(null)
|
||||||
expect(events.automation.created).toHaveBeenCalledTimes(1)
|
expect(events.automation.created).toHaveBeenCalledTimes(1)
|
||||||
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
|
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
@ -241,13 +218,9 @@ describe("/automations", () => {
|
||||||
describe("find", () => {
|
describe("find", () => {
|
||||||
it("should be able to find the automation", async () => {
|
it("should be able to find the automation", async () => {
|
||||||
const automation = await config.createAutomation()
|
const automation = await config.createAutomation()
|
||||||
const res = await request
|
const { _id, _rev } = await config.api.automation.get(automation._id!)
|
||||||
.get(`/api/automations/${automation._id}`)
|
expect(_id).toEqual(automation._id)
|
||||||
.set(config.defaultHeaders())
|
expect(_rev).toEqual(automation._rev)
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body._id).toEqual(automation._id)
|
|
||||||
expect(res.body._rev).toEqual(automation._rev)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -348,106 +321,104 @@ describe("/automations", () => {
|
||||||
|
|
||||||
describe("trigger", () => {
|
describe("trigger", () => {
|
||||||
it("does not trigger an automation when not synchronous and in dev", async () => {
|
it("does not trigger an automation when not synchronous and in dev", async () => {
|
||||||
let automation = newAutomation()
|
const { automation } = await config.api.automation.post(newAutomation())
|
||||||
automation = await config.createAutomation(automation)
|
await config.api.automation.trigger(
|
||||||
const res = await request
|
automation._id!,
|
||||||
.post(`/api/automations/${automation._id}/trigger`)
|
{
|
||||||
.set(config.defaultHeaders())
|
fields: {},
|
||||||
.expect("Content-Type", /json/)
|
timeout: 1000,
|
||||||
.expect(400)
|
},
|
||||||
|
{
|
||||||
expect(res.body.message).toEqual(
|
status: 400,
|
||||||
"Only apps in production support this endpoint"
|
body: {
|
||||||
|
message: "Only apps in production support this endpoint",
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("triggers a synchronous automation", async () => {
|
it("triggers a synchronous automation", async () => {
|
||||||
mocks.licenses.useSyncAutomations()
|
mocks.licenses.useSyncAutomations()
|
||||||
let automation = collectAutomation()
|
const { automation } = await config.api.automation.post(
|
||||||
automation = await config.createAutomation(automation)
|
collectAutomation()
|
||||||
const res = await request
|
)
|
||||||
.post(`/api/automations/${automation._id}/trigger`)
|
await config.api.automation.trigger(
|
||||||
.set(config.defaultHeaders())
|
automation._id!,
|
||||||
.expect("Content-Type", /json/)
|
{
|
||||||
.expect(200)
|
fields: {},
|
||||||
|
timeout: 1000,
|
||||||
expect(res.body.success).toEqual(true)
|
},
|
||||||
expect(res.body.value).toEqual([1, 2, 3])
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
success: true,
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should throw an error when attempting to trigger a disabled automation", async () => {
|
it("should throw an error when attempting to trigger a disabled automation", async () => {
|
||||||
mocks.licenses.useSyncAutomations()
|
mocks.licenses.useSyncAutomations()
|
||||||
let automation = collectAutomation()
|
const { automation } = await config.api.automation.post(
|
||||||
automation = await config.createAutomation({
|
collectAutomation({ disabled: true })
|
||||||
...automation,
|
)
|
||||||
disabled: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request
|
await config.api.automation.trigger(
|
||||||
.post(`/api/automations/${automation._id}/trigger`)
|
automation._id!,
|
||||||
.set(config.defaultHeaders())
|
{
|
||||||
.expect("Content-Type", /json/)
|
fields: {},
|
||||||
.expect(400)
|
timeout: 1000,
|
||||||
|
},
|
||||||
expect(res.body.message).toEqual("Automation is disabled")
|
{
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
message: "Automation is disabled",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("triggers an asynchronous automation", async () => {
|
it("triggers an asynchronous automation", async () => {
|
||||||
let automation = newAutomation()
|
const { automation } = await config.api.automation.post(newAutomation())
|
||||||
automation = await config.createAutomation(automation)
|
|
||||||
await config.publish()
|
await config.publish()
|
||||||
|
|
||||||
const res = await request
|
await config.withProdApp(() =>
|
||||||
.post(`/api/automations/${automation._id}/trigger`)
|
config.api.automation.trigger(
|
||||||
.set(config.defaultHeaders({}, true))
|
automation._id!,
|
||||||
.expect("Content-Type", /json/)
|
{
|
||||||
.expect(200)
|
fields: {},
|
||||||
|
timeout: 1000,
|
||||||
expect(res.body.message).toEqual(
|
},
|
||||||
`Automation ${automation._id} has been triggered.`
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
message: `Automation ${automation._id} has been triggered.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
const update = async (automation: Automation) => {
|
|
||||||
return request
|
|
||||||
.put(`/api/automations`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.send(automation)
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateWithPost = async (automation: Automation) => {
|
|
||||||
return request
|
|
||||||
.post(`/api/automations`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.send(automation)
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
it("updates a automations name", async () => {
|
it("updates a automations name", async () => {
|
||||||
const automation = await config.createAutomation(newAutomation())
|
const { automation } = await config.api.automation.post(basicAutomation())
|
||||||
automation.name = "Updated Name"
|
automation.name = "Updated Name"
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
const res = await update(automation)
|
const { automation: updatedAutomation, message } =
|
||||||
|
await config.api.automation.update(automation)
|
||||||
|
|
||||||
const automationRes = res.body.automation
|
expect(updatedAutomation._id).toEqual(automation._id)
|
||||||
const message = res.body.message
|
expect(updatedAutomation._rev).toBeDefined()
|
||||||
|
expect(updatedAutomation._rev).not.toEqual(automation._rev)
|
||||||
|
|
||||||
// doc attributes
|
expect(updatedAutomation.name).toEqual("Updated Name")
|
||||||
expect(automationRes._id).toEqual(automation._id)
|
|
||||||
expect(automationRes._rev).toBeDefined()
|
|
||||||
expect(automationRes._rev).not.toEqual(automation._rev)
|
|
||||||
// content updates
|
|
||||||
expect(automationRes.name).toEqual("Updated Name")
|
|
||||||
expect(message).toEqual(
|
expect(message).toEqual(
|
||||||
`Automation ${automation._id} updated successfully.`
|
`Automation ${automation._id} updated successfully.`
|
||||||
)
|
)
|
||||||
// events
|
|
||||||
expect(events.automation.created).not.toHaveBeenCalled()
|
expect(events.automation.created).not.toHaveBeenCalled()
|
||||||
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
||||||
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
|
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
|
||||||
|
@ -455,26 +426,23 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates a automations name using POST request", async () => {
|
it("updates a automations name using POST request", async () => {
|
||||||
const automation = await config.createAutomation(newAutomation())
|
const { automation } = await config.api.automation.post(basicAutomation())
|
||||||
automation.name = "Updated Name"
|
automation.name = "Updated Name"
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
// the POST request will defer to the update
|
// the POST request will defer to the update when an id has been supplied.
|
||||||
// when an id has been supplied.
|
const { automation: updatedAutomation, message } =
|
||||||
const res = await updateWithPost(automation)
|
await config.api.automation.post(automation)
|
||||||
|
|
||||||
const automationRes = res.body.automation
|
expect(updatedAutomation._id).toEqual(automation._id)
|
||||||
const message = res.body.message
|
expect(updatedAutomation._rev).toBeDefined()
|
||||||
// doc attributes
|
expect(updatedAutomation._rev).not.toEqual(automation._rev)
|
||||||
expect(automationRes._id).toEqual(automation._id)
|
|
||||||
expect(automationRes._rev).toBeDefined()
|
expect(updatedAutomation.name).toEqual("Updated Name")
|
||||||
expect(automationRes._rev).not.toEqual(automation._rev)
|
|
||||||
// content updates
|
|
||||||
expect(automationRes.name).toEqual("Updated Name")
|
|
||||||
expect(message).toEqual(
|
expect(message).toEqual(
|
||||||
`Automation ${automation._id} updated successfully.`
|
`Automation ${automation._id} updated successfully.`
|
||||||
)
|
)
|
||||||
// events
|
|
||||||
expect(events.automation.created).not.toHaveBeenCalled()
|
expect(events.automation.created).not.toHaveBeenCalled()
|
||||||
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
||||||
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
|
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
|
||||||
|
@ -482,16 +450,14 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates an automation trigger", async () => {
|
it("updates an automation trigger", async () => {
|
||||||
let automation = newAutomation()
|
const { automation } = await config.api.automation.post(newAutomation())
|
||||||
automation = await config.createAutomation(automation)
|
|
||||||
automation.definition.trigger = automationTrigger(
|
automation.definition.trigger = automationTrigger(
|
||||||
TRIGGER_DEFINITIONS.WEBHOOK
|
TRIGGER_DEFINITIONS.WEBHOOK
|
||||||
)
|
)
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
await update(automation)
|
await config.api.automation.update(automation)
|
||||||
|
|
||||||
// events
|
|
||||||
expect(events.automation.created).not.toHaveBeenCalled()
|
expect(events.automation.created).not.toHaveBeenCalled()
|
||||||
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
||||||
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
|
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
|
||||||
|
@ -499,16 +465,13 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("adds automation steps", async () => {
|
it("adds automation steps", async () => {
|
||||||
let automation = newAutomation()
|
const { automation } = await config.api.automation.post(newAutomation())
|
||||||
automation = await config.createAutomation(automation)
|
|
||||||
automation.definition.steps.push(automationStep())
|
automation.definition.steps.push(automationStep())
|
||||||
automation.definition.steps.push(automationStep())
|
automation.definition.steps.push(automationStep())
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
// check the post request honours updates with same id
|
await config.api.automation.update(automation)
|
||||||
await update(automation)
|
|
||||||
|
|
||||||
// events
|
|
||||||
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
|
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
|
||||||
expect(events.automation.created).not.toHaveBeenCalled()
|
expect(events.automation.created).not.toHaveBeenCalled()
|
||||||
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
|
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
|
||||||
|
@ -516,32 +479,25 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("removes automation steps", async () => {
|
it("removes automation steps", async () => {
|
||||||
let automation = newAutomation()
|
const { automation } = await config.api.automation.post(newAutomation())
|
||||||
automation.definition.steps.push(automationStep())
|
|
||||||
automation = await config.createAutomation(automation)
|
|
||||||
automation.definition.steps = []
|
automation.definition.steps = []
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
// check the post request honours updates with same id
|
await config.api.automation.update(automation)
|
||||||
await update(automation)
|
|
||||||
|
|
||||||
// events
|
expect(events.automation.stepDeleted).toHaveBeenCalledTimes(1)
|
||||||
expect(events.automation.stepDeleted).toHaveBeenCalledTimes(2)
|
|
||||||
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
expect(events.automation.stepCreated).not.toHaveBeenCalled()
|
||||||
expect(events.automation.created).not.toHaveBeenCalled()
|
expect(events.automation.created).not.toHaveBeenCalled()
|
||||||
expect(events.automation.triggerUpdated).not.toHaveBeenCalled()
|
expect(events.automation.triggerUpdated).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("adds and removes automation steps", async () => {
|
it("adds and removes automation steps", async () => {
|
||||||
let automation = newAutomation()
|
const { automation } = await config.api.automation.post(newAutomation())
|
||||||
automation = await config.createAutomation(automation)
|
|
||||||
automation.definition.steps = [automationStep(), automationStep()]
|
automation.definition.steps = [automationStep(), automationStep()]
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
// check the post request honours updates with same id
|
await config.api.automation.update(automation)
|
||||||
await update(automation)
|
|
||||||
|
|
||||||
// events
|
|
||||||
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
|
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
|
||||||
expect(events.automation.stepDeleted).toHaveBeenCalledTimes(1)
|
expect(events.automation.stepDeleted).toHaveBeenCalledTimes(1)
|
||||||
expect(events.automation.created).not.toHaveBeenCalled()
|
expect(events.automation.created).not.toHaveBeenCalled()
|
||||||
|
@ -551,16 +507,24 @@ describe("/automations", () => {
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
it("return all the automations for an instance", async () => {
|
it("return all the automations for an instance", async () => {
|
||||||
await clearAllAutomations(config)
|
const fetchResponse = await config.api.automation.fetch()
|
||||||
const autoConfig = await config.createAutomation(basicAutomation())
|
for (const auto of fetchResponse.automations) {
|
||||||
const res = await request
|
await config.api.automation.delete(auto)
|
||||||
.get(`/api/automations`)
|
}
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.automations[0]).toEqual(
|
const { automation: automation1 } = await config.api.automation.post(
|
||||||
expect.objectContaining(autoConfig)
|
newAutomation()
|
||||||
|
)
|
||||||
|
const { automation: automation2 } = await config.api.automation.post(
|
||||||
|
newAutomation()
|
||||||
|
)
|
||||||
|
const { automation: automation3 } = await config.api.automation.post(
|
||||||
|
newAutomation()
|
||||||
|
)
|
||||||
|
|
||||||
|
const { automations } = await config.api.automation.fetch()
|
||||||
|
expect(automations).toEqual(
|
||||||
|
expect.arrayContaining([automation1, automation2, automation3])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -575,29 +539,25 @@ describe("/automations", () => {
|
||||||
|
|
||||||
describe("destroy", () => {
|
describe("destroy", () => {
|
||||||
it("deletes a automation by its ID", async () => {
|
it("deletes a automation by its ID", async () => {
|
||||||
const automation = await config.createAutomation()
|
const { automation } = await config.api.automation.post(newAutomation())
|
||||||
const res = await request
|
const { id } = await config.api.automation.delete(automation)
|
||||||
.delete(`/api/automations/${automation._id}/${automation._rev}`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.id).toEqual(automation._id)
|
expect(id).toEqual(automation._id)
|
||||||
expect(events.automation.deleted).toHaveBeenCalledTimes(1)
|
expect(events.automation.deleted).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("cannot delete a row action automation", async () => {
|
it("cannot delete a row action automation", async () => {
|
||||||
const automation = await config.createAutomation(
|
const { automation } = await config.api.automation.post(
|
||||||
setup.structures.rowActionAutomation()
|
setup.structures.rowActionAutomation()
|
||||||
)
|
)
|
||||||
await request
|
|
||||||
.delete(`/api/automations/${automation._id}/${automation._rev}`)
|
await config.api.automation.delete(automation, {
|
||||||
.set(config.defaultHeaders())
|
status: 422,
|
||||||
.expect("Content-Type", /json/)
|
body: {
|
||||||
.expect(422, {
|
|
||||||
message: "Row actions automations cannot be deleted",
|
message: "Row actions automations cannot be deleted",
|
||||||
status: 422,
|
status: 422,
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
|
||||||
expect(events.automation.deleted).not.toHaveBeenCalled()
|
expect(events.automation.deleted).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
@ -614,10 +574,19 @@ describe("/automations", () => {
|
||||||
|
|
||||||
describe("checkForCollectStep", () => {
|
describe("checkForCollectStep", () => {
|
||||||
it("should return true if a collect step exists in an automation", async () => {
|
it("should return true if a collect step exists in an automation", async () => {
|
||||||
let automation = collectAutomation()
|
const { automation } = await config.api.automation.post(
|
||||||
await config.createAutomation(automation)
|
collectAutomation()
|
||||||
let res = await sdk.automations.utils.checkForCollectStep(automation)
|
)
|
||||||
expect(res).toEqual(true)
|
expect(sdk.automations.utils.checkForCollectStep(automation)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false if a collect step does not exist in an automation", async () => {
|
||||||
|
const { automation } = await config.api.automation.post(newAutomation())
|
||||||
|
expect(sdk.automations.utils.checkForCollectStep(automation)).toEqual(
|
||||||
|
false
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -628,28 +597,45 @@ describe("/automations", () => {
|
||||||
])(
|
])(
|
||||||
"triggers an update row automation and compares new to old rows with old city '%s' and new city '%s'",
|
"triggers an update row automation and compares new to old rows with old city '%s' and new city '%s'",
|
||||||
async ({ oldCity, newCity }) => {
|
async ({ oldCity, newCity }) => {
|
||||||
const expectedResult = oldCity === newCity
|
let table = await config.api.table.save(basicTable())
|
||||||
|
|
||||||
let table = await config.createTable()
|
const { automation } = await config.api.automation.post(
|
||||||
|
filterAutomation({
|
||||||
|
definition: {
|
||||||
|
trigger: {
|
||||||
|
inputs: {
|
||||||
|
tableId: table._id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
inputs: {
|
||||||
|
condition: FilterConditions.EQUAL,
|
||||||
|
field: "{{ trigger.row.City }}",
|
||||||
|
value: "{{ trigger.oldRow.City }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
let automation = await filterAutomation(config.getAppId())
|
const res = await config.api.automation.test(automation._id!, {
|
||||||
automation.definition.trigger.inputs.tableId = table._id
|
fields: {},
|
||||||
automation.definition.steps[0].inputs = {
|
|
||||||
condition: FilterConditions.EQUAL,
|
|
||||||
field: "{{ trigger.row.City }}",
|
|
||||||
value: "{{ trigger.oldRow.City }}",
|
|
||||||
}
|
|
||||||
automation = await config.createAutomation(automation)
|
|
||||||
let triggerInputs = {
|
|
||||||
oldRow: {
|
oldRow: {
|
||||||
City: oldCity,
|
City: oldCity,
|
||||||
},
|
},
|
||||||
row: {
|
row: {
|
||||||
City: newCity,
|
City: newCity,
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isDidNotTriggerResponse(res)) {
|
||||||
|
throw new Error("Automation did not trigger")
|
||||||
}
|
}
|
||||||
const res = await testAutomation(config, automation, triggerInputs)
|
|
||||||
expect(res.body.steps[1].outputs.result).toEqual(expectedResult)
|
const expectedResult = oldCity === newCity
|
||||||
|
expect(res.steps[1].outputs.result).toEqual(expectedResult)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -657,16 +643,18 @@ describe("/automations", () => {
|
||||||
let table: Table
|
let table: Table
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
table = await config.createTable({
|
table = await config.api.table.save(
|
||||||
name: "table",
|
basicTable(undefined, {
|
||||||
type: "table",
|
name: "table",
|
||||||
schema: {
|
type: "table",
|
||||||
Approved: {
|
schema: {
|
||||||
name: "Approved",
|
Approved: {
|
||||||
type: FieldType.BOOLEAN,
|
name: "Approved",
|
||||||
|
type: FieldType.BOOLEAN,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const testCases = [
|
const testCases = [
|
||||||
|
@ -712,33 +700,29 @@ describe("/automations", () => {
|
||||||
it.each(testCases)(
|
it.each(testCases)(
|
||||||
"$description",
|
"$description",
|
||||||
async ({ filters, row, oldRow, expectToRun }) => {
|
async ({ filters, row, oldRow, expectToRun }) => {
|
||||||
let automation = await updateRowAutomationWithFilters(
|
let req = updateRowAutomationWithFilters(config.getAppId(), table._id!)
|
||||||
config.getAppId(),
|
req.definition.trigger.inputs = {
|
||||||
table._id!
|
|
||||||
)
|
|
||||||
automation.definition.trigger.inputs = {
|
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
filters,
|
filters,
|
||||||
}
|
}
|
||||||
automation = await config.createAutomation(automation)
|
|
||||||
|
|
||||||
const inputs = {
|
const { automation } = await config.api.automation.post(req)
|
||||||
row: {
|
const res = await config.api.automation.test(automation._id!, {
|
||||||
tableId: table._id,
|
fields: {},
|
||||||
...row,
|
|
||||||
},
|
|
||||||
oldRow: {
|
oldRow: {
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
...oldRow,
|
...oldRow,
|
||||||
},
|
},
|
||||||
}
|
row: {
|
||||||
|
tableId: table._id,
|
||||||
|
...row,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const res = await testAutomation(config, automation, inputs)
|
if (isDidNotTriggerResponse(res)) {
|
||||||
|
expect(expectToRun).toEqual(false)
|
||||||
if (expectToRun) {
|
|
||||||
expect(res.body.steps[1].outputs.success).toEqual(true)
|
|
||||||
} else {
|
} else {
|
||||||
expect(res.body.outputs.success).toEqual(false)
|
expect(res.steps[1].outputs.success).toEqual(expectToRun)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -53,15 +53,6 @@ export const clearAllApps = async (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clearAllAutomations = async (config: TestConfiguration) => {
|
|
||||||
const { automations } = await config.getAllAutomations()
|
|
||||||
for (let auto of automations) {
|
|
||||||
await context.doInAppContext(config.getAppId(), async () => {
|
|
||||||
await config.deleteAutomation(auto)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const wipeDb = async () => {
|
export const wipeDb = async () => {
|
||||||
const couchInfo = db.getCouchInfo()
|
const couchInfo = db.getCouchInfo()
|
||||||
const nano = Nano({
|
const nano = Nano({
|
||||||
|
|
|
@ -258,7 +258,7 @@ export default class TestConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async withApp(app: App | string, f: () => Promise<void>) {
|
async withApp<R>(app: App | string, f: () => Promise<R>) {
|
||||||
const oldAppId = this.appId
|
const oldAppId = this.appId
|
||||||
this.appId = typeof app === "string" ? app : app.appId
|
this.appId = typeof app === "string" ? app : app.appId
|
||||||
try {
|
try {
|
||||||
|
@ -268,6 +268,10 @@ export default class TestConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async withProdApp<R>(f: () => Promise<R>) {
|
||||||
|
return await this.withApp(this.getProdAppId(), f)
|
||||||
|
}
|
||||||
|
|
||||||
// UTILS
|
// UTILS
|
||||||
|
|
||||||
_req<Req extends Record<string, any> | void, Res>(
|
_req<Req extends Record<string, any> | void, Res>(
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
import {
|
import {
|
||||||
Automation,
|
Automation,
|
||||||
|
CreateAutomationResponse,
|
||||||
|
DeleteAutomationResponse,
|
||||||
FetchAutomationResponse,
|
FetchAutomationResponse,
|
||||||
|
GetAutomationActionDefinitionsResponse,
|
||||||
|
GetAutomationStepDefinitionsResponse,
|
||||||
|
GetAutomationTriggerDefinitionsResponse,
|
||||||
TestAutomationRequest,
|
TestAutomationRequest,
|
||||||
TestAutomationResponse,
|
TestAutomationResponse,
|
||||||
|
TriggerAutomationRequest,
|
||||||
|
TriggerAutomationResponse,
|
||||||
|
UpdateAutomationRequest,
|
||||||
|
UpdateAutomationResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
|
|
||||||
|
@ -20,6 +29,39 @@ export class AutomationAPI extends TestAPI {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getActions = async (
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<GetAutomationActionDefinitionsResponse> => {
|
||||||
|
return await this._get<GetAutomationActionDefinitionsResponse>(
|
||||||
|
`/api/automations/actions/list`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTriggers = async (
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<GetAutomationTriggerDefinitionsResponse> => {
|
||||||
|
return await this._get<GetAutomationTriggerDefinitionsResponse>(
|
||||||
|
`/api/automations/triggers/list`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefinitions = async (
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<GetAutomationStepDefinitionsResponse> => {
|
||||||
|
return await this._get<GetAutomationStepDefinitionsResponse>(
|
||||||
|
`/api/automations/definitions/list`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fetch = async (
|
fetch = async (
|
||||||
expectations?: Expectations
|
expectations?: Expectations
|
||||||
): Promise<FetchAutomationResponse> => {
|
): Promise<FetchAutomationResponse> => {
|
||||||
|
@ -31,11 +73,14 @@ export class AutomationAPI extends TestAPI {
|
||||||
post = async (
|
post = async (
|
||||||
body: Automation,
|
body: Automation,
|
||||||
expectations?: Expectations
|
expectations?: Expectations
|
||||||
): Promise<Automation> => {
|
): Promise<CreateAutomationResponse> => {
|
||||||
const result = await this._post<Automation>(`/api/automations`, {
|
const result = await this._post<CreateAutomationResponse>(
|
||||||
body,
|
`/api/automations`,
|
||||||
expectations,
|
{
|
||||||
})
|
body,
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,4 +97,40 @@ export class AutomationAPI extends TestAPI {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trigger = async (
|
||||||
|
id: string,
|
||||||
|
body: TriggerAutomationRequest,
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<TriggerAutomationResponse> => {
|
||||||
|
return await this._post<TriggerAutomationResponse>(
|
||||||
|
`/api/automations/${id}/trigger`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
update = async (
|
||||||
|
body: UpdateAutomationRequest,
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<UpdateAutomationResponse> => {
|
||||||
|
return await this._put<UpdateAutomationResponse>(`/api/automations`, {
|
||||||
|
body,
|
||||||
|
expectations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async (
|
||||||
|
automation: Automation,
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<DeleteAutomationResponse> => {
|
||||||
|
return await this._delete<DeleteAutomationResponse>(
|
||||||
|
`/api/automations/${automation._id!}/${automation._rev!}`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,43 +19,43 @@ import { PluginAPI } from "./plugin"
|
||||||
import { WebhookAPI } from "./webhook"
|
import { WebhookAPI } from "./webhook"
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
table: TableAPI
|
|
||||||
legacyView: LegacyViewAPI
|
|
||||||
viewV2: ViewV2API
|
|
||||||
row: RowAPI
|
|
||||||
permission: PermissionAPI
|
|
||||||
datasource: DatasourceAPI
|
|
||||||
screen: ScreenAPI
|
|
||||||
application: ApplicationAPI
|
application: ApplicationAPI
|
||||||
backup: BackupAPI
|
|
||||||
attachment: AttachmentAPI
|
attachment: AttachmentAPI
|
||||||
user: UserAPI
|
automation: AutomationAPI
|
||||||
|
backup: BackupAPI
|
||||||
|
datasource: DatasourceAPI
|
||||||
|
legacyView: LegacyViewAPI
|
||||||
|
permission: PermissionAPI
|
||||||
|
plugin: PluginAPI
|
||||||
query: QueryAPI
|
query: QueryAPI
|
||||||
roles: RoleAPI
|
roles: RoleAPI
|
||||||
templates: TemplateAPI
|
row: RowAPI
|
||||||
rowAction: RowActionAPI
|
rowAction: RowActionAPI
|
||||||
automation: AutomationAPI
|
screen: ScreenAPI
|
||||||
plugin: PluginAPI
|
table: TableAPI
|
||||||
|
templates: TemplateAPI
|
||||||
|
user: UserAPI
|
||||||
|
viewV2: ViewV2API
|
||||||
webhook: WebhookAPI
|
webhook: WebhookAPI
|
||||||
|
|
||||||
constructor(config: TestConfiguration) {
|
constructor(config: TestConfiguration) {
|
||||||
this.table = new TableAPI(config)
|
|
||||||
this.legacyView = new LegacyViewAPI(config)
|
|
||||||
this.viewV2 = new ViewV2API(config)
|
|
||||||
this.row = new RowAPI(config)
|
|
||||||
this.permission = new PermissionAPI(config)
|
|
||||||
this.datasource = new DatasourceAPI(config)
|
|
||||||
this.screen = new ScreenAPI(config)
|
|
||||||
this.application = new ApplicationAPI(config)
|
this.application = new ApplicationAPI(config)
|
||||||
this.backup = new BackupAPI(config)
|
|
||||||
this.attachment = new AttachmentAPI(config)
|
this.attachment = new AttachmentAPI(config)
|
||||||
this.user = new UserAPI(config)
|
this.automation = new AutomationAPI(config)
|
||||||
|
this.backup = new BackupAPI(config)
|
||||||
|
this.datasource = new DatasourceAPI(config)
|
||||||
|
this.legacyView = new LegacyViewAPI(config)
|
||||||
|
this.permission = new PermissionAPI(config)
|
||||||
|
this.plugin = new PluginAPI(config)
|
||||||
this.query = new QueryAPI(config)
|
this.query = new QueryAPI(config)
|
||||||
this.roles = new RoleAPI(config)
|
this.roles = new RoleAPI(config)
|
||||||
this.templates = new TemplateAPI(config)
|
this.row = new RowAPI(config)
|
||||||
this.rowAction = new RowActionAPI(config)
|
this.rowAction = new RowActionAPI(config)
|
||||||
this.automation = new AutomationAPI(config)
|
this.screen = new ScreenAPI(config)
|
||||||
this.plugin = new PluginAPI(config)
|
this.table = new TableAPI(config)
|
||||||
|
this.templates = new TemplateAPI(config)
|
||||||
|
this.user = new UserAPI(config)
|
||||||
|
this.viewV2 = new ViewV2API(config)
|
||||||
this.webhook = new WebhookAPI(config)
|
this.webhook = new WebhookAPI(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
Webhook,
|
Webhook,
|
||||||
WebhookActionType,
|
WebhookActionType,
|
||||||
BuiltinPermissionID,
|
BuiltinPermissionID,
|
||||||
|
DeepPartial,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { LoopInput } from "../../definitions/automations"
|
import { LoopInput } from "../../definitions/automations"
|
||||||
import { merge } from "lodash"
|
import { merge } from "lodash"
|
||||||
|
@ -184,21 +185,12 @@ export function newAutomation({
|
||||||
steps,
|
steps,
|
||||||
trigger,
|
trigger,
|
||||||
}: { steps?: AutomationStep[]; trigger?: AutomationTrigger } = {}) {
|
}: { steps?: AutomationStep[]; trigger?: AutomationTrigger } = {}) {
|
||||||
const automation = basicAutomation()
|
return basicAutomation({
|
||||||
|
definition: {
|
||||||
if (trigger) {
|
steps: steps || [automationStep()],
|
||||||
automation.definition.trigger = trigger
|
trigger: trigger || automationTrigger(),
|
||||||
} else {
|
},
|
||||||
automation.definition.trigger = automationTrigger()
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (steps) {
|
|
||||||
automation.definition.steps = steps
|
|
||||||
} else {
|
|
||||||
automation.definition.steps = [automationStep()]
|
|
||||||
}
|
|
||||||
|
|
||||||
return automation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rowActionAutomation() {
|
export function rowActionAutomation() {
|
||||||
|
@ -211,8 +203,8 @@ export function rowActionAutomation() {
|
||||||
return automation
|
return automation
|
||||||
}
|
}
|
||||||
|
|
||||||
export function basicAutomation(appId?: string): Automation {
|
export function basicAutomation(opts?: DeepPartial<Automation>): Automation {
|
||||||
return {
|
const baseAutomation: Automation = {
|
||||||
name: "My Automation",
|
name: "My Automation",
|
||||||
screenId: "kasdkfldsafkl",
|
screenId: "kasdkfldsafkl",
|
||||||
live: true,
|
live: true,
|
||||||
|
@ -241,8 +233,9 @@ export function basicAutomation(appId?: string): Automation {
|
||||||
steps: [],
|
steps: [],
|
||||||
},
|
},
|
||||||
type: "automation",
|
type: "automation",
|
||||||
appId: appId!,
|
appId: "appId",
|
||||||
}
|
}
|
||||||
|
return merge(baseAutomation, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function basicCronAutomation(appId: string, cron: string): Automation {
|
export function basicCronAutomation(appId: string, cron: string): Automation {
|
||||||
|
@ -387,16 +380,21 @@ export function loopAutomation(
|
||||||
return automation as Automation
|
return automation as Automation
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectAutomation(tableId?: string): Automation {
|
export function collectAutomation(opts?: DeepPartial<Automation>): Automation {
|
||||||
const automation: any = {
|
const baseAutomation: Automation = {
|
||||||
|
appId: "appId",
|
||||||
name: "looping",
|
name: "looping",
|
||||||
type: "automation",
|
type: "automation",
|
||||||
definition: {
|
definition: {
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
id: "b",
|
id: "b",
|
||||||
type: "ACTION",
|
name: "b",
|
||||||
|
tagline: "An automation action step",
|
||||||
|
icon: "Icon",
|
||||||
|
type: AutomationStepType.ACTION,
|
||||||
internal: true,
|
internal: true,
|
||||||
|
description: "Execute script",
|
||||||
stepId: AutomationActionStepId.EXECUTE_SCRIPT,
|
stepId: AutomationActionStepId.EXECUTE_SCRIPT,
|
||||||
inputs: {
|
inputs: {
|
||||||
code: "return [1,2,3]",
|
code: "return [1,2,3]",
|
||||||
|
@ -405,8 +403,12 @@ export function collectAutomation(tableId?: string): Automation {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "c",
|
id: "c",
|
||||||
type: "ACTION",
|
name: "c",
|
||||||
|
type: AutomationStepType.ACTION,
|
||||||
|
tagline: "An automation action step",
|
||||||
|
icon: "Icon",
|
||||||
internal: true,
|
internal: true,
|
||||||
|
description: "Collect",
|
||||||
stepId: AutomationActionStepId.COLLECT,
|
stepId: AutomationActionStepId.COLLECT,
|
||||||
inputs: {
|
inputs: {
|
||||||
collection: "{{ literal steps.1.value }}",
|
collection: "{{ literal steps.1.value }}",
|
||||||
|
@ -416,24 +418,28 @@ export function collectAutomation(tableId?: string): Automation {
|
||||||
],
|
],
|
||||||
trigger: {
|
trigger: {
|
||||||
id: "a",
|
id: "a",
|
||||||
type: "TRIGGER",
|
type: AutomationStepType.TRIGGER,
|
||||||
event: AutomationEventType.ROW_SAVE,
|
event: AutomationEventType.ROW_SAVE,
|
||||||
stepId: AutomationTriggerStepId.ROW_SAVED,
|
stepId: AutomationTriggerStepId.ROW_SAVED,
|
||||||
|
name: "trigger Step",
|
||||||
|
tagline: "An automation trigger",
|
||||||
|
description: "A trigger",
|
||||||
|
icon: "Icon",
|
||||||
inputs: {
|
inputs: {
|
||||||
tableId,
|
tableId: "tableId",
|
||||||
},
|
},
|
||||||
schema: TRIGGER_DEFINITIONS.ROW_SAVED.schema,
|
schema: TRIGGER_DEFINITIONS.ROW_SAVED.schema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return automation
|
return merge(baseAutomation, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterAutomation(appId: string, tableId?: string): Automation {
|
export function filterAutomation(opts?: DeepPartial<Automation>): Automation {
|
||||||
const automation: Automation = {
|
const automation: Automation = {
|
||||||
name: "looping",
|
name: "looping",
|
||||||
type: "automation",
|
type: "automation",
|
||||||
appId,
|
appId: "appId",
|
||||||
definition: {
|
definition: {
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
|
@ -459,13 +465,13 @@ export function filterAutomation(appId: string, tableId?: string): Automation {
|
||||||
event: AutomationEventType.ROW_SAVE,
|
event: AutomationEventType.ROW_SAVE,
|
||||||
stepId: AutomationTriggerStepId.ROW_SAVED,
|
stepId: AutomationTriggerStepId.ROW_SAVED,
|
||||||
inputs: {
|
inputs: {
|
||||||
tableId: tableId!,
|
tableId: "tableId",
|
||||||
},
|
},
|
||||||
schema: TRIGGER_DEFINITIONS.ROW_SAVED.schema,
|
schema: TRIGGER_DEFINITIONS.ROW_SAVED.schema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return automation
|
return merge(automation, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateRowAutomationWithFilters(
|
export function updateRowAutomationWithFilters(
|
||||||
|
|
|
@ -75,6 +75,7 @@ export interface TestAutomationRequest {
|
||||||
revision?: string
|
revision?: string
|
||||||
fields: Record<string, any>
|
fields: Record<string, any>
|
||||||
row?: Row
|
row?: Row
|
||||||
|
oldRow?: Row
|
||||||
}
|
}
|
||||||
export type TestAutomationResponse = AutomationResults | DidNotTriggerResponse
|
export type TestAutomationResponse = AutomationResults | DidNotTriggerResponse
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue