From a52cdbe54dd1208f3ead446f28a5987e3831bd6f Mon Sep 17 00:00:00 2001 From: mikesealey Date: Tue, 19 Nov 2024 17:18:11 +0000 Subject: [PATCH 001/321] allows additional CSV delimiters --- .../server/src/api/routes/tests/table.spec.ts | 15 +++++++++++++++ packages/server/src/utilities/csv.ts | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/table.spec.ts b/packages/server/src/api/routes/tests/table.spec.ts index b9d8696714..692a3c4b91 100644 --- a/packages/server/src/api/routes/tests/table.spec.ts +++ b/packages/server/src/api/routes/tests/table.spec.ts @@ -1511,5 +1511,20 @@ datasourceDescribe( }) }) }) + describe("csvToJson", () => { + const delimiters = [",", ";", ":", "|", "~", "\t", " "] + it.only.each(delimiters)( + "can parse CSVs with delimiter; %s", + async delimiter => { + const rows = await config.api.table.csvToJson({ + csvString: [ + ["a", "b", "c", "d", "e"].join(delimiter), + ["1", "2", "3", "4", "5"].join(delimiter), + ].join("\n"), + }) + expect(rows).toEqual([{ a: "1", b: "2", c: "3", d: "4", e: "5" }]) + } + ) + }) } ) diff --git a/packages/server/src/utilities/csv.ts b/packages/server/src/utilities/csv.ts index 43d712165a..4ecdbba37e 100644 --- a/packages/server/src/utilities/csv.ts +++ b/packages/server/src/utilities/csv.ts @@ -9,7 +9,10 @@ export async function jsonFromCsvString(csvString: string) { // is causing issues on conversion. ignoreEmpty will remove the key completly // if empty, so creating this empty object will ensure we return the values // with the keys but empty values - const result = await csv({ ignoreEmpty: false }).fromString(csvString) + const result = await csv({ + ignoreEmpty: false, + delimiter: [",", ";", ":", "|", "~", "\t", " "], + }).fromString(csvString) result.forEach((r, i) => { for (const [key] of Object.entries(r).filter(([, value]) => value === "")) { if (castedWithEmptyValues[i][key] === undefined) { From 401bfa812cd9d464efe385e81d0bb02ec6bf7e97 Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 6 Jan 2025 11:45:53 +0000 Subject: [PATCH 002/321] Screen store to ts and test updates. Minor changes required for component store --- .../builder/src/stores/builder/components.ts | 39 ++- packages/builder/src/stores/builder/index.js | 2 +- .../stores/builder/{screens.js => screens.ts} | 169 ++++++---- .../src/stores/builder/tests/screens.test.js | 299 +++++++++--------- packages/frontend-core/src/utils/utils.js | 2 +- 5 files changed, 300 insertions(+), 211 deletions(-) rename packages/builder/src/stores/builder/{screens.js => screens.ts} (77%) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 9ad9a75f84..bce7fcb71d 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -33,7 +33,16 @@ import { Utils } from "@budibase/frontend-core" import { Component, FieldType, Screen, Table } from "@budibase/types" import { utils } from "@budibase/shared-core" -interface ComponentDefinition { +export interface ComponentState { + components: Record + customComponents: string[] + selectedComponentId?: string | null + componentToPaste?: Component | null + settingsCache: Record + selectedScreenId?: string | null +} + +export interface ComponentDefinition { component: string name: string friendlyName?: string @@ -41,9 +50,11 @@ interface ComponentDefinition { settings?: ComponentSetting[] features?: Record typeSupportPresets?: Record + legalDirectChildren: string[] + illegalChildren: string[] } -interface ComponentSetting { +export interface ComponentSetting { key: string type: string section?: string @@ -54,15 +65,6 @@ interface ComponentSetting { settings?: ComponentSetting[] } -interface ComponentState { - components: Record - customComponents: string[] - selectedComponentId: string | null - componentToPaste?: Component | null - settingsCache: Record - selectedScreenId?: string | null -} - export const INITIAL_COMPONENTS_STATE: ComponentState = { components: {}, customComponents: [], @@ -440,6 +442,11 @@ export class ComponentStore extends BudiStore { * @returns */ createInstance(componentName: string, presetProps: any, parent: any) { + const screen = get(selectedScreen) + if (!screen || !selectedScreen) { + throw "A valid screen must be selected" + } + const definition = this.getDefinition(componentName) if (!definition) { return null @@ -461,7 +468,7 @@ export class ComponentStore extends BudiStore { // Standard post processing this.enrichEmptySettings(instance, { parent, - screen: get(selectedScreen), + screen, useDefaultValues: true, }) @@ -481,7 +488,7 @@ export class ComponentStore extends BudiStore { // Add step name to form steps if (componentName.endsWith("/formstep")) { const parentForm = findClosestMatchingComponent( - get(selectedScreen).props, + screen.props, get(selectedComponent)._id, (component: Component) => component._component.endsWith("/form") ) @@ -841,6 +848,9 @@ export class ComponentStore extends BudiStore { const state = get(this.store) const componentId = state.selectedComponentId const screen = get(selectedScreen) + if (!screen) { + throw "A valid screen must be selected" + } const parent = findComponentParent(screen.props, componentId) const index = parent?._children.findIndex( (x: Component) => x._id === componentId @@ -890,6 +900,9 @@ export class ComponentStore extends BudiStore { const component = get(selectedComponent) const componentId = component?._id const screen = get(selectedScreen) + if (!screen) { + throw "A valid screen must be selected" + } const parent = findComponentParent(screen.props, componentId) const index = parent?._children.findIndex( (x: Component) => x._id === componentId diff --git a/packages/builder/src/stores/builder/index.js b/packages/builder/src/stores/builder/index.js index 08d87bebf5..0d4682b551 100644 --- a/packages/builder/src/stores/builder/index.js +++ b/packages/builder/src/stores/builder/index.js @@ -3,7 +3,7 @@ import { appStore } from "./app.js" import { componentStore, selectedComponent } from "./components" import { navigationStore } from "./navigation.js" import { themeStore } from "./theme.js" -import { screenStore, selectedScreen, sortedScreens } from "./screens.js" +import { screenStore, selectedScreen, sortedScreens } from "./screens" import { builderStore } from "./builder.js" import { hoverStore } from "./hover.js" import { previewStore } from "./preview.js" diff --git a/packages/builder/src/stores/builder/screens.js b/packages/builder/src/stores/builder/screens.ts similarity index 77% rename from packages/builder/src/stores/builder/screens.js rename to packages/builder/src/stores/builder/screens.ts index 8298a1469d..fd16cbfae8 100644 --- a/packages/builder/src/stores/builder/screens.js +++ b/packages/builder/src/stores/builder/screens.ts @@ -13,15 +13,32 @@ import { import { createHistoryStore } from "@/stores/builder/history" import { API } from "@/api" import { BudiStore } from "../BudiStore" +import { + FetchAppPackageResponse, + DeleteScreenResponse, + Screen, + Component, +} from "@budibase/types" +import { ComponentDefinition } from "./components" -export const INITIAL_SCREENS_STATE = { - screens: [], - selectedScreenId: null, +interface ScreenState { + screens: Screen[] + selectedScreenId?: string + selected?: Screen } -export class ScreenStore extends BudiStore { +export const initialScreenState: ScreenState = { + screens: [], +} + +// Review the nulls +export class ScreenStore extends BudiStore { + history: any + delete: any + save: any + constructor() { - super(INITIAL_SCREENS_STATE) + super(initialScreenState) // Bind scope this.select = this.select.bind(this) @@ -34,12 +51,15 @@ export class ScreenStore extends BudiStore { this.deleteScreen = this.deleteScreen.bind(this) this.syncScreenData = this.syncScreenData.bind(this) this.updateSetting = this.updateSetting.bind(this) + // TODO review this behaviour this.sequentialScreenPatch = this.sequentialScreenPatch.bind(this) this.removeCustomLayout = this.removeCustomLayout.bind(this) this.history = createHistoryStore({ - getDoc: id => get(this.store).screens?.find(screen => screen._id === id), + 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)) { @@ -59,14 +79,14 @@ export class ScreenStore extends BudiStore { * Reset entire store back to base config */ reset() { - this.store.set({ ...INITIAL_SCREENS_STATE }) + this.store.set({ ...initialScreenState }) } /** * Replace ALL store screens with application package screens * @param {object} pkg */ - syncAppScreens(pkg) { + syncAppScreens(pkg: FetchAppPackageResponse) { this.update(state => ({ ...state, screens: [...pkg.screens], @@ -79,7 +99,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) @@ -107,14 +127,14 @@ 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 = [] - ) => { - const type = component._component + component: Component, + illegalChildren: string[] = [], + legalDirectChildren: string[] = [] + ): string | undefined => { + const type: string = component._component if (illegalChildren.includes(type)) { return type @@ -137,7 +157,13 @@ export class ScreenStore extends BudiStore { illegalChildren = [] } - const definition = componentStore.getDefinition(component._component) + const definition: ComponentDefinition | null = + componentStore.getDefinition(component._component) + + if (definition == null) { + throw `Invalid defintion ${component._component}` + } + // Reset whitelist for direct children legalDirectChildren = [] if (definition?.legalDirectChildren?.length) { @@ -172,7 +198,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` } } @@ -183,7 +209,7 @@ export class ScreenStore extends BudiStore { * @param {object} screen * @returns {object} */ - async saveScreen(screen) { + async saveScreen(screen: Screen) { const appState = get(appStore) // Validate screen structure if the app supports it @@ -230,7 +256,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 @@ -256,28 +282,51 @@ 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 = ( + // patchFn: (screen: Screen) => any, + // screenId: string + // ) => { + // return Utils.sequential(async () => { + // 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) + // }) + // } + + 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 + } + return this.save(clone) } - return this.save(clone) - }) + ) /** * @param {function} patchFn * @param {string | null} screenId * @returns */ - async patch(patchFn, screenId) { + async patch(patchFn: (screen: Screen) => any, screenId?: string | null) { // Default to the currently selected screen if (!screenId) { const state = get(this.store) @@ -298,7 +347,7 @@ export class ScreenStore extends BudiStore { * @param {object} screen * @returns */ - async replace(screenId, screen) { + async replace(screenId: string, screen: Screen) { if (!screenId) { return } @@ -337,17 +386,25 @@ 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 = [] - screensToDelete.forEach(screen => { - // Delete the screen - promises.push(API.deleteScreen(screen._id, screen._rev)) - // Remove links to this screen - deleteUrls.push(screen.routing.route) - }) + let promises: Promise[] = [] + let deleteUrls: string[] = [] + + // In this instance _id will have been set + // Underline the expectation that _id and _rev will be set after filtering + screensToDelete + .filter( + (screen): screen is Screen & { _id: string; _rev: string } => + !!screen._id || !!screen._rev + ) + .forEach(screen => { + // Delete the screen + promises.push(API.deleteScreen(screen._id, screen._rev)) + // Remove links to this screen + deleteUrls.push(screen.routing.route) + }) await Promise.all(promises) await navigationStore.deleteLink(deleteUrls) const deletedIds = screensToDelete.map(screen => screen._id) @@ -359,8 +416,11 @@ export class ScreenStore extends BudiStore { }) // Deselect the current screen if it was deleted - if (deletedIds.includes(state.selectedScreenId)) { - state.selectedScreenId = null + if ( + state.selectedScreenId && + deletedIds.includes(state.selectedScreenId) + ) { + delete state.selectedScreenId componentStore.update(state => ({ ...state, selectedComponentId: null, @@ -389,13 +449,13 @@ export class ScreenStore extends BudiStore { * @param {any} value * @returns */ - async updateSetting(screen, name, value) { + async updateSetting(screen: Screen, name: string, value: any) { if (!screen || !name) { return } // Apply setting update - const patchFn = screen => { + const patchFn = (screen: Screen) => { if (!screen) { return false } @@ -422,7 +482,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) { @@ -432,11 +492,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" } @@ -448,9 +508,12 @@ 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: Component) => x + ) // Iterate over all components and run checks components.forEach(component => { diff --git a/packages/builder/src/stores/builder/tests/screens.test.js b/packages/builder/src/stores/builder/tests/screens.test.js index 430605b77a..87eb03ea04 100644 --- a/packages/builder/src/stores/builder/tests/screens.test.js +++ b/packages/builder/src/stores/builder/tests/screens.test.js @@ -3,7 +3,7 @@ import { get, writable } from "svelte/store" import { API } from "@/api" import { Constants } from "@budibase/frontend-core" import { componentStore, appStore } from "@/stores/builder" -import { INITIAL_SCREENS_STATE, ScreenStore } from "@/stores/builder/screens" +import { initialScreenState, ScreenStore } from "@/stores/builder/screens" import { getScreenFixture, getComponentFixture, @@ -73,7 +73,7 @@ describe("Screens store", () => { vi.clearAllMocks() const screenStore = new ScreenStore() - ctx.test = { + ctx.bb = { get store() { return get(screenStore) }, @@ -81,74 +81,76 @@ describe("Screens store", () => { } }) - it("Create base screen store with defaults", ctx => { - expect(ctx.test.store).toStrictEqual(INITIAL_SCREENS_STATE) + it("Create base screen store with defaults", ({ bb }) => { + expect(bb.store).toStrictEqual(initialScreenState) }) - it("Syncs all screens from the app package", ctx => { - expect(ctx.test.store.screens.length).toBe(0) + it("Syncs all screens from the app package", ({ bb }) => { + expect(bb.store.screens.length).toBe(0) const screens = Array(2) .fill() .map(() => getScreenFixture().json()) - ctx.test.screenStore.syncAppScreens({ screens }) + bb.screenStore.syncAppScreens({ screens }) - expect(ctx.test.store.screens).toStrictEqual(screens) + expect(bb.store.screens).toStrictEqual(screens) }) - it("Reset the screen store back to the default state", ctx => { - expect(ctx.test.store.screens.length).toBe(0) + it("Reset the screen store back to the default state", ({ bb }) => { + expect(bb.store.screens.length).toBe(0) const screens = Array(2) .fill() .map(() => getScreenFixture().json()) - ctx.test.screenStore.syncAppScreens({ screens }) - expect(ctx.test.store.screens).toStrictEqual(screens) + bb.screenStore.syncAppScreens({ screens }) + expect(bb.store.screens).toStrictEqual(screens) - ctx.test.screenStore.update(state => ({ + bb.screenStore.update(state => ({ ...state, selectedScreenId: screens[0]._id, })) - ctx.test.screenStore.reset() + bb.screenStore.reset() - expect(ctx.test.store).toStrictEqual(INITIAL_SCREENS_STATE) + expect(bb.store).toStrictEqual(initialScreenState) }) - it("Marks a valid screen as selected", ctx => { + it("Marks a valid screen as selected", ({ bb }) => { const screens = Array(2) .fill() .map(() => getScreenFixture().json()) - ctx.test.screenStore.syncAppScreens({ screens }) - expect(ctx.test.store.screens.length).toBe(2) + bb.screenStore.syncAppScreens({ screens }) + expect(bb.store.screens.length).toBe(2) - ctx.test.screenStore.select(screens[0]._id) + bb.screenStore.select(screens[0]._id) - expect(ctx.test.store.selectedScreenId).toEqual(screens[0]._id) + expect(bb.store.selectedScreenId).toEqual(screens[0]._id) }) - it("Skip selecting a screen if it is not present", ctx => { + it("Skip selecting a screen if it is not present", ({ bb }) => { const screens = Array(2) .fill() .map(() => getScreenFixture().json()) - ctx.test.screenStore.syncAppScreens({ screens }) - expect(ctx.test.store.screens.length).toBe(2) + bb.screenStore.syncAppScreens({ screens }) + expect(bb.store.screens.length).toBe(2) - ctx.test.screenStore.select("screen_abc") + bb.screenStore.select("screen_abc") - expect(ctx.test.store.selectedScreenId).toBeNull() + expect(bb.store.selectedScreenId).toBeUndefined() }) - it("Approve a valid empty screen config", ctx => { + it("Approve a valid empty screen config", ({ bb }) => { const coreScreen = getScreenFixture() - ctx.test.screenStore.validate(coreScreen.json()) + bb.screenStore.validate(coreScreen.json()) }) - it("Approve a valid screen config with one component and no illegal children", ctx => { + it("Approve a valid screen config with one component and no illegal children", ({ + bb, + }) => { const coreScreen = getScreenFixture() const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`) @@ -157,12 +159,12 @@ describe("Screens store", () => { const defSpy = vi.spyOn(componentStore, "getDefinition") defSpy.mockReturnValueOnce(COMPONENT_DEFINITIONS.formblock) - ctx.test.screenStore.validate(coreScreen.json()) + bb.screenStore.validate(coreScreen.json()) expect(defSpy).toHaveBeenCalled() }) - it("Reject an attempt to nest invalid components", ctx => { + it("Reject an attempt to nest invalid components", ({ bb }) => { const coreScreen = getScreenFixture() const formOne = getComponentFixture(`${COMP_PREFIX}/form`) @@ -178,14 +180,14 @@ describe("Screens store", () => { return defMap[comp] }) - expect(() => ctx.test.screenStore.validate(coreScreen.json())).toThrowError( + expect(() => bb.screenStore.validate(coreScreen.json())).toThrowError( `You can't place a ${COMPONENT_DEFINITIONS.form.name} here` ) expect(defSpy).toHaveBeenCalled() }) - it("Reject an attempt to deeply nest invalid components", ctx => { + it("Reject an attempt to deeply nest invalid components", ({ bb }) => { const coreScreen = getScreenFixture() const formOne = getComponentFixture(`${COMP_PREFIX}/form`) @@ -210,14 +212,16 @@ describe("Screens store", () => { return defMap[comp] }) - expect(() => ctx.test.screenStore.validate(coreScreen.json())).toThrowError( + expect(() => bb.screenStore.validate(coreScreen.json())).toThrowError( `You can't place a ${COMPONENT_DEFINITIONS.form.name} here` ) expect(defSpy).toHaveBeenCalled() }) - it("Save a brand new screen and add it to the store. No validation", async ctx => { + it("Save a brand new screen and add it to the store. No validation", async ({ + bb, + }) => { const coreScreen = getScreenFixture() const formOne = getComponentFixture(`${COMP_PREFIX}/form`) @@ -225,7 +229,7 @@ describe("Screens store", () => { appStore.set({ features: { componentValidation: false } }) - expect(ctx.test.store.screens.length).toBe(0) + expect(bb.store.screens.length).toBe(0) const newDocId = getScreenDocId() const newDoc = { ...coreScreen.json(), _id: newDocId } @@ -235,15 +239,15 @@ describe("Screens store", () => { vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({ routes: [], }) - await ctx.test.screenStore.save(coreScreen.json()) + await bb.screenStore.save(coreScreen.json()) expect(saveSpy).toHaveBeenCalled() - expect(ctx.test.store.screens.length).toBe(1) + expect(bb.store.screens.length).toBe(1) - expect(ctx.test.store.screens[0]).toStrictEqual(newDoc) + expect(bb.store.screens[0]).toStrictEqual(newDoc) - expect(ctx.test.store.selectedScreenId).toBe(newDocId) + expect(bb.store.selectedScreenId).toBe(newDocId) // The new screen should be selected expect(get(componentStore).selectedComponentId).toBe( @@ -251,7 +255,7 @@ describe("Screens store", () => { ) }) - it("Sync an updated screen to the screen store on save", async ctx => { + it("Sync an updated screen to the screen store on save", async ({ bb }) => { const existingScreens = Array(4) .fill() .map(() => { @@ -261,7 +265,7 @@ describe("Screens store", () => { return screenDoc }) - ctx.test.screenStore.update(state => ({ + bb.screenStore.update(state => ({ ...state, screens: existingScreens.map(screen => screen.json()), })) @@ -279,16 +283,18 @@ describe("Screens store", () => { }) // Saved the existing screen having modified it. - await ctx.test.screenStore.save(existingScreens[2].json()) + await bb.screenStore.save(existingScreens[2].json()) expect(routeSpy).toHaveBeenCalled() expect(saveSpy).toHaveBeenCalled() // On save, the screen is spliced back into the store with the saved content - expect(ctx.test.store.screens[2]).toStrictEqual(existingScreens[2].json()) + expect(bb.store.screens[2]).toStrictEqual(existingScreens[2].json()) }) - it("Sync API data to relevant stores on save. Updated plugins", async ctx => { + it("Sync API data to relevant stores on save. Updated plugins", async ({ + bb, + }) => { const coreScreen = getScreenFixture() const newDocId = getScreenDocId() @@ -318,7 +324,7 @@ describe("Screens store", () => { routes: [], }) - await ctx.test.screenStore.syncScreenData(newDoc) + await bb.screenStore.syncScreenData(newDoc) expect(routeSpy).toHaveBeenCalled() expect(appPackageSpy).toHaveBeenCalled() @@ -326,7 +332,9 @@ describe("Screens store", () => { expect(get(appStore).usedPlugins).toStrictEqual(plugins) }) - it("Sync API updates to relevant stores on save. Plugins unchanged", async ctx => { + it("Sync API updates to relevant stores on save. Plugins unchanged", async ({ + bb, + }) => { const coreScreen = getScreenFixture() const newDocId = getScreenDocId() @@ -343,7 +351,7 @@ describe("Screens store", () => { routes: [], }) - await ctx.test.screenStore.syncScreenData(newDoc) + await bb.screenStore.syncScreenData(newDoc) expect(routeSpy).toHaveBeenCalled() expect(appPackageSpy).not.toHaveBeenCalled() @@ -352,46 +360,48 @@ describe("Screens store", () => { expect(get(appStore).usedPlugins).toStrictEqual([plugin]) }) - it("Proceed to patch if appropriate config are supplied", async ctx => { - vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch").mockImplementation( - () => { - return false - } - ) + it("Proceed to patch if appropriate config are supplied", async ({ bb }) => { + vi.spyOn(bb.screenStore, "sequentialScreenPatch").mockImplementation(() => { + return false + }) const noop = () => {} - await ctx.test.screenStore.patch(noop, "test") - expect(ctx.test.screenStore.sequentialScreenPatch).toHaveBeenCalledWith( + await bb.screenStore.patch(noop, "test") + expect(bb.screenStore.sequentialScreenPatch).toHaveBeenCalledWith( noop, "test" ) }) - it("Return from the patch if all valid config are not present", async ctx => { - vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch") - await ctx.test.screenStore.patch() - expect(ctx.test.screenStore.sequentialScreenPatch).not.toBeCalled() + it("Return from the patch if all valid config are not present", async ({ + bb, + }) => { + vi.spyOn(bb.screenStore, "sequentialScreenPatch") + await bb.screenStore.patch() + expect(bb.screenStore.sequentialScreenPatch).not.toBeCalled() }) - it("Acquire the currently selected screen on patch, if not specified", async ctx => { - vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch") - await ctx.test.screenStore.patch() + it("Acquire the currently selected screen on patch, if not specified", async ({ + bb, + }) => { + vi.spyOn(bb.screenStore, "sequentialScreenPatch") + await bb.screenStore.patch() const noop = () => {} - ctx.test.screenStore.update(state => ({ + bb.screenStore.update(state => ({ ...state, selectedScreenId: "screen_123", })) - await ctx.test.screenStore.patch(noop) - expect(ctx.test.screenStore.sequentialScreenPatch).toHaveBeenCalledWith( + await bb.screenStore.patch(noop) + expect(bb.screenStore.sequentialScreenPatch).toHaveBeenCalledWith( noop, "screen_123" ) }) // Used by the websocket - it("Ignore a call to replace if no screenId is provided", ctx => { + it("Ignore a call to replace if no screenId is provided", ({ bb }) => { const existingScreens = Array(4) .fill() .map(() => { @@ -400,14 +410,16 @@ describe("Screens store", () => { screenDoc._json._id = existingDocId return screenDoc.json() }) - ctx.test.screenStore.syncAppScreens({ screens: existingScreens }) + bb.screenStore.syncAppScreens({ screens: existingScreens }) - ctx.test.screenStore.replace() + bb.screenStore.replace() - expect(ctx.test.store.screens).toStrictEqual(existingScreens) + expect(bb.store.screens).toStrictEqual(existingScreens) }) - it("Remove a screen from the store if a single screenId is supplied", ctx => { + it("Remove a screen from the store if a single screenId is supplied", ({ + bb, + }) => { const existingScreens = Array(4) .fill() .map(() => { @@ -416,17 +428,17 @@ describe("Screens store", () => { screenDoc._json._id = existingDocId return screenDoc.json() }) - ctx.test.screenStore.syncAppScreens({ screens: existingScreens }) + bb.screenStore.syncAppScreens({ screens: existingScreens }) - ctx.test.screenStore.replace(existingScreens[1]._id) + bb.screenStore.replace(existingScreens[1]._id) const filtered = existingScreens.filter( screen => screen._id != existingScreens[1]._id ) - expect(ctx.test.store.screens).toStrictEqual(filtered) + expect(bb.store.screens).toStrictEqual(filtered) }) - it("Replace an existing screen with a new version of itself", ctx => { + it("Replace an existing screen with a new version of itself", ({ bb }) => { const existingScreens = Array(4) .fill() .map(() => { @@ -436,7 +448,7 @@ describe("Screens store", () => { return screenDoc }) - ctx.test.screenStore.update(state => ({ + bb.screenStore.update(state => ({ ...state, screens: existingScreens.map(screen => screen.json()), })) @@ -444,15 +456,14 @@ describe("Screens store", () => { const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`) existingScreens[2].addChild(formBlock) - ctx.test.screenStore.replace( - existingScreens[2]._id, - existingScreens[2].json() - ) + bb.screenStore.replace(existingScreens[2]._id, existingScreens[2].json()) - expect(ctx.test.store.screens.length).toBe(4) + expect(bb.store.screens.length).toBe(4) }) - it("Add a screen when attempting to replace one not present in the store", ctx => { + it("Add a screen when attempting to replace one not present in the store", ({ + bb, + }) => { const existingScreens = Array(4) .fill() .map(() => { @@ -462,7 +473,7 @@ describe("Screens store", () => { return screenDoc }) - ctx.test.screenStore.update(state => ({ + bb.screenStore.update(state => ({ ...state, screens: existingScreens.map(screen => screen.json()), })) @@ -470,13 +481,13 @@ describe("Screens store", () => { const newScreenDoc = getScreenFixture() newScreenDoc._json._id = getScreenDocId() - ctx.test.screenStore.replace(newScreenDoc._json._id, newScreenDoc.json()) + bb.screenStore.replace(newScreenDoc._json._id, newScreenDoc.json()) - expect(ctx.test.store.screens.length).toBe(5) - expect(ctx.test.store.screens[4]).toStrictEqual(newScreenDoc.json()) + expect(bb.store.screens.length).toBe(5) + expect(bb.store.screens[4]).toStrictEqual(newScreenDoc.json()) }) - it("Delete a single screen and remove it from the store", async ctx => { + it("Delete a single screen and remove it from the store", async ({ bb }) => { const existingScreens = Array(3) .fill() .map(() => { @@ -486,14 +497,14 @@ describe("Screens store", () => { return screenDoc }) - ctx.test.screenStore.update(state => ({ + bb.screenStore.update(state => ({ ...state, screens: existingScreens.map(screen => screen.json()), })) const deleteSpy = vi.spyOn(API, "deleteScreen") - await ctx.test.screenStore.delete(existingScreens[2].json()) + await bb.screenStore.delete(existingScreens[2].json()) vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({ routes: [], @@ -501,13 +512,15 @@ describe("Screens store", () => { expect(deleteSpy).toBeCalled() - expect(ctx.test.store.screens.length).toBe(2) + expect(bb.store.screens.length).toBe(2) // Just confirm that the routes at are being initialised expect(get(appStore).routes).toEqual([]) }) - it("Upon delete, reset selected screen and component ids if the screen was selected", async ctx => { + it("Upon delete, reset selected screen and component ids if the screen was selected", async ({ + bb, + }) => { const existingScreens = Array(3) .fill() .map(() => { @@ -517,7 +530,7 @@ describe("Screens store", () => { return screenDoc }) - ctx.test.screenStore.update(state => ({ + bb.screenStore.update(state => ({ ...state, screens: existingScreens.map(screen => screen.json()), selectedScreenId: existingScreens[2]._json._id, @@ -528,14 +541,16 @@ describe("Screens store", () => { selectedComponentId: existingScreens[2]._json._id, })) - await ctx.test.screenStore.delete(existingScreens[2].json()) + await bb.screenStore.delete(existingScreens[2].json()) - expect(ctx.test.store.screens.length).toBe(2) + expect(bb.store.screens.length).toBe(2) expect(get(componentStore).selectedComponentId).toBeNull() - expect(ctx.test.store.selectedScreenId).toBeNull() + expect(bb.store.selectedScreenId).toBeUndefined() }) - it("Delete multiple is not supported and should leave the store unchanged", async ctx => { + it("Delete multiple is not supported and should leave the store unchanged", async ({ + bb, + }) => { const existingScreens = Array(3) .fill() .map(() => { @@ -547,7 +562,7 @@ describe("Screens store", () => { const storeScreens = existingScreens.map(screen => screen.json()) - ctx.test.screenStore.update(state => ({ + bb.screenStore.update(state => ({ ...state, screens: existingScreens.map(screen => screen.json()), })) @@ -556,42 +571,40 @@ describe("Screens store", () => { const deleteSpy = vi.spyOn(API, "deleteScreen") - await ctx.test.screenStore.delete(targets) + await bb.screenStore.delete(targets) expect(deleteSpy).not.toHaveBeenCalled() - expect(ctx.test.store.screens.length).toBe(3) - expect(ctx.test.store.screens).toStrictEqual(storeScreens) + expect(bb.store.screens.length).toBe(3) + expect(bb.store.screens).toStrictEqual(storeScreens) }) - it("Update a screen setting", async ctx => { + it("Update a screen setting", async ({ bb }) => { const screenDoc = getScreenFixture() const existingDocId = getScreenDocId() screenDoc._json._id = existingDocId - await ctx.test.screenStore.update(state => ({ + await bb.screenStore.update(state => ({ ...state, screens: [screenDoc.json()], })) const patchedDoc = screenDoc.json() const patchSpy = vi - .spyOn(ctx.test.screenStore, "patch") + .spyOn(bb.screenStore, "patch") .mockImplementation(async patchFn => { patchFn(patchedDoc) return }) - await ctx.test.screenStore.updateSetting( - patchedDoc, - "showNavigation", - false - ) + await bb.screenStore.updateSetting(patchedDoc, "showNavigation", false) expect(patchSpy).toBeCalled() expect(patchedDoc.showNavigation).toBe(false) }) - it("Ensure only one homescreen per role after updating setting. All screens same role", async ctx => { + it("Ensure only one homescreen per role after updating setting. All screens same role", async ({ + bb, + }) => { const existingScreens = Array(3) .fill() .map(() => { @@ -611,23 +624,21 @@ describe("Screens store", () => { // Set the 2nd screen as the home screen storeScreens[1].routing.homeScreen = true - await ctx.test.screenStore.update(state => ({ + await bb.screenStore.update(state => ({ ...state, screens: storeScreens, })) const patchSpy = vi - .spyOn(ctx.test.screenStore, "patch") + .spyOn(bb.screenStore, "patch") .mockImplementation(async (patchFn, screenId) => { - const target = ctx.test.store.screens.find( - screen => screen._id === screenId - ) + const target = bb.store.screens.find(screen => screen._id === screenId) patchFn(target) - await ctx.test.screenStore.replace(screenId, target) + await bb.screenStore.replace(screenId, target) }) - await ctx.test.screenStore.updateSetting( + await bb.screenStore.updateSetting( storeScreens[0], "routing.homeScreen", true @@ -637,13 +648,15 @@ describe("Screens store", () => { expect(patchSpy).toBeCalledTimes(2) // The new homescreen for BASIC - expect(ctx.test.store.screens[0].routing.homeScreen).toBe(true) + expect(bb.store.screens[0].routing.homeScreen).toBe(true) // The previous home screen for the BASIC role is now unset - expect(ctx.test.store.screens[1].routing.homeScreen).toBe(false) + expect(bb.store.screens[1].routing.homeScreen).toBe(false) }) - it("Ensure only one homescreen per role when updating screen setting. Multiple screen roles", async ctx => { + it("Ensure only one homescreen per role when updating screen setting. Multiple screen roles", async ({ + bb, + }) => { const expectedRoles = [ Constants.Roles.BASIC, Constants.Roles.POWER, @@ -675,30 +688,24 @@ describe("Screens store", () => { sorted[9].routing.homeScreen = true // Set screens state - await ctx.test.screenStore.update(state => ({ + await bb.screenStore.update(state => ({ ...state, screens: sorted, })) const patchSpy = vi - .spyOn(ctx.test.screenStore, "patch") + .spyOn(bb.screenStore, "patch") .mockImplementation(async (patchFn, screenId) => { - const target = ctx.test.store.screens.find( - screen => screen._id === screenId - ) + const target = bb.store.screens.find(screen => screen._id === screenId) patchFn(target) - await ctx.test.screenStore.replace(screenId, target) + await bb.screenStore.replace(screenId, target) }) // ADMIN homeScreen updated from 0 to 2 - await ctx.test.screenStore.updateSetting( - sorted[2], - "routing.homeScreen", - true - ) + await bb.screenStore.updateSetting(sorted[2], "routing.homeScreen", true) - const results = ctx.test.store.screens.reduce((acc, screen) => { + const results = bb.store.screens.reduce((acc, screen) => { if (screen.routing.homeScreen) { acc[screen.routing.roleId] = acc[screen.routing.roleId] || [] acc[screen.routing.roleId].push(screen) @@ -706,7 +713,7 @@ describe("Screens store", () => { return acc }, {}) - const screens = ctx.test.store.screens + const screens = bb.store.screens // Should still only be one of each homescreen expect(results[Constants.Roles.ADMIN].length).toBe(1) expect(screens[2].routing.homeScreen).toBe(true) @@ -724,74 +731,80 @@ describe("Screens store", () => { expect(patchSpy).toBeCalledTimes(2) }) - it("Sequential patch check. Exit if the screenId is not valid.", async ctx => { + it("Sequential patch check. Exit if the screenId is not valid.", async ({ + bb, + }) => { const screenDoc = getScreenFixture() const existingDocId = getScreenDocId() screenDoc._json._id = existingDocId const original = screenDoc.json() - await ctx.test.screenStore.update(state => ({ + await bb.screenStore.update(state => ({ ...state, screens: [original], })) const saveSpy = vi - .spyOn(ctx.test.screenStore, "save") + .spyOn(bb.screenStore, "save") .mockImplementation(async () => { return }) // A screen with this Id does not exist - await ctx.test.screenStore.sequentialScreenPatch(() => {}, "123") + await bb.screenStore.sequentialScreenPatch(() => {}, "123") expect(saveSpy).not.toBeCalled() }) - it("Sequential patch check. Exit if the patchFn result is false", async ctx => { + it("Sequential patch check. Exit if the patchFn result is false", async ({ + bb, + }) => { const screenDoc = getScreenFixture() const existingDocId = getScreenDocId() screenDoc._json._id = existingDocId const original = screenDoc.json() // Set screens state - await ctx.test.screenStore.update(state => ({ + await bb.screenStore.update(state => ({ ...state, screens: [original], })) const saveSpy = vi - .spyOn(ctx.test.screenStore, "save") + .spyOn(bb.screenStore, "save") .mockImplementation(async () => { return }) // Returning false from the patch will abort the save - await ctx.test.screenStore.sequentialScreenPatch(() => { + await bb.screenStore.sequentialScreenPatch(() => { return false }, "123") expect(saveSpy).not.toBeCalled() }) - it("Sequential patch check. Patch applied and save requested", async ctx => { + it("Sequential patch check. Patch applied and save requested", async ({ + bb, + }) => { const screenDoc = getScreenFixture() const existingDocId = getScreenDocId() screenDoc._json._id = existingDocId const original = screenDoc.json() - await ctx.test.screenStore.update(state => ({ + await bb.screenStore.update(state => ({ ...state, screens: [original], })) const saveSpy = vi - .spyOn(ctx.test.screenStore, "save") + .spyOn(bb.screenStore, "save") .mockImplementation(async () => { return }) - await ctx.test.screenStore.sequentialScreenPatch(screen => { + await bb.screenStore.sequentialScreenPatch(screen => { screen.name = "updated" }, existingDocId) diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js index c424aea5b2..eeff561215 100644 --- a/packages/frontend-core/src/utils/utils.js +++ b/packages/frontend-core/src/utils/utils.js @@ -8,7 +8,7 @@ export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) * Utility to wrap an async function and ensure all invocations happen * sequentially. * @param fn the async function to run - * @return {Promise} a sequential version of the function + * @return {Function} a sequential version of the function */ export const sequential = fn => { let queue = [] From f1d57906b589642e90c457eaf956b6f48753ff3e Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 6 Jan 2025 14:25:13 +0000 Subject: [PATCH 003/321] Clean up jsdoc comments and remove unnecessary comments --- .../builder/src/stores/builder/screens.ts | 57 ++++++------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index fd16cbfae8..a749ded1f9 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -18,6 +18,7 @@ import { DeleteScreenResponse, Screen, Component, + SaveScreenResponse, } from "@budibase/types" import { ComponentDefinition } from "./components" @@ -51,7 +52,6 @@ export class ScreenStore extends BudiStore { this.deleteScreen = this.deleteScreen.bind(this) this.syncScreenData = this.syncScreenData.bind(this) this.updateSetting = this.updateSetting.bind(this) - // TODO review this behaviour this.sequentialScreenPatch = this.sequentialScreenPatch.bind(this) this.removeCustomLayout = this.removeCustomLayout.bind(this) @@ -84,7 +84,7 @@ export class ScreenStore extends BudiStore { /** * Replace ALL store screens with application package screens - * @param {object} pkg + * @param {FetchAppPackageResponse} pkg */ syncAppScreens(pkg: FetchAppPackageResponse) { this.update(state => ({ @@ -123,7 +123,7 @@ export class ScreenStore extends BudiStore { * Recursively parses the entire screen doc and checks for components * violating illegal child configurations. * - * @param {object} screen + * @param {Screen} screen * @throws Will throw an error containing the name of the component causing * the invalid screen state */ @@ -206,8 +206,7 @@ export class ScreenStore extends BudiStore { * Core save method. If creating a new screen, the store will sync the target * screen id to ensure that it is selected in the builder * - * @param {object} screen - * @returns {object} + * @param {Screen} screen The screen being modified/created */ async saveScreen(screen: Screen) { const appState = get(appStore) @@ -254,7 +253,7 @@ export class ScreenStore extends BudiStore { /** * After saving a screen, sync plugins and routes to the appStore - * @param {object} savedScreen + * @param {Screen} savedScreen */ async syncScreenData(savedScreen: Screen) { const appState = get(appStore) @@ -282,27 +281,6 @@ 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 = ( - // patchFn: (screen: Screen) => any, - // screenId: string - // ) => { - // return Utils.sequential(async () => { - // 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 - // } - // return this.save(clone) - // }) - // } - sequentialScreenPatch = Utils.sequential( async (patchFn: (screen: Screen) => any, screenId: string) => { const state = get(this.store) @@ -322,11 +300,13 @@ export class ScreenStore extends BudiStore { ) /** - * @param {function} patchFn + * @param {Function} patchFn the patch action to be applied * @param {string | null} screenId - * @returns */ - async patch(patchFn: (screen: Screen) => any, screenId?: string | null) { + async patch( + patchFn: (screen: Screen) => any, + screenId?: string | null + ): Promise { // Default to the currently selected screen if (!screenId) { const state = get(this.store) @@ -343,9 +323,9 @@ export class ScreenStore extends BudiStore { * the screen supplied. If no screen is provided, the target has * been removed by another user and will be filtered from the store. * Used to marshal updates for the websocket - * @param {string} screenId - * @param {object} screen - * @returns + * + * @param {string} screenId the target screen id + * @param {Screen} screen the replacement screen */ async replace(screenId: string, screen: Screen) { if (!screenId) { @@ -383,10 +363,9 @@ export class ScreenStore extends BudiStore { * Any deleted screens will then have their routes/links purged * * Wrapped by {@link delete} - * @param {object | array} screens - * @returns + * @param {Screen | Screen[]} screens */ - async deleteScreen(screens: Screen[]) { + async deleteScreen(screens: Screen | Screen[]) { const screensToDelete = Array.isArray(screens) ? screens : [screens] // Build array of promises to speed up bulk deletions let promises: Promise[] = [] @@ -435,7 +414,6 @@ export class ScreenStore extends BudiStore { return state }) - return null } /** @@ -444,10 +422,9 @@ export class ScreenStore extends BudiStore { * After a successful update, this method ensures that there is only * ONE home screen per user Role. * - * @param {object} screen + * @param {Screen} screen * @param {string} name e.g "routing.homeScreen" or "showNavigation" * @param {any} value - * @returns */ async updateSetting(screen: Screen, name: string, value: any) { if (!screen || !name) { @@ -506,7 +483,7 @@ export class ScreenStore extends BudiStore { /** * Parse the entire screen component tree and ensure settings are valid * and up-to-date. Ensures stability after a product update. - * @param {object} screen + * @param {Screen} screen */ async enrichEmptySettings(screen: Screen) { // Flatten the recursive component tree From 744b1d3dbcba03f6fd848c4d38dab4828f5fa5f8 Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 7 Jan 2025 09:32:02 +0000 Subject: [PATCH 004/321] Screen type fixes --- .../builder/src/stores/builder/componentTreeNodes.ts | 7 ++++++- packages/builder/src/stores/builder/websocket.ts | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/builder/componentTreeNodes.ts b/packages/builder/src/stores/builder/componentTreeNodes.ts index 420c540e37..cec4e3a4a0 100644 --- a/packages/builder/src/stores/builder/componentTreeNodes.ts +++ b/packages/builder/src/stores/builder/componentTreeNodes.ts @@ -49,7 +49,12 @@ 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 | undefined = get(selectedScreenStore) + + if (!selectedScreen) { + console.error("Invalid node " + componentId) + return {} + } 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..a56fec2227 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, + Table, + UIUser, + Screen, +} from "@budibase/types" export const createBuilderWebsocket = (appId: string) => { const socket = createWebsocket("/socket/builder") From 4acb8fae99b39ef03a1817af83ecfcda1926c858 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 11:09:22 +0000 Subject: [PATCH 005/321] Convert portal templates store --- packages/builder/src/stores/portal/templates.js | 16 ---------------- packages/builder/src/stores/portal/templates.ts | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 16 deletions(-) delete mode 100644 packages/builder/src/stores/portal/templates.js create mode 100644 packages/builder/src/stores/portal/templates.ts diff --git a/packages/builder/src/stores/portal/templates.js b/packages/builder/src/stores/portal/templates.js deleted file mode 100644 index 2ff23866ab..0000000000 --- a/packages/builder/src/stores/portal/templates.js +++ /dev/null @@ -1,16 +0,0 @@ -import { writable } from "svelte/store" -import { API } from "@/api" - -export function templatesStore() { - const { subscribe, set } = writable([]) - - return { - subscribe, - load: async () => { - const templates = await API.getAppTemplates() - set(templates) - }, - } -} - -export const templates = templatesStore() diff --git a/packages/builder/src/stores/portal/templates.ts b/packages/builder/src/stores/portal/templates.ts new file mode 100644 index 0000000000..caf1a54ced --- /dev/null +++ b/packages/builder/src/stores/portal/templates.ts @@ -0,0 +1,16 @@ +import { API } from "@/api" +import { BudiStore } from "../BudiStore" +import { TemplateMetadata } from "@budibase/types" + +class TemplateStore extends BudiStore { + constructor() { + super([]) + } + + async load() { + const templates = await API.getAppTemplates() + this.set(templates) + } +} + +export const templates = new TemplateStore() From 085617a5220be1dcbc2d31cde6cb645709b6cf67 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 11:17:11 +0000 Subject: [PATCH 006/321] Convert portal temporal store to TS --- .../portal/licensing/LicensingOverlays.svelte | 4 +- .../portal/licensing/licensingBanners.js | 4 +- .../builder/src/stores/portal/temporal.js | 45 ---------------- .../builder/src/stores/portal/temporal.ts | 53 +++++++++++++++++++ 4 files changed, 57 insertions(+), 49 deletions(-) delete mode 100644 packages/builder/src/stores/portal/temporal.js create mode 100644 packages/builder/src/stores/portal/temporal.ts diff --git a/packages/builder/src/components/portal/licensing/LicensingOverlays.svelte b/packages/builder/src/components/portal/licensing/LicensingOverlays.svelte index 3de2b464e0..8280251839 100644 --- a/packages/builder/src/components/portal/licensing/LicensingOverlays.svelte +++ b/packages/builder/src/components/portal/licensing/LicensingOverlays.svelte @@ -20,7 +20,7 @@ const processModals = () => { const defaultCacheFn = key => { - temporalStore.actions.setExpiring(key, {}, oneDayInSeconds) + temporalStore.setExpiring(key, {}, oneDayInSeconds) } const dismissableModals = [ @@ -50,7 +50,7 @@ }, ] return dismissableModals.filter(modal => { - return !temporalStore.actions.getExpiring(modal.key) && modal.criteria() + return !temporalStore.getExpiring(modal.key) && modal.criteria() }) } diff --git a/packages/builder/src/components/portal/licensing/licensingBanners.js b/packages/builder/src/components/portal/licensing/licensingBanners.js index 230b9fe6f6..62ca6caa1b 100644 --- a/packages/builder/src/components/portal/licensing/licensingBanners.js +++ b/packages/builder/src/components/portal/licensing/licensingBanners.js @@ -6,7 +6,7 @@ import { BANNER_TYPES } from "@budibase/bbui" const oneDayInSeconds = 86400 const defaultCacheFn = key => { - temporalStore.actions.setExpiring(key, {}, oneDayInSeconds) + temporalStore.setExpiring(key, {}, oneDayInSeconds) } const upgradeAction = key => { @@ -148,7 +148,7 @@ export const getBanners = () => { buildUsersAboveLimitBanner(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER), ].filter(licensingBanner => { return ( - !temporalStore.actions.getExpiring(licensingBanner.key) && + !temporalStore.getExpiring(licensingBanner.key) && licensingBanner.criteria() ) }) diff --git a/packages/builder/src/stores/portal/temporal.js b/packages/builder/src/stores/portal/temporal.js deleted file mode 100644 index 96b47d1c7f..0000000000 --- a/packages/builder/src/stores/portal/temporal.js +++ /dev/null @@ -1,45 +0,0 @@ -import { createLocalStorageStore } from "@budibase/frontend-core" -import { get } from "svelte/store" - -export const createTemporalStore = () => { - const initialValue = {} - - const localStorageKey = `bb-temporal` - const store = createLocalStorageStore(localStorageKey, initialValue) - - const setExpiring = (key, data, duration) => { - const updated = { - ...data, - expiry: Date.now() + duration * 1000, - } - - store.update(state => ({ - ...state, - [key]: updated, - })) - } - - const getExpiring = key => { - const entry = get(store)[key] - if (!entry) { - return - } - const currentExpiry = entry.expiry - if (currentExpiry < Date.now()) { - store.update(state => { - delete state[key] - return state - }) - return null - } else { - return entry - } - } - - return { - subscribe: store.subscribe, - actions: { setExpiring, getExpiring }, - } -} - -export const temporalStore = createTemporalStore() diff --git a/packages/builder/src/stores/portal/temporal.ts b/packages/builder/src/stores/portal/temporal.ts new file mode 100644 index 0000000000..acbe6feff2 --- /dev/null +++ b/packages/builder/src/stores/portal/temporal.ts @@ -0,0 +1,53 @@ +import { get } from "svelte/store" +import { BudiStore, PersistenceType } from "../BudiStore" + +type TemporalItem = Record & { expiry: number } +type TemporalState = Record + +class TemporalStore extends BudiStore { + constructor() { + super( + {}, + { + persistence: { + key: "bb-temporal", + type: PersistenceType.LOCAL, + }, + } + ) + } + + setExpiring = ( + key: string, + data: Record, + durationSeconds: number + ) => { + const updated: TemporalItem = { + ...data, + expiry: Date.now() + durationSeconds * 1000, + } + this.update(state => ({ + ...state, + [key]: updated, + })) + } + + getExpiring(key: string) { + const entry = get(this.store)[key] + if (!entry) { + return null + } + const currentExpiry = entry.expiry + if (currentExpiry < Date.now()) { + this.update(state => { + delete state[key] + return state + }) + return null + } else { + return entry + } + } +} + +export const temporalStore = new TemporalStore() From 5f3825118f175b66c03c2ac2147c0364ca8f838f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 11:23:09 +0000 Subject: [PATCH 007/321] Convert portal theme store to TS --- packages/builder/src/stores/portal/theme.js | 37 ----------------- packages/builder/src/stores/portal/theme.ts | 45 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 37 deletions(-) delete mode 100644 packages/builder/src/stores/portal/theme.js create mode 100644 packages/builder/src/stores/portal/theme.ts diff --git a/packages/builder/src/stores/portal/theme.js b/packages/builder/src/stores/portal/theme.js deleted file mode 100644 index 11a87845e1..0000000000 --- a/packages/builder/src/stores/portal/theme.js +++ /dev/null @@ -1,37 +0,0 @@ -import { createLocalStorageStore } from "@budibase/frontend-core" -import { derived } from "svelte/store" -import { - DefaultBuilderTheme, - ensureValidTheme, - getThemeClassNames, - ThemeOptions, - ThemeClassPrefix, -} from "@budibase/shared-core" - -export const getThemeStore = () => { - const themeElement = document.documentElement - const initialValue = { - theme: DefaultBuilderTheme, - } - const store = createLocalStorageStore("bb-theme", initialValue) - const derivedStore = derived(store, $store => ({ - ...$store, - theme: ensureValidTheme($store.theme, DefaultBuilderTheme), - })) - - // Update theme class when store changes - derivedStore.subscribe(({ theme }) => { - const classNames = getThemeClassNames(theme).split(" ") - ThemeOptions.forEach(option => { - const className = `${ThemeClassPrefix}${option.id}` - themeElement.classList.toggle(className, classNames.includes(className)) - }) - }) - - return { - ...store, - subscribe: derivedStore.subscribe, - } -} - -export const themeStore = getThemeStore() diff --git a/packages/builder/src/stores/portal/theme.ts b/packages/builder/src/stores/portal/theme.ts new file mode 100644 index 0000000000..5198555024 --- /dev/null +++ b/packages/builder/src/stores/portal/theme.ts @@ -0,0 +1,45 @@ +import { derived, Writable } from "svelte/store" +import { + DefaultBuilderTheme, + ensureValidTheme, + getThemeClassNames, + ThemeOptions, + ThemeClassPrefix, +} from "@budibase/shared-core" +import { Theme } from "@budibase/types" +import { DerivedBudiStore, PersistenceType } from "../BudiStore" + +interface ThemeState { + theme: Theme +} + +class ThemeStore extends DerivedBudiStore { + constructor() { + const makeDerivedStore = (store: Writable) => { + return derived(store, $store => ({ + ...$store, + theme: ensureValidTheme($store.theme, DefaultBuilderTheme), + })) + } + super({ theme: DefaultBuilderTheme }, makeDerivedStore, { + persistence: { + key: "bb-theme", + type: PersistenceType.LOCAL, + }, + }) + + // Update theme class when store changes + this.subscribe(({ theme }) => { + const classNames = getThemeClassNames(theme).split(" ") + ThemeOptions.forEach(option => { + const className = `${ThemeClassPrefix}${option.id}` + document.documentElement.classList.toggle( + className, + classNames.includes(className) + ) + }) + }) + } +} + +export const themeStore = new ThemeStore() From 65bd89250de002307ce62c1c2845ba1efa3e48ad Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 11:24:04 +0000 Subject: [PATCH 008/321] Convert portal barrel file to TS --- packages/builder/src/stores/portal/{index.js => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/builder/src/stores/portal/{index.js => index.ts} (100%) diff --git a/packages/builder/src/stores/portal/index.js b/packages/builder/src/stores/portal/index.ts similarity index 100% rename from packages/builder/src/stores/portal/index.js rename to packages/builder/src/stores/portal/index.ts From 12b283d41ed8b7be2e41b099321adcf47efe1ad0 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 11:28:04 +0000 Subject: [PATCH 009/321] Convert portal navigation store to BudiStore --- .../builder/src/pages/builder/_layout.svelte | 2 +- .../builder/src/stores/portal/navigation.ts | 31 +++++++------------ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/builder/src/pages/builder/_layout.svelte b/packages/builder/src/pages/builder/_layout.svelte index 209cf2746d..7e765d7366 100644 --- a/packages/builder/src/pages/builder/_layout.svelte +++ b/packages/builder/src/pages/builder/_layout.svelte @@ -18,7 +18,7 @@ $: useAccountPortal = cloud && !$admin.disableAccountPortal - navigation.actions.init($redirect) + navigation.init($redirect) const validateTenantId = async () => { const host = window.location.host diff --git a/packages/builder/src/stores/portal/navigation.ts b/packages/builder/src/stores/portal/navigation.ts index 4eb50bc84f..f289c86bf9 100644 --- a/packages/builder/src/stores/portal/navigation.ts +++ b/packages/builder/src/stores/portal/navigation.ts @@ -1,38 +1,31 @@ -import { writable } from "svelte/store" +import { BudiStore } from "../BudiStore" type GotoFuncType = (path: string) => void -interface PortalNavigationStore { +interface NavigationState { initialisated: boolean goto: GotoFuncType } -export function createNavigationStore() { - const store = writable({ - initialisated: false, - goto: undefined as any, - }) - const { set, subscribe } = store +class NavigationStore extends BudiStore { + constructor() { + super({ + initialisated: false, + goto: undefined as any, + }) + } - const init = (gotoFunc: GotoFuncType) => { + init(gotoFunc: GotoFuncType) { if (typeof gotoFunc !== "function") { throw new Error( `gotoFunc must be a function, found a "${typeof gotoFunc}" instead` ) } - - set({ + this.set({ initialisated: true, goto: gotoFunc, }) } - - return { - subscribe, - actions: { - init, - }, - } } -export const navigation = createNavigationStore() +export const navigation = new NavigationStore() From 502c1605307c3efdcb4c6ea27e5a5a2e08a9ccff Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 11:31:44 +0000 Subject: [PATCH 010/321] Convert admin store to BudiStore --- packages/builder/src/stores/portal/admin.ts | 71 ++++++++++----------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/packages/builder/src/stores/portal/admin.ts b/packages/builder/src/stores/portal/admin.ts index f262d54bff..1cb0be1313 100644 --- a/packages/builder/src/stores/portal/admin.ts +++ b/packages/builder/src/stores/portal/admin.ts @@ -1,4 +1,4 @@ -import { writable, get } from "svelte/store" +import { get } from "svelte/store" import { API } from "@/api" import { auth } from "@/stores/portal" import { banner } from "@budibase/bbui" @@ -7,42 +7,44 @@ import { GetEnvironmentResponse, SystemStatusResponse, } from "@budibase/types" +import { BudiStore } from "../BudiStore" -interface PortalAdminStore extends GetEnvironmentResponse { +interface AdminState extends GetEnvironmentResponse { loaded: boolean checklist?: ConfigChecklistResponse status?: SystemStatusResponse } -export function createAdminStore() { - const admin = writable({ - loaded: false, - multiTenancy: false, - cloud: false, - isDev: false, - disableAccountPortal: false, - offlineMode: false, - maintenance: [], - }) +class AdminStore extends BudiStore { + constructor() { + super({ + loaded: false, + multiTenancy: false, + cloud: false, + isDev: false, + disableAccountPortal: false, + offlineMode: false, + maintenance: [], + }) + } - async function init() { - await getChecklist() - await getEnvironment() + async init() { + await this.getChecklist() + await this.getEnvironment() // enable system status checks in the cloud - if (get(admin).cloud) { - await getSystemStatus() - checkStatus() + if (get(this.store).cloud) { + await this.getSystemStatus() + this.checkStatus() } - - admin.update(store => { + this.update(store => { store.loaded = true return store }) } - async function getEnvironment() { + async getEnvironment() { const environment = await API.getEnvironment() - admin.update(store => { + this.update(store => { store.multiTenancy = environment.multiTenancy store.cloud = environment.cloud store.disableAccountPortal = environment.disableAccountPortal @@ -56,43 +58,36 @@ export function createAdminStore() { }) } - const checkStatus = async () => { - const health = get(admin)?.status?.health + async checkStatus() { + const health = get(this.store).status?.health if (!health?.passing) { await banner.showStatus() } } - async function getSystemStatus() { + async getSystemStatus() { const status = await API.getSystemStatus() - admin.update(store => { + this.update(store => { store.status = status return store }) } - async function getChecklist() { + async getChecklist() { const tenantId = get(auth).tenantId const checklist = await API.getChecklist(tenantId) - admin.update(store => { + this.update(store => { store.checklist = checklist return store }) } - function unload() { - admin.update(store => { + unload() { + this.update(store => { store.loaded = false return store }) } - - return { - subscribe: admin.subscribe, - init, - unload, - getChecklist, - } } -export const admin = createAdminStore() +export const admin = new AdminStore() From 153d905921c48be839c8aa1a74d76743017fe8b3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 11:33:39 +0000 Subject: [PATCH 011/321] Update exports --- packages/builder/src/stores/portal/admin.test.js | 4 ++-- packages/builder/src/stores/portal/admin.ts | 2 +- packages/builder/src/stores/portal/auditLogs.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/stores/portal/admin.test.js b/packages/builder/src/stores/portal/admin.test.js index 1528042630..261e94eb6f 100644 --- a/packages/builder/src/stores/portal/admin.test.js +++ b/packages/builder/src/stores/portal/admin.test.js @@ -1,5 +1,5 @@ import { it, expect, describe, beforeEach, vi } from "vitest" -import { createAdminStore } from "./admin" +import { AdminStore } from "./admin" import { writable, get } from "svelte/store" import { API } from "@/api" import { auth } from "@/stores/portal" @@ -46,7 +46,7 @@ describe("admin store", () => { ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() } writable.mockReturnValue(ctx.writableReturn) - ctx.returnedStore = createAdminStore() + ctx.returnedStore = new AdminStore() }) it("returns the created store", ctx => { diff --git a/packages/builder/src/stores/portal/admin.ts b/packages/builder/src/stores/portal/admin.ts index 1cb0be1313..90e3a5cdc9 100644 --- a/packages/builder/src/stores/portal/admin.ts +++ b/packages/builder/src/stores/portal/admin.ts @@ -15,7 +15,7 @@ interface AdminState extends GetEnvironmentResponse { status?: SystemStatusResponse } -class AdminStore extends BudiStore { +export class AdminStore extends BudiStore { constructor() { super({ loaded: false, diff --git a/packages/builder/src/stores/portal/auditLogs.ts b/packages/builder/src/stores/portal/auditLogs.ts index ff29f0cd1b..6f11f228d6 100644 --- a/packages/builder/src/stores/portal/auditLogs.ts +++ b/packages/builder/src/stores/portal/auditLogs.ts @@ -13,7 +13,7 @@ interface PortalAuditLogsStore { logs?: SearchAuditLogsResponse } -export class AuditLogsStore extends BudiStore { +class AuditLogsStore extends BudiStore { constructor() { super({}) } From 9e10921e2981b73b8ca497354d0b55c5089e7fff Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 12:01:29 +0000 Subject: [PATCH 012/321] Update tests --- packages/builder/src/stores/portal/admin.test.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/builder/src/stores/portal/admin.test.js b/packages/builder/src/stores/portal/admin.test.js index 261e94eb6f..8924a5e6fb 100644 --- a/packages/builder/src/stores/portal/admin.test.js +++ b/packages/builder/src/stores/portal/admin.test.js @@ -49,15 +49,6 @@ describe("admin store", () => { ctx.returnedStore = new AdminStore() }) - it("returns the created store", ctx => { - expect(ctx.returnedStore).toEqual({ - subscribe: expect.toBe(ctx.writableReturn.subscribe), - init: expect.toBeFunc(), - unload: expect.toBeFunc(), - getChecklist: expect.toBeFunc(), - }) - }) - describe("init method", () => { beforeEach(async ctx => { let getMockIndex = 0 From 1e0938e93e2650e70764d309a1ba07251e415836 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 16:09:01 +0000 Subject: [PATCH 013/321] Add dummy action buttons for new panels --- .../design/[screenId]/_components/AppPanel.svelte | 12 ++++++++++-- .../[screenId]/_components/BindingsPanel.svelte | 14 ++++++++++++++ .../[screenId]/_components/StatePanel.svelte | 14 ++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte create mode 100644 packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte index 29e2ce03ff..3df2332543 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte @@ -3,6 +3,9 @@ import AppPreview from "./AppPreview.svelte" import { screenStore, appStore } from "@/stores/builder" import UndoRedoControl from "@/components/common/UndoRedoControl.svelte" + import { ActionButton } from "@budibase/bbui" + import BindingsPanel from "./BindingsPanel.svelte" + import StatePanel from "./StatePanel.svelte"
@@ -10,12 +13,14 @@
-
-
{#if $appStore.clientFeatures.devicePreview} {/if}
+
+ + +
{#key $appStore.version} @@ -50,6 +55,9 @@ margin-bottom: 9px; } + .header-left { + display: flex; + } .header-left :global(div) { border-right: none; } diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte new file mode 100644 index 0000000000..73cac27331 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte @@ -0,0 +1,14 @@ + + +Bindings + + + + Some awesome bindings content. + + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte new file mode 100644 index 0000000000..5212f243af --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -0,0 +1,14 @@ + + +State + + + + Some awesome state content. + + From 99537ae93d982b96d53fccb66d368cd026c3ac97 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Tue, 14 Jan 2025 15:06:17 +0000 Subject: [PATCH 014/321] Find components with state vars and allow user to select --- .../[screenId]/_components/StatePanel.svelte | 147 +++++++++++++++++- 1 file changed, 142 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index 5212f243af..a98b03b0c2 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -1,14 +1,151 @@ - State - Some awesome state content. + +
+ +
{@html JSON.stringify(stateValue, null, 2)}
+
+ {#if componentsUsingState.length > 0} +
+

Components using this state:

+ {#each componentsUsingState as component} + + {/each} +
+ {/if}
+ + From cc765ef6b44e38ed5e7f82faae7a886b0726a3c5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 14 Jan 2025 15:44:10 +0000 Subject: [PATCH 015/321] Use global context when requesting client context and no component is selected --- packages/client/src/constants.js | 1 + packages/client/src/index.js | 6 ++++-- packages/client/src/stores/components.js | 3 ++- packages/client/src/stores/screens.js | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/client/src/constants.js b/packages/client/src/constants.js index f7e3e86d40..a3d06fd48f 100644 --- a/packages/client/src/constants.js +++ b/packages/client/src/constants.js @@ -15,6 +15,7 @@ export const ActionTypes = { export const DNDPlaceholderID = "dnd-placeholder" export const ScreenslotType = "screenslot" +export const ScreenslotID = "screenslot" export const GridRowHeight = 24 export const GridColumns = 12 export const GridSpacing = 4 diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 9cef52bb1e..e3f5c34d3e 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -86,8 +86,10 @@ const loadBudibase = async () => { dndStore.actions.reset() } } else if (type === "request-context") { - const { selectedComponentInstance } = get(componentStore) - const context = selectedComponentInstance?.getDataContext() + const { selectedComponentInstance, screenslotInstance } = + get(componentStore) + const instance = selectedComponentInstance || screenslotInstance + const context = instance?.getDataContext() let stringifiedContext = null try { stringifiedContext = JSON.stringify(context) diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index d4afa6c7f1..8a8f82883d 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -6,7 +6,7 @@ import { screenStore } from "./screens" import { builderStore } from "./builder" import Router from "../components/Router.svelte" import * as AppComponents from "../components/app/index.js" -import { ScreenslotType } from "../constants.js" +import { ScreenslotID, ScreenslotType } from "../constants.js" export const BudibasePrefix = "@budibase/standard-components/" @@ -43,6 +43,7 @@ const createComponentStore = () => { selectedComponentDefinition: definition, selectedComponentPath: selectedPath?.map(component => component._id), mountedComponentCount: Object.keys($store.mountedComponents).length, + screenslotInstance: $store.mountedComponents[ScreenslotID], } } ) diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index bc87216660..7261ca375a 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -7,7 +7,7 @@ import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js" import { RoleUtils } from "@budibase/frontend-core" import { findComponentById, findComponentParent } from "../utils/components.js" import { Helpers } from "@budibase/bbui" -import { DNDPlaceholderID } from "constants" +import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "constants" const createScreenStore = () => { const store = derived( @@ -163,8 +163,8 @@ const createScreenStore = () => { _component: "@budibase/standard-components/layout", _children: [ { - _component: "screenslot", - _id: "screenslot", + _component: ScreenslotType, + _id: ScreenslotID, _styles: { normal: { flex: "1 1 auto", From 7bd718b0d9005f4c84a8711de075fda46fb95215 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Wed, 15 Jan 2025 09:53:27 +0000 Subject: [PATCH 016/321] work to highlight setting when selected --- .../[screenId]/_components/StatePanel.svelte | 114 +++++++++--------- 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index a98b03b0c2..d3c579441b 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -1,7 +1,11 @@ State @@ -72,19 +104,15 @@ -
- -
{@html JSON.stringify(stateValue, null, 2)}
-
{#if componentsUsingState.length > 0}

Components using this state:

{#each componentsUsingState as component} {/each}
@@ -93,36 +121,6 @@
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte index 73cac27331..6af802c570 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte @@ -1,14 +1,102 @@ - -Bindings +Bindings - - Some awesome bindings content. + + From 094881e807c011ccfbfdee94334d29e5867d1e74 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Wed, 15 Jan 2025 15:54:40 +0000 Subject: [PATCH 020/321] clear highlight state as part of component select --- .../settings/controls/PropertyControl.svelte | 7 ------- .../[screenId]/_components/StatePanel.svelte | 14 +++----------- packages/builder/src/stores/builder/components.ts | 6 ++++++ 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte index 52bcc7bce2..b3eca2485a 100644 --- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte +++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte @@ -5,7 +5,6 @@ runtimeToReadableBinding, } from "@/dataBinding" import { builderStore } from "@/stores/builder" - import { onDestroy } from "svelte" export let label = "" export let labelHidden = false @@ -79,12 +78,6 @@ ? defaultValue : enriched } - - onDestroy(() => { - if (highlightedSettings) { - builderStore.highlightSetting(null) - } - })
- import { onDestroy } from "svelte" import { ActionButton, Modal, ModalContent, Combobox } from "@budibase/bbui" import { getAllStateVariables } from "@/dataBinding" import { @@ -94,17 +93,10 @@ }) { componentStore.select(component.id) - // Delay highlighting until after component selection and re-render (i know this is disgusting) - setTimeout(() => { - component.settings.forEach(setting => { - builderStore.highlightSetting(setting) - }) - }, 100) + component.settings.forEach(setting => { + builderStore.highlightSetting(setting) + }) } - - onDestroy(() => { - builderStore.highlightSetting(undefined) - }) State diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 9ad9a75f84..450487a52f 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -20,6 +20,7 @@ import { previewStore, tables, componentTreeNodesStore, + builderStore, } from "@/stores/builder" import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding" import { @@ -716,6 +717,11 @@ export class ComponentStore extends BudiStore { * @param {string} componentId */ select(componentId: string) { + builderStore.update(state => { + state.highlightedSettings = null + return state + }) + this.update(state => { state.selectedComponentId = componentId return state From cf291b60be9b099be244c8ee518c6380fda38c95 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 15 Jan 2025 21:08:37 +0000 Subject: [PATCH 021/321] Add style improvements --- .../BindingExplorer/BindingNode.svelte | 60 +++++++++------ .../_components/BindingsPanel.svelte | 77 ++++++++----------- 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingExplorer/BindingNode.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingExplorer/BindingNode.svelte index 155312c03e..2253cc782c 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingExplorer/BindingNode.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingExplorer/BindingNode.svelte @@ -1,5 +1,6 @@
- {#if label} + {#if label != null}
{#if expandable} @@ -107,7 +118,7 @@
{/if} - {#if expandable && (expanded || !label)} + {#if expandable && (expanded || label == null)}
{#each keys as key} - import { ActionButton, Modal, ModalContent } from "@budibase/bbui" + import { ActionButton, Modal, ModalContent, Helpers } from "@budibase/bbui" import { previewStore, selectedScreen, @@ -7,14 +7,8 @@ snippets, } from "@/stores/builder" import { getBindableProperties } from "@/dataBinding" - import { processObjectSync } from "@budibase/string-templates" import BindingNode from "./BindingExplorer/BindingNode.svelte" - - enum ValueType { - Object = "Object", - Array = "Array", - Primitive = "Primitive", - } + import { processObjectSync } from "@budibase/string-templates" // Minimal typing for the real data binding structure, as none exists type DataBinding = { @@ -23,45 +17,21 @@ readableBinding: string } - type BindingEntry = { - readableBinding: string - runtimeBinding: string | null - value: any - valueType: ValueType - } - - type BindingMap = { - [key: string]: BindingEntry - } - let modal: any - $: context = { - ...($previewStore.selectedComponentContext || {}), - date: new Date(), - string: "foo", - number: 1234, - undefined: undefined, - null: null, - true: true, - false: false, - array: [1, 2, 3], - object: { foo: "bar" }, - error: new Error(), - } + $: previewContext = $previewStore.selectedComponentContext || {} $: selectedComponentId = $componentStore.selectedComponentId + $: context = makeContext(previewContext, bindings) $: bindings = getBindableProperties($selectedScreen, selectedComponentId) - $: enrichedBindings = enrichBindings(bindings, context, $snippets) const show = () => { previewStore.requestComponentContext() modal.show() } - const enrichBindings = ( - bindings: DataBinding[], - context: Record, - snippets: any + const makeContext = ( + previewContext: Record, + bindings: DataBinding[] ) => { // Create a single big array to enrich in one go const bindingStrings = bindings.map(binding => { @@ -74,17 +44,36 @@ } }) const bindingEvauations = processObjectSync(bindingStrings, { - ...context, - snippets, + ...previewContext, + snippets: $snippets, }) as any[] // Enrich bindings with evaluations and highlighted HTML - const flatBindings = bindings.map((binding, idx) => ({ - ...binding, - value: bindingEvauations[idx], - })) + const enrichedBindings: any[] = bindings.map((binding, idx) => { + return { + ...binding, + value: bindingEvauations[idx], + } + }) - return flatBindings + let context = { + _dataTypes: { + date: new Date(), + string: "foo", + number: 1234, + undefined: undefined, + null: null, + true: true, + false: false, + array: [1, 2, 3], + object: { foo: "bar" }, + error: new Error(), + }, + } + for (let binding of enrichedBindings) { + Helpers.deepSet(context, binding.readableBinding, binding.value) + } + return context } From 59d71119ad002d8f83fab4e3b015bdccfa9c88bd Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 15 Jan 2025 21:25:48 +0000 Subject: [PATCH 022/321] Test --- asd | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 asd diff --git a/asd b/asd new file mode 100644 index 0000000000..e69de29bb2 From e14bf007dba0f53c5c9b3cf0e1afc21d7604fb3d Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 16 Jan 2025 08:59:04 +0000 Subject: [PATCH 023/321] some tidy up --- packages/bbui/src/Popover/Popover.svelte | 2 +- .../design/[screenId]/_components/StatePanel.svelte | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 5a5b4b2c14..caba27cf25 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -9,7 +9,7 @@ const dispatch = createEventDispatcher() - export let anchor: any | undefined = undefined + export let anchor: HTMLElement | undefined = undefined export let align: string | undefined = "right" export let portalTarget: any | undefined = undefined export let minWidth: number | undefined = undefined diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index 0786c76d20..10ba825423 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -73,7 +73,7 @@ return componentsUsingState } - function handleKeySelect(event: CustomEvent) { + function handleStateKeySelect(event: CustomEvent) { selectedKey = event.detail if (!selectedKey) { throw new Error("No state key selected") @@ -106,7 +106,7 @@ {#if componentsUsingState.length > 0}
From fa01802127dca5346ff2b2aba549d83a6cacab71 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Jan 2025 10:52:13 +0000 Subject: [PATCH 024/321] Update import --- packages/client/src/stores/components.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index 8a8f82883d..024e7b4edc 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -6,7 +6,7 @@ import { screenStore } from "./screens" import { builderStore } from "./builder" import Router from "../components/Router.svelte" import * as AppComponents from "../components/app/index.js" -import { ScreenslotID, ScreenslotType } from "../constants.js" +import { ScreenslotID, ScreenslotType } from "../constants" export const BudibasePrefix = "@budibase/standard-components/" From 5acd6ab3d60ae2451f71590153b934b776ca0ff9 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Jan 2025 12:19:45 +0000 Subject: [PATCH 025/321] Add hover state and expansion to binding labels --- .../BindingExplorer/BindingNode.svelte | 34 +++++++++++++------ .../_components/BindingsPanel.svelte | 4 +-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingExplorer/BindingNode.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingExplorer/BindingNode.svelte index 2253cc782c..2c4c5844fb 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingExplorer/BindingNode.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingExplorer/BindingNode.svelte @@ -1,9 +1,8 @@ + + + + +
{#if label != null}
-
+
{#if expandable}
(expanded = !expanded)} > {label}
- (valueExpanded = !valueExpanded)} > -
- {displayValue} -
-
+ {displayValue} +
{/if} {#if expandable && (expanded || label == null)} @@ -150,24 +156,33 @@ overflow: hidden; } .binding-arrow { - margin-right: 2px; + margin: -3px 2px -2px 0; flex: 0 0 18px; + transition: transform 130ms ease-out; + } + .binding-arrow :global(svg) { + width: 9px; + } + .binding-arrow.expanded { + transform: rotate(90deg); } .binding-text { display: flex; flex-direction: row; - align-items: center; font-family: monospace; font-size: 12px; + align-items: flex-start; width: 100%; } .binding-children { display: flex; flex-direction: column; gap: 8px; - /* border-left: 1px solid var(--spectrum-global-color-gray-400); */ - /* margin-left: 20px; */ - padding-left: 18px; + /*padding-left: 18px;*/ + + border-left: 1px solid var(--spectrum-global-color-gray-400); + margin-left: 20px; + padding-left: 3px; } .binding-children.root { border-left: none; @@ -176,31 +191,40 @@ } /* Size label and value according to type */ - .binding-label, - .binding-value { + .binding-label { + flex: 0 1 auto; + margin-right: 8px; + transition: color 130ms ease-out; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } - .binding-label { - flex: 0 0 auto; - max-width: 50%; - margin-right: 8px; - transition: color 130ms ease-out; - } .binding-label.expandable:hover { cursor: pointer; color: var(--spectrum-global-color-gray-900); } .binding-value { - flex: 0 1 auto; - } - .binding-label.expandable { - flex: 0 1 auto; - max-width: none; - } - .binding-value.expandable { flex: 0 0 auto; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + transition: filter 130ms ease-out; + } + .binding-value.primitive:hover { + filter: brightness(1.25); + cursor: pointer; + } + .binding-value.expanded { + word-break: break-all; + white-space: wrap; + } + + .binding-label.primitive { + flex: 0 0 auto; + max-width: 50%; + } + .binding-value.primitive { + flex: 0 1 auto; } /* Trim spans in the highlighted HTML */ diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte index b7bc394539..1da4c42ac6 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/BindingsPanel.svelte @@ -1,5 +1,5 @@ -Bindings - - - + + + + Data context + + +
- - +
+
+ + From 3cdcc44cbeb74651b59b10d968204646315e7426 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Fri, 17 Jan 2025 10:10:52 +0000 Subject: [PATCH 028/321] further popover changes / ts updates --- packages/bbui/src/Form/Combobox.svelte | 21 +-- packages/bbui/src/Form/Core/Combobox.svelte | 14 +- packages/bbui/src/Icon/Icon.svelte | 31 ++-- .../[screenId]/_components/StatePanel.svelte | 162 +++++++++++------- 4 files changed, 134 insertions(+), 94 deletions(-) diff --git a/packages/bbui/src/Form/Combobox.svelte b/packages/bbui/src/Form/Combobox.svelte index 18a2359c30..44854d949e 100644 --- a/packages/bbui/src/Form/Combobox.svelte +++ b/packages/bbui/src/Form/Combobox.svelte @@ -1,17 +1,17 @@ - - import { ActionButton, Modal, ModalContent, Combobox } from "@budibase/bbui" + import { ActionButton, Popover, Divider, Icon, Select } from "@budibase/bbui" import { getAllStateVariables } from "@/dataBinding" import { componentStore, @@ -11,25 +11,33 @@ findHBSBlocks, isJSBinding, } from "@budibase/string-templates" + import { onMount } from "svelte" type ComponentUsingState = { id: string name: string settings: string[] - }[] - - let modal: Modal - let selectedKey: string | null = null - let componentsUsingState: ComponentUsingState = [] - let componentsUpdatingState: ComponentUsingState = [] + } const keyOptions = getAllStateVariables() + let popoverAnchor: any + let popover: any + let selectedKey: string | null + let componentsUsingState: ComponentUsingState[] = [] + let componentsUpdatingState: ComponentUsingState[] = [] + + onMount(() => { + if (selectedKey) { + searchComponents(selectedKey) + } + }) + function findComponentsUpdatingState( component: any, stateKey: string - ): ComponentUsingState { - let foundComponents: ComponentUsingState = [] + ): ComponentUsingState[] { + let foundComponents: ComponentUsingState[] = [] const eventHandlerProps = ["onClick", "onChange"] @@ -66,12 +74,8 @@ function findComponentsUsingState( component: any, stateKey: string - ): Array<{ id: string; name: string; settings: string[] }> { - let componentsUsingState: Array<{ - id: string - name: string - settings: string[] - }> = [] + ): ComponentUsingState[] { + let componentsUsingState: ComponentUsingState[] = [] const { _children, ...componentSettings } = component let settingsWithState: string[] = [] @@ -114,61 +118,83 @@ return componentsUsingState } + function searchComponents(stateKey: string) { + if (!stateKey || !$selectedScreen?.props) { + return + } + componentsUsingState = findComponentsUsingState( + $selectedScreen.props, + stateKey + ) + componentsUpdatingState = findComponentsUpdatingState( + $selectedScreen.props, + stateKey + ) + } + function handleStateKeySelect(event: CustomEvent) { selectedKey = event.detail if (!selectedKey) { throw new Error("No state key selected") } - if ($selectedScreen?.props) { - componentsUsingState = findComponentsUsingState( - $selectedScreen.props, - selectedKey - ) - componentsUpdatingState = findComponentsUpdatingState( - $selectedScreen.props, - selectedKey - ) - } + searchComponents(selectedKey) } - function onClickComponentLink(component: { - id: string - name: string - settings: string[] - }) { + function onClickComponentLink(component: ComponentUsingState) { componentStore.select(component.id) component.settings.forEach(setting => { builderStore.highlightSetting(setting) }) + popover.hide() } -State +
+ State +
+ + +
+
+
State
+
+ +
+
+
+ Showing state variables for {$selectedScreen?.routing?.route.split( + "/" + )[1]}: +
+
+ -
- -
state inspector here
- - - {#if componentsUsingState.length > 0} -
- Updates: - {#each componentsUsingState as component, i} - - {/each} -
- {/if} - {#if componentsUpdatingState.length > 0} -
- Updated by: - {#each componentsUpdatingState as component, i} - - {/each} -
- {/if} +
+
State
+
+ Showing state variables for {$selectedScreen?.routing?.route.split("/")[1]}:
- +
+ + {#if editorError} +
{editorError}
+ {/if}
@@ -211,29 +223,26 @@
Updates: {#each componentsUsingState as component, i} + {#if i > 0}{", "}{/if} {/each}
{/if} {#if componentsUpdatingState.length > 0}
- Updated by: + Controlled by: {#each componentsUpdatingState as component, i} + {#if i > 0}{", "}{/if} {/each}
@@ -255,6 +264,14 @@ color: var(--spectrum-global-color-gray-700); font-size: var(--spectrum-global-dimension-font-size-75); } + .error { + color: var( + --spectrum-semantic-negative-color-default, + var(--spectrum-global-color-red-500) + ); + font-size: var(--spectrum-global-dimension-font-size-75); + margin-top: var(--spectrum-global-dimension-size-75); + } .component-link { display: inline; border: none; @@ -268,10 +285,4 @@ .component-link:hover { text-decoration: underline; } - - .split-title { - display: flex; - justify-content: space-between; - align-items: center; - } From c199b681e3308c0233cf3d000b526383a1200a22 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Tue, 21 Jan 2025 16:04:14 +0000 Subject: [PATCH 080/321] some duplicate code --- .../design/[screenId]/_components/StatePanel.svelte | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index 99f4b252a2..cf6cd12d91 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -178,10 +178,6 @@ editorError = null previewStore.updateState(stateUpdate) - previewStore.updateState(stateUpdate) - - previewStore.updateState(stateUpdate) - previewStore.setSelectedComponentContext({ ...$previewStore.selectedComponentContext, state: stateUpdate, From 221021ae9b8051e3ca3a9257ef00c8bf33a0e08e Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 21 Jan 2025 17:00:40 +0000 Subject: [PATCH 081/321] Support for our own vm-browserify implementation which re-uses the iframe for running JS in the frontend, to improve performance. --- packages/string-templates/package.json | 1 + packages/string-templates/src/index.ts | 6 +++--- yarn.lock | 18 +++++++++++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 1d7a4507ab..74d9aaa85a 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@budibase/handlebars-helpers": "^0.13.2", + "@budibase/vm-browserify": "^1.1.3", "dayjs": "^1.10.8", "handlebars": "^4.7.8", "lodash.clonedeep": "^4.5.0" diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 8dda8b71ab..bd008dd4d2 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -1,4 +1,4 @@ -import { createContext, runInNewContext } from "vm" +import vm from "@budibase/vm-browserify" import { create, TemplateDelegate } from "handlebars" import { registerAll, registerMinimum } from "./helpers/index" import { postprocess, postprocessWithLogs, preprocess } from "./processors" @@ -511,11 +511,11 @@ export function browserJSSetup() { * Use polyfilled vm to run JS scripts in a browser Env */ setJSRunner((js: string, context: Record) => { - createContext(context) + vm.createContext(context) const wrappedJs = frontendWrapJS(js) - const result = runInNewContext(wrappedJs, context, { timeout: 1000 }) + const result = vm.runInNewContext(wrappedJs, context) if (result.error) { throw new UserScriptError(result.error) } diff --git a/yarn.lock b/yarn.lock index 453dc45128..e25ff97747 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2131,9 +2131,9 @@ through2 "^2.0.0" "@budibase/pro@npm:@budibase/pro@latest": - version "3.2.44" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.44.tgz#90367bb2167aafd8c809e000a57d349e5dc4bb78" - integrity sha512-Zv2PBVUZUS6/psOpIRIDlW3jrOHWWPhpQXzCk00kIQJaqjkdcvuTXSedQ70u537sQmLu8JsSWbui9MdfF8ksVw== + version "3.2.47" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.47.tgz#150d7b16b14932d03c84bdb0e6d570d490c28a5c" + integrity sha512-UeTIq7yzMUK6w/akUsRafoD/Kif6PXv4d7K1arn8GTMjwFm9QYu2hg1YkQ+duNdwyZ/GEPlEAV5SYK+NDgtpdA== dependencies: "@anthropic-ai/sdk" "^0.27.3" "@budibase/backend-core" "*" @@ -2152,6 +2152,13 @@ scim-patch "^0.8.1" scim2-parse-filter "^0.2.8" +"@budibase/vm-browserify@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@budibase/vm-browserify/-/vm-browserify-1.1.3.tgz#63f7917671a0f0cb760e3aa37cfd5dfa32e997ed" + integrity sha512-CuoNb2xwS8TT2ZfG9YqC8QCTcG3ZPLwH4m00sfPDluwmdp3U3HGg/UKWRIqKC6Wv8Mywy1q6bxmSx6Vf40V52w== + dependencies: + indexof "^0.0.1" + "@bull-board/api@5.10.2": version "5.10.2" resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-5.10.2.tgz#ae8ff6918b23897bf879a6ead3683f964374c4b3" @@ -11925,6 +11932,11 @@ indexes-of@^1.0.1: resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" integrity sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA== +indexof@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg== + infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" From 0e6ad4db930d7a50cd23264e9a2be3ef6c853585 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 21 Jan 2025 17:06:09 +0000 Subject: [PATCH 082/321] Remove testStep endpoint and fix types. --- .../server/src/api/controllers/automation.ts | 52 +------------------ packages/server/src/api/routes/automation.ts | 12 +---- .../server/src/automations/steps/createRow.ts | 9 ++-- .../server/src/automations/steps/deleteRow.ts | 9 ++-- .../src/automations/steps/executeQuery.ts | 4 +- .../src/automations/steps/executeScript.ts | 4 +- .../server/src/automations/steps/updateRow.ts | 9 ++-- .../server/src/automations/steps/utils.ts | 4 +- packages/server/src/events/NoopEmitter.ts | 39 -------------- packages/server/src/events/index.ts | 1 - .../src/tests/utilities/api/automation.ts | 17 ------ packages/types/src/api/web/app/automation.ts | 7 --- 12 files changed, 26 insertions(+), 141 deletions(-) delete mode 100644 packages/server/src/events/NoopEmitter.ts diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index a77014cf31..13d057ebb7 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -28,18 +28,11 @@ import { TriggerAutomationResponse, TestAutomationRequest, TestAutomationResponse, - TestAutomationStepRequest, - TestAutomationStepResponse, } from "@budibase/types" -import { - getActionDefinitions as actionDefs, - getAction, -} from "../../automations/actions" +import { getActionDefinitions as actionDefs } from "../../automations/actions" import sdk from "../../sdk" import { builderSocket } from "../../websockets" import env from "../../environment" -import { NoopEmitter } from "../../events" -import { enrichBaseContext } from "../../threads/automation" async function getActionDefinitions() { return removeDeprecated(await actionDefs()) @@ -260,46 +253,3 @@ export async function test( await events.automation.tested(automation) } - -export async function testStep( - ctx: UserCtx -) { - const { id, stepId } = ctx.params - const db = context.getAppDB() - const automation = await db.tryGet(id) - if (!automation) { - ctx.throw(404, `Automation ${ctx.params.id} not found`) - } - - const step = automation.definition.steps.find(s => s.stepId === stepId) - if (!step) { - ctx.throw(404, `Step ${stepId} not found on automation ${id}`) - } - - if (step.stepId === AutomationActionStepId.BRANCH) { - ctx.throw(400, "Branch steps cannot be tested directly") - } - if (step.stepId === AutomationActionStepId.LOOP) { - ctx.throw(400, "Loop steps cannot be tested directly") - } - - const { body } = ctx.request - - const fn = await getAction(step.stepId) - if (!fn) { - ctx.throw(400, `Step ${stepId} is not a valid step`) - } - - const output = await withTestFlag( - automation._id!, - async () => - await fn({ - inputs: body.inputs, - context: await enrichBaseContext(body.context), - appId: ctx.appId, - emitter: new NoopEmitter(), - }) - ) - - ctx.body = output -} diff --git a/packages/server/src/api/routes/automation.ts b/packages/server/src/api/routes/automation.ts index ea905be0cd..489487271c 100644 --- a/packages/server/src/api/routes/automation.ts +++ b/packages/server/src/api/routes/automation.ts @@ -1,6 +1,6 @@ import Router from "@koa/router" import * as controller from "../controllers/automation" -import authorized, { authorizedResource } from "../../middleware/authorized" +import authorized from "../../middleware/authorized" import { permissions } from "@budibase/backend-core" import { bodyResource, paramResource } from "../../middleware/resourceId" import { @@ -82,15 +82,5 @@ router ), controller.test ) - .post( - "/api/automations/:id/step/:stepId/test", - appInfoMiddleware({ appType: AppType.DEV }), - authorizedResource( - permissions.PermissionType.AUTOMATION, - permissions.PermissionLevel.EXECUTE, - "id" - ), - controller.testStep - ) export default router diff --git a/packages/server/src/automations/steps/createRow.ts b/packages/server/src/automations/steps/createRow.ts index 24dada422d..cf915dd300 100644 --- a/packages/server/src/automations/steps/createRow.ts +++ b/packages/server/src/automations/steps/createRow.ts @@ -5,8 +5,11 @@ import { sendAutomationAttachmentsToStorage, } from "../automationUtils" import { buildCtx } from "./utils" -import { CreateRowStepInputs, CreateRowStepOutputs } from "@budibase/types" -import { EventEmitter } from "events" +import { + ContextEmitter, + CreateRowStepInputs, + CreateRowStepOutputs, +} from "@budibase/types" export async function run({ inputs, @@ -15,7 +18,7 @@ export async function run({ }: { inputs: CreateRowStepInputs appId: string - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.row == null || inputs.row.tableId == null) { return { diff --git a/packages/server/src/automations/steps/deleteRow.ts b/packages/server/src/automations/steps/deleteRow.ts index 7c50fe4dcb..2498a4e4de 100644 --- a/packages/server/src/automations/steps/deleteRow.ts +++ b/packages/server/src/automations/steps/deleteRow.ts @@ -1,8 +1,11 @@ -import { EventEmitter } from "events" import { destroy } from "../../api/controllers/row" import { buildCtx } from "./utils" import { getError } from "../automationUtils" -import { DeleteRowStepInputs, DeleteRowStepOutputs } from "@budibase/types" +import { + ContextEmitter, + DeleteRowStepInputs, + DeleteRowStepOutputs, +} from "@budibase/types" export async function run({ inputs, @@ -11,7 +14,7 @@ export async function run({ }: { inputs: DeleteRowStepInputs appId: string - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.id == null) { return { diff --git a/packages/server/src/automations/steps/executeQuery.ts b/packages/server/src/automations/steps/executeQuery.ts index 9816e31b1e..ad99240eb8 100644 --- a/packages/server/src/automations/steps/executeQuery.ts +++ b/packages/server/src/automations/steps/executeQuery.ts @@ -1,8 +1,8 @@ -import { EventEmitter } from "events" import * as queryController from "../../api/controllers/query" import { buildCtx } from "./utils" import * as automationUtils from "../automationUtils" import { + ContextEmitter, ExecuteQueryStepInputs, ExecuteQueryStepOutputs, } from "@budibase/types" @@ -14,7 +14,7 @@ export async function run({ }: { inputs: ExecuteQueryStepInputs appId: string - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.query == null) { return { diff --git a/packages/server/src/automations/steps/executeScript.ts b/packages/server/src/automations/steps/executeScript.ts index 105543d34c..db05d0937a 100644 --- a/packages/server/src/automations/steps/executeScript.ts +++ b/packages/server/src/automations/steps/executeScript.ts @@ -2,10 +2,10 @@ import * as scriptController from "../../api/controllers/script" import { buildCtx } from "./utils" import * as automationUtils from "../automationUtils" import { + ContextEmitter, ExecuteScriptStepInputs, ExecuteScriptStepOutputs, } from "@budibase/types" -import { EventEmitter } from "events" export async function run({ inputs, @@ -16,7 +16,7 @@ export async function run({ inputs: ExecuteScriptStepInputs appId: string context: object - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.code == null) { return { diff --git a/packages/server/src/automations/steps/updateRow.ts b/packages/server/src/automations/steps/updateRow.ts index 46ae2a5c74..7a62e40706 100644 --- a/packages/server/src/automations/steps/updateRow.ts +++ b/packages/server/src/automations/steps/updateRow.ts @@ -1,8 +1,11 @@ -import { EventEmitter } from "events" import * as rowController from "../../api/controllers/row" import * as automationUtils from "../automationUtils" import { buildCtx } from "./utils" -import { UpdateRowStepInputs, UpdateRowStepOutputs } from "@budibase/types" +import { + ContextEmitter, + UpdateRowStepInputs, + UpdateRowStepOutputs, +} from "@budibase/types" export async function run({ inputs, @@ -11,7 +14,7 @@ export async function run({ }: { inputs: UpdateRowStepInputs appId: string - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.rowId == null || inputs.row == null) { return { diff --git a/packages/server/src/automations/steps/utils.ts b/packages/server/src/automations/steps/utils.ts index 8b99044303..20f1e67589 100644 --- a/packages/server/src/automations/steps/utils.ts +++ b/packages/server/src/automations/steps/utils.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from "events" +import { ContextEmitter } from "@budibase/types" export async function getFetchResponse(fetched: any) { let status = fetched.status, @@ -22,7 +22,7 @@ export async function getFetchResponse(fetched: any) { // opts can contain, body, params and version export function buildCtx( appId: string, - emitter?: EventEmitter | null, + emitter?: ContextEmitter | null, opts: any = {} ) { const ctx: any = { diff --git a/packages/server/src/events/NoopEmitter.ts b/packages/server/src/events/NoopEmitter.ts deleted file mode 100644 index ed87618ead..0000000000 --- a/packages/server/src/events/NoopEmitter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { EventEmitter } from "events" -import { - Table, - Row, - ContextEmitter, - EventType, - UserBindings, -} from "@budibase/types" - -export class NoopEmitter extends EventEmitter implements ContextEmitter { - emitRow(values: { - eventName: EventType.ROW_SAVE - appId: string - row: Row - table: Table - user: UserBindings - }): void - emitRow(values: { - eventName: EventType.ROW_UPDATE - appId: string - row: Row - table: Table - oldRow: Row - user: UserBindings - }): void - emitRow(values: { - eventName: EventType.ROW_DELETE - appId: string - row: Row - user: UserBindings - }): void - emitRow(_values: unknown): void { - return - } - - emitTable(_eventName: string, _appId: string, _table?: Table) { - return - } -} diff --git a/packages/server/src/events/index.ts b/packages/server/src/events/index.ts index 90bf932bcf..23c3f3e512 100644 --- a/packages/server/src/events/index.ts +++ b/packages/server/src/events/index.ts @@ -2,6 +2,5 @@ import BudibaseEmitter from "./BudibaseEmitter" const emitter = new BudibaseEmitter() -export { NoopEmitter } from "./NoopEmitter" export { init } from "./docUpdates" export default emitter diff --git a/packages/server/src/tests/utilities/api/automation.ts b/packages/server/src/tests/utilities/api/automation.ts index 6041664999..3f51385251 100644 --- a/packages/server/src/tests/utilities/api/automation.ts +++ b/packages/server/src/tests/utilities/api/automation.ts @@ -3,8 +3,6 @@ import { FetchAutomationResponse, TestAutomationRequest, TestAutomationResponse, - TestAutomationStepRequest, - TestAutomationStepResponse, } from "@budibase/types" import { Expectations, TestAPI } from "./base" @@ -54,19 +52,4 @@ export class AutomationAPI extends TestAPI { } ) } - - testStep = async ( - id: string, - stepId: string, - body: TestAutomationStepRequest, - expectations?: Expectations - ): Promise => { - return await this._post( - `/api/automations/${id}/steps/${stepId}/test`, - { - body, - expectations, - } - ) - } } diff --git a/packages/types/src/api/web/app/automation.ts b/packages/types/src/api/web/app/automation.ts index edff4b5eaf..b97dee0baf 100644 --- a/packages/types/src/api/web/app/automation.ts +++ b/packages/types/src/api/web/app/automation.ts @@ -83,10 +83,3 @@ export function isDidNotTriggerResponse( ): response is DidNotTriggerResponse { return !!("message" in response && response.message) } - -export interface TestAutomationStepRequest { - inputs: Record - context: Record -} - -export type TestAutomationStepResponse = any From cad6a08bf87570c2485cafc9538460a2cfee8f7e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 22 Jan 2025 11:14:50 +0100 Subject: [PATCH 083/321] Move reactiviness --- packages/client/src/components/Component.svelte | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 7f825c0601..57daf8cd12 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -139,6 +139,7 @@ // Derive definition properties which can all be optional, so need to be // coerced to booleans + $: invalidSettings = $componentErrors[instance._id] $: hasChildren = !!definition?.hasChildren $: showEmptyState = definition?.showEmptyState !== false $: hasMissingRequiredSettings = missingRequiredSettings?.length > 0 @@ -374,9 +375,6 @@ } } - // Check for invalid settings - $: invalidSettings = $componentErrors[id] - // Extracts a map of all context keys which are required by action settings // to provide the functions to evaluate at runtime. This needs done manually // as the action definitions themselves do not specify bindings for action From 4d85006c35e4d9b2643192902d077147069c6de8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 22 Jan 2025 11:53:50 +0100 Subject: [PATCH 084/321] server nodemon, watch .hbs --- packages/server/nodemon.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/nodemon.json b/packages/server/nodemon.json index ac8c38ccb2..9871b9339e 100644 --- a/packages/server/nodemon.json +++ b/packages/server/nodemon.json @@ -7,7 +7,7 @@ "../shared-core/src", "../string-templates/src" ], - "ext": "js,ts,json,svelte", + "ext": "js,ts,json,svelte,hbs", "ignore": [ "**/*.spec.ts", "**/*.spec.js", From 1294f83ccb9d80f7bcacaf89e9cab7b43f6c4d70 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 22 Jan 2025 11:09:31 +0000 Subject: [PATCH 085/321] Bump version to 3.3.0 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 50582f0a95..a7d534ff01 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.47", + "version": "3.3.0", "npmClient": "yarn", "concurrency": 20, "command": { From 2213cd56c46c8f8204c98cd961dbd39482b57271 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 22 Jan 2025 12:18:55 +0100 Subject: [PATCH 086/321] Remove unnecessary derived --- packages/client/src/components/Component.svelte | 3 +-- packages/client/src/stores/derived/componentErrors.ts | 6 ------ packages/client/src/stores/derived/index.js | 1 - 3 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 packages/client/src/stores/derived/componentErrors.ts diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 57daf8cd12..8d79de2ac4 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -23,7 +23,6 @@ appStore, dndComponentPath, dndIsDragging, - componentErrors, } from "stores" import { Helpers } from "@budibase/bbui" import { getActiveConditions, reduceConditionActions } from "utils/conditions" @@ -139,7 +138,7 @@ // Derive definition properties which can all be optional, so need to be // coerced to booleans - $: invalidSettings = $componentErrors[instance._id] + $: invalidSettings = $builderStore.componentErrors[instance._id] $: hasChildren = !!definition?.hasChildren $: showEmptyState = definition?.showEmptyState !== false $: hasMissingRequiredSettings = missingRequiredSettings?.length > 0 diff --git a/packages/client/src/stores/derived/componentErrors.ts b/packages/client/src/stores/derived/componentErrors.ts deleted file mode 100644 index 68e87a061d..0000000000 --- a/packages/client/src/stores/derived/componentErrors.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { derived } from "svelte/store" -import { builderStore } from "../builder.js" - -export const componentErrors = derived([builderStore], ([$builderStore]) => { - return $builderStore.componentErrors -}) diff --git a/packages/client/src/stores/derived/index.js b/packages/client/src/stores/derived/index.js index e7e70d8952..337c73831f 100644 --- a/packages/client/src/stores/derived/index.js +++ b/packages/client/src/stores/derived/index.js @@ -5,4 +5,3 @@ export { currentRole } from "./currentRole.js" export { dndComponentPath } from "./dndComponentPath.js" export { devToolsEnabled } from "./devToolsEnabled.js" export { snippets } from "./snippets.js" -export { componentErrors } from "./componentErrors" From d3b22e461e3f0f328bab54e4b2b58eb6d833e28e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 22 Jan 2025 16:18:29 +0100 Subject: [PATCH 087/321] Handle errors as a part of the instance to avoid extra refreshes --- packages/client/src/components/Component.svelte | 2 +- packages/client/src/stores/screens.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 8d79de2ac4..a86d43c676 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -138,7 +138,7 @@ // Derive definition properties which can all be optional, so need to be // coerced to booleans - $: invalidSettings = $builderStore.componentErrors[instance._id] + $: invalidSettings = instance?._meta?.errors $: hasChildren = !!definition?.hasChildren $: showEmptyState = definition?.showEmptyState !== false $: hasMissingRequiredSettings = missingRequiredSettings?.length > 0 diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index bc87216660..491d8d4236 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -42,6 +42,14 @@ const createScreenStore = () => { if ($builderStore.layout) { activeLayout = $builderStore.layout } + + // Attach meta + const errors = $builderStore.componentErrors || {} + const attachComponentMeta = component => { + component._meta = { errors: errors[component._id] || [] } + component._children?.forEach(attachComponentMeta) + } + attachComponentMeta(activeScreen.props) } else { // Find the correct screen by matching the current route screens = $appStore.screens || [] From 84c8507bad7e3ff8df572cf389e004f546be264b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 22 Jan 2025 16:20:33 +0100 Subject: [PATCH 088/321] Renames --- packages/client/src/components/Component.svelte | 10 +++++----- .../components/error-states/ComponentErrorState.svelte | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index a86d43c676..236bcb7c7e 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -103,7 +103,7 @@ let settingsDefinition let settingsDefinitionMap let missingRequiredSettings = false - let invalidSettings = false + let componentErrors = false // Temporary styles which can be added in the app preview for things like // DND. We clear these whenever a new instance is received. @@ -138,12 +138,12 @@ // Derive definition properties which can all be optional, so need to be // coerced to booleans - $: invalidSettings = instance?._meta?.errors + $: componentErrors = instance?._meta?.errors $: hasChildren = !!definition?.hasChildren $: showEmptyState = definition?.showEmptyState !== false $: hasMissingRequiredSettings = missingRequiredSettings?.length > 0 $: editable = !!definition?.editable && !hasMissingRequiredSettings - $: hasInvalidSettings = invalidSettings?.length > 0 + $: hasComponentErrors = componentErrors?.length > 0 $: requiredAncestors = definition?.requiredAncestors || [] $: missingRequiredAncestors = requiredAncestors.filter( ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`) @@ -152,7 +152,7 @@ $: errorState = hasMissingRequiredSettings || hasMissingRequiredAncestors || - hasInvalidSettings + hasComponentErrors // Interactive components can be selected, dragged and highlighted inside // the builder preview @@ -698,7 +698,7 @@ {:else} diff --git a/packages/client/src/components/error-states/ComponentErrorState.svelte b/packages/client/src/components/error-states/ComponentErrorState.svelte index 8cf1ad6dfb..9eace07018 100644 --- a/packages/client/src/components/error-states/ComponentErrorState.svelte +++ b/packages/client/src/components/error-states/ComponentErrorState.svelte @@ -8,7 +8,7 @@ | { key: string; label: string }[] | undefined export let missingRequiredAncestors: string[] | undefined - export let invalidSettings: string[] | undefined + export let componentErrors: string[] | undefined const component = getContext("component") const { styleable, builderStore } = getContext("sdk") @@ -16,7 +16,7 @@ $: styles = { ...$component.styles, normal: {}, custom: null, empty: true } $: requiredSetting = missingRequiredSettings?.[0] $: requiredAncestor = missingRequiredAncestors?.[0] - $: invalidSetting = invalidSettings?.[0] + $: errorMessage = componentErrors?.[0] {#if $builderStore.inBuilder} @@ -25,8 +25,8 @@ {#if requiredAncestor} - {:else if invalidSetting} - {invalidSetting} + {:else if errorMessage} + {errorMessage} {:else if requiredSetting} {/if} From c9feae9665a33466e9486c7ca97a2f42b067142c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 22 Jan 2025 16:26:09 +0100 Subject: [PATCH 089/321] Simplify derived screenComponentErrors --- .../[screenId]/_components/AppPreview.svelte | 4 +- packages/builder/src/stores/builder/index.js | 4 +- .../src/stores/builder/screenComponent.ts | 112 +++++++----------- 3 files changed, 49 insertions(+), 71 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index fc0b67f63d..3951c0e902 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -11,7 +11,7 @@ selectedScreen, hoverStore, componentTreeNodesStore, - screenComponentStore, + screenComponentErrors, snippets, } from "@/stores/builder" import ConfirmDialog from "@/components/common/ConfirmDialog.svelte" @@ -69,7 +69,7 @@ port: window.location.port, }, snippets: $snippets, - componentErrors: $screenComponentStore.errors, + componentErrors: $screenComponentErrors, } // Refresh the preview when required diff --git a/packages/builder/src/stores/builder/index.js b/packages/builder/src/stores/builder/index.js index 23491996d1..892b72c7ab 100644 --- a/packages/builder/src/stores/builder/index.js +++ b/packages/builder/src/stores/builder/index.js @@ -16,7 +16,7 @@ import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js" import { deploymentStore } from "./deployments.js" import { contextMenuStore } from "./contextMenu.js" import { snippets } from "./snippets" -import { screenComponentStore } from "./screenComponent" +import { screenComponentErrors } from "./screenComponent" // Backend import { tables } from "./tables" @@ -68,7 +68,7 @@ export { snippets, rowActions, appPublished, - screenComponentStore, + screenComponentErrors, } export const reset = () => { diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index b4eb01a3b7..19bafeade3 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -2,79 +2,57 @@ import { derived } from "svelte/store" import { tables } from "./tables" import { selectedScreen } from "./screens" import { viewsV2 } from "./viewsV2" -import { DerivedBudiStore } from "../BudiStore" import { findComponentsBySettingsType } from "@/helpers/screen" import { Screen, Table, ViewV2 } from "@budibase/types" -interface BuilderScreenComponentStore {} +export const screenComponentErrors = derived( + [selectedScreen, tables, viewsV2], + ([$selectedScreen, $tables, $viewsV2]): Record => { + function flattenTablesAndViews(tables: Table[], views: ViewV2[]) { + return { + ...tables.reduce( + (list, table) => ({ + ...list, + [table._id!]: table, + }), + {} + ), + ...views.reduce( + (list, view) => ({ + ...list, + [view.id]: view, + }), + {} + ), + } + } -interface DerivedScreenComponentStore extends BuilderScreenComponentStore { - errors: Record -} + function getInvalidDatasources( + screen: Screen, + datasources: Record + ) { + const friendlyNameByType = { + table: "table", + view: "view", + viewV2: "view", + } -export class ScreenComponentStore extends DerivedBudiStore< - BuilderScreenComponentStore, - DerivedScreenComponentStore -> { - constructor() { - const makeDerivedStore = () => { - return derived( - [selectedScreen, tables, viewsV2], - ([$selectedScreen, $tables, $viewsV2]): DerivedScreenComponentStore => { - const datasources = flattenTablesAndViews($tables.list, $viewsV2.list) - return { - errors: getInvalidDatasources($selectedScreen, datasources), - } + const result: Record = {} + for (const component of findComponentsBySettingsType(screen, "table")) { + const { resourceId, type, label } = component.dataSource + if (!datasources[resourceId]) { + const friendlyTypeName = + friendlyNameByType[type as keyof typeof friendlyNameByType] + result[component._id!] = [ + `The ${friendlyTypeName} named "${label}" does not exist`, + ] } - ) + } + + return result } - super({}, makeDerivedStore) + const datasources = flattenTablesAndViews($tables.list, $viewsV2.list) + return getInvalidDatasources($selectedScreen, datasources) } -} - -export const screenComponentStore = new ScreenComponentStore() - -function flattenTablesAndViews(tables: Table[], views: ViewV2[]) { - return { - ...tables.reduce( - (list, table) => ({ - ...list, - [table._id!]: table, - }), - {} - ), - ...views.reduce( - (list, view) => ({ - ...list, - [view.id]: view, - }), - {} - ), - } -} - -function getInvalidDatasources( - screen: Screen, - datasources: Record -) { - const friendlyNameByType = { - table: "table", - view: "view", - viewV2: "view", - } - - const result: Record = {} - for (const component of findComponentsBySettingsType(screen, "table")) { - const { resourceId, type, label } = component.dataSource - if (!datasources[resourceId]) { - const friendlyTypeName = - friendlyNameByType[type as keyof typeof friendlyNameByType] - result[component._id!] = [ - `The ${friendlyTypeName} named "${label}" does not exist`, - ] - } - } - - return result -} +) From 2a5865ecaf6eb9de7a5a6e808bb0580e792ae630 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 22 Jan 2025 16:38:29 +0100 Subject: [PATCH 090/321] Fix creating new table screen modal --- packages/builder/src/helpers/data/format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/helpers/data/format.js b/packages/builder/src/helpers/data/format.js index 428ce273b2..ba274d5464 100644 --- a/packages/builder/src/helpers/data/format.js +++ b/packages/builder/src/helpers/data/format.js @@ -11,7 +11,7 @@ export const datasourceSelect = { }, viewV2: (view, datasources) => { const datasource = datasources - .filter(f => f.entities) + ?.filter(f => f.entities) .flatMap(d => d.entities) .find(ds => ds._id === view.tableId) return { From abc1ba33356b3a2a8cb67ca586e53c8e712d74dd Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 22 Jan 2025 15:46:49 +0000 Subject: [PATCH 091/321] Bump version to 3.3.1 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index a7d534ff01..13040cb50c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.3.0", + "version": "3.3.1", "npmClient": "yarn", "concurrency": 20, "command": { From fbbe1738db7a4006e7bbe8dfea73de26e7b4c33d Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 22 Jan 2025 16:11:54 +0000 Subject: [PATCH 092/321] Using sandboxed iframe to limit to scripting only. --- packages/string-templates/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 74d9aaa85a..b1b4b9ef55 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@budibase/handlebars-helpers": "^0.13.2", - "@budibase/vm-browserify": "^1.1.3", + "@budibase/vm-browserify": "^1.1.4", "dayjs": "^1.10.8", "handlebars": "^4.7.8", "lodash.clonedeep": "^4.5.0" diff --git a/yarn.lock b/yarn.lock index e25ff97747..a375c05ffd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2152,10 +2152,10 @@ scim-patch "^0.8.1" scim2-parse-filter "^0.2.8" -"@budibase/vm-browserify@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@budibase/vm-browserify/-/vm-browserify-1.1.3.tgz#63f7917671a0f0cb760e3aa37cfd5dfa32e997ed" - integrity sha512-CuoNb2xwS8TT2ZfG9YqC8QCTcG3ZPLwH4m00sfPDluwmdp3U3HGg/UKWRIqKC6Wv8Mywy1q6bxmSx6Vf40V52w== +"@budibase/vm-browserify@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@budibase/vm-browserify/-/vm-browserify-1.1.4.tgz#eecb001bd9521cb7647e26fb4d2d29d0a4dce262" + integrity sha512-/dyOLj+jQNKe6sVfLP6NdwA79OZxEWHCa41VGsjKJC9DYo6l2fEcL5BNXq2pATqrbgWmOlEbcRulfZ+7W0QRUg== dependencies: indexof "^0.0.1" From 5c9cc915ff9c7e1da7538e282c02b6503bd006e3 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 22 Jan 2025 17:32:34 +0100 Subject: [PATCH 093/321] Remove magic string for settings --- packages/builder/src/helpers/screen.ts | 30 ++++++++++++++----- .../src/stores/builder/screenComponent.ts | 7 +++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/builder/src/helpers/screen.ts b/packages/builder/src/helpers/screen.ts index e83805a511..71623844de 100644 --- a/packages/builder/src/helpers/screen.ts +++ b/packages/builder/src/helpers/screen.ts @@ -2,7 +2,13 @@ import { Component, Screen, ScreenProps } from "@budibase/types" import clientManifest from "@budibase/client/manifest.json" export function findComponentsBySettingsType(screen: Screen, type: string) { - const result: Component[] = [] + const result: { + component: Component + setting: { + type: string + key: string + } + }[] = [] function recurseFieldComponentsInChildren( component: ScreenProps, type: string @@ -11,14 +17,15 @@ export function findComponentsBySettingsType(screen: Screen, type: string) { return } - const componentType = component._component.split("/").slice(-1)[0] - const definition = - clientManifest[componentType as keyof typeof clientManifest] - if ( + const definition = getManifestDefinition(component) + const setting = "settings" in definition && - definition.settings.some((s: any) => s.type === type) - ) { - result.push(component) + definition.settings.find((s: any) => s.type === type) + if (setting && "type" in setting) { + result.push({ + component, + setting: { type: setting.type!, key: setting.key! }, + }) } component._children?.forEach(child => { recurseFieldComponentsInChildren(child, type) @@ -28,3 +35,10 @@ export function findComponentsBySettingsType(screen: Screen, type: string) { recurseFieldComponentsInChildren(screen?.props, type) return result } + +function getManifestDefinition(component: Component) { + const componentType = component._component.split("/").slice(-1)[0] + const definition = + clientManifest[componentType as keyof typeof clientManifest] + return definition +} diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index 19bafeade3..a061158e6a 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -38,8 +38,11 @@ export const screenComponentErrors = derived( } const result: Record = {} - for (const component of findComponentsBySettingsType(screen, "table")) { - const { resourceId, type, label } = component.dataSource + for (const { component, setting } of findComponentsBySettingsType( + screen, + "table" + )) { + const { resourceId, type, label } = component[setting.key] if (!datasources[resourceId]) { const friendlyTypeName = friendlyNameByType[type as keyof typeof friendlyNameByType] From 8cab28d5b64147b6d6c1dd4bcb398e3cb8daa788 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 23 Jan 2025 10:08:34 +0000 Subject: [PATCH 094/321] add link for each individual setting --- .../Component/ComponentSettingsPanel.svelte | 9 + .../Component/ConditionalUISection.svelte | 23 ++- .../[screenId]/_components/StatePanel.svelte | 162 +++++++++++------- 3 files changed, 134 insertions(+), 60 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte index 30f11e6cab..d9862e6e14 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte @@ -16,6 +16,7 @@ } from "@/dataBinding" import { ActionButton, notifications } from "@budibase/bbui" import { capitalise } from "@/helpers" + import { builderStore } from "@/stores/builder" import TourWrap from "@/components/portal/onboarding/TourWrap.svelte" import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js" @@ -55,6 +56,14 @@ $: id = $selectedComponent?._id $: id, (section = tabs[0]) $: componentName = getComponentName(componentInstance) + + $: highlightedSettings = $builderStore.highlightedSettings + $: if (highlightedSettings?.length) { + const settings = highlightedSettings.map(s => s.key) + if (settings.length === 1 && settings[0] === "_conditions") { + section = "conditions" + } + } {#if $selectedComponent} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ConditionalUISection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ConditionalUISection.svelte index b40a6af3c6..d91799b6da 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ConditionalUISection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ConditionalUISection.svelte @@ -9,6 +9,7 @@ import { componentStore } from "@/stores/builder" import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte" import ComponentSettingsSection from "./ComponentSettingsSection.svelte" + import { builderStore } from "@/stores/builder" export let componentInstance export let componentDefinition @@ -18,6 +19,10 @@ let tempValue let drawer + $: highlightCondition = $builderStore.highlightedSettings?.find( + setting => setting.key === "_conditions" + ) + const openDrawer = () => { tempValue = JSON.parse(JSON.stringify(componentInstance?._conditions ?? [])) drawer.show() @@ -52,7 +57,9 @@ /> - {conditionText} +
+ {conditionText} +
@@ -61,3 +68,17 @@ + + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index cf6cd12d91..9dc3d3137d 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -1,5 +1,5 @@
- State -
Showing state variables for this screen
0 ? false : "No state variables found"} options={keyOptions} on:change={handleStateKeySelect} />
-
- -
- + {#if selectedKey && keyOptions.length > 0} +
+ handleStateInspectorChange(e)} + {bindings} + /> +
+ {/if} {#if componentsUsingState.length > 0}
Updates: @@ -276,7 +288,6 @@ From 8ca5cb559921c13167ce63fdb454884be7c8210a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 28 Jan 2025 17:43:03 +0000 Subject: [PATCH 171/321] 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 680f52adbd8e96c6e3998fe099802413838fd0c6 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Wed, 29 Jan 2025 09:35:13 +0000 Subject: [PATCH 172/321] remove link --- .../design/[screenId]/_components/StatePanel.svelte | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index e3fa50f07f..61630f3c98 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -246,15 +246,6 @@
-
0 ? false : "No state variables found"} - options={keyOptions} - on:change={handleStateKeySelect} - /> -
+ +
+ + + diff --git a/packages/types/src/ui/common.ts b/packages/types/src/ui/common.ts new file mode 100644 index 0000000000..2f5d864ca0 --- /dev/null +++ b/packages/types/src/ui/common.ts @@ -0,0 +1,3 @@ +export type UIEvent = Event & { + currentTarget: EventTarget & HTMLInputElement +} & { key?: string } & { target?: any } diff --git a/packages/types/src/ui/index.ts b/packages/types/src/ui/index.ts index 6e5f37608c..9c4ba93058 100644 --- a/packages/types/src/ui/index.ts +++ b/packages/types/src/ui/index.ts @@ -3,3 +3,4 @@ export * from "./bindings" export * from "./components" export * from "./dataFetch" export * from "./datasource" +export * from "./common" From 794acbb569754353e63e327e29198289382d9a30 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 29 Jan 2025 18:04:27 +0100 Subject: [PATCH 191/321] Fix iife reference --- packages/server/src/jsRunner/bundles/snippets.ts | 3 +-- packages/string-templates/package.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/server/src/jsRunner/bundles/snippets.ts b/packages/server/src/jsRunner/bundles/snippets.ts index 8244b2eef8..343cccd36f 100644 --- a/packages/server/src/jsRunner/bundles/snippets.ts +++ b/packages/server/src/jsRunner/bundles/snippets.ts @@ -1,6 +1,5 @@ // @ts-ignore -// eslint-disable-next-line local-rules/no-budibase-imports -import { iifeWrapper } from "@budibase/string-templates/iife" +import { iifeWrapper } from "@budibase/string-templates" export default new Proxy( {}, diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index b1b4b9ef55..acd0ea4fa8 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -11,8 +11,7 @@ "require": "./dist/bundle.cjs", "import": "./dist/bundle.mjs" }, - "./package.json": "./package.json", - "./iife": "./dist/iife.mjs" + "./package.json": "./package.json" }, "scripts": { "build": "tsc --emitDeclarationOnly && rollup -c", From 795df854af65be542c70deb1cc05a49698e5ec0c Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 29 Jan 2025 17:09:40 +0000 Subject: [PATCH 192/321] More TS fixes. --- packages/bbui/src/Form/Core/TextField.svelte | 8 ++++---- packages/bbui/src/Form/Input.svelte | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bbui/src/Form/Core/TextField.svelte b/packages/bbui/src/Form/Core/TextField.svelte index a4f614ce29..2de310cd25 100644 --- a/packages/bbui/src/Form/Core/TextField.svelte +++ b/packages/bbui/src/Form/Core/TextField.svelte @@ -4,7 +4,7 @@ import type { UIEvent } from "@budibase/types" export let value: string | null = null - export let placeholder = null + export let placeholder: string | null = null export let type = "text" export let disabled = false export let id = null @@ -12,8 +12,8 @@ export let updateOnChange = true export let quiet = false export let align: string | null = null - export let autofocus = false - export let autocomplete = null + export let autofocus: boolean | null = false + export let autocomplete: string | null = null const dispatch = createEventDispatcher() @@ -71,7 +71,7 @@ onMount(async () => { if (disabled) return - focus = autofocus + focus = autofocus || false if (focus) { await tick() field.focus() diff --git a/packages/bbui/src/Form/Input.svelte b/packages/bbui/src/Form/Input.svelte index 39055237f0..f141e85a51 100644 --- a/packages/bbui/src/Form/Input.svelte +++ b/packages/bbui/src/Form/Input.svelte @@ -13,8 +13,8 @@ export let error = null export let updateOnChange = true export let quiet = false - export let autofocus - export let autocomplete + export let autofocus: boolean | null = null + export let autocomplete: string | null = null export let helpText = null const dispatch = createEventDispatcher() From 52b504a607eae49e30dbe6e8900ccbb3da1dcf35 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 29 Jan 2025 18:11:39 +0100 Subject: [PATCH 193/321] Add objectId test --- .../api/routes/tests/queries/mongodb.spec.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index a37957fe7e..c42227b623 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -634,6 +634,28 @@ if (descriptions.length) { } }) }) + + it("should be able to select a ObjectId in a transformer", async () => { + const query = await createQuery({ + fields: { + json: {}, + extra: { + actionType: "find", + }, + }, + transformer: "return data.map(x => ({ id: x._id }))", + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { id: expectValidId }, + { id: expectValidId }, + { id: expectValidId }, + { id: expectValidId }, + { id: expectValidId }, + ]) + }) }) it("should throw an error if the incorrect actionType is specified", async () => { From b8e5512f307a5bb03adcab66ad35bad56da59dc8 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 29 Jan 2025 17:40:45 +0000 Subject: [PATCH 194/321] Remove table word, pass this in instead --- .../modals/DeleteDataConfirmationModal.svelte | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index 64c70f0dd0..d9e55a4cf2 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -5,6 +5,7 @@ import type { Table, View, Datasource, Query } from "@budibase/types" export let source: Table | View | Datasource | Query | undefined + export let type: "table" | "view" | "datasource" | "query" export let deleteSourceFn: () => Promise let confirmDeleteDialog: any @@ -54,7 +55,7 @@

- Are you sure you wish to delete the table - + Are you sure you wish to delete the {type} + {source?.name} @@ -71,13 +72,13 @@

-

All table data will be deleted{viewsMessage}.

+

All {type} data will be deleted{viewsMessage}.

This action cannot be undone.

{#if affectedScreens.length > 0}
    {#each affectedScreens as item} @@ -109,7 +110,7 @@ max-width: 100%; } - .tableNameLine { + .sourceNameLine { display: inline-flex; max-width: 100%; vertical-align: bottom; From 6a94aada1932b5dc350bc2f6db0205ff161cbb8e Mon Sep 17 00:00:00 2001 From: mikesealey Date: Wed, 29 Jan 2025 17:52:14 +0000 Subject: [PATCH 195/321] adds more robust testing for complex cases --- packages/server/src/utilities/csv.ts | 22 +++++--------- .../server/src/utilities/tests/csv.spec.ts | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/server/src/utilities/csv.ts b/packages/server/src/utilities/csv.ts index 1e139f36fa..880a078a2a 100644 --- a/packages/server/src/utilities/csv.ts +++ b/packages/server/src/utilities/csv.ts @@ -4,15 +4,10 @@ export async function jsonFromCsvString(csvString: string) { const possibleDelimeters = [",", ";", ":", "|", "~", "\t", " "] for (let i = 0; i < possibleDelimeters.length; i++) { - let numOfHeaders: number | undefined = undefined + let headers: string[] | undefined = undefined let headerMismatch = false try { - const castedWithEmptyValues = await csv({ - ignoreEmpty: true, - delimiter: possibleDelimeters[i], - }).fromString(csvString) - // By default the csvtojson library casts empty values as empty strings. This // is causing issues on conversion. ignoreEmpty will remove the key completly // if empty, so creating this empty object will ensure we return the values @@ -28,18 +23,17 @@ export async function jsonFromCsvString(csvString: string) { // If the number of columms in each row is different to // the number of headers, this isn't the right delimiter const columns = Object.keys(r) - if (numOfHeaders == null) { - numOfHeaders = columns.length + if (headers == null) { + headers = columns } - if (numOfHeaders === 1 || numOfHeaders !== columns.length) { + if (headers.length === 1 || headers.length !== columns.length) { headerMismatch = true break } - for (const [key] of Object.entries(r).filter( - ([, value]) => value === "" - )) { - if (castedWithEmptyValues[i][key] === undefined) { - r[key] = null + + for (const header of headers) { + if (r[header] === undefined || r[header] === "") { + r[header] = null } } } diff --git a/packages/server/src/utilities/tests/csv.spec.ts b/packages/server/src/utilities/tests/csv.spec.ts index 14063d0e8e..85796307f9 100644 --- a/packages/server/src/utilities/tests/csv.spec.ts +++ b/packages/server/src/utilities/tests/csv.spec.ts @@ -1,3 +1,4 @@ +import { delimiter } from "path" import { jsonFromCsvString } from "../csv" describe("csv", () => { @@ -29,5 +30,34 @@ describe("csv", () => { expect(Object.keys(r)).toEqual(["id", "optional", "title"]) ) }) + + const possibleDelimeters = [",", ";", ":", "|", "~", "\t", " "] + + const csvArray = [ + ["id", "title"], + ["1", "aaa"], + ["2", "bbb"], + ["3", "c ccc"], + ["", ""], + [":5", "eee5:e"], + ] + + test.each(possibleDelimeters)( + "Should parse with delimiter %s", + async delimiter => { + const csvString = csvArray + .map(row => row.map(col => `"${col}"`).join(delimiter)) + .join("\n") + const result = await jsonFromCsvString(csvString) + + expect(result).toEqual([ + { id: "1", title: "aaa" }, + { id: "2", title: "bbb" }, + { id: "3", title: "c ccc" }, + { id: null, title: null }, + { id: ":5", title: "eee5:e" }, + ]) + } + ) }) }) From 690f1a6ae6ba26e04c625991326efdb4f3ebf85b Mon Sep 17 00:00:00 2001 From: mikesealey Date: Wed, 29 Jan 2025 18:28:24 +0000 Subject: [PATCH 196/321] addresses lint warnings --- packages/server/src/utilities/csv.ts | 2 +- packages/server/src/utilities/tests/csv.spec.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/server/src/utilities/csv.ts b/packages/server/src/utilities/csv.ts index 880a078a2a..eb42d118ca 100644 --- a/packages/server/src/utilities/csv.ts +++ b/packages/server/src/utilities/csv.ts @@ -16,7 +16,7 @@ export async function jsonFromCsvString(csvString: string) { ignoreEmpty: false, delimiter: possibleDelimeters[i], }).fromString(csvString) - for (const [i, r] of result.entries()) { + for (const [, r] of result.entries()) { // The purpose of this is to find rows that have been split // into the wrong number of columns - Any valid .CSV file will have // the same number of colums in each row diff --git a/packages/server/src/utilities/tests/csv.spec.ts b/packages/server/src/utilities/tests/csv.spec.ts index 85796307f9..b1dc192bf0 100644 --- a/packages/server/src/utilities/tests/csv.spec.ts +++ b/packages/server/src/utilities/tests/csv.spec.ts @@ -1,4 +1,3 @@ -import { delimiter } from "path" import { jsonFromCsvString } from "../csv" describe("csv", () => { From 11b6547c21de504d1d914f340123b41f10929027 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 29 Jan 2025 20:50:09 +0100 Subject: [PATCH 197/321] Inject polyfills --- .../bundles/bson-polyfills.ivm.bundle.js | 6 ++++ packages/server/src/jsRunner/bundles/index.ts | 2 ++ .../server/src/jsRunner/vm/isolated-vm.ts | 31 +++++++------------ 3 files changed, 19 insertions(+), 20 deletions(-) create mode 100644 packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js diff --git a/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js b/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js new file mode 100644 index 0000000000..e8147c63da --- /dev/null +++ b/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js @@ -0,0 +1,6 @@ +function atob(...args){ + return atobCB(...args) +} +function btoa(...args){ + return btoaCB(...args) +} diff --git a/packages/server/src/jsRunner/bundles/index.ts b/packages/server/src/jsRunner/bundles/index.ts index b62adac1cc..3a00ee96cc 100644 --- a/packages/server/src/jsRunner/bundles/index.ts +++ b/packages/server/src/jsRunner/bundles/index.ts @@ -5,6 +5,7 @@ export const enum BundleType { BSON = "bson", SNIPPETS = "snippets", BUFFER = "buffer", + BSON_POLYFILLS = "bson_polyfills", } const bundleSourceFile: Record = { @@ -12,6 +13,7 @@ const bundleSourceFile: Record = { [BundleType.BSON]: "./bson.ivm.bundle.js", [BundleType.SNIPPETS]: "./snippets.ivm.bundle.js", [BundleType.BUFFER]: "./buffer.ivm.bundle.js", + [BundleType.BSON_POLYFILLS]: "./bson-polyfills.ivm.bundle.js", } const bundleSourceCode: Partial> = {} diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index 3863be742d..65a2615ed7 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -173,30 +173,21 @@ export class IsolatedVM implements VM { return result } ), + atobCB: new ivm.Callback((...args: Parameters) => { + const result = atob(...args) + return result + }), + btoaCB: new ivm.Callback((...args: Parameters) => { + const result = btoa(...args) + return result + }), }) - // "Polyfilling" text decoder. `bson.deserialize` requires decoding. We are creating a bridge function so we don't need to inject the full library - const textDecoderPolyfill = class TextDecoderMock { - constructorArgs - - constructor(...constructorArgs: any) { - this.constructorArgs = constructorArgs - } - - decode(...input: any) { - // @ts-expect-error - this is going to run in the isolate, where this function will be available - // eslint-disable-next-line no-undef - return textDecoderCb({ - constructorArgs: this.constructorArgs, - functionArgs: input, - }) - } - } - .toString() - .replace(/TextDecoderMock/, "TextDecoder") + // "Polyfilling" text decoder and other utils. `bson.deserialize` requires decoding. We are creating a bridge function so we don't need to inject the full library + const bsonPolyfills = loadBundle(BundleType.BSON_POLYFILLS) const script = this.isolate.compileScriptSync( - `${textDecoderPolyfill};${bsonSource}` + `${bsonPolyfills};${bsonSource}` ) script.runSync(this.vm, { timeout: this.invocationTimeout, release: false }) new Promise(() => { From 9ff29c794e906202550dbce0e4621c02810bfa2c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 29 Jan 2025 21:01:19 +0100 Subject: [PATCH 198/321] Add TextDecoder polyfill --- .prettierignore | 3 ++- .../bundles/bson-polyfills.ivm.bundle.js | 23 +++++++++++++++---- .../server/src/jsRunner/vm/isolated-vm.ts | 5 ++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.prettierignore b/.prettierignore index b1ee287391..b0f9f8cdbf 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,4 +9,5 @@ packages/backend-core/coverage packages/builder/.routify packages/sdk/sdk packages/pro/coverage -**/*.ivm.bundle.js \ No newline at end of file +**/*.ivm.bundle.js +!**/bson-polyfills.ivm.bundle.js \ No newline at end of file diff --git a/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js b/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js index e8147c63da..33a3e9deaa 100644 --- a/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js +++ b/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js @@ -1,6 +1,21 @@ -function atob(...args){ - return atobCB(...args) +function atob(...args) { + return atobCB(...args) } -function btoa(...args){ - return btoaCB(...args) +function btoa(...args) { + return btoaCB(...args) +} + +class TextDecoder { + constructorArgs + + constructor(...constructorArgs) { + this.constructorArgs = constructorArgs + } + + decode(...input) { + return textDecoderCb({ + constructorArgs: this.constructorArgs, + functionArgs: input, + }) + } } diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index 65a2615ed7..4b17d52dba 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -161,6 +161,8 @@ export class IsolatedVM implements VM { const bsonSource = loadBundle(BundleType.BSON) + // "Polyfilling" text decoder and other utils. `bson.deserialize` requires decoding. We are creating a bridge function so we don't need to inject the full library + const bsonPolyfills = loadBundle(BundleType.BSON_POLYFILLS) this.addToContext({ textDecoderCb: new ivm.Callback( (args: { @@ -183,9 +185,6 @@ export class IsolatedVM implements VM { }), }) - // "Polyfilling" text decoder and other utils. `bson.deserialize` requires decoding. We are creating a bridge function so we don't need to inject the full library - const bsonPolyfills = loadBundle(BundleType.BSON_POLYFILLS) - const script = this.isolate.compileScriptSync( `${bsonPolyfills};${bsonSource}` ) From 148924f82123b9a20fe28dde1327124b3eb1e5b3 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 08:47:18 +0100 Subject: [PATCH 199/321] Use polyfills directly --- .../bundles/bson-polyfills.ivm.bundle.js | 83 +++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js b/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js index 33a3e9deaa..e7949042c8 100644 --- a/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js +++ b/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js @@ -1,8 +1,81 @@ -function atob(...args) { - return atobCB(...args) -} -function btoa(...args) { - return btoaCB(...args) +if (typeof btoa !== "function") { + var chars = { + ascii: function () { + return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" + }, + indices: function () { + if (!this.cache) { + this.cache = {} + var ascii = chars.ascii() + + for (var c = 0; c < ascii.length; c++) { + var chr = ascii[c] + this.cache[chr] = c + } + } + return this.cache + }, + } + + function atob(b64) { + var indices = chars.indices(), + pos = b64.indexOf("="), + padded = pos > -1, + len = padded ? pos : b64.length, + i = -1, + data = "" + + while (i < len) { + var code = + (indices[b64[++i]] << 18) | + (indices[b64[++i]] << 12) | + (indices[b64[++i]] << 6) | + indices[b64[++i]] + if (code !== 0) { + data += String.fromCharCode( + (code >>> 16) & 255, + (code >>> 8) & 255, + code & 255 + ) + } + } + + if (padded) { + data = data.slice(0, pos - b64.length) + } + + return data + } + + function btoa(data) { + var ascii = chars.ascii(), + len = data.length - 1, + i = -1, + b64 = "" + + while (i < len) { + var code = + (data.charCodeAt(++i) << 16) | + (data.charCodeAt(++i) << 8) | + data.charCodeAt(++i) + b64 += + ascii[(code >>> 18) & 63] + + ascii[(code >>> 12) & 63] + + ascii[(code >>> 6) & 63] + + ascii[code & 63] + } + + var pads = data.length % 3 + if (pads > 0) { + b64 = b64.slice(0, pads - 3) + + while (b64.length % 4 !== 0) { + b64 += "=" + } + } + + return b64 + } } class TextDecoder { From 7fdb22459ae9b2b015e35ecbbf2d341ea795e571 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 08:47:24 +0100 Subject: [PATCH 200/321] Use polyfills directly --- packages/server/src/jsRunner/vm/isolated-vm.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index 4b17d52dba..3123bcaa44 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -175,14 +175,6 @@ export class IsolatedVM implements VM { return result } ), - atobCB: new ivm.Callback((...args: Parameters) => { - const result = atob(...args) - return result - }), - btoaCB: new ivm.Callback((...args: Parameters) => { - const result = btoa(...args) - return result - }), }) const script = this.isolate.compileScriptSync( From 4614a13abf21b845f6d362acc940f535f821a7b5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 30 Jan 2025 08:47:35 +0000 Subject: [PATCH 201/321] Lint --- packages/frontend-core/src/components/grid/cells/DataCell.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte index cdd0d50093..9edaa6af87 100644 --- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte @@ -3,7 +3,6 @@ import GridCell from "./GridCell.svelte" import { getCellRenderer } from "../lib/renderers" import { derived, writable } from "svelte/store" - import { processStringSync } from "@budibase/string-templates" const { rows, From cfe296bcc904c589a28a8bd918279ef60ab5357c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 10:55:57 +0100 Subject: [PATCH 202/321] Add polyfills --- .../bundles/bson-polyfills.ivm.bundle.js | 52 ++++++++++++++----- .../server/src/jsRunner/vm/isolated-vm.ts | 14 ----- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js b/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js index e7949042c8..94d08b84ed 100644 --- a/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js +++ b/packages/server/src/jsRunner/bundles/bson-polyfills.ivm.bundle.js @@ -78,17 +78,45 @@ if (typeof btoa !== "function") { } } -class TextDecoder { - constructorArgs - - constructor(...constructorArgs) { - this.constructorArgs = constructorArgs - } - - decode(...input) { - return textDecoderCb({ - constructorArgs: this.constructorArgs, - functionArgs: input, - }) +if (typeof TextDecoder === "undefined") { + globalThis.TextDecoder = class { + constructor(encoding = "utf8") { + if (encoding !== "utf8") { + throw new Error( + `Only UTF-8 is supported in this polyfill. Recieved: ${encoding}` + ) + } + } + decode(buffer) { + return String.fromCharCode(...buffer) + } + } +} + +if (typeof TextEncoder === "undefined") { + globalThis.TextEncoder = class { + encode(str) { + const utf8 = [] + for (const i = 0; i < str.length; i++) { + const codePoint = str.charCodeAt(i) + + if (codePoint < 0x80) { + utf8.push(codePoint) + } else if (codePoint < 0x800) { + utf8.push(0xc0 | (codePoint >> 6)) + utf8.push(0x80 | (codePoint & 0x3f)) + } else if (codePoint < 0x10000) { + utf8.push(0xe0 | (codePoint >> 12)) + utf8.push(0x80 | ((codePoint >> 6) & 0x3f)) + utf8.push(0x80 | (codePoint & 0x3f)) + } else { + utf8.push(0xf0 | (codePoint >> 18)) + utf8.push(0x80 | ((codePoint >> 12) & 0x3f)) + utf8.push(0x80 | ((codePoint >> 6) & 0x3f)) + utf8.push(0x80 | (codePoint & 0x3f)) + } + } + return new Uint8Array(utf8) + } } } diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index 3123bcaa44..37ee048dc2 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -161,21 +161,7 @@ export class IsolatedVM implements VM { const bsonSource = loadBundle(BundleType.BSON) - // "Polyfilling" text decoder and other utils. `bson.deserialize` requires decoding. We are creating a bridge function so we don't need to inject the full library const bsonPolyfills = loadBundle(BundleType.BSON_POLYFILLS) - this.addToContext({ - textDecoderCb: new ivm.Callback( - (args: { - constructorArgs: any - functionArgs: Parameters["decode"]> - }) => { - const result = new TextDecoder(...args.constructorArgs).decode( - ...args.functionArgs - ) - return result - } - ), - }) const script = this.isolate.compileScriptSync( `${bsonPolyfills};${bsonSource}` From cda47842730443d9bcbf02475c257c89bc11a7f9 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 30 Jan 2025 09:58:23 +0000 Subject: [PATCH 203/321] Add info to format and ensure all custom-formatted cells are displayed as plain text --- .../design/settings/controls/PropertyControl.svelte | 2 +- packages/client/manifest.json | 3 ++- packages/frontend-core/src/components/grid/lib/renderers.ts | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte index 65b3ed9395..55680160a2 100644 --- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte +++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte @@ -172,7 +172,7 @@ } .text { font-size: var(--spectrum-global-dimension-font-size-75); - color: var(--grey-6); + color: var(--spectrum-global-color-gray-700); grid-column: 2 / 2; } diff --git a/packages/client/manifest.json b/packages/client/manifest.json index a2db6eca6d..c451fc0905 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3093,7 +3093,8 @@ { "type": "text", "label": "Format", - "key": "format" + "key": "format", + "info": "Changing the format will display the value as plain text" } ] }, diff --git a/packages/frontend-core/src/components/grid/lib/renderers.ts b/packages/frontend-core/src/components/grid/lib/renderers.ts index b009806cc4..c1b979dd99 100644 --- a/packages/frontend-core/src/components/grid/lib/renderers.ts +++ b/packages/frontend-core/src/components/grid/lib/renderers.ts @@ -50,10 +50,12 @@ function getCellRendererByType(type: FieldType | "role" | undefined) { } export const getCellRenderer = (column: UIColumn) => { + if (column.format) { + return TextCell + } if (column.calculationType) { return NumberCell } - return ( getCellRendererByType(column.schema?.cellRenderType) || getCellRendererByType(column.schema?.type) || From 42eefddd5cc6b3a877368f59556cf24070c3478f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 11:26:05 +0100 Subject: [PATCH 204/321] Add tests --- .../api/routes/tests/queries/mongodb.spec.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index c42227b623..065efc95ae 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -656,6 +656,122 @@ if (descriptions.length) { { id: expectValidId }, ]) }) + + it("can handle all bson field types", async () => { + collection = generator.guid() + await withCollection(async collection => { + await collection.insertOne({ + _id: new BSON.ObjectId("65b0123456789abcdef01234"), + stringField: "This is a string", + numberField: 42, + doubleField: new BSON.Double(42.42), + integerField: new BSON.Int32(123), + longField: new BSON.Long("9223372036854775807"), + booleanField: true, + nullField: null, + arrayField: [1, 2, 3, "four", { nested: true }], + objectField: { + nestedString: "nested", + nestedNumber: 99, + }, + dateField: { $date: "2025-01-30T12:00:00Z" }, + // timestampField: new BSON.Timestamp({ t: 1706616000, i: 1 }), + binaryField: new BSON.Binary( + new TextEncoder().encode("bufferValue") + ), + objectIdField: new BSON.ObjectId("65b0123456789abcdef01235"), + regexField: new BSON.BSONRegExp("^Hello.*", "i"), + minKeyField: new BSON.MinKey(), + maxKeyField: new BSON.MaxKey(), + decimalField: new BSON.Decimal128("12345.6789"), + codeField: new BSON.Code( + "function() { return 'Hello, World!'; }" + ), + codeWithScopeField: new BSON.Code( + "function(x) { return x * 2; }", + { x: 10 } + ), + }) + }) + + const query = await createQuery({ + fields: { + json: {}, + extra: { + actionType: "find", + collection, + }, + }, + transformer: "return data.map(x => ({ ...x }))", + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + _id: "65b0123456789abcdef01234", + arrayField: [ + 1, + 2, + 3, + "four", + { + nested: true, + }, + ], + binaryField: "YnVmZmVyVmFsdWU=", + booleanField: true, + codeField: { + code: "function() { return 'Hello, World!'; }", + }, + codeWithScopeField: { + code: "function(x) { return x * 2; }", + scope: { + x: 10, + }, + }, + dateField: "2025-01-30T12:00:00.000Z", + decimalField: { + bytes: { + "0": 21, + "1": 205, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 56, + "15": 48, + "2": 91, + "3": 7, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + }, + }, + doubleField: 42.42, + integerField: 123, + longField: { + high: 2147483647, + low: -1, + unsigned: false, + }, + maxKeyField: {}, + minKeyField: {}, + nullField: null, + numberField: 42, + objectField: { + nestedNumber: 99, + nestedString: "nested", + }, + objectIdField: "65b0123456789abcdef01235", + regexField: {}, + stringField: "This is a string", + }, + ]) + }) }) it("should throw an error if the incorrect actionType is specified", async () => { From cfba775636be13d97f8c2e7676e0dcc29e85a325 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 11:33:58 +0100 Subject: [PATCH 205/321] Check types --- .../api/routes/tests/queries/mongodb.spec.ts | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index 065efc95ae..c8c12acbf9 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -674,7 +674,7 @@ if (descriptions.length) { nestedString: "nested", nestedNumber: 99, }, - dateField: { $date: "2025-01-30T12:00:00Z" }, + dateField: new Date(Date.UTC(2025, 0, 30, 12, 30, 20)), // timestampField: new BSON.Timestamp({ t: 1706616000, i: 1 }), binaryField: new BSON.Binary( new TextEncoder().encode("bufferValue") @@ -702,7 +702,12 @@ if (descriptions.length) { collection, }, }, - transformer: "return data.map(x => ({ ...x }))", + transformer: `return data.map(x => ({ + ...x, + binaryField: x.binaryField?.toString('utf8'), + decimalField: x.decimalField.toString(), + longField: x.longField.toString() + }))`, }) const result = await config.api.query.execute(query._id!) @@ -719,7 +724,7 @@ if (descriptions.length) { nested: true, }, ], - binaryField: "YnVmZmVyVmFsdWU=", + binaryField: "bufferValue", booleanField: true, codeField: { code: "function() { return 'Hello, World!'; }", @@ -730,34 +735,11 @@ if (descriptions.length) { x: 10, }, }, - dateField: "2025-01-30T12:00:00.000Z", - decimalField: { - bytes: { - "0": 21, - "1": 205, - "10": 0, - "11": 0, - "12": 0, - "13": 0, - "14": 56, - "15": 48, - "2": 91, - "3": 7, - "4": 0, - "5": 0, - "6": 0, - "7": 0, - "8": 0, - "9": 0, - }, - }, + dateField: "2025-01-30T12:30:20.000Z", + decimalField: "12345.6789", doubleField: 42.42, integerField: 123, - longField: { - high: 2147483647, - low: -1, - unsigned: false, - }, + longField: "9223372036854775807", maxKeyField: {}, minKeyField: {}, nullField: null, From 49d50e053fdc8994cddbced63c192c39bb646c40 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 11:38:04 +0100 Subject: [PATCH 206/321] Check types --- packages/server/src/api/routes/tests/queries/mongodb.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index c8c12acbf9..63280f85c1 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -706,7 +706,8 @@ if (descriptions.length) { ...x, binaryField: x.binaryField?.toString('utf8'), decimalField: x.decimalField.toString(), - longField: x.longField.toString() + longField: x.longField.toString(), + regexField: x.regexField.toString() }))`, }) @@ -749,7 +750,7 @@ if (descriptions.length) { nestedString: "nested", }, objectIdField: "65b0123456789abcdef01235", - regexField: {}, + regexField: "/^Hello.*/i", stringField: "This is a string", }, ]) From 5d6088104d71570e58fdd299bbaa50b8de602d8b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 12:06:01 +0100 Subject: [PATCH 207/321] Improve tests --- .../server/src/api/routes/tests/queries/mongodb.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index 63280f85c1..d74db5a7d3 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -675,7 +675,7 @@ if (descriptions.length) { nestedNumber: 99, }, dateField: new Date(Date.UTC(2025, 0, 30, 12, 30, 20)), - // timestampField: new BSON.Timestamp({ t: 1706616000, i: 1 }), + timestampField: new BSON.Timestamp({ t: 1706616000, i: 1 }), binaryField: new BSON.Binary( new TextEncoder().encode("bufferValue") ), @@ -707,7 +707,9 @@ if (descriptions.length) { binaryField: x.binaryField?.toString('utf8'), decimalField: x.decimalField.toString(), longField: x.longField.toString(), - regexField: x.regexField.toString() + regexField: x.regexField.toString(), + // TODO: currenlty not supported, it looks like there is bug in the library. Getting: Timestamp constructed from { t, i } must provide t as a number + timestampField: null }))`, }) From 79405dd3429f2368fedc745de142fd551e96d542 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 12:06:17 +0100 Subject: [PATCH 208/321] Fix test --- packages/server/src/api/routes/tests/queries/mongodb.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index d74db5a7d3..9c4502d192 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -754,6 +754,7 @@ if (descriptions.length) { objectIdField: "65b0123456789abcdef01235", regexField: "/^Hello.*/i", stringField: "This is a string", + timestampField: null, }, ]) }) From 768522b41055a87fb1abf2b0d36c48f142a73dcf Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 12:12:28 +0100 Subject: [PATCH 209/321] Rename --- packages/server/src/api/routes/tests/queries/mongodb.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index 9c4502d192..37af4e74e1 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -657,7 +657,7 @@ if (descriptions.length) { ]) }) - it("can handle all bson field types", async () => { + it("can handle all bson field types with transformers", async () => { collection = generator.guid() await withCollection(async collection => { await collection.insertOne({ From 735b8206ce737706b062327bb601f0b5084895ba Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 12:39:25 +0100 Subject: [PATCH 210/321] 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) { From e94bbbde34ba4db234598fab49fbd56e79e0fd7b Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 30 Jan 2025 12:36:38 +0000 Subject: [PATCH 211/321] Bump version to 3.3.4 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 8d7460f053..b08a471b05 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.3.3", + "version": "3.3.4", "npmClient": "yarn", "concurrency": 20, "command": { From dd3c68ec0f19440a1180777c291c0a86e6960538 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 30 Jan 2025 13:40:14 +0000 Subject: [PATCH 212/321] Ensure new row cells don't use custom formats --- .../src/components/grid/cells/DataCell.svelte | 8 ++++++-- .../frontend-core/src/components/grid/lib/renderers.ts | 3 --- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte index 9edaa6af87..4f181ab12e 100644 --- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte @@ -3,6 +3,7 @@ import GridCell from "./GridCell.svelte" import { getCellRenderer } from "../lib/renderers" import { derived, writable } from "svelte/store" + import TextCell from "./TextCell.svelte" const { rows, @@ -36,7 +37,10 @@ let api - $: value = column.format ? column.format(row) : row[column.name] + // Get the appropriate cell renderer and value + $: hasCustomFormat = column.format && !row._isNewRow + $: renderer = hasCustomFormat ? TextCell : getCellRenderer(column) + $: value = hasCustomFormat ? column.format(row) : row[column.name] // Get the error for this cell if the cell is focused or selected $: error = getErrorStore(rowFocused, cellId) @@ -138,7 +142,7 @@ }} > { - if (column.format) { - return TextCell - } if (column.calculationType) { return NumberCell } From 301bedb5f49de6cb1c9dca95ba9058995840a720 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 30 Jan 2025 13:40:30 +0000 Subject: [PATCH 213/321] Ensure custom format cells are readonly --- packages/frontend-core/src/components/grid/cells/DataCell.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte index 4f181ab12e..395f2da6e1 100644 --- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte @@ -47,6 +47,7 @@ // Determine if the cell is editable $: readonly = + hasCustomFormat || columns.actions.isReadonly(column) || (!$config.canEditRows && !row._isNewRow) From d37fedaae7a6bd752f482f29a1df957c11bce687 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 30 Jan 2025 13:46:10 +0000 Subject: [PATCH 214/321] Copy formatted value when copying values from cells --- .../frontend-core/src/components/grid/cells/DataCell.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte index 395f2da6e1..bdf16eab7d 100644 --- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte @@ -76,7 +76,7 @@ onKeyDown: (...params) => api?.onKeyDown?.(...params), isReadonly: () => readonly, getType: () => column.schema.type, - getValue: () => row[column.name], + getValue: () => value, setValue: (value, options = { apply: true }) => { validation.actions.setError(cellId, null) updateValue({ From 6d22718e077e3b47c34c42efb4b7f21f5b8a1c82 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 30 Jan 2025 14:50:51 +0000 Subject: [PATCH 215/321] Updates state panel to address missing usages --- .../[screenId]/_components/StatePanel.svelte | 100 +++++++++++++----- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index c992fa7f07..05f444a9f2 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -36,11 +36,11 @@ let previousScreenId: string | undefined = undefined $: { - const screenChanged = + const hasScreenChanged = $selectedScreen && $selectedScreen._id !== previousScreenId const previewContext = $previewStore.selectedComponentContext || {} - if (screenChanged) { + if (hasScreenChanged) { selectedKey = keyOptions[0] componentsUsingState = [] componentsUpdatingState = [] @@ -66,38 +66,67 @@ const eventHandlerProps = [ "onClick", - "onChange", "onRowClick", "onChange", "buttonOnClick", ] + const isStateUpdateHandler = (handler: any) => + handler["##eventHandlerType"] === "Update State" && + handler.parameters?.key === stateKey + + const checkEventHandlers = ( + handlers: any[], + componentId: string, + instanceName: string, + setting: string + ) => { + if (!Array.isArray(handlers)) return + + handlers.forEach(handler => { + if (isStateUpdateHandler(handler)) { + foundComponents.push({ + id: componentId, + name: instanceName, + settings: [setting], + }) + } + }) + } + eventHandlerProps.forEach(eventType => { - const handlers = component[eventType] - if (Array.isArray(handlers)) { - handlers.forEach(handler => { - if ( - handler["##eventHandlerType"] === "Update State" && - handler.parameters?.key === stateKey - ) { - foundComponents.push({ - id: component._id!, - name: component._instanceName, - settings: [eventType], - }) - } + checkEventHandlers( + component[eventType], + component._id!, + component._instanceName, + eventType + ) + }) + + Object.entries(component).forEach(([propName, propValue]) => { + if (Array.isArray(propValue)) { + propValue.forEach(item => { + eventHandlerProps.forEach(eventType => { + checkEventHandlers( + item[eventType], + component._id!, + component._instanceName, + propName + ) + }) }) } }) if (component._children) { - for (let child of component._children) { - foundComponents = [ - ...foundComponents, - ...findComponentsUpdatingState(child, stateKey), - ] - } + foundComponents = [ + ...foundComponents, + ...component._children.flatMap(child => + findComponentsUpdatingState(child, stateKey) + ), + ] } + return foundComponents } @@ -113,11 +142,32 @@ const getSettingsWithState = (component: any, stateKey: string): string[] => { const settingsWithState: string[] = [] - for (const [setting, value] of Object.entries(component)) { - if (typeof value === "string" && hasStateBinding(value, stateKey)) { - settingsWithState.push(setting) + + const searchForStateBinding = (value: any, path: string[]) => { + if (typeof value === "string") { + if (hasStateBinding(value, stateKey)) { + const topLevelProperty = path[0] + if (!settingsWithState.includes(topLevelProperty)) { + settingsWithState.push(topLevelProperty) + } + } + } else if (Array.isArray(value)) { + value.forEach((item, index) => { + searchForStateBinding(item, [...path, `${index}`]) + }) + } else if (typeof value === "object" && value !== null) { + Object.entries(value).forEach(([key, val]) => { + searchForStateBinding(val, [...path, key]) + }) } } + + Object.entries(component).forEach(([key, value]) => { + if (["_children", "_styles", "_conditions"].includes(key)) return + + searchForStateBinding(value, [key]) + }) + return settingsWithState } From 12f9d7efb6e56aa6259450622799597a44f54ebb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 30 Jan 2025 15:11:21 +0000 Subject: [PATCH 216/321] Reduce frequency of column formatter invocation by caching formatted display values into rows --- .../src/components/app/GridBlock.svelte | 6 +-- .../src/components/grid/cells/DataCell.svelte | 2 +- .../src/components/grid/stores/rows.ts | 54 +++++++++++++------ packages/types/src/ui/stores/grid/columns.ts | 4 +- packages/types/src/ui/stores/grid/table.ts | 4 +- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 4da5ed4e28..1f4792ea8a 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -115,12 +115,10 @@ } const createFormatter = column => { - if (!column.format?.length) { + if (typeof column.format !== "string" || !column.format.trim().length) { return null } - return row => { - return processStringSync(column.format, { [id]: row }) - } + return row => processStringSync(column.format, { [id]: row }) } const enrichButtons = buttons => { diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte index bdf16eab7d..486d9e1952 100644 --- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte @@ -40,7 +40,7 @@ // Get the appropriate cell renderer and value $: hasCustomFormat = column.format && !row._isNewRow $: renderer = hasCustomFormat ? TextCell : getCellRenderer(column) - $: value = hasCustomFormat ? column.format(row) : row[column.name] + $: value = hasCustomFormat ? row.__formatted?.[column.name] : row[column.name] // Get the error for this cell if the cell is focused or selected $: error = getErrorStore(rowFocused, cellId) diff --git a/packages/frontend-core/src/components/grid/stores/rows.ts b/packages/frontend-core/src/components/grid/stores/rows.ts index 7e58808327..22357f66a7 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.ts +++ b/packages/frontend-core/src/components/grid/stores/rows.ts @@ -16,6 +16,7 @@ import { Store as StoreContext } from "." interface IndexedUIRow extends UIRow { __idx: number + __formatted: Record } interface RowStore { @@ -114,26 +115,44 @@ export const createStores = (): RowStore => { export const deriveStores = (context: StoreContext): RowDerivedStore => { const { rows, enrichedSchema } = context - // Enrich rows with an index property and any pending changes + // Enrich rows with an index property and additional values const enrichedRows = derived( [rows, enrichedSchema], ([$rows, $enrichedSchema]) => { - const customColumns = Object.values($enrichedSchema || {}).filter( - f => f.related - ) - return $rows.map((row, idx) => ({ - ...row, - __idx: idx, - ...customColumns.reduce>((map, column) => { - const fromField = $enrichedSchema![column.related!.field] - map[column.name] = getRelatedTableValues( - row, - { ...column, related: column.related! }, - fromField - ) - return map - }, {}), - })) + // Find columns which require additional processing + const cols = Object.values($enrichedSchema || {}) + const relatedColumns = cols.filter(col => col.related) + const formattedColumns = cols.filter(col => col.format) + + return $rows.map((row, idx) => { + // Derive any values that need enriched from related rows + const relatedValues = relatedColumns.reduce>( + (map, column) => { + const fromField = $enrichedSchema![column.related!.field] + map[column.name] = getRelatedTableValues( + row, + { ...column, related: column.related! }, + fromField + ) + return map + }, + {} + ) + // Derive any display-only formatted values for this row + const formattedValues = formattedColumns.reduce>( + (map, column) => { + map[column.name] = column.format!(row) + return map + }, + {} + ) + return { + ...row, + ...relatedValues, + __formatted: formattedValues, + __idx: idx, + } + }) } ) @@ -791,6 +810,7 @@ export const createActions = (context: StoreContext): RowActionStore => { let clone: Row = { ...row } delete clone.__idx delete clone.__metadata + delete clone.__formatted if (!get(hasBudibaseIdentifiers) && isGeneratedRowID(clone._id!)) { delete clone._id } diff --git a/packages/types/src/ui/stores/grid/columns.ts b/packages/types/src/ui/stores/grid/columns.ts index aa5124ba04..c6ea801713 100644 --- a/packages/types/src/ui/stores/grid/columns.ts +++ b/packages/types/src/ui/stores/grid/columns.ts @@ -1,10 +1,10 @@ -import { CalculationType, FieldSchema, FieldType } from "@budibase/types" +import { CalculationType, FieldSchema, FieldType, UIRow } from "@budibase/types" export type UIColumn = FieldSchema & { label: string readonly: boolean conditions: any - format?: () => any + format?: (row: UIRow) => any related?: { field: string subField: string diff --git a/packages/types/src/ui/stores/grid/table.ts b/packages/types/src/ui/stores/grid/table.ts index 7b6d659e4c..5eea063084 100644 --- a/packages/types/src/ui/stores/grid/table.ts +++ b/packages/types/src/ui/stores/grid/table.ts @@ -5,6 +5,7 @@ import { RelationSchemaField, SortOrder, Table, + UIRow, UISearchFilter, } from "@budibase/types" @@ -24,9 +25,10 @@ export interface UITable extends Table { export type UIFieldSchema = FieldSchema & BasicViewFieldMetadata & { related?: { field: string; subField: string } - columns?: Record + columns?: Record cellRenderType?: string disabled?: boolean + format?: (row: UIRow) => any } interface UIRelationSchemaField extends RelationSchemaField { From 60fd8fb1e1001b13d794a2c9a1a29ccea17c133d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 30 Jan 2025 15:14:49 +0000 Subject: [PATCH 217/321] Fix typo --- packages/types/src/ui/stores/grid/table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/ui/stores/grid/table.ts b/packages/types/src/ui/stores/grid/table.ts index 5eea063084..9a0a54c5c6 100644 --- a/packages/types/src/ui/stores/grid/table.ts +++ b/packages/types/src/ui/stores/grid/table.ts @@ -25,7 +25,7 @@ export interface UITable extends Table { export type UIFieldSchema = FieldSchema & BasicViewFieldMetadata & { related?: { field: string; subField: string } - columns?: Record + columns?: Record cellRenderType?: string disabled?: boolean format?: (row: UIRow) => any From 5513293ef6ebc7ebb0954be015967a6adb727aa7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 30 Jan 2025 15:17:41 +0000 Subject: [PATCH 218/321] Shorten info text to fit in one line --- packages/client/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index c451fc0905..4b13713984 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3094,7 +3094,7 @@ "type": "text", "label": "Format", "key": "format", - "info": "Changing the format will display the value as plain text" + "info": "Changing format will display values as text" } ] }, From 2dc3a693a8c588da2036cbe5d8de43601d8e4e42 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 30 Jan 2025 15:22:04 +0000 Subject: [PATCH 219/321] Bump version to 3.3.5 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index b08a471b05..c16b958d24 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.3.4", + "version": "3.3.5", "npmClient": "yarn", "concurrency": 20, "command": { From e9bf78abe089169a850af032d482d8778028dcc4 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 30 Jan 2025 16:12:12 +0000 Subject: [PATCH 220/321] unneeded flatmap --- .../design/[screenId]/_components/StatePanel.svelte | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index 05f444a9f2..f62adf3c9f 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -119,12 +119,12 @@ }) if (component._children) { - foundComponents = [ - ...foundComponents, - ...component._children.flatMap(child => - findComponentsUpdatingState(child, stateKey) - ), - ] + for (let child of component._children) { + foundComponents = [ + ...foundComponents, + ...findComponentsUpdatingState(child, stateKey), + ] + } } return foundComponents From 1b8a229e8508548fecdae8a1072003960dc3088c Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 30 Jan 2025 16:36:44 +0000 Subject: [PATCH 221/321] Functions to get source type and usage endpoint for screens. --- packages/backend-core/src/docIds/params.ts | 6 +++++- packages/server/src/api/controllers/screen.ts | 7 +++++++ packages/server/src/api/routes/screen.ts | 5 +++++ packages/server/src/sdk/app/common/index.ts | 1 + packages/server/src/sdk/app/common/utils.ts | 15 +++++++++++++++ packages/server/src/sdk/index.ts | 2 ++ packages/types/src/api/web/app/screen.ts | 7 +++++++ packages/types/src/documents/app/datasource.ts | 7 +++++++ 8 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/sdk/app/common/index.ts create mode 100644 packages/server/src/sdk/app/common/utils.ts diff --git a/packages/backend-core/src/docIds/params.ts b/packages/backend-core/src/docIds/params.ts index 016604b69b..5f1c053bde 100644 --- a/packages/backend-core/src/docIds/params.ts +++ b/packages/backend-core/src/docIds/params.ts @@ -83,11 +83,15 @@ export function isViewId(id: string): boolean { /** * Check if a given ID is that of a datasource or datasource plus. */ -export const isDatasourceId = (id: string): boolean => { +export function isDatasourceId(id: string): boolean { // this covers both datasources and datasource plus return !!id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`) } +export function isQueryId(id: string): boolean { + return !!id && id.startsWith(`${DocumentType.QUERY}${SEPARATOR}`) +} + /** * Gets parameters for retrieving workspaces. */ diff --git a/packages/server/src/api/controllers/screen.ts b/packages/server/src/api/controllers/screen.ts index ad000871fe..99a6b5ab52 100644 --- a/packages/server/src/api/controllers/screen.ts +++ b/packages/server/src/api/controllers/screen.ts @@ -16,8 +16,10 @@ import { SaveScreenRequest, SaveScreenResponse, DeleteScreenResponse, + UsageScreenResponse, } from "@budibase/types" import { builderSocket } from "../../websockets" +import sdk from "../../sdk" export async function fetch(ctx: UserCtx) { const db = context.getAppDB() @@ -140,3 +142,8 @@ function findPlugins(component: ScreenProps, foundPlugins: string[]) { } component._children.forEach(child => findPlugins(child, foundPlugins)) } + +export async function usage(ctx: UserCtx) { + const sourceId = ctx.params.sourceId + const sourceType = sdk.common.getSourceType(sourceId) +} diff --git a/packages/server/src/api/routes/screen.ts b/packages/server/src/api/routes/screen.ts index 7caa37bbfc..6329175fb6 100644 --- a/packages/server/src/api/routes/screen.ts +++ b/packages/server/src/api/routes/screen.ts @@ -19,5 +19,10 @@ router authorized(permissions.BUILDER), controller.destroy ) + .post( + "/api/screens/usage/:sourceId", + authorized(permissions.BUILDER), + controller.usage + ) export default router diff --git a/packages/server/src/sdk/app/common/index.ts b/packages/server/src/sdk/app/common/index.ts new file mode 100644 index 0000000000..3eeaeaa90c --- /dev/null +++ b/packages/server/src/sdk/app/common/index.ts @@ -0,0 +1 @@ +export * from "./utils" diff --git a/packages/server/src/sdk/app/common/utils.ts b/packages/server/src/sdk/app/common/utils.ts new file mode 100644 index 0000000000..ed4e4c9a4f --- /dev/null +++ b/packages/server/src/sdk/app/common/utils.ts @@ -0,0 +1,15 @@ +import { SourceType } from "@budibase/types" +import { docIds } from "@budibase/backend-core" + +export function getSourceType(sourceId: string): SourceType { + if (docIds.isTableId(sourceId)) { + return SourceType.TABLE + } else if (docIds.isViewId(sourceId)) { + return SourceType.VIEW + } else if (docIds.isDatasourceId(sourceId)) { + return SourceType.DATASOURCE + } else if (docIds.isQueryId(sourceId)) { + return SourceType.QUERY + } + throw new Error("Unknown source type - cannot find document type") +} diff --git a/packages/server/src/sdk/index.ts b/packages/server/src/sdk/index.ts index a871546b60..91dd3dffff 100644 --- a/packages/server/src/sdk/index.ts +++ b/packages/server/src/sdk/index.ts @@ -11,6 +11,7 @@ import { default as plugins } from "./plugins" import * as views from "./app/views" import * as permissions from "./app/permissions" import * as rowActions from "./app/rowActions" +import * as common from "./app/common" const sdk = { backups, @@ -26,6 +27,7 @@ const sdk = { permissions, links, rowActions, + common, } // default export for TS diff --git a/packages/types/src/api/web/app/screen.ts b/packages/types/src/api/web/app/screen.ts index 372d1ba2ea..f7cd5c0bf7 100644 --- a/packages/types/src/api/web/app/screen.ts +++ b/packages/types/src/api/web/app/screen.ts @@ -15,3 +15,10 @@ export interface SaveScreenResponse extends Screen {} export interface DeleteScreenResponse { message: string } + +export interface UsageScreenResponse { + screens: { + url: string + _id: string + }[] +} diff --git a/packages/types/src/documents/app/datasource.ts b/packages/types/src/documents/app/datasource.ts index a0be7bd80d..46d2a81de0 100644 --- a/packages/types/src/documents/app/datasource.ts +++ b/packages/types/src/documents/app/datasource.ts @@ -57,3 +57,10 @@ export interface RestConfig { } dynamicVariables?: DynamicVariable[] } + +export enum SourceType { + DATASOURCE = "datasource", + QUERY = "query", + TABLE = "table", + VIEW = "view", +} From 47fdd0f64ffd0ccd3ec80b8e04031565fbcffe2b Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 30 Jan 2025 17:55:53 +0000 Subject: [PATCH 222/321] Still requires tests, but main body of endpoint to check screen usage. --- packages/server/src/api/controllers/screen.ts | 28 ++++++++++++------- packages/server/src/sdk/app/screens/index.ts | 1 + .../server/src/sdk/app/screens/screens.ts | 15 ++++++++++ packages/server/src/sdk/index.ts | 2 ++ .../shared-core/src/sdk/documents/index.ts | 1 + .../shared-core/src/sdk/documents/screens.ts | 24 ++++++++++++++++ packages/shared-core/src/utils.ts | 17 +++++++++++ packages/types/src/api/web/app/screen.ts | 15 ++++++---- 8 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 packages/server/src/sdk/app/screens/index.ts create mode 100644 packages/server/src/sdk/app/screens/screens.ts create mode 100644 packages/shared-core/src/sdk/documents/screens.ts diff --git a/packages/server/src/api/controllers/screen.ts b/packages/server/src/api/controllers/screen.ts index 99a6b5ab52..403059efde 100644 --- a/packages/server/src/api/controllers/screen.ts +++ b/packages/server/src/api/controllers/screen.ts @@ -1,4 +1,4 @@ -import { getScreenParams, generateScreenID, DocumentType } from "../../db/utils" +import { generateScreenID, DocumentType } from "../../db/utils" import { events, context, @@ -17,20 +17,14 @@ import { SaveScreenResponse, DeleteScreenResponse, UsageScreenResponse, + ScreenUsage, } from "@budibase/types" import { builderSocket } from "../../websockets" import sdk from "../../sdk" +import { sdk as sharedSdk } from "@budibase/shared-core" export async function fetch(ctx: UserCtx) { - const db = context.getAppDB() - - const screens = ( - await db.allDocs( - getScreenParams(null, { - include_docs: true, - }) - ) - ).rows.map((el: any) => el.doc) + const screens = await sdk.screens.fetch() const roleId = ctx.user?.role?._id as string if (!roleId) { @@ -146,4 +140,18 @@ function findPlugins(component: ScreenProps, foundPlugins: string[]) { export async function usage(ctx: UserCtx) { const sourceId = ctx.params.sourceId const sourceType = sdk.common.getSourceType(sourceId) + const allScreens = await sdk.screens.fetch() + const response: ScreenUsage[] = [] + for (let screen of allScreens) { + if (sharedSdk.screens.findInSettings(screen, sourceId)) { + response.push({ + url: screen.routing.route, + _id: screen._id!, + }) + } + } + ctx.body = { + sourceType, + screens: response, + } } diff --git a/packages/server/src/sdk/app/screens/index.ts b/packages/server/src/sdk/app/screens/index.ts new file mode 100644 index 0000000000..84f4bad6bb --- /dev/null +++ b/packages/server/src/sdk/app/screens/index.ts @@ -0,0 +1 @@ +export * from "./screens" diff --git a/packages/server/src/sdk/app/screens/screens.ts b/packages/server/src/sdk/app/screens/screens.ts new file mode 100644 index 0000000000..c600825efb --- /dev/null +++ b/packages/server/src/sdk/app/screens/screens.ts @@ -0,0 +1,15 @@ +import { getScreenParams } from "../../../db/utils" +import { context } from "@budibase/backend-core" +import { Screen } from "@budibase/types" + +export async function fetch(): Promise { + const db = context.getAppDB() + + return ( + await db.allDocs( + getScreenParams(null, { + include_docs: true, + }) + ) + ).rows.map(el => el.doc!) +} diff --git a/packages/server/src/sdk/index.ts b/packages/server/src/sdk/index.ts index 91dd3dffff..e3e88c25c4 100644 --- a/packages/server/src/sdk/index.ts +++ b/packages/server/src/sdk/index.ts @@ -11,6 +11,7 @@ import { default as plugins } from "./plugins" import * as views from "./app/views" import * as permissions from "./app/permissions" import * as rowActions from "./app/rowActions" +import * as screens from "./app/screens" import * as common from "./app/common" const sdk = { @@ -23,6 +24,7 @@ const sdk = { datasources, queries, plugins, + screens, views, permissions, links, diff --git a/packages/shared-core/src/sdk/documents/index.ts b/packages/shared-core/src/sdk/documents/index.ts index 4b17c1ea08..502e968a15 100644 --- a/packages/shared-core/src/sdk/documents/index.ts +++ b/packages/shared-core/src/sdk/documents/index.ts @@ -1,3 +1,4 @@ export * as applications from "./applications" export * as automations from "./automations" export * as users from "./users" +export * as screens from "./screens" diff --git a/packages/shared-core/src/sdk/documents/screens.ts b/packages/shared-core/src/sdk/documents/screens.ts new file mode 100644 index 0000000000..8fdd7087c8 --- /dev/null +++ b/packages/shared-core/src/sdk/documents/screens.ts @@ -0,0 +1,24 @@ +import { Screen } from "@budibase/types" +import { flattenObject } from "../../utils" + +export function findInSettings(screen: Screen, toFind: string) { + const flattened = flattenObject(screen.props) + const foundIn: { setting: string; value: string }[] = [] + for (let key of Object.keys(flattened)) { + let found = false + if (typeof flattened[key] === "string") { + found = flattened[key].includes(toFind) + } else if (Array.isArray(flattened[key])) { + found = flattened[key].find( + (el: any) => typeof el === "string" && el.includes(toFind) + ) + } + if (found) { + foundIn.push({ + setting: key, + value: flattened[key], + }) + } + } + return foundIn +} diff --git a/packages/shared-core/src/utils.ts b/packages/shared-core/src/utils.ts index fac8fa61ee..afc1c8872b 100644 --- a/packages/shared-core/src/utils.ts +++ b/packages/shared-core/src/utils.ts @@ -173,3 +173,20 @@ export function processSearchFilters( ], } } + +export function flattenObject( + obj: Record, + parentKey: string = "", + state: Record = {} +) { + for (const key of Object.keys(obj)) { + const newKey = parentKey.length ? `${parentKey}.${key}` : key + const value = obj[key] + if (value && typeof value === "object" && !Array.isArray(value)) { + flattenObject(value, newKey, state) + } else { + state[newKey] = value + } + } + return state +} diff --git a/packages/types/src/api/web/app/screen.ts b/packages/types/src/api/web/app/screen.ts index f7cd5c0bf7..280c623216 100644 --- a/packages/types/src/api/web/app/screen.ts +++ b/packages/types/src/api/web/app/screen.ts @@ -1,4 +1,4 @@ -import { ScreenRoutingJson, Screen } from "../../../documents" +import { ScreenRoutingJson, Screen, SourceType } from "../../../documents" export interface FetchScreenRoutingResponse { routes: ScreenRoutingJson @@ -16,9 +16,12 @@ export interface DeleteScreenResponse { message: string } -export interface UsageScreenResponse { - screens: { - url: string - _id: string - }[] +export interface ScreenUsage { + url: string + _id: string +} + +export interface UsageScreenResponse { + sourceType: SourceType + screens: ScreenUsage[] } From 3b0c4d1008a13a33bb1c5ec68b3f6cf5531169cb Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 31 Jan 2025 09:08:19 +0100 Subject: [PATCH 223/321] Use existing definitions instead of using the manifest --- packages/builder/src/helpers/screen.ts | 46 ------------- .../src/stores/builder/screenComponent.ts | 69 ++++++++++++++++--- 2 files changed, 60 insertions(+), 55 deletions(-) delete mode 100644 packages/builder/src/helpers/screen.ts diff --git a/packages/builder/src/helpers/screen.ts b/packages/builder/src/helpers/screen.ts deleted file mode 100644 index 296a597adb..0000000000 --- a/packages/builder/src/helpers/screen.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, Screen, ScreenProps } from "@budibase/types" -import clientManifest from "@budibase/client/manifest.json" - -export function findComponentsBySettingsType( - screen: Screen, - type: string | string[] -) { - const typesArray = Array.isArray(type) ? type : [type] - - const result: { - component: Component - setting: { - type: string - key: string - } - }[] = [] - function recurseFieldComponentsInChildren(component: ScreenProps) { - if (!component) { - return - } - - const definition = getManifestDefinition(component) - const setting = - "settings" in definition && - definition.settings.find((s: any) => typesArray.includes(s.type)) - if (setting && "type" in setting) { - result.push({ - component, - setting: { type: setting.type!, key: setting.key! }, - }) - } - component._children?.forEach(child => { - recurseFieldComponentsInChildren(child) - }) - } - - recurseFieldComponentsInChildren(screen?.props) - return result -} - -function getManifestDefinition(component: Component) { - const componentType = component._component.split("/").slice(-1)[0] - const definition = - clientManifest[componentType as keyof typeof clientManifest] - return definition -} diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index 226d233090..640d4e6db3 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -1,13 +1,18 @@ -import { derived } from "svelte/store" +import { derived, get } from "svelte/store" import { tables } from "./tables" import { selectedScreen } from "./screens" import { viewsV2 } from "./viewsV2" -import { findComponentsBySettingsType } from "@/helpers/screen" -import { UIDatasourceType, Screen } from "@budibase/types" +import { + UIDatasourceType, + Screen, + Component, + ScreenProps, +} from "@budibase/types" import { queries } from "./queries" import { views } from "./views" import { bindings, featureFlag } from "@/helpers" import { getBindableProperties } from "@/dataBinding" +import { ComponentDefinition, componentStore } from "./components" function reduceBy( key: TKey, @@ -38,11 +43,15 @@ const validationKeyByType: Record = { } export const screenComponentErrors = derived( - [selectedScreen, tables, views, viewsV2, queries], - ([$selectedScreen, $tables, $views, $viewsV2, $queries]): Record< - string, - string[] - > => { + [selectedScreen, tables, views, viewsV2, queries, componentStore], + ([ + $selectedScreen, + $tables, + $views, + $viewsV2, + $queries, + $componentStore, + ]): Record => { if (!featureFlag.isEnabled("CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS")) { return {} } @@ -51,9 +60,11 @@ export const screenComponentErrors = derived( datasources: Record ) { const result: Record = {} + for (const { component, setting } of findComponentsBySettingsType( screen, - ["table", "dataSource"] + ["table", "dataSource"], + $componentStore.components )) { const componentSettings = component[setting.key] if (!componentSettings) { @@ -111,3 +122,43 @@ export const screenComponentErrors = derived( return getInvalidDatasources($selectedScreen, datasources) } ) + +function findComponentsBySettingsType( + screen: Screen, + type: string | string[], + definitions: Record +) { + const typesArray = Array.isArray(type) ? type : [type] + + const result: { + component: Component + setting: { + type: string + key: string + } + }[] = [] + + function recurseFieldComponentsInChildren(component: ScreenProps) { + if (!component) { + return + } + + const definition = definitions[component._component] + + const setting = definition?.settings?.find((s: any) => + typesArray.includes(s.type) + ) + if (setting && "type" in setting) { + result.push({ + component, + setting: { type: setting.type!, key: setting.key! }, + }) + } + component._children?.forEach(child => { + recurseFieldComponentsInChildren(child) + }) + } + + recurseFieldComponentsInChildren(screen?.props) + return result +} From fdb16f463e7028eb71d180f41b75b7fdd1232821 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 31 Jan 2025 09:08:54 +0100 Subject: [PATCH 224/321] Don't allow importing the manifest --- eslint-local-rules/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js index 9348706399..d9d894c33e 100644 --- a/eslint-local-rules/index.js +++ b/eslint-local-rules/index.js @@ -41,12 +41,11 @@ module.exports = { if ( /^@budibase\/[^/]+\/.*$/.test(importPath) && importPath !== "@budibase/backend-core/tests" && - importPath !== "@budibase/string-templates/test/utils" && - importPath !== "@budibase/client/manifest.json" + importPath !== "@budibase/string-templates/test/utils" ) { context.report({ node, - message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests, @budibase/string-templates/test/utils and @budibase/client/manifest.json.`, + message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`, }) } }, From 4ea8b60b3b5961429b6a6d0874624261a4ce3fc1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 12:48:59 +0100 Subject: [PATCH 225/321] Renames --- packages/builder/src/stores/builder/components.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 90e1abfecf..043c2da275 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -452,7 +452,7 @@ export class ComponentStore extends BudiStore { * @returns */ createInstance( - componentName: string, + componentType: string, presetProps: any, parent: any ): Component | null { @@ -461,11 +461,13 @@ export class ComponentStore extends BudiStore { throw "A valid screen must be selected" } - const definition = this.getDefinition(componentName) + const definition = this.getDefinition(componentType) if (!definition) { return null } + let componentName = `New ${definition.friendlyName || definition.name}` + // Generate basic component structure let instance: Component = { _id: Helpers.uuid(), @@ -475,7 +477,7 @@ export class ComponentStore extends BudiStore { hover: {}, active: {}, }, - _instanceName: `New ${definition.friendlyName || definition.name}`, + _instanceName: componentName, ...presetProps, } @@ -500,7 +502,7 @@ export class ComponentStore extends BudiStore { } // Add step name to form steps - if (componentName.endsWith("/formstep")) { + if (componentType.endsWith("/formstep")) { const parentForm = findClosestMatchingComponent( screen.props, get(selectedComponent)?._id, @@ -529,14 +531,14 @@ export class ComponentStore extends BudiStore { * @returns */ async create( - componentName: string, + componentType: string, presetProps: any, parent: Component, index: number ) { const state = get(this.store) const componentInstance = this.createInstance( - componentName, + componentType, presetProps, parent ) From b7ce306e6a0949fb4f34c7e15bccb2b7a1f3c00f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 16:30:55 +0100 Subject: [PATCH 226/321] Add counter --- .../builder/src/stores/builder/components.ts | 9 +++++++++ packages/builder/src/stores/builder/index.js | 3 ++- .../src/stores/builder/screenComponent.ts | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 043c2da275..7aedf05438 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -20,6 +20,7 @@ import { previewStore, tables, componentTreeNodesStore, + screenComponents, } from "@/stores/builder" import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding" import { @@ -467,6 +468,14 @@ export class ComponentStore extends BudiStore { } let componentName = `New ${definition.friendlyName || definition.name}` + const $screenComponents = get(screenComponents) + + const sameNameCount = $screenComponents.filter(c => + new RegExp(`^${componentName}( \\d*)?$`).test(c._instanceName) + ).length + if (sameNameCount) { + componentName = `${componentName} ${sameNameCount + 1}` + } // Generate basic component structure let instance: Component = { diff --git a/packages/builder/src/stores/builder/index.js b/packages/builder/src/stores/builder/index.js index 54c1aa39a2..7dd7f67828 100644 --- a/packages/builder/src/stores/builder/index.js +++ b/packages/builder/src/stores/builder/index.js @@ -16,7 +16,7 @@ import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js" import { deploymentStore } from "./deployments.js" import { contextMenuStore } from "./contextMenu.js" import { snippets } from "./snippets" -import { screenComponentErrors } from "./screenComponent" +import { screenComponents, screenComponentErrors } from "./screenComponent" // Backend import { tables } from "./tables" @@ -68,6 +68,7 @@ export { snippets, rowActions, appPublished, + screenComponents, screenComponentErrors, } diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index 226d233090..56e9d311a4 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -3,11 +3,12 @@ import { tables } from "./tables" import { selectedScreen } from "./screens" import { viewsV2 } from "./viewsV2" import { findComponentsBySettingsType } from "@/helpers/screen" -import { UIDatasourceType, Screen } from "@budibase/types" +import { UIDatasourceType, Screen, Component } from "@budibase/types" import { queries } from "./queries" import { views } from "./views" import { bindings, featureFlag } from "@/helpers" import { getBindableProperties } from "@/dataBinding" +import { findAllComponents } from "@/helpers/components" function reduceBy( key: TKey, @@ -111,3 +112,16 @@ export const screenComponentErrors = derived( return getInvalidDatasources($selectedScreen, datasources) } ) + +export const screenComponents = derived( + [selectedScreen], + ([$selectedScreen]) => { + if (!$selectedScreen) { + return [] + } + const allComponents = findAllComponents( + $selectedScreen.props + ) as Component[] + return allComponents + } +) From 6f2d279f0e48c0c3557dca806ff715036b22bb33 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 16:47:04 +0100 Subject: [PATCH 227/321] Use existing utils --- packages/builder/src/helpers/duplicate.ts | 6 ++++-- .../builder/src/stores/builder/components.ts | 18 +++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/builder/src/helpers/duplicate.ts b/packages/builder/src/helpers/duplicate.ts index b4740b3e52..eb705c6525 100644 --- a/packages/builder/src/helpers/duplicate.ts +++ b/packages/builder/src/helpers/duplicate.ts @@ -76,13 +76,15 @@ export const getSequentialName = ( { getName, numberFirstItem, + separator = "", }: { getName?: (item: T) => string numberFirstItem?: boolean + separator?: string } = {} ) => { if (!prefix?.length) { - return null + return "" } const trimmedPrefix = prefix.trim() const firstName = numberFirstItem ? `${prefix}1` : trimmedPrefix @@ -107,5 +109,5 @@ export const getSequentialName = ( max = num } }) - return max === 0 ? firstName : `${prefix}${max + 1}` + return max === 0 ? firstName : `${prefix}${separator}${max + 1}` } diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 7aedf05438..6693cbf4dc 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -38,6 +38,7 @@ import { Table, } from "@budibase/types" import { utils } from "@budibase/shared-core" +import { getSequentialName } from "@/helpers/duplicate" interface Component extends ComponentType { _id: string @@ -467,15 +468,14 @@ export class ComponentStore extends BudiStore { return null } - let componentName = `New ${definition.friendlyName || definition.name}` - const $screenComponents = get(screenComponents) - - const sameNameCount = $screenComponents.filter(c => - new RegExp(`^${componentName}( \\d*)?$`).test(c._instanceName) - ).length - if (sameNameCount) { - componentName = `${componentName} ${sameNameCount + 1}` - } + const componentName = getSequentialName( + get(screenComponents), + `New ${definition.friendlyName || definition.name}`, + { + getName: c => c._instanceName, + separator: " ", + } + ) // Generate basic component structure let instance: Component = { From 2f4e798cf592d12681371d005b49c3c0279c3b20 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 Jan 2025 16:57:48 +0100 Subject: [PATCH 228/321] Fix test --- packages/builder/src/helpers/tests/duplicate.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/helpers/tests/duplicate.test.ts b/packages/builder/src/helpers/tests/duplicate.test.ts index 131e76a6c2..5e956d7b1c 100644 --- a/packages/builder/src/helpers/tests/duplicate.test.ts +++ b/packages/builder/src/helpers/tests/duplicate.test.ts @@ -49,7 +49,7 @@ describe("getSequentialName", () => { it("handles nullish prefix", async () => { const name = getSequentialName([], null) - expect(name).toBe(null) + expect(name).toBe("") }) it("handles just the prefix", async () => { From aace287ded0e17dbb45a5af9c91b182742d74f64 Mon Sep 17 00:00:00 2001 From: mikesealey Date: Fri, 31 Jan 2025 10:40:03 +0000 Subject: [PATCH 229/321] fixes spelling of delimiters --- packages/server/src/utilities/csv.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/utilities/csv.ts b/packages/server/src/utilities/csv.ts index eb42d118ca..d87d327f7d 100644 --- a/packages/server/src/utilities/csv.ts +++ b/packages/server/src/utilities/csv.ts @@ -1,9 +1,9 @@ import csv from "csvtojson" export async function jsonFromCsvString(csvString: string) { - const possibleDelimeters = [",", ";", ":", "|", "~", "\t", " "] + const possibleDelimiters = [",", ";", ":", "|", "~", "\t", " "] - for (let i = 0; i < possibleDelimeters.length; i++) { + for (let i = 0; i < possibleDelimiters.length; i++) { let headers: string[] | undefined = undefined let headerMismatch = false @@ -14,7 +14,7 @@ export async function jsonFromCsvString(csvString: string) { // with the keys but empty values const result = await csv({ ignoreEmpty: false, - delimiter: possibleDelimeters[i], + delimiter: possibleDelimiters[i], }).fromString(csvString) for (const [, r] of result.entries()) { // The purpose of this is to find rows that have been split From f6b6ac78102c767fd437fdd615215b0ea19a9f95 Mon Sep 17 00:00:00 2001 From: mikesealey Date: Fri, 31 Jan 2025 10:51:22 +0000 Subject: [PATCH 230/321] more descriptive variable name for 'row' --- packages/server/src/utilities/csv.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/utilities/csv.ts b/packages/server/src/utilities/csv.ts index d87d327f7d..ac9304b12a 100644 --- a/packages/server/src/utilities/csv.ts +++ b/packages/server/src/utilities/csv.ts @@ -16,13 +16,13 @@ export async function jsonFromCsvString(csvString: string) { ignoreEmpty: false, delimiter: possibleDelimiters[i], }).fromString(csvString) - for (const [, r] of result.entries()) { + for (const [, row] of result.entries()) { // The purpose of this is to find rows that have been split // into the wrong number of columns - Any valid .CSV file will have // the same number of colums in each row // If the number of columms in each row is different to // the number of headers, this isn't the right delimiter - const columns = Object.keys(r) + const columns = Object.keys(row) if (headers == null) { headers = columns } @@ -32,8 +32,8 @@ export async function jsonFromCsvString(csvString: string) { } for (const header of headers) { - if (r[header] === undefined || r[header] === "") { - r[header] = null + if (row[header] === undefined || row[header] === "") { + row[header] = null } } } From a340ec0d29f0e466c769d52d76001f805fa3579c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 31 Jan 2025 09:10:07 +0100 Subject: [PATCH 231/321] Update feature flag --- packages/builder/src/stores/builder/screenComponent.ts | 2 +- packages/types/src/sdk/featureFlag.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index 684c08605a..f1a9440c02 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -53,7 +53,7 @@ export const screenComponentErrors = derived( $queries, $componentStore, ]): Record => { - if (!featureFlag.isEnabled("CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS")) { + if (!featureFlag.isEnabled("CHECK_COMPONENT_SETTINGS_ERRORS")) { return {} } function getInvalidDatasources( diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts index d9f092c80a..97893a1b5e 100644 --- a/packages/types/src/sdk/featureFlag.ts +++ b/packages/types/src/sdk/featureFlag.ts @@ -1,6 +1,6 @@ export enum FeatureFlag { USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR", - CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS = "CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS", + CHECK_COMPONENT_SETTINGS_ERRORS = "CHECK_COMPONENT_SETTINGS_ERRORS", // Account-portal DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL", @@ -8,7 +8,7 @@ export enum FeatureFlag { export const FeatureFlagDefaults = { [FeatureFlag.USE_ZOD_VALIDATOR]: false, - [FeatureFlag.CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS]: false, + [FeatureFlag.CHECK_COMPONENT_SETTINGS_ERRORS]: false, // Account-portal [FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false, From 4a68d1d09be3087f63058ef47d1010608db1916b Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 31 Jan 2025 11:24:51 +0000 Subject: [PATCH 232/321] Bump version to 3.3.6 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index c16b958d24..15f405c847 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.3.5", + "version": "3.3.6", "npmClient": "yarn", "concurrency": 20, "command": { From 233112e30fe9ccfe21c7abfaf799cadf21f657d3 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Fri, 31 Jan 2025 11:25:57 +0000 Subject: [PATCH 233/321] pr feedback --- .../[screenId]/_components/StatePanel.svelte | 122 ++++++------------ .../builder/src/stores/builder/components.ts | 1 + 2 files changed, 41 insertions(+), 82 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index f62adf3c9f..5b8b0e1794 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -87,7 +87,7 @@ if (isStateUpdateHandler(handler)) { foundComponents.push({ id: componentId, - name: instanceName, + name: instanceName + " - " + setting, settings: [setting], }) } @@ -103,20 +103,22 @@ ) }) - Object.entries(component).forEach(([propName, propValue]) => { - if (Array.isArray(propValue)) { - propValue.forEach(item => { - eventHandlerProps.forEach(eventType => { - checkEventHandlers( - item[eventType], - component._id!, - component._instanceName, - propName - ) + Object.entries(component) + .filter(([key]) => key !== "_children") + .forEach(([propName, propValue]) => { + if (Array.isArray(propValue)) { + propValue.forEach(item => { + eventHandlerProps.forEach(eventType => { + checkEventHandlers( + item[eventType], + component._id!, + component._instanceName, + propName + ) + }) }) - }) - } - }) + } + }) if (component._children) { for (let child of component._children) { @@ -140,84 +142,41 @@ return bindings.join(" ").includes(stateKey) } - const getSettingsWithState = (component: any, stateKey: string): string[] => { - const settingsWithState: string[] = [] - - const searchForStateBinding = (value: any, path: string[]) => { - if (typeof value === "string") { - if (hasStateBinding(value, stateKey)) { - const topLevelProperty = path[0] - if (!settingsWithState.includes(topLevelProperty)) { - settingsWithState.push(topLevelProperty) - } - } - } else if (Array.isArray(value)) { - value.forEach((item, index) => { - searchForStateBinding(item, [...path, `${index}`]) - }) - } else if (typeof value === "object" && value !== null) { - Object.entries(value).forEach(([key, val]) => { - searchForStateBinding(val, [...path, key]) - }) - } - } - - Object.entries(component).forEach(([key, value]) => { - if (["_children", "_styles", "_conditions"].includes(key)) return - - searchForStateBinding(value, [key]) - }) - - return settingsWithState + const getSettingsWithState = ( + component: Component, + stateKey: string + ): string[] => { + return Object.entries(component) + .filter(([key]) => key !== "_children") + .filter(([_, value]) => hasStateBinding(JSON.stringify(value), stateKey)) + .map(([key]) => key) } - - const checkConditions = (conditions: any[], stateKey: string): boolean => { - return conditions.some(condition => - [condition.referenceValue, condition.newValue].some( - value => typeof value === "string" && hasStateBinding(value, stateKey) - ) - ) - } - - const checkStyles = (styles: any, stateKey: string): boolean => { - return ( - typeof styles?.custom === "string" && - hasStateBinding(styles.custom, stateKey) - ) - } - const findComponentsUsingState = ( - component: any, + component: Component, stateKey: string ): ComponentUsingState[] => { let componentsUsingState: ComponentUsingState[] = [] - const { _children, _styles, _conditions, ...componentSettings } = component + const { _children } = component - const settingsWithState = getSettingsWithState(componentSettings, stateKey) + const settingsWithState = getSettingsWithState(component, stateKey) settingsWithState.forEach(setting => { + if (setting === "_conditions") { + setting = "Conditions" + } else if (setting === "_styles") { + setting = "Styles" + } + const label = + componentStore + .getDefinition(component._component) + ?.settings?.find(t => t.key === setting)?.label || setting + componentsUsingState.push({ - id: component._id, - name: `${component._instanceName} - ${setting}`, + id: component._id!, + name: `${component._instanceName} - ${label}`, settings: [setting], }) }) - if (_conditions?.length > 0 && checkConditions(_conditions, stateKey)) { - componentsUsingState.push({ - id: component._id, - name: `${component._instanceName} - conditions`, - settings: ["_conditions"], - }) - } - - if (_styles && checkStyles(_styles, stateKey)) { - componentsUsingState.push({ - id: component._id, - name: `${component._instanceName} - styles`, - settings: ["_styles"], - }) - } - if (_children) { for (let child of _children) { componentsUsingState = [ @@ -234,7 +193,6 @@ if (!stateKey || !$selectedScreen?.props) { return } - const componentStateUpdates = findComponentsUpdatingState( $selectedScreen.props, stateKey @@ -254,7 +212,7 @@ ) .map(() => ({ id: $selectedScreen._id!, - name: "Screen onLoad", + name: "Screen - onLoad", settings: ["onLoad"], })) || [] diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 4e25a919ac..c1c11442a9 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -67,6 +67,7 @@ export interface ComponentDefinition { export interface ComponentSetting { key: string type: string + label?: string section?: string name?: string defaultValue?: any From 833403df37709f72662158aa6e4f3cb753e299d0 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Fri, 31 Jan 2025 11:58:03 +0000 Subject: [PATCH 234/321] small update to handle special case --- .../design/[screenId]/_components/StatePanel.svelte | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte index 5b8b0e1794..8394d3166f 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/StatePanel.svelte @@ -160,15 +160,17 @@ const settingsWithState = getSettingsWithState(component, stateKey) settingsWithState.forEach(setting => { + const label = + componentStore + .getDefinition(component._component) + ?.settings?.find(t => t.key === setting)?.label || setting + + // These have no label so have to set manually if (setting === "_conditions") { setting = "Conditions" } else if (setting === "_styles") { setting = "Styles" } - const label = - componentStore - .getDefinition(component._component) - ?.settings?.find(t => t.key === setting)?.label || setting componentsUsingState.push({ id: component._id!, @@ -193,6 +195,7 @@ if (!stateKey || !$selectedScreen?.props) { return } + console.log($selectedScreen) const componentStateUpdates = findComponentsUpdatingState( $selectedScreen.props, stateKey From 141fc902c3c50c3c2809dcf1f494800b8294ec2f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 31 Jan 2025 13:28:38 +0100 Subject: [PATCH 235/321] Remove unnecessary "in" checks --- packages/builder/src/stores/builder/screenComponent.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index 3338796283..de63c3fbb3 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -136,11 +136,8 @@ function getMissingRequiredSettings( const result: Record = {} for (const component of allComponents) { const definition = definitions[component._component] - if (!("settings" in definition)) { - continue - } - const missingRequiredSettings = definition.settings?.filter( + const missingRequiredSettings = definition?.settings?.filter( (setting: any) => { let empty = component[setting.key] == null || component[setting.key] === "" @@ -258,10 +255,10 @@ function findComponentsBySettingsType( const setting = definition?.settings?.find((s: any) => typesArray.includes(s.type) ) - if (setting && "type" in setting) { + if (setting) { result.push({ component, - setting: { type: setting.type!, key: setting.key! }, + setting: { type: setting.type, key: setting.key }, }) } component._children?.forEach(child => { From ea2b7b879154eab0f9520fce268fe06b1991509b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 31 Jan 2025 13:55:15 +0100 Subject: [PATCH 236/321] Move utils and types --- .../builder/src/stores/builder/components.ts | 26 ++-------------- packages/client/src/utils/componentProps.js | 23 -------------- .../frontend-core/src/utils/components.ts | 26 ++++++++++++++++ packages/frontend-core/src/utils/index.ts | 1 + packages/types/src/ui/components/index.ts | 30 +++++++++++++++++++ 5 files changed, 59 insertions(+), 47 deletions(-) create mode 100644 packages/frontend-core/src/utils/components.ts diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index f0dbc560c1..84ec963406 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -32,6 +32,8 @@ import { import { BudiStore } from "../BudiStore" import { Utils } from "@budibase/frontend-core" import { + ComponentDefinition, + ComponentSetting, Component as ComponentType, FieldType, Screen, @@ -53,30 +55,6 @@ export interface ComponentState { selectedScreenId?: string | null } -export interface ComponentDefinition { - component: string - name: string - friendlyName?: string - hasChildren?: boolean - settings?: ComponentSetting[] - features?: Record - typeSupportPresets?: Record - legalDirectChildren: string[] - requiredAncestors?: string[] - illegalChildren: string[] -} - -export interface ComponentSetting { - key: string - type: string - section?: string - name?: string - defaultValue?: any - selectAllFields?: boolean - resetOn?: string | string[] - settings?: ComponentSetting[] -} - export const INITIAL_COMPONENTS_STATE: ComponentState = { components: {}, customComponents: [], diff --git a/packages/client/src/utils/componentProps.js b/packages/client/src/utils/componentProps.js index bdf74c8014..eec284e3d8 100644 --- a/packages/client/src/utils/componentProps.js +++ b/packages/client/src/utils/componentProps.js @@ -97,26 +97,3 @@ export const propsUseBinding = (props, bindingKey) => { } return false } - -/** - * Gets the definition of this component's settings from the manifest - */ -export const getSettingsDefinition = definition => { - if (!definition) { - return [] - } - let settings = [] - definition.settings?.forEach(setting => { - if (setting.section) { - settings = settings.concat( - (setting.settings || [])?.map(childSetting => ({ - ...childSetting, - sectionDependsOn: setting.dependsOn, - })) - ) - } else { - settings.push(setting) - } - }) - return settings -} diff --git a/packages/frontend-core/src/utils/components.ts b/packages/frontend-core/src/utils/components.ts new file mode 100644 index 0000000000..60eb520258 --- /dev/null +++ b/packages/frontend-core/src/utils/components.ts @@ -0,0 +1,26 @@ +import { ComponentDefinition, ComponentSetting } from "@budibase/types" + +/** + * Gets the definition of this component's settings from the manifest + */ +export const getSettingsDefinition = ( + definition: ComponentDefinition +): ComponentSetting[] => { + if (!definition) { + return [] + } + let settings: ComponentSetting[] = [] + definition.settings?.forEach(setting => { + if (setting.section) { + settings = settings.concat( + (setting.settings || [])?.map(childSetting => ({ + ...childSetting, + sectionDependsOn: setting.dependsOn, + })) + ) + } else { + settings.push(setting) + } + }) + return settings +} diff --git a/packages/frontend-core/src/utils/index.ts b/packages/frontend-core/src/utils/index.ts index efc694d268..3fa9f66b22 100644 --- a/packages/frontend-core/src/utils/index.ts +++ b/packages/frontend-core/src/utils/index.ts @@ -13,3 +13,4 @@ export * from "./download" export * from "./settings" export * from "./relatedColumns" export * from "./table" +export * from "./components" diff --git a/packages/types/src/ui/components/index.ts b/packages/types/src/ui/components/index.ts index 611f92b9a4..61b0c6e564 100644 --- a/packages/types/src/ui/components/index.ts +++ b/packages/types/src/ui/components/index.ts @@ -1,3 +1,33 @@ export * from "./sidepanel" export * from "./codeEditor" export * from "./errors" + +export interface ComponentDefinition { + component: string + name: string + friendlyName?: string + hasChildren?: boolean + settings?: ComponentSetting[] + features?: Record + typeSupportPresets?: Record + legalDirectChildren: string[] + requiredAncestors?: string[] + illegalChildren: string[] +} + +export interface ComponentSetting { + key: string + type: string + section?: string + name?: string + defaultValue?: any + selectAllFields?: boolean + resetOn?: string | string[] + settings?: ComponentSetting[] + dependsOn?: + | string + | { + setting: string + value: string + } +} From 9976bcf125e45cbecb70fd8eb28467d195f1fc8c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 31 Jan 2025 13:55:27 +0100 Subject: [PATCH 237/321] Handle sections properly --- .../src/stores/builder/screenComponent.ts | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index de63c3fbb3..1d82924463 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -8,13 +8,15 @@ import { Component, UIComponentError, ScreenProps, + ComponentDefinition, } from "@budibase/types" import { queries } from "./queries" import { views } from "./views" import { findAllComponents } from "@/helpers/components" import { bindings, featureFlag } from "@/helpers" import { getBindableProperties } from "@/dataBinding" -import { componentStore, ComponentDefinition } from "./components" +import { componentStore } from "./components" +import { getSettingsDefinition } from "@budibase/frontend-core" function reduceBy( key: TKey, @@ -137,41 +139,41 @@ function getMissingRequiredSettings( for (const component of allComponents) { const definition = definitions[component._component] - const missingRequiredSettings = definition?.settings?.filter( - (setting: any) => { - let empty = - component[setting.key] == null || component[setting.key] === "" - let missing = setting.required && empty + const settings = getSettingsDefinition(definition) - // Check if this setting depends on another, as it may not be required - if (setting.dependsOn) { - const dependsOnKey = setting.dependsOn.setting || setting.dependsOn - const dependsOnValue = setting.dependsOn.value - const realDependentValue = component[dependsOnKey] + const missingRequiredSettings = settings.filter((setting: any) => { + let empty = + component[setting.key] == null || component[setting.key] === "" + let missing = setting.required && empty - const sectionDependsOnKey = - setting.sectionDependsOn?.setting || setting.sectionDependsOn - const sectionDependsOnValue = setting.sectionDependsOn?.value - const sectionRealDependentValue = component[sectionDependsOnKey] + // Check if this setting depends on another, as it may not be required + if (setting.dependsOn) { + const dependsOnKey = setting.dependsOn.setting || setting.dependsOn + const dependsOnValue = setting.dependsOn.value + const realDependentValue = component[dependsOnKey] - if (dependsOnValue == null && realDependentValue == null) { - return false - } - if (dependsOnValue != null && dependsOnValue !== realDependentValue) { - return false - } + const sectionDependsOnKey = + setting.sectionDependsOn?.setting || setting.sectionDependsOn + const sectionDependsOnValue = setting.sectionDependsOn?.value + const sectionRealDependentValue = component[sectionDependsOnKey] - if ( - sectionDependsOnValue != null && - sectionDependsOnValue !== sectionRealDependentValue - ) { - return false - } + if (dependsOnValue == null && realDependentValue == null) { + return false + } + if (dependsOnValue != null && dependsOnValue !== realDependentValue) { + return false } - return missing + if ( + sectionDependsOnValue != null && + sectionDependsOnValue !== sectionRealDependentValue + ) { + return false + } } - ) + + return missing + }) if (missingRequiredSettings?.length) { result[component._id!] = missingRequiredSettings.map((s: any) => ({ From 1274593cb20d81ae27b00eb74e0fdae30854bad2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 31 Jan 2025 13:57:37 +0100 Subject: [PATCH 238/321] Fix types --- packages/builder/src/stores/builder/screens.ts | 2 +- packages/client/src/components/Component.svelte | 7 ++----- .../devtools/DevToolsComponentSettingsTab.svelte | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index 64fe31752d..3f098334f0 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -19,8 +19,8 @@ import { Screen, Component, SaveScreenResponse, + ComponentDefinition, } from "@budibase/types" -import { ComponentDefinition } from "./components" interface ScreenState { screens: Screen[] diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index e023214083..3e22ffada1 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -11,11 +11,8 @@ - - -
    -

    - Are you sure you wish to delete the table - - - - {table.name} - ? - -

    - -

    All table data will be deleted{viewsMessage}.

    -

    This action cannot be undone.

    - - {#if screensPossiblyAffected.length > 0} -
    - -
      - {#each screensPossiblyAffected as item} -
    • - {item.text} -
    • - {/each} -
    -
    -
    - {/if} -

    Please enter the table name below to confirm.

    - -
    -
    - - diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte index f3deccb3c9..fe9d02af40 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte @@ -8,7 +8,7 @@ import NavItem from "@/components/common/NavItem.svelte" import { isActive } from "@roxi/routify" import EditModal from "./EditModal.svelte" - import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte" + import DeleteConfirmationModal from "../../modals/DeleteDataConfirmationModal.svelte" import { Icon } from "@budibase/bbui" import { DB_TYPE_EXTERNAL } from "@/constants/backend" @@ -65,4 +65,4 @@ {/if} - + diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index 62d55d2283..033358b7b3 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -1,16 +1,24 @@

    - Are you sure you wish to delete the {type} + Are you sure you wish to delete the {sourceType} @@ -83,13 +141,15 @@

    -

    All {type} data will be deleted{viewsMessage}.

    +

    + All {sourceType} data will be deleted{viewsMessage}. +

    This action cannot be undone.

    {#if affectedScreens.length > 0}
      {#each affectedScreens as item} diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/DeleteViewModal.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/DeleteViewModal.svelte deleted file mode 100644 index 5a23c976f8..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/DeleteViewModal.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte index e9c5745cc6..f32767451a 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte @@ -10,9 +10,8 @@ import { Icon, ActionButton, ActionMenu, MenuItem } from "@budibase/bbui" import { params, url } from "@roxi/routify" import EditViewModal from "./EditViewModal.svelte" - import DeleteViewModal from "./DeleteViewModal.svelte" import EditTableModal from "@/components/backend/TableNavigator/TableNavItem/EditModal.svelte" - import DeleteTableModal from "@/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte" + import DeleteConfirmationModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte" import { UserAvatars } from "@budibase/frontend-core" import { DB_TYPE_EXTERNAL } from "@/constants/backend" import { TableNames } from "@/constants" @@ -314,12 +313,12 @@ {#if table && tableEditable} - + {/if} {#if editableView} - + {/if} diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index 033358b7b3..ace6dd696f 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -3,6 +3,7 @@ import { appStore, datasources, + queries, screenStore, tables, views, @@ -106,6 +107,20 @@ } } + async function deleteQuery(query: Query) { + try { + // Go back to the datasource if we are deleting the active query + if ($queries.selectedQueryId === query._id) { + $goto(`./datasource/${query.datasourceId}`) + } + await queries.delete(query) + await datasources.fetch() + notifications.success("Query deleted") + } catch (error) { + notifications.error("Error deleting query") + } + } + async function deleteSource() { if (!source || !sourceType) { throw new Error("Unable to delete - no data source found.") @@ -116,8 +131,9 @@ return await deleteTable(source as Table) case SourceType.VIEW: return await deleteView(source as ViewV2) - case SourceType.DATASOURCE: case SourceType.QUERY: + return await deleteQuery(source as Query) + case SourceType.DATASOURCE: } } @@ -164,7 +180,9 @@
    {/if}

    - Please enter the "{source?.name}" below to confirm. + Please enter the "{source?.name}" below to confirm.

    From 01f1fc656f991812ba2ed1d887ec07362db3d6b8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 4 Feb 2025 10:15:19 +0100 Subject: [PATCH 272/321] Remove flag --- packages/builder/src/stores/builder/screenComponent.ts | 7 ++----- packages/types/src/sdk/featureFlag.ts | 2 -- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index d1999a9fd5..62bc97cc27 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -13,7 +13,7 @@ import { import { queries } from "./queries" import { views } from "./views" import { findAllComponents } from "@/helpers/components" -import { bindings, featureFlag } from "@/helpers" +import { bindings } from "@/helpers" import { getBindableProperties } from "@/dataBinding" import { componentStore } from "./components" import { getSettingsDefinition } from "@budibase/frontend-core" @@ -56,10 +56,7 @@ export const screenComponentErrors = derived( $queries, $componentStore, ]): Record => { - if ( - !featureFlag.isEnabled("CHECK_COMPONENT_SETTINGS_ERRORS") || - !$selectedScreen - ) { + if (!$selectedScreen) { return {} } diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts index 97893a1b5e..996d3bba8d 100644 --- a/packages/types/src/sdk/featureFlag.ts +++ b/packages/types/src/sdk/featureFlag.ts @@ -1,6 +1,5 @@ export enum FeatureFlag { USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR", - CHECK_COMPONENT_SETTINGS_ERRORS = "CHECK_COMPONENT_SETTINGS_ERRORS", // Account-portal DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL", @@ -8,7 +7,6 @@ export enum FeatureFlag { export const FeatureFlagDefaults = { [FeatureFlag.USE_ZOD_VALIDATOR]: false, - [FeatureFlag.CHECK_COMPONENT_SETTINGS_ERRORS]: false, // Account-portal [FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false, From 29fde6c4272097de9a0c38cf83de1f7167b48d7c Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Tue, 4 Feb 2025 09:17:24 +0000 Subject: [PATCH 273/321] Fix for visual bug in Picker component --- packages/bbui/src/Form/Core/Picker.svelte | 6 +++--- packages/bbui/src/Form/Core/Select.svelte | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 8541858923..404d36f468 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -34,12 +34,12 @@ export let getOptionValue = (option: O, _index?: number) => option as unknown as V export let getOptionIcon = (option: O, _index?: number) => - option as unknown as O - export let useOptionIconImage = false + option?.icon ?? null export let getOptionColour = (option: O, _index?: number) => option as unknown as O export let getOptionSubtitle = (option: O, _index?: number) => - option as unknown as O + option?.subtitle ?? null + export let useOptionIconImage = false export let open: boolean = false export let readonly: boolean = false export let quiet: boolean = false diff --git a/packages/bbui/src/Form/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index 2957c0fec2..4a921f163c 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -17,11 +17,11 @@ export let getOptionValue = (option: O, _index?: number) => option as unknown as V export let getOptionIcon = (option: O, _index?: number) => - option as unknown as string + option?.icon ?? undefined export let getOptionColour = (option: O, _index?: number) => - option as unknown as string + option?.colour ?? undefined export let getOptionSubtitle = (option: O, _index?: number) => - option as unknown as string + option?.subtitle ?? undefined export let compare = (option: O, value: V) => option === value export let useOptionIconImage = false export let isOptionEnabled = (option: O, _index?: number) => From 67339ebfcde49db449fce1d49ed2751bccf320fc Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Tue, 4 Feb 2025 09:29:51 +0000 Subject: [PATCH 274/321] align picker and select --- packages/bbui/src/Form/Core/Picker.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 404d36f468..cac6f523c6 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -34,11 +34,11 @@ export let getOptionValue = (option: O, _index?: number) => option as unknown as V export let getOptionIcon = (option: O, _index?: number) => - option?.icon ?? null + option?.icon ?? undefined export let getOptionColour = (option: O, _index?: number) => - option as unknown as O + option?.colour ?? undefined export let getOptionSubtitle = (option: O, _index?: number) => - option?.subtitle ?? null + option?.subtitle ?? undefined export let useOptionIconImage = false export let open: boolean = false export let readonly: boolean = false From 251974d0b3b25ce630c80c54e6c7dab755d72037 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 4 Feb 2025 09:55:32 +0000 Subject: [PATCH 275/321] Bump version to 3.4.1 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 6ed1f62d3c..faedd55ccb 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.4.0", + "version": "3.4.1", "npmClient": "yarn", "concurrency": 20, "command": { From 72a645b80e05bbc73645e197ec948106f0487a56 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 4 Feb 2025 10:43:49 +0000 Subject: [PATCH 276/321] Fix row.spec.ts --- .../tests/core/utilities/mocks/licenses.ts | 39 ++++++++++++------- .../server/src/api/routes/tests/row.spec.ts | 18 +++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts index 5ba6fb36a1..436e915b81 100644 --- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts @@ -1,5 +1,12 @@ -import { Feature, License, Quotas } from "@budibase/types" +import { + Feature, + License, + MonthlyQuotaName, + QuotaType, + QuotaUsageType, +} from "@budibase/types" import cloneDeep from "lodash/cloneDeep" +import merge from "lodash/merge" let CLOUD_FREE_LICENSE: License let UNLIMITED_LICENSE: License @@ -27,18 +34,19 @@ export function initInternal(opts: { export interface UseLicenseOpts { features?: Feature[] - quotas?: Quotas + monthlyQuotas?: [MonthlyQuotaName, number][] } // LICENSES export const useLicense = (license: License, opts?: UseLicenseOpts) => { - if (opts) { - if (opts.features) { - license.features.push(...opts.features) - } - if (opts.quotas) { - license.quotas = opts.quotas + if (opts?.features) { + license.features.push(...opts.features) + } + if (opts?.monthlyQuotas) { + for (const [name, value] of opts.monthlyQuotas) { + license.quotas[QuotaType.USAGE][QuotaUsageType.MONTHLY][name].value = + value } } @@ -57,12 +65,9 @@ export const useCloudFree = () => { // FEATURES -const useFeature = (feature: Feature) => { +const useFeature = (feature: Feature, extra?: Partial) => { const license = cloneDeep(getCachedLicense() || UNLIMITED_LICENSE) - const opts: UseLicenseOpts = { - features: [feature], - } - + const opts: UseLicenseOpts = merge({ features: [feature] }, extra) return useLicense(license, opts) } @@ -102,8 +107,12 @@ export const useAppBuilders = () => { return useFeature(Feature.APP_BUILDERS) } -export const useBudibaseAI = () => { - return useFeature(Feature.BUDIBASE_AI) +export const useBudibaseAI = (opts?: { monthlyQuota?: number }) => { + return useFeature(Feature.BUDIBASE_AI, { + monthlyQuotas: [ + [MonthlyQuotaName.BUDIBASE_AI_CREDITS, opts?.monthlyQuota || 1000], + ], + }) } export const useAICustomConfigs = () => { diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 576f0bb663..70a0bb2de9 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -39,6 +39,7 @@ import { RowExportFormat, RelationSchemaField, FormulaResponseType, + MonthlyQuotaName, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" import _, { merge } from "lodash" @@ -169,6 +170,18 @@ if (descriptions.length) { ) } + const resetRowUsage = async () => { + await config.doInContext( + undefined, + async () => + await quotas.setUsage( + 0, + StaticQuotaName.ROWS, + QuotaUsageType.STATIC + ) + ) + } + const getRowUsage = async () => { const { total } = await config.doInContext(undefined, () => quotas.getCurrentUsageValues( @@ -206,6 +219,10 @@ if (descriptions.length) { table = await config.api.table.save(defaultTable()) }) + beforeEach(async () => { + await resetRowUsage() + }) + describe("create", () => { it("creates a new row successfully", async () => { const rowUsage = await getRowUsage() @@ -3317,6 +3334,7 @@ if (descriptions.length) { beforeAll(async () => { mocks.licenses.useBudibaseAI() mocks.licenses.useAICustomConfigs() + envCleanup = setEnv({ OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd", }) From 32b0e34b663e3001c9d9497eba1e2820b9744ce4 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 4 Feb 2025 10:56:08 +0000 Subject: [PATCH 277/321] respond to pr feedback --- packages/server/src/automations/triggers.ts | 8 ++++---- packages/server/src/automations/utils.ts | 9 ++++++--- packages/server/src/sdk/app/automations/crud.ts | 4 ++-- packages/server/src/sdk/app/rowActions/crud.ts | 4 ++-- packages/server/tsconfig.json | 14 +------------- 5 files changed, 15 insertions(+), 24 deletions(-) diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index bfd1b5c47f..2ac90f3f9c 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -1,7 +1,7 @@ import emitter from "../events/index" import { getAutomationParams, isDevAppID } from "../db/utils" import { coerce } from "../utilities/rowProcessor" -import { definitions } from "../../../shared-core/src/automations/triggers" +import { automations } from "@budibase/shared-core" // need this to call directly, so we can get a response import { automationQueue } from "./bullboard" import { checkTestFlag } from "../utilities/redis" @@ -26,7 +26,7 @@ import { import { executeInThread } from "../threads/automation" import { dataFilters, sdk } from "@budibase/shared-core" -export const TRIGGER_DEFINITIONS = definitions +export const TRIGGER_DEFINITIONS = automations.triggers.definitions const JOB_OPTS = { removeOnComplete: true, removeOnFail: true, @@ -273,8 +273,8 @@ async function checkTriggerFilters( } if ( - trigger.stepId === definitions.ROW_UPDATED.stepId || - trigger.stepId === definitions.ROW_SAVED.stepId + trigger.stepId === automations.triggers.definitions.ROW_UPDATED.stepId || + trigger.stepId === automations.triggers.definitions.ROW_SAVED.stepId ) { const newRow = await automationUtils.cleanUpRow(tableId, event.row) return rowPassesFilters(newRow, filters) diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 74cc64a95e..9bfcc6cf8a 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -1,5 +1,5 @@ import { Thread, ThreadType } from "../threads" -import { definitions } from "../../../shared-core/src/automations/triggers" +import { automations } from "@budibase/shared-core" import { automationQueue } from "./bullboard" import { updateEntityMetadata } from "../utilities" import { context, db as dbCore, utils } from "@budibase/backend-core" @@ -19,7 +19,7 @@ import { automationsEnabled } from "../features" import { helpers, REBOOT_CRON } from "@budibase/shared-core" import tracer from "dd-trace" -const CRON_STEP_ID = definitions.CRON.stepId +const CRON_STEP_ID = automations.triggers.definitions.CRON.stepId let Runner: Thread if (automationsEnabled()) { Runner = new Thread(ThreadType.AUTOMATION) @@ -255,7 +255,10 @@ export async function cleanupAutomations(appId: any) { * @return if it is recurring (cron). */ export function isRecurring(automation: Automation) { - return automation.definition.trigger.stepId === definitions.CRON.stepId + return ( + automation.definition.trigger.stepId === + automations.triggers.definitions.CRON.stepId + ) } export function isErrorInOutput(output: { diff --git a/packages/server/src/sdk/app/automations/crud.ts b/packages/server/src/sdk/app/automations/crud.ts index 487f93cfa6..cd8af1e548 100644 --- a/packages/server/src/sdk/app/automations/crud.ts +++ b/packages/server/src/sdk/app/automations/crud.ts @@ -13,7 +13,7 @@ import { HTTPError, db as dbCore, } from "@budibase/backend-core" -import { definitions } from "../../../../../shared-core/src/automations/triggers" +import { automations as sharedAutomations } from "@budibase/shared-core" import automations from "." export interface PersistedAutomation extends Automation { @@ -202,7 +202,7 @@ export async function remove(automationId: string, rev: string) { * written to DB (this does not write to DB as it would be wasteful to repeat). */ async function checkForWebhooks({ oldAuto, newAuto }: any) { - const WH_STEP_ID = definitions.WEBHOOK.stepId + const WH_STEP_ID = sharedAutomations.triggers.definitions.WEBHOOK.stepId const appId = context.getAppId() if (!appId) { diff --git a/packages/server/src/sdk/app/rowActions/crud.ts b/packages/server/src/sdk/app/rowActions/crud.ts index ca6f7b8f1a..64bb4729ce 100644 --- a/packages/server/src/sdk/app/rowActions/crud.ts +++ b/packages/server/src/sdk/app/rowActions/crud.ts @@ -9,7 +9,7 @@ import { } from "@budibase/types" import { generateRowActionsID } from "../../../db/utils" import automations from "../automations" -import { definitions as TRIGGER_DEFINITIONS } from "../../../../../shared-core/src/automations/triggers" +import { automations as sharedAutomations } from "@budibase/shared-core" import * as triggers from "../../../automations/triggers" import sdk from "../.." @@ -59,7 +59,7 @@ export async function create(tableId: string, rowAction: { name: string }) { definition: { trigger: { id: "trigger", - ...TRIGGER_DEFINITIONS.ROW_ACTION, + ...sharedAutomations.triggers.definitions.ROW_ACTION, stepId: AutomationTriggerStepId.ROW_ACTION, inputs: { tableId, diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 475f0b3e2d..c962785810 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -4,18 +4,6 @@ "require": ["tsconfig-paths/register"], "swc": true }, - "include": [ - "src/**/*", - "specs", - "__mocks__", - "../shared-core/src/automations/triggers/app.ts", - "../shared-core/src/automations/triggers/cron.ts", - "../shared-core/src/automations/triggers/index.ts", - "../shared-core/src/automations/triggers/rowAction.ts", - "../shared-core/src/automations/triggers/rowDeleted.ts", - "../shared-core/src/automations/triggers/rowSaved.ts", - "../shared-core/src/automations/triggers/rowUpdated.ts", - "../shared-core/src/automations/triggers/webhook.ts" - ], + "include": ["src/**/*", "specs", "__mocks__"], "exclude": ["node_modules", "dist"] } From 0a27087f5c1c23d8d755217a3521388132906a69 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 4 Feb 2025 10:58:22 +0000 Subject: [PATCH 278/321] missed a bit --- packages/server/src/threads/automation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 1a56139e52..174efa0fe0 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -11,7 +11,7 @@ import { replaceFakeBindings } from "../automations/loopUtils" import { dataFilters, helpers, utils } from "@budibase/shared-core" import { default as AutomationEmitter } from "../events/AutomationEmitter" import { generateAutomationMetadataID, isProdAppID } from "../db/utils" -import { definitions as triggerDefs } from "../../../shared-core/src/automations/triggers" +import { automations } from "@budibase/shared-core" import { AutomationErrors, MAX_AUTOMATION_RECURRING_ERRORS } from "../constants" import { storeLog } from "../automations/logging" import { @@ -50,7 +50,7 @@ import env from "../environment" import tracer from "dd-trace" threadUtils.threadSetup() -const CRON_STEP_ID = triggerDefs.CRON.stepId +const CRON_STEP_ID = automations.triggers.definitions.CRON.stepId const STOPPED_STATUS = { success: true, status: AutomationStatus.STOPPED } function getLoopIterations(loopStep: LoopStep) { From b491d8c784eab30b128e6c5d648bfffbbcc3a7eb Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 4 Feb 2025 11:16:27 +0000 Subject: [PATCH 279/321] Fix a few more test files. --- .../server/src/api/routes/tests/row.spec.ts | 1 - .../src/automations/tests/branching.spec.ts | 43 +++---------------- .../automations/tests/steps/updateRow.spec.ts | 30 +++---------- .../tests/triggers/webhook.spec.ts | 11 ++--- 4 files changed, 15 insertions(+), 70 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 70a0bb2de9..87002670b7 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -39,7 +39,6 @@ import { RowExportFormat, RelationSchemaField, FormulaResponseType, - MonthlyQuotaName, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" import _, { merge } from "lodash" diff --git a/packages/server/src/automations/tests/branching.spec.ts b/packages/server/src/automations/tests/branching.spec.ts index c2e3f50b8a..1a514e0537 100644 --- a/packages/server/src/automations/tests/branching.spec.ts +++ b/packages/server/src/automations/tests/branching.spec.ts @@ -24,11 +24,7 @@ describe("Branching automations", () => { const branch2LogId = "33333333-3333-3333-3333-333333333333" const branch2Id = "44444444-4444-4444-4444-444444444444" - const builder = createAutomationBuilder({ - name: "Test Trigger with Loop and Create Row", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: {} }) .serverLog( { text: "Starting automation" }, @@ -87,11 +83,7 @@ describe("Branching automations", () => { }) it("should execute correct branch based on string equality", async () => { - const builder = createAutomationBuilder({ - name: "String Equality Branching", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: { status: "active" } }) .branch({ activeBranch: { @@ -116,11 +108,7 @@ describe("Branching automations", () => { }) it("should handle multiple conditions with AND operator", async () => { - const builder = createAutomationBuilder({ - name: "Multiple AND Conditions Branching", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: { status: "active", role: "admin" } }) .branch({ activeAdminBranch: { @@ -148,11 +136,7 @@ describe("Branching automations", () => { }) it("should handle multiple conditions with OR operator", async () => { - const builder = createAutomationBuilder({ - name: "Multiple OR Conditions Branching", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: { status: "test", role: "user" } }) .branch({ specialBranch: { @@ -184,11 +168,7 @@ describe("Branching automations", () => { }) it("should stop the branch automation when no conditions are met", async () => { - const builder = createAutomationBuilder({ - name: "Multiple OR Conditions Branching", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: { status: "test", role: "user" } }) .createRow({ row: { name: "Test", tableId: table._id } }) .branch({ @@ -215,7 +195,6 @@ describe("Branching automations", () => { }, }, }) - .serverLog({ text: "Test" }) .run() expect(results.steps[1].outputs.status).toEqual( @@ -225,11 +204,7 @@ describe("Branching automations", () => { }) it("evaluate multiple conditions", async () => { - const builder = createAutomationBuilder({ - name: "evaluate multiple conditions", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: { test_trigger: true } }) .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .branch({ @@ -270,11 +245,7 @@ describe("Branching automations", () => { }) it("evaluate multiple conditions with interpolated text", async () => { - const builder = createAutomationBuilder({ - name: "evaluate multiple conditions", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: { test_trigger: true } }) .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .branch({ diff --git a/packages/server/src/automations/tests/steps/updateRow.spec.ts b/packages/server/src/automations/tests/steps/updateRow.spec.ts index 5dc80e2df0..79fed5f613 100644 --- a/packages/server/src/automations/tests/steps/updateRow.spec.ts +++ b/packages/server/src/automations/tests/steps/updateRow.spec.ts @@ -30,11 +30,7 @@ describe("test the update row action", () => { }) it("should be able to run the update row action", async () => { - const builder = createAutomationBuilder({ - name: "Update Row Automation", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: {} }) .updateRow({ rowId: row._id!, @@ -57,11 +53,7 @@ describe("test the update row action", () => { }) it("should check invalid inputs return an error", async () => { - const builder = createAutomationBuilder({ - name: "Invalid Inputs Automation", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: {} }) .updateRow({ meta: {}, row: {}, rowId: "" }) .run() @@ -70,11 +62,7 @@ describe("test the update row action", () => { }) it("should return an error when table doesn't exist", async () => { - const builder = createAutomationBuilder({ - name: "Nonexistent Table Automation", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: {} }) .updateRow({ row: { _id: "invalid" }, @@ -118,11 +106,7 @@ describe("test the update row action", () => { user2: [{ _id: user2._id }], }) - const builder = createAutomationBuilder({ - name: "Link Preservation Automation", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: {} }) .updateRow({ rowId: row._id!, @@ -176,11 +160,7 @@ describe("test the update row action", () => { user2: [{ _id: user2._id }], }) - const builder = createAutomationBuilder({ - name: "Link Overwrite Automation", - }) - - const results = await builder + const results = await createAutomationBuilder({ config }) .appAction({ fields: {} }) .updateRow({ rowId: row._id!, diff --git a/packages/server/src/automations/tests/triggers/webhook.spec.ts b/packages/server/src/automations/tests/triggers/webhook.spec.ts index bce454371f..2e0df5de49 100644 --- a/packages/server/src/automations/tests/triggers/webhook.spec.ts +++ b/packages/server/src/automations/tests/triggers/webhook.spec.ts @@ -11,11 +11,8 @@ describe("Branching automations", () => { let table: Table let webhook: Webhook - async function createWebhookAutomation(testName: string) { - const builder = createAutomationBuilder({ - name: testName, - }) - const automation = await builder + async function createWebhookAutomation() { + const automation = await createAutomationBuilder({ config }) .webhook({ fields: { parameter: "string" } }) .createRow({ row: { tableId: table._id!, name: "{{ trigger.parameter }}" }, @@ -50,9 +47,7 @@ describe("Branching automations", () => { }) it("should run the webhook automation - checking for parameters", async () => { - const { webhook } = await createWebhookAutomation( - "Check a basic webhook works as expected" - ) + const { webhook } = await createWebhookAutomation() const res = await config.api.webhook.trigger( config.getProdAppId(), webhook._id!, From 9ad4e1048acf0b210f992c05b02e95de63ca27eb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 4 Feb 2025 12:00:42 +0000 Subject: [PATCH 280/321] Detect iframe clicks via loss of focus --- packages/bbui/src/Actions/click_outside.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/bbui/src/Actions/click_outside.ts b/packages/bbui/src/Actions/click_outside.ts index 248a03039e..0c2eb036bc 100644 --- a/packages/bbui/src/Actions/click_outside.ts +++ b/packages/bbui/src/Actions/click_outside.ts @@ -34,7 +34,7 @@ let candidateTarget: HTMLElement | undefined // Processes a "click outside" event and invokes callbacks if our source element // is valid const handleClick = (e: MouseEvent) => { - const target = e.target as HTMLElement + const target = (e.target || e.relatedTarget) as HTMLElement // Ignore click if this is an ignored class if (target.closest('[data-ignore-click-outside="true"]')) { @@ -91,9 +91,19 @@ const handleMouseDown = (e: MouseEvent) => { document.addEventListener("click", handleMouseUp, true) } +// Handle iframe clicks by detecting a loss of focus on the main window +const handleBlur = () => { + if (document.activeElement?.tagName === "IFRAME") { + handleClick( + new MouseEvent("click", { relatedTarget: document.activeElement }) + ) + } +} + // Global singleton listeners for our events document.addEventListener("mousedown", handleMouseDown) document.addEventListener("contextmenu", handleClick) +window.addEventListener("blur", handleBlur) /** * Adds or updates a click handler From bd6d6534bef7feb9e6fe083c7125c5f88d5f877e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 4 Feb 2025 15:14:58 +0000 Subject: [PATCH 281/321] Some typing improvements in automation queues. --- .../backend-core/src/queue/inMemoryQueue.ts | 7 +++-- packages/server/src/automations/bullboard.ts | 26 +++++++--------- packages/server/src/automations/index.ts | 5 ++- .../automations/tests/triggers/cron.spec.ts | 31 +++++++++++++++++++ packages/server/src/automations/utils.ts | 2 +- 5 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 packages/server/src/automations/tests/triggers/cron.spec.ts diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts index dd8d3daa37..5c52c693fb 100644 --- a/packages/backend-core/src/queue/inMemoryQueue.ts +++ b/packages/backend-core/src/queue/inMemoryQueue.ts @@ -30,9 +30,10 @@ function newJob(queue: string, message: any, opts?: JobOptions): JobMessage { } /** - * This is designed to replicate Bull (https://github.com/OptimalBits/bull) in memory as a sort of mock. - * It is relatively simple, using an event emitter internally to register when messages are available - * to the consumers - in can support many inputs and many consumers. + * This is designed to replicate Bull (https://github.com/OptimalBits/bull) in + * memory as a sort of mock. It is relatively simple, using an event emitter + * internally to register when messages are available to the consumers - in can + * support many inputs and many consumers. */ class InMemoryQueue implements Partial { _name: string diff --git a/packages/server/src/automations/bullboard.ts b/packages/server/src/automations/bullboard.ts index aa4287b2d0..349282e863 100644 --- a/packages/server/src/automations/bullboard.ts +++ b/packages/server/src/automations/bullboard.ts @@ -5,9 +5,9 @@ import * as automation from "../threads/automation" import { backups } from "@budibase/pro" import { getAppMigrationQueue } from "../appMigrations/queue" import { createBullBoard } from "@bull-board/api" -import BullQueue from "bull" +import { AutomationData } from "@budibase/types" -export const automationQueue: BullQueue.Queue = queue.createQueue( +export const automationQueue = queue.createQueue( queue.JobQueue.AUTOMATION, { removeStalledCb: automation.removeStalled } ) @@ -16,24 +16,20 @@ const PATH_PREFIX = "/bulladmin" export async function init() { // Set up queues for bull board admin + const queues = [new BullAdapter(automationQueue)] + const backupQueue = backups.getBackupQueue() - const appMigrationQueue = getAppMigrationQueue() - const queues = [automationQueue] if (backupQueue) { - queues.push(backupQueue) + queues.push(new BullAdapter(backupQueue)) } + + const appMigrationQueue = getAppMigrationQueue() if (appMigrationQueue) { - queues.push(appMigrationQueue) + queues.push(new BullAdapter(appMigrationQueue)) } - const adapters = [] - const serverAdapter: any = new KoaAdapter() - for (let queue of queues) { - adapters.push(new BullAdapter(queue)) - } - createBullBoard({ - queues: adapters, - serverAdapter, - }) + + const serverAdapter = new KoaAdapter() + createBullBoard({ queues, serverAdapter }) serverAdapter.setBasePath(PATH_PREFIX) return serverAdapter.registerPlugin() } diff --git a/packages/server/src/automations/index.ts b/packages/server/src/automations/index.ts index 4ef3210932..5f9ca1aa60 100644 --- a/packages/server/src/automations/index.ts +++ b/packages/server/src/automations/index.ts @@ -1,7 +1,6 @@ import { processEvent } from "./utils" import { automationQueue } from "./bullboard" import { rebootTrigger } from "./triggers" -import BullQueue from "bull" import { automationsEnabled } from "../features" export { automationQueue } from "./bullboard" @@ -25,6 +24,6 @@ export async function init() { return promise } -export function getQueues(): BullQueue.Queue[] { - return [automationQueue] +export function getQueue() { + return automationQueue } diff --git a/packages/server/src/automations/tests/triggers/cron.spec.ts b/packages/server/src/automations/tests/triggers/cron.spec.ts new file mode 100644 index 0000000000..af82eb3797 --- /dev/null +++ b/packages/server/src/automations/tests/triggers/cron.spec.ts @@ -0,0 +1,31 @@ +import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" +import TestConfiguration from "../../../tests/utilities/TestConfiguration" +import { getQueue } from "../.." + +describe("cron trigger", () => { + const config = new TestConfiguration() + + beforeEach(async () => { + await config.init() + }) + + afterAll(() => { + config.end() + }) + + it("should run the webhook automation - checking for parameters", async () => { + const queue = getQueue() + expect(await queue.count()).toEqual(0) + + await createAutomationBuilder({ config }) + .cron({ cron: "* * * * *" }) + .serverLog({ + text: "Hello, world!", + }) + .save() + + await config.publish() + + expect(await queue.count()).toEqual(1) + }) +}) diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 9bfcc6cf8a..83665fc975 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -230,7 +230,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) { // can't use getAppDB here as this is likely to be called from dev app, // but this call could be for dev app or prod app, need to just use what // was passed in - await dbCore.doWithDB(appId, async (db: any) => { + await dbCore.doWithDB(appId, async db => { const response = await db.put(automation) automation._id = response.id automation._rev = response.rev From cbc13ad567342d6dc0a4b071d2dc1614305677a3 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 4 Feb 2025 17:10:22 +0000 Subject: [PATCH 282/321] Updating datasource to delete through it. --- .../DatasourceNavItem.svelte | 7 +++- .../DeleteConfirmationModal.svelte | 37 ------------------- .../modals/DeleteDataConfirmationModal.svelte | 18 ++++++++- 3 files changed, 22 insertions(+), 40 deletions(-) delete mode 100644 packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DeleteConfirmationModal.svelte diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DatasourceNavItem.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DatasourceNavItem.svelte index d538a9b1b7..16423335a6 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DatasourceNavItem.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DatasourceNavItem.svelte @@ -7,7 +7,7 @@ import IntegrationIcon from "@/components/backend/DatasourceNavigator/IntegrationIcon.svelte" import { Icon } from "@budibase/bbui" import UpdateDatasourceModal from "@/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte" - import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte" + import DeleteDataConfirmModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte" export let datasource @@ -71,7 +71,10 @@ {/if} - + From 877f30b7a0abf6c398e820057e979f7d1042060c Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 5 Feb 2025 16:29:21 +0000 Subject: [PATCH 294/321] No input required for deletion. --- .../modals/DeleteDataConfirmationModal.svelte | 82 ++++++++----------- .../components/common/ConfirmDialog.svelte | 2 +- 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index 2e7c8d5078..db13558713 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -1,5 +1,5 @@ - -
    -

    - - {@html buildMessage()} -

    +{#if sourceType} + +
    +

    + + {@html buildMessage()} +

    - {#if affectedScreens.length > 0} -
    - -
      - {#each affectedScreens as item} -
    • - {item.text} -
    • - {/each} -
    -
    -
    - {/if} -
    -
    + {#if affectedScreens.length > 0} +
    + +
      + {#each affectedScreens as item} +
    • + {item.text} +
    • + {/each} +
    +
    +
    + {/if} +
    +
    +{/if} From 74f2ece72b890b4c60cfb5f6fb4a332e6e5fa8f9 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 5 Feb 2025 17:19:11 +0000 Subject: [PATCH 297/321] Refactor automation builder. --- .../tests/triggers/createRow.spec.ts | 67 +++ .../automations/tests/triggers/cron.spec.ts | 2 +- .../tests/triggers/webhook.spec.ts | 2 - .../tests/utilities/AutomationTestBuilder.ts | 445 +++--------------- .../src/documents/app/automation/schema.ts | 23 + 5 files changed, 167 insertions(+), 372 deletions(-) create mode 100644 packages/server/src/automations/tests/triggers/createRow.spec.ts diff --git a/packages/server/src/automations/tests/triggers/createRow.spec.ts b/packages/server/src/automations/tests/triggers/createRow.spec.ts new file mode 100644 index 0000000000..84f136c8a6 --- /dev/null +++ b/packages/server/src/automations/tests/triggers/createRow.spec.ts @@ -0,0 +1,67 @@ +import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" +import TestConfiguration from "../../../tests/utilities/TestConfiguration" +import { getQueue } from "../.." +import { Job } from "bull" +import { basicTable } from "../../../tests/utilities/structures" +import { Table } from "@budibase/types" + +describe("cron trigger", () => { + const config = new TestConfiguration() + let table: Table + + beforeAll(async () => { + await config.init() + table = await config.api.table.save(basicTable()) + }) + + afterAll(() => { + config.end() + }) + + it("should successfully fire", async () => { + const queue = getQueue() + expect(await queue.getCompletedCount()).toEqual(0) + + const jobPromise = new Promise(resolve => { + queue.on("completed", async job => { + resolve(job) + }) + }) + + await createAutomationBuilder({ config }) + .rowSaved({ tableId: table._id! }) + .cron({ cron: "* * * * *" }) + .serverLog({ + text: "Hello, world!", + }) + .save() + + await config.api.application.publish(config.getAppId()) + + expect(await queue.getCompletedCount()).toEqual(1) + + const job = await jobPromise + const repeat = job.opts?.repeat + if (!repeat || !("cron" in repeat)) { + throw new Error("Expected cron repeat") + } + expect(repeat.cron).toEqual("* * * * *") + }) + + it("should fail if the cron expression is invalid", async () => { + await createAutomationBuilder({ config }) + .cron({ cron: "* * * * * *" }) + .serverLog({ + text: "Hello, world!", + }) + .save() + + await config.api.application.publish(config.getAppId(), { + status: 500, + body: { + message: + 'Deployment Failed: Invalid automation CRON "* * * * * *" - Expected 5 values, but got 6.', + }, + }) + }) +}) diff --git a/packages/server/src/automations/tests/triggers/cron.spec.ts b/packages/server/src/automations/tests/triggers/cron.spec.ts index 956f054ca4..baddb1dd51 100644 --- a/packages/server/src/automations/tests/triggers/cron.spec.ts +++ b/packages/server/src/automations/tests/triggers/cron.spec.ts @@ -6,7 +6,7 @@ import { Job } from "bull" describe("cron trigger", () => { const config = new TestConfiguration() - beforeEach(async () => { + beforeAll(async () => { await config.init() }) diff --git a/packages/server/src/automations/tests/triggers/webhook.spec.ts b/packages/server/src/automations/tests/triggers/webhook.spec.ts index 2e0df5de49..a0b5e7f195 100644 --- a/packages/server/src/automations/tests/triggers/webhook.spec.ts +++ b/packages/server/src/automations/tests/triggers/webhook.spec.ts @@ -1,4 +1,3 @@ -import * as automation from "../../index" import { Table, Webhook, WebhookActionType } from "@budibase/types" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" import { mocks } from "@budibase/backend-core/tests" @@ -37,7 +36,6 @@ describe("Branching automations", () => { } beforeEach(async () => { - await automation.init() await config.init() table = await config.createTable() }) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 74e0ae87fb..9484f5cf21 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -2,51 +2,24 @@ import { v4 as uuidv4 } from "uuid" import { BUILTIN_ACTION_DEFINITIONS } from "../../actions" import { TRIGGER_DEFINITIONS } from "../../triggers" import { - AppActionTriggerInputs, AppActionTriggerOutputs, Automation, AutomationActionStepId, AutomationStep, AutomationStepInputs, AutomationTrigger, - AutomationTriggerDefinition, AutomationTriggerInputs, + AutomationTriggerOutputs, AutomationTriggerStepId, - BashStepInputs, - Branch, BranchStepInputs, - CollectStepInputs, - CreateRowStepInputs, - CronTriggerInputs, CronTriggerOutputs, - DelayStepInputs, - DeleteRowStepInputs, - DiscordStepInputs, - ExecuteQueryStepInputs, - ExecuteScriptStepInputs, - FilterStepInputs, isDidNotTriggerResponse, - LoopStepInputs, - MakeIntegrationInputs, - n8nStepInputs, - OpenAIStepInputs, - OutgoingWebhookStepInputs, - QueryRowsStepInputs, - RowCreatedTriggerInputs, RowCreatedTriggerOutputs, - RowDeletedTriggerInputs, RowDeletedTriggerOutputs, - RowUpdatedTriggerInputs, RowUpdatedTriggerOutputs, SearchFilters, - ServerLogStepInputs, - SmtpEmailStepInputs, TestAutomationRequest, - TriggerAutomationStepInputs, - UpdateRowStepInputs, - WebhookTriggerInputs, WebhookTriggerOutputs, - ZapierStepInputs, } from "@budibase/types" import TestConfiguration from "../../../tests/utilities/TestConfiguration" import * as setup from "../utilities" @@ -74,28 +47,53 @@ class BaseStepBuilder { protected steps: AutomationStep[] = [] protected stepNames: { [key: string]: string } = {} - protected step( - stepId: TStep, - stepSchema: Omit, - inputs: AutomationStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - const id = opts?.stepId || uuidv4() - this.steps.push({ - ...stepSchema, - inputs: inputs as any, - id, - stepId, - name: opts?.stepName || stepSchema.name, - }) - if (opts?.stepName) { - this.stepNames[id] = opts.stepName + protected createStepFn(stepId: TStep) { + return ( + inputs: AutomationStepInputs, + opts?: { stepName?: string; stepId?: string } + ) => { + const schema = BUILTIN_ACTION_DEFINITIONS[stepId] + const id = opts?.stepId || uuidv4() + this.steps.push({ + ...schema, + inputs: inputs as any, + id, + stepId, + name: opts?.stepName || schema.name, + }) + if (opts?.stepName) { + this.stepNames[id] = opts.stepName + } + return this } - return this } + + createRow = this.createStepFn(AutomationActionStepId.CREATE_ROW) + updateRow = this.createStepFn(AutomationActionStepId.UPDATE_ROW) + deleteRow = this.createStepFn(AutomationActionStepId.DELETE_ROW) + sendSmtpEmail = this.createStepFn(AutomationActionStepId.SEND_EMAIL_SMTP) + executeQuery = this.createStepFn(AutomationActionStepId.EXECUTE_QUERY) + queryRows = this.createStepFn(AutomationActionStepId.QUERY_ROWS) + loop = this.createStepFn(AutomationActionStepId.LOOP) + serverLog = this.createStepFn(AutomationActionStepId.SERVER_LOG) + executeScript = this.createStepFn(AutomationActionStepId.EXECUTE_SCRIPT) + filter = this.createStepFn(AutomationActionStepId.FILTER) + bash = this.createStepFn(AutomationActionStepId.EXECUTE_BASH) + openai = this.createStepFn(AutomationActionStepId.OPENAI) + collect = this.createStepFn(AutomationActionStepId.COLLECT) + zapier = this.createStepFn(AutomationActionStepId.zapier) + triggerAutomationRun = this.createStepFn( + AutomationActionStepId.TRIGGER_AUTOMATION_RUN + ) + outgoingWebhook = this.createStepFn(AutomationActionStepId.OUTGOING_WEBHOOK) + n8n = this.createStepFn(AutomationActionStepId.n8n) + make = this.createStepFn(AutomationActionStepId.integromat) + discord = this.createStepFn(AutomationActionStepId.discord) + delay = this.createStepFn(AutomationActionStepId.DELAY) + protected addBranchStep(branchConfig: BranchConfig): void { const branchStepInputs: BranchStepInputs = { - branches: [] as Branch[], + branches: [], children: {}, } @@ -118,243 +116,6 @@ class BaseStepBuilder { } this.steps.push(branchStep) } - - // STEPS - createRow( - inputs: CreateRowStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.CREATE_ROW, - BUILTIN_ACTION_DEFINITIONS.CREATE_ROW, - inputs, - opts - ) - } - - updateRow( - inputs: UpdateRowStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.UPDATE_ROW, - BUILTIN_ACTION_DEFINITIONS.UPDATE_ROW, - inputs, - opts - ) - } - - deleteRow( - inputs: DeleteRowStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.DELETE_ROW, - BUILTIN_ACTION_DEFINITIONS.DELETE_ROW, - inputs, - opts - ) - } - - sendSmtpEmail( - inputs: SmtpEmailStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.SEND_EMAIL_SMTP, - BUILTIN_ACTION_DEFINITIONS.SEND_EMAIL_SMTP, - inputs, - opts - ) - } - - executeQuery( - inputs: ExecuteQueryStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.EXECUTE_QUERY, - BUILTIN_ACTION_DEFINITIONS.EXECUTE_QUERY, - inputs, - opts - ) - } - - queryRows( - inputs: QueryRowsStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.QUERY_ROWS, - BUILTIN_ACTION_DEFINITIONS.QUERY_ROWS, - inputs, - opts - ) - } - - loop( - inputs: LoopStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.LOOP, - BUILTIN_ACTION_DEFINITIONS.LOOP, - inputs, - opts - ) - } - - serverLog( - input: ServerLogStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.SERVER_LOG, - BUILTIN_ACTION_DEFINITIONS.SERVER_LOG, - input, - opts - ) - } - - executeScript( - input: ExecuteScriptStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.EXECUTE_SCRIPT, - BUILTIN_ACTION_DEFINITIONS.EXECUTE_SCRIPT, - input, - opts - ) - } - - filter(input: FilterStepInputs): this { - return this.step( - AutomationActionStepId.FILTER, - BUILTIN_ACTION_DEFINITIONS.FILTER, - input - ) - } - - bash( - input: BashStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.EXECUTE_BASH, - BUILTIN_ACTION_DEFINITIONS.EXECUTE_BASH, - input, - opts - ) - } - - openai( - input: OpenAIStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.OPENAI, - BUILTIN_ACTION_DEFINITIONS.OPENAI, - input, - opts - ) - } - - collect( - input: CollectStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.COLLECT, - BUILTIN_ACTION_DEFINITIONS.COLLECT, - input, - opts - ) - } - - zapier( - input: ZapierStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.zapier, - BUILTIN_ACTION_DEFINITIONS.zapier, - input, - opts - ) - } - - triggerAutomationRun( - input: TriggerAutomationStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.TRIGGER_AUTOMATION_RUN, - BUILTIN_ACTION_DEFINITIONS.TRIGGER_AUTOMATION_RUN, - input, - opts - ) - } - - outgoingWebhook( - input: OutgoingWebhookStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.OUTGOING_WEBHOOK, - BUILTIN_ACTION_DEFINITIONS.OUTGOING_WEBHOOK, - input, - opts - ) - } - - n8n( - input: n8nStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.n8n, - BUILTIN_ACTION_DEFINITIONS.n8n, - input, - opts - ) - } - - make( - input: MakeIntegrationInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.integromat, - BUILTIN_ACTION_DEFINITIONS.integromat, - input, - opts - ) - } - - discord( - input: DiscordStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.discord, - BUILTIN_ACTION_DEFINITIONS.discord, - input, - opts - ) - } - - delay( - input: DelayStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.DELAY, - BUILTIN_ACTION_DEFINITIONS.DELAY, - input, - opts - ) - } } class StepBuilder extends BaseStepBuilder { @@ -379,11 +140,17 @@ class AutomationBuilder extends BaseStepBuilder { ) { super() this.config = options.config || setup.getConfig() + this.triggerOutputs = { fields: {} } this.automationConfig = { name: options.name || `Test Automation ${uuidv4()}`, definition: { steps: [], - trigger: {} as AutomationTrigger, + trigger: { + ...TRIGGER_DEFINITIONS[AutomationTriggerStepId.APP], + stepId: AutomationTriggerStepId.APP, + inputs: this.triggerOutputs, + id: uuidv4(), + }, stepNames: {}, }, type: "automation", @@ -391,94 +158,34 @@ class AutomationBuilder extends BaseStepBuilder { } } - // TRIGGERS - rowSaved(inputs: RowCreatedTriggerInputs, outputs: RowCreatedTriggerOutputs) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.ROW_SAVED, - AutomationTriggerStepId.ROW_SAVED, - inputs, - outputs - ) - } - - rowUpdated( - inputs: RowUpdatedTriggerInputs, - outputs: RowUpdatedTriggerOutputs - ) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.ROW_UPDATED, - AutomationTriggerStepId.ROW_UPDATED, - inputs, - outputs - ) - } - - rowDeleted( - inputs: RowDeletedTriggerInputs, - outputs: RowDeletedTriggerOutputs - ) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.ROW_DELETED, - AutomationTriggerStepId.ROW_DELETED, - inputs, - outputs - ) - } - - appAction(outputs: AppActionTriggerOutputs, inputs?: AppActionTriggerInputs) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.APP, - AutomationTriggerStepId.APP, - inputs, - outputs - ) - } - - webhook(outputs: WebhookTriggerOutputs, inputs?: WebhookTriggerInputs) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.WEBHOOK, - AutomationTriggerStepId.WEBHOOK, - inputs, - outputs - ) - } - - cron(inputs: CronTriggerInputs, outputs?: CronTriggerOutputs) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.CRON, - AutomationTriggerStepId.CRON, - inputs, - outputs - ) - } - - private trigger( - triggerSchema: AutomationTriggerDefinition, - stepId: TStep, - inputs?: AutomationTriggerInputs, - outputs?: TriggerOutputs - ): this { - if (this.triggerSet) { - throw new Error("Only one trigger can be set for an automation.") + protected createTriggerFn< + TStep extends AutomationTriggerStepId, + TInput = AutomationTriggerInputs, + TOutput = AutomationTriggerOutputs + >(stepId: TStep) { + return (inputs: TInput, outputs?: TOutput) => { + if (this.triggerSet) { + throw new Error("Only one trigger can be set for an automation.") + } + this.triggerOutputs = outputs as TriggerOutputs | undefined + this.automationConfig.definition.trigger = { + ...TRIGGER_DEFINITIONS[stepId], + stepId, + inputs, + id: uuidv4(), + } as AutomationTrigger + this.triggerSet = true + return this } - this.automationConfig.definition.trigger = { - ...triggerSchema, - stepId, - inputs: inputs || ({} as any), - id: uuidv4(), - } - this.triggerOutputs = outputs - this.triggerSet = true - - return this } + rowSaved = this.createTriggerFn(AutomationTriggerStepId.ROW_SAVED) + rowUpdated = this.createTriggerFn(AutomationTriggerStepId.ROW_UPDATED) + rowDeleted = this.createTriggerFn(AutomationTriggerStepId.ROW_DELETED) + appAction = this.createTriggerFn(AutomationTriggerStepId.APP) + webhook = this.createTriggerFn(AutomationTriggerStepId.WEBHOOK) + cron = this.createTriggerFn(AutomationTriggerStepId.CRON) + branch(branchConfig: BranchConfig): this { this.addBranchStep(branchConfig) return this diff --git a/packages/types/src/documents/app/automation/schema.ts b/packages/types/src/documents/app/automation/schema.ts index 84bfebf6bf..952397b511 100644 --- a/packages/types/src/documents/app/automation/schema.ts +++ b/packages/types/src/documents/app/automation/schema.ts @@ -52,6 +52,12 @@ import { RowDeletedTriggerInputs, BranchStepInputs, BaseAutomationOutputs, + AppActionTriggerOutputs, + CronTriggerOutputs, + RowDeletedTriggerOutputs, + RowCreatedTriggerOutputs, + RowUpdatedTriggerOutputs, + WebhookTriggerOutputs, } from "./StepInputsOutputs" export type ActionImplementations = { @@ -341,6 +347,23 @@ export type AutomationTriggerInputs = ? Record : never +export type AutomationTriggerOutputs = + T extends AutomationTriggerStepId.APP + ? AppActionTriggerOutputs + : T extends AutomationTriggerStepId.CRON + ? CronTriggerOutputs + : T extends AutomationTriggerStepId.ROW_ACTION + ? Record + : T extends AutomationTriggerStepId.ROW_DELETED + ? RowDeletedTriggerOutputs + : T extends AutomationTriggerStepId.ROW_SAVED + ? RowCreatedTriggerOutputs + : T extends AutomationTriggerStepId.ROW_UPDATED + ? RowUpdatedTriggerOutputs + : T extends AutomationTriggerStepId.WEBHOOK + ? WebhookTriggerOutputs + : never + export interface AutomationTriggerSchema< TTrigger extends AutomationTriggerStepId > extends AutomationStepSchemaBase { From d2902464c06ff93f2f0ecb27736fdc9021d1a982 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 5 Feb 2025 17:30:48 +0000 Subject: [PATCH 298/321] Remove no longer needed appAction calls. --- .../src/api/routes/tests/automation.spec.ts | 1 - .../src/automations/tests/branching.spec.ts | 1 - .../src/automations/tests/scenarios.spec.ts | 8 --- .../src/automations/tests/steps/bash.spec.ts | 2 - .../src/automations/tests/steps/delay.spec.ts | 5 +- .../automations/tests/steps/deleteRow.spec.ts | 3 - .../automations/tests/steps/discord.spec.ts | 1 - .../tests/steps/executeScript.spec.ts | 3 - .../automations/tests/steps/filter.spec.ts | 2 - .../src/automations/tests/steps/loop.spec.ts | 9 --- .../src/automations/tests/steps/make.spec.ts | 3 - .../src/automations/tests/steps/n8n.spec.ts | 4 -- .../automations/tests/steps/openai.spec.ts | 4 -- .../tests/steps/outgoingWebhook.spec.ts | 2 - .../automations/tests/steps/queryRows.spec.ts | 6 -- .../automations/tests/steps/serverLog.spec.ts | 1 - .../tests/steps/triggerAutomationRun.spec.ts | 3 - .../automations/tests/steps/updateRow.spec.ts | 5 -- .../automations/tests/steps/zapier.spec.ts | 3 - .../tests/triggers/createRow.spec.ts | 67 ------------------- .../tests/utilities/AutomationTestBuilder.ts | 30 +++++++-- 21 files changed, 24 insertions(+), 139 deletions(-) delete mode 100644 packages/server/src/automations/tests/triggers/createRow.spec.ts diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index 94517db67a..eeceabdb2f 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -257,7 +257,6 @@ describe("/automations", () => { appId: config.getAppId(), config, }) - .appAction({ fields: {} }) .serverLog({ text: "{{ settings.url }}", }) diff --git a/packages/server/src/automations/tests/branching.spec.ts b/packages/server/src/automations/tests/branching.spec.ts index 1a514e0537..e6a250577d 100644 --- a/packages/server/src/automations/tests/branching.spec.ts +++ b/packages/server/src/automations/tests/branching.spec.ts @@ -25,7 +25,6 @@ describe("Branching automations", () => { const branch2Id = "44444444-4444-4444-4444-444444444444" const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .serverLog( { text: "Starting automation" }, { stepName: "FirstLog", stepId: firstLogId } diff --git a/packages/server/src/automations/tests/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios.spec.ts index 8d6e051ce2..6c40b793dd 100644 --- a/packages/server/src/automations/tests/scenarios.spec.ts +++ b/packages/server/src/automations/tests/scenarios.spec.ts @@ -66,7 +66,6 @@ describe("Automation Scenarios", () => { await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .queryRows({ tableId: table._id!, }) @@ -85,7 +84,6 @@ describe("Automation Scenarios", () => { await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .queryRows({ tableId: table._id!, }) @@ -127,7 +125,6 @@ describe("Automation Scenarios", () => { }) const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .createRow( { row: { @@ -196,7 +193,6 @@ describe("Automation Scenarios", () => { await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .queryRows( { tableId: table._id!, @@ -246,7 +242,6 @@ describe("Automation Scenarios", () => { it("should stop an automation if the condition is not met", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .createRow({ row: { name: "Equal Test", @@ -272,7 +267,6 @@ describe("Automation Scenarios", () => { it("should continue the automation if the condition is met", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .createRow({ row: { name: "Not Equal Test", @@ -339,7 +333,6 @@ describe("Automation Scenarios", () => { "should pass the filter when condition is $condition", async ({ condition, value, rowValue, expectPass }) => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .createRow({ row: { name: `${condition} Test`, @@ -389,7 +382,6 @@ describe("Automation Scenarios", () => { it("Check user is passed through from app trigger", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .serverLog({ text: "{{ [user].[email] }}" }) .run() diff --git a/packages/server/src/automations/tests/steps/bash.spec.ts b/packages/server/src/automations/tests/steps/bash.spec.ts index ef10f9b568..edf01bb3e2 100644 --- a/packages/server/src/automations/tests/steps/bash.spec.ts +++ b/packages/server/src/automations/tests/steps/bash.spec.ts @@ -65,7 +65,6 @@ describe("Execute Bash Automations", () => { it("should integrate bash output with row operations", async () => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .queryRows( { tableId: table._id!, @@ -122,7 +121,6 @@ describe("Execute Bash Automations", () => { it("should handle null values gracefully", async () => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .bash( // @ts-expect-error - testing null input { code: null }, diff --git a/packages/server/src/automations/tests/steps/delay.spec.ts b/packages/server/src/automations/tests/steps/delay.spec.ts index a9e97b502f..89b85530e1 100644 --- a/packages/server/src/automations/tests/steps/delay.spec.ts +++ b/packages/server/src/automations/tests/steps/delay.spec.ts @@ -16,10 +16,7 @@ describe("test the delay logic", () => { const time = 100 const before = performance.now() - await createAutomationBuilder({ config }) - .appAction({ fields: {} }) - .delay({ time }) - .run() + await createAutomationBuilder({ config }).delay({ time }).run() const now = performance.now() diff --git a/packages/server/src/automations/tests/steps/deleteRow.spec.ts b/packages/server/src/automations/tests/steps/deleteRow.spec.ts index 1815edb4d3..52a5dacdd7 100644 --- a/packages/server/src/automations/tests/steps/deleteRow.spec.ts +++ b/packages/server/src/automations/tests/steps/deleteRow.spec.ts @@ -21,7 +21,6 @@ describe("test the delete row action", () => { it("should be able to run the delete row action", async () => { await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .deleteRow({ tableId: table._id!, id: row._id!, @@ -36,7 +35,6 @@ describe("test the delete row action", () => { it("should check invalid inputs return an error", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .deleteRow({ tableId: "", id: "", revision: "" }) .run() @@ -45,7 +43,6 @@ describe("test the delete row action", () => { it("should return an error when table doesn't exist", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .deleteRow({ tableId: "invalid", id: "invalid", diff --git a/packages/server/src/automations/tests/steps/discord.spec.ts b/packages/server/src/automations/tests/steps/discord.spec.ts index 5dc9c1088a..9d4d3f6033 100644 --- a/packages/server/src/automations/tests/steps/discord.spec.ts +++ b/packages/server/src/automations/tests/steps/discord.spec.ts @@ -20,7 +20,6 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .discord({ url: "http://www.example.com", username: "joe_bloggs", diff --git a/packages/server/src/automations/tests/steps/executeScript.spec.ts b/packages/server/src/automations/tests/steps/executeScript.spec.ts index 4470b8e760..2d510e86e0 100644 --- a/packages/server/src/automations/tests/steps/executeScript.spec.ts +++ b/packages/server/src/automations/tests/steps/executeScript.spec.ts @@ -21,7 +21,6 @@ describe("Execute Script Automations", () => { it("should execute a basic script and return the result", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .executeScript({ code: "return 2 + 2" }) .run() @@ -44,7 +43,6 @@ describe("Execute Script Automations", () => { it("should handle script execution errors gracefully", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .executeScript({ code: "return nonexistentVariable.map(x => x)" }) .run() @@ -73,7 +71,6 @@ describe("Execute Script Automations", () => { it("should use multiple steps and validate script execution", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .serverLog( { text: "Starting multi-step automation" }, { stepId: "start-log-step" } diff --git a/packages/server/src/automations/tests/steps/filter.spec.ts b/packages/server/src/automations/tests/steps/filter.spec.ts index b0943f3fa8..0d4ac0f01a 100644 --- a/packages/server/src/automations/tests/steps/filter.spec.ts +++ b/packages/server/src/automations/tests/steps/filter.spec.ts @@ -43,7 +43,6 @@ describe("test the filter logic", () => { ] it.each(pass)("should pass %p %p %p", async (field, condition, value) => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .filter({ field, condition: stringToFilterCondition(condition), value }) .run() @@ -61,7 +60,6 @@ describe("test the filter logic", () => { ] it.each(fail)("should fail %p %p %p", async (field, condition, value) => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .filter({ field, condition: stringToFilterCondition(condition), value }) .run() diff --git a/packages/server/src/automations/tests/steps/loop.spec.ts b/packages/server/src/automations/tests/steps/loop.spec.ts index af174c33ca..764f55d9ad 100644 --- a/packages/server/src/automations/tests/steps/loop.spec.ts +++ b/packages/server/src/automations/tests/steps/loop.spec.ts @@ -152,7 +152,6 @@ describe("Attempt to run a basic loop automation", () => { it("if an incorrect type is passed to the loop it should return an error", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .loop({ option: LoopStepType.ARRAY, binding: "1, 2, 3", @@ -168,7 +167,6 @@ describe("Attempt to run a basic loop automation", () => { it("ensure the loop stops if the failure condition is reached", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], @@ -187,7 +185,6 @@ describe("Attempt to run a basic loop automation", () => { it("ensure the loop stops if the max iterations are reached", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], @@ -202,7 +199,6 @@ describe("Attempt to run a basic loop automation", () => { it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], @@ -279,7 +275,6 @@ describe("Attempt to run a basic loop automation", () => { it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], @@ -301,7 +296,6 @@ describe("Attempt to run a basic loop automation", () => { it("should use automation names to loop with", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .loop( { option: LoopStepType.ARRAY, @@ -353,7 +347,6 @@ describe("Attempt to run a basic loop automation", () => { await config.api.row.bulkImport(table._id!, { rows }) const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .queryRows({ tableId: table._id!, }) @@ -433,7 +426,6 @@ describe("Attempt to run a basic loop automation", () => { await config.api.row.bulkImport(table._id!, { rows }) const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .queryRows( { tableId: table._id!, @@ -516,7 +508,6 @@ describe("Attempt to run a basic loop automation", () => { await config.api.row.bulkImport(table._id!, { rows }) const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .queryRows({ tableId: table._id!, }) diff --git a/packages/server/src/automations/tests/steps/make.spec.ts b/packages/server/src/automations/tests/steps/make.spec.ts index 8ba5d3f8b7..a26f9f73ae 100644 --- a/packages/server/src/automations/tests/steps/make.spec.ts +++ b/packages/server/src/automations/tests/steps/make.spec.ts @@ -20,7 +20,6 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .make({ url: "http://www.example.com", body: null, @@ -47,7 +46,6 @@ describe("test the outgoing webhook action", () => { .reply(200, { foo: "bar" }) const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .make({ body: { value: JSON.stringify(payload) }, url: "http://www.example.com", @@ -60,7 +58,6 @@ describe("test the outgoing webhook action", () => { it("should return a 400 if the JSON payload string is malformed", async () => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .make({ body: { value: "{ invalid json }" }, url: "http://www.example.com", diff --git a/packages/server/src/automations/tests/steps/n8n.spec.ts b/packages/server/src/automations/tests/steps/n8n.spec.ts index 4427ea33e0..04d82fcd77 100644 --- a/packages/server/src/automations/tests/steps/n8n.spec.ts +++ b/packages/server/src/automations/tests/steps/n8n.spec.ts @@ -21,7 +21,6 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action and default to 'get'", async () => { nock("http://www.example.com/").get("/").reply(200, { foo: "bar" }) const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .n8n({ url: "http://www.example.com", body: { test: "IGNORE_ME" }, @@ -40,7 +39,6 @@ describe("test the outgoing webhook action", () => { .reply(200) const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .n8n({ url: "http://www.example.com", body: { value: JSON.stringify({ name: "Adam", age: 9 }) }, @@ -54,7 +52,6 @@ describe("test the outgoing webhook action", () => { it("should return a 400 if the JSON payload string is malformed", async () => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .n8n({ url: "http://www.example.com", body: { value: "{ value1 1 }" }, @@ -74,7 +71,6 @@ describe("test the outgoing webhook action", () => { .reply(200) const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .n8n({ url: "http://www.example.com", method: HttpMethod.HEAD, diff --git a/packages/server/src/automations/tests/steps/openai.spec.ts b/packages/server/src/automations/tests/steps/openai.spec.ts index 3030b3db39..7e9fa1f933 100644 --- a/packages/server/src/automations/tests/steps/openai.spec.ts +++ b/packages/server/src/automations/tests/steps/openai.spec.ts @@ -58,7 +58,6 @@ describe("test the openai action", () => { // own API key. We don't count this against your quota. const result = await expectAIUsage(0, () => createAutomationBuilder({ config }) - .appAction({ fields: {} }) .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) .run() ) @@ -70,7 +69,6 @@ describe("test the openai action", () => { it("should present the correct error message when a prompt is not provided", async () => { const result = await expectAIUsage(0, () => createAutomationBuilder({ config }) - .appAction({ fields: {} }) .openai({ prompt: "", model: Model.GPT_4O_MINI }) .run() ) @@ -86,7 +84,6 @@ describe("test the openai action", () => { const result = await expectAIUsage(0, () => createAutomationBuilder({ config }) - .appAction({ fields: {} }) .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) .run() ) @@ -109,7 +106,6 @@ describe("test the openai action", () => { // key, so we charge users for it. const result = await expectAIUsage(14, () => createAutomationBuilder({ config }) - .appAction({ fields: {} }) .openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" }) .run() ) diff --git a/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts b/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts index b1ff5aa264..9ef3cf1e57 100644 --- a/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts +++ b/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts @@ -24,7 +24,6 @@ describe("test the outgoing webhook action", () => { .reply(200, { foo: "bar" }) const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .outgoingWebhook({ requestMethod: RequestType.POST, url: "http://www.example.com", @@ -40,7 +39,6 @@ describe("test the outgoing webhook action", () => { it("should return an error if something goes wrong in fetch", async () => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .outgoingWebhook({ requestMethod: RequestType.GET, url: "www.invalid.com", diff --git a/packages/server/src/automations/tests/steps/queryRows.spec.ts b/packages/server/src/automations/tests/steps/queryRows.spec.ts index 0030e7fc61..1d79d8e046 100644 --- a/packages/server/src/automations/tests/steps/queryRows.spec.ts +++ b/packages/server/src/automations/tests/steps/queryRows.spec.ts @@ -32,7 +32,6 @@ describe("Test a query step automation", () => { name: "Basic Query Test", config, }) - .appAction({ fields: {} }) .queryRows( { tableId: table._id!, @@ -60,7 +59,6 @@ describe("Test a query step automation", () => { name: "Empty Filter Test", config, }) - .appAction({ fields: {} }) .queryRows( { tableId: table._id!, @@ -84,7 +82,6 @@ describe("Test a query step automation", () => { name: "Return None Test", config, }) - .appAction({ fields: {} }) .queryRows( { tableId: table._id!, @@ -109,7 +106,6 @@ describe("Test a query step automation", () => { name: "Null Filter Test", config, }) - .appAction({ fields: {} }) .queryRows( { tableId: table._id!, @@ -138,7 +134,6 @@ describe("Test a query step automation", () => { name: "Return All Test", config, }) - .appAction({ fields: {} }) .queryRows( { tableId: table._id!, @@ -169,7 +164,6 @@ describe("Test a query step automation", () => { name: "Return All Test", config, }) - .appAction({ fields: {} }) .queryRows( { tableId: tableWithSpaces._id!, diff --git a/packages/server/src/automations/tests/steps/serverLog.spec.ts b/packages/server/src/automations/tests/steps/serverLog.spec.ts index 556f7b174c..73d72b9a99 100644 --- a/packages/server/src/automations/tests/steps/serverLog.spec.ts +++ b/packages/server/src/automations/tests/steps/serverLog.spec.ts @@ -14,7 +14,6 @@ describe("test the server log action", () => { it("should be able to log the text", async () => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .serverLog({ text: "Hello World" }) .run() expect(result.steps[0].outputs.message).toEqual( diff --git a/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts b/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts index 674f2386c1..4047c23566 100644 --- a/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts +++ b/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts @@ -18,12 +18,10 @@ describe("Test triggering an automation from another automation", () => { it("should trigger an other server log automation", async () => { const automation = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .serverLog({ text: "Hello World" }) .save() const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .triggerAutomationRun({ automation: { automationId: automation._id!, @@ -37,7 +35,6 @@ describe("Test triggering an automation from another automation", () => { it("should fail gracefully if the automation id is incorrect", async () => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .triggerAutomationRun({ automation: { // @ts-expect-error - incorrect on purpose diff --git a/packages/server/src/automations/tests/steps/updateRow.spec.ts b/packages/server/src/automations/tests/steps/updateRow.spec.ts index 79fed5f613..ca3854f745 100644 --- a/packages/server/src/automations/tests/steps/updateRow.spec.ts +++ b/packages/server/src/automations/tests/steps/updateRow.spec.ts @@ -31,7 +31,6 @@ describe("test the update row action", () => { it("should be able to run the update row action", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .updateRow({ rowId: row._id!, row: { @@ -54,7 +53,6 @@ describe("test the update row action", () => { it("should check invalid inputs return an error", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .updateRow({ meta: {}, row: {}, rowId: "" }) .run() @@ -63,7 +61,6 @@ describe("test the update row action", () => { it("should return an error when table doesn't exist", async () => { const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .updateRow({ row: { _id: "invalid" }, rowId: "invalid", @@ -107,7 +104,6 @@ describe("test the update row action", () => { }) const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .updateRow({ rowId: row._id!, row: { @@ -161,7 +157,6 @@ describe("test the update row action", () => { }) const results = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .updateRow({ rowId: row._id!, row: { diff --git a/packages/server/src/automations/tests/steps/zapier.spec.ts b/packages/server/src/automations/tests/steps/zapier.spec.ts index 17a0a7c7bf..331acb68f3 100644 --- a/packages/server/src/automations/tests/steps/zapier.spec.ts +++ b/packages/server/src/automations/tests/steps/zapier.spec.ts @@ -21,7 +21,6 @@ describe("test the outgoing webhook action", () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .zapier({ url: "http://www.example.com", body: null }) .run() @@ -45,7 +44,6 @@ describe("test the outgoing webhook action", () => { .reply(200, { foo: "bar" }) const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .zapier({ url: "http://www.example.com", body: { value: JSON.stringify(payload) }, @@ -58,7 +56,6 @@ describe("test the outgoing webhook action", () => { it("should return a 400 if the JSON payload string is malformed", async () => { const result = await createAutomationBuilder({ config }) - .appAction({ fields: {} }) .zapier({ url: "http://www.example.com", body: { value: "{ invalid json }" }, diff --git a/packages/server/src/automations/tests/triggers/createRow.spec.ts b/packages/server/src/automations/tests/triggers/createRow.spec.ts deleted file mode 100644 index 84f136c8a6..0000000000 --- a/packages/server/src/automations/tests/triggers/createRow.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" -import TestConfiguration from "../../../tests/utilities/TestConfiguration" -import { getQueue } from "../.." -import { Job } from "bull" -import { basicTable } from "../../../tests/utilities/structures" -import { Table } from "@budibase/types" - -describe("cron trigger", () => { - const config = new TestConfiguration() - let table: Table - - beforeAll(async () => { - await config.init() - table = await config.api.table.save(basicTable()) - }) - - afterAll(() => { - config.end() - }) - - it("should successfully fire", async () => { - const queue = getQueue() - expect(await queue.getCompletedCount()).toEqual(0) - - const jobPromise = new Promise(resolve => { - queue.on("completed", async job => { - resolve(job) - }) - }) - - await createAutomationBuilder({ config }) - .rowSaved({ tableId: table._id! }) - .cron({ cron: "* * * * *" }) - .serverLog({ - text: "Hello, world!", - }) - .save() - - await config.api.application.publish(config.getAppId()) - - expect(await queue.getCompletedCount()).toEqual(1) - - const job = await jobPromise - const repeat = job.opts?.repeat - if (!repeat || !("cron" in repeat)) { - throw new Error("Expected cron repeat") - } - expect(repeat.cron).toEqual("* * * * *") - }) - - it("should fail if the cron expression is invalid", async () => { - await createAutomationBuilder({ config }) - .cron({ cron: "* * * * * *" }) - .serverLog({ - text: "Hello, world!", - }) - .save() - - await config.api.application.publish(config.getAppId(), { - status: 500, - body: { - message: - 'Deployment Failed: Invalid automation CRON "* * * * * *" - Expected 5 values, but got 6.', - }, - }) - }) -}) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 9484f5cf21..e059bd1c04 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -158,7 +158,7 @@ class AutomationBuilder extends BaseStepBuilder { } } - protected createTriggerFn< + protected triggerInputOutput< TStep extends AutomationTriggerStepId, TInput = AutomationTriggerInputs, TOutput = AutomationTriggerOutputs @@ -179,12 +179,28 @@ class AutomationBuilder extends BaseStepBuilder { } } - rowSaved = this.createTriggerFn(AutomationTriggerStepId.ROW_SAVED) - rowUpdated = this.createTriggerFn(AutomationTriggerStepId.ROW_UPDATED) - rowDeleted = this.createTriggerFn(AutomationTriggerStepId.ROW_DELETED) - appAction = this.createTriggerFn(AutomationTriggerStepId.APP) - webhook = this.createTriggerFn(AutomationTriggerStepId.WEBHOOK) - cron = this.createTriggerFn(AutomationTriggerStepId.CRON) + protected triggerOutputOnly< + TStep extends AutomationTriggerStepId, + TOutput = AutomationTriggerOutputs + >(stepId: TStep) { + return (outputs: TOutput) => { + this.triggerOutputs = outputs as TriggerOutputs + this.automationConfig.definition.trigger = { + ...TRIGGER_DEFINITIONS[stepId], + stepId, + id: uuidv4(), + } as AutomationTrigger + this.triggerSet = true + return this + } + } + + rowSaved = this.triggerInputOutput(AutomationTriggerStepId.ROW_SAVED) + rowUpdated = this.triggerInputOutput(AutomationTriggerStepId.ROW_UPDATED) + rowDeleted = this.triggerInputOutput(AutomationTriggerStepId.ROW_DELETED) + appAction = this.triggerOutputOnly(AutomationTriggerStepId.APP) + webhook = this.triggerInputOutput(AutomationTriggerStepId.WEBHOOK) + cron = this.triggerInputOutput(AutomationTriggerStepId.CRON) branch(branchConfig: BranchConfig): this { this.addBranchStep(branchConfig) From 880f27e0ff6193c913015e0dd8dde1f65b312933 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 5 Feb 2025 17:39:31 +0000 Subject: [PATCH 299/321] Changing how messaging is generated. --- .../modals/DeleteDataConfirmationModal.svelte | 74 +++++++++---------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index 0649e40c09..4d2a60d2f5 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -25,22 +25,7 @@ $: isInternalTable = isInternal && sourceType === SourceType.TABLE - const getViewsMessage = () => { - if (!source || !("views" in source)) { - return "" - } - const views = Object.values(source?.views ?? []) - if (views.length < 1) { - return "" - } - if (views.length === 1) { - return "1 view" - } - - return `${views.length} views` - } - - const getQueriesMessage = () => { + const getDatasourceQueries = () => { if (sourceType !== SourceType.DATASOURCE) { return "" } @@ -48,14 +33,7 @@ const queryList = get(queries).list.filter( query => query.datasourceId === sourceId ) - if (queryList.length < 1) { - return "" - } - if (queryList.length === 1) { - return "1 query" - } - - return `${queryList.length} queries` + return queryList } function getSourceID(): string { @@ -168,21 +146,35 @@ } } - function buildMessage() { - let message = "" - if (isInternalTable) { - message = `All ${sourceType} data will also be deleted` - const viewsMessage = getViewsMessage() - if (viewsMessage) { - message += `, including ${viewsMessage}. ` - } else { - message += ". " + function buildMessage(sourceType: string) { + if (!source) { + return "" + } + let message = `Removing ${source?.name} ` + let initialLength = message.length + if (sourceType === SourceType.TABLE) { + const views = "views" in source ? Object.values(source?.views ?? []) : [] + if (isInternalTable) { + message += `will delete its data${ + views.length ? `, views (${views.length})` : "" + }` + } else if (views.length) { + message += `will delete its views (${views.length})` } } else if (sourceType === SourceType.DATASOURCE) { - const queriesMessage = getQueriesMessage() - message = `This will include deleting ${queriesMessage}. ` + const queryList = getDatasourceQueries() + if (queryList.length) { + message += `will delete its queries (${queryList.length})` + } + } + if (affectedScreens.length) { + message += + initialLength !== message.length + ? ", and break connected screens:" + : "will break connected screens:" + } else { + message += "." } - message += "This action cannot be undone." return message } @@ -192,12 +184,12 @@ okText="Delete" onOk={deleteSource} onCancel={hideDeleteDialog} - title={`Are you sure you want to delete ${source?.name}?`} + title={`Are you sure you want to delete this ${sourceType}?`} >
    - {#if affectedScreens.length > 0} + {#if affectedScreens.length > 0 && sourceType}

    - Removing this {sourceType} will break the following screens: + {buildMessage(sourceType)} {#each affectedScreens as item, idx} {/if}

    - - {@html buildMessage()} + This action cannot be undone.

    @@ -228,6 +219,7 @@ .screens { display: flex; flex-direction: row; + padding-bottom: var(--spacing-l); gap: var(--spacing-xs); } From d62be2629c43a1b5480dc4ee4191384924d85469 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 5 Feb 2025 17:39:38 +0000 Subject: [PATCH 300/321] Simplify calls to createAutomationBuilder. --- .../src/api/routes/tests/automation.spec.ts | 26 +++------------- .../src/automations/tests/branching.spec.ts | 14 ++++----- .../src/automations/tests/scenarios.spec.ts | 22 ++++++------- .../src/automations/tests/steps/bash.spec.ts | 10 +++--- .../automations/tests/steps/createRow.spec.ts | 12 +++---- .../tests/steps/cron-automations.spec.ts | 2 +- .../src/automations/tests/steps/delay.spec.ts | 2 +- .../automations/tests/steps/deleteRow.spec.ts | 6 ++-- .../automations/tests/steps/discord.spec.ts | 2 +- .../tests/steps/executeScript.spec.ts | 11 ++++--- .../automations/tests/steps/filter.spec.ts | 4 +-- .../src/automations/tests/steps/loop.spec.ts | 24 +++++++------- .../src/automations/tests/steps/make.spec.ts | 6 ++-- .../src/automations/tests/steps/n8n.spec.ts | 8 ++--- .../automations/tests/steps/openai.spec.ts | 8 ++--- .../tests/steps/outgoingWebhook.spec.ts | 4 +-- .../automations/tests/steps/queryRows.spec.ts | 30 ++++-------------- .../automations/tests/steps/serverLog.spec.ts | 2 +- .../tests/steps/triggerAutomationRun.spec.ts | 6 ++-- .../automations/tests/steps/updateRow.spec.ts | 10 +++--- .../automations/tests/steps/zapier.spec.ts | 6 ++-- .../automations/tests/triggers/cron.spec.ts | 4 +-- .../tests/triggers/webhook.spec.ts | 2 +- .../tests/utilities/AutomationTestBuilder.ts | 31 +++++++++---------- 24 files changed, 109 insertions(+), 143 deletions(-) diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index eeceabdb2f..5b451eef4a 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -107,10 +107,7 @@ describe("/automations", () => { }) it("Should ensure you can't have a branch as not a last step", async () => { - const automation = createAutomationBuilder({ - name: "String Equality Branching", - appId: config.getAppId(), - }) + const automation = createAutomationBuilder(config) .appAction({ fields: { status: "active" } }) .branch({ activeBranch: { @@ -134,10 +131,7 @@ describe("/automations", () => { }) it("Should check validation on an automation that has a branch step with no children", async () => { - const automation = createAutomationBuilder({ - name: "String Equality Branching", - appId: config.getAppId(), - }) + const automation = createAutomationBuilder(config) .appAction({ fields: { status: "active" } }) .branch({}) .serverLog({ text: "Inactive user" }) @@ -153,10 +147,7 @@ describe("/automations", () => { }) it("Should check validation on a branch step with empty conditions", async () => { - const automation = createAutomationBuilder({ - name: "String Equality Branching", - appId: config.getAppId(), - }) + const automation = createAutomationBuilder(config) .appAction({ fields: { status: "active" } }) .branch({ activeBranch: { @@ -177,10 +168,7 @@ describe("/automations", () => { }) it("Should check validation on an branch that has a condition that is not valid", async () => { - const automation = createAutomationBuilder({ - name: "String Equality Branching", - appId: config.getAppId(), - }) + const automation = createAutomationBuilder(config) .appAction({ fields: { status: "active" } }) .branch({ activeBranch: { @@ -252,11 +240,7 @@ describe("/automations", () => { }) it("should be able to access platformUrl, logoUrl and company in the automation", async () => { - const result = await createAutomationBuilder({ - name: "Test Automation", - appId: config.getAppId(), - config, - }) + const result = await createAutomationBuilder(config) .serverLog({ text: "{{ settings.url }}", }) diff --git a/packages/server/src/automations/tests/branching.spec.ts b/packages/server/src/automations/tests/branching.spec.ts index e6a250577d..d9fad543f6 100644 --- a/packages/server/src/automations/tests/branching.spec.ts +++ b/packages/server/src/automations/tests/branching.spec.ts @@ -24,7 +24,7 @@ describe("Branching automations", () => { const branch2LogId = "33333333-3333-3333-3333-333333333333" const branch2Id = "44444444-4444-4444-4444-444444444444" - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .serverLog( { text: "Starting automation" }, { stepName: "FirstLog", stepId: firstLogId } @@ -82,7 +82,7 @@ describe("Branching automations", () => { }) it("should execute correct branch based on string equality", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .appAction({ fields: { status: "active" } }) .branch({ activeBranch: { @@ -107,7 +107,7 @@ describe("Branching automations", () => { }) it("should handle multiple conditions with AND operator", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .appAction({ fields: { status: "active", role: "admin" } }) .branch({ activeAdminBranch: { @@ -135,7 +135,7 @@ describe("Branching automations", () => { }) it("should handle multiple conditions with OR operator", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .appAction({ fields: { status: "test", role: "user" } }) .branch({ specialBranch: { @@ -167,7 +167,7 @@ describe("Branching automations", () => { }) it("should stop the branch automation when no conditions are met", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .appAction({ fields: { status: "test", role: "user" } }) .createRow({ row: { name: "Test", tableId: table._id } }) .branch({ @@ -203,7 +203,7 @@ describe("Branching automations", () => { }) it("evaluate multiple conditions", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .appAction({ fields: { test_trigger: true } }) .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .branch({ @@ -244,7 +244,7 @@ describe("Branching automations", () => { }) it("evaluate multiple conditions with interpolated text", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .appAction({ fields: { test_trigger: true } }) .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .branch({ diff --git a/packages/server/src/automations/tests/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios.spec.ts index 6c40b793dd..2c13b7c019 100644 --- a/packages/server/src/automations/tests/scenarios.spec.ts +++ b/packages/server/src/automations/tests/scenarios.spec.ts @@ -29,7 +29,7 @@ describe("Automation Scenarios", () => { it("should trigger an automation which then creates a row", async () => { const table = await config.api.table.save(basicTable()) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .rowUpdated( { tableId: table._id! }, { @@ -65,7 +65,7 @@ describe("Automation Scenarios", () => { } await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .queryRows({ tableId: table._id!, }) @@ -83,7 +83,7 @@ describe("Automation Scenarios", () => { } await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .queryRows({ tableId: table._id!, }) @@ -124,7 +124,7 @@ describe("Automation Scenarios", () => { }, }) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .createRow( { row: { @@ -192,7 +192,7 @@ describe("Automation Scenarios", () => { } await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .queryRows( { tableId: table._id!, @@ -241,7 +241,7 @@ describe("Automation Scenarios", () => { }) it("should stop an automation if the condition is not met", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .createRow({ row: { name: "Equal Test", @@ -266,7 +266,7 @@ describe("Automation Scenarios", () => { }) it("should continue the automation if the condition is met", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .createRow({ row: { name: "Not Equal Test", @@ -332,7 +332,7 @@ describe("Automation Scenarios", () => { it.each(testCases)( "should pass the filter when condition is $condition", async ({ condition, value, rowValue, expectPass }) => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .createRow({ row: { name: `${condition} Test`, @@ -366,7 +366,7 @@ describe("Automation Scenarios", () => { it("Check user is passed through from row trigger", async () => { const table = await config.api.table.save(basicTable()) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .rowUpdated( { tableId: table._id! }, { @@ -381,7 +381,7 @@ describe("Automation Scenarios", () => { }) it("Check user is passed through from app trigger", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .serverLog({ text: "{{ [user].[email] }}" }) .run() @@ -452,7 +452,7 @@ if (descriptions.length) { queryVerb: "read", }) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .appAction({ fields: {}, }) diff --git a/packages/server/src/automations/tests/steps/bash.spec.ts b/packages/server/src/automations/tests/steps/bash.spec.ts index edf01bb3e2..9b797f9479 100644 --- a/packages/server/src/automations/tests/steps/bash.spec.ts +++ b/packages/server/src/automations/tests/steps/bash.spec.ts @@ -24,7 +24,7 @@ describe("Execute Bash Automations", () => { }) it("should use trigger data in bash command and pass output to subsequent steps", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .appAction({ fields: { command: "hello world" } }) .bash( { code: "echo '{{ trigger.fields.command }}'" }, @@ -43,7 +43,7 @@ describe("Execute Bash Automations", () => { }) it("should chain multiple bash commands using previous outputs", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .appAction({ fields: { filename: "testfile.txt" } }) .bash( { code: "echo 'initial content' > {{ trigger.fields.filename }}" }, @@ -64,7 +64,7 @@ describe("Execute Bash Automations", () => { }) it("should integrate bash output with row operations", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .queryRows( { tableId: table._id!, @@ -93,7 +93,7 @@ describe("Execute Bash Automations", () => { }) it("should handle bash output in conditional logic", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .appAction({ fields: { threshold: "5" } }) .bash( { code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" }, @@ -120,7 +120,7 @@ describe("Execute Bash Automations", () => { }) it("should handle null values gracefully", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .bash( // @ts-expect-error - testing null input { code: null }, diff --git a/packages/server/src/automations/tests/steps/createRow.spec.ts b/packages/server/src/automations/tests/steps/createRow.spec.ts index 5b86556ae3..4456550cb3 100644 --- a/packages/server/src/automations/tests/steps/createRow.spec.ts +++ b/packages/server/src/automations/tests/steps/createRow.spec.ts @@ -40,7 +40,7 @@ describe("test the create row action", () => { }) it("should be able to run the action", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .appAction({ fields: { status: "new" } }) .serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" }) .createRow({ row }, { stepName: "CreateRow" }) @@ -66,7 +66,7 @@ describe("test the create row action", () => { }) it("should return an error (not throw) when bad info provided", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .appAction({ fields: { status: "error" } }) .serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" }) .createRow( @@ -84,7 +84,7 @@ describe("test the create row action", () => { }) it("should check invalid inputs return an error", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .appAction({ fields: { status: "invalid" } }) .serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" }) .createRow({ row: {} }, { stepName: "CreateRow" }) @@ -122,7 +122,7 @@ describe("test the create row action", () => { ] attachmentRow.file_attachment = attachmentObject - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .appAction({ fields: { type: "attachment" } }) .serverLog( { text: "Processing attachment upload" }, @@ -173,7 +173,7 @@ describe("test the create row action", () => { } attachmentRow.single_file_attachment = attachmentObject - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .appAction({ fields: { type: "single-attachment" } }) .serverLog( { text: "Processing single attachment" }, @@ -244,7 +244,7 @@ describe("test the create row action", () => { } attachmentRow.single_file_attachment = attachmentObject - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .appAction({ fields: { type: "invalid-attachment" } }) .serverLog( { text: "Testing invalid attachment keys" }, diff --git a/packages/server/src/automations/tests/steps/cron-automations.spec.ts b/packages/server/src/automations/tests/steps/cron-automations.spec.ts index 6a82ac1cde..ed0729bd38 100644 --- a/packages/server/src/automations/tests/steps/cron-automations.spec.ts +++ b/packages/server/src/automations/tests/steps/cron-automations.spec.ts @@ -27,7 +27,7 @@ describe("cron automations", () => { }) it("should initialise the automation timestamp", async () => { - await createAutomationBuilder({ config }).cron({ cron: "* * * * *" }).save() + await createAutomationBuilder(config).cron({ cron: "* * * * *" }).save() tk.travel(Date.now() + oneMinuteInMs) await config.publish() diff --git a/packages/server/src/automations/tests/steps/delay.spec.ts b/packages/server/src/automations/tests/steps/delay.spec.ts index 89b85530e1..9dc6470f5a 100644 --- a/packages/server/src/automations/tests/steps/delay.spec.ts +++ b/packages/server/src/automations/tests/steps/delay.spec.ts @@ -16,7 +16,7 @@ describe("test the delay logic", () => { const time = 100 const before = performance.now() - await createAutomationBuilder({ config }).delay({ time }).run() + await createAutomationBuilder(config).delay({ time }).run() const now = performance.now() diff --git a/packages/server/src/automations/tests/steps/deleteRow.spec.ts b/packages/server/src/automations/tests/steps/deleteRow.spec.ts index 52a5dacdd7..ee8c9b329f 100644 --- a/packages/server/src/automations/tests/steps/deleteRow.spec.ts +++ b/packages/server/src/automations/tests/steps/deleteRow.spec.ts @@ -20,7 +20,7 @@ describe("test the delete row action", () => { }) it("should be able to run the delete row action", async () => { - await createAutomationBuilder({ config }) + await createAutomationBuilder(config) .deleteRow({ tableId: table._id!, id: row._id!, @@ -34,7 +34,7 @@ describe("test the delete row action", () => { }) it("should check invalid inputs return an error", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .deleteRow({ tableId: "", id: "", revision: "" }) .run() @@ -42,7 +42,7 @@ describe("test the delete row action", () => { }) it("should return an error when table doesn't exist", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .deleteRow({ tableId: "invalid", id: "invalid", diff --git a/packages/server/src/automations/tests/steps/discord.spec.ts b/packages/server/src/automations/tests/steps/discord.spec.ts index 9d4d3f6033..361b3517f3 100644 --- a/packages/server/src/automations/tests/steps/discord.spec.ts +++ b/packages/server/src/automations/tests/steps/discord.spec.ts @@ -19,7 +19,7 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .discord({ url: "http://www.example.com", username: "joe_bloggs", diff --git a/packages/server/src/automations/tests/steps/executeScript.spec.ts b/packages/server/src/automations/tests/steps/executeScript.spec.ts index 2d510e86e0..e2bea9d0c1 100644 --- a/packages/server/src/automations/tests/steps/executeScript.spec.ts +++ b/packages/server/src/automations/tests/steps/executeScript.spec.ts @@ -20,7 +20,7 @@ describe("Execute Script Automations", () => { }) it("should execute a basic script and return the result", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .executeScript({ code: "return 2 + 2" }) .run() @@ -28,7 +28,7 @@ describe("Execute Script Automations", () => { }) it("should access bindings from previous steps", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .appAction({ fields: { data: [1, 2, 3] } }) .executeScript( { @@ -42,7 +42,7 @@ describe("Execute Script Automations", () => { }) it("should handle script execution errors gracefully", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .executeScript({ code: "return nonexistentVariable.map(x => x)" }) .run() @@ -53,7 +53,7 @@ describe("Execute Script Automations", () => { }) it("should handle conditional logic in scripts", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .appAction({ fields: { value: 10 } }) .executeScript({ code: ` @@ -70,7 +70,8 @@ describe("Execute Script Automations", () => { }) it("should use multiple steps and validate script execution", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) + .appAction({ fields: {} }) .serverLog( { text: "Starting multi-step automation" }, { stepId: "start-log-step" } diff --git a/packages/server/src/automations/tests/steps/filter.spec.ts b/packages/server/src/automations/tests/steps/filter.spec.ts index 0d4ac0f01a..51af262aff 100644 --- a/packages/server/src/automations/tests/steps/filter.spec.ts +++ b/packages/server/src/automations/tests/steps/filter.spec.ts @@ -42,7 +42,7 @@ describe("test the filter logic", () => { [new Date().toISOString(), ">", new Date(-10000).toISOString()], ] it.each(pass)("should pass %p %p %p", async (field, condition, value) => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .filter({ field, condition: stringToFilterCondition(condition), value }) .run() @@ -59,7 +59,7 @@ describe("test the filter logic", () => { [{}, "==", {}], ] it.each(fail)("should fail %p %p %p", async (field, condition, value) => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .filter({ field, condition: stringToFilterCondition(condition), value }) .run() diff --git a/packages/server/src/automations/tests/steps/loop.spec.ts b/packages/server/src/automations/tests/steps/loop.spec.ts index 764f55d9ad..ba74bf6778 100644 --- a/packages/server/src/automations/tests/steps/loop.spec.ts +++ b/packages/server/src/automations/tests/steps/loop.spec.ts @@ -72,7 +72,7 @@ describe("Attempt to run a basic loop automation", () => { }) it("should run an automation with a trigger, loop, and create row step", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .rowSaved( { tableId: table._id! }, { @@ -115,7 +115,7 @@ describe("Attempt to run a basic loop automation", () => { }) it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .rowSaved( { tableId: table._id! }, { @@ -151,7 +151,7 @@ describe("Attempt to run a basic loop automation", () => { }) it("if an incorrect type is passed to the loop it should return an error", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .loop({ option: LoopStepType.ARRAY, binding: "1, 2, 3", @@ -166,7 +166,7 @@ describe("Attempt to run a basic loop automation", () => { }) it("ensure the loop stops if the failure condition is reached", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], @@ -184,7 +184,7 @@ describe("Attempt to run a basic loop automation", () => { }) it("ensure the loop stops if the max iterations are reached", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], @@ -198,7 +198,7 @@ describe("Attempt to run a basic loop automation", () => { }) it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], @@ -212,7 +212,7 @@ describe("Attempt to run a basic loop automation", () => { }) it("should run an automation where a loop is successfully run twice", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .rowSaved( { tableId: table._id! }, { @@ -274,7 +274,7 @@ describe("Attempt to run a basic loop automation", () => { }) it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], @@ -295,7 +295,7 @@ describe("Attempt to run a basic loop automation", () => { }) it("should use automation names to loop with", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .loop( { option: LoopStepType.ARRAY, @@ -346,7 +346,7 @@ describe("Attempt to run a basic loop automation", () => { await config.api.row.bulkImport(table._id!, { rows }) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .queryRows({ tableId: table._id!, }) @@ -425,7 +425,7 @@ describe("Attempt to run a basic loop automation", () => { await config.api.row.bulkImport(table._id!, { rows }) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .queryRows( { tableId: table._id!, @@ -507,7 +507,7 @@ describe("Attempt to run a basic loop automation", () => { await config.api.row.bulkImport(table._id!, { rows }) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .queryRows({ tableId: table._id!, }) diff --git a/packages/server/src/automations/tests/steps/make.spec.ts b/packages/server/src/automations/tests/steps/make.spec.ts index a26f9f73ae..2d118d943f 100644 --- a/packages/server/src/automations/tests/steps/make.spec.ts +++ b/packages/server/src/automations/tests/steps/make.spec.ts @@ -19,7 +19,7 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .make({ url: "http://www.example.com", body: null, @@ -45,7 +45,7 @@ describe("test the outgoing webhook action", () => { .post("/", payload) .reply(200, { foo: "bar" }) - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .make({ body: { value: JSON.stringify(payload) }, url: "http://www.example.com", @@ -57,7 +57,7 @@ describe("test the outgoing webhook action", () => { }) it("should return a 400 if the JSON payload string is malformed", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .make({ body: { value: "{ invalid json }" }, url: "http://www.example.com", diff --git a/packages/server/src/automations/tests/steps/n8n.spec.ts b/packages/server/src/automations/tests/steps/n8n.spec.ts index 04d82fcd77..d3efb1aaeb 100644 --- a/packages/server/src/automations/tests/steps/n8n.spec.ts +++ b/packages/server/src/automations/tests/steps/n8n.spec.ts @@ -20,7 +20,7 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action and default to 'get'", async () => { nock("http://www.example.com/").get("/").reply(200, { foo: "bar" }) - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .n8n({ url: "http://www.example.com", body: { test: "IGNORE_ME" }, @@ -38,7 +38,7 @@ describe("test the outgoing webhook action", () => { .post("/", { name: "Adam", age: 9 }) .reply(200) - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .n8n({ url: "http://www.example.com", body: { value: JSON.stringify({ name: "Adam", age: 9 }) }, @@ -51,7 +51,7 @@ describe("test the outgoing webhook action", () => { }) it("should return a 400 if the JSON payload string is malformed", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .n8n({ url: "http://www.example.com", body: { value: "{ value1 1 }" }, @@ -70,7 +70,7 @@ describe("test the outgoing webhook action", () => { .head("/", body => body === "") .reply(200) - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .n8n({ url: "http://www.example.com", method: HttpMethod.HEAD, diff --git a/packages/server/src/automations/tests/steps/openai.spec.ts b/packages/server/src/automations/tests/steps/openai.spec.ts index 7e9fa1f933..4fbf0aa6d6 100644 --- a/packages/server/src/automations/tests/steps/openai.spec.ts +++ b/packages/server/src/automations/tests/steps/openai.spec.ts @@ -57,7 +57,7 @@ describe("test the openai action", () => { // means it goes through the "legacy" path which requires you to set your // own API key. We don't count this against your quota. const result = await expectAIUsage(0, () => - createAutomationBuilder({ config }) + createAutomationBuilder(config) .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) .run() ) @@ -68,7 +68,7 @@ describe("test the openai action", () => { it("should present the correct error message when a prompt is not provided", async () => { const result = await expectAIUsage(0, () => - createAutomationBuilder({ config }) + createAutomationBuilder(config) .openai({ prompt: "", model: Model.GPT_4O_MINI }) .run() ) @@ -83,7 +83,7 @@ describe("test the openai action", () => { mockChatGPTError() const result = await expectAIUsage(0, () => - createAutomationBuilder({ config }) + createAutomationBuilder(config) .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) .run() ) @@ -105,7 +105,7 @@ describe("test the openai action", () => { // calculation we use to approximate cost. This uses Budibase's OpenAI API // key, so we charge users for it. const result = await expectAIUsage(14, () => - createAutomationBuilder({ config }) + createAutomationBuilder(config) .openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" }) .run() ) diff --git a/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts b/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts index 9ef3cf1e57..b1d13c6917 100644 --- a/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts +++ b/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts @@ -23,7 +23,7 @@ describe("test the outgoing webhook action", () => { .post("/", { a: 1 }) .reply(200, { foo: "bar" }) - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .outgoingWebhook({ requestMethod: RequestType.POST, url: "http://www.example.com", @@ -38,7 +38,7 @@ describe("test the outgoing webhook action", () => { }) it("should return an error if something goes wrong in fetch", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .outgoingWebhook({ requestMethod: RequestType.GET, url: "www.invalid.com", diff --git a/packages/server/src/automations/tests/steps/queryRows.spec.ts b/packages/server/src/automations/tests/steps/queryRows.spec.ts index 1d79d8e046..9cda9c94c0 100644 --- a/packages/server/src/automations/tests/steps/queryRows.spec.ts +++ b/packages/server/src/automations/tests/steps/queryRows.spec.ts @@ -28,10 +28,7 @@ describe("Test a query step automation", () => { }) it("should be able to run the query step", async () => { - const result = await createAutomationBuilder({ - name: "Basic Query Test", - config, - }) + const result = await createAutomationBuilder(config) .queryRows( { tableId: table._id!, @@ -55,10 +52,7 @@ describe("Test a query step automation", () => { }) it("Returns all rows when onEmptyFilter has no value and no filters are passed", async () => { - const result = await createAutomationBuilder({ - name: "Empty Filter Test", - config, - }) + const result = await createAutomationBuilder(config) .queryRows( { tableId: table._id!, @@ -78,10 +72,7 @@ describe("Test a query step automation", () => { }) it("Returns no rows when onEmptyFilter is RETURN_NONE and theres no filters", async () => { - const result = await createAutomationBuilder({ - name: "Return None Test", - config, - }) + const result = await createAutomationBuilder(config) .queryRows( { tableId: table._id!, @@ -102,10 +93,7 @@ describe("Test a query step automation", () => { }) it("Returns no rows when onEmptyFilters RETURN_NONE and a filter is passed with a null value", async () => { - const result = await createAutomationBuilder({ - name: "Null Filter Test", - config, - }) + const result = await createAutomationBuilder(config) .queryRows( { tableId: table._id!, @@ -130,10 +118,7 @@ describe("Test a query step automation", () => { }) it("Returns rows when onEmptyFilter is RETURN_ALL and no filter is passed", async () => { - const result = await createAutomationBuilder({ - name: "Return All Test", - config, - }) + const result = await createAutomationBuilder(config) .queryRows( { tableId: table._id!, @@ -160,10 +145,7 @@ describe("Test a query step automation", () => { await config.api.row.save(tableWithSpaces._id!, { name: NAME, }) - const result = await createAutomationBuilder({ - name: "Return All Test", - config, - }) + const result = await createAutomationBuilder(config) .queryRows( { tableId: tableWithSpaces._id!, diff --git a/packages/server/src/automations/tests/steps/serverLog.spec.ts b/packages/server/src/automations/tests/steps/serverLog.spec.ts index 73d72b9a99..44a9f068b1 100644 --- a/packages/server/src/automations/tests/steps/serverLog.spec.ts +++ b/packages/server/src/automations/tests/steps/serverLog.spec.ts @@ -13,7 +13,7 @@ describe("test the server log action", () => { }) it("should be able to log the text", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .serverLog({ text: "Hello World" }) .run() expect(result.steps[0].outputs.message).toEqual( diff --git a/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts b/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts index 4047c23566..ef851bc047 100644 --- a/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts +++ b/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts @@ -17,11 +17,11 @@ describe("Test triggering an automation from another automation", () => { }) it("should trigger an other server log automation", async () => { - const automation = await createAutomationBuilder({ config }) + const automation = await createAutomationBuilder(config) .serverLog({ text: "Hello World" }) .save() - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .triggerAutomationRun({ automation: { automationId: automation._id!, @@ -34,7 +34,7 @@ describe("Test triggering an automation from another automation", () => { }) it("should fail gracefully if the automation id is incorrect", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .triggerAutomationRun({ automation: { // @ts-expect-error - incorrect on purpose diff --git a/packages/server/src/automations/tests/steps/updateRow.spec.ts b/packages/server/src/automations/tests/steps/updateRow.spec.ts index ca3854f745..32c7b90446 100644 --- a/packages/server/src/automations/tests/steps/updateRow.spec.ts +++ b/packages/server/src/automations/tests/steps/updateRow.spec.ts @@ -30,7 +30,7 @@ describe("test the update row action", () => { }) it("should be able to run the update row action", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .updateRow({ rowId: row._id!, row: { @@ -52,7 +52,7 @@ describe("test the update row action", () => { }) it("should check invalid inputs return an error", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .updateRow({ meta: {}, row: {}, rowId: "" }) .run() @@ -60,7 +60,7 @@ describe("test the update row action", () => { }) it("should return an error when table doesn't exist", async () => { - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .updateRow({ row: { _id: "invalid" }, rowId: "invalid", @@ -103,7 +103,7 @@ describe("test the update row action", () => { user2: [{ _id: user2._id }], }) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .updateRow({ rowId: row._id!, row: { @@ -156,7 +156,7 @@ describe("test the update row action", () => { user2: [{ _id: user2._id }], }) - const results = await createAutomationBuilder({ config }) + const results = await createAutomationBuilder(config) .updateRow({ rowId: row._id!, row: { diff --git a/packages/server/src/automations/tests/steps/zapier.spec.ts b/packages/server/src/automations/tests/steps/zapier.spec.ts index 331acb68f3..e897083d18 100644 --- a/packages/server/src/automations/tests/steps/zapier.spec.ts +++ b/packages/server/src/automations/tests/steps/zapier.spec.ts @@ -20,7 +20,7 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .zapier({ url: "http://www.example.com", body: null }) .run() @@ -43,7 +43,7 @@ describe("test the outgoing webhook action", () => { .post("/", { ...payload, platform: "budibase" }) .reply(200, { foo: "bar" }) - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .zapier({ url: "http://www.example.com", body: { value: JSON.stringify(payload) }, @@ -55,7 +55,7 @@ describe("test the outgoing webhook action", () => { }) it("should return a 400 if the JSON payload string is malformed", async () => { - const result = await createAutomationBuilder({ config }) + const result = await createAutomationBuilder(config) .zapier({ url: "http://www.example.com", body: { value: "{ invalid json }" }, diff --git a/packages/server/src/automations/tests/triggers/cron.spec.ts b/packages/server/src/automations/tests/triggers/cron.spec.ts index baddb1dd51..84fe90c314 100644 --- a/packages/server/src/automations/tests/triggers/cron.spec.ts +++ b/packages/server/src/automations/tests/triggers/cron.spec.ts @@ -24,7 +24,7 @@ describe("cron trigger", () => { }) }) - await createAutomationBuilder({ config }) + await createAutomationBuilder(config) .cron({ cron: "* * * * *" }) .serverLog({ text: "Hello, world!", @@ -44,7 +44,7 @@ describe("cron trigger", () => { }) it("should fail if the cron expression is invalid", async () => { - await createAutomationBuilder({ config }) + await createAutomationBuilder(config) .cron({ cron: "* * * * * *" }) .serverLog({ text: "Hello, world!", diff --git a/packages/server/src/automations/tests/triggers/webhook.spec.ts b/packages/server/src/automations/tests/triggers/webhook.spec.ts index a0b5e7f195..61fab1e891 100644 --- a/packages/server/src/automations/tests/triggers/webhook.spec.ts +++ b/packages/server/src/automations/tests/triggers/webhook.spec.ts @@ -11,7 +11,7 @@ describe("Branching automations", () => { let webhook: Webhook async function createWebhookAutomation() { - const automation = await createAutomationBuilder({ config }) + const automation = await createAutomationBuilder(config) .webhook({ fields: { parameter: "string" } }) .createRow({ row: { tableId: table._id!, name: "{{ trigger.parameter }}" }, diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index e059bd1c04..5cb982eb9f 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -135,14 +135,12 @@ class AutomationBuilder extends BaseStepBuilder { private triggerOutputs: TriggerOutputs private triggerSet = false - constructor( - options: { name?: string; appId?: string; config?: TestConfiguration } = {} - ) { + constructor(config?: TestConfiguration) { super() - this.config = options.config || setup.getConfig() + this.config = config || setup.getConfig() this.triggerOutputs = { fields: {} } this.automationConfig = { - name: options.name || `Test Automation ${uuidv4()}`, + name: `Test Automation ${uuidv4()}`, definition: { steps: [], trigger: { @@ -154,10 +152,15 @@ class AutomationBuilder extends BaseStepBuilder { stepNames: {}, }, type: "automation", - appId: options.appId ?? this.config.getAppId(), + appId: this.config.getAppId(), } } + name(n: string): this { + this.automationConfig.name = n + return this + } + protected triggerInputOutput< TStep extends AutomationTriggerStepId, TInput = AutomationTriggerInputs, @@ -195,10 +198,13 @@ class AutomationBuilder extends BaseStepBuilder { } } + // The input and output for appAction is identical, and we only ever seem to + // set the output, so we're ignoring the input for now. + appAction = this.triggerOutputOnly(AutomationTriggerStepId.APP) + rowSaved = this.triggerInputOutput(AutomationTriggerStepId.ROW_SAVED) rowUpdated = this.triggerInputOutput(AutomationTriggerStepId.ROW_UPDATED) rowDeleted = this.triggerInputOutput(AutomationTriggerStepId.ROW_DELETED) - appAction = this.triggerOutputOnly(AutomationTriggerStepId.APP) webhook = this.triggerInputOutput(AutomationTriggerStepId.WEBHOOK) cron = this.triggerInputOutput(AutomationTriggerStepId.CRON) @@ -214,9 +220,6 @@ class AutomationBuilder extends BaseStepBuilder { } async save() { - if (!Object.keys(this.automationConfig.definition.trigger).length) { - throw new Error("Please add a trigger to this automation test") - } this.automationConfig.definition.steps = this.steps const { automation } = await this.config.api.automation.post(this.build()) return automation @@ -241,10 +244,6 @@ class AutomationBuilder extends BaseStepBuilder { } } -export function createAutomationBuilder(options?: { - name?: string - appId?: string - config?: TestConfiguration -}) { - return new AutomationBuilder(options) +export function createAutomationBuilder(config?: TestConfiguration) { + return new AutomationBuilder(config) } From 00b02b1762b0b31d6f155948ddcb6ddb113e1150 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 5 Feb 2025 17:45:35 +0000 Subject: [PATCH 301/321] Make config required. --- .../src/automations/tests/utilities/AutomationTestBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 5cb982eb9f..16a049c556 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -244,6 +244,6 @@ class AutomationBuilder extends BaseStepBuilder { } } -export function createAutomationBuilder(config?: TestConfiguration) { +export function createAutomationBuilder(config: TestConfiguration) { return new AutomationBuilder(config) } From 3139fccf2f55e4fc780a9ce19e2de2e2614a8491 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 6 Feb 2025 11:52:41 +0000 Subject: [PATCH 302/321] Refactor automation builder into single class. --- .../tests/utilities/AutomationTestBuilder.ts | 87 ++++++++----------- 1 file changed, 35 insertions(+), 52 deletions(-) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 16a049c556..2ce2ca3f2e 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -22,7 +22,6 @@ import { WebhookTriggerOutputs, } from "@budibase/types" import TestConfiguration from "../../../tests/utilities/TestConfiguration" -import * as setup from "../utilities" import { automations } from "@budibase/shared-core" type TriggerOutputs = @@ -34,7 +33,7 @@ type TriggerOutputs = | CronTriggerOutputs | undefined -type StepBuilderFunction = (stepBuilder: StepBuilder) => void +type StepBuilderFunction = (stepBuilder: AutomationBuilder) => void type BranchConfig = { [key: string]: { @@ -43,9 +42,38 @@ type BranchConfig = { } } -class BaseStepBuilder { - protected steps: AutomationStep[] = [] - protected stepNames: { [key: string]: string } = {} +class AutomationBuilder { + private automationConfig: Automation + private triggerOutputs: TriggerOutputs + private triggerSet = false + private config: TestConfiguration + private steps: AutomationStep[] = [] + private stepNames: { [key: string]: string } = {} + + constructor(config: TestConfiguration) { + this.config = config + this.triggerOutputs = { fields: {} } + this.automationConfig = { + name: `Test Automation ${uuidv4()}`, + definition: { + steps: [], + trigger: { + ...TRIGGER_DEFINITIONS[AutomationTriggerStepId.APP], + stepId: AutomationTriggerStepId.APP, + inputs: this.triggerOutputs, + id: uuidv4(), + }, + stepNames: {}, + }, + type: "automation", + appId: this.config.getAppId(), + } + } + + name(n: string): this { + this.automationConfig.name = n + return this + } protected createStepFn(stepId: TStep) { return ( @@ -98,7 +126,7 @@ class BaseStepBuilder { } Object.entries(branchConfig).forEach(([key, branch]) => { - const stepBuilder = new StepBuilder() + const stepBuilder = new AutomationBuilder(this.config) branch.steps(stepBuilder) let branchId = uuidv4() branchStepInputs.branches.push({ @@ -106,7 +134,7 @@ class BaseStepBuilder { condition: branch.condition, id: branchId, }) - branchStepInputs.children![branchId] = stepBuilder.build() + branchStepInputs.children![branchId] = stepBuilder.steps }) const branchStep: AutomationStep = { ...automations.steps.branch.definition, @@ -116,51 +144,11 @@ class BaseStepBuilder { } this.steps.push(branchStep) } -} - -class StepBuilder extends BaseStepBuilder { - build(): AutomationStep[] { - return this.steps - } branch(branchConfig: BranchConfig): this { this.addBranchStep(branchConfig) return this } -} - -class AutomationBuilder extends BaseStepBuilder { - private automationConfig: Automation - private config: TestConfiguration - private triggerOutputs: TriggerOutputs - private triggerSet = false - - constructor(config?: TestConfiguration) { - super() - this.config = config || setup.getConfig() - this.triggerOutputs = { fields: {} } - this.automationConfig = { - name: `Test Automation ${uuidv4()}`, - definition: { - steps: [], - trigger: { - ...TRIGGER_DEFINITIONS[AutomationTriggerStepId.APP], - stepId: AutomationTriggerStepId.APP, - inputs: this.triggerOutputs, - id: uuidv4(), - }, - stepNames: {}, - }, - type: "automation", - appId: this.config.getAppId(), - } - } - - name(n: string): this { - this.automationConfig.name = n - return this - } - protected triggerInputOutput< TStep extends AutomationTriggerStepId, TInput = AutomationTriggerInputs, @@ -208,11 +196,6 @@ class AutomationBuilder extends BaseStepBuilder { webhook = this.triggerInputOutput(AutomationTriggerStepId.WEBHOOK) cron = this.triggerInputOutput(AutomationTriggerStepId.CRON) - branch(branchConfig: BranchConfig): this { - this.addBranchStep(branchConfig) - return this - } - build(): Automation { this.automationConfig.definition.steps = this.steps this.automationConfig.definition.stepNames = this.stepNames From fdf82a62760fa79e2f74edf3a1f7e50f37319595 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 6 Feb 2025 15:30:30 +0000 Subject: [PATCH 303/321] Some updates, deleting external tables does delete data so warn about this. --- .../modals/DeleteDataConfirmationModal.svelte | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index 4d2a60d2f5..0ef177db64 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -150,24 +150,23 @@ if (!source) { return "" } + const screenCount = affectedScreens.length let message = `Removing ${source?.name} ` let initialLength = message.length if (sourceType === SourceType.TABLE) { const views = "views" in source ? Object.values(source?.views ?? []) : [] - if (isInternalTable) { - message += `will delete its data${ - views.length ? `, views (${views.length})` : "" - }` - } else if (views.length) { - message += `will delete its views (${views.length})` - } + message += `will delete its data${ + views.length + ? `${screenCount ? "," : " and"} views (${views.length})` + : "" + }` } else if (sourceType === SourceType.DATASOURCE) { const queryList = getDatasourceQueries() if (queryList.length) { message += `will delete its queries (${queryList.length})` } } - if (affectedScreens.length) { + if (screenCount) { message += initialLength !== message.length ? ", and break connected screens:" @@ -187,16 +186,20 @@ title={`Are you sure you want to delete this ${sourceType}?`} >
    - {#if affectedScreens.length > 0 && sourceType} + {#if sourceType}

    {buildMessage(sourceType)} - - {#each affectedScreens as item, idx} - {item.text}{idx !== affectedScreens.length - 1 ? "," : ""} - {/each} - + {#if affectedScreens.length > 0} + + {#each affectedScreens as item, idx} + {item.text}{idx !== affectedScreens.length - 1 + ? "," + : ""} + {/each} + + {/if}

    {/if}

    From ac2f40e9551811e87d8c15fbb9608acce26272b3 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 6 Feb 2025 15:46:02 +0000 Subject: [PATCH 304/321] Move trigger output to the run function so it's easier to understand its purpose. --- .../src/api/routes/tests/automation.spec.ts | 11 +- .../src/automations/tests/branching.spec.ts | 27 +-- .../src/automations/tests/scenarios.spec.ts | 56 ++--- .../tests/steps/executeScript.spec.ts | 18 +- .../automations/tests/steps/openai.spec.ts | 12 +- .../automations/tests/steps/queryRows.spec.ts | 18 +- .../tests/triggers/webhook.spec.ts | 2 +- .../tests/utilities/AutomationTestBuilder.ts | 210 ++++++++---------- .../app/automation/StepInputsOutputs.ts | 4 - .../src/documents/app/automation/schema.ts | 3 +- 10 files changed, 176 insertions(+), 185 deletions(-) diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index 5b451eef4a..7a86548278 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -108,7 +108,7 @@ describe("/automations", () => { it("Should ensure you can't have a branch as not a last step", async () => { const automation = createAutomationBuilder(config) - .appAction({ fields: { status: "active" } }) + .appAction() .branch({ activeBranch: { steps: stepBuilder => @@ -132,7 +132,7 @@ describe("/automations", () => { it("Should check validation on an automation that has a branch step with no children", async () => { const automation = createAutomationBuilder(config) - .appAction({ fields: { status: "active" } }) + .appAction() .branch({}) .serverLog({ text: "Inactive user" }) .build() @@ -148,7 +148,7 @@ describe("/automations", () => { it("Should check validation on a branch step with empty conditions", async () => { const automation = createAutomationBuilder(config) - .appAction({ fields: { status: "active" } }) + .appAction() .branch({ activeBranch: { steps: stepBuilder => @@ -169,7 +169,7 @@ describe("/automations", () => { it("Should check validation on an branch that has a condition that is not valid", async () => { const automation = createAutomationBuilder(config) - .appAction({ fields: { status: "active" } }) + .appAction() .branch({ activeBranch: { steps: stepBuilder => @@ -241,6 +241,7 @@ describe("/automations", () => { it("should be able to access platformUrl, logoUrl and company in the automation", async () => { const result = await createAutomationBuilder(config) + .appAction() .serverLog({ text: "{{ settings.url }}", }) @@ -250,7 +251,7 @@ describe("/automations", () => { .serverLog({ text: "{{ settings.company }}", }) - .run() + .run({ fields: {} }) expect(result.steps[0].outputs.message).toEndWith("https://example.com") expect(result.steps[1].outputs.message).toEndWith( diff --git a/packages/server/src/automations/tests/branching.spec.ts b/packages/server/src/automations/tests/branching.spec.ts index d9fad543f6..da3f10ec53 100644 --- a/packages/server/src/automations/tests/branching.spec.ts +++ b/packages/server/src/automations/tests/branching.spec.ts @@ -25,6 +25,7 @@ describe("Branching automations", () => { const branch2Id = "44444444-4444-4444-4444-444444444444" const results = await createAutomationBuilder(config) + .appAction() .serverLog( { text: "Starting automation" }, { stepName: "FirstLog", stepId: firstLogId } @@ -75,7 +76,7 @@ describe("Branching automations", () => { }, }, }) - .run() + .run({ fields: {} }) expect(results.steps[3].outputs.status).toContain("branch1 branch taken") expect(results.steps[4].outputs.message).toContain("Branch 1.1") @@ -83,7 +84,7 @@ describe("Branching automations", () => { it("should execute correct branch based on string equality", async () => { const results = await createAutomationBuilder(config) - .appAction({ fields: { status: "active" } }) + .appAction() .branch({ activeBranch: { steps: stepBuilder => stepBuilder.serverLog({ text: "Active user" }), @@ -99,7 +100,7 @@ describe("Branching automations", () => { }, }, }) - .run() + .run({ fields: { status: "active" } }) expect(results.steps[0].outputs.status).toContain( "activeBranch branch taken" ) @@ -108,7 +109,7 @@ describe("Branching automations", () => { it("should handle multiple conditions with AND operator", async () => { const results = await createAutomationBuilder(config) - .appAction({ fields: { status: "active", role: "admin" } }) + .appAction() .branch({ activeAdminBranch: { steps: stepBuilder => @@ -129,14 +130,14 @@ describe("Branching automations", () => { }, }, }) - .run() + .run({ fields: { status: "active", role: "admin" } }) expect(results.steps[1].outputs.message).toContain("Active admin user") }) it("should handle multiple conditions with OR operator", async () => { const results = await createAutomationBuilder(config) - .appAction({ fields: { status: "test", role: "user" } }) + .appAction() .branch({ specialBranch: { steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }), @@ -161,14 +162,14 @@ describe("Branching automations", () => { }, }, }) - .run() + .run({ fields: { status: "test", role: "user" } }) expect(results.steps[1].outputs.message).toContain("Special user") }) it("should stop the branch automation when no conditions are met", async () => { const results = await createAutomationBuilder(config) - .appAction({ fields: { status: "test", role: "user" } }) + .appAction() .createRow({ row: { name: "Test", tableId: table._id } }) .branch({ specialBranch: { @@ -194,7 +195,7 @@ describe("Branching automations", () => { }, }, }) - .run() + .run({ fields: { status: "test", role: "user" } }) expect(results.steps[1].outputs.status).toEqual( AutomationStatus.NO_CONDITION_MET @@ -204,7 +205,7 @@ describe("Branching automations", () => { it("evaluate multiple conditions", async () => { const results = await createAutomationBuilder(config) - .appAction({ fields: { test_trigger: true } }) + .appAction() .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .branch({ specialBranch: { @@ -238,14 +239,14 @@ describe("Branching automations", () => { }, }, }) - .run() + .run({ fields: { test_trigger: true } }) expect(results.steps[2].outputs.message).toContain("Special user") }) it("evaluate multiple conditions with interpolated text", async () => { const results = await createAutomationBuilder(config) - .appAction({ fields: { test_trigger: true } }) + .appAction() .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .branch({ specialBranch: { @@ -275,7 +276,7 @@ describe("Branching automations", () => { }, }, }) - .run() + .run({ fields: { test_trigger: true } }) expect(results.steps[2].outputs.message).toContain("Special user") }) diff --git a/packages/server/src/automations/tests/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios.spec.ts index 2c13b7c019..c831d6f203 100644 --- a/packages/server/src/automations/tests/scenarios.spec.ts +++ b/packages/server/src/automations/tests/scenarios.spec.ts @@ -30,13 +30,7 @@ describe("Automation Scenarios", () => { const table = await config.api.table.save(basicTable()) const results = await createAutomationBuilder(config) - .rowUpdated( - { tableId: table._id! }, - { - row: { name: "Test", description: "TEST" }, - id: "1234", - } - ) + .rowUpdated({ tableId: table._id! }) .createRow({ row: { name: "{{trigger.row.name}}", @@ -44,7 +38,10 @@ describe("Automation Scenarios", () => { tableId: table._id, }, }) - .run() + .run({ + row: { name: "Test", description: "TEST" }, + id: "1234", + }) expect(results.steps).toHaveLength(1) @@ -66,10 +63,11 @@ describe("Automation Scenarios", () => { await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) const results = await createAutomationBuilder(config) + .appAction() .queryRows({ tableId: table._id!, }) - .run() + .run({ fields: {} }) expect(results.steps).toHaveLength(1) expect(results.steps[0].outputs.rows).toHaveLength(2) @@ -84,6 +82,7 @@ describe("Automation Scenarios", () => { await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) const results = await createAutomationBuilder(config) + .appAction() .queryRows({ tableId: table._id!, }) @@ -94,7 +93,7 @@ describe("Automation Scenarios", () => { .queryRows({ tableId: table._id!, }) - .run() + .run({ fields: {} }) expect(results.steps).toHaveLength(3) expect(results.steps[1].outputs.success).toBeTruthy() @@ -125,6 +124,7 @@ describe("Automation Scenarios", () => { }) const results = await createAutomationBuilder(config) + .appAction() .createRow( { row: { @@ -153,7 +153,7 @@ describe("Automation Scenarios", () => { }, { stepName: "QueryRowsStep" } ) - .run() + .run({ fields: {} }) expect(results.steps).toHaveLength(3) @@ -193,6 +193,7 @@ describe("Automation Scenarios", () => { await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) const results = await createAutomationBuilder(config) + .appAction() .queryRows( { tableId: table._id!, @@ -206,7 +207,7 @@ describe("Automation Scenarios", () => { .queryRows({ tableId: table._id!, }) - .run() + .run({ fields: {} }) expect(results.steps).toHaveLength(3) expect(results.steps[1].outputs.success).toBeTruthy() @@ -242,6 +243,7 @@ describe("Automation Scenarios", () => { it("should stop an automation if the condition is not met", async () => { const results = await createAutomationBuilder(config) + .appAction() .createRow({ row: { name: "Equal Test", @@ -258,7 +260,7 @@ describe("Automation Scenarios", () => { value: 20, }) .serverLog({ text: "Equal condition met" }) - .run() + .run({ fields: {} }) expect(results.steps[2].outputs.success).toBeTrue() expect(results.steps[2].outputs.result).toBeFalse() @@ -267,6 +269,7 @@ describe("Automation Scenarios", () => { it("should continue the automation if the condition is met", async () => { const results = await createAutomationBuilder(config) + .appAction() .createRow({ row: { name: "Not Equal Test", @@ -283,7 +286,7 @@ describe("Automation Scenarios", () => { value: 20, }) .serverLog({ text: "Not Equal condition met" }) - .run() + .run({ fields: {} }) expect(results.steps[2].outputs.success).toBeTrue() expect(results.steps[2].outputs.result).toBeTrue() @@ -333,6 +336,7 @@ describe("Automation Scenarios", () => { "should pass the filter when condition is $condition", async ({ condition, value, rowValue, expectPass }) => { const results = await createAutomationBuilder(config) + .appAction() .createRow({ row: { name: `${condition} Test`, @@ -351,7 +355,7 @@ describe("Automation Scenarios", () => { .serverLog({ text: `${condition} condition ${expectPass ? "passed" : "failed"}`, }) - .run() + .run({ fields: {} }) expect(results.steps[2].outputs.result).toBe(expectPass) if (expectPass) { @@ -367,23 +371,21 @@ describe("Automation Scenarios", () => { const table = await config.api.table.save(basicTable()) const results = await createAutomationBuilder(config) - .rowUpdated( - { tableId: table._id! }, - { - row: { name: "Test", description: "TEST" }, - id: "1234", - } - ) + .rowUpdated({ tableId: table._id! }) .serverLog({ text: "{{ [user].[email] }}" }) - .run() + .run({ + row: { name: "Test", description: "TEST" }, + id: "1234", + }) expect(results.steps[0].outputs.message).toContain("example.com") }) it("Check user is passed through from app trigger", async () => { const results = await createAutomationBuilder(config) + .appAction() .serverLog({ text: "{{ [user].[email] }}" }) - .run() + .run({ fields: {} }) expect(results.steps[0].outputs.message).toContain("example.com") }) @@ -453,9 +455,7 @@ if (descriptions.length) { }) const results = await createAutomationBuilder(config) - .appAction({ - fields: {}, - }) + .appAction() .executeQuery({ query: { queryId: query._id!, @@ -475,7 +475,7 @@ if (descriptions.length) { .queryRows({ tableId: newTable._id!, }) - .run() + .run({ fields: {} }) expect(results.steps).toHaveLength(3) diff --git a/packages/server/src/automations/tests/steps/executeScript.spec.ts b/packages/server/src/automations/tests/steps/executeScript.spec.ts index e2bea9d0c1..9f9b4e71f4 100644 --- a/packages/server/src/automations/tests/steps/executeScript.spec.ts +++ b/packages/server/src/automations/tests/steps/executeScript.spec.ts @@ -21,30 +21,32 @@ describe("Execute Script Automations", () => { it("should execute a basic script and return the result", async () => { const results = await createAutomationBuilder(config) + .appAction() .executeScript({ code: "return 2 + 2" }) - .run() + .run({ fields: {} }) expect(results.steps[0].outputs.value).toEqual(4) }) it("should access bindings from previous steps", async () => { const results = await createAutomationBuilder(config) - .appAction({ fields: { data: [1, 2, 3] } }) + .appAction() .executeScript( { code: "return trigger.fields.data.map(x => x * 2)", }, { stepId: "binding-script-step" } ) - .run() + .run({ fields: { data: [1, 2, 3] } }) expect(results.steps[0].outputs.value).toEqual([2, 4, 6]) }) it("should handle script execution errors gracefully", async () => { const results = await createAutomationBuilder(config) + .appAction() .executeScript({ code: "return nonexistentVariable.map(x => x)" }) - .run() + .run({ fields: {} }) expect(results.steps[0].outputs.response).toContain( "ReferenceError: nonexistentVariable is not defined" @@ -54,7 +56,7 @@ describe("Execute Script Automations", () => { it("should handle conditional logic in scripts", async () => { const results = await createAutomationBuilder(config) - .appAction({ fields: { value: 10 } }) + .appAction() .executeScript({ code: ` if (trigger.fields.value > 5) { @@ -64,14 +66,14 @@ describe("Execute Script Automations", () => { } `, }) - .run() + .run({ fields: { value: 10 } }) expect(results.steps[0].outputs.value).toEqual("Value is greater than 5") }) it("should use multiple steps and validate script execution", async () => { const results = await createAutomationBuilder(config) - .appAction({ fields: {} }) + .appAction() .serverLog( { text: "Starting multi-step automation" }, { stepId: "start-log-step" } @@ -92,7 +94,7 @@ describe("Execute Script Automations", () => { .serverLog({ text: `Final result is {{ steps.ScriptingStep1.value }}`, }) - .run() + .run({ fields: {} }) expect(results.steps[0].outputs.message).toContain( "Starting multi-step automation" diff --git a/packages/server/src/automations/tests/steps/openai.spec.ts b/packages/server/src/automations/tests/steps/openai.spec.ts index 4fbf0aa6d6..eef4bca25b 100644 --- a/packages/server/src/automations/tests/steps/openai.spec.ts +++ b/packages/server/src/automations/tests/steps/openai.spec.ts @@ -58,8 +58,9 @@ describe("test the openai action", () => { // own API key. We don't count this against your quota. const result = await expectAIUsage(0, () => createAutomationBuilder(config) + .appAction() .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) - .run() + .run({ fields: {} }) ) expect(result.steps[0].outputs.response).toEqual("This is a test") @@ -69,8 +70,9 @@ describe("test the openai action", () => { it("should present the correct error message when a prompt is not provided", async () => { const result = await expectAIUsage(0, () => createAutomationBuilder(config) + .appAction() .openai({ prompt: "", model: Model.GPT_4O_MINI }) - .run() + .run({ fields: {} }) ) expect(result.steps[0].outputs.response).toEqual( @@ -84,8 +86,9 @@ describe("test the openai action", () => { const result = await expectAIUsage(0, () => createAutomationBuilder(config) + .appAction() .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) - .run() + .run({ fields: {} }) ) expect(result.steps[0].outputs.response).toEqual( @@ -106,8 +109,9 @@ describe("test the openai action", () => { // key, so we charge users for it. const result = await expectAIUsage(14, () => createAutomationBuilder(config) + .appAction() .openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" }) - .run() + .run({ fields: {} }) ) expect(result.steps[0].outputs.response).toEqual("This is a test") diff --git a/packages/server/src/automations/tests/steps/queryRows.spec.ts b/packages/server/src/automations/tests/steps/queryRows.spec.ts index 9cda9c94c0..7d54590987 100644 --- a/packages/server/src/automations/tests/steps/queryRows.spec.ts +++ b/packages/server/src/automations/tests/steps/queryRows.spec.ts @@ -29,6 +29,7 @@ describe("Test a query step automation", () => { it("should be able to run the query step", async () => { const result = await createAutomationBuilder(config) + .appAction() .queryRows( { tableId: table._id!, @@ -43,7 +44,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query All Rows" } ) - .run() + .run({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -53,6 +54,7 @@ describe("Test a query step automation", () => { it("Returns all rows when onEmptyFilter has no value and no filters are passed", async () => { const result = await createAutomationBuilder(config) + .appAction() .queryRows( { tableId: table._id!, @@ -63,7 +65,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query With Empty Filter" } ) - .run() + .run({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -73,6 +75,7 @@ describe("Test a query step automation", () => { it("Returns no rows when onEmptyFilter is RETURN_NONE and theres no filters", async () => { const result = await createAutomationBuilder(config) + .appAction() .queryRows( { tableId: table._id!, @@ -85,7 +88,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query With Return None" } ) - .run() + .run({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -94,6 +97,7 @@ describe("Test a query step automation", () => { it("Returns no rows when onEmptyFilters RETURN_NONE and a filter is passed with a null value", async () => { const result = await createAutomationBuilder(config) + .appAction() .queryRows( { tableId: table._id!, @@ -110,7 +114,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query With Null Filter" } ) - .run() + .run({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -119,6 +123,7 @@ describe("Test a query step automation", () => { it("Returns rows when onEmptyFilter is RETURN_ALL and no filter is passed", async () => { const result = await createAutomationBuilder(config) + .appAction() .queryRows( { tableId: table._id!, @@ -130,7 +135,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query With Return All" } ) - .run() + .run({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -146,6 +151,7 @@ describe("Test a query step automation", () => { name: NAME, }) const result = await createAutomationBuilder(config) + .appAction() .queryRows( { tableId: tableWithSpaces._id!, @@ -154,7 +160,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query table with spaces" } ) - .run() + .run({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() expect(result.steps[0].outputs.rows.length).toBe(1) diff --git a/packages/server/src/automations/tests/triggers/webhook.spec.ts b/packages/server/src/automations/tests/triggers/webhook.spec.ts index 61fab1e891..e549a6f496 100644 --- a/packages/server/src/automations/tests/triggers/webhook.spec.ts +++ b/packages/server/src/automations/tests/triggers/webhook.spec.ts @@ -11,7 +11,7 @@ describe("Branching automations", () => { let webhook: Webhook async function createWebhookAutomation() { - const automation = await createAutomationBuilder(config) + const { automation } = await createAutomationBuilder(config) .webhook({ fields: { parameter: "string" } }) .createRow({ row: { tableId: table._id!, name: "{{ trigger.parameter }}" }, diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 2ce2ca3f2e..b29124ad0e 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -2,38 +2,26 @@ import { v4 as uuidv4 } from "uuid" import { BUILTIN_ACTION_DEFINITIONS } from "../../actions" import { TRIGGER_DEFINITIONS } from "../../triggers" import { - AppActionTriggerOutputs, Automation, AutomationActionStepId, AutomationStep, AutomationStepInputs, AutomationTrigger, + AutomationTriggerDefinition, AutomationTriggerInputs, AutomationTriggerOutputs, AutomationTriggerStepId, BranchStepInputs, - CronTriggerOutputs, isDidNotTriggerResponse, - RowCreatedTriggerOutputs, - RowDeletedTriggerOutputs, - RowUpdatedTriggerOutputs, SearchFilters, TestAutomationRequest, - WebhookTriggerOutputs, } from "@budibase/types" import TestConfiguration from "../../../tests/utilities/TestConfiguration" import { automations } from "@budibase/shared-core" -type TriggerOutputs = - | RowCreatedTriggerOutputs - | RowUpdatedTriggerOutputs - | RowDeletedTriggerOutputs - | AppActionTriggerOutputs - | WebhookTriggerOutputs - | CronTriggerOutputs - | undefined - -type StepBuilderFunction = (stepBuilder: AutomationBuilder) => void +type StepBuilderFunction = ( + stepBuilder: BranchStepBuilder +) => void type BranchConfig = { [key: string]: { @@ -42,38 +30,42 @@ type BranchConfig = { } } -class AutomationBuilder { - private automationConfig: Automation - private triggerOutputs: TriggerOutputs - private triggerSet = false +class TriggerBuilder { private config: TestConfiguration - private steps: AutomationStep[] = [] - private stepNames: { [key: string]: string } = {} constructor(config: TestConfiguration) { this.config = config - this.triggerOutputs = { fields: {} } - this.automationConfig = { - name: `Test Automation ${uuidv4()}`, - definition: { - steps: [], - trigger: { - ...TRIGGER_DEFINITIONS[AutomationTriggerStepId.APP], - stepId: AutomationTriggerStepId.APP, - inputs: this.triggerOutputs, - id: uuidv4(), - }, - stepNames: {}, - }, - type: "automation", - appId: this.config.getAppId(), + } + + protected trigger< + TStep extends AutomationTriggerStepId, + TInput = AutomationTriggerInputs + >(stepId: TStep) { + return (inputs: TInput) => { + const definition: AutomationTriggerDefinition = + TRIGGER_DEFINITIONS[stepId] + const trigger: AutomationTrigger = { + ...definition, + stepId, + inputs: (inputs || {}) as any, + id: uuidv4(), + } + return new StepBuilder(this.config, trigger) } } - name(n: string): this { - this.automationConfig.name = n - return this - } + appAction = this.trigger(AutomationTriggerStepId.APP) + + rowSaved = this.trigger(AutomationTriggerStepId.ROW_SAVED) + rowUpdated = this.trigger(AutomationTriggerStepId.ROW_UPDATED) + rowDeleted = this.trigger(AutomationTriggerStepId.ROW_DELETED) + webhook = this.trigger(AutomationTriggerStepId.WEBHOOK) + cron = this.trigger(AutomationTriggerStepId.CRON) +} + +class BranchStepBuilder { + protected steps: AutomationStep[] = [] + protected stepNames: { [key: string]: string } = {} protected createStepFn(stepId: TStep) { return ( @@ -120,113 +112,103 @@ class AutomationBuilder { delay = this.createStepFn(AutomationActionStepId.DELAY) protected addBranchStep(branchConfig: BranchConfig): void { - const branchStepInputs: BranchStepInputs = { + const inputs: BranchStepInputs = { branches: [], children: {}, } - Object.entries(branchConfig).forEach(([key, branch]) => { - const stepBuilder = new AutomationBuilder(this.config) - branch.steps(stepBuilder) - let branchId = uuidv4() - branchStepInputs.branches.push({ - name: key, - condition: branch.condition, - id: branchId, - }) - branchStepInputs.children![branchId] = stepBuilder.steps - }) - const branchStep: AutomationStep = { + for (const [name, branch] of Object.entries(branchConfig)) { + const builder = new BranchStepBuilder() + branch.steps(builder) + let id = uuidv4() + inputs.branches.push({ name, condition: branch.condition, id }) + inputs.children![id] = builder.steps + } + + this.steps.push({ ...automations.steps.branch.definition, id: uuidv4(), stepId: AutomationActionStepId.BRANCH, - inputs: branchStepInputs, - } - this.steps.push(branchStep) + inputs, + }) } branch(branchConfig: BranchConfig): this { this.addBranchStep(branchConfig) return this } - protected triggerInputOutput< - TStep extends AutomationTriggerStepId, - TInput = AutomationTriggerInputs, - TOutput = AutomationTriggerOutputs - >(stepId: TStep) { - return (inputs: TInput, outputs?: TOutput) => { - if (this.triggerSet) { - throw new Error("Only one trigger can be set for an automation.") - } - this.triggerOutputs = outputs as TriggerOutputs | undefined - this.automationConfig.definition.trigger = { - ...TRIGGER_DEFINITIONS[stepId], - stepId, - inputs, - id: uuidv4(), - } as AutomationTrigger - this.triggerSet = true - return this - } +} + +class StepBuilder< + TStep extends AutomationTriggerStepId +> extends BranchStepBuilder { + private config: TestConfiguration + private trigger: AutomationTrigger + private _name: string | undefined = undefined + + constructor(config: TestConfiguration, trigger: AutomationTrigger) { + super() + this.config = config + this.trigger = trigger } - protected triggerOutputOnly< - TStep extends AutomationTriggerStepId, - TOutput = AutomationTriggerOutputs - >(stepId: TStep) { - return (outputs: TOutput) => { - this.triggerOutputs = outputs as TriggerOutputs - this.automationConfig.definition.trigger = { - ...TRIGGER_DEFINITIONS[stepId], - stepId, - id: uuidv4(), - } as AutomationTrigger - this.triggerSet = true - return this - } + name(n: string): this { + this._name = n + return this } - // The input and output for appAction is identical, and we only ever seem to - // set the output, so we're ignoring the input for now. - appAction = this.triggerOutputOnly(AutomationTriggerStepId.APP) - - rowSaved = this.triggerInputOutput(AutomationTriggerStepId.ROW_SAVED) - rowUpdated = this.triggerInputOutput(AutomationTriggerStepId.ROW_UPDATED) - rowDeleted = this.triggerInputOutput(AutomationTriggerStepId.ROW_DELETED) - webhook = this.triggerInputOutput(AutomationTriggerStepId.WEBHOOK) - cron = this.triggerInputOutput(AutomationTriggerStepId.CRON) - build(): Automation { - this.automationConfig.definition.steps = this.steps - this.automationConfig.definition.stepNames = this.stepNames - return this.automationConfig + const name = this._name || `Test Automation ${uuidv4()}` + return { + name, + definition: { + steps: this.steps, + trigger: this.trigger, + stepNames: this.stepNames, + }, + type: "automation", + appId: this.config.getAppId(), + } } async save() { - this.automationConfig.definition.steps = this.steps const { automation } = await this.config.api.automation.post(this.build()) - return automation + return new AutomationRunner(this.config, automation) } - async run() { - const automation = await this.save() + async run(outputs: AutomationTriggerOutputs) { + const runner = await this.save() + return await runner.run(outputs) + } +} + +class AutomationRunner { + private config: TestConfiguration + automation: Automation + + constructor(config: TestConfiguration, automation: Automation) { + this.config = config + this.automation = automation + } + + async run(outputs: AutomationTriggerOutputs) { const response = await this.config.api.automation.test( - automation._id!, - this.triggerOutputs as TestAutomationRequest + this.automation._id!, + // TODO: figure out why this cast is needed. + outputs as TestAutomationRequest ) if (isDidNotTriggerResponse(response)) { throw new Error(response.message) } + // Remove the trigger step from the response. response.steps.shift() - return { - trigger: response.trigger, - steps: response.steps, - } + + return response } } export function createAutomationBuilder(config: TestConfiguration) { - return new AutomationBuilder(config) + return new TriggerBuilder(config) } diff --git a/packages/types/src/documents/app/automation/StepInputsOutputs.ts b/packages/types/src/documents/app/automation/StepInputsOutputs.ts index b9c54cec34..18a6f86284 100644 --- a/packages/types/src/documents/app/automation/StepInputsOutputs.ts +++ b/packages/types/src/documents/app/automation/StepInputsOutputs.ts @@ -253,10 +253,6 @@ export type OutgoingWebhookStepInputs = { headers: string | Record } -export type AppActionTriggerInputs = { - fields: object -} - export type AppActionTriggerOutputs = { fields: object } diff --git a/packages/types/src/documents/app/automation/schema.ts b/packages/types/src/documents/app/automation/schema.ts index 952397b511..66a6f508fe 100644 --- a/packages/types/src/documents/app/automation/schema.ts +++ b/packages/types/src/documents/app/automation/schema.ts @@ -45,7 +45,6 @@ import { OpenAIStepInputs, OpenAIStepOutputs, LoopStepInputs, - AppActionTriggerInputs, CronTriggerInputs, RowUpdatedTriggerInputs, RowCreatedTriggerInputs, @@ -332,7 +331,7 @@ export type AutomationTriggerDefinition = Omit< export type AutomationTriggerInputs = T extends AutomationTriggerStepId.APP - ? AppActionTriggerInputs + ? void : T extends AutomationTriggerStepId.CRON ? CronTriggerInputs : T extends AutomationTriggerStepId.ROW_ACTION From 9f28eb00d4c462822badd12712797f401e9e50bf Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 6 Feb 2025 15:47:10 +0000 Subject: [PATCH 305/321] Rename triggers to start with 'on' --- .../src/api/routes/tests/automation.spec.ts | 10 ++++----- .../src/automations/tests/branching.spec.ts | 14 ++++++------ .../src/automations/tests/scenarios.spec.ts | 22 +++++++++---------- .../src/automations/tests/steps/bash.spec.ts | 6 ++--- .../automations/tests/steps/createRow.spec.ts | 12 +++++----- .../tests/steps/cron-automations.spec.ts | 2 +- .../tests/steps/executeScript.spec.ts | 10 ++++----- .../src/automations/tests/steps/loop.spec.ts | 6 ++--- .../automations/tests/steps/openai.spec.ts | 8 +++---- .../automations/tests/steps/queryRows.spec.ts | 12 +++++----- .../automations/tests/triggers/cron.spec.ts | 4 ++-- .../tests/triggers/webhook.spec.ts | 2 +- .../tests/utilities/AutomationTestBuilder.ts | 12 +++++----- 13 files changed, 60 insertions(+), 60 deletions(-) diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index 7a86548278..a27c21ded4 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -108,7 +108,7 @@ describe("/automations", () => { it("Should ensure you can't have a branch as not a last step", async () => { const automation = createAutomationBuilder(config) - .appAction() + .onAppAction() .branch({ activeBranch: { steps: stepBuilder => @@ -132,7 +132,7 @@ describe("/automations", () => { it("Should check validation on an automation that has a branch step with no children", async () => { const automation = createAutomationBuilder(config) - .appAction() + .onAppAction() .branch({}) .serverLog({ text: "Inactive user" }) .build() @@ -148,7 +148,7 @@ describe("/automations", () => { it("Should check validation on a branch step with empty conditions", async () => { const automation = createAutomationBuilder(config) - .appAction() + .onAppAction() .branch({ activeBranch: { steps: stepBuilder => @@ -169,7 +169,7 @@ describe("/automations", () => { it("Should check validation on an branch that has a condition that is not valid", async () => { const automation = createAutomationBuilder(config) - .appAction() + .onAppAction() .branch({ activeBranch: { steps: stepBuilder => @@ -241,7 +241,7 @@ describe("/automations", () => { it("should be able to access platformUrl, logoUrl and company in the automation", async () => { const result = await createAutomationBuilder(config) - .appAction() + .onAppAction() .serverLog({ text: "{{ settings.url }}", }) diff --git a/packages/server/src/automations/tests/branching.spec.ts b/packages/server/src/automations/tests/branching.spec.ts index da3f10ec53..feac5f80f5 100644 --- a/packages/server/src/automations/tests/branching.spec.ts +++ b/packages/server/src/automations/tests/branching.spec.ts @@ -25,7 +25,7 @@ describe("Branching automations", () => { const branch2Id = "44444444-4444-4444-4444-444444444444" const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .serverLog( { text: "Starting automation" }, { stepName: "FirstLog", stepId: firstLogId } @@ -84,7 +84,7 @@ describe("Branching automations", () => { it("should execute correct branch based on string equality", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .branch({ activeBranch: { steps: stepBuilder => stepBuilder.serverLog({ text: "Active user" }), @@ -109,7 +109,7 @@ describe("Branching automations", () => { it("should handle multiple conditions with AND operator", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .branch({ activeAdminBranch: { steps: stepBuilder => @@ -137,7 +137,7 @@ describe("Branching automations", () => { it("should handle multiple conditions with OR operator", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .branch({ specialBranch: { steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }), @@ -169,7 +169,7 @@ describe("Branching automations", () => { it("should stop the branch automation when no conditions are met", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .createRow({ row: { name: "Test", tableId: table._id } }) .branch({ specialBranch: { @@ -205,7 +205,7 @@ describe("Branching automations", () => { it("evaluate multiple conditions", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .branch({ specialBranch: { @@ -246,7 +246,7 @@ describe("Branching automations", () => { it("evaluate multiple conditions with interpolated text", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .branch({ specialBranch: { diff --git a/packages/server/src/automations/tests/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios.spec.ts index c831d6f203..e37aea838e 100644 --- a/packages/server/src/automations/tests/scenarios.spec.ts +++ b/packages/server/src/automations/tests/scenarios.spec.ts @@ -30,7 +30,7 @@ describe("Automation Scenarios", () => { const table = await config.api.table.save(basicTable()) const results = await createAutomationBuilder(config) - .rowUpdated({ tableId: table._id! }) + .onRowUpdated({ tableId: table._id! }) .createRow({ row: { name: "{{trigger.row.name}}", @@ -63,7 +63,7 @@ describe("Automation Scenarios", () => { await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .queryRows({ tableId: table._id!, }) @@ -82,7 +82,7 @@ describe("Automation Scenarios", () => { await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .queryRows({ tableId: table._id!, }) @@ -124,7 +124,7 @@ describe("Automation Scenarios", () => { }) const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .createRow( { row: { @@ -193,7 +193,7 @@ describe("Automation Scenarios", () => { await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .queryRows( { tableId: table._id!, @@ -243,7 +243,7 @@ describe("Automation Scenarios", () => { it("should stop an automation if the condition is not met", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .createRow({ row: { name: "Equal Test", @@ -269,7 +269,7 @@ describe("Automation Scenarios", () => { it("should continue the automation if the condition is met", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .createRow({ row: { name: "Not Equal Test", @@ -336,7 +336,7 @@ describe("Automation Scenarios", () => { "should pass the filter when condition is $condition", async ({ condition, value, rowValue, expectPass }) => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .createRow({ row: { name: `${condition} Test`, @@ -371,7 +371,7 @@ describe("Automation Scenarios", () => { const table = await config.api.table.save(basicTable()) const results = await createAutomationBuilder(config) - .rowUpdated({ tableId: table._id! }) + .onRowUpdated({ tableId: table._id! }) .serverLog({ text: "{{ [user].[email] }}" }) .run({ row: { name: "Test", description: "TEST" }, @@ -383,7 +383,7 @@ describe("Automation Scenarios", () => { it("Check user is passed through from app trigger", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .serverLog({ text: "{{ [user].[email] }}" }) .run({ fields: {} }) @@ -455,7 +455,7 @@ if (descriptions.length) { }) const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .executeQuery({ query: { queryId: query._id!, diff --git a/packages/server/src/automations/tests/steps/bash.spec.ts b/packages/server/src/automations/tests/steps/bash.spec.ts index 9b797f9479..2174a6db42 100644 --- a/packages/server/src/automations/tests/steps/bash.spec.ts +++ b/packages/server/src/automations/tests/steps/bash.spec.ts @@ -25,7 +25,7 @@ describe("Execute Bash Automations", () => { it("should use trigger data in bash command and pass output to subsequent steps", async () => { const result = await createAutomationBuilder(config) - .appAction({ fields: { command: "hello world" } }) + .onAppAction({ fields: { command: "hello world" } }) .bash( { code: "echo '{{ trigger.fields.command }}'" }, { stepName: "Echo Command" } @@ -44,7 +44,7 @@ describe("Execute Bash Automations", () => { it("should chain multiple bash commands using previous outputs", async () => { const result = await createAutomationBuilder(config) - .appAction({ fields: { filename: "testfile.txt" } }) + .onAppAction({ fields: { filename: "testfile.txt" } }) .bash( { code: "echo 'initial content' > {{ trigger.fields.filename }}" }, { stepName: "Create File" } @@ -94,7 +94,7 @@ describe("Execute Bash Automations", () => { it("should handle bash output in conditional logic", async () => { const result = await createAutomationBuilder(config) - .appAction({ fields: { threshold: "5" } }) + .onAppAction({ fields: { threshold: "5" } }) .bash( { code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" }, { stepName: "Calculate Value" } diff --git a/packages/server/src/automations/tests/steps/createRow.spec.ts b/packages/server/src/automations/tests/steps/createRow.spec.ts index 4456550cb3..9d2fb5374f 100644 --- a/packages/server/src/automations/tests/steps/createRow.spec.ts +++ b/packages/server/src/automations/tests/steps/createRow.spec.ts @@ -41,7 +41,7 @@ describe("test the create row action", () => { it("should be able to run the action", async () => { const result = await createAutomationBuilder(config) - .appAction({ fields: { status: "new" } }) + .onAppAction({ fields: { status: "new" } }) .serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" }) .createRow({ row }, { stepName: "CreateRow" }) .serverLog( @@ -67,7 +67,7 @@ describe("test the create row action", () => { it("should return an error (not throw) when bad info provided", async () => { const result = await createAutomationBuilder(config) - .appAction({ fields: { status: "error" } }) + .onAppAction({ fields: { status: "error" } }) .serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" }) .createRow( { @@ -85,7 +85,7 @@ describe("test the create row action", () => { it("should check invalid inputs return an error", async () => { const result = await createAutomationBuilder(config) - .appAction({ fields: { status: "invalid" } }) + .onAppAction({ fields: { status: "invalid" } }) .serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" }) .createRow({ row: {} }, { stepName: "CreateRow" }) .filter({ @@ -123,7 +123,7 @@ describe("test the create row action", () => { attachmentRow.file_attachment = attachmentObject const result = await createAutomationBuilder(config) - .appAction({ fields: { type: "attachment" } }) + .onAppAction({ fields: { type: "attachment" } }) .serverLog( { text: "Processing attachment upload" }, { stepName: "StartLog" } @@ -174,7 +174,7 @@ describe("test the create row action", () => { attachmentRow.single_file_attachment = attachmentObject const result = await createAutomationBuilder(config) - .appAction({ fields: { type: "single-attachment" } }) + .onAppAction({ fields: { type: "single-attachment" } }) .serverLog( { text: "Processing single attachment" }, { stepName: "StartLog" } @@ -245,7 +245,7 @@ describe("test the create row action", () => { attachmentRow.single_file_attachment = attachmentObject const result = await createAutomationBuilder(config) - .appAction({ fields: { type: "invalid-attachment" } }) + .onAppAction({ fields: { type: "invalid-attachment" } }) .serverLog( { text: "Testing invalid attachment keys" }, { stepName: "StartLog" } diff --git a/packages/server/src/automations/tests/steps/cron-automations.spec.ts b/packages/server/src/automations/tests/steps/cron-automations.spec.ts index ed0729bd38..41de957c52 100644 --- a/packages/server/src/automations/tests/steps/cron-automations.spec.ts +++ b/packages/server/src/automations/tests/steps/cron-automations.spec.ts @@ -27,7 +27,7 @@ describe("cron automations", () => { }) it("should initialise the automation timestamp", async () => { - await createAutomationBuilder(config).cron({ cron: "* * * * *" }).save() + await createAutomationBuilder(config).onCron({ cron: "* * * * *" }).save() tk.travel(Date.now() + oneMinuteInMs) await config.publish() diff --git a/packages/server/src/automations/tests/steps/executeScript.spec.ts b/packages/server/src/automations/tests/steps/executeScript.spec.ts index 9f9b4e71f4..351af09a4a 100644 --- a/packages/server/src/automations/tests/steps/executeScript.spec.ts +++ b/packages/server/src/automations/tests/steps/executeScript.spec.ts @@ -21,7 +21,7 @@ describe("Execute Script Automations", () => { it("should execute a basic script and return the result", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .executeScript({ code: "return 2 + 2" }) .run({ fields: {} }) @@ -30,7 +30,7 @@ describe("Execute Script Automations", () => { it("should access bindings from previous steps", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .executeScript( { code: "return trigger.fields.data.map(x => x * 2)", @@ -44,7 +44,7 @@ describe("Execute Script Automations", () => { it("should handle script execution errors gracefully", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .executeScript({ code: "return nonexistentVariable.map(x => x)" }) .run({ fields: {} }) @@ -56,7 +56,7 @@ describe("Execute Script Automations", () => { it("should handle conditional logic in scripts", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .executeScript({ code: ` if (trigger.fields.value > 5) { @@ -73,7 +73,7 @@ describe("Execute Script Automations", () => { it("should use multiple steps and validate script execution", async () => { const results = await createAutomationBuilder(config) - .appAction() + .onAppAction() .serverLog( { text: "Starting multi-step automation" }, { stepId: "start-log-step" } diff --git a/packages/server/src/automations/tests/steps/loop.spec.ts b/packages/server/src/automations/tests/steps/loop.spec.ts index ba74bf6778..3d53b3aa01 100644 --- a/packages/server/src/automations/tests/steps/loop.spec.ts +++ b/packages/server/src/automations/tests/steps/loop.spec.ts @@ -73,7 +73,7 @@ describe("Attempt to run a basic loop automation", () => { it("should run an automation with a trigger, loop, and create row step", async () => { const results = await createAutomationBuilder(config) - .rowSaved( + .onRowSaved( { tableId: table._id! }, { row: { @@ -116,7 +116,7 @@ describe("Attempt to run a basic loop automation", () => { it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => { const results = await createAutomationBuilder(config) - .rowSaved( + .onRowSaved( { tableId: table._id! }, { row: { @@ -213,7 +213,7 @@ describe("Attempt to run a basic loop automation", () => { it("should run an automation where a loop is successfully run twice", async () => { const results = await createAutomationBuilder(config) - .rowSaved( + .onRowSaved( { tableId: table._id! }, { row: { diff --git a/packages/server/src/automations/tests/steps/openai.spec.ts b/packages/server/src/automations/tests/steps/openai.spec.ts index eef4bca25b..fb3382cff7 100644 --- a/packages/server/src/automations/tests/steps/openai.spec.ts +++ b/packages/server/src/automations/tests/steps/openai.spec.ts @@ -58,7 +58,7 @@ describe("test the openai action", () => { // own API key. We don't count this against your quota. const result = await expectAIUsage(0, () => createAutomationBuilder(config) - .appAction() + .onAppAction() .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) .run({ fields: {} }) ) @@ -70,7 +70,7 @@ describe("test the openai action", () => { it("should present the correct error message when a prompt is not provided", async () => { const result = await expectAIUsage(0, () => createAutomationBuilder(config) - .appAction() + .onAppAction() .openai({ prompt: "", model: Model.GPT_4O_MINI }) .run({ fields: {} }) ) @@ -86,7 +86,7 @@ describe("test the openai action", () => { const result = await expectAIUsage(0, () => createAutomationBuilder(config) - .appAction() + .onAppAction() .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) .run({ fields: {} }) ) @@ -109,7 +109,7 @@ describe("test the openai action", () => { // key, so we charge users for it. const result = await expectAIUsage(14, () => createAutomationBuilder(config) - .appAction() + .onAppAction() .openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" }) .run({ fields: {} }) ) diff --git a/packages/server/src/automations/tests/steps/queryRows.spec.ts b/packages/server/src/automations/tests/steps/queryRows.spec.ts index 7d54590987..b377d05e73 100644 --- a/packages/server/src/automations/tests/steps/queryRows.spec.ts +++ b/packages/server/src/automations/tests/steps/queryRows.spec.ts @@ -29,7 +29,7 @@ describe("Test a query step automation", () => { it("should be able to run the query step", async () => { const result = await createAutomationBuilder(config) - .appAction() + .onAppAction() .queryRows( { tableId: table._id!, @@ -54,7 +54,7 @@ describe("Test a query step automation", () => { it("Returns all rows when onEmptyFilter has no value and no filters are passed", async () => { const result = await createAutomationBuilder(config) - .appAction() + .onAppAction() .queryRows( { tableId: table._id!, @@ -75,7 +75,7 @@ describe("Test a query step automation", () => { it("Returns no rows when onEmptyFilter is RETURN_NONE and theres no filters", async () => { const result = await createAutomationBuilder(config) - .appAction() + .onAppAction() .queryRows( { tableId: table._id!, @@ -97,7 +97,7 @@ describe("Test a query step automation", () => { it("Returns no rows when onEmptyFilters RETURN_NONE and a filter is passed with a null value", async () => { const result = await createAutomationBuilder(config) - .appAction() + .onAppAction() .queryRows( { tableId: table._id!, @@ -123,7 +123,7 @@ describe("Test a query step automation", () => { it("Returns rows when onEmptyFilter is RETURN_ALL and no filter is passed", async () => { const result = await createAutomationBuilder(config) - .appAction() + .onAppAction() .queryRows( { tableId: table._id!, @@ -151,7 +151,7 @@ describe("Test a query step automation", () => { name: NAME, }) const result = await createAutomationBuilder(config) - .appAction() + .onAppAction() .queryRows( { tableId: tableWithSpaces._id!, diff --git a/packages/server/src/automations/tests/triggers/cron.spec.ts b/packages/server/src/automations/tests/triggers/cron.spec.ts index 84fe90c314..28ef49e5f5 100644 --- a/packages/server/src/automations/tests/triggers/cron.spec.ts +++ b/packages/server/src/automations/tests/triggers/cron.spec.ts @@ -25,7 +25,7 @@ describe("cron trigger", () => { }) await createAutomationBuilder(config) - .cron({ cron: "* * * * *" }) + .onCron({ cron: "* * * * *" }) .serverLog({ text: "Hello, world!", }) @@ -45,7 +45,7 @@ describe("cron trigger", () => { it("should fail if the cron expression is invalid", async () => { await createAutomationBuilder(config) - .cron({ cron: "* * * * * *" }) + .onCron({ cron: "* * * * * *" }) .serverLog({ text: "Hello, world!", }) diff --git a/packages/server/src/automations/tests/triggers/webhook.spec.ts b/packages/server/src/automations/tests/triggers/webhook.spec.ts index e549a6f496..664812f860 100644 --- a/packages/server/src/automations/tests/triggers/webhook.spec.ts +++ b/packages/server/src/automations/tests/triggers/webhook.spec.ts @@ -12,7 +12,7 @@ describe("Branching automations", () => { async function createWebhookAutomation() { const { automation } = await createAutomationBuilder(config) - .webhook({ fields: { parameter: "string" } }) + .onWebhook({ fields: { parameter: "string" } }) .createRow({ row: { tableId: table._id!, name: "{{ trigger.parameter }}" }, }) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index b29124ad0e..00f87d3616 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -54,13 +54,13 @@ class TriggerBuilder { } } - appAction = this.trigger(AutomationTriggerStepId.APP) + onAppAction = this.trigger(AutomationTriggerStepId.APP) - rowSaved = this.trigger(AutomationTriggerStepId.ROW_SAVED) - rowUpdated = this.trigger(AutomationTriggerStepId.ROW_UPDATED) - rowDeleted = this.trigger(AutomationTriggerStepId.ROW_DELETED) - webhook = this.trigger(AutomationTriggerStepId.WEBHOOK) - cron = this.trigger(AutomationTriggerStepId.CRON) + onRowSaved = this.trigger(AutomationTriggerStepId.ROW_SAVED) + onRowUpdated = this.trigger(AutomationTriggerStepId.ROW_UPDATED) + onRowDeleted = this.trigger(AutomationTriggerStepId.ROW_DELETED) + onWebhook = this.trigger(AutomationTriggerStepId.WEBHOOK) + onCron = this.trigger(AutomationTriggerStepId.CRON) } class BranchStepBuilder { From 924f4009921572753f17c31660584abd7fadc0f3 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 6 Feb 2025 15:58:15 +0000 Subject: [PATCH 306/321] Rename run to test to better reflect the API being hit. --- .../src/api/routes/tests/automation.spec.ts | 2 +- .../src/automations/tests/branching.spec.ts | 14 ++-- .../src/automations/tests/scenarios.spec.ts | 22 +++---- .../src/automations/tests/steps/bash.spec.ts | 6 +- .../automations/tests/steps/createRow.spec.ts | 12 ++-- .../tests/steps/executeScript.spec.ts | 10 +-- .../src/automations/tests/steps/loop.spec.ts | 6 +- .../automations/tests/steps/openai.spec.ts | 8 +-- .../automations/tests/steps/queryRows.spec.ts | 12 ++-- .../tests/utilities/AutomationTestBuilder.ts | 64 +++++++++---------- 10 files changed, 78 insertions(+), 78 deletions(-) diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index a27c21ded4..1d7b9cd6ed 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -251,7 +251,7 @@ describe("/automations", () => { .serverLog({ text: "{{ settings.company }}", }) - .run({ fields: {} }) + .test({ fields: {} }) expect(result.steps[0].outputs.message).toEndWith("https://example.com") expect(result.steps[1].outputs.message).toEndWith( diff --git a/packages/server/src/automations/tests/branching.spec.ts b/packages/server/src/automations/tests/branching.spec.ts index feac5f80f5..bf9b9ce3f8 100644 --- a/packages/server/src/automations/tests/branching.spec.ts +++ b/packages/server/src/automations/tests/branching.spec.ts @@ -76,7 +76,7 @@ describe("Branching automations", () => { }, }, }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps[3].outputs.status).toContain("branch1 branch taken") expect(results.steps[4].outputs.message).toContain("Branch 1.1") @@ -100,7 +100,7 @@ describe("Branching automations", () => { }, }, }) - .run({ fields: { status: "active" } }) + .test({ fields: { status: "active" } }) expect(results.steps[0].outputs.status).toContain( "activeBranch branch taken" ) @@ -130,7 +130,7 @@ describe("Branching automations", () => { }, }, }) - .run({ fields: { status: "active", role: "admin" } }) + .test({ fields: { status: "active", role: "admin" } }) expect(results.steps[1].outputs.message).toContain("Active admin user") }) @@ -162,7 +162,7 @@ describe("Branching automations", () => { }, }, }) - .run({ fields: { status: "test", role: "user" } }) + .test({ fields: { status: "test", role: "user" } }) expect(results.steps[1].outputs.message).toContain("Special user") }) @@ -195,7 +195,7 @@ describe("Branching automations", () => { }, }, }) - .run({ fields: { status: "test", role: "user" } }) + .test({ fields: { status: "test", role: "user" } }) expect(results.steps[1].outputs.status).toEqual( AutomationStatus.NO_CONDITION_MET @@ -239,7 +239,7 @@ describe("Branching automations", () => { }, }, }) - .run({ fields: { test_trigger: true } }) + .test({ fields: { test_trigger: true } }) expect(results.steps[2].outputs.message).toContain("Special user") }) @@ -276,7 +276,7 @@ describe("Branching automations", () => { }, }, }) - .run({ fields: { test_trigger: true } }) + .test({ fields: { test_trigger: true } }) expect(results.steps[2].outputs.message).toContain("Special user") }) diff --git a/packages/server/src/automations/tests/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios.spec.ts index e37aea838e..3015e75018 100644 --- a/packages/server/src/automations/tests/scenarios.spec.ts +++ b/packages/server/src/automations/tests/scenarios.spec.ts @@ -38,7 +38,7 @@ describe("Automation Scenarios", () => { tableId: table._id, }, }) - .run({ + .test({ row: { name: "Test", description: "TEST" }, id: "1234", }) @@ -67,7 +67,7 @@ describe("Automation Scenarios", () => { .queryRows({ tableId: table._id!, }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps).toHaveLength(1) expect(results.steps[0].outputs.rows).toHaveLength(2) @@ -93,7 +93,7 @@ describe("Automation Scenarios", () => { .queryRows({ tableId: table._id!, }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps).toHaveLength(3) expect(results.steps[1].outputs.success).toBeTruthy() @@ -153,7 +153,7 @@ describe("Automation Scenarios", () => { }, { stepName: "QueryRowsStep" } ) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps).toHaveLength(3) @@ -207,7 +207,7 @@ describe("Automation Scenarios", () => { .queryRows({ tableId: table._id!, }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps).toHaveLength(3) expect(results.steps[1].outputs.success).toBeTruthy() @@ -260,7 +260,7 @@ describe("Automation Scenarios", () => { value: 20, }) .serverLog({ text: "Equal condition met" }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps[2].outputs.success).toBeTrue() expect(results.steps[2].outputs.result).toBeFalse() @@ -286,7 +286,7 @@ describe("Automation Scenarios", () => { value: 20, }) .serverLog({ text: "Not Equal condition met" }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps[2].outputs.success).toBeTrue() expect(results.steps[2].outputs.result).toBeTrue() @@ -355,7 +355,7 @@ describe("Automation Scenarios", () => { .serverLog({ text: `${condition} condition ${expectPass ? "passed" : "failed"}`, }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps[2].outputs.result).toBe(expectPass) if (expectPass) { @@ -373,7 +373,7 @@ describe("Automation Scenarios", () => { const results = await createAutomationBuilder(config) .onRowUpdated({ tableId: table._id! }) .serverLog({ text: "{{ [user].[email] }}" }) - .run({ + .test({ row: { name: "Test", description: "TEST" }, id: "1234", }) @@ -385,7 +385,7 @@ describe("Automation Scenarios", () => { const results = await createAutomationBuilder(config) .onAppAction() .serverLog({ text: "{{ [user].[email] }}" }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps[0].outputs.message).toContain("example.com") }) @@ -475,7 +475,7 @@ if (descriptions.length) { .queryRows({ tableId: newTable._id!, }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps).toHaveLength(3) diff --git a/packages/server/src/automations/tests/steps/bash.spec.ts b/packages/server/src/automations/tests/steps/bash.spec.ts index 2174a6db42..48174ceb04 100644 --- a/packages/server/src/automations/tests/steps/bash.spec.ts +++ b/packages/server/src/automations/tests/steps/bash.spec.ts @@ -34,7 +34,7 @@ describe("Execute Bash Automations", () => { { text: "Bash output was: {{ steps.[Echo Command].stdout }}" }, { stepName: "Log Output" } ) - .run() + .test() expect(result.steps[0].outputs.stdout).toEqual("hello world\n") expect(result.steps[1].outputs.message).toContain( @@ -57,7 +57,7 @@ describe("Execute Bash Automations", () => { { code: "rm {{ trigger.fields.filename }}" }, { stepName: "Cleanup" } ) - .run() + .test() expect(result.steps[1].outputs.stdout).toEqual("INITIAL CONTENT\n") expect(result.steps[1].outputs.success).toEqual(true) @@ -112,7 +112,7 @@ describe("Execute Bash Automations", () => { { text: "Value was {{ steps.[Check Value].value }}" }, { stepName: "Log Result" } ) - .run() + .test() expect(result.steps[0].outputs.stdout).toEqual("10\n") expect(result.steps[1].outputs.value).toEqual("high") diff --git a/packages/server/src/automations/tests/steps/createRow.spec.ts b/packages/server/src/automations/tests/steps/createRow.spec.ts index 9d2fb5374f..a4523e15ef 100644 --- a/packages/server/src/automations/tests/steps/createRow.spec.ts +++ b/packages/server/src/automations/tests/steps/createRow.spec.ts @@ -48,7 +48,7 @@ describe("test the create row action", () => { { text: "Row created with ID: {{ stepsByName.CreateRow.row._id }}" }, { stepName: "CreationLog" } ) - .run() + .test() expect(result.steps[1].outputs.success).toBeDefined() expect(result.steps[1].outputs.id).toBeDefined() @@ -78,7 +78,7 @@ describe("test the create row action", () => { }, { stepName: "CreateRow" } ) - .run() + .test() expect(result.steps[1].outputs.success).toEqual(false) }) @@ -97,7 +97,7 @@ describe("test the create row action", () => { { text: "This log should not appear" }, { stepName: "SkippedLog" } ) - .run() + .test() expect(result.steps[1].outputs.success).toEqual(false) expect(result.steps.length).toBeLessThan(4) @@ -140,7 +140,7 @@ describe("test the create row action", () => { }, { stepName: "UploadLog" } ) - .run() + .test() expect(result.steps[1].outputs.success).toEqual(true) expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key") @@ -209,7 +209,7 @@ describe("test the create row action", () => { }, }, }) - .run() + .test() expect(result.steps[1].outputs.success).toEqual(true) expect(result.steps[1].outputs.row.single_file_attachment).toHaveProperty( @@ -278,7 +278,7 @@ describe("test the create row action", () => { }, }, }) - .run() + .test() expect(result.steps[1].outputs.success).toEqual(false) expect(result.steps[1].outputs.response).toEqual( diff --git a/packages/server/src/automations/tests/steps/executeScript.spec.ts b/packages/server/src/automations/tests/steps/executeScript.spec.ts index 351af09a4a..117c2341ba 100644 --- a/packages/server/src/automations/tests/steps/executeScript.spec.ts +++ b/packages/server/src/automations/tests/steps/executeScript.spec.ts @@ -23,7 +23,7 @@ describe("Execute Script Automations", () => { const results = await createAutomationBuilder(config) .onAppAction() .executeScript({ code: "return 2 + 2" }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps[0].outputs.value).toEqual(4) }) @@ -37,7 +37,7 @@ describe("Execute Script Automations", () => { }, { stepId: "binding-script-step" } ) - .run({ fields: { data: [1, 2, 3] } }) + .test({ fields: { data: [1, 2, 3] } }) expect(results.steps[0].outputs.value).toEqual([2, 4, 6]) }) @@ -46,7 +46,7 @@ describe("Execute Script Automations", () => { const results = await createAutomationBuilder(config) .onAppAction() .executeScript({ code: "return nonexistentVariable.map(x => x)" }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps[0].outputs.response).toContain( "ReferenceError: nonexistentVariable is not defined" @@ -66,7 +66,7 @@ describe("Execute Script Automations", () => { } `, }) - .run({ fields: { value: 10 } }) + .test({ fields: { value: 10 } }) expect(results.steps[0].outputs.value).toEqual("Value is greater than 5") }) @@ -94,7 +94,7 @@ describe("Execute Script Automations", () => { .serverLog({ text: `Final result is {{ steps.ScriptingStep1.value }}`, }) - .run({ fields: {} }) + .test({ fields: {} }) expect(results.steps[0].outputs.message).toContain( "Starting multi-step automation" diff --git a/packages/server/src/automations/tests/steps/loop.spec.ts b/packages/server/src/automations/tests/steps/loop.spec.ts index 3d53b3aa01..5e4f3e8126 100644 --- a/packages/server/src/automations/tests/steps/loop.spec.ts +++ b/packages/server/src/automations/tests/steps/loop.spec.ts @@ -95,7 +95,7 @@ describe("Attempt to run a basic loop automation", () => { tableId: table._id, }, }) - .run() + .test() expect(results.trigger).toBeDefined() expect(results.steps).toHaveLength(1) @@ -136,7 +136,7 @@ describe("Attempt to run a basic loop automation", () => { }) .serverLog({ text: "Message {{loop.currentItem}}" }) .serverLog({ text: "{{steps.1.rows.0._id}}" }) - .run() + .test() results.steps[1].outputs.items.forEach( (output: ServerLogStepOutputs, index: number) => { @@ -240,7 +240,7 @@ describe("Attempt to run a basic loop automation", () => { binding: "Message 1,Message 2,Message 3", }) .serverLog({ text: "{{loop.currentItem}}" }) - .run() + .test() expect(results.trigger).toBeDefined() expect(results.steps).toHaveLength(2) diff --git a/packages/server/src/automations/tests/steps/openai.spec.ts b/packages/server/src/automations/tests/steps/openai.spec.ts index fb3382cff7..d5f002571d 100644 --- a/packages/server/src/automations/tests/steps/openai.spec.ts +++ b/packages/server/src/automations/tests/steps/openai.spec.ts @@ -60,7 +60,7 @@ describe("test the openai action", () => { createAutomationBuilder(config) .onAppAction() .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) - .run({ fields: {} }) + .test({ fields: {} }) ) expect(result.steps[0].outputs.response).toEqual("This is a test") @@ -72,7 +72,7 @@ describe("test the openai action", () => { createAutomationBuilder(config) .onAppAction() .openai({ prompt: "", model: Model.GPT_4O_MINI }) - .run({ fields: {} }) + .test({ fields: {} }) ) expect(result.steps[0].outputs.response).toEqual( @@ -88,7 +88,7 @@ describe("test the openai action", () => { createAutomationBuilder(config) .onAppAction() .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) - .run({ fields: {} }) + .test({ fields: {} }) ) expect(result.steps[0].outputs.response).toEqual( @@ -111,7 +111,7 @@ describe("test the openai action", () => { createAutomationBuilder(config) .onAppAction() .openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" }) - .run({ fields: {} }) + .test({ fields: {} }) ) expect(result.steps[0].outputs.response).toEqual("This is a test") diff --git a/packages/server/src/automations/tests/steps/queryRows.spec.ts b/packages/server/src/automations/tests/steps/queryRows.spec.ts index b377d05e73..f6d756e770 100644 --- a/packages/server/src/automations/tests/steps/queryRows.spec.ts +++ b/packages/server/src/automations/tests/steps/queryRows.spec.ts @@ -44,7 +44,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query All Rows" } ) - .run({ fields: {} }) + .test({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -65,7 +65,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query With Empty Filter" } ) - .run({ fields: {} }) + .test({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -88,7 +88,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query With Return None" } ) - .run({ fields: {} }) + .test({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -114,7 +114,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query With Null Filter" } ) - .run({ fields: {} }) + .test({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -135,7 +135,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query With Return All" } ) - .run({ fields: {} }) + .test({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() @@ -160,7 +160,7 @@ describe("Test a query step automation", () => { }, { stepName: "Query table with spaces" } ) - .run({ fields: {} }) + .test({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.rows).toBeDefined() expect(result.steps[0].outputs.rows.length).toBe(1) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 00f87d3616..835a177ea8 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -31,7 +31,7 @@ type BranchConfig = { } class TriggerBuilder { - private config: TestConfiguration + private readonly config: TestConfiguration constructor(config: TestConfiguration) { this.config = config @@ -64,10 +64,10 @@ class TriggerBuilder { } class BranchStepBuilder { - protected steps: AutomationStep[] = [] - protected stepNames: { [key: string]: string } = {} + protected readonly steps: AutomationStep[] = [] + protected readonly stepNames: { [key: string]: string } = {} - protected createStepFn(stepId: TStep) { + protected step(stepId: TStep) { return ( inputs: AutomationStepInputs, opts?: { stepName?: string; stepId?: string } @@ -88,28 +88,28 @@ class BranchStepBuilder { } } - createRow = this.createStepFn(AutomationActionStepId.CREATE_ROW) - updateRow = this.createStepFn(AutomationActionStepId.UPDATE_ROW) - deleteRow = this.createStepFn(AutomationActionStepId.DELETE_ROW) - sendSmtpEmail = this.createStepFn(AutomationActionStepId.SEND_EMAIL_SMTP) - executeQuery = this.createStepFn(AutomationActionStepId.EXECUTE_QUERY) - queryRows = this.createStepFn(AutomationActionStepId.QUERY_ROWS) - loop = this.createStepFn(AutomationActionStepId.LOOP) - serverLog = this.createStepFn(AutomationActionStepId.SERVER_LOG) - executeScript = this.createStepFn(AutomationActionStepId.EXECUTE_SCRIPT) - filter = this.createStepFn(AutomationActionStepId.FILTER) - bash = this.createStepFn(AutomationActionStepId.EXECUTE_BASH) - openai = this.createStepFn(AutomationActionStepId.OPENAI) - collect = this.createStepFn(AutomationActionStepId.COLLECT) - zapier = this.createStepFn(AutomationActionStepId.zapier) - triggerAutomationRun = this.createStepFn( + createRow = this.step(AutomationActionStepId.CREATE_ROW) + updateRow = this.step(AutomationActionStepId.UPDATE_ROW) + deleteRow = this.step(AutomationActionStepId.DELETE_ROW) + sendSmtpEmail = this.step(AutomationActionStepId.SEND_EMAIL_SMTP) + executeQuery = this.step(AutomationActionStepId.EXECUTE_QUERY) + queryRows = this.step(AutomationActionStepId.QUERY_ROWS) + loop = this.step(AutomationActionStepId.LOOP) + serverLog = this.step(AutomationActionStepId.SERVER_LOG) + executeScript = this.step(AutomationActionStepId.EXECUTE_SCRIPT) + filter = this.step(AutomationActionStepId.FILTER) + bash = this.step(AutomationActionStepId.EXECUTE_BASH) + openai = this.step(AutomationActionStepId.OPENAI) + collect = this.step(AutomationActionStepId.COLLECT) + zapier = this.step(AutomationActionStepId.zapier) + triggerAutomationRun = this.step( AutomationActionStepId.TRIGGER_AUTOMATION_RUN ) - outgoingWebhook = this.createStepFn(AutomationActionStepId.OUTGOING_WEBHOOK) - n8n = this.createStepFn(AutomationActionStepId.n8n) - make = this.createStepFn(AutomationActionStepId.integromat) - discord = this.createStepFn(AutomationActionStepId.discord) - delay = this.createStepFn(AutomationActionStepId.DELAY) + outgoingWebhook = this.step(AutomationActionStepId.OUTGOING_WEBHOOK) + n8n = this.step(AutomationActionStepId.n8n) + make = this.step(AutomationActionStepId.integromat) + discord = this.step(AutomationActionStepId.discord) + delay = this.step(AutomationActionStepId.DELAY) protected addBranchStep(branchConfig: BranchConfig): void { const inputs: BranchStepInputs = { @@ -142,8 +142,8 @@ class BranchStepBuilder { class StepBuilder< TStep extends AutomationTriggerStepId > extends BranchStepBuilder { - private config: TestConfiguration - private trigger: AutomationTrigger + private readonly config: TestConfiguration + private readonly trigger: AutomationTrigger private _name: string | undefined = undefined constructor(config: TestConfiguration, trigger: AutomationTrigger) { @@ -176,26 +176,26 @@ class StepBuilder< return new AutomationRunner(this.config, automation) } - async run(outputs: AutomationTriggerOutputs) { + async test(triggerOutput: AutomationTriggerOutputs) { const runner = await this.save() - return await runner.run(outputs) + return await runner.test(triggerOutput) } } class AutomationRunner { - private config: TestConfiguration - automation: Automation + private readonly config: TestConfiguration + readonly automation: Automation constructor(config: TestConfiguration, automation: Automation) { this.config = config this.automation = automation } - async run(outputs: AutomationTriggerOutputs) { + async test(triggerOutput: AutomationTriggerOutputs) { const response = await this.config.api.automation.test( this.automation._id!, // TODO: figure out why this cast is needed. - outputs as TestAutomationRequest + triggerOutput as TestAutomationRequest ) if (isDidNotTriggerResponse(response)) { From 03e4cfe0b479ef4869df9f00744db78b43d9e278 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 6 Feb 2025 16:44:47 +0000 Subject: [PATCH 307/321] Missed a bunch of spots in the refactor. --- .../src/automations/tests/steps/bash.spec.ts | 18 ++-- .../automations/tests/steps/createRow.spec.ts | 24 ++--- .../src/automations/tests/steps/delay.spec.ts | 5 +- .../automations/tests/steps/deleteRow.spec.ts | 9 +- .../automations/tests/steps/discord.spec.ts | 3 +- .../automations/tests/steps/filter.spec.ts | 6 +- .../src/automations/tests/steps/loop.spec.ts | 90 +++++++++---------- .../src/automations/tests/steps/make.spec.ts | 9 +- .../src/automations/tests/steps/n8n.spec.ts | 12 ++- .../tests/steps/outgoingWebhook.spec.ts | 6 +- .../automations/tests/steps/serverLog.spec.ts | 3 +- .../tests/steps/triggerAutomationRun.spec.ts | 9 +- .../automations/tests/steps/updateRow.spec.ts | 15 ++-- .../automations/tests/steps/zapier.spec.ts | 9 +- 14 files changed, 125 insertions(+), 93 deletions(-) diff --git a/packages/server/src/automations/tests/steps/bash.spec.ts b/packages/server/src/automations/tests/steps/bash.spec.ts index 48174ceb04..a2172c3578 100644 --- a/packages/server/src/automations/tests/steps/bash.spec.ts +++ b/packages/server/src/automations/tests/steps/bash.spec.ts @@ -25,7 +25,7 @@ describe("Execute Bash Automations", () => { it("should use trigger data in bash command and pass output to subsequent steps", async () => { const result = await createAutomationBuilder(config) - .onAppAction({ fields: { command: "hello world" } }) + .onAppAction() .bash( { code: "echo '{{ trigger.fields.command }}'" }, { stepName: "Echo Command" } @@ -34,7 +34,7 @@ describe("Execute Bash Automations", () => { { text: "Bash output was: {{ steps.[Echo Command].stdout }}" }, { stepName: "Log Output" } ) - .test() + .test({ fields: { command: "hello world" } }) expect(result.steps[0].outputs.stdout).toEqual("hello world\n") expect(result.steps[1].outputs.message).toContain( @@ -44,7 +44,7 @@ describe("Execute Bash Automations", () => { it("should chain multiple bash commands using previous outputs", async () => { const result = await createAutomationBuilder(config) - .onAppAction({ fields: { filename: "testfile.txt" } }) + .onAppAction() .bash( { code: "echo 'initial content' > {{ trigger.fields.filename }}" }, { stepName: "Create File" } @@ -57,7 +57,7 @@ describe("Execute Bash Automations", () => { { code: "rm {{ trigger.fields.filename }}" }, { stepName: "Cleanup" } ) - .test() + .test({ fields: { filename: "testfile.txt" } }) expect(result.steps[1].outputs.stdout).toEqual("INITIAL CONTENT\n") expect(result.steps[1].outputs.success).toEqual(true) @@ -65,6 +65,7 @@ describe("Execute Bash Automations", () => { it("should integrate bash output with row operations", async () => { const result = await createAutomationBuilder(config) + .onAppAction() .queryRows( { tableId: table._id!, @@ -82,7 +83,7 @@ describe("Execute Bash Automations", () => { { text: "{{ steps.[Process Row Data].stdout }}" }, { stepName: "Log Result" } ) - .run() + .test({ fields: {} }) expect(result.steps[1].outputs.stdout).toContain( "Row data: test row - test description" @@ -94,7 +95,7 @@ describe("Execute Bash Automations", () => { it("should handle bash output in conditional logic", async () => { const result = await createAutomationBuilder(config) - .onAppAction({ fields: { threshold: "5" } }) + .onAppAction() .bash( { code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" }, { stepName: "Calculate Value" } @@ -112,7 +113,7 @@ describe("Execute Bash Automations", () => { { text: "Value was {{ steps.[Check Value].value }}" }, { stepName: "Log Result" } ) - .test() + .test({ fields: { threshold: "5" } }) expect(result.steps[0].outputs.stdout).toEqual("10\n") expect(result.steps[1].outputs.value).toEqual("high") @@ -121,12 +122,13 @@ describe("Execute Bash Automations", () => { it("should handle null values gracefully", async () => { const result = await createAutomationBuilder(config) + .onAppAction() .bash( // @ts-expect-error - testing null input { code: null }, { stepName: "Null Command" } ) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.stdout).toBe( "Budibase bash automation failed: Invalid inputs" diff --git a/packages/server/src/automations/tests/steps/createRow.spec.ts b/packages/server/src/automations/tests/steps/createRow.spec.ts index a4523e15ef..ec644c2b5e 100644 --- a/packages/server/src/automations/tests/steps/createRow.spec.ts +++ b/packages/server/src/automations/tests/steps/createRow.spec.ts @@ -41,14 +41,14 @@ describe("test the create row action", () => { it("should be able to run the action", async () => { const result = await createAutomationBuilder(config) - .onAppAction({ fields: { status: "new" } }) + .onAppAction() .serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" }) .createRow({ row }, { stepName: "CreateRow" }) .serverLog( { text: "Row created with ID: {{ stepsByName.CreateRow.row._id }}" }, { stepName: "CreationLog" } ) - .test() + .test({ fields: { status: "new" } }) expect(result.steps[1].outputs.success).toBeDefined() expect(result.steps[1].outputs.id).toBeDefined() @@ -67,7 +67,7 @@ describe("test the create row action", () => { it("should return an error (not throw) when bad info provided", async () => { const result = await createAutomationBuilder(config) - .onAppAction({ fields: { status: "error" } }) + .onAppAction() .serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" }) .createRow( { @@ -78,14 +78,14 @@ describe("test the create row action", () => { }, { stepName: "CreateRow" } ) - .test() + .test({ fields: { status: "error" } }) expect(result.steps[1].outputs.success).toEqual(false) }) it("should check invalid inputs return an error", async () => { const result = await createAutomationBuilder(config) - .onAppAction({ fields: { status: "invalid" } }) + .onAppAction() .serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" }) .createRow({ row: {} }, { stepName: "CreateRow" }) .filter({ @@ -97,7 +97,7 @@ describe("test the create row action", () => { { text: "This log should not appear" }, { stepName: "SkippedLog" } ) - .test() + .test({ fields: { status: "invalid" } }) expect(result.steps[1].outputs.success).toEqual(false) expect(result.steps.length).toBeLessThan(4) @@ -123,7 +123,7 @@ describe("test the create row action", () => { attachmentRow.file_attachment = attachmentObject const result = await createAutomationBuilder(config) - .onAppAction({ fields: { type: "attachment" } }) + .onAppAction() .serverLog( { text: "Processing attachment upload" }, { stepName: "StartLog" } @@ -140,7 +140,7 @@ describe("test the create row action", () => { }, { stepName: "UploadLog" } ) - .test() + .test({ fields: { type: "attachment" } }) expect(result.steps[1].outputs.success).toEqual(true) expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key") @@ -174,7 +174,7 @@ describe("test the create row action", () => { attachmentRow.single_file_attachment = attachmentObject const result = await createAutomationBuilder(config) - .onAppAction({ fields: { type: "single-attachment" } }) + .onAppAction() .serverLog( { text: "Processing single attachment" }, { stepName: "StartLog" } @@ -209,7 +209,7 @@ describe("test the create row action", () => { }, }, }) - .test() + .test({ fields: { type: "single-attachment" } }) expect(result.steps[1].outputs.success).toEqual(true) expect(result.steps[1].outputs.row.single_file_attachment).toHaveProperty( @@ -245,7 +245,7 @@ describe("test the create row action", () => { attachmentRow.single_file_attachment = attachmentObject const result = await createAutomationBuilder(config) - .onAppAction({ fields: { type: "invalid-attachment" } }) + .onAppAction() .serverLog( { text: "Testing invalid attachment keys" }, { stepName: "StartLog" } @@ -278,7 +278,7 @@ describe("test the create row action", () => { }, }, }) - .test() + .test({ fields: { type: "invalid-attachment" } }) expect(result.steps[1].outputs.success).toEqual(false) expect(result.steps[1].outputs.response).toEqual( diff --git a/packages/server/src/automations/tests/steps/delay.spec.ts b/packages/server/src/automations/tests/steps/delay.spec.ts index 9dc6470f5a..173beccbda 100644 --- a/packages/server/src/automations/tests/steps/delay.spec.ts +++ b/packages/server/src/automations/tests/steps/delay.spec.ts @@ -16,7 +16,10 @@ describe("test the delay logic", () => { const time = 100 const before = performance.now() - await createAutomationBuilder(config).delay({ time }).run() + await createAutomationBuilder(config) + .onAppAction() + .delay({ time }) + .test({ fields: {} }) const now = performance.now() diff --git a/packages/server/src/automations/tests/steps/deleteRow.spec.ts b/packages/server/src/automations/tests/steps/deleteRow.spec.ts index ee8c9b329f..8c141f82da 100644 --- a/packages/server/src/automations/tests/steps/deleteRow.spec.ts +++ b/packages/server/src/automations/tests/steps/deleteRow.spec.ts @@ -21,12 +21,13 @@ describe("test the delete row action", () => { it("should be able to run the delete row action", async () => { await createAutomationBuilder(config) + .onAppAction() .deleteRow({ tableId: table._id!, id: row._id!, revision: row._rev, }) - .run() + .test({ fields: {} }) await config.api.row.get(table._id!, row._id!, { status: 404, @@ -35,20 +36,22 @@ describe("test the delete row action", () => { it("should check invalid inputs return an error", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .deleteRow({ tableId: "", id: "", revision: "" }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs.success).toEqual(false) }) it("should return an error when table doesn't exist", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .deleteRow({ tableId: "invalid", id: "invalid", revision: "invalid", }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs.success).toEqual(false) }) diff --git a/packages/server/src/automations/tests/steps/discord.spec.ts b/packages/server/src/automations/tests/steps/discord.spec.ts index 361b3517f3..9618a0c994 100644 --- a/packages/server/src/automations/tests/steps/discord.spec.ts +++ b/packages/server/src/automations/tests/steps/discord.spec.ts @@ -20,12 +20,13 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) const result = await createAutomationBuilder(config) + .onAppAction() .discord({ url: "http://www.example.com", username: "joe_bloggs", content: "Hello, world", }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.success).toEqual(true) }) diff --git a/packages/server/src/automations/tests/steps/filter.spec.ts b/packages/server/src/automations/tests/steps/filter.spec.ts index 51af262aff..23c191b38d 100644 --- a/packages/server/src/automations/tests/steps/filter.spec.ts +++ b/packages/server/src/automations/tests/steps/filter.spec.ts @@ -43,8 +43,9 @@ describe("test the filter logic", () => { ] it.each(pass)("should pass %p %p %p", async (field, condition, value) => { const result = await createAutomationBuilder(config) + .onAppAction() .filter({ field, condition: stringToFilterCondition(condition), value }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.result).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true) @@ -60,8 +61,9 @@ describe("test the filter logic", () => { ] it.each(fail)("should fail %p %p %p", async (field, condition, value) => { const result = await createAutomationBuilder(config) + .onAppAction() .filter({ field, condition: stringToFilterCondition(condition), value }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.result).toEqual(false) expect(result.steps[0].outputs.success).toEqual(true) diff --git a/packages/server/src/automations/tests/steps/loop.spec.ts b/packages/server/src/automations/tests/steps/loop.spec.ts index 5e4f3e8126..1f985bd504 100644 --- a/packages/server/src/automations/tests/steps/loop.spec.ts +++ b/packages/server/src/automations/tests/steps/loop.spec.ts @@ -73,17 +73,7 @@ describe("Attempt to run a basic loop automation", () => { it("should run an automation with a trigger, loop, and create row step", async () => { const results = await createAutomationBuilder(config) - .onRowSaved( - { tableId: table._id! }, - { - row: { - name: "Trigger Row", - description: "This row triggers the automation", - }, - id: "1234", - revision: "1", - } - ) + .onRowSaved({ tableId: table._id! }) .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], @@ -95,7 +85,14 @@ describe("Attempt to run a basic loop automation", () => { tableId: table._id, }, }) - .test() + .test({ + row: { + name: "Trigger Row", + description: "This row triggers the automation", + }, + id: "1234", + revision: "1", + }) expect(results.trigger).toBeDefined() expect(results.steps).toHaveLength(1) @@ -116,17 +113,7 @@ describe("Attempt to run a basic loop automation", () => { it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => { const results = await createAutomationBuilder(config) - .onRowSaved( - { tableId: table._id! }, - { - row: { - name: "Trigger Row", - description: "This row triggers the automation", - }, - id: "1234", - revision: "1", - } - ) + .onRowSaved({ tableId: table._id! }) .queryRows({ tableId: table._id!, }) @@ -136,7 +123,14 @@ describe("Attempt to run a basic loop automation", () => { }) .serverLog({ text: "Message {{loop.currentItem}}" }) .serverLog({ text: "{{steps.1.rows.0._id}}" }) - .test() + .test({ + row: { + name: "Trigger Row", + description: "This row triggers the automation", + }, + id: "1234", + revision: "1", + }) results.steps[1].outputs.items.forEach( (output: ServerLogStepOutputs, index: number) => { @@ -152,12 +146,13 @@ describe("Attempt to run a basic loop automation", () => { it("if an incorrect type is passed to the loop it should return an error", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: "1, 2, 3", }) .serverLog({ text: "Message {{loop.currentItem}}" }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs).toEqual({ success: false, @@ -167,13 +162,14 @@ describe("Attempt to run a basic loop automation", () => { it("ensure the loop stops if the failure condition is reached", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], failure: "test2", }) .serverLog({ text: "Message {{loop.currentItem}}" }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs).toEqual( expect.objectContaining({ @@ -185,6 +181,7 @@ describe("Attempt to run a basic loop automation", () => { it("ensure the loop stops if the max iterations are reached", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], @@ -192,13 +189,14 @@ describe("Attempt to run a basic loop automation", () => { }) .serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{steps.1.iterations}}" }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs.iterations).toBe(2) }) it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], @@ -206,24 +204,14 @@ describe("Attempt to run a basic loop automation", () => { }) .serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{steps.1.iterations}}" }) - .run() + .test({ fields: {} }) expect(results.steps[1].outputs.message).toContain("- 2") }) it("should run an automation where a loop is successfully run twice", async () => { const results = await createAutomationBuilder(config) - .onRowSaved( - { tableId: table._id! }, - { - row: { - name: "Trigger Row", - description: "This row triggers the automation", - }, - id: "1234", - revision: "1", - } - ) + .onRowSaved({ tableId: table._id! }) .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], @@ -240,7 +228,14 @@ describe("Attempt to run a basic loop automation", () => { binding: "Message 1,Message 2,Message 3", }) .serverLog({ text: "{{loop.currentItem}}" }) - .test() + .test({ + row: { + name: "Trigger Row", + description: "This row triggers the automation", + }, + id: "1234", + revision: "1", + }) expect(results.trigger).toBeDefined() expect(results.steps).toHaveLength(2) @@ -275,6 +270,7 @@ describe("Attempt to run a basic loop automation", () => { it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], @@ -287,7 +283,7 @@ describe("Attempt to run a basic loop automation", () => { }) .serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{steps.3.iterations}}" }) - .run() + .test({ fields: {} }) // We want to ensure that bindings are corr expect(results.steps[1].outputs.message).toContain("- 3") @@ -296,6 +292,7 @@ describe("Attempt to run a basic loop automation", () => { it("should use automation names to loop with", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .loop( { option: LoopStepType.ARRAY, @@ -311,7 +308,7 @@ describe("Attempt to run a basic loop automation", () => { { text: "{{steps.FirstLoopLog.iterations}}" }, { stepName: "FirstLoopIterationLog" } ) - .run() + .test({ fields: {} }) expect(results.steps[1].outputs.message).toContain("- 3") }) @@ -347,6 +344,7 @@ describe("Attempt to run a basic loop automation", () => { await config.api.row.bulkImport(table._id!, { rows }) const results = await createAutomationBuilder(config) + .onAppAction() .queryRows({ tableId: table._id!, }) @@ -366,7 +364,7 @@ describe("Attempt to run a basic loop automation", () => { .queryRows({ tableId: table._id!, }) - .run() + .test({ fields: {} }) const expectedRows = [ { name: "Updated Row 1", value: 1 }, @@ -426,6 +424,7 @@ describe("Attempt to run a basic loop automation", () => { await config.api.row.bulkImport(table._id!, { rows }) const results = await createAutomationBuilder(config) + .onAppAction() .queryRows( { tableId: table._id!, @@ -448,7 +447,7 @@ describe("Attempt to run a basic loop automation", () => { .queryRows({ tableId: table._id!, }) - .run() + .test({ fields: {} }) const expectedRows = [ { name: "Updated Row 1", value: 1 }, @@ -508,6 +507,7 @@ describe("Attempt to run a basic loop automation", () => { await config.api.row.bulkImport(table._id!, { rows }) const results = await createAutomationBuilder(config) + .onAppAction() .queryRows({ tableId: table._id!, }) @@ -522,7 +522,7 @@ describe("Attempt to run a basic loop automation", () => { .queryRows({ tableId: table._id!, }) - .run() + .test({ fields: {} }) expect(results.steps).toHaveLength(3) diff --git a/packages/server/src/automations/tests/steps/make.spec.ts b/packages/server/src/automations/tests/steps/make.spec.ts index 2d118d943f..bbc0c3791a 100644 --- a/packages/server/src/automations/tests/steps/make.spec.ts +++ b/packages/server/src/automations/tests/steps/make.spec.ts @@ -20,11 +20,12 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) const result = await createAutomationBuilder(config) + .onAppAction() .make({ url: "http://www.example.com", body: null, }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.success).toEqual(true) @@ -46,11 +47,12 @@ describe("test the outgoing webhook action", () => { .reply(200, { foo: "bar" }) const result = await createAutomationBuilder(config) + .onAppAction() .make({ body: { value: JSON.stringify(payload) }, url: "http://www.example.com", }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.success).toEqual(true) @@ -58,11 +60,12 @@ describe("test the outgoing webhook action", () => { it("should return a 400 if the JSON payload string is malformed", async () => { const result = await createAutomationBuilder(config) + .onAppAction() .make({ body: { value: "{ invalid json }" }, url: "http://www.example.com", }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.httpStatus).toEqual(400) expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON") diff --git a/packages/server/src/automations/tests/steps/n8n.spec.ts b/packages/server/src/automations/tests/steps/n8n.spec.ts index d3efb1aaeb..4ee3123d98 100644 --- a/packages/server/src/automations/tests/steps/n8n.spec.ts +++ b/packages/server/src/automations/tests/steps/n8n.spec.ts @@ -21,12 +21,13 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action and default to 'get'", async () => { nock("http://www.example.com/").get("/").reply(200, { foo: "bar" }) const result = await createAutomationBuilder(config) + .onAppAction() .n8n({ url: "http://www.example.com", body: { test: "IGNORE_ME" }, authorization: "", }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.response).toEqual({ foo: "bar" }) expect(result.steps[0].outputs.httpStatus).toEqual(200) @@ -39,26 +40,28 @@ describe("test the outgoing webhook action", () => { .reply(200) const result = await createAutomationBuilder(config) + .onAppAction() .n8n({ url: "http://www.example.com", body: { value: JSON.stringify({ name: "Adam", age: 9 }) }, method: HttpMethod.POST, authorization: "", }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.success).toEqual(true) }) it("should return a 400 if the JSON payload string is malformed", async () => { const result = await createAutomationBuilder(config) + .onAppAction() .n8n({ url: "http://www.example.com", body: { value: "{ value1 1 }" }, method: HttpMethod.POST, authorization: "", }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.httpStatus).toEqual(400) expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON") @@ -71,13 +74,14 @@ describe("test the outgoing webhook action", () => { .reply(200) const result = await createAutomationBuilder(config) + .onAppAction() .n8n({ url: "http://www.example.com", method: HttpMethod.HEAD, body: { test: "IGNORE_ME" }, authorization: "", }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.success).toEqual(true) }) diff --git a/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts b/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts index b1d13c6917..85ccfb8eac 100644 --- a/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts +++ b/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts @@ -24,13 +24,14 @@ describe("test the outgoing webhook action", () => { .reply(200, { foo: "bar" }) const result = await createAutomationBuilder(config) + .onAppAction() .outgoingWebhook({ requestMethod: RequestType.POST, url: "http://www.example.com", requestBody: JSON.stringify({ a: 1 }), headers: {}, }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.httpStatus).toEqual(200) @@ -39,13 +40,14 @@ describe("test the outgoing webhook action", () => { it("should return an error if something goes wrong in fetch", async () => { const result = await createAutomationBuilder(config) + .onAppAction() .outgoingWebhook({ requestMethod: RequestType.GET, url: "www.invalid.com", requestBody: "", headers: {}, }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.success).toEqual(false) }) }) diff --git a/packages/server/src/automations/tests/steps/serverLog.spec.ts b/packages/server/src/automations/tests/steps/serverLog.spec.ts index 44a9f068b1..82f097d0da 100644 --- a/packages/server/src/automations/tests/steps/serverLog.spec.ts +++ b/packages/server/src/automations/tests/steps/serverLog.spec.ts @@ -14,8 +14,9 @@ describe("test the server log action", () => { it("should be able to log the text", async () => { const result = await createAutomationBuilder(config) + .onAppAction() .serverLog({ text: "Hello World" }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.message).toEqual( `App ${config.getAppId()} - Hello World` ) diff --git a/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts b/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts index ef851bc047..8d4a29c2b6 100644 --- a/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts +++ b/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts @@ -17,24 +17,27 @@ describe("Test triggering an automation from another automation", () => { }) it("should trigger an other server log automation", async () => { - const automation = await createAutomationBuilder(config) + const { automation } = await createAutomationBuilder(config) + .onAppAction() .serverLog({ text: "Hello World" }) .save() const result = await createAutomationBuilder(config) + .onAppAction() .triggerAutomationRun({ automation: { automationId: automation._id!, }, timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT, }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.success).toBe(true) }) it("should fail gracefully if the automation id is incorrect", async () => { const result = await createAutomationBuilder(config) + .onAppAction() .triggerAutomationRun({ automation: { // @ts-expect-error - incorrect on purpose @@ -42,7 +45,7 @@ describe("Test triggering an automation from another automation", () => { }, timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT, }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.success).toBe(false) }) diff --git a/packages/server/src/automations/tests/steps/updateRow.spec.ts b/packages/server/src/automations/tests/steps/updateRow.spec.ts index 32c7b90446..a2f1825099 100644 --- a/packages/server/src/automations/tests/steps/updateRow.spec.ts +++ b/packages/server/src/automations/tests/steps/updateRow.spec.ts @@ -31,6 +31,7 @@ describe("test the update row action", () => { it("should be able to run the update row action", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .updateRow({ rowId: row._id!, row: { @@ -40,7 +41,7 @@ describe("test the update row action", () => { }, meta: {}, }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs.success).toEqual(true) const updatedRow = await config.api.row.get( @@ -53,20 +54,22 @@ describe("test the update row action", () => { it("should check invalid inputs return an error", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .updateRow({ meta: {}, row: {}, rowId: "" }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs.success).toEqual(false) }) it("should return an error when table doesn't exist", async () => { const results = await createAutomationBuilder(config) + .onAppAction() .updateRow({ row: { _id: "invalid" }, rowId: "invalid", meta: {}, }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs.success).toEqual(false) }) @@ -104,6 +107,7 @@ describe("test the update row action", () => { }) const results = await createAutomationBuilder(config) + .onAppAction() .updateRow({ rowId: row._id!, row: { @@ -115,7 +119,7 @@ describe("test the update row action", () => { }, meta: {}, }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs.success).toEqual(true) @@ -157,6 +161,7 @@ describe("test the update row action", () => { }) const results = await createAutomationBuilder(config) + .onAppAction() .updateRow({ rowId: row._id!, row: { @@ -174,7 +179,7 @@ describe("test the update row action", () => { }, }, }) - .run() + .test({ fields: {} }) expect(results.steps[0].outputs.success).toEqual(true) diff --git a/packages/server/src/automations/tests/steps/zapier.spec.ts b/packages/server/src/automations/tests/steps/zapier.spec.ts index e897083d18..e6b5417563 100644 --- a/packages/server/src/automations/tests/steps/zapier.spec.ts +++ b/packages/server/src/automations/tests/steps/zapier.spec.ts @@ -21,8 +21,9 @@ describe("test the outgoing webhook action", () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) const result = await createAutomationBuilder(config) + .onAppAction() .zapier({ url: "http://www.example.com", body: null }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.success).toEqual(true) @@ -44,11 +45,12 @@ describe("test the outgoing webhook action", () => { .reply(200, { foo: "bar" }) const result = await createAutomationBuilder(config) + .onAppAction() .zapier({ url: "http://www.example.com", body: { value: JSON.stringify(payload) }, }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.success).toEqual(true) @@ -56,11 +58,12 @@ describe("test the outgoing webhook action", () => { it("should return a 400 if the JSON payload string is malformed", async () => { const result = await createAutomationBuilder(config) + .onAppAction() .zapier({ url: "http://www.example.com", body: { value: "{ invalid json }" }, }) - .run() + .test({ fields: {} }) expect(result.steps[0].outputs.success).toEqual(false) expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON") From 399cf0ef6c9189553bb31ca508ad5da910682892 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 6 Feb 2025 16:48:17 +0000 Subject: [PATCH 308/321] Removing unused elements. --- .../backend/modals/DeleteDataConfirmationModal.svelte | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index 0ef177db64..39832cd491 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -1,5 +1,5 @@ From 28e931bc643ce93c6b5210bf043e3e35304d0c59 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 6 Feb 2025 17:06:36 +0000 Subject: [PATCH 309/321] Fix types. --- packages/server/src/tests/utilities/structures.ts | 4 +--- packages/types/src/documents/app/automation/schema.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index f99d961ae6..fa91c7ca0f 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -217,10 +217,8 @@ export function basicAutomation(opts?: DeepPartial): Automation { icon: "test", description: "test", type: AutomationStepType.TRIGGER, + inputs: {}, id: "test", - inputs: { - fields: {}, - }, schema: { inputs: { properties: {}, diff --git a/packages/types/src/documents/app/automation/schema.ts b/packages/types/src/documents/app/automation/schema.ts index 66a6f508fe..820858b48c 100644 --- a/packages/types/src/documents/app/automation/schema.ts +++ b/packages/types/src/documents/app/automation/schema.ts @@ -331,7 +331,7 @@ export type AutomationTriggerDefinition = Omit< export type AutomationTriggerInputs = T extends AutomationTriggerStepId.APP - ? void + ? void | Record : T extends AutomationTriggerStepId.CRON ? CronTriggerInputs : T extends AutomationTriggerStepId.ROW_ACTION From e8c79bc72008d757e8cbef488c66c96f9384c27f Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 6 Feb 2025 17:24:04 +0000 Subject: [PATCH 310/321] Most PR comments. --- .../modals/DeleteDataConfirmationModal.svelte | 7 +- .../src/api/routes/tests/screen.spec.ts | 99 +++++++------------ packages/server/src/constants/screens.ts | 4 +- packages/server/src/sdk/app/common/utils.ts | 2 +- .../shared-core/src/sdk/documents/screens.ts | 12 +-- 5 files changed, 46 insertions(+), 78 deletions(-) diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index 39832cd491..f55e0e7013 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -10,6 +10,7 @@ viewsV2, } from "@/stores/builder" import ConfirmDialog from "@/components/common/ConfirmDialog.svelte" + import { helpers } from "@budibase/shared-core" import { SourceType } from "@budibase/types" import { goto, params } from "@roxi/routify" import { DB_TYPE_EXTERNAL } from "@/constants/backend" @@ -85,7 +86,7 @@ async function deleteView(view: ViewV2 | View) { try { - if ("version" in view && view.version === 2) { + if ("version" in view && helpers.views.isCalculationView(view)) { await viewsV2.delete(view as ViewV2) } else { await views.delete(view as View) @@ -99,10 +100,10 @@ async function deleteDatasource(datasource: Datasource) { try { - const isSelected = - get(datasources).selectedDatasourceId === datasource._id await datasources.delete(datasource) notifications.success("Datasource deleted") + const isSelected = + get(datasources).selectedDatasourceId === datasource._id if (isSelected) { $goto("./datasource") } diff --git a/packages/server/src/api/routes/tests/screen.spec.ts b/packages/server/src/api/routes/tests/screen.spec.ts index 90ec4a567f..b6d6ceda3b 100644 --- a/packages/server/src/api/routes/tests/screen.spec.ts +++ b/packages/server/src/api/routes/tests/screen.spec.ts @@ -33,7 +33,7 @@ describe("/screens", () => { describe("fetch", () => { it("should be able to create a layout", async () => { - const screens = await config.api.screen.list({ status: 200 }) + const screens = await config.api.screen.list() expect(screens.length).toEqual(1) expect(screens.some(s => s._id === screen._id)).toEqual(true) }) @@ -67,28 +67,22 @@ describe("/screens", () => { inherits: [role1._id!, role2._id!], permissionId: BuiltinPermissionID.WRITE, }) - screen1 = await config.api.screen.save( - { - ...basicScreen(), - routing: { - roleId: role1._id!, - route: "/foo", - homeScreen: false, - }, + screen1 = await config.api.screen.save({ + ...basicScreen(), + routing: { + roleId: role1._id!, + route: "/foo", + homeScreen: false, }, - { status: 200 } - ) - screen2 = await config.api.screen.save( - { - ...basicScreen(), - routing: { - roleId: role2._id!, - route: "/bar", - homeScreen: false, - }, + }) + screen2 = await config.api.screen.save({ + ...basicScreen(), + routing: { + roleId: role2._id!, + route: "/bar", + homeScreen: false, }, - { status: 200 } - ) + }) // get into prod app await config.publish() }) @@ -96,10 +90,7 @@ describe("/screens", () => { async function checkScreens(roleId: string, screenIds: string[]) { await config.loginAsRole(roleId, async () => { const res = await config.api.application.getDefinition( - config.prodAppId!, - { - status: 200, - } + config.prodAppId! ) expect(res.screens.length).toEqual(screenIds.length) expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort()) @@ -129,10 +120,7 @@ describe("/screens", () => { }, async () => { const res = await config.api.application.getDefinition( - config.prodAppId!, - { - status: 200, - } + config.prodAppId! ) const screenIds = [screen._id!, screen1._id!] expect(res.screens.length).toEqual(screenIds.length) @@ -149,9 +137,7 @@ describe("/screens", () => { it("should be able to create a screen", async () => { const screen = basicScreen() - const responseScreen = await config.api.screen.save(screen, { - status: 200, - }) + const responseScreen = await config.api.screen.save(screen) expect(responseScreen._rev).toBeDefined() expect(responseScreen.name).toEqual(screen.name) @@ -160,13 +146,13 @@ describe("/screens", () => { it("should be able to update a screen", async () => { const screen = basicScreen() - let responseScreen = await config.api.screen.save(screen, { status: 200 }) + let responseScreen = await config.api.screen.save(screen) screen._id = responseScreen._id screen._rev = responseScreen._rev screen.name = "edit" jest.clearAllMocks() - responseScreen = await config.api.screen.save(screen, { status: 200 }) + responseScreen = await config.api.screen.save(screen) expect(responseScreen._rev).toBeDefined() expect(responseScreen.name).toEqual(screen.name) @@ -186,8 +172,7 @@ describe("/screens", () => { it("should be able to delete the screen", async () => { const response = await config.api.screen.destroy( screen._id!, - screen._rev!, - { status: 200 } + screen._rev! ) expect(response.message).toBeDefined() expect(events.screen.deleted).toHaveBeenCalledTimes(1) @@ -205,9 +190,7 @@ describe("/screens", () => { describe("usage", () => { beforeEach(async () => { await config.init() - await config.api.screen.save(basicScreen(), { - status: 200, - }) + await config.api.screen.save(basicScreen()) }) function confirmScreen(usage: UsageOfScreensResponse, screen: Screen) { @@ -217,57 +200,41 @@ describe("/screens", () => { } it("should find table usage", async () => { - const table = await config.api.table.save(basicTable(), { status: 200 }) + const table = await config.api.table.save(basicTable()) const screen = await config.api.screen.save( - tableScreen("BudibaseDB", table), - { status: 200 } + tableScreen("BudibaseDB", table) ) - const usage = await config.api.screen.usage(table._id!, { status: 200 }) + const usage = await config.api.screen.usage(table._id!) expect(usage.sourceType).toEqual(SourceType.TABLE) confirmScreen(usage, screen) }) it("should find view usage", async () => { - const table = await config.api.table.save(basicTable(), { status: 200 }) + const table = await config.api.table.save(basicTable()) const view = await config.api.viewV2.create( viewV2.createRequest(table._id!), { status: 201 } ) const screen = await config.api.screen.save( - viewScreen("BudibaseDB", view), - { - status: 200, - } + viewScreen("BudibaseDB", view) ) - const usage = await config.api.screen.usage(view.id, { status: 200 }) + const usage = await config.api.screen.usage(view.id) expect(usage.sourceType).toEqual(SourceType.VIEW) confirmScreen(usage, screen) }) it("should find datasource/query usage", async () => { const datasource = await config.api.datasource.create( - basicDatasource().datasource, - { - status: 200, - } + basicDatasource().datasource ) - const query = await config.api.query.save(basicQuery(datasource._id!), { - status: 200, - }) + const query = await config.api.query.save(basicQuery(datasource._id!)) const screen = await config.api.screen.save( - queryScreen(datasource._id!, query), - { - status: 200, - } + queryScreen(datasource._id!, query) ) - const dsUsage = await config.api.screen.usage(datasource._id!, { - status: 200, - }) + const dsUsage = await config.api.screen.usage(datasource._id!) expect(dsUsage.sourceType).toEqual(SourceType.DATASOURCE) confirmScreen(dsUsage, screen) - const queryUsage = await config.api.screen.usage(query._id!, { - status: 200, - }) + const queryUsage = await config.api.screen.usage(query._id!) expect(queryUsage.sourceType).toEqual(SourceType.QUERY) confirmScreen(queryUsage, screen) }) diff --git a/packages/server/src/constants/screens.ts b/packages/server/src/constants/screens.ts index 52f0b3d9ff..1cbc049e42 100644 --- a/packages/server/src/constants/screens.ts +++ b/packages/server/src/constants/screens.ts @@ -1,6 +1,6 @@ import { roles } from "@budibase/backend-core" import { BASE_LAYOUT_PROP_IDS } from "./layouts" -import { Screen, Table, Query, ViewV2 } from "@budibase/types" +import { Screen, Table, Query, ViewV2, Component } from "@budibase/types" export function createHomeScreen( config: { @@ -54,7 +54,7 @@ export function createHomeScreen( } } -function heading(text: string) { +function heading(text: string): Component { return { _id: "c1bff24cd821e41d18c894ac77a80ef99", _component: "@budibase/standard-components/heading", diff --git a/packages/server/src/sdk/app/common/utils.ts b/packages/server/src/sdk/app/common/utils.ts index ed4e4c9a4f..6dac729872 100644 --- a/packages/server/src/sdk/app/common/utils.ts +++ b/packages/server/src/sdk/app/common/utils.ts @@ -11,5 +11,5 @@ export function getSourceType(sourceId: string): SourceType { } else if (docIds.isQueryId(sourceId)) { return SourceType.QUERY } - throw new Error("Unknown source type - cannot find document type") + throw new Error(`Unknown source type for source "${sourceId}"`) } diff --git a/packages/shared-core/src/sdk/documents/screens.ts b/packages/shared-core/src/sdk/documents/screens.ts index 8b6ea276b2..3ceffd26a6 100644 --- a/packages/shared-core/src/sdk/documents/screens.ts +++ b/packages/shared-core/src/sdk/documents/screens.ts @@ -3,17 +3,17 @@ import { Screen } from "@budibase/types" export function findInSettings(screen: Screen, toFind: string) { const foundIn: { setting: string; value: string }[] = [] function recurse(props: Record, parentKey = "") { - for (let key of Object.keys(props)) { - if (!props[key]) { + for (const [key, value] of Object.entries(props)) { + if (!value) { continue } - if (typeof props[key] === "string" && props[key].includes(toFind)) { + if (typeof value === "string" && value.includes(toFind)) { foundIn.push({ setting: parentKey ? `${parentKey}.${key}` : key, - value: props[key], + value: value, }) - } else if (typeof props[key] === "object") { - recurse(props[key], key) + } else if (typeof value === "object") { + recurse(value, key) } } } From 83fdbccd6b1f4aaa8f84495e165ad56ab0015004 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 6 Feb 2025 17:34:44 +0000 Subject: [PATCH 311/321] export directly. --- .../src/api/routes/tests/screen.spec.ts | 12 +++++----- .../server/src/tests/utilities/structures.ts | 24 +++++-------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/packages/server/src/api/routes/tests/screen.spec.ts b/packages/server/src/api/routes/tests/screen.spec.ts index b6d6ceda3b..261c287087 100644 --- a/packages/server/src/api/routes/tests/screen.spec.ts +++ b/packages/server/src/api/routes/tests/screen.spec.ts @@ -11,9 +11,9 @@ import { const { basicScreen, - tableScreen, - viewScreen, - queryScreen, + createTableScreen, + createViewScreen, + createQueryScreen, basicTable, viewV2, basicQuery, @@ -202,7 +202,7 @@ describe("/screens", () => { it("should find table usage", async () => { const table = await config.api.table.save(basicTable()) const screen = await config.api.screen.save( - tableScreen("BudibaseDB", table) + createTableScreen("BudibaseDB", table) ) const usage = await config.api.screen.usage(table._id!) expect(usage.sourceType).toEqual(SourceType.TABLE) @@ -216,7 +216,7 @@ describe("/screens", () => { { status: 201 } ) const screen = await config.api.screen.save( - viewScreen("BudibaseDB", view) + createViewScreen("BudibaseDB", view) ) const usage = await config.api.screen.usage(view.id) expect(usage.sourceType).toEqual(SourceType.VIEW) @@ -229,7 +229,7 @@ describe("/screens", () => { ) const query = await config.api.query.save(basicQuery(datasource._id!)) const screen = await config.api.screen.save( - queryScreen(datasource._id!, query) + createQueryScreen(datasource._id!, query) ) const dsUsage = await config.api.screen.usage(datasource._id!) expect(dsUsage.sourceType).toEqual(SourceType.DATASOURCE) diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 864b9f5399..7ca326e825 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -1,10 +1,5 @@ import { roles, utils } from "@budibase/backend-core" -import { - createHomeScreen, - createTableScreen, - createQueryScreen, - createViewScreen, -} from "../../constants/screens" +import { createHomeScreen } from "../../constants/screens" import { EMPTY_LAYOUT } from "../../constants/layouts" import { cloneDeep } from "lodash/fp" import { @@ -45,6 +40,11 @@ import { import { LoopInput } from "../../definitions/automations" import { merge } from "lodash" import { generator } from "@budibase/backend-core/tests" +export { + createTableScreen, + createQueryScreen, + createViewScreen, +} from "../../constants/screens" const { BUILTIN_ROLE_IDS } = roles @@ -523,18 +523,6 @@ export function basicScreen(route = "/") { }) } -export function tableScreen(datasourceName: string, table: Table) { - return createTableScreen(datasourceName, table) -} - -export function viewScreen(datasourceName: string, view: ViewV2) { - return createViewScreen(datasourceName, view) -} - -export function queryScreen(datasourceId: string, query: Query) { - return createQueryScreen(datasourceId, query) -} - export function powerScreen(route = "/") { return createHomeScreen({ roleId: BUILTIN_ROLE_IDS.POWER, From a5f2a050617495a847917ca1e14b7decdccdfb38 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 6 Feb 2025 17:42:06 +0000 Subject: [PATCH 312/321] Linting. --- packages/server/src/tests/utilities/structures.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 7ca326e825..3055b9c760 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -35,7 +35,6 @@ import { WebhookActionType, BuiltinPermissionID, DeepPartial, - ViewV2, } from "@budibase/types" import { LoopInput } from "../../definitions/automations" import { merge } from "lodash" From f9c75a213534a2ca7a8b5188328f871b462ade76 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 6 Feb 2025 17:53:23 +0000 Subject: [PATCH 313/321] Final comments. --- .../backend/modals/DeleteDataConfirmationModal.svelte | 2 +- packages/server/src/api/routes/tests/screen.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index f55e0e7013..e3656328d7 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -86,7 +86,7 @@ async function deleteView(view: ViewV2 | View) { try { - if ("version" in view && helpers.views.isCalculationView(view)) { + if ("version" in view && helpers.views.isV2(view)) { await viewsV2.delete(view as ViewV2) } else { await views.delete(view as View) diff --git a/packages/server/src/api/routes/tests/screen.spec.ts b/packages/server/src/api/routes/tests/screen.spec.ts index 261c287087..a4c0b23232 100644 --- a/packages/server/src/api/routes/tests/screen.spec.ts +++ b/packages/server/src/api/routes/tests/screen.spec.ts @@ -90,7 +90,7 @@ describe("/screens", () => { async function checkScreens(roleId: string, screenIds: string[]) { await config.loginAsRole(roleId, async () => { const res = await config.api.application.getDefinition( - config.prodAppId! + config.getProdAppId() ) expect(res.screens.length).toEqual(screenIds.length) expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort()) From c09809932d0cd91ef17c073273bd09adad82e11c Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 6 Feb 2025 17:54:24 +0000 Subject: [PATCH 314/321] cleanup. --- .../backend/modals/DeleteDataConfirmationModal.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index e3656328d7..b52d3bce3c 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -86,7 +86,7 @@ async function deleteView(view: ViewV2 | View) { try { - if ("version" in view && helpers.views.isV2(view)) { + if (helpers.views.isV2(view)) { await viewsV2.delete(view as ViewV2) } else { await views.delete(view as View) From 85b2db1f023d49bc066315ee020c01db4ab4f8c9 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 7 Feb 2025 11:52:31 +0000 Subject: [PATCH 315/321] Majority of comments. --- .../backend/modals/DeleteDataConfirmationModal.svelte | 7 ++++--- packages/builder/src/stores/builder/screens.ts | 4 ++-- packages/frontend-core/src/api/screens.ts | 6 +++--- packages/server/src/api/controllers/screen.ts | 10 ++-------- packages/server/src/api/routes/tests/screen.spec.ts | 4 ++-- packages/server/src/tests/utilities/api/screen.ts | 6 +++--- packages/shared-core/src/sdk/documents/screens.ts | 4 ++-- packages/types/src/api/web/app/screen.ts | 3 +-- 8 files changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte index b52d3bce3c..82271bd066 100644 --- a/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte +++ b/packages/builder/src/components/backend/modals/DeleteDataConfirmationModal.svelte @@ -10,7 +10,7 @@ viewsV2, } from "@/stores/builder" import ConfirmDialog from "@/components/common/ConfirmDialog.svelte" - import { helpers } from "@budibase/shared-core" + import { helpers, utils } from "@budibase/shared-core" import { SourceType } from "@budibase/types" import { goto, params } from "@roxi/routify" import { DB_TYPE_EXTERNAL } from "@/constants/backend" @@ -45,7 +45,7 @@ } export const show = async () => { - const usage = await screenStore.usageOfScreens(getSourceID()) + const usage = await screenStore.usageInScreens(getSourceID()) affectedScreens = processScreens(usage.screens) sourceType = usage.sourceType confirmDeleteDialog.show() @@ -93,7 +93,6 @@ } notifications.success("View deleted") } catch (error) { - console.error(error) notifications.error("Error deleting view") } } @@ -140,6 +139,8 @@ return await deleteQuery(source as Query) case SourceType.DATASOURCE: return await deleteDatasource(source as Datasource) + default: + utils.unreachable(sourceType) } } diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index 1f23f9b1d1..b7d9a8be30 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -504,8 +504,8 @@ export class ScreenStore extends BudiStore { /** * Provides a list of screens that are used by a given source ID (table, view, datasource, query) */ - async usageOfScreens(sourceId: string) { - return API.usageOfScreens(sourceId) + async usageInScreens(sourceId: string) { + return API.usageInScreens(sourceId) } } diff --git a/packages/frontend-core/src/api/screens.ts b/packages/frontend-core/src/api/screens.ts index 8147db738d..0bcca9cfd3 100644 --- a/packages/frontend-core/src/api/screens.ts +++ b/packages/frontend-core/src/api/screens.ts @@ -2,14 +2,14 @@ import { DeleteScreenResponse, SaveScreenRequest, SaveScreenResponse, - UsageOfScreensResponse, + UsageInScreensResponse, } from "@budibase/types" import { BaseAPIClient } from "./types" export interface ScreenEndpoints { saveScreen: (screen: SaveScreenRequest) => Promise deleteScreen: (id: string, rev: string) => Promise - usageOfScreens: (sourceId: string) => Promise + usageInScreens: (sourceId: string) => Promise } export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({ @@ -35,7 +35,7 @@ export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({ }) }, - usageOfScreens: async sourceId => { + usageInScreens: async sourceId => { return await API.post({ url: `/api/screens/usage/${sourceId}`, }) diff --git a/packages/server/src/api/controllers/screen.ts b/packages/server/src/api/controllers/screen.ts index ea00e60e1a..456d5729db 100644 --- a/packages/server/src/api/controllers/screen.ts +++ b/packages/server/src/api/controllers/screen.ts @@ -17,7 +17,7 @@ import { ScreenProps, ScreenUsage, SourceType, - UsageOfScreensResponse, + UsageInScreensResponse, UserCtx, } from "@budibase/types" import { builderSocket } from "../../websockets" @@ -138,7 +138,7 @@ function findPlugins(component: ScreenProps, foundPlugins: string[]) { component._children.forEach(child => findPlugins(child, foundPlugins)) } -export async function usage(ctx: UserCtx) { +export async function usage(ctx: UserCtx) { const sourceId = ctx.params.sourceId const sourceType = sdk.common.getSourceType(sourceId) const allScreens = await sdk.screens.fetch() @@ -152,14 +152,8 @@ export async function usage(ctx: UserCtx) { }) } } - const isInternalTable = - sourceType === SourceType.TABLE && - sdk.tables.isInternal({ tableId: sourceId }) - const isInternalView = - sourceType === SourceType.VIEW && sdk.views.isInternal(sourceId) ctx.body = { sourceType, - internal: isInternalTable || isInternalView, screens: response, } } diff --git a/packages/server/src/api/routes/tests/screen.spec.ts b/packages/server/src/api/routes/tests/screen.spec.ts index a4c0b23232..32f4e4b361 100644 --- a/packages/server/src/api/routes/tests/screen.spec.ts +++ b/packages/server/src/api/routes/tests/screen.spec.ts @@ -6,7 +6,7 @@ import { Role, BuiltinPermissionID, SourceType, - UsageOfScreensResponse, + UsageInScreensResponse, } from "@budibase/types" const { @@ -193,7 +193,7 @@ describe("/screens", () => { await config.api.screen.save(basicScreen()) }) - function confirmScreen(usage: UsageOfScreensResponse, screen: Screen) { + function confirmScreen(usage: UsageInScreensResponse, screen: Screen) { expect(usage.screens.length).toEqual(1) expect(usage.screens[0].url).toEqual(screen.routing.route) expect(usage.screens[0]._id).toEqual(screen._id!) diff --git a/packages/server/src/tests/utilities/api/screen.ts b/packages/server/src/tests/utilities/api/screen.ts index bbbe704a29..8d4c0d9ac4 100644 --- a/packages/server/src/tests/utilities/api/screen.ts +++ b/packages/server/src/tests/utilities/api/screen.ts @@ -1,4 +1,4 @@ -import { Screen, UsageOfScreensResponse } from "@budibase/types" +import { Screen, UsageInScreensResponse } from "@budibase/types" import { Expectations, TestAPI } from "./base" export class ScreenAPI extends TestAPI { @@ -32,8 +32,8 @@ export class ScreenAPI extends TestAPI { usage = async ( sourceId: string, expectations?: Expectations - ): Promise => { - return this._post( + ): Promise => { + return this._post( `/api/screens/usage/${sourceId}`, { expectations, diff --git a/packages/shared-core/src/sdk/documents/screens.ts b/packages/shared-core/src/sdk/documents/screens.ts index 3ceffd26a6..218c6673aa 100644 --- a/packages/shared-core/src/sdk/documents/screens.ts +++ b/packages/shared-core/src/sdk/documents/screens.ts @@ -1,8 +1,8 @@ -import { Screen } from "@budibase/types" +import { Screen, Component } from "@budibase/types" export function findInSettings(screen: Screen, toFind: string) { const foundIn: { setting: string; value: string }[] = [] - function recurse(props: Record, parentKey = "") { + function recurse(props: Component, parentKey = "") { for (const [key, value] of Object.entries(props)) { if (!value) { continue diff --git a/packages/types/src/api/web/app/screen.ts b/packages/types/src/api/web/app/screen.ts index 9fc201d651..2ddac2f19a 100644 --- a/packages/types/src/api/web/app/screen.ts +++ b/packages/types/src/api/web/app/screen.ts @@ -21,8 +21,7 @@ export interface ScreenUsage { _id: string } -export interface UsageOfScreensResponse { +export interface UsageInScreensResponse { sourceType: SourceType - internal: boolean screens: ScreenUsage[] } From 126befde3ea1d40245eab697e0156211c31088ef Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 7 Feb 2025 12:02:58 +0000 Subject: [PATCH 316/321] Updating UIEvent to an interface. --- packages/types/src/ui/common.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/types/src/ui/common.ts b/packages/types/src/ui/common.ts index 2f5d864ca0..0f5a2590b9 100644 --- a/packages/types/src/ui/common.ts +++ b/packages/types/src/ui/common.ts @@ -1,3 +1,5 @@ -export type UIEvent = Event & { +export interface UIEvent extends Omit { currentTarget: EventTarget & HTMLInputElement -} & { key?: string } & { target?: any } + key?: string + target?: any +} From 6435c9b529bb6411734a6338ddec8488e9db7a0b Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 7 Feb 2025 12:05:45 +0000 Subject: [PATCH 317/321] Linting. --- packages/server/src/api/controllers/screen.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/api/controllers/screen.ts b/packages/server/src/api/controllers/screen.ts index 456d5729db..038d87d548 100644 --- a/packages/server/src/api/controllers/screen.ts +++ b/packages/server/src/api/controllers/screen.ts @@ -16,7 +16,6 @@ import { Screen, ScreenProps, ScreenUsage, - SourceType, UsageInScreensResponse, UserCtx, } from "@budibase/types" From 5df197a36ea0c7bac123550e7b99d9e46f15535f Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 7 Feb 2025 13:49:34 +0000 Subject: [PATCH 318/321] Disabling cell formatting, it is currently incompatiable with cell/row conditions in grids and to avoid breaking those for now we are removing the capability until both can be a aligned to use the same mechanism of binding evaluation. --- packages/client/manifest.json | 9 +-------- packages/client/src/components/app/GridBlock.svelte | 2 +- .../src/components/grid/stores/conditions.ts | 1 + 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 4b13713984..c236dd1ad9 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3089,12 +3089,6 @@ "type": "tableConditions", "label": "Conditions", "key": "conditions" - }, - { - "type": "text", - "label": "Format", - "key": "format", - "info": "Changing format will display values as text" } ] }, @@ -7691,8 +7685,7 @@ { "type": "columns/grid", "key": "columns", - "resetOn": "table", - "nested": true + "resetOn": "table" } ] }, diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 1f4792ea8a..52040b8e58 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -105,7 +105,7 @@ order: idx, conditions: column.conditions, visible: !!column.active, - format: createFormatter(column), + // format: createFormatter(column), } if (column.width) { overrides[column.field].width = column.width diff --git a/packages/frontend-core/src/components/grid/stores/conditions.ts b/packages/frontend-core/src/components/grid/stores/conditions.ts index cf47456b44..37f8f3432a 100644 --- a/packages/frontend-core/src/components/grid/stores/conditions.ts +++ b/packages/frontend-core/src/components/grid/stores/conditions.ts @@ -173,5 +173,6 @@ const evaluateConditions = (row: UIRow, conditions: UICondition[]) => { // Swallow } } + console.log(metadata) return metadata } From 73345d8c05ed34cc1daf4958192f8e39f9c416d5 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 7 Feb 2025 13:53:30 +0000 Subject: [PATCH 319/321] Linting. --- packages/client/src/components/app/GridBlock.svelte | 12 ++++++------ .../src/components/grid/stores/conditions.ts | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 52040b8e58..7f4990ace0 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -114,12 +114,12 @@ return overrides } - const createFormatter = column => { - if (typeof column.format !== "string" || !column.format.trim().length) { - return null - } - return row => processStringSync(column.format, { [id]: row }) - } + // const createFormatter = column => { + // if (typeof column.format !== "string" || !column.format.trim().length) { + // return null + // } + // return row => processStringSync(column.format, { [id]: row }) + // } const enrichButtons = buttons => { if (!buttons?.length) { diff --git a/packages/frontend-core/src/components/grid/stores/conditions.ts b/packages/frontend-core/src/components/grid/stores/conditions.ts index 37f8f3432a..cf47456b44 100644 --- a/packages/frontend-core/src/components/grid/stores/conditions.ts +++ b/packages/frontend-core/src/components/grid/stores/conditions.ts @@ -173,6 +173,5 @@ const evaluateConditions = (row: UIRow, conditions: UICondition[]) => { // Swallow } } - console.log(metadata) return metadata } From e9e1eb7a2db04c34b72cc88ca03f7b64447fe9ee Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 7 Feb 2025 13:56:34 +0000 Subject: [PATCH 320/321] More linting. --- packages/client/src/components/app/GridBlock.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 7f4990ace0..b75ca46223 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -5,7 +5,7 @@ import { get, derived, readable } from "svelte/store" import { featuresStore } from "stores" import { Grid } from "@budibase/frontend-core" - import { processStringSync } from "@budibase/string-templates" + // import { processStringSync } from "@budibase/string-templates" // table is actually any datasource, but called table for legacy compatibility export let table From ad063b7de6c92664fd22af20cfbdb7d358358cf1 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 7 Feb 2025 14:10:38 +0000 Subject: [PATCH 321/321] Bump version to 3.4.4 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index bdb933c0c5..730d145ced 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.4.3", + "version": "3.4.4", "npmClient": "yarn", "concurrency": 20, "command": {