From a2d36410d60105f2cb7aa05d0117c2844a35c7c5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 17 Feb 2025 14:44:39 +0100 Subject: [PATCH 01/16] Convert file --- packages/builder/src/helpers/{components.js => components.ts} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename packages/builder/src/helpers/{components.js => components.ts} (98%) diff --git a/packages/builder/src/helpers/components.js b/packages/builder/src/helpers/components.ts similarity index 98% rename from packages/builder/src/helpers/components.js rename to packages/builder/src/helpers/components.ts index b1116d7750..f03551076e 100644 --- a/packages/builder/src/helpers/components.js +++ b/packages/builder/src/helpers/components.ts @@ -8,13 +8,14 @@ import { } from "@budibase/string-templates" import { capitalise } from "@/helpers" import { Constants } from "@budibase/frontend-core" +import { ScreenProps } from "@budibase/types" const { ContextScopes } = Constants /** * Recursively searches for a specific component ID */ -export const findComponent = (rootComponent, id) => { +export const findComponent = (rootComponent: ScreenProps, id: string) => { return searchComponentTree(rootComponent, comp => comp._id === id) } From b931a24d5ee8b4799f7067ef394e8668e60c2ae0 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 17 Feb 2025 17:21:23 +0100 Subject: [PATCH 02/16] Types --- packages/builder/src/helpers/components.ts | 77 +++++++++++++--------- packages/frontend-core/src/constants.ts | 5 +- packages/types/src/ui/components/index.ts | 15 +++++ packages/types/src/ui/stores/preview.ts | 1 - 4 files changed, 63 insertions(+), 35 deletions(-) diff --git a/packages/builder/src/helpers/components.ts b/packages/builder/src/helpers/components.ts index f03551076e..916e518b3b 100644 --- a/packages/builder/src/helpers/components.ts +++ b/packages/builder/src/helpers/components.ts @@ -8,28 +8,32 @@ import { } from "@budibase/string-templates" import { capitalise } from "@/helpers" import { Constants } from "@budibase/frontend-core" -import { ScreenProps } from "@budibase/types" +import { Component, ComponentContext } from "@budibase/types" const { ContextScopes } = Constants /** * Recursively searches for a specific component ID */ -export const findComponent = (rootComponent: ScreenProps, id: string) => { +export const findComponent = (rootComponent: Component, id: string) => { return searchComponentTree(rootComponent, comp => comp._id === id) } /** * Recursively searches for a specific component type */ -export const findComponentType = (rootComponent, type) => { +export const findComponentType = (rootComponent: Component, type: string) => { return searchComponentTree(rootComponent, comp => comp._component === type) } /** * Recursively searches for the parent component of a specific component ID */ -export const findComponentParent = (rootComponent, id, parentComponent) => { +export const findComponentParent = ( + rootComponent: Component, + id: string, + parentComponent: Component +): Component | null => { if (!rootComponent || !id) { return null } @@ -52,7 +56,11 @@ export const findComponentParent = (rootComponent, id, parentComponent) => { * Recursively searches for a specific component ID and records the component * path to this component */ -export const findComponentPath = (rootComponent, id, path = []) => { +export const findComponentPath = ( + rootComponent: Component, + id: string, + path: Component[] = [] +): Component[] => { if (!rootComponent || !id) { return [] } @@ -76,11 +84,14 @@ export const findComponentPath = (rootComponent, id, path = []) => { * Recurses through the component tree and finds all components which match * a certain selector */ -export const findAllMatchingComponents = (rootComponent, selector) => { +export const findAllMatchingComponents = ( + rootComponent: Component, + selector: (component: Component) => boolean +) => { if (!rootComponent || !selector) { return [] } - let components = [] + let components: Component[] = [] if (rootComponent._children) { rootComponent._children.forEach(child => { components = [ @@ -98,7 +109,7 @@ export const findAllMatchingComponents = (rootComponent, selector) => { /** * Recurses through the component tree and finds all components. */ -export const findAllComponents = rootComponent => { +export const findAllComponents = (rootComponent: Component) => { return findAllMatchingComponents(rootComponent, () => true) } @@ -106,9 +117,9 @@ export const findAllComponents = rootComponent => { * Finds the closest parent component which matches certain criteria */ export const findClosestMatchingComponent = ( - rootComponent, - componentId, - selector + rootComponent: Component, + componentId: string, + selector: (component: Component) => boolean ) => { if (!selector) { return null @@ -126,7 +137,10 @@ export const findClosestMatchingComponent = ( * Recurses through a component tree evaluating a matching function against * components until a match is found */ -const searchComponentTree = (rootComponent, matchComponent) => { +const searchComponentTree = ( + rootComponent: Component, + matchComponent: (component: Component) => boolean +): Component | null => { if (!rootComponent || !matchComponent) { return null } @@ -151,15 +165,18 @@ const searchComponentTree = (rootComponent, matchComponent) => { * This mutates the object in place. * @param component the component to randomise */ -export const makeComponentUnique = component => { +export const makeComponentUnique = (component: Component) => { if (!component) { return } // Generate a full set of component ID replacements in this tree - const idReplacements = [] - const generateIdReplacements = (component, replacements) => { - const oldId = component._id + const idReplacements: [string, string][] = [] + const generateIdReplacements = ( + component: Component, + replacements: [string, string][] + ) => { + const oldId = component._id! const newId = Helpers.uuid() replacements.push([oldId, newId]) component._children?.forEach(x => generateIdReplacements(x, replacements)) @@ -183,9 +200,9 @@ export const makeComponentUnique = component => { let js = decodeJSBinding(sanitizedBinding) if (js != null) { // Replace ID inside JS binding - idReplacements.forEach(([oldId, newId]) => { + for (const [oldId, newId] of idReplacements) { js = js.replace(new RegExp(oldId, "g"), newId) - }) + } // Create new valid JS binding let newBinding = encodeJSBinding(js) @@ -205,7 +222,7 @@ export const makeComponentUnique = component => { return JSON.parse(definition) } -export const getComponentText = component => { +export const getComponentText = (component: Component) => { if (component == null) { return "" } @@ -219,7 +236,7 @@ export const getComponentText = component => { return capitalise(type) } -export const getComponentName = component => { +export const getComponentName = (component: Component) => { if (component == null) { return "" } @@ -230,9 +247,9 @@ export const getComponentName = component => { } // Gets all contexts exposed by a certain component type, including actions -export const getComponentContexts = component => { +export const getComponentContexts = (component: string) => { const def = componentStore.getDefinition(component) - let contexts = [] + let contexts: ComponentContext[] = [] if (def?.context) { contexts = Array.isArray(def.context) ? [...def.context] : [def.context] } @@ -252,9 +269,9 @@ export const getComponentContexts = component => { * Recurses through the component tree and builds a tree of contexts provided * by components. */ -export const buildContextTree = ( - rootComponent, - tree = { root: [] }, +const buildContextTree = ( + rootComponent: Component, + tree: Record = { root: [] }, currentBranch = "root" ) => { // Sanity check @@ -265,12 +282,12 @@ export const buildContextTree = ( // Process this component's contexts const contexts = getComponentContexts(rootComponent._component) if (contexts.length) { - tree[currentBranch].push(rootComponent._id) + tree[currentBranch].push(rootComponent._id!) // If we provide local context, start a new branch for our children if (contexts.some(context => context.scope === ContextScopes.Local)) { - currentBranch = rootComponent._id - tree[rootComponent._id] = [] + currentBranch = rootComponent._id! + tree[rootComponent._id!] = [] } } @@ -300,9 +317,9 @@ export const buildContextTreeLookupMap = rootComponent => { } // Get a flat list of ids for all descendants of a component -export const getChildIdsForComponent = component => { +export const getChildIdsForComponent = (component: Component): string[] => { return [ - component._id, + component._id!, ...(component?._children ?? []).map(getChildIdsForComponent).flat(1), ] } diff --git a/packages/frontend-core/src/constants.ts b/packages/frontend-core/src/constants.ts index 907d91825f..28210117ba 100644 --- a/packages/frontend-core/src/constants.ts +++ b/packages/frontend-core/src/constants.ts @@ -112,10 +112,7 @@ export const EventPublishType = { ENV_VAR_UPGRADE_PANEL_OPENED: "environment_variable_upgrade_panel_opened", } -export const ContextScopes = { - Local: "local", - Global: "global", -} +export { ComponentContextScopes } from "@budibase/types" export const TypeIconMap = { [FieldType.STRING]: "Text", diff --git a/packages/types/src/ui/components/index.ts b/packages/types/src/ui/components/index.ts index f477ed2bd3..4be203151e 100644 --- a/packages/types/src/ui/components/index.ts +++ b/packages/types/src/ui/components/index.ts @@ -28,6 +28,8 @@ export interface ComponentDefinition { width: number height: number } + context?: ComponentContext | ComponentContext[] + actions?: (string | any)[] } export type DependsOnComponentSetting = @@ -56,3 +58,16 @@ export interface ComponentSetting { self: boolean } } + +export interface ComponentContext { + type: ComponentContextType + scope: ComponentContextScopes + actions: any[] +} + +export type ComponentContextType = "action" + +export const enum ComponentContextScopes { + Local = "local", + Global = "global", +} diff --git a/packages/types/src/ui/stores/preview.ts b/packages/types/src/ui/stores/preview.ts index d9f5f2ac46..4d09366ff5 100644 --- a/packages/types/src/ui/stores/preview.ts +++ b/packages/types/src/ui/stores/preview.ts @@ -1,2 +1 @@ export type PreviewDevice = "desktop" | "tablet" | "mobile" -export type ComponentContext = Record From fbd020ec31602141d788c7edee6f963834548a5d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 17 Feb 2025 17:22:50 +0100 Subject: [PATCH 03/16] Fixes --- packages/builder/src/helpers/components.ts | 4 ++-- packages/frontend-core/src/constants.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/builder/src/helpers/components.ts b/packages/builder/src/helpers/components.ts index 916e518b3b..550b49c34e 100644 --- a/packages/builder/src/helpers/components.ts +++ b/packages/builder/src/helpers/components.ts @@ -305,9 +305,9 @@ const buildContextTree = ( * Generates a lookup map of which context branch all components in a component * tree are inside. */ -export const buildContextTreeLookupMap = rootComponent => { +export const buildContextTreeLookupMap = (rootComponent: Component) => { const tree = buildContextTree(rootComponent) - let map = {} + const map: Record = {} Object.entries(tree).forEach(([branch, ids]) => { ids.forEach(id => { map[id] = branch diff --git a/packages/frontend-core/src/constants.ts b/packages/frontend-core/src/constants.ts index 28210117ba..3e51719bfd 100644 --- a/packages/frontend-core/src/constants.ts +++ b/packages/frontend-core/src/constants.ts @@ -112,7 +112,7 @@ export const EventPublishType = { ENV_VAR_UPGRADE_PANEL_OPENED: "environment_variable_upgrade_panel_opened", } -export { ComponentContextScopes } from "@budibase/types" +export { ComponentContextScopes as ContextScopes } from "@budibase/types" export const TypeIconMap = { [FieldType.STRING]: "Text", From 47b7408022fa60b4ea0f0e61dca837013c60fd13 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 17 Feb 2025 18:31:34 +0100 Subject: [PATCH 04/16] Add extra context --- packages/types/src/ui/components/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/ui/components/index.ts b/packages/types/src/ui/components/index.ts index 4be203151e..60e22f6539 100644 --- a/packages/types/src/ui/components/index.ts +++ b/packages/types/src/ui/components/index.ts @@ -65,7 +65,7 @@ export interface ComponentContext { actions: any[] } -export type ComponentContextType = "action" +export type ComponentContextType = "context" | "action" export const enum ComponentContextScopes { Local = "local", From dbb783c13fc84faffaf9930dbb292f49bd0ee5b1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 17 Feb 2025 18:48:40 +0100 Subject: [PATCH 05/16] Fix types --- packages/builder/src/helpers/components.ts | 10 +- .../src/stores/builder/componentTreeNodes.ts | 2 +- .../builder/src/stores/builder/components.ts | 117 +++++++++--------- .../builder/src/stores/builder/screens.ts | 2 +- 4 files changed, 65 insertions(+), 66 deletions(-) diff --git a/packages/builder/src/helpers/components.ts b/packages/builder/src/helpers/components.ts index 550b49c34e..be14866f7a 100644 --- a/packages/builder/src/helpers/components.ts +++ b/packages/builder/src/helpers/components.ts @@ -30,9 +30,9 @@ export const findComponentType = (rootComponent: Component, type: string) => { * Recursively searches for the parent component of a specific component ID */ export const findComponentParent = ( - rootComponent: Component, - id: string, - parentComponent: Component + rootComponent: Component | undefined, + id: string | undefined, + parentComponent: Component | null = null ): Component | null => { if (!rootComponent || !id) { return null @@ -58,7 +58,7 @@ export const findComponentParent = ( */ export const findComponentPath = ( rootComponent: Component, - id: string, + id: string | undefined, path: Component[] = [] ): Component[] => { if (!rootComponent || !id) { @@ -118,7 +118,7 @@ export const findAllComponents = (rootComponent: Component) => { */ export const findClosestMatchingComponent = ( rootComponent: Component, - componentId: string, + componentId: string | undefined, selector: (component: Component) => boolean ) => { if (!selector) { diff --git a/packages/builder/src/stores/builder/componentTreeNodes.ts b/packages/builder/src/stores/builder/componentTreeNodes.ts index b88563de6a..0e4dd5738b 100644 --- a/packages/builder/src/stores/builder/componentTreeNodes.ts +++ b/packages/builder/src/stores/builder/componentTreeNodes.ts @@ -58,7 +58,7 @@ export class ComponentTreeNodesStore extends BudiStore { const path = findComponentPath(selectedScreen.props, componentId) - const componentIds = path.map((component: Component) => component._id) + const componentIds = path.map((component: Component) => component._id!) this.update((openNodes: OpenNodesState) => { const newNodes = Object.fromEntries( diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index 38fa9a6a41..f1bd1e77c4 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -1,3 +1,5 @@ +// TODO: analise and fix all the undefined ! and ? + import { get, derived } from "svelte/store" import { cloneDeep } from "lodash/fp" import { API } from "@/api" @@ -36,7 +38,7 @@ import { Utils } from "@budibase/frontend-core" import { ComponentDefinition, ComponentSetting, - Component as ComponentType, + Component, ComponentCondition, FieldType, Screen, @@ -45,10 +47,6 @@ import { import { utils } from "@budibase/shared-core" import { getSequentialName } from "@/helpers/duplicate" -interface Component extends ComponentType { - _id: string -} - export interface ComponentState { components: Record customComponents: string[] @@ -182,7 +180,7 @@ export class ComponentStore extends BudiStore { * Takes an enriched component instance and applies any required migration * logic */ - migrateSettings(enrichedComponent: Component) { + migrateSettings(enrichedComponent: Component | null) { const componentPrefix = "@budibase/standard-components" let migrated = false @@ -487,7 +485,7 @@ export class ComponentStore extends BudiStore { (component: Component) => component._component.endsWith("/form") ) const formSteps = findAllMatchingComponents( - parentForm, + parentForm!, (component: Component) => component._component.endsWith("/formstep") ) extras.step = formSteps.length + 1 @@ -519,7 +517,7 @@ export class ComponentStore extends BudiStore { // Insert in position if specified if (parent && index != null) { await screenStore.patch((screen: Screen) => { - let parentComponent = findComponent(screen.props, parent) + let parentComponent = findComponent(screen.props, parent._id!)! if (!parentComponent._children?.length) { parentComponent._children = [componentInstance] } else { @@ -538,7 +536,7 @@ export class ComponentStore extends BudiStore { } const currentComponent = findComponent( screen.props, - selectedComponentId + selectedComponentId! ) if (!currentComponent) { return false @@ -581,7 +579,7 @@ export class ComponentStore extends BudiStore { return state }) - componentTreeNodesStore.makeNodeVisible(componentInstance._id) + componentTreeNodesStore.makeNodeVisible(componentInstance._id!) // Log event analytics.captureEvent(Events.COMPONENT_CREATED, { @@ -633,7 +631,7 @@ export class ComponentStore extends BudiStore { // Determine the next component to select, and select it before deletion // to avoid an intermediate state of no component selection const state = get(this.store) - let nextId = "" + let nextId: string | null = "" if (state.selectedComponentId === component._id) { nextId = this.getNext() if (!nextId) { @@ -646,7 +644,7 @@ export class ComponentStore extends BudiStore { nextId = nextId.replace("-navigation", "-screen") } this.update(state => { - state.selectedComponentId = nextId + state.selectedComponentId = nextId! return state }) } @@ -654,18 +652,18 @@ export class ComponentStore extends BudiStore { // Patch screen await screenStore.patch((screen: Screen) => { // Check component exists - component = findComponent(screen.props, component._id) - if (!component) { + const updatedComponent = findComponent(screen.props, component._id!) + if (!updatedComponent) { return false } // Check component has a valid parent - const parent = findComponentParent(screen.props, component._id) + const parent = findComponentParent(screen.props, updatedComponent._id) if (!parent) { return false } - parent._children = parent._children.filter( - (child: Component) => child._id !== component._id + parent._children = parent._children!.filter( + (child: Component) => child._id !== updatedComponent._id ) }, null) } @@ -706,7 +704,7 @@ export class ComponentStore extends BudiStore { } async paste( - targetComponent: Component, + targetComponent: Component | null, mode: string, targetScreen: Screen, selectComponent = true @@ -729,7 +727,7 @@ export class ComponentStore extends BudiStore { // Patch screen const patch = (screen: Screen) => { // Get up to date ref to target - targetComponent = findComponent(screen.props, targetComponent._id) + targetComponent = findComponent(screen.props, targetComponent!._id!) if (!targetComponent) { return false } @@ -743,7 +741,7 @@ export class ComponentStore extends BudiStore { if (!cut) { componentToPaste = makeComponentUnique(componentToPaste) } - newComponentId = componentToPaste._id + newComponentId = componentToPaste._id! // Strip grid position metadata if pasting into a new screen, but keep // alignment metadata @@ -791,7 +789,7 @@ export class ComponentStore extends BudiStore { } const targetIndex = parent._children.findIndex( (component: Component) => { - return component._id === targetComponent._id + return component._id === targetComponent!._id } ) const index = mode === "above" ? targetIndex : targetIndex + 1 @@ -820,8 +818,8 @@ export class ComponentStore extends BudiStore { if (!screen) { throw "A valid screen must be selected" } - const parent = findComponentParent(screen.props, componentId) - const index = parent?._children.findIndex( + const parent = findComponentParent(screen.props, componentId)! + const index = parent?._children?.findIndex( (x: Component) => x._id === componentId ) @@ -839,29 +837,29 @@ export class ComponentStore extends BudiStore { } // If we have siblings above us, choose the sibling or a descendant - if (index > 0) { + if (index !== undefined && index > 0) { // If sibling before us accepts children, and is not collapsed, select a descendant - const previousSibling = parent._children[index - 1] + const previousSibling = parent._children![index - 1] if ( previousSibling._children?.length && - componentTreeNodesStore.isNodeExpanded(previousSibling._id) + componentTreeNodesStore.isNodeExpanded(previousSibling._id!) ) { let target = previousSibling while ( target._children?.length && - componentTreeNodesStore.isNodeExpanded(target._id) + componentTreeNodesStore.isNodeExpanded(target._id!) ) { target = target._children[target._children.length - 1] } - return target._id + return target._id! } // Otherwise just select sibling - return previousSibling._id + return previousSibling._id! } // If no siblings above us, select the parent - return parent._id + return parent._id! } getNext() { @@ -873,9 +871,9 @@ export class ComponentStore extends BudiStore { throw "A valid screen must be selected" } const parent = findComponentParent(screen.props, componentId) - const index = parent?._children.findIndex( + const index = parent?._children?.findIndex( (x: Component) => x._id === componentId - ) + )! // Check for screen and navigation component edge cases const screenComponentId = `${screen._id}-screen` @@ -888,37 +886,38 @@ export class ComponentStore extends BudiStore { if ( component?._children?.length && (state.selectedComponentId === navComponentId || - componentTreeNodesStore.isNodeExpanded(component._id)) + componentTreeNodesStore.isNodeExpanded(component._id!)) ) { - return component._children[0]._id + return component._children[0]._id! } else if (!parent) { return null } // Otherwise select the next sibling if we have one - if (index < parent._children.length - 1) { - const nextSibling = parent._children[index + 1] - return nextSibling._id + if (index < parent._children!.length - 1) { + const nextSibling = parent._children![index + 1] + return nextSibling._id! } // Last child, select our parents next sibling let target = parent let targetParent = findComponentParent(screen.props, target._id) - let targetIndex = targetParent?._children.findIndex( + let targetIndex = targetParent?._children?.findIndex( (child: Component) => child._id === target._id - ) + )! while ( targetParent != null && - targetIndex === targetParent._children?.length - 1 + targetParent._children && + targetIndex === targetParent._children.length - 1 ) { target = targetParent targetParent = findComponentParent(screen.props, target._id) - targetIndex = targetParent?._children.findIndex( + targetIndex = targetParent?._children!.findIndex( (child: Component) => child._id === target._id - ) + )! } if (targetParent) { - return targetParent._children[targetIndex + 1]._id + return targetParent._children![targetIndex + 1]._id! } else { return null } @@ -950,16 +949,16 @@ export class ComponentStore extends BudiStore { const parent = findComponentParent(screen.props, componentId) // Check we aren't right at the top of the tree - const index = parent?._children.findIndex( + const index = parent?._children?.findIndex( (x: Component) => x._id === componentId - ) + )! if (!parent || (index === 0 && parent._id === screen.props._id)) { return } // Copy original component and remove it from the parent - const originalComponent = cloneDeep(parent._children[index]) - parent._children = parent._children.filter( + const originalComponent = cloneDeep(parent._children![index]) + parent._children = parent._children!.filter( (component: Component) => component._id !== componentId ) @@ -971,9 +970,9 @@ export class ComponentStore extends BudiStore { const definition = this.getDefinition(previousSibling._component) if ( definition?.hasChildren && - componentTreeNodesStore.isNodeExpanded(previousSibling._id) + componentTreeNodesStore.isNodeExpanded(previousSibling._id!) ) { - previousSibling._children.push(originalComponent) + previousSibling._children!.push(originalComponent) } // Otherwise just move component above sibling @@ -985,11 +984,11 @@ export class ComponentStore extends BudiStore { // If no siblings above us, go above the parent as long as it isn't // the screen else if (parent._id !== screen.props._id) { - const grandParent = findComponentParent(screen.props, parent._id) - const parentIndex = grandParent._children.findIndex( + const grandParent = findComponentParent(screen.props, parent._id)! + const parentIndex = grandParent._children!.findIndex( (child: Component) => child._id === parent._id ) - grandParent._children.splice(parentIndex, 0, originalComponent) + grandParent._children!.splice(parentIndex, 0, originalComponent) } }, null) } @@ -1028,9 +1027,9 @@ export class ComponentStore extends BudiStore { const definition = this.getDefinition(nextSibling._component) if ( definition?.hasChildren && - componentTreeNodesStore.isNodeExpanded(nextSibling._id) + componentTreeNodesStore.isNodeExpanded(nextSibling._id!) ) { - nextSibling._children.splice(0, 0, originalComponent) + nextSibling._children!.splice(0, 0, originalComponent) } // Otherwise move below next sibling @@ -1041,11 +1040,11 @@ export class ComponentStore extends BudiStore { // Last child, so move below our parent else { - const grandParent = findComponentParent(screen.props, parent._id) - const parentIndex = grandParent._children.findIndex( + const grandParent = findComponentParent(screen.props, parent._id)! + const parentIndex = grandParent._children!.findIndex( (child: Component) => child._id === parent._id ) - grandParent._children.splice(parentIndex + 1, 0, originalComponent) + grandParent._children!.splice(parentIndex + 1, 0, originalComponent) } }, null) } @@ -1208,13 +1207,13 @@ export class ComponentStore extends BudiStore { } // Replace component with parent - const index = oldParentDefinition._children.findIndex( + const index = oldParentDefinition._children!.findIndex( (component: Component) => component._id === componentId ) if (index === -1) { return false } - oldParentDefinition._children[index] = { + oldParentDefinition._children![index] = { ...newParentDefinition, _children: [definition], } diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index b7d9a8be30..51072adbb8 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -490,7 +490,7 @@ export class ScreenStore extends BudiStore { // Flatten the recursive component tree const components = findAllMatchingComponents( screen.props, - (x: Component) => x + (x: Component) => !!x ) // Iterate over all components and run checks From 8018f5f25be1dc5d11eeb7f86148a63a2b18c4c2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 17 Feb 2025 19:32:16 +0100 Subject: [PATCH 06/16] Fix types --- packages/types/src/ui/components/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/types/src/ui/components/index.ts b/packages/types/src/ui/components/index.ts index 60e22f6539..455763abda 100644 --- a/packages/types/src/ui/components/index.ts +++ b/packages/types/src/ui/components/index.ts @@ -63,6 +63,9 @@ export interface ComponentContext { type: ComponentContextType scope: ComponentContextScopes actions: any[] + url?: Record + query?: string + state?: Record } export type ComponentContextType = "context" | "action" From 69e1ae0aec72ddb26fccf66d834f38a59dae7446 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 17 Feb 2025 19:48:15 +0100 Subject: [PATCH 07/16] Cleanups --- packages/builder/src/helpers/components.ts | 2 +- packages/builder/src/stores/builder/components.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/helpers/components.ts b/packages/builder/src/helpers/components.ts index be14866f7a..074f74f06d 100644 --- a/packages/builder/src/helpers/components.ts +++ b/packages/builder/src/helpers/components.ts @@ -85,7 +85,7 @@ export const findComponentPath = ( * a certain selector */ export const findAllMatchingComponents = ( - rootComponent: Component, + rootComponent: Component | null, selector: (component: Component) => boolean ) => { if (!rootComponent || !selector) { diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index f1bd1e77c4..af3716e207 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -485,7 +485,7 @@ export class ComponentStore extends BudiStore { (component: Component) => component._component.endsWith("/form") ) const formSteps = findAllMatchingComponents( - parentForm!, + parentForm, (component: Component) => component._component.endsWith("/formstep") ) extras.step = formSteps.length + 1 @@ -644,7 +644,7 @@ export class ComponentStore extends BudiStore { nextId = nextId.replace("-navigation", "-screen") } this.update(state => { - state.selectedComponentId = nextId! + state.selectedComponentId = nextId ?? undefined return state }) } @@ -704,7 +704,7 @@ export class ComponentStore extends BudiStore { } async paste( - targetComponent: Component | null, + targetComponent: Component, mode: string, targetScreen: Screen, selectComponent = true @@ -727,7 +727,7 @@ export class ComponentStore extends BudiStore { // Patch screen const patch = (screen: Screen) => { // Get up to date ref to target - targetComponent = findComponent(screen.props, targetComponent!._id!) + targetComponent = findComponent(screen.props, targetComponent!._id!)! if (!targetComponent) { return false } @@ -789,7 +789,7 @@ export class ComponentStore extends BudiStore { } const targetIndex = parent._children.findIndex( (component: Component) => { - return component._id === targetComponent!._id + return component._id === targetComponent._id } ) const index = mode === "above" ? targetIndex : targetIndex + 1 From a5709c9de5675cb87b48d29d8a3ad304e718a48e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 18 Feb 2025 10:48:16 +0000 Subject: [PATCH 08/16] Test cron removal. --- .../backend-core/src/queue/inMemoryQueue.ts | 47 ++++++++++---- packages/backend-core/src/queue/index.ts | 1 + .../server/src/automations/logging/index.ts | 13 +++- .../automations/tests/triggers/cron.spec.ts | 53 ++++++++------- .../src/automations/tests/utilities/index.ts | 53 +++++++++++++-- packages/server/src/threads/automation.ts | 64 ++++++++----------- 6 files changed, 151 insertions(+), 80 deletions(-) diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts index 45d88a7a38..9a6375e485 100644 --- a/packages/backend-core/src/queue/inMemoryQueue.ts +++ b/packages/backend-core/src/queue/inMemoryQueue.ts @@ -3,6 +3,7 @@ import { newid } from "../utils" import { Queue, QueueOptions, JobOptions } from "./queue" import { helpers } from "@budibase/shared-core" import { Job, JobId, JobInformation } from "bull" +import { cloneDeep } from "lodash" function jobToJobInformation(job: Job): JobInformation { let cron = "" @@ -33,7 +34,7 @@ function jobToJobInformation(job: Job): JobInformation { } } -interface JobMessage extends Partial> { +export interface TestQueueMessage extends Partial> { id: string timestamp: number queue: Queue @@ -47,15 +48,15 @@ interface JobMessage extends Partial> { * internally to register when messages are available to the consumers - in can * support many inputs and many consumers. */ -class InMemoryQueue implements Partial { +export class InMemoryQueue implements Partial> { _name: string _opts?: QueueOptions - _messages: JobMessage[] + _messages: TestQueueMessage[] _queuedJobIds: Set _emitter: NodeJS.EventEmitter<{ - message: [JobMessage] - completed: [Job] - removed: [JobMessage] + message: [TestQueueMessage] + completed: [Job] + removed: [TestQueueMessage] }> _runCount: number _addCount: number @@ -86,10 +87,13 @@ class InMemoryQueue implements Partial { */ async process(concurrencyOrFunc: number | any, func?: any) { func = typeof concurrencyOrFunc === "number" ? func : concurrencyOrFunc - this._emitter.on("message", async message => { + this._emitter.on("message", async msg => { + const message = cloneDeep(msg) + + const isManualTrigger = (message as any).manualTrigger === true // For the purpose of testing, don't trigger cron jobs immediately. // Require the test to trigger them manually with timestamps. - if (message.opts?.repeat != null) { + if (!isManualTrigger && message.opts?.repeat != null) { return } @@ -107,7 +111,7 @@ class InMemoryQueue implements Partial { if (resp.then != null) { try { await retryFunc(resp) - this._emitter.emit("completed", message as Job) + this._emitter.emit("completed", message as Job) } catch (e: any) { console.error(e) } @@ -124,7 +128,6 @@ class InMemoryQueue implements Partial { return this as any } - // simply puts a message to the queue and emits to the queue for processing /** * Simple function to replicate the add message functionality of Bull, putting * a new message on the queue. This then emits an event which will be used to @@ -133,7 +136,14 @@ class InMemoryQueue implements Partial { * a JSON message as this is required by Bull. * @param repeat serves no purpose for the import queue. */ - async add(data: any, opts?: JobOptions) { + // add(name: string, data: T, opts?: JobOptions): Promise>; + async add(data: T | string, optsOrT?: JobOptions | T) { + if (typeof data === "string") { + throw new Error("doesn't support named jobs") + } + + const opts = optsOrT as JobOptions + const jobId = opts?.jobId?.toString() if (jobId && this._queuedJobIds.has(jobId)) { console.log(`Ignoring already queued job ${jobId}`) @@ -148,7 +158,7 @@ class InMemoryQueue implements Partial { } const pushMessage = () => { - const message: JobMessage = { + const message: TestQueueMessage = { id: newid(), timestamp: Date.now(), queue: this as unknown as Queue, @@ -176,7 +186,7 @@ class InMemoryQueue implements Partial { async removeRepeatableByKey(id: string) { for (const [idx, message] of this._messages.entries()) { - if (message.opts?.jobId?.toString() === id) { + if (message.id === id) { this._messages.splice(idx, 1) this._emitter.emit("removed", message) return @@ -204,6 +214,17 @@ class InMemoryQueue implements Partial { return null } + manualTrigger(id: JobId) { + for (const message of this._messages) { + if (message.id === id) { + const forceMessage = { ...message, manualTrigger: true } + this._emitter.emit("message", forceMessage) + return + } + } + throw new Error(`Job with id ${id} not found`) + } + on(event: string, callback: (...args: any[]) => void): Queue { // @ts-expect-error - this callback can be one of many types this._emitter.on(event, callback) diff --git a/packages/backend-core/src/queue/index.ts b/packages/backend-core/src/queue/index.ts index b7d565ba13..5603c40513 100644 --- a/packages/backend-core/src/queue/index.ts +++ b/packages/backend-core/src/queue/index.ts @@ -1,2 +1,3 @@ export * from "./queue" export * from "./constants" +export * from "./inMemoryQueue" diff --git a/packages/server/src/automations/logging/index.ts b/packages/server/src/automations/logging/index.ts index 9d16f15a67..ed8dd9db88 100644 --- a/packages/server/src/automations/logging/index.ts +++ b/packages/server/src/automations/logging/index.ts @@ -1,7 +1,7 @@ import env from "../../environment" import { AutomationResults, Automation, App } from "@budibase/types" import { automations } from "@budibase/pro" -import { db as dbUtils } from "@budibase/backend-core" +import { db as dbUtils, logging } from "@budibase/backend-core" import sizeof from "object-sizeof" const MAX_LOG_SIZE_MB = 5 @@ -32,7 +32,16 @@ export async function storeLog( if (bytes / MB_IN_BYTES > MAX_LOG_SIZE_MB) { sanitiseResults(results) } - await automations.logs.storeLog(automation, results) + try { + await automations.logs.storeLog(automation, results) + } catch (e: any) { + if (e.status === 413 && e.request?.data) { + // if content is too large we shouldn't log it + delete e.request.data + e.request.data = { message: "removed due to large size" } + } + logging.logAlert("Error writing automation log", e) + } } export async function checkAppMetadata(apps: App[]) { diff --git a/packages/server/src/automations/tests/triggers/cron.spec.ts b/packages/server/src/automations/tests/triggers/cron.spec.ts index b445b28820..8f29d2aff2 100644 --- a/packages/server/src/automations/tests/triggers/cron.spec.ts +++ b/packages/server/src/automations/tests/triggers/cron.spec.ts @@ -1,11 +1,15 @@ import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" import TestConfiguration from "../../../tests/utilities/TestConfiguration" import { - captureAutomationQueueMessages, + captureAutomationMessages, + captureAutomationRemovals, captureAutomationResults, + triggerCron, } from "../utilities" import { automations } from "@budibase/pro" -import { AutomationStatus } from "@budibase/types" +import { AutomationData, AutomationStatus } from "@budibase/types" +import { MAX_AUTOMATION_RECURRING_ERRORS } from "../../../constants" +import { Job } from "bull" describe("cron trigger", () => { const config = new TestConfiguration() @@ -33,7 +37,7 @@ describe("cron trigger", () => { }) .save() - const messages = await captureAutomationQueueMessages(automation, () => + const messages = await captureAutomationMessages(automation, () => config.api.application.publish() ) expect(messages).toHaveLength(1) @@ -62,8 +66,8 @@ describe("cron trigger", () => { }) }) - it("should stop if the job fails more than 3 times", async () => { - const runner = await createAutomationBuilder(config) + it.only("should stop if the job fails more than 3 times", async () => { + const { automation } = await createAutomationBuilder(config) .onCron({ cron: "* * * * *" }) .queryRows({ // @ts-expect-error intentionally sending invalid data @@ -71,28 +75,31 @@ describe("cron trigger", () => { }) .save() - await config.api.application.publish() - - const results = await captureAutomationResults( - runner.automation, - async () => { - await runner.trigger({ timeout: 1000, fields: {} }) - await runner.trigger({ timeout: 1000, fields: {} }) - await runner.trigger({ timeout: 1000, fields: {} }) - await runner.trigger({ timeout: 1000, fields: {} }) - await runner.trigger({ timeout: 1000, fields: {} }) - } + const [message] = await captureAutomationMessages(automation, () => + config.api.application.publish() ) - expect(results).toHaveLength(5) - await config.withProdApp(async () => { - const { - data: [latest, ..._], - } = await automations.logs.logSearch({ - automationId: runner.automation._id, + let results: Job[] = [] + const removed = await captureAutomationRemovals(automation, async () => { + results = await captureAutomationResults(automation, async () => { + for (let i = 0; i < MAX_AUTOMATION_RECURRING_ERRORS; i++) { + triggerCron(message) + } + }) }) - expect(latest.status).toEqual(AutomationStatus.STOPPED_ERROR) + + expect(removed).toHaveLength(1) + expect(removed[0].id).toEqual(message.id) + + expect(results).toHaveLength(5) + + const search = await automations.logs.logSearch({ + automationId: automation._id, + status: AutomationStatus.STOPPED_ERROR, + }) + expect(search.data).toHaveLength(1) + expect(search.data[0].status).toEqual(AutomationStatus.STOPPED_ERROR) }) }) diff --git a/packages/server/src/automations/tests/utilities/index.ts b/packages/server/src/automations/tests/utilities/index.ts index fcde6170f2..0648a5cb0a 100644 --- a/packages/server/src/automations/tests/utilities/index.ts +++ b/packages/server/src/automations/tests/utilities/index.ts @@ -6,6 +6,7 @@ import { Knex } from "knex" import { getQueue } from "../.." import { Job } from "bull" import { helpers } from "@budibase/shared-core" +import { queue } from "@budibase/backend-core" let config: TestConfiguration @@ -20,6 +21,17 @@ export function afterAll() { config.end() } +export function getTestQueue(): queue.InMemoryQueue { + return getQueue() as unknown as queue.InMemoryQueue +} + +export function triggerCron(message: Job) { + if (!message.opts?.repeat || !("cron" in message.opts.repeat)) { + throw new Error("Expected cron message") + } + getTestQueue().manualTrigger(message.id) +} + export async function runInProd(fn: any) { env._set("NODE_ENV", "production") let error @@ -34,9 +46,41 @@ export async function runInProd(fn: any) { } } -export async function captureAllAutomationQueueMessages( +export async function captureAllAutomationRemovals(f: () => Promise) { + const messages: Job[] = [] + const queue = getQueue() + + const messageListener = async (message: Job) => { + messages.push(message) + } + + queue.on("removed", messageListener) + try { + await f() + // Queue messages tend to be send asynchronously in API handlers, so there's + // no guarantee that awaiting this function will have queued anything yet. + // We wait here to make sure we're queued _after_ any existing async work. + await helpers.wait(100) + } finally { + queue.off("removed", messageListener) + } + + return messages +} + +export async function captureAutomationRemovals( + automation: Automation | string, f: () => Promise ) { + const messages = await captureAllAutomationRemovals(f) + return messages.filter( + m => + m.data.automation._id === + (typeof automation === "string" ? automation : automation._id) + ) +} + +export async function captureAllAutomationMessages(f: () => Promise) { const messages: Job[] = [] const queue = getQueue() @@ -58,11 +102,11 @@ export async function captureAllAutomationQueueMessages( return messages } -export async function captureAutomationQueueMessages( +export async function captureAutomationMessages( automation: Automation | string, f: () => Promise ) { - const messages = await captureAllAutomationQueueMessages(f) + const messages = await captureAllAutomationMessages(f) return messages.filter( m => m.data.automation._id === @@ -87,7 +131,8 @@ export async function captureAllAutomationResults( } const messageListener = async (message: Job) => { // Don't count cron messages, as they don't get triggered automatically. - if (message.opts?.repeat != null) { + const isManualTrigger = (message as any).manualTrigger === true + if (!isManualTrigger && message.opts?.repeat != null) { return } messagesOutstanding++ diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 039c3636f0..409065927b 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -181,17 +181,6 @@ class Orchestrator { await storeLog(automation, this.executionOutput) } - async checkIfShouldStop(): Promise { - const metadata = await this.getMetadata() - if (!metadata.errorCount || !this.isCron()) { - return false - } - if (metadata.errorCount >= MAX_AUTOMATION_RECURRING_ERRORS) { - return true - } - return false - } - async getMetadata(): Promise { const metadataId = generateAutomationMetadataID(this.automation._id!) const db = context.getAppDB() @@ -200,24 +189,29 @@ class Orchestrator { } async incrementErrorCount() { - for (let attempt = 0; attempt < 3; attempt++) { + const db = context.getAppDB() + let err: Error | undefined = undefined + for (let attempt = 0; attempt < 10; attempt++) { const metadata = await this.getMetadata() metadata.errorCount ||= 0 metadata.errorCount++ - const db = context.getAppDB() try { await db.put(metadata) - return - } catch (err) { - logging.logAlertWithInfo( - "Failed to update error count in automation metadata", - db.name, - this.automation._id!, - err - ) + return metadata.errorCount + } catch (error: any) { + err = error + await helpers.wait(Math.random() * 10) } } + + logging.logAlertWithInfo( + "Failed to update error count in automation metadata", + db.name, + this.automation._id!, + err + ) + return undefined } updateExecutionOutput(id: string, stepId: string, inputs: any, outputs: any) { @@ -295,28 +289,22 @@ class Orchestrator { } ) - try { - await storeLog(this.automation, this.executionOutput) - } catch (e: any) { - if (e.status === 413 && e.request?.data) { - // if content is too large we shouldn't log it - delete e.request.data - e.request.data = { message: "removed due to large size" } - } - logging.logAlert("Error writing automation log", e) - } + let errorCount = 0 if ( isProdAppID(this.appId) && this.isCron() && isErrorInOutput(this.executionOutput) ) { - await this.incrementErrorCount() - if (await this.checkIfShouldStop()) { - await this.stopCron("errors") - span?.addTags({ shouldStop: true }) - return - } + errorCount = (await this.incrementErrorCount()) || 0 } + + if (errorCount >= MAX_AUTOMATION_RECURRING_ERRORS) { + await this.stopCron("errors") + span?.addTags({ shouldStop: true }) + } else { + await storeLog(this.automation, this.executionOutput) + } + return this.executionOutput } ) @@ -743,7 +731,7 @@ export async function executeInThread( })) as AutomationResponse } -export const removeStalled = async (job: Job) => { +export const removeStalled = async (job: Job) => { const appId = job.data.event.appId if (!appId) { throw new Error("Unable to execute, event doesn't contain app ID.") From 318c96e9c0c56962251dce09f23e169a79218da4 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 18 Feb 2025 10:48:43 +0000 Subject: [PATCH 09/16] Remove .only --- packages/server/src/automations/tests/triggers/cron.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/automations/tests/triggers/cron.spec.ts b/packages/server/src/automations/tests/triggers/cron.spec.ts index 8f29d2aff2..b7a9f1f213 100644 --- a/packages/server/src/automations/tests/triggers/cron.spec.ts +++ b/packages/server/src/automations/tests/triggers/cron.spec.ts @@ -66,7 +66,7 @@ describe("cron trigger", () => { }) }) - it.only("should stop if the job fails more than 3 times", async () => { + it("should stop if the job fails more than 3 times", async () => { const { automation } = await createAutomationBuilder(config) .onCron({ cron: "* * * * *" }) .queryRows({ From 9c445c1a8c927173813524000282795d1267f9ab Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 18 Feb 2025 11:07:48 +0000 Subject: [PATCH 10/16] Fix loop.spec.ts timeout failures. --- packages/server/src/automations/tests/steps/loop.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/server/src/automations/tests/steps/loop.spec.ts b/packages/server/src/automations/tests/steps/loop.spec.ts index f8af7dcf9f..88e641f5ff 100644 --- a/packages/server/src/automations/tests/steps/loop.spec.ts +++ b/packages/server/src/automations/tests/steps/loop.spec.ts @@ -21,6 +21,11 @@ describe("Attempt to run a basic loop automation", () => { }) beforeEach(async () => { + const { automations } = await config.api.automation.fetch() + for (const automation of automations) { + await config.api.automation.delete(automation) + } + table = await config.api.table.save(basicTable()) await config.api.row.save(table._id!, {}) }) From 5aeac61cd1da0f21df63c2041baafc59315aa261 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 18 Feb 2025 11:17:05 +0000 Subject: [PATCH 11/16] Cleanup. --- packages/backend-core/src/queue/inMemoryQueue.ts | 1 - packages/server/src/threads/automation.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts index 9a6375e485..daf9efa054 100644 --- a/packages/backend-core/src/queue/inMemoryQueue.ts +++ b/packages/backend-core/src/queue/inMemoryQueue.ts @@ -136,7 +136,6 @@ export class InMemoryQueue implements Partial> { * a JSON message as this is required by Bull. * @param repeat serves no purpose for the import queue. */ - // add(name: string, data: T, opts?: JobOptions): Promise>; async add(data: T | string, optsOrT?: JobOptions | T) { if (typeof data === "string") { throw new Error("doesn't support named jobs") diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 409065927b..f854635559 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -201,7 +201,7 @@ class Orchestrator { return metadata.errorCount } catch (error: any) { err = error - await helpers.wait(Math.random() * 10) + await helpers.wait(1000 + Math.random() * 1000) } } From 7c6e9644ca4998d48cfd93f19b8556ac4cd71f77 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 18 Feb 2025 13:59:07 +0100 Subject: [PATCH 12/16] Fix parent usages --- packages/builder/src/stores/builder/components.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/stores/builder/components.ts b/packages/builder/src/stores/builder/components.ts index af3716e207..e141de01ac 100644 --- a/packages/builder/src/stores/builder/components.ts +++ b/packages/builder/src/stores/builder/components.ts @@ -230,7 +230,7 @@ export class ComponentStore extends BudiStore { enrichEmptySettings( component: Component, - opts: { screen?: Screen; parent?: Component; useDefaultValues?: boolean } + opts: { screen?: Screen; parent?: string; useDefaultValues?: boolean } ) { if (!component?._component) { return @@ -238,7 +238,7 @@ export class ComponentStore extends BudiStore { const defaultDS = this.getDefaultDatasource() const settings = this.getComponentSettings(component._component) const { parent, screen, useDefaultValues } = opts || {} - const treeId = parent?._id || component._id + const treeId = parent || component._id if (!screen) { return } @@ -423,7 +423,7 @@ export class ComponentStore extends BudiStore { createInstance( componentType: string, presetProps?: Record, - parent?: Component + parent?: string ): Component | null { const screen = get(selectedScreen) if (!screen) { @@ -501,7 +501,7 @@ export class ComponentStore extends BudiStore { async create( componentType: string, presetProps?: Record, - parent?: Component, + parent?: string, index?: number ) { const state = get(this.store) @@ -517,7 +517,7 @@ export class ComponentStore extends BudiStore { // Insert in position if specified if (parent && index != null) { await screenStore.patch((screen: Screen) => { - let parentComponent = findComponent(screen.props, parent._id!)! + let parentComponent = findComponent(screen.props, parent)! if (!parentComponent._children?.length) { parentComponent._children = [componentInstance] } else { From 6952ca325ace7894011b980d42ee94013317a794 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 18 Feb 2025 14:01:32 +0000 Subject: [PATCH 13/16] Respond to Adri's feedback. --- packages/backend-core/src/queue/inMemoryQueue.ts | 7 +++---- .../src/automations/tests/triggers/cron.spec.ts | 4 ++-- .../src/automations/tests/utilities/index.ts | 15 +++++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts index daf9efa054..842d3243bc 100644 --- a/packages/backend-core/src/queue/inMemoryQueue.ts +++ b/packages/backend-core/src/queue/inMemoryQueue.ts @@ -40,6 +40,7 @@ export interface TestQueueMessage extends Partial> { queue: Queue data: any opts?: JobOptions + manualTrigger?: boolean } /** @@ -90,10 +91,9 @@ export class InMemoryQueue implements Partial> { this._emitter.on("message", async msg => { const message = cloneDeep(msg) - const isManualTrigger = (message as any).manualTrigger === true // For the purpose of testing, don't trigger cron jobs immediately. // Require the test to trigger them manually with timestamps. - if (!isManualTrigger && message.opts?.repeat != null) { + if (!message.manualTrigger && message.opts?.repeat != null) { return } @@ -216,8 +216,7 @@ export class InMemoryQueue implements Partial> { manualTrigger(id: JobId) { for (const message of this._messages) { if (message.id === id) { - const forceMessage = { ...message, manualTrigger: true } - this._emitter.emit("message", forceMessage) + this._emitter.emit("message", { ...message, manualTrigger: true }) return } } diff --git a/packages/server/src/automations/tests/triggers/cron.spec.ts b/packages/server/src/automations/tests/triggers/cron.spec.ts index b7a9f1f213..90d29a60c1 100644 --- a/packages/server/src/automations/tests/triggers/cron.spec.ts +++ b/packages/server/src/automations/tests/triggers/cron.spec.ts @@ -9,7 +9,7 @@ import { import { automations } from "@budibase/pro" import { AutomationData, AutomationStatus } from "@budibase/types" import { MAX_AUTOMATION_RECURRING_ERRORS } from "../../../constants" -import { Job } from "bull" +import { queue } from "@budibase/backend-core" describe("cron trigger", () => { const config = new TestConfiguration() @@ -80,7 +80,7 @@ describe("cron trigger", () => { ) await config.withProdApp(async () => { - let results: Job[] = [] + let results: queue.TestQueueMessage[] = [] const removed = await captureAutomationRemovals(automation, async () => { results = await captureAutomationResults(automation, async () => { for (let i = 0; i < MAX_AUTOMATION_RECURRING_ERRORS; i++) { diff --git a/packages/server/src/automations/tests/utilities/index.ts b/packages/server/src/automations/tests/utilities/index.ts index 0648a5cb0a..4b41f1d977 100644 --- a/packages/server/src/automations/tests/utilities/index.ts +++ b/packages/server/src/automations/tests/utilities/index.ts @@ -120,19 +120,22 @@ export async function captureAutomationMessages( */ export async function captureAllAutomationResults( f: () => Promise -): Promise[]> { - const runs: Job[] = [] +): Promise[]> { + const runs: queue.TestQueueMessage[] = [] const queue = getQueue() let messagesOutstanding = 0 - const completedListener = async (job: Job) => { + const completedListener = async ( + job: queue.TestQueueMessage + ) => { runs.push(job) messagesOutstanding-- } - const messageListener = async (message: Job) => { + const messageListener = async ( + message: queue.TestQueueMessage + ) => { // Don't count cron messages, as they don't get triggered automatically. - const isManualTrigger = (message as any).manualTrigger === true - if (!isManualTrigger && message.opts?.repeat != null) { + if (!message.manualTrigger && message.opts?.repeat != null) { return } messagesOutstanding++ From e474a5ae234b608ef5cf2b0bec8e2cc949379315 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 18 Feb 2025 15:47:46 +0100 Subject: [PATCH 14/16] Split types --- .../controls/URLVariableTestInput.svelte | 4 ++-- .../builder/src/stores/builder/preview.ts | 4 ++-- packages/types/src/ui/components/index.ts | 21 +++++++++++++------ packages/types/src/ui/stores/preview.ts | 2 ++ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/URLVariableTestInput.svelte b/packages/builder/src/components/design/settings/controls/URLVariableTestInput.svelte index a75d7e625d..2aeb6f2205 100644 --- a/packages/builder/src/components/design/settings/controls/URLVariableTestInput.svelte +++ b/packages/builder/src/components/design/settings/controls/URLVariableTestInput.svelte @@ -2,7 +2,7 @@ import { onMount } from "svelte" import { Input, Label } from "@budibase/bbui" import { previewStore, selectedScreen } from "@/stores/builder" - import type { ComponentContext } from "@budibase/types" + import type { AppContext, ComponentContext } from "@budibase/types" export let baseRoute = "" @@ -31,7 +31,7 @@ // This function is needed to repopulate the test value from componentContext // when a user navigates to another component and then back again - const updateTestValueFromContext = (context: ComponentContext | null) => { + const updateTestValueFromContext = (context: AppContext | null) => { if (context?.url && !testValue) { const { wild, ...urlParams } = context.url const queryParams = context.query diff --git a/packages/builder/src/stores/builder/preview.ts b/packages/builder/src/stores/builder/preview.ts index 0fef91d6b9..d95903b3a3 100644 --- a/packages/builder/src/stores/builder/preview.ts +++ b/packages/builder/src/stores/builder/preview.ts @@ -1,6 +1,6 @@ import { get } from "svelte/store" import { BudiStore } from "../BudiStore" -import { PreviewDevice, ComponentContext } from "@budibase/types" +import { PreviewDevice, ComponentContext, AppContext } from "@budibase/types" type PreviewEventHandler = (name: string, payload?: any) => void @@ -8,7 +8,7 @@ interface PreviewState { previewDevice: PreviewDevice previewEventHandler: PreviewEventHandler | null showPreview: boolean - selectedComponentContext: ComponentContext | null + selectedComponentContext: AppContext | null } const INITIAL_PREVIEW_STATE: PreviewState = { diff --git a/packages/types/src/ui/components/index.ts b/packages/types/src/ui/components/index.ts index 455763abda..c34174120c 100644 --- a/packages/types/src/ui/components/index.ts +++ b/packages/types/src/ui/components/index.ts @@ -58,17 +58,26 @@ export interface ComponentSetting { self: boolean } } +interface ComponentAction { + type: string + suffix?: string +} + +interface ComponentStaticContextValue { + label: string + key: string + type: string // technically this is a long list of options but there are too many to enumerate +} export interface ComponentContext { type: ComponentContextType - scope: ComponentContextScopes - actions: any[] - url?: Record - query?: string - state?: Record + scope?: ComponentContextScopes + actions?: ComponentAction[] + suffix?: string + values?: ComponentStaticContextValue[] } -export type ComponentContextType = "context" | "action" +export type ComponentContextType = "action" | "static" | "schema" | "form" export const enum ComponentContextScopes { Local = "local", diff --git a/packages/types/src/ui/stores/preview.ts b/packages/types/src/ui/stores/preview.ts index 4d09366ff5..75309f5dda 100644 --- a/packages/types/src/ui/stores/preview.ts +++ b/packages/types/src/ui/stores/preview.ts @@ -1 +1,3 @@ export type PreviewDevice = "desktop" | "tablet" | "mobile" + +export type AppContext = Record From 72263c06dad721d14a59e475a10c01ad63fa1395 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 18 Feb 2025 16:05:53 +0100 Subject: [PATCH 15/16] Lint --- .../design/settings/controls/URLVariableTestInput.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/design/settings/controls/URLVariableTestInput.svelte b/packages/builder/src/components/design/settings/controls/URLVariableTestInput.svelte index 2aeb6f2205..6a6cb081ef 100644 --- a/packages/builder/src/components/design/settings/controls/URLVariableTestInput.svelte +++ b/packages/builder/src/components/design/settings/controls/URLVariableTestInput.svelte @@ -2,7 +2,7 @@ import { onMount } from "svelte" import { Input, Label } from "@budibase/bbui" import { previewStore, selectedScreen } from "@/stores/builder" - import type { AppContext, ComponentContext } from "@budibase/types" + import type { AppContext } from "@budibase/types" export let baseRoute = "" From eac80ac9aba371ea10a6ea6bb8758d4fd828dca8 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 19 Feb 2025 08:37:44 +0000 Subject: [PATCH 16/16] Bump version to 3.4.12 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index d9d9e01bef..09c739cc8c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.4.11", + "version": "3.4.12", "npmClient": "yarn", "concurrency": 20, "command": {