From 0c12217b14d806c6439694dbd8de11922b3e2ef2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 20:11:28 +0100 Subject: [PATCH 01/29] Convert screens store to ts --- .../src/stores/builder/{screens.js => screens.ts} | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename packages/builder/src/stores/builder/{screens.js => screens.ts} (98%) diff --git a/packages/builder/src/stores/builder/screens.js b/packages/builder/src/stores/builder/screens.ts similarity index 98% rename from packages/builder/src/stores/builder/screens.js rename to packages/builder/src/stores/builder/screens.ts index 8298a1469d..5c1fa6da1a 100644 --- a/packages/builder/src/stores/builder/screens.js +++ b/packages/builder/src/stores/builder/screens.ts @@ -13,13 +13,19 @@ import { import { createHistoryStore } from "@/stores/builder/history" import { API } from "@/api" import { BudiStore } from "../BudiStore" +import { Screen } from "@budibase/types" -export const INITIAL_SCREENS_STATE = { +interface ScreenState { + screens: Screen[] + selectedScreenId: string | null +} + +export const INITIAL_SCREENS_STATE: ScreenState = { screens: [], selectedScreenId: null, } -export class ScreenStore extends BudiStore { +export class ScreenStore extends BudiStore { constructor() { super(INITIAL_SCREENS_STATE) From 8160914bb33954af7d49b5eb493551c4f571fa86 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 20:26:37 +0100 Subject: [PATCH 02/29] Initial typings --- .../builder/src/stores/builder/components.ts | 13 +-- .../builder/src/stores/builder/screens.ts | 87 ++++++++++--------- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 9ad9a75f84..e60d4e80a4 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -57,7 +57,7 @@ interface ComponentSetting { interface ComponentState { components: Record customComponents: string[] - selectedComponentId: string | null + selectedComponentId: string | null | undefined componentToPaste?: Component | null settingsCache: Record selectedScreenId?: string | null @@ -478,10 +478,11 @@ export class ComponentStore extends BudiStore { extras._children = [] } + const $selectedScreen = get(selectedScreen) // Add step name to form steps - if (componentName.endsWith("/formstep")) { + if (componentName.endsWith("/formstep") && $selectedScreen) { const parentForm = findClosestMatchingComponent( - get(selectedScreen).props, + $selectedScreen.props, get(selectedComponent)._id, (component: Component) => component._component.endsWith("/form") ) @@ -608,7 +609,7 @@ export class ComponentStore extends BudiStore { async patch( patchFn: (component: Component, screen: Screen) => any, componentId?: string, - screenId?: string + screenId?: string | null ) { // Use selected component by default if (!componentId || !screenId) { @@ -840,7 +841,7 @@ export class ComponentStore extends BudiStore { getPrevious() { const state = get(this.store) const componentId = state.selectedComponentId - const screen = get(selectedScreen) + const screen = get(selectedScreen)! const parent = findComponentParent(screen.props, componentId) const index = parent?._children.findIndex( (x: Component) => x._id === componentId @@ -889,7 +890,7 @@ export class ComponentStore extends BudiStore { const state = get(this.store) const component = get(selectedComponent) const componentId = component?._id - const screen = get(selectedScreen) + const screen = get(selectedScreen)! const parent = findComponentParent(screen.props, componentId) const index = parent?._children.findIndex( (x: Component) => x._id === componentId diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index 5c1fa6da1a..15c4c9b88f 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -13,11 +13,11 @@ import { import { createHistoryStore } from "@/stores/builder/history" import { API } from "@/api" import { BudiStore } from "../BudiStore" -import { Screen } from "@budibase/types" +import { Component, Screen } from "@budibase/types" interface ScreenState { screens: Screen[] - selectedScreenId: string | null + selectedScreenId: string | null | undefined } export const INITIAL_SCREENS_STATE: ScreenState = { @@ -43,7 +43,7 @@ export class ScreenStore extends BudiStore { this.sequentialScreenPatch = this.sequentialScreenPatch.bind(this) this.removeCustomLayout = this.removeCustomLayout.bind(this) - this.history = createHistoryStore({ + const history = createHistoryStore({ getDoc: id => get(this.store).screens?.find(screen => screen._id === id), selectDoc: this.select, afterAction: () => { @@ -57,8 +57,8 @@ export class ScreenStore extends BudiStore { }, }) - this.delete = this.history.wrapDeleteDoc(this.deleteScreen) - this.save = this.history.wrapSaveDoc(this.saveScreen) + this.delete = history.wrapDeleteDoc(this.deleteScreen) + this.save = history.wrapSaveDoc(this.saveScreen) } /** @@ -72,7 +72,7 @@ export class ScreenStore extends BudiStore { * Replace ALL store screens with application package screens * @param {object} pkg */ - syncAppScreens(pkg) { + syncAppScreens(pkg: { screens: Screen[] }) { this.update(state => ({ ...state, screens: [...pkg.screens], @@ -85,7 +85,7 @@ export class ScreenStore extends BudiStore { * @param {string} screenId * @returns */ - select(screenId) { + select(screenId: string) { // Check this screen exists const state = get(this.store) const screen = state.screens.find(screen => screen._id === screenId) @@ -113,12 +113,12 @@ export class ScreenStore extends BudiStore { * @throws Will throw an error containing the name of the component causing * the invalid screen state */ - validate(screen) { + validate(screen: Screen) { // Recursive function to find any illegal children in component trees const findIllegalChild = ( - component, - illegalChildren = [], - legalDirectChildren = [] + component: Component, + illegalChildren: string[] = [], + legalDirectChildren: string[] = [] ) => { const type = component._component @@ -178,7 +178,7 @@ export class ScreenStore extends BudiStore { const illegalChild = findIllegalChild(screen.props) if (illegalChild) { const def = componentStore.getDefinition(illegalChild) - throw `You can't place a ${def.name} here` + throw `You can't place a ${def?.name} here` } } @@ -189,7 +189,7 @@ export class ScreenStore extends BudiStore { * @param {object} screen * @returns {object} */ - async saveScreen(screen) { + async saveScreen(screen: Screen): Promise { const appState = get(appStore) // Validate screen structure if the app supports it @@ -236,7 +236,7 @@ export class ScreenStore extends BudiStore { * After saving a screen, sync plugins and routes to the appStore * @param {object} savedScreen */ - async syncScreenData(savedScreen) { + async syncScreenData(savedScreen: Screen) { const appState = get(appStore) // If plugins changed we need to fetch the latest app metadata let usedPlugins = appState.usedPlugins @@ -262,21 +262,23 @@ export class ScreenStore extends BudiStore { * This is slightly better than just a traditional "patch" endpoint and this * supports deeply mutating the current doc rather than just appending data. */ - sequentialScreenPatch = Utils.sequential(async (patchFn, screenId) => { - const state = get(this.store) - const screen = state.screens.find(screen => screen._id === screenId) - if (!screen) { - return - } - let clone = cloneDeep(screen) - const result = patchFn(clone) + sequentialScreenPatch = Utils.sequential( + async (patchFn: (screen: Screen) => any, screenId: string) => { + const state = get(this.store) + const screen = state.screens.find(screen => screen._id === screenId) + if (!screen) { + return + } + let clone = cloneDeep(screen) + const result = patchFn(clone) - // An explicit false result means skip this change - if (result === false) { - return + // An explicit false result means skip this change + if (result === false) { + return + } + return this.save(clone) } - return this.save(clone) - }) + ) /** * @param {function} patchFn @@ -304,7 +306,7 @@ export class ScreenStore extends BudiStore { * @param {object} screen * @returns */ - async replace(screenId, screen) { + async replace(screenId: string, screen: Screen) { if (!screenId) { return } @@ -343,14 +345,14 @@ export class ScreenStore extends BudiStore { * @param {object | array} screens * @returns */ - async deleteScreen(screens) { + async deleteScreen(screens: Screen[]) { const screensToDelete = Array.isArray(screens) ? screens : [screens] // Build array of promises to speed up bulk deletions - let promises = [] - let deleteUrls = [] + let promises: Promise[] = [] + let deleteUrls: string[] = [] screensToDelete.forEach(screen => { // Delete the screen - promises.push(API.deleteScreen(screen._id, screen._rev)) + promises.push(API.deleteScreen(screen._id!, screen._rev!)) // Remove links to this screen deleteUrls.push(screen.routing.route) }) @@ -365,7 +367,10 @@ export class ScreenStore extends BudiStore { }) // Deselect the current screen if it was deleted - if (deletedIds.includes(state.selectedScreenId)) { + if ( + state.selectedScreenId && + deletedIds.includes(state.selectedScreenId) + ) { state.selectedScreenId = null componentStore.update(state => ({ ...state, @@ -395,13 +400,13 @@ export class ScreenStore extends BudiStore { * @param {any} value * @returns */ - async updateSetting(screen, name, value) { + async updateSetting(screen: Screen, name: string, value: string) { if (!screen || !name) { return } // Apply setting update - const patchFn = screen => { + const patchFn = (screen: Screen) => { if (!screen) { return false } @@ -428,7 +433,7 @@ export class ScreenStore extends BudiStore { ) }) if (otherHomeScreens.length && updatedScreen.routing.homeScreen) { - const patchFn = screen => { + const patchFn = (screen: Screen) => { screen.routing.homeScreen = false } for (let otherHomeScreen of otherHomeScreens) { @@ -438,11 +443,11 @@ export class ScreenStore extends BudiStore { } // Move to layouts store - async removeCustomLayout(screen) { + async removeCustomLayout(screen: Screen) { // Pull relevant settings from old layout, if required const layout = get(layoutStore).layouts.find(x => x._id === screen.layoutId) - const patchFn = screen => { - screen.layoutId = null + const patchFn = (screen: Screen) => { + delete screen.layoutId screen.showNavigation = layout?.props.navigation !== "None" screen.width = layout?.props.width || "Large" } @@ -454,9 +459,9 @@ export class ScreenStore extends BudiStore { * and up-to-date. Ensures stability after a product update. * @param {object} screen */ - async enrichEmptySettings(screen) { + async enrichEmptySettings(screen: Screen) { // Flatten the recursive component tree - const components = findAllMatchingComponents(screen.props, x => x) + const components = findAllMatchingComponents(screen.props, (x: string) => x) // Iterate over all components and run checks components.forEach(component => { From ce322211b993e770017f859c6030c037485d77a7 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 20:29:08 +0100 Subject: [PATCH 03/29] Initial convert history store --- packages/builder/src/stores/builder/{history.js => history.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/builder/src/stores/builder/{history.js => history.ts} (99%) diff --git a/packages/builder/src/stores/builder/history.js b/packages/builder/src/stores/builder/history.ts similarity index 99% rename from packages/builder/src/stores/builder/history.js rename to packages/builder/src/stores/builder/history.ts index 62a8ed2f97..1889b0b5f6 100644 --- a/packages/builder/src/stores/builder/history.js +++ b/packages/builder/src/stores/builder/history.ts @@ -1,4 +1,4 @@ -import * as jsonpatch from "fast-json-patch/index.mjs" +import * as jsonpatch from "fast-json-patch" import { writable, derived, get } from "svelte/store" export const Operations = { From 08f9b2046e0ff93a90146e0722359bf55b3eb20a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 20:58:44 +0100 Subject: [PATCH 04/29] Fix types --- .../builder/src/stores/builder/history.ts | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/builder/src/stores/builder/history.ts b/packages/builder/src/stores/builder/history.ts index 1889b0b5f6..e7888f7be4 100644 --- a/packages/builder/src/stores/builder/history.ts +++ b/packages/builder/src/stores/builder/history.ts @@ -1,13 +1,27 @@ import * as jsonpatch from "fast-json-patch" import { writable, derived, get } from "svelte/store" -export const Operations = { - Add: "Add", - Delete: "Delete", - Change: "Change", +export const enum Operations { + Add = "Add", + Delete = "Delete", + Change = "Change", } -export const initialState = { +interface Operator { + id: string + type: Operations + doc: any + forwardPatch?: jsonpatch.Operation[] + backwardsPatch?: jsonpatch.Operation[] +} + +interface HistoryState { + history: any[] + position: number + loading?: boolean +} + +export const initialState: HistoryState = { history: [], position: 0, loading: false, @@ -18,6 +32,11 @@ export const createHistoryStore = ({ selectDoc, beforeAction, afterAction, +}: { + getDoc: any + selectDoc: any + beforeAction: any + afterAction: any }) => { // Use a derived store to check if we are able to undo or redo any operations const store = writable(initialState) @@ -31,8 +50,8 @@ export const createHistoryStore = ({ // Wrapped versions of essential functions which we call ourselves when using // undo and redo - let saveFn - let deleteFn + let saveFn: any + let deleteFn: any /** * Internal util to set the loading flag @@ -66,14 +85,14 @@ export const createHistoryStore = ({ * For internal use only. * @param operation the operation to save */ - const saveOperation = operation => { + const saveOperation = (operation: Operator) => { 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() + operation.id = Math.random().toString() history = [...history.slice(0, state.position), operation] position += 1 } else { @@ -93,8 +112,8 @@ export const createHistoryStore = ({ * @param fn the save function * @returns {function} a wrapped version of the save function */ - const wrapSaveDoc = fn => { - saveFn = async (doc, operationId) => { + const wrapSaveDoc = (fn: (doc: any) => Promise) => { + saveFn = async (doc: any, operationId: string) => { // Only works on a single doc at a time if (!doc || Array.isArray(doc)) { return @@ -141,8 +160,8 @@ export const createHistoryStore = ({ * @param fn the delete function * @returns {function} a wrapped version of the delete function */ - const wrapDeleteDoc = fn => { - deleteFn = async (doc, operationId) => { + const wrapDeleteDoc = (fn: (doc: any) => Promise) => { + deleteFn = async (doc: any, operationId: string) => { // Only works on a single doc at a time if (!doc || Array.isArray(doc)) { return From f0ec4c5e00890fae1a2896e2d772137e3ea11097 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 21:04:50 +0100 Subject: [PATCH 05/29] Use generics on history --- .../builder/src/stores/builder/automations.ts | 2 +- packages/builder/src/stores/builder/history.ts | 16 ++++++++-------- packages/builder/src/stores/builder/screens.ts | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/builder/src/stores/builder/automations.ts b/packages/builder/src/stores/builder/automations.ts index 9b20b4cd03..7a1e856369 100644 --- a/packages/builder/src/stores/builder/automations.ts +++ b/packages/builder/src/stores/builder/automations.ts @@ -1434,7 +1434,7 @@ class AutomationStore extends BudiStore { constructor() { super(initialAutomationState) this.actions = automationActions(this) - this.history = createHistoryStore({ + this.history = createHistoryStore({ getDoc: this.actions.getDefinition.bind(this), selectDoc: this.actions.select.bind(this), beforeAction: () => {}, diff --git a/packages/builder/src/stores/builder/history.ts b/packages/builder/src/stores/builder/history.ts index e7888f7be4..b2aa01c03b 100644 --- a/packages/builder/src/stores/builder/history.ts +++ b/packages/builder/src/stores/builder/history.ts @@ -7,10 +7,10 @@ export const enum Operations { Change = "Change", } -interface Operator { +interface Operator { id: string type: Operations - doc: any + doc: T forwardPatch?: jsonpatch.Operation[] backwardsPatch?: jsonpatch.Operation[] } @@ -27,16 +27,16 @@ export const initialState: HistoryState = { loading: false, } -export const createHistoryStore = ({ +export const createHistoryStore = ({ getDoc, selectDoc, beforeAction, afterAction, }: { - getDoc: any - selectDoc: any - beforeAction: any - afterAction: any + getDoc: (id: string) => T | undefined + selectDoc: (id: string) => void + beforeAction?: (operation?: Operator) => void + afterAction?: (operation?: Operator) => void }) => { // Use a derived store to check if we are able to undo or redo any operations const store = writable(initialState) @@ -85,7 +85,7 @@ export const createHistoryStore = ({ * For internal use only. * @param operation the operation to save */ - const saveOperation = (operation: Operator) => { + const saveOperation = (operation: Operator) => { store.update(state => { // Update history let history = state.history diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index 15c4c9b88f..fff54b364d 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -18,6 +18,7 @@ import { Component, Screen } from "@budibase/types" interface ScreenState { screens: Screen[] selectedScreenId: string | null | undefined + selected?: Screen } export const INITIAL_SCREENS_STATE: ScreenState = { @@ -285,7 +286,7 @@ export class ScreenStore extends BudiStore { * @param {string | null} screenId * @returns */ - async patch(patchFn, screenId) { + async patch(patchFn, screenId: string) { // Default to the currently selected screen if (!screenId) { const state = get(this.store) From 25d450602f553fb2b31e42555085d20afbd11d3f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 21:08:31 +0100 Subject: [PATCH 06/29] Type history --- .../builder/src/stores/builder/history.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/builder/src/stores/builder/history.ts b/packages/builder/src/stores/builder/history.ts index b2aa01c03b..132dcd9ab8 100644 --- a/packages/builder/src/stores/builder/history.ts +++ b/packages/builder/src/stores/builder/history.ts @@ -1,3 +1,4 @@ +import { Document } from "@budibase/types" import * as jsonpatch from "fast-json-patch" import { writable, derived, get } from "svelte/store" @@ -7,7 +8,7 @@ export const enum Operations { Change = "Change", } -interface Operator { +interface Operator { id: string type: Operations doc: T @@ -15,19 +16,19 @@ interface Operator { backwardsPatch?: jsonpatch.Operation[] } -interface HistoryState { - history: any[] +interface HistoryState { + history: Operator[] position: number loading?: boolean } -export const initialState: HistoryState = { +export const initialState: HistoryState = { history: [], position: 0, loading: false, } -export const createHistoryStore = ({ +export const createHistoryStore = ({ getDoc, selectDoc, beforeAction, @@ -39,7 +40,7 @@ export const createHistoryStore = ({ afterAction?: (operation?: Operator) => void }) => { // Use a derived store to check if we are able to undo or redo any operations - const store = writable(initialState) + const store = writable>(initialState) const derivedStore = derived(store, $store => { return { ...$store, @@ -220,7 +221,7 @@ export const createHistoryStore = ({ // Undo ADD if (operation.type === Operations.Add) { // 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 await deleteFn(doc, operation.id) } @@ -238,7 +239,7 @@ export const createHistoryStore = ({ // Undo CHANGE else { // 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) { jsonpatch.applyPatch( doc, @@ -302,7 +303,7 @@ export const createHistoryStore = ({ // Redo DELETE else if (operation.type === Operations.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 await deleteFn(doc, operation.id) } @@ -310,7 +311,7 @@ export const createHistoryStore = ({ // Redo CHANGE else { // 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) { jsonpatch.applyPatch(doc, jsonpatch.deepClone(operation.forwardPatch)) await saveFn(doc, operation.id) From 6f063f6a213226c9cfbd25865c66161b5ad9e61c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 21:12:22 +0100 Subject: [PATCH 07/29] Type anys --- packages/builder/src/stores/builder/history.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/builder/src/stores/builder/history.ts b/packages/builder/src/stores/builder/history.ts index 132dcd9ab8..3f9b156498 100644 --- a/packages/builder/src/stores/builder/history.ts +++ b/packages/builder/src/stores/builder/history.ts @@ -51,8 +51,8 @@ export const createHistoryStore = ({ // Wrapped versions of essential functions which we call ourselves when using // undo and redo - let saveFn: any - let deleteFn: any + let saveFn: (doc: T, operationId: string) => Promise + let deleteFn: (doc: T, operationId: string) => Promise /** * Internal util to set the loading flag @@ -113,15 +113,15 @@ export const createHistoryStore = ({ * @param fn the save function * @returns {function} a wrapped version of the save function */ - const wrapSaveDoc = (fn: (doc: any) => Promise) => { - saveFn = async (doc: any, operationId: string) => { + const wrapSaveDoc = (fn: (doc: T) => Promise) => { + saveFn = async (doc: T, operationId: string) => { // Only works on a single doc at a time if (!doc || Array.isArray(doc)) { return } startLoading() try { - const oldDoc = getDoc(doc._id) + const oldDoc = getDoc(doc._id!) const newDoc = jsonpatch.deepClone(await fn(doc)) // Store the change @@ -161,8 +161,8 @@ export const createHistoryStore = ({ * @param fn the delete function * @returns {function} a wrapped version of the delete function */ - const wrapDeleteDoc = (fn: (doc: any) => Promise) => { - deleteFn = async (doc: any, operationId: string) => { + const wrapDeleteDoc = (fn: (doc: T) => Promise) => { + deleteFn = async (doc: T, operationId: string) => { // Only works on a single doc at a time if (!doc || Array.isArray(doc)) { return From 26955d5cd9d0168f39481cd6b3e09efb5c332daa Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 21:30:00 +0100 Subject: [PATCH 08/29] Type history --- .../builder/src/stores/builder/automations.ts | 6 ++-- .../builder/src/stores/builder/history.ts | 35 ++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/builder/src/stores/builder/automations.ts b/packages/builder/src/stores/builder/automations.ts index 7a1e856369..5e17b46155 100644 --- a/packages/builder/src/stores/builder/automations.ts +++ b/packages/builder/src/stores/builder/automations.ts @@ -2,7 +2,7 @@ import { derived, get } from "svelte/store" import { API } from "@/api" import { cloneDeep } from "lodash/fp" import { generate } from "shortid" -import { createHistoryStore } from "@/stores/builder/history" +import { createHistoryStore, HistoryStore } from "@/stores/builder/history" import { licensing } from "@/stores/portal" import { tables, appStore } from "@/stores/builder" import { notifications } from "@budibase/bbui" @@ -1428,13 +1428,13 @@ const automationActions = (store: AutomationStore) => ({ }) class AutomationStore extends BudiStore { - history: any + history: HistoryStore actions: ReturnType constructor() { super(initialAutomationState) this.actions = automationActions(this) - this.history = createHistoryStore({ + this.history = createHistoryStore({ getDoc: this.actions.getDefinition.bind(this), selectDoc: this.actions.select.bind(this), beforeAction: () => {}, diff --git a/packages/builder/src/stores/builder/history.ts b/packages/builder/src/stores/builder/history.ts index 3f9b156498..916e417872 100644 --- a/packages/builder/src/stores/builder/history.ts +++ b/packages/builder/src/stores/builder/history.ts @@ -1,6 +1,6 @@ import { Document } from "@budibase/types" import * as jsonpatch from "fast-json-patch" -import { writable, derived, get } from "svelte/store" +import { writable, derived, get, Readable } from "svelte/store" export const enum Operations { Add = "Add", @@ -9,7 +9,7 @@ export const enum Operations { } interface Operator { - id: string + id?: string type: Operations doc: T forwardPatch?: jsonpatch.Operation[] @@ -28,6 +28,25 @@ export const initialState: HistoryState = { loading: false, } +export interface HistoryStore + extends Readable< + HistoryState & { + canUndo: boolean + canRedo: boolean + } + > { + wrapSaveDoc: ( + fn: (doc: T) => Promise + ) => (doc: T, operationId?: string) => Promise + wrapDeleteDoc: ( + fn: (doc: T) => Promise + ) => (doc: T, operationId?: string) => Promise + + reset: () => void + undo: () => Promise + redo: () => Promise +} + export const createHistoryStore = ({ getDoc, selectDoc, @@ -38,7 +57,7 @@ export const createHistoryStore = ({ selectDoc: (id: string) => void beforeAction?: (operation?: Operator) => void afterAction?: (operation?: Operator) => void -}) => { +}): HistoryStore => { // 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 => { @@ -51,8 +70,8 @@ export const createHistoryStore = ({ // Wrapped versions of essential functions which we call ourselves when using // undo and redo - let saveFn: (doc: T, operationId: string) => Promise - let deleteFn: (doc: T, operationId: string) => Promise + let saveFn: (doc: T, operationId?: string) => Promise + let deleteFn: (doc: T, operationId?: string) => Promise /** * Internal util to set the loading flag @@ -113,8 +132,8 @@ export const createHistoryStore = ({ * @param fn the save function * @returns {function} a wrapped version of the save function */ - const wrapSaveDoc = (fn: (doc: T) => Promise) => { - saveFn = async (doc: T, operationId: string) => { + const wrapSaveDoc = (fn: (doc: T) => Promise) => { + saveFn = async (doc: T, operationId?: string) => { // Only works on a single doc at a time if (!doc || Array.isArray(doc)) { return @@ -162,7 +181,7 @@ export const createHistoryStore = ({ * @returns {function} a wrapped version of the delete function */ const wrapDeleteDoc = (fn: (doc: T) => Promise) => { - deleteFn = async (doc: T, operationId: string) => { + deleteFn = async (doc: T, operationId?: string) => { // Only works on a single doc at a time if (!doc || Array.isArray(doc)) { return From 13027c6a6d890000532e4f298c821570dfd0c6ca Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 22:58:37 +0100 Subject: [PATCH 09/29] Type utils --- .../src/utils/{utils.js => utils.ts} | 71 +++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) rename packages/frontend-core/src/utils/{utils.js => utils.ts} (82%) diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.ts similarity index 82% rename from packages/frontend-core/src/utils/utils.js rename to packages/frontend-core/src/utils/utils.ts index f0635fbeac..0eaca6548e 100644 --- a/packages/frontend-core/src/utils/utils.js +++ b/packages/frontend-core/src/utils/utils.ts @@ -1,8 +1,10 @@ import { makePropSafe as safe } from "@budibase/string-templates" import { Helpers } from "@budibase/bbui" 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 @@ -10,10 +12,15 @@ export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) * @param fn the async function to run * @return {Promise} a sequential version of the function */ -export const sequential = fn => { - let queue = [] - return (...params) => { - return new Promise((resolve, reject) => { +export const sequential = < + TReturn, + TFunction extends (...args: any[]) => Promise +>( + fn: TFunction +): ((...args: Parameters) => Promise) => { + let queue: any[] = [] + return (...params: Parameters) => { + return new Promise((resolve, reject) => { queue.push(async () => { let data, error try { @@ -45,9 +52,9 @@ export const sequential = fn => { * @param minDelay the minimum delay between invocations * @returns a debounced version of the callback */ -export const debounce = (callback, minDelay = 1000) => { - let timeout - return async (...params) => { +export const debounce = (callback: Function, minDelay = 1000) => { + let timeout: NodeJS.Timeout + return async (...params: any[]) => { return new Promise(resolve => { if (timeout) { clearTimeout(timeout) @@ -70,11 +77,11 @@ export const debounce = (callback, minDelay = 1000) => { * @param minDelay * @returns {Function} a throttled version function */ -export const throttle = (callback, minDelay = 1000) => { - let lastParams +export const throttle = (callback: Function, minDelay = 1000) => { + let lastParams: any[] let stalled = false let pending = false - const invoke = (...params) => { + const invoke = (...params: any[]) => { lastParams = params if (stalled) { pending = true @@ -98,10 +105,10 @@ export const throttle = (callback, minDelay = 1000) => { * @param callback the function to run * @returns {Function} */ -export const domDebounce = callback => { +export const domDebounce = (callback: Function) => { let active = false - let lastParams - return (...params) => { + let lastParams: any[] + return (...params: any[]) => { lastParams = params if (!active) { active = true @@ -119,7 +126,17 @@ export const domDebounce = callback => { * * @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 { _id, actionType, @@ -227,7 +244,11 @@ export const buildFormBlockButtonConfig = props => { const defaultButtons = [] - if (["Update", "Create"].includes(actionType) && showSaveButton !== false) { + if ( + actionType && + ["Update", "Create"].includes(actionType) && + showSaveButton !== false + ) { defaultButtons.push({ text: saveText || "Save", _id: Helpers.uuid(), @@ -251,7 +272,13 @@ export const buildFormBlockButtonConfig = props => { 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 || {} // Sanity check @@ -361,7 +388,7 @@ export const buildMultiStepFormBlockDefaultProps = props => { * @param {Object} filter UI filter * @returns {Object} parsed filter */ -export function parseFilter(filter) { +export function parseFilter(filter: UISearchFilter) { if (!filter?.groups) { return filter } @@ -369,13 +396,13 @@ export function parseFilter(filter) { const update = cloneDeep(filter) update.groups = update.groups - .map(group => { - group.filters = group.filters.filter(filter => { + ?.map(group => { + group.filters = group.filters?.filter((filter: any) => { 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 } From 058b3def3a1e235361da31da883bc41a39273864 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 23:03:01 +0100 Subject: [PATCH 10/29] Type remaining screens --- .../builder/src/stores/builder/components.ts | 1 + .../builder/src/stores/builder/screens.ts | 28 ++++++++++--------- packages/frontend-core/src/utils/utils.ts | 18 ++++++------ 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index e60d4e80a4..8fa7f5113d 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -41,6 +41,7 @@ interface ComponentDefinition { settings?: ComponentSetting[] features?: Record typeSupportPresets?: Record + illegalChildren?: string[] } interface ComponentSetting { diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index fff54b364d..d87d12f23c 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -27,6 +27,9 @@ export const INITIAL_SCREENS_STATE: ScreenState = { } export class ScreenStore extends BudiStore { + save: (doc: Screen) => Promise + delete: (doc: Screen) => Promise + constructor() { super(INITIAL_SCREENS_STATE) @@ -120,7 +123,7 @@ export class ScreenStore extends BudiStore { component: Component, illegalChildren: string[] = [], legalDirectChildren: string[] = [] - ) => { + ): string | undefined => { const type = component._component if (illegalChildren.includes(type)) { @@ -145,13 +148,6 @@ export class ScreenStore extends BudiStore { } const definition = componentStore.getDefinition(component._component) - // Reset whitelist for direct children - legalDirectChildren = [] - if (definition?.legalDirectChildren?.length) { - legalDirectChildren = definition.legalDirectChildren.map(x => { - return `@budibase/standard-components/${x}` - }) - } // Append blacklisted components and remove duplicates if (definition?.illegalChildren?.length) { @@ -264,7 +260,10 @@ export class ScreenStore extends BudiStore { * supports deeply mutating the current doc rather than just appending data. */ sequentialScreenPatch = Utils.sequential( - async (patchFn: (screen: Screen) => any, screenId: string) => { + async ( + patchFn: (screen: Screen) => any, + screenId: string + ): Promise => { const state = get(this.store) const screen = state.screens.find(screen => screen._id === screenId) if (!screen) { @@ -286,7 +285,10 @@ export class ScreenStore extends BudiStore { * @param {string | null} screenId * @returns */ - async patch(patchFn, screenId: string) { + async patch( + patchFn: (screen: Screen) => void, + screenId: string | undefined | null + ) { // Default to the currently selected screen if (!screenId) { const state = get(this.store) @@ -346,8 +348,8 @@ export class ScreenStore extends BudiStore { * @param {object | array} screens * @returns */ - async deleteScreen(screens: Screen[]) { - const screensToDelete = Array.isArray(screens) ? screens : [screens] + async deleteScreen(screen: Screen) { + const screensToDelete = [screen] // Build array of promises to speed up bulk deletions let promises: Promise[] = [] let deleteUrls: string[] = [] @@ -387,7 +389,7 @@ export class ScreenStore extends BudiStore { return state }) - return null + return } /** diff --git a/packages/frontend-core/src/utils/utils.ts b/packages/frontend-core/src/utils/utils.ts index 0eaca6548e..124f0f03b9 100644 --- a/packages/frontend-core/src/utils/utils.ts +++ b/packages/frontend-core/src/utils/utils.ts @@ -127,15 +127,15 @@ export const domDebounce = (callback: Function) => { * @param {any} props * */ export const buildFormBlockButtonConfig = (props?: { - _id: string - actionType: string - dataSource: { resourceId: string } - notificationOverride: boolean - actionUrl: string - showDeleteButton: boolean - deleteButtonLabel: string - showSaveButton: boolean - saveButtonLabel: string + _id?: string + actionType?: string + dataSource?: { resourceId: string } + notificationOverride?: boolean + actionUrl?: string + showDeleteButton?: boolean + deleteButtonLabel?: string + showSaveButton?: boolean + saveButtonLabel?: string }) => { const { _id, From 4671633bfeb6086113b2f06658d12e8d4acae419 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 20 Jan 2025 23:14:45 +0100 Subject: [PATCH 11/29] Fix types --- .../builder/src/stores/builder/componentTreeNodes.ts | 2 +- packages/builder/src/stores/builder/websocket.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/builder/componentTreeNodes.ts b/packages/builder/src/stores/builder/componentTreeNodes.ts index 420c540e37..367bc1f0d9 100644 --- a/packages/builder/src/stores/builder/componentTreeNodes.ts +++ b/packages/builder/src/stores/builder/componentTreeNodes.ts @@ -49,7 +49,7 @@ export class ComponentTreeNodesStore extends BudiStore { // Will ensure all parents of a node are expanded so that it is visible in the tree makeNodeVisible(componentId: string) { - const selectedScreen: Screen = get(selectedScreenStore) + const selectedScreen: Screen = get(selectedScreenStore)! const path = findComponentPath(selectedScreen.props, componentId) diff --git a/packages/builder/src/stores/builder/websocket.ts b/packages/builder/src/stores/builder/websocket.ts index bd9e2c8d4d..b9b6c0eb63 100644 --- a/packages/builder/src/stores/builder/websocket.ts +++ b/packages/builder/src/stores/builder/websocket.ts @@ -16,7 +16,14 @@ import { auth, appsStore } from "@/stores/portal" import { screenStore } from "./screens" import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core" import { notifications } from "@budibase/bbui" -import { Automation, Datasource, Role, Table, UIUser } from "@budibase/types" +import { + Automation, + Datasource, + Role, + Screen, + Table, + UIUser, +} from "@budibase/types" export const createBuilderWebsocket = (appId: string) => { const socket = createWebsocket("/socket/builder") From 79180bfac5512304dbf1d91234dec774f5bff42f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 Jan 2025 09:48:58 +0100 Subject: [PATCH 12/29] Cleanups --- packages/builder/src/stores/builder/componentTreeNodes.ts | 6 +++--- packages/builder/src/stores/builder/components.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/stores/builder/componentTreeNodes.ts b/packages/builder/src/stores/builder/componentTreeNodes.ts index 367bc1f0d9..775985abdc 100644 --- a/packages/builder/src/stores/builder/componentTreeNodes.ts +++ b/packages/builder/src/stores/builder/componentTreeNodes.ts @@ -1,7 +1,7 @@ import { get } from "svelte/store" import { selectedScreen as selectedScreenStore } from "./screens" import { findComponentPath } from "@/helpers/components" -import { Screen, Component } from "@budibase/types" +import { Component } from "@budibase/types" import { BudiStore, PersistenceType } from "@/stores/BudiStore" interface OpenNodesState { @@ -49,9 +49,9 @@ export class ComponentTreeNodesStore extends BudiStore { // Will ensure all parents of a node are expanded so that it is visible in the tree makeNodeVisible(componentId: string) { - const selectedScreen: Screen = get(selectedScreenStore)! + const selectedScreen = get(selectedScreenStore) - const path = findComponentPath(selectedScreen.props, componentId) + const path = findComponentPath(selectedScreen?.props, componentId) const componentIds = path.map((component: Component) => component._id) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 8fa7f5113d..d831d35ab6 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -610,14 +610,14 @@ export class ComponentStore extends BudiStore { async patch( patchFn: (component: Component, screen: Screen) => any, componentId?: string, - screenId?: string | null + screenId?: string ) { // Use selected component by default if (!componentId || !screenId) { const state = get(this.store) componentId = componentId ?? state.selectedComponentId ?? undefined const screenState = get(screenStore) - screenId = screenId || screenState.selectedScreenId + screenId = (screenId || screenState.selectedScreenId) ?? undefined } if (!componentId || !screenId || !patchFn) { return From f5e8ed6e37cf7afeb3378a4038d8c01bf7d35fc4 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 Jan 2025 09:50:02 +0100 Subject: [PATCH 13/29] Fix tests --- packages/builder/src/stores/builder/history.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/builder/src/stores/builder/history.ts b/packages/builder/src/stores/builder/history.ts index 916e417872..6568c5abca 100644 --- a/packages/builder/src/stores/builder/history.ts +++ b/packages/builder/src/stores/builder/history.ts @@ -9,7 +9,7 @@ export const enum Operations { } interface Operator { - id?: string + id?: number type: Operations doc: T forwardPatch?: jsonpatch.Operation[] @@ -22,7 +22,7 @@ interface HistoryState { loading?: boolean } -export const initialState: HistoryState = { +export const initialState = { history: [], position: 0, loading: false, @@ -37,10 +37,10 @@ export interface HistoryStore > { wrapSaveDoc: ( fn: (doc: T) => Promise - ) => (doc: T, operationId?: string) => Promise + ) => (doc: T, operationId?: number) => Promise wrapDeleteDoc: ( fn: (doc: T) => Promise - ) => (doc: T, operationId?: string) => Promise + ) => (doc: T, operationId?: number) => Promise reset: () => void undo: () => Promise @@ -70,8 +70,8 @@ export const createHistoryStore = ({ // Wrapped versions of essential functions which we call ourselves when using // undo and redo - let saveFn: (doc: T, operationId?: string) => Promise - let deleteFn: (doc: T, operationId?: string) => Promise + let saveFn: (doc: T, operationId?: number) => Promise + let deleteFn: (doc: T, operationId?: number) => Promise /** * Internal util to set the loading flag @@ -112,7 +112,7 @@ export const createHistoryStore = ({ let position = state.position if (!operation.id) { // Every time a new operation occurs we discard any redo potential - operation.id = Math.random().toString() + operation.id = Math.random() history = [...history.slice(0, state.position), operation] position += 1 } else { @@ -133,7 +133,7 @@ export const createHistoryStore = ({ * @returns {function} a wrapped version of the save function */ const wrapSaveDoc = (fn: (doc: T) => Promise) => { - saveFn = async (doc: T, operationId?: string) => { + saveFn = async (doc: T, operationId?: number) => { // Only works on a single doc at a time if (!doc || Array.isArray(doc)) { return @@ -181,7 +181,7 @@ export const createHistoryStore = ({ * @returns {function} a wrapped version of the delete function */ const wrapDeleteDoc = (fn: (doc: T) => Promise) => { - deleteFn = async (doc: T, operationId?: string) => { + deleteFn = async (doc: T, operationId?: number) => { // Only works on a single doc at a time if (!doc || Array.isArray(doc)) { return From 15ffe58a8164b5b5b8f1d70cedcd4cd6e7321922 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 Jan 2025 10:38:50 +0100 Subject: [PATCH 14/29] Fix undefined --- packages/builder/src/stores/builder/screens.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index d87d12f23c..a31c8fb728 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -10,7 +10,7 @@ import { navigationStore, selectedComponent, } from "@/stores/builder" -import { createHistoryStore } from "@/stores/builder/history" +import { createHistoryStore, HistoryStore } from "@/stores/builder/history" import { API } from "@/api" import { BudiStore } from "../BudiStore" import { Component, Screen } from "@budibase/types" @@ -27,6 +27,7 @@ export const INITIAL_SCREENS_STATE: ScreenState = { } export class ScreenStore extends BudiStore { + history: HistoryStore save: (doc: Screen) => Promise delete: (doc: Screen) => Promise @@ -47,7 +48,7 @@ export class ScreenStore extends BudiStore { this.sequentialScreenPatch = this.sequentialScreenPatch.bind(this) this.removeCustomLayout = this.removeCustomLayout.bind(this) - const history = createHistoryStore({ + this.history = createHistoryStore({ getDoc: id => get(this.store).screens?.find(screen => screen._id === id), selectDoc: this.select, afterAction: () => { @@ -61,8 +62,8 @@ export class ScreenStore extends BudiStore { }, }) - this.delete = history.wrapDeleteDoc(this.deleteScreen) - this.save = history.wrapSaveDoc(this.saveScreen) + this.delete = this.history.wrapDeleteDoc(this.deleteScreen) + this.save = this.history.wrapSaveDoc(this.saveScreen) } /** From dbefd82e9a30afe70dc4ad330adca3b8457a457f Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Fri, 24 Jan 2025 16:57:51 +0000 Subject: [PATCH 15/29] Some improvements to UX suggested by Andrew K. --- .../bindings/EvaluationSidePanel.svelte | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index c47840ea83..dbeb4152ca 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -19,7 +19,7 @@ $: success = !error && !empty $: highlightedResult = highlight(expressionResult) $: highlightedLogs = expressionLogs.map(l => ({ - log: highlight(l.log.join(", ")), + log: l.log.map(part => highlight(part)).join(", "), line: l.line, type: l.type, })) @@ -95,7 +95,9 @@ {#if empty} Your expression will be evaluated here {:else if error} - {formatError(expressionError)} +
+ {formatError(expressionError)} +
{:else}
{#each highlightedLogs as logLine} @@ -118,13 +120,15 @@ {@html logLine.log}
{#if logLine.line} - :{logLine.line} + :{logLine.line} {/if} {/each}
- - {@html highlightedResult} +
+ + {@html highlightedResult} +
{/if} @@ -169,29 +173,33 @@ .header.error::before { background: var(--error-bg); } + .error-msg { + padding-top: var(--spacing-m); + } .body { flex: 1 1 auto; - padding: var(--spacing-m) var(--spacing-l); + padding: 0 var(--spacing-l); font-family: var(--font-mono); font-size: 12px; overflow-y: auto; overflow-x: hidden; - white-space: pre-line; - word-wrap: break-word; + word-wrap: anywhere; height: 0; } .output-lines { display: flex; flex-direction: column; - gap: var(--spacing-xs); } .line { - border-bottom: var(--border-light); display: flex; flex-direction: row; justify-content: space-between; align-items: end; - padding: var(--spacing-s); + padding: var(--spacing-m); + word-wrap: anywhere; + } + .line:not(:first-of-type) { + border-top: var(--border-light); } .icon-log { display: flex; From 2ef2855febcbbba5de85422c75b5e7739921f5ff Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Fri, 24 Jan 2025 17:23:56 +0000 Subject: [PATCH 16/29] Prettier run. --- .../src/components/common/bindings/EvaluationSidePanel.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index dbeb4152ca..b11c2ca9ad 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -120,7 +120,9 @@ {@html logLine.log} {#if logLine.line} - :{logLine.line} + :{logLine.line} {/if} {/each} From 8842bfe3b400371c2395bd3de360fbfacffec3e2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 27 Jan 2025 15:50:36 +0100 Subject: [PATCH 17/29] Fix types --- .../src/stores/builder/componentTreeNodes.ts | 2 +- packages/builder/src/stores/builder/screens.ts | 7 +++++-- packages/frontend-core/src/utils/utils.ts | 14 ++++++++------ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/builder/src/stores/builder/componentTreeNodes.ts b/packages/builder/src/stores/builder/componentTreeNodes.ts index 2f34e79d01..17793c9672 100644 --- a/packages/builder/src/stores/builder/componentTreeNodes.ts +++ b/packages/builder/src/stores/builder/componentTreeNodes.ts @@ -1,7 +1,7 @@ import { get } from "svelte/store" import { selectedScreen as selectedScreenStore } from "./screens" import { findComponentPath } from "@/helpers/components" -import { Component } from "@budibase/types" +import { Component, Screen } from "@budibase/types" import { BudiStore, PersistenceType } from "@/stores/BudiStore" interface OpenNodesState { diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index 5163c6a3ea..04405feb70 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -35,7 +35,7 @@ export const initialScreenState: ScreenState = { export class ScreenStore extends BudiStore { history: any delete: any - save: any + save: (screen: Screen) => Promise constructor() { super(initialScreenState) @@ -281,7 +281,10 @@ export class ScreenStore extends BudiStore { * supports deeply mutating the current doc rather than just appending data. */ sequentialScreenPatch = Utils.sequential( - async (patchFn: (screen: Screen) => any, screenId: string) => { + async ( + patchFn: (screen: Screen) => boolean, + screenId: string + ): Promise => { const state = get(this.store) const screen = state.screens.find(screen => screen._id === screenId) if (!screen) { diff --git a/packages/frontend-core/src/utils/utils.ts b/packages/frontend-core/src/utils/utils.ts index 5f7f7063a7..b75ae02d07 100644 --- a/packages/frontend-core/src/utils/utils.ts +++ b/packages/frontend-core/src/utils/utils.ts @@ -17,12 +17,13 @@ export const sequential = < TFunction extends (...args: any[]) => Promise >( fn: TFunction -): ((...args: Parameters) => Promise) => { - let queue: any[] = [] - return (...params: Parameters) => { - return new Promise((resolve, reject) => { +): TFunction => { + let queue: (() => Promise)[] = [] + const result = (...params: Parameters) => { + return new Promise((resolve, reject) => { queue.push(async () => { - let data, error + let data: TReturn | undefined + let error: unknown try { data = await fn(...params) } catch (err) { @@ -35,7 +36,7 @@ export const sequential = < if (error) { reject(error) } else { - resolve(data) + resolve(data!) } }) if (queue.length === 1) { @@ -43,6 +44,7 @@ export const sequential = < } }) } + return result as TFunction } /** From 3f10a3404cf4a3305f04a4fa455844c5cec964aa Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 27 Jan 2025 16:00:44 +0100 Subject: [PATCH 18/29] Types --- packages/builder/src/stores/builder/screens.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index 04405feb70..646f598403 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -10,7 +10,7 @@ import { navigationStore, selectedComponent, } from "@/stores/builder" -import { createHistoryStore } from "@/stores/builder/history" +import { createHistoryStore, HistoryStore } from "@/stores/builder/history" import { API } from "@/api" import { BudiStore } from "../BudiStore" import { @@ -33,8 +33,8 @@ export const initialScreenState: ScreenState = { // Review the nulls export class ScreenStore extends BudiStore { - history: any - delete: any + history: HistoryStore + delete: (screens: Screen) => Promise save: (screen: Screen) => Promise constructor() { @@ -365,10 +365,10 @@ export class ScreenStore extends BudiStore { * Any deleted screens will then have their routes/links purged * * Wrapped by {@link delete} - * @param {Screen | Screen[]} screens + * @param {Screen } screens */ - async deleteScreen(screens: Screen | Screen[]) { - const screensToDelete = Array.isArray(screens) ? screens : [screens] + async deleteScreen(screen: Screen) { + const screensToDelete = [screen] // Build array of promises to speed up bulk deletions let promises: Promise[] = [] let deleteUrls: string[] = [] From d322f85acfb70499c44facda56fe5f83749faddb Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 27 Jan 2025 16:02:59 +0100 Subject: [PATCH 19/29] Clean code --- packages/builder/src/stores/builder/componentTreeNodes.ts | 2 +- packages/builder/src/stores/builder/components.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/stores/builder/componentTreeNodes.ts b/packages/builder/src/stores/builder/componentTreeNodes.ts index 17793c9672..b88563de6a 100644 --- a/packages/builder/src/stores/builder/componentTreeNodes.ts +++ b/packages/builder/src/stores/builder/componentTreeNodes.ts @@ -56,7 +56,7 @@ export class ComponentTreeNodesStore extends BudiStore { return {} } - const path = findComponentPath(selectedScreen?.props, componentId) + const path = findComponentPath(selectedScreen.props, componentId) const componentIds = path.map((component: Component) => component._id) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 1944096f5b..46d3e07eae 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -483,9 +483,8 @@ export class ComponentStore extends BudiStore { extras._children = [] } - const $selectedScreen = get(selectedScreen) // Add step name to form steps - if (componentName.endsWith("/formstep") && $selectedScreen) { + if (componentName.endsWith("/formstep")) { const parentForm = findClosestMatchingComponent( screen.props, get(selectedComponent)._id, @@ -621,7 +620,7 @@ export class ComponentStore extends BudiStore { const state = get(this.store) componentId = componentId ?? state.selectedComponentId ?? undefined const screenState = get(screenStore) - screenId = (screenId || screenState.selectedScreenId) ?? undefined + screenId = screenId || screenState.selectedScreenId } if (!componentId || !screenId || !patchFn) { return From 8ca5cb559921c13167ce63fdb454884be7c8210a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 28 Jan 2025 17:43:03 +0000 Subject: [PATCH 20/29] Do a typing pass on automation.spec.ts --- .../src/api/routes/tests/automation.spec.ts | 440 +++++++++--------- .../routes/tests/utilities/TestFunctions.ts | 9 - .../src/tests/utilities/TestConfiguration.ts | 6 +- .../src/tests/utilities/api/automation.ts | 91 +++- .../server/src/tests/utilities/api/index.ts | 48 +- .../server/src/tests/utilities/structures.ts | 64 +-- packages/types/src/api/web/app/automation.ts | 1 + 7 files changed, 363 insertions(+), 296 deletions(-) diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index 5c0b86d9a0..94517db67a 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -1,7 +1,6 @@ import { checkBuilderEndpoint, getAllTableRows, - clearAllAutomations, testAutomation, } from "./utilities/TestFunctions" import * as setup from "./utilities" @@ -12,9 +11,9 @@ import { import { configs, context, events } from "@budibase/backend-core" import sdk from "../../../sdk" import { - Automation, ConfigType, FieldType, + isDidNotTriggerResponse, SettingsConfig, Table, } from "@budibase/types" @@ -22,11 +21,13 @@ import { mocks } from "@budibase/backend-core/tests" import { removeDeprecated } from "../../../automations/utils" import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder" 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 MAX_RETRIES = 4 -let { +const { basicAutomation, newAutomation, automationTrigger, @@ -37,10 +38,11 @@ let { } = setup.structures describe("/automations", () => { - let request = setup.getRequest() - let config = setup.getConfig() + const config = new TestConfiguration() - afterAll(setup.afterAll) + afterAll(() => { + config.end() + }) beforeAll(async () => { await config.init() @@ -52,40 +54,26 @@ describe("/automations", () => { describe("get definitions", () => { it("returns a list of definitions for actions", async () => { - const res = await request - .get(`/api/automations/action/list`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - - expect(Object.keys(res.body).length).not.toEqual(0) + const res = await config.api.automation.getActions() + expect(Object.keys(res).length).not.toEqual(0) }) it("returns a list of definitions for triggerInfo", async () => { - const res = await request - .get(`/api/automations/trigger/list`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - - expect(Object.keys(res.body).length).not.toEqual(0) + const res = await config.api.automation.getTriggers() + expect(Object.keys(res).length).not.toEqual(0) }) it("returns all of the definitions in one", async () => { - const res = await request - .get(`/api/automations/definitions/list`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const { action, trigger } = await config.api.automation.getDefinitions() let definitionsLength = Object.keys( removeDeprecated(BUILTIN_ACTION_DEFINITIONS) ).length - expect(Object.keys(res.body.action).length).toBeGreaterThanOrEqual( + expect(Object.keys(action).length).toBeGreaterThanOrEqual( definitionsLength ) - expect(Object.keys(res.body.trigger).length).toEqual( + expect(Object.keys(trigger).length).toEqual( Object.keys(removeDeprecated(TRIGGER_DEFINITIONS)).length ) }) @@ -93,38 +81,27 @@ describe("/automations", () => { describe("create", () => { it("creates an automation with no steps", async () => { - const automation = newAutomation() - automation.definition.steps = [] + const { message, automation } = await config.api.automation.post( + newAutomation({ steps: [] }) + ) - const res = await request - .post(`/api/automations`) - .set(config.defaultHeaders()) - .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(message).toEqual("Automation created successfully") + expect(automation.name).toEqual("My Automation") + expect(automation._id).not.toEqual(null) expect(events.automation.created).toHaveBeenCalledTimes(1) expect(events.automation.stepCreated).not.toHaveBeenCalled() }) it("creates an automation with steps", async () => { - const automation = newAutomation() - automation.definition.steps.push(automationStep()) jest.clearAllMocks() - const res = await request - .post(`/api/automations`) - .set(config.defaultHeaders()) - .send(automation) - .expect("Content-Type", /json/) - .expect(200) + const { message, automation } = await config.api.automation.post( + newAutomation({ steps: [automationStep(), automationStep()] }) + ) - 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(message).toEqual("Automation created successfully") + expect(automation.name).toEqual("My Automation") + expect(automation._id).not.toEqual(null) expect(events.automation.created).toHaveBeenCalledTimes(1) expect(events.automation.stepCreated).toHaveBeenCalledTimes(2) }) @@ -241,13 +218,9 @@ describe("/automations", () => { describe("find", () => { it("should be able to find the automation", async () => { const automation = await config.createAutomation() - const res = await request - .get(`/api/automations/${automation._id}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body._id).toEqual(automation._id) - expect(res.body._rev).toEqual(automation._rev) + const { _id, _rev } = await config.api.automation.get(automation._id!) + expect(_id).toEqual(automation._id) + expect(_rev).toEqual(automation._rev) }) }) @@ -348,106 +321,104 @@ describe("/automations", () => { describe("trigger", () => { it("does not trigger an automation when not synchronous and in dev", async () => { - let automation = newAutomation() - automation = await config.createAutomation(automation) - const res = await request - .post(`/api/automations/${automation._id}/trigger`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(400) - - expect(res.body.message).toEqual( - "Only apps in production support this endpoint" + const { automation } = await config.api.automation.post(newAutomation()) + await config.api.automation.trigger( + automation._id!, + { + fields: {}, + timeout: 1000, + }, + { + status: 400, + body: { + message: "Only apps in production support this endpoint", + }, + } ) }) it("triggers a synchronous automation", async () => { mocks.licenses.useSyncAutomations() - let automation = collectAutomation() - automation = await config.createAutomation(automation) - const res = await request - .post(`/api/automations/${automation._id}/trigger`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - - expect(res.body.success).toEqual(true) - expect(res.body.value).toEqual([1, 2, 3]) + const { automation } = await config.api.automation.post( + collectAutomation() + ) + await config.api.automation.trigger( + automation._id!, + { + fields: {}, + timeout: 1000, + }, + { + status: 200, + body: { + success: true, + value: [1, 2, 3], + }, + } + ) }) it("should throw an error when attempting to trigger a disabled automation", async () => { mocks.licenses.useSyncAutomations() - let automation = collectAutomation() - automation = await config.createAutomation({ - ...automation, - disabled: true, - }) + const { automation } = await config.api.automation.post( + collectAutomation({ disabled: true }) + ) - const res = await request - .post(`/api/automations/${automation._id}/trigger`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(400) - - expect(res.body.message).toEqual("Automation is disabled") + await config.api.automation.trigger( + automation._id!, + { + fields: {}, + timeout: 1000, + }, + { + status: 400, + body: { + message: "Automation is disabled", + }, + } + ) }) it("triggers an asynchronous automation", async () => { - let automation = newAutomation() - automation = await config.createAutomation(automation) + const { automation } = await config.api.automation.post(newAutomation()) await config.publish() - const res = await request - .post(`/api/automations/${automation._id}/trigger`) - .set(config.defaultHeaders({}, true)) - .expect("Content-Type", /json/) - .expect(200) - - expect(res.body.message).toEqual( - `Automation ${automation._id} has been triggered.` + await config.withProdApp(() => + config.api.automation.trigger( + automation._id!, + { + fields: {}, + timeout: 1000, + }, + { + status: 200, + body: { + message: `Automation ${automation._id} has been triggered.`, + }, + } + ) ) }) }) 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 () => { - const automation = await config.createAutomation(newAutomation()) + const { automation } = await config.api.automation.post(basicAutomation()) automation.name = "Updated Name" jest.clearAllMocks() - const res = await update(automation) + const { automation: updatedAutomation, message } = + await config.api.automation.update(automation) - const automationRes = res.body.automation - const message = res.body.message + expect(updatedAutomation._id).toEqual(automation._id) + expect(updatedAutomation._rev).toBeDefined() + expect(updatedAutomation._rev).not.toEqual(automation._rev) - // doc attributes - 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(updatedAutomation.name).toEqual("Updated Name") expect(message).toEqual( `Automation ${automation._id} updated successfully.` ) - // events + expect(events.automation.created).not.toHaveBeenCalled() expect(events.automation.stepCreated).not.toHaveBeenCalled() expect(events.automation.stepDeleted).not.toHaveBeenCalled() @@ -455,26 +426,23 @@ describe("/automations", () => { }) 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" jest.clearAllMocks() - // the POST request will defer to the update - // when an id has been supplied. - const res = await updateWithPost(automation) + // the POST request will defer to the update when an id has been supplied. + const { automation: updatedAutomation, message } = + await config.api.automation.post(automation) - const automationRes = res.body.automation - const message = res.body.message - // doc attributes - 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(updatedAutomation._id).toEqual(automation._id) + expect(updatedAutomation._rev).toBeDefined() + expect(updatedAutomation._rev).not.toEqual(automation._rev) + + expect(updatedAutomation.name).toEqual("Updated Name") expect(message).toEqual( `Automation ${automation._id} updated successfully.` ) - // events + expect(events.automation.created).not.toHaveBeenCalled() expect(events.automation.stepCreated).not.toHaveBeenCalled() expect(events.automation.stepDeleted).not.toHaveBeenCalled() @@ -482,16 +450,14 @@ describe("/automations", () => { }) it("updates an automation trigger", async () => { - let automation = newAutomation() - automation = await config.createAutomation(automation) + const { automation } = await config.api.automation.post(newAutomation()) automation.definition.trigger = automationTrigger( TRIGGER_DEFINITIONS.WEBHOOK ) jest.clearAllMocks() - await update(automation) + await config.api.automation.update(automation) - // events expect(events.automation.created).not.toHaveBeenCalled() expect(events.automation.stepCreated).not.toHaveBeenCalled() expect(events.automation.stepDeleted).not.toHaveBeenCalled() @@ -499,16 +465,13 @@ describe("/automations", () => { }) it("adds automation steps", async () => { - let automation = newAutomation() - automation = await config.createAutomation(automation) + const { automation } = await config.api.automation.post(newAutomation()) automation.definition.steps.push(automationStep()) automation.definition.steps.push(automationStep()) jest.clearAllMocks() - // check the post request honours updates with same id - await update(automation) + await config.api.automation.update(automation) - // events expect(events.automation.stepCreated).toHaveBeenCalledTimes(2) expect(events.automation.created).not.toHaveBeenCalled() expect(events.automation.stepDeleted).not.toHaveBeenCalled() @@ -516,32 +479,25 @@ describe("/automations", () => { }) it("removes automation steps", async () => { - let automation = newAutomation() - automation.definition.steps.push(automationStep()) - automation = await config.createAutomation(automation) + const { automation } = await config.api.automation.post(newAutomation()) automation.definition.steps = [] jest.clearAllMocks() - // check the post request honours updates with same id - await update(automation) + await config.api.automation.update(automation) - // events - expect(events.automation.stepDeleted).toHaveBeenCalledTimes(2) + expect(events.automation.stepDeleted).toHaveBeenCalledTimes(1) expect(events.automation.stepCreated).not.toHaveBeenCalled() expect(events.automation.created).not.toHaveBeenCalled() expect(events.automation.triggerUpdated).not.toHaveBeenCalled() }) it("adds and removes automation steps", async () => { - let automation = newAutomation() - automation = await config.createAutomation(automation) + const { automation } = await config.api.automation.post(newAutomation()) automation.definition.steps = [automationStep(), automationStep()] jest.clearAllMocks() - // check the post request honours updates with same id - await update(automation) + await config.api.automation.update(automation) - // events expect(events.automation.stepCreated).toHaveBeenCalledTimes(2) expect(events.automation.stepDeleted).toHaveBeenCalledTimes(1) expect(events.automation.created).not.toHaveBeenCalled() @@ -551,16 +507,24 @@ describe("/automations", () => { describe("fetch", () => { it("return all the automations for an instance", async () => { - await clearAllAutomations(config) - const autoConfig = await config.createAutomation(basicAutomation()) - const res = await request - .get(`/api/automations`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const fetchResponse = await config.api.automation.fetch() + for (const auto of fetchResponse.automations) { + await config.api.automation.delete(auto) + } - expect(res.body.automations[0]).toEqual( - expect.objectContaining(autoConfig) + const { automation: automation1 } = await config.api.automation.post( + 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", () => { it("deletes a automation by its ID", async () => { - const automation = await config.createAutomation() - const res = await request - .delete(`/api/automations/${automation._id}/${automation._rev}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const { automation } = await config.api.automation.post(newAutomation()) + const { id } = await config.api.automation.delete(automation) - expect(res.body.id).toEqual(automation._id) + expect(id).toEqual(automation._id) expect(events.automation.deleted).toHaveBeenCalledTimes(1) }) it("cannot delete a row action automation", async () => { - const automation = await config.createAutomation( + const { automation } = await config.api.automation.post( setup.structures.rowActionAutomation() ) - await request - .delete(`/api/automations/${automation._id}/${automation._rev}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(422, { + + await config.api.automation.delete(automation, { + status: 422, + body: { message: "Row actions automations cannot be deleted", status: 422, - }) + }, + }) expect(events.automation.deleted).not.toHaveBeenCalled() }) @@ -614,10 +574,19 @@ describe("/automations", () => { describe("checkForCollectStep", () => { it("should return true if a collect step exists in an automation", async () => { - let automation = collectAutomation() - await config.createAutomation(automation) - let res = await sdk.automations.utils.checkForCollectStep(automation) - expect(res).toEqual(true) + const { automation } = await config.api.automation.post( + collectAutomation() + ) + 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'", 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()) - automation.definition.trigger.inputs.tableId = table._id - automation.definition.steps[0].inputs = { - condition: FilterConditions.EQUAL, - field: "{{ trigger.row.City }}", - value: "{{ trigger.oldRow.City }}", - } - automation = await config.createAutomation(automation) - let triggerInputs = { + const res = await config.api.automation.test(automation._id!, { + fields: {}, oldRow: { City: oldCity, }, row: { 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 beforeAll(async () => { - table = await config.createTable({ - name: "table", - type: "table", - schema: { - Approved: { - name: "Approved", - type: FieldType.BOOLEAN, + table = await config.api.table.save( + basicTable(undefined, { + name: "table", + type: "table", + schema: { + Approved: { + name: "Approved", + type: FieldType.BOOLEAN, + }, }, - }, - }) + }) + ) }) const testCases = [ @@ -712,33 +700,29 @@ describe("/automations", () => { it.each(testCases)( "$description", async ({ filters, row, oldRow, expectToRun }) => { - let automation = await updateRowAutomationWithFilters( - config.getAppId(), - table._id! - ) - automation.definition.trigger.inputs = { + let req = updateRowAutomationWithFilters(config.getAppId(), table._id!) + req.definition.trigger.inputs = { tableId: table._id, filters, } - automation = await config.createAutomation(automation) - const inputs = { - row: { - tableId: table._id, - ...row, - }, + const { automation } = await config.api.automation.post(req) + const res = await config.api.automation.test(automation._id!, { + fields: {}, oldRow: { tableId: table._id, ...oldRow, }, - } + row: { + tableId: table._id, + ...row, + }, + }) - const res = await testAutomation(config, automation, inputs) - - if (expectToRun) { - expect(res.body.steps[1].outputs.success).toEqual(true) + if (isDidNotTriggerResponse(res)) { + expect(expectToRun).toEqual(false) } else { - expect(res.body.outputs.success).toEqual(false) + expect(res.steps[1].outputs.success).toEqual(expectToRun) } } ) diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts index 9d5417d041..a232fec859 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts @@ -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 () => { const couchInfo = db.getCouchInfo() const nano = Nano({ diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 2d36e7855b..1f464b2ea4 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -258,7 +258,7 @@ export default class TestConfiguration { } } - async withApp(app: App | string, f: () => Promise) { + async withApp(app: App | string, f: () => Promise) { const oldAppId = this.appId this.appId = typeof app === "string" ? app : app.appId try { @@ -268,6 +268,10 @@ export default class TestConfiguration { } } + async withProdApp(f: () => Promise) { + return await this.withApp(this.getProdAppId(), f) + } + // UTILS _req | void, Res>( diff --git a/packages/server/src/tests/utilities/api/automation.ts b/packages/server/src/tests/utilities/api/automation.ts index 3f51385251..c4438560ae 100644 --- a/packages/server/src/tests/utilities/api/automation.ts +++ b/packages/server/src/tests/utilities/api/automation.ts @@ -1,8 +1,17 @@ import { Automation, + CreateAutomationResponse, + DeleteAutomationResponse, FetchAutomationResponse, + GetAutomationActionDefinitionsResponse, + GetAutomationStepDefinitionsResponse, + GetAutomationTriggerDefinitionsResponse, TestAutomationRequest, TestAutomationResponse, + TriggerAutomationRequest, + TriggerAutomationResponse, + UpdateAutomationRequest, + UpdateAutomationResponse, } from "@budibase/types" import { Expectations, TestAPI } from "./base" @@ -20,6 +29,39 @@ export class AutomationAPI extends TestAPI { return result } + getActions = async ( + expectations?: Expectations + ): Promise => { + return await this._get( + `/api/automations/actions/list`, + { + expectations, + } + ) + } + + getTriggers = async ( + expectations?: Expectations + ): Promise => { + return await this._get( + `/api/automations/triggers/list`, + { + expectations, + } + ) + } + + getDefinitions = async ( + expectations?: Expectations + ): Promise => { + return await this._get( + `/api/automations/definitions/list`, + { + expectations, + } + ) + } + fetch = async ( expectations?: Expectations ): Promise => { @@ -31,11 +73,14 @@ export class AutomationAPI extends TestAPI { post = async ( body: Automation, expectations?: Expectations - ): Promise => { - const result = await this._post(`/api/automations`, { - body, - expectations, - }) + ): Promise => { + const result = await this._post( + `/api/automations`, + { + body, + expectations, + } + ) return result } @@ -52,4 +97,40 @@ export class AutomationAPI extends TestAPI { } ) } + + trigger = async ( + id: string, + body: TriggerAutomationRequest, + expectations?: Expectations + ): Promise => { + return await this._post( + `/api/automations/${id}/trigger`, + { + expectations, + body, + } + ) + } + + update = async ( + body: UpdateAutomationRequest, + expectations?: Expectations + ): Promise => { + return await this._put(`/api/automations`, { + body, + expectations, + }) + } + + delete = async ( + automation: Automation, + expectations?: Expectations + ): Promise => { + return await this._delete( + `/api/automations/${automation._id!}/${automation._rev!}`, + { + expectations, + } + ) + } } diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index c5eede18d6..2fdf726b6c 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -19,43 +19,43 @@ import { PluginAPI } from "./plugin" import { WebhookAPI } from "./webhook" export default class API { - table: TableAPI - legacyView: LegacyViewAPI - viewV2: ViewV2API - row: RowAPI - permission: PermissionAPI - datasource: DatasourceAPI - screen: ScreenAPI application: ApplicationAPI - backup: BackupAPI attachment: AttachmentAPI - user: UserAPI + automation: AutomationAPI + backup: BackupAPI + datasource: DatasourceAPI + legacyView: LegacyViewAPI + permission: PermissionAPI + plugin: PluginAPI query: QueryAPI roles: RoleAPI - templates: TemplateAPI + row: RowAPI rowAction: RowActionAPI - automation: AutomationAPI - plugin: PluginAPI + screen: ScreenAPI + table: TableAPI + templates: TemplateAPI + user: UserAPI + viewV2: ViewV2API webhook: WebhookAPI 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.backup = new BackupAPI(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.roles = new RoleAPI(config) - this.templates = new TemplateAPI(config) + this.row = new RowAPI(config) this.rowAction = new RowActionAPI(config) - this.automation = new AutomationAPI(config) - this.plugin = new PluginAPI(config) + this.screen = new ScreenAPI(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) } } diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 3058a706c1..0c74a0faa2 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -34,6 +34,7 @@ import { Webhook, WebhookActionType, BuiltinPermissionID, + DeepPartial, } from "@budibase/types" import { LoopInput } from "../../definitions/automations" import { merge } from "lodash" @@ -184,21 +185,12 @@ export function newAutomation({ steps, trigger, }: { steps?: AutomationStep[]; trigger?: AutomationTrigger } = {}) { - const automation = basicAutomation() - - if (trigger) { - automation.definition.trigger = trigger - } else { - automation.definition.trigger = automationTrigger() - } - - if (steps) { - automation.definition.steps = steps - } else { - automation.definition.steps = [automationStep()] - } - - return automation + return basicAutomation({ + definition: { + steps: steps || [automationStep()], + trigger: trigger || automationTrigger(), + }, + }) } export function rowActionAutomation() { @@ -211,8 +203,8 @@ export function rowActionAutomation() { return automation } -export function basicAutomation(appId?: string): Automation { - return { +export function basicAutomation(opts?: DeepPartial): Automation { + const baseAutomation: Automation = { name: "My Automation", screenId: "kasdkfldsafkl", live: true, @@ -241,8 +233,9 @@ export function basicAutomation(appId?: string): Automation { steps: [], }, type: "automation", - appId: appId!, + appId: "appId", } + return merge(baseAutomation, opts) } export function basicCronAutomation(appId: string, cron: string): Automation { @@ -387,16 +380,21 @@ export function loopAutomation( return automation as Automation } -export function collectAutomation(tableId?: string): Automation { - const automation: any = { +export function collectAutomation(opts?: DeepPartial): Automation { + const baseAutomation: Automation = { + appId: "appId", name: "looping", type: "automation", definition: { steps: [ { id: "b", - type: "ACTION", + name: "b", + tagline: "An automation action step", + icon: "Icon", + type: AutomationStepType.ACTION, internal: true, + description: "Execute script", stepId: AutomationActionStepId.EXECUTE_SCRIPT, inputs: { code: "return [1,2,3]", @@ -405,8 +403,12 @@ export function collectAutomation(tableId?: string): Automation { }, { id: "c", - type: "ACTION", + name: "c", + type: AutomationStepType.ACTION, + tagline: "An automation action step", + icon: "Icon", internal: true, + description: "Collect", stepId: AutomationActionStepId.COLLECT, inputs: { collection: "{{ literal steps.1.value }}", @@ -416,24 +418,28 @@ export function collectAutomation(tableId?: string): Automation { ], trigger: { id: "a", - type: "TRIGGER", + type: AutomationStepType.TRIGGER, event: AutomationEventType.ROW_SAVE, stepId: AutomationTriggerStepId.ROW_SAVED, + name: "trigger Step", + tagline: "An automation trigger", + description: "A trigger", + icon: "Icon", inputs: { - tableId, + tableId: "tableId", }, 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 { const automation: Automation = { name: "looping", type: "automation", - appId, + appId: "appId", definition: { steps: [ { @@ -459,13 +465,13 @@ export function filterAutomation(appId: string, tableId?: string): Automation { event: AutomationEventType.ROW_SAVE, stepId: AutomationTriggerStepId.ROW_SAVED, inputs: { - tableId: tableId!, + tableId: "tableId", }, schema: TRIGGER_DEFINITIONS.ROW_SAVED.schema, }, }, } - return automation + return merge(automation, opts) } export function updateRowAutomationWithFilters( diff --git a/packages/types/src/api/web/app/automation.ts b/packages/types/src/api/web/app/automation.ts index b97dee0baf..f72966d100 100644 --- a/packages/types/src/api/web/app/automation.ts +++ b/packages/types/src/api/web/app/automation.ts @@ -75,6 +75,7 @@ export interface TestAutomationRequest { revision?: string fields: Record row?: Row + oldRow?: Row } export type TestAutomationResponse = AutomationResults | DidNotTriggerResponse From 9d5f501d2a9c0e38ffc08b56685d004ca6d1e30d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 28 Jan 2025 15:50:35 +0100 Subject: [PATCH 21/29] Remove any --- packages/builder/src/stores/builder/components.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 46d3e07eae..d506d508e7 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -254,7 +254,10 @@ export class ComponentStore extends BudiStore { * @param {object} opts * @returns */ - enrichEmptySettings(component: Component, opts: any) { + enrichEmptySettings( + component: Component, + opts: { screen?: Screen; parent?: Component; useDefaultValues?: boolean } + ) { if (!component?._component) { return } @@ -1339,7 +1342,7 @@ export const componentStore = new ComponentStore() export const selectedComponent = derived( [componentStore, selectedScreen], - ([$store, $selectedScreen]) => { + ([$store, $selectedScreen]): Component | null => { if ( $selectedScreen && $store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`) From 12ab5637c1cea9c2c5f4b392961153462a391b9d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 29 Jan 2025 10:54:20 +0100 Subject: [PATCH 22/29] Fix selecting screen on undo --- packages/builder/src/stores/builder/automations.ts | 2 -- packages/builder/src/stores/builder/screens.ts | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/stores/builder/automations.ts b/packages/builder/src/stores/builder/automations.ts index 9b20b4cd03..98213581a6 100644 --- a/packages/builder/src/stores/builder/automations.ts +++ b/packages/builder/src/stores/builder/automations.ts @@ -1437,8 +1437,6 @@ class AutomationStore extends BudiStore { this.history = createHistoryStore({ getDoc: this.actions.getDefinition.bind(this), selectDoc: this.actions.select.bind(this), - beforeAction: () => {}, - afterAction: () => {}, }) // Then wrap save and delete with history diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index 5163c6a3ea..a1b51875b3 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -58,13 +58,12 @@ export class ScreenStore extends BudiStore { getDoc: (id: string) => get(this.store).screens?.find(screen => screen._id === id), selectDoc: this.select, - beforeAction: () => {}, afterAction: () => { // Ensure a valid component is selected if (!get(selectedComponent)) { - this.update(state => ({ + componentStore.update(state => ({ ...state, - selectedComponentId: get(selectedScreen)?.props._id, + selectedComponentId: get(selectedScreen)?._id, })) } }, From e34812719bb716b8748014729cb909056dff9576 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 29 Jan 2025 11:03:38 +0100 Subject: [PATCH 23/29] Fix anys and undefineds --- .../builder/src/stores/builder/components.ts | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index d506d508e7..30370f1c32 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -30,9 +30,18 @@ import { } from "@/constants/backend" import { BudiStore } from "../BudiStore" 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" +interface Component extends ComponentType { + _id: string +} + export interface ComponentState { components: Record customComponents: string[] @@ -442,7 +451,11 @@ export class ComponentStore extends BudiStore { * @param {object} parent * @returns */ - createInstance(componentName: string, presetProps: any, parent: any) { + createInstance( + componentName: string, + presetProps: any, + parent: any + ): Component | null { const screen = get(selectedScreen) if (!screen) { throw "A valid screen must be selected" @@ -454,7 +467,7 @@ export class ComponentStore extends BudiStore { } // Generate basic component structure - let instance = { + let instance: Component = { _id: Helpers.uuid(), _component: definition.component, _styles: { @@ -481,7 +494,7 @@ export class ComponentStore extends BudiStore { } // Custom post processing for creation only - let extras: any = {} + let extras: Partial = {} if (definition.hasChildren) { extras._children = [] } @@ -490,7 +503,7 @@ export class ComponentStore extends BudiStore { if (componentName.endsWith("/formstep")) { const parentForm = findClosestMatchingComponent( screen.props, - get(selectedComponent)._id, + get(selectedComponent)?._id, (component: Component) => component._component.endsWith("/form") ) const formSteps = findAllMatchingComponents( @@ -775,7 +788,7 @@ export class ComponentStore extends BudiStore { if (!cut) { componentToPaste = makeComponentUnique(componentToPaste) } - newComponentId = componentToPaste._id! + newComponentId = componentToPaste._id // Strip grid position metadata if pasting into a new screen, but keep // alignment metadata @@ -918,7 +931,7 @@ export class ComponentStore extends BudiStore { // If we have children, select first child, and the node is not collapsed if ( - component._children?.length && + component?._children?.length && (state.selectedComponentId === navComponentId || componentTreeNodesStore.isNodeExpanded(component._id)) ) { @@ -1347,7 +1360,10 @@ export const selectedComponent = derived( $selectedScreen && $store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`) ) { - return $selectedScreen?.props + return { + ...$selectedScreen.props, + _id: $selectedScreen.props._id!, + } } if (!$selectedScreen || !$store.selectedComponentId) { return null From 53f0c3ce92962315c83a215a06e8ae7d16ec6a81 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 29 Jan 2025 11:19:28 +0100 Subject: [PATCH 24/29] Fix types --- packages/builder/src/stores/builder/history.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/builder/history.js b/packages/builder/src/stores/builder/history.js index 62a8ed2f97..50d68bd543 100644 --- a/packages/builder/src/stores/builder/history.js +++ b/packages/builder/src/stores/builder/history.js @@ -16,8 +16,8 @@ export const initialState = { export const createHistoryStore = ({ getDoc, selectDoc, - beforeAction, - afterAction, + beforeAction = () => {}, + afterAction = () => {}, }) => { // Use a derived store to check if we are able to undo or redo any operations const store = writable(initialState) From ba448c31642a2d3c7809b9ca4b29c944bece0e73 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 29 Jan 2025 11:24:20 +0100 Subject: [PATCH 25/29] Fix types --- packages/builder/src/stores/builder/components.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 30370f1c32..90e1abfecf 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -376,7 +376,7 @@ export class ComponentStore extends BudiStore { getSchemaForDatasource(screen, 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)) { fieldTypes = [fieldTypes] } @@ -531,7 +531,7 @@ export class ComponentStore extends BudiStore { async create( componentName: string, presetProps: any, - parent: any, + parent: Component, index: number ) { const state = get(this.store) From ca556cf16611710413a28cce3bd2bf9e11d498f5 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 29 Jan 2025 12:56:28 +0000 Subject: [PATCH 26/29] PR comments. --- .../src/components/common/bindings/EvaluationSidePanel.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index b11c2ca9ad..1e951b00cb 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -180,8 +180,8 @@ } .body { flex: 1 1 auto; - padding: 0 var(--spacing-l); font-family: var(--font-mono); + margin: 0 var(--spacing-m); font-size: 12px; overflow-y: auto; overflow-x: hidden; @@ -197,7 +197,7 @@ flex-direction: row; justify-content: space-between; align-items: end; - padding: var(--spacing-m); + padding: var(--spacing-m) 0; word-wrap: anywhere; } .line:not(:first-of-type) { From 7f55db4bf5d820b8e42eb9e55f1fb6a677265b6e Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 29 Jan 2025 16:48:12 +0000 Subject: [PATCH 27/29] force apt update before certbot install --- hosting/single/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index e4858d4af0..ae6c55d9de 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -86,7 +86,7 @@ COPY hosting/single/ssh/sshd_config /etc/ COPY hosting/single/ssh/ssh_setup.sh /tmp # setup letsencrypt certificate -RUN apt-get install -y certbot python3-certbot-nginx +RUN apt-get update && apt-get install -y certbot python3-certbot-nginx COPY hosting/letsencrypt /app/letsencrypt RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh From df4cb7b35eb08621393e01877aa0a9c5557ec015 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 29 Jan 2025 16:54:37 +0000 Subject: [PATCH 28/29] under apt-get update --- hosting/single/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index ae6c55d9de..e4858d4af0 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -86,7 +86,7 @@ COPY hosting/single/ssh/sshd_config /etc/ COPY hosting/single/ssh/ssh_setup.sh /tmp # setup letsencrypt certificate -RUN apt-get update && apt-get install -y certbot python3-certbot-nginx +RUN apt-get install -y certbot python3-certbot-nginx COPY hosting/letsencrypt /app/letsencrypt RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh From 735b8206ce737706b062327bb601f0b5084895ba Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 12:39:25 +0100 Subject: [PATCH 29/29] Types --- packages/frontend-core/src/utils/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/utils/utils.ts b/packages/frontend-core/src/utils/utils.ts index b75ae02d07..85cc5de54b 100644 --- a/packages/frontend-core/src/utils/utils.ts +++ b/packages/frontend-core/src/utils/utils.ts @@ -55,7 +55,7 @@ export const sequential = < * @returns a debounced version of the callback */ export const debounce = (callback: Function, minDelay = 1000) => { - let timeout: NodeJS.Timeout + let timeout: ReturnType return async (...params: any[]) => { return new Promise(resolve => { if (timeout) {