diff --git a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte index 6c06ce4e79..84b55c403f 100644 --- a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte +++ b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte @@ -51,6 +51,7 @@ } input.hide-arrows { -moz-appearance: textfield; + appearance: textfield; } input[type="time"]::-webkit-calendar-picker-indicator { display: none; diff --git a/packages/bbui/src/Form/Core/Slider.svelte b/packages/bbui/src/Form/Core/Slider.svelte index 1a601d0185..0fcadd8bad 100644 --- a/packages/bbui/src/Form/Core/Slider.svelte +++ b/packages/bbui/src/Form/Core/Slider.svelte @@ -39,6 +39,7 @@ padding: 0; margin: 0; -webkit-appearance: none; + appearance: none; background: transparent; } input::-webkit-slider-thumb { diff --git a/packages/bbui/src/Tabs/Tabs.svelte b/packages/bbui/src/Tabs/Tabs.svelte index c94b396398..3955145ad1 100644 --- a/packages/bbui/src/Tabs/Tabs.svelte +++ b/packages/bbui/src/Tabs/Tabs.svelte @@ -124,8 +124,6 @@ .spectrum-Tabs-selectionIndicator.emphasized { background-color: var(--spectrum-global-color-blue-400); } - .spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator { - } .noHorizPadding { padding: 0; } diff --git a/packages/bbui/src/Tooltip/AbsTooltip.svelte b/packages/bbui/src/Tooltip/AbsTooltip.svelte index 4d2399aaf8..b85f4e1c03 100644 --- a/packages/bbui/src/Tooltip/AbsTooltip.svelte +++ b/packages/bbui/src/Tooltip/AbsTooltip.svelte @@ -134,6 +134,7 @@ .spectrum-Tooltip-label { display: -webkit-box; -webkit-line-clamp: 3; + line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; font-size: 12px; diff --git a/packages/builder/package.json b/packages/builder/package.json index f2a829d5a9..71d1c32008 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -94,6 +94,7 @@ "@sveltejs/vite-plugin-svelte": "1.4.0", "@testing-library/jest-dom": "6.4.2", "@testing-library/svelte": "^4.1.0", + "@types/shortid": "^2.2.0", "babel-jest": "^29.6.2", "identity-obj-proxy": "^3.0.0", "jest": "29.7.0", diff --git a/packages/builder/src/dataBinding.js b/packages/builder/src/dataBinding.js index c89221f163..c026a36cb3 100644 --- a/packages/builder/src/dataBinding.js +++ b/packages/builder/src/dataBinding.js @@ -1507,7 +1507,12 @@ export const updateReferencesInObject = ({ // Migrate references // Switch all bindings to reference their ids -export const migrateReferencesInObject = ({ obj, label = "steps", steps }) => { +export const migrateReferencesInObject = ({ + obj, + label = "steps", + steps, + originalIndex, +}) => { const stepIndexRegex = new RegExp(`{{\\s*${label}\\.(\\d+)\\.`, "g") const updateActionStep = (str, index, replaceWith) => str.replace(`{{ ${label}.${index}.`, `{{ ${label}.${replaceWith}.`) @@ -1528,6 +1533,7 @@ export const migrateReferencesInObject = ({ obj, label = "steps", steps }) => { migrateReferencesInObject({ obj: obj[key], steps, + originalIndex, }) } } diff --git a/packages/builder/src/stores/builder/app.js b/packages/builder/src/stores/builder/app.ts similarity index 55% rename from packages/builder/src/stores/builder/app.js rename to packages/builder/src/stores/builder/app.ts index 3b9e4e0b3c..f98e79eff1 100644 --- a/packages/builder/src/stores/builder/app.js +++ b/packages/builder/src/stores/builder/app.ts @@ -1,7 +1,54 @@ import { API } from "api" import { BudiStore } from "../BudiStore" +import { + App, + AppFeatures, + AppIcon, + AutomationSettings, + Plugin, +} from "@budibase/types" -export const INITIAL_APP_META_STATE = { +interface ClientFeatures { + spectrumThemes: boolean + intelligentLoading: boolean + deviceAwareness: boolean + state: boolean + rowSelection: boolean + customThemes: boolean + devicePreview: boolean + messagePassing: boolean + continueIfAction: boolean + showNotificationAction: boolean + sidePanel: boolean +} + +interface TypeSupportPresets { + [key: string]: any +} + +interface AppMetaState { + appId: string + name: string + url: string + libraries: string[] + clientFeatures: ClientFeatures + typeSupportPresets: TypeSupportPresets + features: AppFeatures + clientLibPath: string + hasLock: boolean + appInstance: { _id: string } | null + initialised: boolean + hasAppPackage: boolean + usedPlugins: Plugin[] | null + automations: AutomationSettings + routes: { [key: string]: any } + version?: string + revertableVersion?: string + upgradableVersion?: string + icon?: AppIcon +} + +export const INITIAL_APP_META_STATE: AppMetaState = { appId: "", name: "", url: "", @@ -34,23 +81,27 @@ export const INITIAL_APP_META_STATE = { routes: {}, } -export class AppMetaStore extends BudiStore { +export class AppMetaStore extends BudiStore { constructor() { super(INITIAL_APP_META_STATE) } - reset() { + reset(): void { this.store.set({ ...INITIAL_APP_META_STATE }) } - syncAppPackage(pkg) { + syncAppPackage(pkg: { + application: App + clientLibPath: string + hasLock: boolean + }): void { const { application: app, clientLibPath, hasLock } = pkg this.update(state => ({ ...state, name: app.name, appId: app.appId, - url: app.url, + url: app.url || "", hasLock, clientLibPath, libraries: app.componentLibraries, @@ -58,8 +109,8 @@ export class AppMetaStore extends BudiStore { appInstance: app.instance, revertableVersion: app.revertableVersion, upgradableVersion: app.upgradableVersion, - usedPlugins: app.usedPlugins, - icon: app.icon || {}, + usedPlugins: app.usedPlugins || null, + icon: app.icon, features: { ...INITIAL_APP_META_STATE.features, ...app.features, @@ -70,7 +121,7 @@ export class AppMetaStore extends BudiStore { })) } - syncClientFeatures(features) { + syncClientFeatures(features: Partial): void { this.update(state => ({ ...state, clientFeatures: { @@ -80,14 +131,14 @@ export class AppMetaStore extends BudiStore { })) } - syncClientTypeSupportPresets(typeSupportPresets) { + syncClientTypeSupportPresets(typeSupportPresets: TypeSupportPresets): void { this.update(state => ({ ...state, typeSupportPresets, })) } - async syncAppRoutes() { + async syncAppRoutes(): Promise { const resp = await API.fetchAppRoutes() this.update(state => ({ ...state, @@ -96,7 +147,7 @@ export class AppMetaStore extends BudiStore { } // Returned from socket - syncMetadata(metadata) { + syncMetadata(metadata: { name: string; url: string; icon?: AppIcon }): void { const { name, url, icon } = metadata this.update(state => ({ ...state, diff --git a/packages/builder/src/stores/builder/automations.js b/packages/builder/src/stores/builder/automations.ts similarity index 66% rename from packages/builder/src/stores/builder/automations.js rename to packages/builder/src/stores/builder/automations.ts index 365f5a8e03..a70ef76821 100644 --- a/packages/builder/src/stores/builder/automations.js +++ b/packages/builder/src/stores/builder/automations.ts @@ -1,4 +1,4 @@ -import { writable, get, derived } from "svelte/store" +import { derived, get } from "svelte/store" import { API } from "api" import { cloneDeep } from "lodash/fp" import { generate } from "shortid" @@ -17,6 +17,19 @@ import { AutomationEventType, AutomationStepType, AutomationActionStepId, + Automation, + AutomationStep, + Table, + Branch, + AutomationTrigger, + AutomationStatus, + UILogicalOperator, + EmptyFilterOption, + AutomationIOType, + AutomationStepSchema, + AutomationTriggerSchema, + BranchPath, + BlockDefinitions, } from "@budibase/types" import { ActionStepID } from "constants/backend/automations" import { FIELDS } from "constants/backend" @@ -24,8 +37,23 @@ import { sdk } from "@budibase/shared-core" import { rowActions } from "./rowActions" import { getNewStepName } from "helpers/automations/nameHelpers" import { QueryUtils } from "@budibase/frontend-core" +import { BudiStore, DerivedBudiStore } from "stores/BudiStore" +import { appStore } from "stores/builder" -const initialAutomationState = { +interface AutomationState { + automations: Automation[] + testResults: any | null + showTestPanel: boolean + blockDefinitions: BlockDefinitions + selectedAutomationId: string | null +} + +interface DerivedAutomationState extends AutomationState { + data: Automation | null + blockRefs: Record +} + +const initialAutomationState: AutomationState = { automations: [], testResults: null, showTestPanel: false, @@ -37,25 +65,11 @@ const initialAutomationState = { selectedAutomationId: null, } -// If this functions, remove the actions elements -export const createAutomationStore = () => { - const store = writable(initialAutomationState) - - store.actions = automationActions(store) - - // Setup history for automations - const history = createHistoryStore({ - getDoc: store.actions.getDefinition, - selectDoc: store.actions.select, - }) - - store.actions.save = history.wrapSaveDoc(store.actions.save) - store.actions.delete = history.wrapDeleteDoc(store.actions.delete) - return { store, history } -} - -const getFinalDefinitions = (triggers, actions) => { - const creatable = {} +const getFinalDefinitions = ( + triggers: Record, + actions: Record +): BlockDefinitions => { + const creatable: Record = {} Object.entries(triggers).forEach(entry => { if (entry[0] === AutomationTriggerStepId.ROW_ACTION) { return @@ -69,7 +83,7 @@ const getFinalDefinitions = (triggers, actions) => { } } -const automationActions = store => ({ +const automationActions = (store: AutomationStore) => ({ /** * Move a given block from one location on the tree to another. * @@ -77,7 +91,11 @@ const automationActions = store => ({ * @param {Object} destPath the destinationPart * @param {Object} automation the automaton to be mutated */ - moveBlock: async (sourcePath, destPath, automation) => { + moveBlock: async ( + sourcePath: BranchPath[], + destPath: BranchPath[], + automation: Automation + ) => { // The last part of the source node address, containing the id. const pathSource = sourcePath.at(-1) @@ -85,13 +103,13 @@ const automationActions = store => ({ const pathEnd = destPath.at(-1) // Check if dragging a step into its own drag zone - const isOwnDragzone = pathSource.id === pathEnd.id + const isOwnDragzone = pathSource?.id === pathEnd?.id // Check if dragging the first branch step into the branch node drag zone const isFirstBranchStep = - pathEnd.branchStepId && - pathEnd.branchIdx === pathSource.branchIdx && - pathSource.stepIdx === 0 + pathEnd?.branchStepId && + pathEnd.branchIdx === pathSource?.branchIdx && + pathSource?.stepIdx === 0 // If dragging into an area that will not affect the tree structure // Ignore the drag and drop. @@ -108,19 +126,21 @@ const automationActions = store => ({ // Traverse again as deleting the node from its original location // will redefine all proceding node locations - const newRefs = {} + const newRefs: Record = {} store.actions.traverse(newRefs, newAutomation) let finalPath // If dropping in a branch-step dropzone you need to find // the updated parent step route then add the branch details again - if (pathEnd.branchStepId) { + if (pathEnd?.branchStepId) { const branchStepRef = newRefs[pathEnd.branchStepId] finalPath = branchStepRef.pathTo finalPath.push(pathEnd) } else { // Place the target 1 after the drop - finalPath = newRefs[pathEnd.id].pathTo + if (pathEnd?.id) { + finalPath = newRefs[pathEnd.id].pathTo + } finalPath.at(-1).stepIdx += 1 } @@ -140,7 +160,6 @@ const automationActions = store => ({ console.error("Error moving automation block ", e) } }, - /** * Core delete function that will delete the node at the provided * location. Loops require 2 deletes so the function returns an array. @@ -150,7 +169,7 @@ const automationActions = store => ({ * @param {*} automation the automation to alter. * @returns {Object} contains the deleted nodes and new updated automation */ - deleteBlock: (pathTo, automation) => { + deleteBlock: (pathTo: Array, automation: Automation) => { let newAutomation = cloneDeep(automation) const steps = [ @@ -158,20 +177,20 @@ const automationActions = store => ({ ...newAutomation.definition.steps, ] - let cache + let cache: any pathTo.forEach((path, pathIdx, array) => { const final = pathIdx === array.length - 1 const { stepIdx, branchIdx } = path - const deleteCore = (steps, idx) => { + const deleteCore = (steps: AutomationStep[], idx: number) => { const targetBlock = steps[idx] // By default, include the id of the target block const idsToDelete = [targetBlock.id] - const blocksDeleted = [] + const blocksDeleted: AutomationStep[] = [] // If deleting a looped block, ensure all related block references are // collated beforehand. Delete can then be handled atomically - const loopSteps = {} + const loopSteps: Record = {} steps.forEach(child => { const { blockToLoop, id: loopBlockId } = child if (blockToLoop) { @@ -228,7 +247,6 @@ const automationActions = store => ({ // should be 1-2 blocks in an array return cache }, - /** * Build metadata for the automation tree. Store the path and * note any loop information used when rendering @@ -236,8 +254,12 @@ const automationActions = store => ({ * @param {Object} block * @param {Array} pathTo */ - registerBlock: (blocks, block, pathTo, terminating) => { - // Directly mutate the `blocks` object without reassigning + registerBlock: ( + blocks: Record, + block: AutomationStep | AutomationTrigger, + pathTo: Array, + terminating: boolean + ) => { blocks[block.id] = { ...(blocks[block.id] || {}), pathTo, @@ -253,20 +275,21 @@ const automationActions = store => ({ } } }, + /** * Build a sequential list of all steps on the step path provided * * @param {Array} pathWay e.g. [{stepIdx:2},{branchIdx:0, stepIdx:2},...] * @returns {Array} all steps encountered on the provided path */ - getPathSteps: (pathWay, automation) => { + getPathSteps: (pathWay: Array, automation: Automation) => { // Base Steps, including trigger const steps = [ automation.definition.trigger, ...automation.definition.steps, ] - let result + let result: (AutomationStep | AutomationTrigger)[] = [] pathWay.forEach(path => { const { stepIdx, branchIdx } = path let last = result ? result[result.length - 1] : [] @@ -275,13 +298,14 @@ const automationActions = store => ({ result = steps.slice(0, stepIdx + 1) return } - - if (Number.isInteger(branchIdx)) { - const branchId = last.inputs.branches[branchIdx].id - const children = last.inputs.children[branchId] - const stepChildren = children.slice(0, stepIdx + 1) - // Preceeding steps. - result = result.concat(stepChildren) + if (last && "inputs" in last) { + if (Number.isInteger(branchIdx)) { + const branchId = last.inputs.branches[branchIdx].id + const children = last.inputs.children[branchId] + const stepChildren = children.slice(0, stepIdx + 1) + // Preceeding steps. + result = result.concat(stepChildren) + } } }) return result @@ -298,18 +322,27 @@ const automationActions = store => ({ * @param {Boolean} insert defaults to false * @returns */ - updateStep: (pathWay, automation, update, insert = false) => { + updateStep: ( + pathWay: Array, + automation: Automation, + update: AutomationStep | AutomationTrigger, + insert = false + ) => { let newAutomation = cloneDeep(automation) - const finalise = (dest, idx, update) => { + const finalise = ( + dest: AutomationStep[], + idx: number, + update: AutomationStep | AutomationTrigger + ) => { dest.splice( idx, - insert ? 0 : update.length || 1, + insert ? 0 : Array.isArray(update) ? update.length : 1, ...(Array.isArray(update) ? update : [update]) ) } - let cache = null + let cache: any = null pathWay.forEach((path, idx, array) => { const { stepIdx, branchIdx } = path let final = idx === array.length - 1 @@ -365,7 +398,7 @@ const automationActions = store => ({ * @returns {Array} all available user bindings */ buildUserBindings: () => { - return getUserBindings().map(binding => { + return getUserBindings().map((binding: any) => { return { ...binding, category: "User", @@ -393,7 +426,6 @@ const automationActions = store => ({ } }) }, - /** * Take the supplied step id and aggregate all bindings for every * step preceding it. @@ -401,15 +433,14 @@ const automationActions = store => ({ * @param {string} id the step id of the target * @returns {Array} all bindings on the path to this step */ - getPathBindings: id => { - const block = get(selectedAutomation).blockRefs[id] - const bindings = store.actions.getAvailableBindings( + getPathBindings: (id: string) => { + const block = get(selectedAutomation)?.blockRefs[id] + return store.actions.getAvailableBindings( block, - get(selectedAutomation).data + get(selectedAutomation)?.data ) - - return bindings }, + /** * Takes the provided automation and traverses all possible paths. * References to all nodes/steps encountered on the way are stored @@ -418,8 +449,8 @@ const automationActions = store => ({ * * @param {Object} automation */ - traverse: (blockRefs, automation) => { - let blocks = [] + traverse: (blockRefs: Record, automation: Automation) => { + let blocks: (AutomationStep | AutomationTrigger)[] = [] if (!automation || !blockRefs) { return } @@ -428,7 +459,13 @@ const automationActions = store => ({ } blocks = blocks.concat(automation.definition.steps || []) - const treeTraverse = (block, pathTo, stepIdx, branchIdx, terminating) => { + const treeTraverse = ( + block: AutomationStep | AutomationTrigger, + pathTo: Array | null, + stepIdx: number, + branchIdx: number | null, + terminating: boolean + ) => { const pathToCurrentNode = [ ...(pathTo || []), { @@ -437,14 +474,16 @@ const automationActions = store => ({ id: block.id, }, ] - const branches = block.inputs?.branches || [] + const branches: Branch[] = block.inputs?.branches || [] branches.forEach((branch, bIdx) => { - block.inputs?.children[branch.id].forEach((bBlock, sIdx, array) => { - const ended = - array.length - 1 === sIdx && !bBlock.inputs?.branches?.length - treeTraverse(bBlock, pathToCurrentNode, sIdx, bIdx, ended) - }) + block.inputs?.children[branch.id].forEach( + (bBlock: AutomationStep, sIdx: number, array: AutomationStep[]) => { + const ended = + array.length - 1 === sIdx && !bBlock.inputs?.branches?.length + treeTraverse(bBlock, pathToCurrentNode, sIdx, bIdx, ended) + } + ) }) store.actions.registerBlock( @@ -463,21 +502,13 @@ const automationActions = store => ({ return blockRefs }, - /** - * Build a list of all bindings specifically on the path - * preceding the provided block. - * - * @param {Object} block step object - * @param {Object} automation The complete automation - * @returns - */ - getAvailableBindings: (block, automation) => { + getAvailableBindings: (block: any, automation: Automation | null) => { if (!block || !automation?.definition) { return [] } // Registered blocks - const blocks = get(selectedAutomation).blockRefs + const blocks = get(selectedAutomation)?.blockRefs // Get all preceeding steps, including the trigger // Filter out the target step as we don't want to include itself @@ -490,19 +521,34 @@ const automationActions = store => ({ .getPathSteps(block.pathTo, automation) .at(-1) - // Extract all outputs from all previous steps as available bindingsx§x - let bindings = [] - const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => { + // Extract all outputs from all previous steps as available bindings + let bindings: any[] = [] + const addBinding = ( + name: string, + value: any, + icon: string, + idx: number, + isLoopBlock: boolean, + bindingName?: string + ) => { if (!name) return - const runtimeBinding = determineRuntimeBinding( + const runtimeBinding = store.actions.determineRuntimeBinding( name, idx, isLoopBlock, - bindingName + bindingName, + automation, + currentBlock, + pathSteps + ) + const categoryName = store.actions.determineCategoryName( + idx, + isLoopBlock, + bindingName, + loopBlockCount ) - const categoryName = determineCategoryName(idx, isLoopBlock, bindingName) bindings.push( - createBindingObject( + store.actions.createBindingObject( name, value, icon, @@ -516,93 +562,6 @@ const automationActions = store => ({ ) } - const determineRuntimeBinding = (name, idx, isLoopBlock) => { - let runtimeName - - /* Begin special cases for generating custom schemas based on triggers */ - if ( - idx === 0 && - automation.definition.trigger?.event === AutomationEventType.APP_TRIGGER - ) { - return `trigger.fields.${name}` - } - - if ( - idx === 0 && - (automation.definition.trigger?.event === - AutomationEventType.ROW_UPDATE || - automation.definition.trigger?.event === AutomationEventType.ROW_SAVE) - ) { - let noRowKeywordBindings = ["id", "revision", "oldRow"] - if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}` - } - /* End special cases for generating custom schemas based on triggers */ - - if (isLoopBlock) { - runtimeName = `loop.${name}` - } else if (idx === 0) { - runtimeName = `trigger.${name}` - } else if ( - currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT - ) { - const stepId = pathSteps[idx].id - if (!stepId) { - notifications.error("Error generating binding: Step ID not found.") - return null - } - runtimeName = `steps["${stepId}"].${name}` - } else { - const stepId = pathSteps[idx].id - if (!stepId) { - notifications.error("Error generating binding: Step ID not found.") - return null - } - runtimeName = `steps.${stepId}.${name}` - } - - return runtimeName - } - - const determineCategoryName = (idx, isLoopBlock, bindingName) => { - if (idx === 0) return "Trigger outputs" - if (isLoopBlock) return "Loop Outputs" - return bindingName - ? `${bindingName} outputs` - : `Step ${idx - loopBlockCount} outputs` - } - - const createBindingObject = ( - name, - value, - icon, - idx, - loopBlockCount, - isLoopBlock, - runtimeBinding, - categoryName, - bindingName - ) => { - const field = Object.values(FIELDS).find( - field => field.type === value.type && field.subtype === value.subtype - ) - return { - readableBinding: - bindingName && !isLoopBlock && idx !== 0 - ? `steps.${bindingName}.${name}` - : runtimeBinding, - runtimeBinding, - type: value.type, - description: value.description, - icon, - category: categoryName, - display: { - type: field?.name || value.type, - name, - rank: isLoopBlock ? idx + 1 : idx - loopBlockCount, - }, - } - } - let loopBlockCount = 0 for (let blockIdx = 0; blockIdx < pathSteps.length; blockIdx++) { @@ -611,20 +570,18 @@ const automationActions = store => ({ automation.definition.stepNames?.[pathBlock.id] || pathBlock.name let schema = cloneDeep(pathBlock?.schema?.outputs?.properties) ?? {} - - const isLoopBlock = - pathBlock.stepId === ActionStepID.LOOP && - pathBlock.blockToLoop in blocks - + let isLoopBlock = false + if (pathBlock.blockToLoop) { + isLoopBlock = + pathBlock.stepId === ActionStepID.LOOP && + pathBlock.blockToLoop in blocks + } const isTrigger = pathBlock.type === AutomationStepType.TRIGGER - // Add the loop schema - // Should only be visible for blocks[pathBlock.id].looped - // Only a once otherwise there will be 1 per loop block if (isLoopBlock && loopBlockCount == 0) { schema = { currentItem: { - type: "string", + type: AutomationIOType.STRING, description: "the item currently being executed", }, } @@ -641,17 +598,16 @@ const automationActions = store => ({ pathBlock.event === AutomationEventType.ROW_UPDATE || pathBlock.event === AutomationEventType.ROW_SAVE ) { - let table = get(tables).list.find( - table => table._id === pathBlock.inputs.tableId + let table: any = get(tables).list.find( + (table: Table) => table._id === pathBlock.inputs.tableId ) - // We want to generate our own schema for the bindings from the table schema itself + for (const key in table?.schema) { schema[key] = { type: table.schema[key].type, subtype: table.schema[key].subtype, } } - // remove the original binding delete schema.row } else if (pathBlock.event === AutomationEventType.APP_TRIGGER) { schema = Object.fromEntries( @@ -686,114 +642,113 @@ const automationActions = store => ({ } return bindings }, - definitions: async () => { - const response = await API.getAutomationDefinitions() - store.update(state => { - state.blockDefinitions = getFinalDefinitions( - response.trigger, - response.action - ) - return state - }) - return response - }, - fetch: async () => { - const [automationResponse, definitions] = await Promise.all([ - API.getAutomations(), - API.getAutomationDefinitions(), - ]) - store.update(state => { - state.automations = automationResponse.automations - state.automations.sort((a, b) => { - return a.name < b.name ? -1 : 1 - }) - state.blockDefinitions = getFinalDefinitions( - definitions.trigger, - definitions.action - ) - return state - }) - }, - create: async (name, trigger) => { - const automation = { - name, - type: "automation", - definition: { - steps: [], - trigger, - }, - disabled: false, - } - const response = await store.actions.save(automation) - return response - }, - duplicate: async automation => { - const response = await store.actions.save({ - ...automation, - name: `${automation.name} - copy`, - _id: undefined, - _ref: undefined, - }) - return response - }, - save: async automation => { - const response = await API.updateAutomation(automation) - await store.actions.fetch() - store.actions.select(response._id) - return response.automation - }, - delete: async automation => { - const isRowAction = sdk.automations.isRowAction(automation) - if (isRowAction) { - await rowActions.delete( - automation.definition.trigger.inputs.tableId, - automation.definition.trigger.inputs.rowActionId - ) + determineRuntimeBinding: ( + name: string, + idx: number, + isLoopBlock: boolean, + bindingName: string | undefined, + automation: Automation, + currentBlock: AutomationStep | AutomationTrigger | undefined, + pathSteps: (AutomationStep | AutomationTrigger)[] + ) => { + let runtimeName: string | null + + /* Begin special cases for generating custom schemas based on triggers */ + if ( + idx === 0 && + automation.definition.trigger?.event === AutomationEventType.APP_TRIGGER + ) { + return `trigger.fields.${name}` + } + + if ( + idx === 0 && + (automation.definition.trigger?.event === + AutomationEventType.ROW_UPDATE || + automation.definition.trigger?.event === AutomationEventType.ROW_SAVE) + ) { + let noRowKeywordBindings = ["id", "revision", "oldRow"] + if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}` + } + /* End special cases for generating custom schemas based on triggers */ + + if (isLoopBlock) { + runtimeName = `loop.${name}` + } else if (idx === 0) { + runtimeName = `trigger.${name}` + } else if (currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT) { + const stepId = pathSteps[idx].id + if (!stepId) { + notifications.error("Error generating binding: Step ID not found.") + return null + } + runtimeName = `steps["${stepId}"].${name}` } else { - await API.deleteAutomation(automation?._id, automation?._rev) + const stepId = pathSteps[idx].id + if (!stepId) { + notifications.error("Error generating binding: Step ID not found.") + return null + } + runtimeName = `steps.${stepId}.${name}` } - store.update(state => { - // Remove the automation - state.automations = state.automations.filter( - x => x._id !== automation._id - ) - - // Select a new automation if required - if (automation._id === state.selectedAutomationId) { - state.selectedAutomationId = state.automations[0]?._id || null - } - - return state - }) + return runtimeName }, - toggleDisabled: async automationId => { - let automation - try { - automation = store.actions.getDefinition(automationId) - if (!automation) { - return - } - automation.disabled = !automation.disabled - await store.actions.save(automation) - notifications.success( - `Automation ${ - automation.disabled ? "disabled" : "enabled" - } successfully` - ) - } catch (error) { - notifications.error( - `Error ${ - automation && automation.disabled ? "disabling" : "enabling" - } automation` - ) + + determineCategoryName: ( + idx: number, + isLoopBlock: boolean, + bindingName: string | undefined, + loopBlockCount: number + ) => { + if (idx === 0) return "Trigger outputs" + if (isLoopBlock) return "Loop Outputs" + return bindingName + ? `${bindingName} outputs` + : `Step ${idx - loopBlockCount} outputs` + }, + + createBindingObject: ( + name: string, + value: any, + icon: string, + idx: number, + loopBlockCount: number, + isLoopBlock: boolean, + runtimeBinding: string | null, + categoryName: string, + bindingName?: string + ) => { + const field = Object.values(FIELDS).find( + field => + field.type === value.type && + ("subtype" in field ? field.subtype === value.subtype : true) + ) + return { + readableBinding: + bindingName && !isLoopBlock && idx !== 0 + ? `steps.${bindingName}.${name}` + : runtimeBinding, + runtimeBinding, + type: value.type, + description: value.description, + icon, + category: categoryName, + display: { + type: field?.name || value.type, + name, + rank: isLoopBlock ? idx + 1 : idx - loopBlockCount, + }, } }, - processBlockInputs: async (block, data) => { + processBlockInputs: async ( + block: AutomationStep, + data: Record + ) => { // Create new modified block - let newBlock = { + let newBlock: AutomationStep & { inputs: any } = { ...block, inputs: { ...block.inputs, @@ -810,7 +765,10 @@ const automationActions = store => ({ }) // Create new modified automation - const automation = get(selectedAutomation).data + const automation = get(selectedAutomation)?.data + if (!automation) { + return false + } const newAutomation = store.actions.getUpdatedDefinition( automation, newBlock @@ -823,18 +781,23 @@ const automationActions = store => ({ return newAutomation }, - updateBlockInputs: async (block, data) => { + + updateBlockInputs: async ( + block: AutomationStep, + data: Record + ) => { const newAutomation = await store.actions.processBlockInputs(block, data) if (newAutomation === false) { return } await store.actions.save(newAutomation) }, - test: async (automation, testData) => { - let result + + test: async (automation: Automation, testData: any) => { + let result: any try { - result = await API.testAutomation(automation?._id, testData) - } catch (err) { + result = await API.testAutomation(automation._id!, testData) + } catch (err: any) { const message = err.message || err.status || JSON.stringify(err) throw `Automation test failed - ${message}` } @@ -849,36 +812,33 @@ const automationActions = store => ({ return state }) }, - getDefinition: id => { - return get(store).automations?.find(x => x._id === id) - }, - getUpdatedDefinition: (automation, block) => { - let newAutomation + + getUpdatedDefinition: ( + automation: Automation, + block: AutomationStep | AutomationTrigger + ): Automation => { + let newAutomation: Automation if (automation.definition.trigger?.id === block.id) { newAutomation = cloneDeep(automation) - newAutomation.definition.trigger = block + newAutomation.definition.trigger = block as AutomationTrigger } else { - const pathToStep = get(selectedAutomation).blockRefs[block.id].pathTo - newAutomation = automationStore.actions.updateStep( - pathToStep, - automation, - block - ) + const pathToStep = get(selectedAutomation)!.blockRefs[block.id].pathTo + newAutomation = store.actions.updateStep(pathToStep, automation, block) } return newAutomation }, - select: id => { - if (!id || id === get(store).selectedAutomationId) { - return - } - store.update(state => { - state.selectedAutomationId = id - state.testResults = null - state.showTestPanel = false - return state - }) - }, - getLogs: async ({ automationId, startDate, status, page } = {}) => { + + getLogs: async ({ + automationId, + startDate, + status, + page, + }: { + automationId?: string + startDate?: string + status?: AutomationStatus + page?: string + } = {}) => { return await API.getAutomationLogs({ automationId, startDate, @@ -886,19 +846,33 @@ const automationActions = store => ({ page, }) }, - clearLogErrors: async ({ automationId, appId } = {}) => { + + clearLogErrors: async ({ + automationId, + appId, + }: { + automationId: string + appId: string + }) => { + if (!automationId || !appId) { + throw new Error("automationId and appId are required") + } return await API.clearAutomationLogErrors(automationId, appId) }, - addTestDataToAutomation: data => { - let newAutomation = cloneDeep(get(selectedAutomation).data) + + addTestDataToAutomation: (data: any) => { + let newAutomation = cloneDeep(get(selectedAutomation)?.data) + if (!newAutomation) { + return newAutomation + } newAutomation.testData = { ...newAutomation.testData, ...data, } return newAutomation }, - constructBlock(type, stepId, blockDefinition) { - let newName + + constructBlock: (type: string, stepId: string, blockDefinition: any) => { const newStep = { ...blockDefinition, inputs: blockDefinition.inputs || {}, @@ -906,10 +880,11 @@ const automationActions = store => ({ type, id: generate(), } - newName = getNewStepName(get(selectedAutomation)?.data, newStep) + const newName = getNewStepName(get(selectedAutomation)?.data, newStep) newStep.name = newName return newStep }, + /** * Generate a new branch block for adding to the automation * There are a minimum of 2 empty branches by default. @@ -917,13 +892,8 @@ const automationActions = store => ({ * @returns {Object} - a default branch block */ generateBranchBlock: () => { - const branchDefinition = get(automationStore).blockDefinitions.ACTION.BRANCH - const branchBlock = automationStore.actions.constructBlock( - "ACTION", - "BRANCH", - branchDefinition - ) - return branchBlock + const branchDefinition = get(store).blockDefinitions.ACTION.BRANCH + return store.actions.constructBlock("ACTION", "BRANCH", branchDefinition) }, /** @@ -933,8 +903,11 @@ const automationActions = store => ({ * @param {Object} block the new block * @param {Array} pathWay location of insert point */ - addBlockToAutomation: async (block, pathWay) => { - const automation = get(selectedAutomation).data + addBlockToAutomation: async (block: AutomationStep, pathWay: Array) => { + const automation = get(selectedAutomation)?.data + if (!automation) { + return + } let newAutomation = cloneDeep(automation) const steps = [ @@ -942,24 +915,23 @@ const automationActions = store => ({ ...newAutomation.definition.steps, ] - let cache + let cache: + | AutomationStepSchema + | AutomationTriggerSchema + pathWay.forEach((path, pathIdx, array) => { const { stepIdx, branchIdx } = path const final = pathIdx === array.length - 1 - const insertBlock = (steps, stepIdx) => { + const insertBlock = (steps: AutomationStep[], stepIdx: number) => { const isBranchNode = !Number.isInteger(stepIdx) - - // If it's a loop block, insert at the looped block stepIdx const insertIdx = block.blockToLoop || isBranchNode ? stepIdx : stepIdx + 1 - steps.splice(insertIdx, 0, block) } if (!cache) { if (final) { - // Offset path to accommodate the trigger insertBlock(newAutomation.definition.steps, stepIdx - 1) cache = block } else { @@ -967,7 +939,6 @@ const automationActions = store => ({ } return } - if (Number.isInteger(branchIdx)) { const branchId = cache.inputs.branches[branchIdx].id const children = cache.inputs.children[branchId] @@ -997,8 +968,8 @@ const automationActions = store => ({ */ generateDefaultConditions: () => { const baseConditionUI = { - logicalOperator: "all", - onEmptyFilter: "none", + logicalOperator: UILogicalOperator.ALL, + onEmptyFilter: EmptyFilterOption.RETURN_NONE, groups: [], } return { @@ -1015,16 +986,16 @@ const automationActions = store => ({ * @param {Array} path - the insertion point on the tree. * @param {Object} automation - the target automation to update. */ - branchAutomation: async (path, automation) => { + branchAutomation: async (path: Array, automation: Automation) => { const insertPoint = path.at(-1) let newAutomation = cloneDeep(automation) - let cache = null + let cache: any let atRoot = false // Generate a default empty branch - const createBranch = name => { + const createBranch = (name: string) => { return { - name: name, + name, ...store.actions.generateDefaultConditions(), id: generate(), } @@ -1089,7 +1060,7 @@ const automationActions = store => ({ // Init the branch children. Shift all steps following the new branch step // into the 0th branch. newBranch.inputs.children = newBranch.inputs.branches.reduce( - (acc, branch, idx) => { + (acc: Record, branch: Branch, idx: number) => { acc[branch.id] = idx == 0 ? cache.slice(insertIdx + 1) : [] return acc }, @@ -1117,14 +1088,20 @@ const automationActions = store => ({ * @param {Object} automation * @param {Object} block */ - branchLeft: async (pathTo, automation, block) => { + branchLeft: async ( + pathTo: Array, + automation: Automation, + block: AutomationStep + ) => { const update = store.actions.shiftBranch(pathTo, block) - const updatedAuto = store.actions.updateStep( - pathTo.slice(0, -1), - automation, - update - ) - await store.actions.save(updatedAuto) + if (update) { + const updatedAuto = store.actions.updateStep( + pathTo.slice(0, -1), + automation, + update + ) + await store.actions.save(updatedAuto) + } }, /** @@ -1134,14 +1111,20 @@ const automationActions = store => ({ * @param {Object} automation * @param {Object} block */ - branchRight: async (pathTo, automation, block) => { + branchRight: async ( + pathTo: Array, + automation: Automation, + block: AutomationStep + ) => { const update = store.actions.shiftBranch(pathTo, block, 1) - const updatedAuto = store.actions.updateStep( - pathTo.slice(0, -1), - automation, - update - ) - await store.actions.save(updatedAuto) + if (update) { + const updatedAuto = store.actions.updateStep( + pathTo.slice(0, -1), + automation, + update + ) + await store.actions.save(updatedAuto) + } }, /** @@ -1151,7 +1134,7 @@ const automationActions = store => ({ * @param {Number} direction - the direction of the swap. Defaults to -1 for left, add 1 for right * @returns */ - shiftBranch(pathTo, block, direction = -1) { + shiftBranch: (pathTo: Array, block: AutomationStep, direction = -1) => { let newBlock = cloneDeep(block) const branchPath = pathTo.at(-1) const targetIdx = branchPath.branchIdx @@ -1162,10 +1145,7 @@ const automationActions = store => ({ } let [neighbour] = newBlock.inputs.branches.splice(targetIdx + direction, 1) - - // Put it back in the previous position. newBlock.inputs.branches.splice(targetIdx, 0, neighbour) - return newBlock }, @@ -1177,9 +1157,9 @@ const automationActions = store => ({ * @param {Array} path * @param {Array} automation */ - deleteBranch: async (path, automation) => { + deleteBranch: async (path: Array, automation: Automation) => { let newAutomation = cloneDeep(automation) - let cache = [] + let cache: any = [] path.forEach((path, pathIdx, array) => { const { stepIdx, branchIdx } = path @@ -1246,15 +1226,14 @@ const automationActions = store => ({ } }, - saveAutomationName: async (blockId, name) => { - const automation = get(selectedAutomation).data + saveAutomationName: async (blockId: string, name: string) => { + const automation = get(selectedAutomation)?.data let newAutomation = cloneDeep(automation) if (!newAutomation) { return } const newName = name.trim() - newAutomation.definition.stepNames = { ...newAutomation.definition.stepNames, [blockId]: newName, @@ -1262,13 +1241,14 @@ const automationActions = store => ({ await store.actions.save(newAutomation) }, - deleteAutomationName: async blockId => { - const automation = get(selectedAutomation).data + + deleteAutomationName: async (blockId: string) => { + const automation = get(selectedAutomation)?.data let newAutomation = cloneDeep(automation) - if (!automation) { + if (!automation || !newAutomation) { return } - if (newAutomation.definition.stepNames) { + if (newAutomation?.definition.stepNames) { delete newAutomation.definition.stepNames[blockId] } @@ -1281,8 +1261,11 @@ const automationActions = store => ({ * * @param {Array} pathTo the path to the target node */ - deleteAutomationBlock: async pathTo => { + deleteAutomationBlock: async (pathTo: Array) => { const automation = get(selectedAutomation)?.data + if (!automation) { + return + } const { newAutomation } = store.actions.deleteBlock(pathTo, automation) @@ -1294,83 +1277,240 @@ const automationActions = store => ({ } }, - replace: async (automationId, automation) => { + replace: (automationId: string, automation?: Automation) => { if (!automation) { - store.update(state => { - // Remove the automation + store.store.update(state => { state.automations = state.automations.filter( x => x._id !== automationId ) - // Select a new automation if required if (automationId === state.selectedAutomationId) { - store.actions.select(state.automations[0]?._id) + store.actions.select(state.automations[0]?._id || null) } return state }) } else { - const index = get(store).automations.findIndex( + const index = get(store.store).automations.findIndex( x => x._id === automation._id ) if (index === -1) { - // Automation addition - store.update(state => ({ + store.store.update(state => ({ ...state, automations: [...state.automations, automation], })) } else { - // Automation update - store.update(state => { + store.store.update(state => { state.automations[index] = automation return state }) } } }, + + create: async (name: string, trigger: AutomationTrigger) => { + const automation: Automation = { + name, + type: "automation", + appId: get(appStore).appId, + definition: { + steps: [], + trigger, + }, + disabled: false, + } + const response = await store.actions.save(automation) + return response + }, + + duplicate: async (automation: Automation) => { + const response = await store.actions.save({ + ...automation, + name: `${automation.name} - copy`, + _id: undefined, + _rev: undefined, + }) + return response + }, + + toggleDisabled: async (automationId: string) => { + let automation: Automation | undefined + try { + automation = store.actions.getDefinition(automationId) + if (!automation) { + return + } + automation.disabled = !automation.disabled + await store.actions.save(automation) + notifications.success( + `Automation ${ + automation.disabled ? "disabled" : "enabled" + } successfully` + ) + } catch (error) { + notifications.error( + `Error ${automation?.disabled ? "disabling" : "enabling"} automation` + ) + } + }, + + definitions: async () => { + const response = await API.getAutomationDefinitions() + store.update(state => { + state.blockDefinitions = getFinalDefinitions( + response.trigger, + response.action + ) + return state + }) + return response + }, + + fetch: async () => { + const [automationResponse, definitions] = await Promise.all([ + API.getAutomations(), + API.getAutomationDefinitions(), + ]) + store.update(state => { + state.automations = automationResponse.automations + state.automations.sort((a, b) => { + return a.name < b.name ? -1 : 1 + }) + state.blockDefinitions = getFinalDefinitions( + definitions.trigger, + definitions.action + ) + return state + }) + }, + + select: (id: string | null) => { + if (!id || id === get(store).selectedAutomationId) { + return + } + store.update(state => { + state.selectedAutomationId = id + state.testResults = null + state.showTestPanel = false + return state + }) + }, + + getDefinition: (id: string): Automation | undefined => { + return get(store.store).automations?.find(x => x._id === id) + }, + + save: async (automation: Automation) => { + const response = await API.updateAutomation(automation) + await store.actions.fetch() + store.actions.select(response.automation._id!) + return response.automation + }, + + delete: async (automation: Automation) => { + const isRowAction = sdk.automations.isRowAction(automation) + if (isRowAction) { + await rowActions.delete( + automation.definition.trigger.inputs.tableId, + automation.definition.trigger.inputs.rowActionId + ) + } else { + await API.deleteAutomation(automation._id!, automation._rev!) + } + + store.update(state => { + state.automations = state.automations.filter( + x => x._id !== automation._id + ) + if (automation._id === state.selectedAutomationId) { + state.selectedAutomationId = state.automations[0]?._id || null + } + return state + }) + }, }) -const automations = createAutomationStore() +class AutomationStore extends BudiStore { + history: any + actions: ReturnType -export const automationStore = automations.store - -export const automationHistoryStore = automations.history - -// Derived automation state -export const selectedAutomation = derived(automationStore, $automationStore => { - if (!$automationStore.selectedAutomationId) { - return null - } - - const selected = $automationStore.automations?.find( - x => x._id === $automationStore.selectedAutomationId - ) - - // Traverse the entire tree and record all nodes found - // Also store any info relevant to the UX - const blockRefs = {} - automationStore.actions.traverse(blockRefs, selected) - - // Parse the steps for references to sequential binding - // Replace all bindings with id based alternatives - const updatedAuto = cloneDeep(selected) - Object.values(blockRefs) - .filter(blockRef => { - // Pulls out all distinct terminating nodes - return blockRef.terminating - }) - .forEach(blockRef => { - automationStore.actions - .getPathSteps(blockRef.pathTo, updatedAuto) - .forEach((step, idx, steps) => { - migrateReferencesInObject({ - obj: step, - originalIndex: idx, - steps, - }) - }) + constructor() { + super(initialAutomationState) + this.actions = automationActions(this) + this.history = createHistoryStore({ + getDoc: this.actions.getDefinition.bind(this), + selectDoc: this.actions.select.bind(this), + beforeAction: () => {}, + afterAction: () => {}, }) - return { - data: updatedAuto, - blockRefs, + // Then wrap save and delete with history + const originalSave = this.actions.save.bind(this.actions) + const originalDelete = this.actions.delete.bind(this.actions) + this.actions.save = this.history.wrapSaveDoc(originalSave) + this.actions.delete = this.history.wrapDeleteDoc(originalDelete) } -}) +} + +export const automationStore = new AutomationStore() +export const automationHistoryStore = automationStore.history + +export class SelectedAutomationStore extends DerivedBudiStore< + AutomationState, + DerivedAutomationState +> { + constructor(automationStore: AutomationStore) { + const makeDerivedStore = () => { + return derived(automationStore, $store => { + if (!$store.selectedAutomationId) { + return { + data: null, + blockRefs: {}, + ...$store, + } + } + + const selected = $store.automations?.find( + x => x._id === $store.selectedAutomationId + ) + + if (!selected) { + return { + data: null, + blockRefs: {}, + ...$store, + } + } + + const blockRefs: Record = {} + const updatedAuto = cloneDeep(selected) + + // Only traverse if we have a valid automation + if (updatedAuto) { + automationStore.actions.traverse(blockRefs, updatedAuto) + + Object.values(blockRefs) + .filter(blockRef => blockRef.terminating) + .forEach(blockRef => { + automationStore.actions + .getPathSteps(blockRef.pathTo, updatedAuto) + .forEach((step, idx, steps) => { + migrateReferencesInObject({ + obj: step, + originalIndex: idx, + steps, + }) + }) + }) + } + + return { + data: updatedAuto, + blockRefs, + ...$store, + } + }) + } + + super(initialAutomationState, makeDerivedStore) + } +} +export const selectedAutomation = new SelectedAutomationStore(automationStore) diff --git a/packages/builder/src/stores/builder/builder.js b/packages/builder/src/stores/builder/builder.ts similarity index 68% rename from packages/builder/src/stores/builder/builder.js rename to packages/builder/src/stores/builder/builder.ts index 9b5a847680..00f6c5fce7 100644 --- a/packages/builder/src/stores/builder/builder.js +++ b/packages/builder/src/stores/builder/builder.ts @@ -1,10 +1,28 @@ import { get } from "svelte/store" import { createBuilderWebsocket } from "./websocket.js" +import { Socket } from "socket.io-client" import { BuilderSocketEvent } from "@budibase/shared-core" import { BudiStore } from "../BudiStore.js" import { TOUR_KEYS } from "components/portal/onboarding/tours.js" +import { App } from "@budibase/types" -export const INITIAL_BUILDER_STATE = { +interface BuilderState { + previousTopNavPath: Record + highlightedSetting: { + key: string + type: "info" | string + } | null + propertyFocus: string | null + builderSidePanel: boolean + onboarding: boolean + tourNodes: Record | null + tourKey: string | null + tourStepKey: string | null + hoveredComponentId: string | null + websocket?: Socket +} + +export const INITIAL_BUILDER_STATE: BuilderState = { previousTopNavPath: {}, highlightedSetting: null, propertyFocus: null, @@ -16,7 +34,9 @@ export const INITIAL_BUILDER_STATE = { hoveredComponentId: null, } -export class BuilderStore extends BudiStore { +export class BuilderStore extends BudiStore { + websocket?: Socket + constructor() { super({ ...INITIAL_BUILDER_STATE }) @@ -32,11 +52,9 @@ export class BuilderStore extends BudiStore { this.registerTourNode = this.registerTourNode.bind(this) this.destroyTourNode = this.destroyTourNode.bind(this) this.startBuilderOnboarding = this.startBuilderOnboarding.bind(this) - - this.websocket } - init(app) { + init(app: App): void { if (!app?.appId) { console.error("BuilderStore: No appId supplied for websocket") return @@ -46,45 +64,46 @@ export class BuilderStore extends BudiStore { } } - refresh() { - this.store.set(this.store.get()) + refresh(): void { + const currentState = get(this.store) + this.store.set(currentState) } - reset() { + reset(): void { this.store.set({ ...INITIAL_BUILDER_STATE }) this.websocket?.disconnect() - this.websocket = null + this.websocket = undefined } - highlightSetting(key, type) { + highlightSetting(key?: string, type?: string): void { this.update(state => ({ ...state, highlightedSetting: key ? { key, type: type || "info" } : null, })) } - propertyFocus(key) { + propertyFocus(key: string | null): void { this.update(state => ({ ...state, propertyFocus: key, })) } - showBuilderSidePanel() { + showBuilderSidePanel(): void { this.update(state => ({ ...state, builderSidePanel: true, })) } - hideBuilderSidePanel() { + hideBuilderSidePanel(): void { this.update(state => ({ ...state, builderSidePanel: false, })) } - setPreviousTopNavPath(route, url) { + setPreviousTopNavPath(route: string, url: string): void { this.update(state => ({ ...state, previousTopNavPath: { @@ -94,13 +113,13 @@ export class BuilderStore extends BudiStore { })) } - selectResource(id) { - this.websocket.emit(BuilderSocketEvent.SelectResource, { + selectResource(id: string): void { + this.websocket?.emit(BuilderSocketEvent.SelectResource, { resourceId: id, }) } - registerTourNode(tourStepKey, node) { + registerTourNode(tourStepKey: string, node: HTMLElement): void { this.update(state => { const update = { ...state, @@ -113,7 +132,7 @@ export class BuilderStore extends BudiStore { }) } - destroyTourNode(tourStepKey) { + destroyTourNode(tourStepKey: string): void { const store = get(this.store) if (store.tourNodes?.[tourStepKey]) { const nodes = { ...store.tourNodes } @@ -125,7 +144,7 @@ export class BuilderStore extends BudiStore { } } - startBuilderOnboarding() { + startBuilderOnboarding(): void { this.update(state => ({ ...state, onboarding: true, @@ -133,19 +152,19 @@ export class BuilderStore extends BudiStore { })) } - endBuilderOnboarding() { + endBuilderOnboarding(): void { this.update(state => ({ ...state, onboarding: false, })) } - setTour(tourKey) { + setTour(tourKey?: string | null): void { this.update(state => ({ ...state, tourStepKey: null, tourNodes: null, - tourKey: tourKey, + tourKey: tourKey || null, })) } } diff --git a/packages/builder/src/stores/builder/contextMenu.js b/packages/builder/src/stores/builder/contextMenu.js deleted file mode 100644 index 2b8808570e..0000000000 --- a/packages/builder/src/stores/builder/contextMenu.js +++ /dev/null @@ -1,28 +0,0 @@ -import { writable } from "svelte/store" - -export const INITIAL_CONTEXT_MENU_STATE = { - id: null, - items: [], - position: { x: 0, y: 0 }, - visible: false, -} - -export function createViewsStore() { - const store = writable({ ...INITIAL_CONTEXT_MENU_STATE }) - - const open = (id, items, position) => { - store.set({ id, items, position, visible: true }) - } - - const close = () => { - store.set({ ...INITIAL_CONTEXT_MENU_STATE }) - } - - return { - subscribe: store.subscribe, - open, - close, - } -} - -export const contextMenuStore = createViewsStore() diff --git a/packages/builder/src/stores/builder/contextMenu.ts b/packages/builder/src/stores/builder/contextMenu.ts new file mode 100644 index 0000000000..dc205c7fea --- /dev/null +++ b/packages/builder/src/stores/builder/contextMenu.ts @@ -0,0 +1,46 @@ +import { writable } from "svelte/store" + +interface Position { + x: number + y: number +} + +interface MenuItem { + label: string + icon?: string + action: () => void +} + +interface ContextMenuState { + id: string | null + items: MenuItem[] + position: Position + visible: boolean +} + +export const INITIAL_CONTEXT_MENU_STATE: ContextMenuState = { + id: null, + items: [], + position: { x: 0, y: 0 }, + visible: false, +} + +export function createViewsStore() { + const store = writable({ ...INITIAL_CONTEXT_MENU_STATE }) + + const open = (id: string, items: MenuItem[], position: Position): void => { + store.set({ id, items, position, visible: true }) + } + + const close = (): void => { + store.set({ ...INITIAL_CONTEXT_MENU_STATE }) + } + + return { + subscribe: store.subscribe, + open, + close, + } +} + +export const contextMenuStore = createViewsStore() diff --git a/packages/builder/src/stores/builder/deployments.js b/packages/builder/src/stores/builder/deployments.ts similarity index 62% rename from packages/builder/src/stores/builder/deployments.js rename to packages/builder/src/stores/builder/deployments.ts index dafdb1dabc..130e52bc91 100644 --- a/packages/builder/src/stores/builder/deployments.js +++ b/packages/builder/src/stores/builder/deployments.ts @@ -1,11 +1,12 @@ -import { writable } from "svelte/store" +import { writable, type Writable } from "svelte/store" import { API } from "api" import { notifications } from "@budibase/bbui" +import { DeploymentProgressResponse } from "@budibase/types" export const createDeploymentStore = () => { - let store = writable([]) + let store: Writable = writable([]) - const load = async () => { + const load = async (): Promise => { try { store.set(await API.getAppDeployments()) } catch (err) { diff --git a/packages/builder/src/stores/builder/tests/builder.test.js b/packages/builder/src/stores/builder/tests/builder.test.js index e6f52689aa..f3c42dae72 100644 --- a/packages/builder/src/stores/builder/tests/builder.test.js +++ b/packages/builder/src/stores/builder/tests/builder.test.js @@ -65,7 +65,7 @@ describe("Builder store", () => { ctx.test.builderStore.reset() expect(disconnected).toBe(true) expect(ctx.test.store).toStrictEqual(INITIAL_BUILDER_STATE) - expect(ctx.test.builderStore.websocket).toBeNull() + expect(ctx.test.builderStore.websocket).toBeUndefined() }) it("Attempt to emit a resource select event to the websocket on select", ctx => { diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js index 72a44a8fa0..b5dd022c5c 100644 --- a/packages/builder/src/stores/portal/licensing.js +++ b/packages/builder/src/stores/portal/licensing.js @@ -22,6 +22,7 @@ export const createLicensingStore = () => { backupsEnabled: false, brandingEnabled: false, scimEnabled: false, + environmentVariablesEnabled: false, budibaseAIEnabled: false, customAIConfigsEnabled: false, auditLogsEnabled: false, diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 8377b13ea2..b1f311183a 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -5,6 +5,9 @@ "author": "Budibase", "license": "MPL-2.0", "svelte": "./src/index.ts", + "scripts": { + "check:types": "yarn svelte-check" + }, "dependencies": { "@budibase/bbui": "*", "@budibase/shared-core": "*", @@ -13,5 +16,8 @@ "lodash": "4.17.21", "shortid": "2.2.15", "socket.io-client": "^4.7.5" + }, + "devDependencies": { + "svelte-check": "^4.1.0" } } diff --git a/packages/frontend-core/src/components/grid/cells/AICell.svelte b/packages/frontend-core/src/components/grid/cells/AICell.svelte index 38e81cefd3..b56e67b752 100644 --- a/packages/frontend-core/src/components/grid/cells/AICell.svelte +++ b/packages/frontend-core/src/components/grid/cells/AICell.svelte @@ -73,6 +73,7 @@ .value { display: -webkit-box; -webkit-line-clamp: var(--content-lines); + line-clamp: var(--content-lines); -webkit-box-orient: vertical; overflow: hidden; line-height: 20px; diff --git a/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte b/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte index 7829e5da7d..80da91e091 100644 --- a/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte @@ -93,6 +93,7 @@ .value { display: -webkit-box; -webkit-line-clamp: var(--content-lines); + line-clamp: var(--content-lines); -webkit-box-orient: vertical; overflow: hidden; line-height: 20px; diff --git a/packages/frontend-core/src/components/grid/cells/TextCell.svelte b/packages/frontend-core/src/components/grid/cells/TextCell.svelte index 9275bca3c6..b9a63eb401 100644 --- a/packages/frontend-core/src/components/grid/cells/TextCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/TextCell.svelte @@ -74,12 +74,14 @@ .value { display: -webkit-box; -webkit-line-clamp: var(--content-lines); + line-clamp: var(--content-lines); -webkit-box-orient: vertical; overflow: hidden; line-height: 20px; } .number .value { -webkit-line-clamp: 1; + line-clamp: 1; } input { flex: 1 1 auto; @@ -110,5 +112,6 @@ } input[type="number"] { -moz-appearance: textfield; + appearance: textfield; } diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.ts similarity index 67% rename from packages/frontend-core/src/components/grid/stores/datasource.js rename to packages/frontend-core/src/components/grid/stores/datasource.ts index 6aa607f7ed..7aee6e8515 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.ts @@ -1,10 +1,54 @@ -import { derived, get } from "svelte/store" +import { derived, get, Readable, Writable } from "svelte/store" import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch" import { enrichSchemaWithRelColumns, memo } from "../../../utils" import { cloneDeep } from "lodash" -import { ViewV2Type } from "@budibase/types" +import { + Row, + SaveRowRequest, + SaveTableRequest, + UIDatasource, + UIFieldMutation, + UIFieldSchema, + UpdateViewRequest, + ViewV2Type, +} from "@budibase/types" +import { Store as StoreContext } from "." +import { DatasourceActions } from "./datasources" -export const createStores = () => { +interface DatasourceStore { + definition: Writable + schemaMutations: Writable> + subSchemaMutations: Writable>> +} + +interface DerivedDatasourceStore { + schema: Readable | null> + enrichedSchema: Readable | null> + hasBudibaseIdentifiers: Readable +} + +interface ActionDatasourceStore { + datasource: DatasourceStore["definition"] & { + actions: DatasourceActions & { + refreshDefinition: () => Promise + changePrimaryDisplay: (column: string) => Promise + addSchemaMutation: (field: string, mutation: UIFieldMutation) => void + addSubSchemaMutation: ( + field: string, + fromField: string, + mutation: UIFieldMutation + ) => void + saveSchemaMutations: () => Promise + resetSchemaMutations: () => void + } + } +} + +export type Store = DatasourceStore & + DerivedDatasourceStore & + ActionDatasourceStore + +export const createStores = (): DatasourceStore => { const definition = memo(null) const schemaMutations = memo({}) const subSchemaMutations = memo({}) @@ -16,7 +60,7 @@ export const createStores = () => { } } -export const deriveStores = context => { +export const deriveStores = (context: StoreContext): DerivedDatasourceStore => { const { API, definition, @@ -27,7 +71,7 @@ export const deriveStores = context => { } = context const schema = derived(definition, $definition => { - let schema = getDatasourceSchema({ + let schema: Record = getDatasourceSchema({ API, datasource: get(datasource), definition: $definition, @@ -40,7 +84,7 @@ export const deriveStores = context => { // Certain datasources like queries use primitives. Object.keys(schema || {}).forEach(key => { if (typeof schema[key] !== "object") { - schema[key] = { type: schema[key] } + schema[key] = { name: key, type: schema[key] } } }) @@ -58,19 +102,18 @@ export const deriveStores = context => { const schemaWithRelatedColumns = enrichSchemaWithRelColumns($schema) - const enrichedSchema = {} - Object.keys(schemaWithRelatedColumns).forEach(field => { + const enrichedSchema: Record = {} + Object.keys(schemaWithRelatedColumns || {}).forEach(field => { enrichedSchema[field] = { - ...schemaWithRelatedColumns[field], + ...schemaWithRelatedColumns?.[field], ...$schemaOverrides?.[field], ...$schemaMutations[field], } if ($subSchemaMutations[field]) { enrichedSchema[field].columns ??= {} - for (const [fieldName, mutation] of Object.entries( - $subSchemaMutations[field] - )) { + for (const fieldName of Object.keys($subSchemaMutations[field])) { + const mutation = $subSchemaMutations[field][fieldName] enrichedSchema[field].columns[fieldName] = { ...enrichedSchema[field].columns[fieldName], ...mutation, @@ -87,7 +130,7 @@ export const deriveStores = context => { ([$datasource, $definition]) => { let type = $datasource?.type if (type === "provider") { - type = $datasource.value?.datasource?.type + type = ($datasource as any).value?.datasource?.type } // Handle calculation views if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) { @@ -104,7 +147,7 @@ export const deriveStores = context => { } } -export const createActions = context => { +export const createActions = (context: StoreContext): ActionDatasourceStore => { const { API, datasource, @@ -147,21 +190,23 @@ export const createActions = context => { } // Saves the datasource definition - const saveDefinition = async newDefinition => { + const saveDefinition = async ( + newDefinition: SaveTableRequest | UpdateViewRequest + ) => { // Update local state const originalDefinition = get(definition) - definition.set(newDefinition) + definition.set(newDefinition as UIDatasource) // Update server if (get(config).canSaveSchema) { try { - await getAPI()?.actions.saveDefinition(newDefinition) + await getAPI()?.actions.saveDefinition(newDefinition as never) // Broadcast change so external state can be updated, as this change // will not be received by the builder websocket because we caused it // ourselves dispatch("updatedatasource", newDefinition) - } catch (error) { + } catch (error: any) { const msg = error?.message || error || "Unknown error" get(notifications).error(`Error saving schema: ${msg}`) @@ -172,7 +217,7 @@ export const createActions = context => { } // Updates the datasources primary display column - const changePrimaryDisplay = async column => { + const changePrimaryDisplay = async (column: string) => { let newDefinition = cloneDeep(get(definition)) // Update primary display @@ -183,12 +228,14 @@ export const createActions = context => { newDefinition.schema[column].constraints = {} } newDefinition.schema[column].constraints.presence = { allowEmpty: false } - delete newDefinition.schema[column].default - return await saveDefinition(newDefinition) + if ("default" in newDefinition.schema[column]) { + delete newDefinition.schema[column].default + } + return await saveDefinition(newDefinition as any) } // Adds a schema mutation for a single field - const addSchemaMutation = (field, mutation) => { + const addSchemaMutation = (field: string, mutation: UIFieldMutation) => { if (!field || !mutation) { return } @@ -204,7 +251,11 @@ export const createActions = context => { } // Adds a nested schema mutation for a single field - const addSubSchemaMutation = (field, fromField, mutation) => { + const addSubSchemaMutation = ( + field: string, + fromField: string, + mutation: UIFieldMutation + ) => { if (!field || !fromField || !mutation) { return } @@ -231,8 +282,8 @@ export const createActions = context => { const $definition = get(definition) const $schemaMutations = get(schemaMutations) const $subSchemaMutations = get(subSchemaMutations) - const $schema = get(schema) - let newSchema = {} + const $schema = get(schema) || {} + let newSchema: Record = {} // Build new updated datasource schema Object.keys($schema).forEach(column => { @@ -242,9 +293,8 @@ export const createActions = context => { } if ($subSchemaMutations[column]) { newSchema[column].columns ??= {} - for (const [fieldName, mutation] of Object.entries( - $subSchemaMutations[column] - )) { + for (const fieldName of Object.keys($subSchemaMutations[column])) { + const mutation = $subSchemaMutations[column][fieldName] newSchema[column].columns[fieldName] = { ...newSchema[column].columns[fieldName], ...mutation, @@ -257,7 +307,7 @@ export const createActions = context => { await saveDefinition({ ...$definition, schema: newSchema, - }) + } as any) resetSchemaMutations() } @@ -267,32 +317,32 @@ export const createActions = context => { } // Adds a row to the datasource - const addRow = async row => { + const addRow = async (row: SaveRowRequest) => { return await getAPI()?.actions.addRow(row) } // Updates an existing row in the datasource - const updateRow = async row => { + const updateRow = async (row: SaveRowRequest) => { return await getAPI()?.actions.updateRow(row) } // Deletes rows from the datasource - const deleteRows = async rows => { + const deleteRows = async (rows: Row[]) => { return await getAPI()?.actions.deleteRows(rows) } // Gets a single row from a datasource - const getRow = async id => { + const getRow = async (id: string) => { return await getAPI()?.actions.getRow(id) } // Checks if a certain datasource config is valid - const isDatasourceValid = datasource => { + const isDatasourceValid = (datasource: UIDatasource) => { return getAPI()?.actions.isDatasourceValid(datasource) } // Checks if this datasource can use a specific column by name - const canUseColumn = name => { + const canUseColumn = (name: string) => { return getAPI()?.actions.canUseColumn(name) } diff --git a/packages/frontend-core/src/components/grid/stores/datasources/index.ts b/packages/frontend-core/src/components/grid/stores/datasources/index.ts new file mode 100644 index 0000000000..c58aef37e9 --- /dev/null +++ b/packages/frontend-core/src/components/grid/stores/datasources/index.ts @@ -0,0 +1,31 @@ +import { + Row, + SaveRowRequest, + SaveTableRequest, + UIDatasource, + UpdateViewRequest, +} from "@budibase/types" + +interface DatasourceBaseActions< + TSaveDefinitionRequest = UpdateViewRequest | SaveTableRequest +> { + saveDefinition: (newDefinition: TSaveDefinitionRequest) => Promise + addRow: (row: SaveRowRequest) => Promise + updateRow: (row: SaveRowRequest) => Promise + deleteRows: (rows: Row[]) => Promise + getRow: (id: string) => Promise + isDatasourceValid: (datasource: UIDatasource) => boolean | void + canUseColumn: (name: string) => boolean | void +} + +export interface DatasourceTableActions + extends DatasourceBaseActions {} + +export interface DatasourceViewActions + extends DatasourceBaseActions {} + +export interface DatasourceNonPlusActions + extends DatasourceBaseActions {} + +export type DatasourceActions = + | DatasourceTableActions & DatasourceViewActions & DatasourceNonPlusActions diff --git a/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.ts b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.ts index dcc4d47076..17e5e8b8d9 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.ts +++ b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.ts @@ -1,18 +1,11 @@ import { SortOrder, UIDatasource } from "@budibase/types" import { get } from "svelte/store" import { Store as StoreContext } from ".." +import { DatasourceNonPlusActions } from "." interface NonPlusActions { nonPlus: { - actions: { - saveDefinition: () => Promise - addRow: () => Promise - updateRow: () => Promise - deleteRows: () => Promise - getRow: () => Promise - isDatasourceValid: (datasource: UIDatasource) => boolean - canUseColumn: (name: string) => boolean - } + actions: DatasourceNonPlusActions } } diff --git a/packages/frontend-core/src/components/grid/stores/datasources/table.ts b/packages/frontend-core/src/components/grid/stores/datasources/table.ts index e905c89e44..894a65ba4c 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/table.ts +++ b/packages/frontend-core/src/components/grid/stores/datasources/table.ts @@ -1,27 +1,19 @@ import { Row, SaveRowRequest, - SaveRowResponse, SaveTableRequest, SortOrder, UIDatasource, } from "@budibase/types" import { get } from "svelte/store" import { Store as StoreContext } from ".." +import { DatasourceTableActions } from "." const SuppressErrors = true interface TableActions { table: { - actions: { - saveDefinition: (newDefinition: SaveTableRequest) => Promise - addRow: (row: SaveRowRequest) => Promise - updateRow: (row: SaveRowRequest) => Promise - deleteRows: (rows: (string | Row)[]) => Promise - getRow: (id: string) => Promise - isDatasourceValid: (datasource: UIDatasource) => boolean - canUseColumn: (name: string) => boolean - } + actions: DatasourceTableActions } } @@ -42,7 +34,7 @@ export const createActions = (context: StoreContext): TableActions => { return await API.saveRow(row, SuppressErrors) } - const deleteRows = async (rows: (string | Row)[]) => { + const deleteRows = async (rows: Row[]) => { await API.deleteRows(get(datasource).tableId, rows) } diff --git a/packages/frontend-core/src/components/grid/stores/datasources/viewV2.ts b/packages/frontend-core/src/components/grid/stores/datasources/viewV2.ts index 677a85312f..d9cac5397d 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/viewV2.ts +++ b/packages/frontend-core/src/components/grid/stores/datasources/viewV2.ts @@ -4,23 +4,17 @@ import { SaveRowRequest, SortOrder, UIDatasource, + UIView, UpdateViewRequest, } from "@budibase/types" import { Store as StoreContext } from ".." +import { DatasourceViewActions } from "." const SuppressErrors = true interface ViewActions { viewV2: { - actions: { - saveDefinition: (newDefinition: UpdateViewRequest) => Promise - addRow: (row: SaveRowRequest) => Promise - updateRow: (row: SaveRowRequest) => Promise - deleteRows: (rows: (string | Row)[]) => Promise - getRow: (id: string) => Promise - isDatasourceValid: (datasource: UIDatasource) => boolean - canUseColumn: (name: string) => boolean - } + actions: DatasourceViewActions } } @@ -46,7 +40,7 @@ export const createActions = (context: StoreContext): ViewActions => { } } - const deleteRows = async (rows: (string | Row)[]) => { + const deleteRows = async (rows: Row[]) => { await API.deleteRows(get(datasource).id, rows) } @@ -154,7 +148,7 @@ export const initialise = (context: StoreContext) => { unsubscribers.push( sort.subscribe(async $sort => { // Ensure we're updating the correct view - const $view = get(definition) + const $view = get(definition) as UIView if ($view?.id !== $datasource.id) { return } @@ -205,7 +199,7 @@ export const initialise = (context: StoreContext) => { await datasource.actions.saveDefinition({ ...$view, queryUI: $filter, - }) + } as never as UpdateViewRequest) // Refresh data since view definition changed await rows.actions.refreshData() diff --git a/packages/frontend-core/src/components/grid/stores/index.ts b/packages/frontend-core/src/components/grid/stores/index.ts index 1ef5da03b6..d0413cb80a 100644 --- a/packages/frontend-core/src/components/grid/stores/index.ts +++ b/packages/frontend-core/src/components/grid/stores/index.ts @@ -59,11 +59,12 @@ export type Store = BaseStore & Columns.Store & Table.Store & ViewV2.Store & - NonPlus.Store & { + NonPlus.Store & + Datasource.Store & + Validation.Store & + Users.Store & + Menu.Store & { // TODO while typing the rest of stores - datasource: Writable & { actions: any } - definition: Writable - enrichedSchema: any fetch: Writable filter: Writable inlineFilters: Writable @@ -75,6 +76,16 @@ export type Store = BaseStore & rows: Writable & { actions: any } subscribe: any config: Writable + dispatch: (event: string, data: any) => any + notifications: Writable + schemaOverrides: Writable + focusedCellId: Writable + previousFocusedRowId: Writable + gridID: string + selectedRows: Writable + selectedRowCount: Writable + selectedCellMap: Writable + selectedCellCount: Writable } export const attachStores = (context: Store): Store => { @@ -106,5 +117,5 @@ export const attachStores = (context: Store): Store => { } } - return context + return context as Store } diff --git a/packages/frontend-core/src/components/grid/stores/menu.js b/packages/frontend-core/src/components/grid/stores/menu.ts similarity index 77% rename from packages/frontend-core/src/components/grid/stores/menu.js rename to packages/frontend-core/src/components/grid/stores/menu.ts index 22bf26fff5..27e41c412b 100644 --- a/packages/frontend-core/src/components/grid/stores/menu.js +++ b/packages/frontend-core/src/components/grid/stores/menu.ts @@ -1,8 +1,24 @@ -import { writable, get } from "svelte/store" +import { writable, get, Writable } from "svelte/store" + +import { Store as StoreContext } from "." import { parseCellID } from "../lib/utils" +interface MenuStoreData { + left: number + top: number + visible: boolean + multiRowMode: boolean + multiCellMode: boolean +} + +interface MenuStore { + menu: Writable +} + +export type Store = MenuStore + export const createStores = () => { - const menu = writable({ + const menu = writable({ left: 0, top: 0, visible: false, @@ -14,7 +30,7 @@ export const createStores = () => { } } -export const createActions = context => { +export const createActions = (context: StoreContext) => { const { menu, focusedCellId, @@ -25,7 +41,7 @@ export const createActions = context => { selectedCellCount, } = context - const open = (cellId, e) => { + const open = (cellId: string, e: MouseEvent) => { e.preventDefault() e.stopPropagation() @@ -37,7 +53,7 @@ export const createActions = context => { } // Compute bounds of cell relative to outer data node - const targetBounds = e.target.getBoundingClientRect() + const targetBounds = (e.target as HTMLElement).getBoundingClientRect() const dataBounds = dataNode.getBoundingClientRect() // Check if there are multiple rows selected, and if this is one of them diff --git a/packages/frontend-core/src/components/grid/stores/users.js b/packages/frontend-core/src/components/grid/stores/users.ts similarity index 56% rename from packages/frontend-core/src/components/grid/stores/users.js rename to packages/frontend-core/src/components/grid/stores/users.ts index 64c1e27835..b3dffbcb1b 100644 --- a/packages/frontend-core/src/components/grid/stores/users.js +++ b/packages/frontend-core/src/components/grid/stores/users.ts @@ -1,11 +1,38 @@ -import { writable, get, derived } from "svelte/store" +import { writable, get, derived, Writable, Readable } from "svelte/store" import { helpers } from "@budibase/shared-core" +import { Store as StoreContext } from "." +import { UIUser } from "@budibase/types" -export const createStores = () => { - const users = writable([]) +interface UIEnrichedUser extends UIUser { + color: string + label: string +} + +interface UsersStore { + users: Writable +} + +interface DerivedUsersStore { + userCellMap: Readable> +} + +interface ActionUserStore { + users: UsersStore["users"] & + Readable & { + actions: { + updateUser: (user: UIUser) => void + removeUser: (sessionId: string) => void + } + } +} + +export type Store = DerivedUsersStore & ActionUserStore + +export const createStores = (): UsersStore => { + const users = writable([]) const enrichedUsers = derived(users, $users => { - return $users.map(user => ({ + return $users.map(user => ({ ...user, color: helpers.getUserColor(user), label: helpers.getUserLabel(user), @@ -20,7 +47,7 @@ export const createStores = () => { } } -export const deriveStores = context => { +export const deriveStores = (context: StoreContext): DerivedUsersStore => { const { users, focusedCellId } = context // Generate a lookup map of cell ID to the user that has it selected, to make @@ -28,7 +55,7 @@ export const deriveStores = context => { const userCellMap = derived( [users, focusedCellId], ([$users, $focusedCellId]) => { - let map = {} + let map: Record = {} $users.forEach(user => { const cellId = user.gridMetadata?.focusedCellId if (cellId && cellId !== $focusedCellId) { @@ -44,10 +71,10 @@ export const deriveStores = context => { } } -export const createActions = context => { +export const createActions = (context: StoreContext): ActionUserStore => { const { users } = context - const updateUser = user => { + const updateUser = (user: UIUser) => { const $users = get(users) if (!$users.some(x => x.sessionId === user.sessionId)) { users.set([...$users, user]) @@ -60,7 +87,7 @@ export const createActions = context => { } } - const removeUser = sessionId => { + const removeUser = (sessionId: string) => { users.update(state => { return state.filter(x => x.sessionId !== sessionId) }) diff --git a/packages/frontend-core/src/components/grid/stores/validation.js b/packages/frontend-core/src/components/grid/stores/validation.ts similarity index 69% rename from packages/frontend-core/src/components/grid/stores/validation.js rename to packages/frontend-core/src/components/grid/stores/validation.ts index 93e67e1d31..32bb1cf978 100644 --- a/packages/frontend-core/src/components/grid/stores/validation.js +++ b/packages/frontend-core/src/components/grid/stores/validation.ts @@ -1,10 +1,21 @@ -import { writable, get, derived } from "svelte/store" +import { writable, get, derived, Writable, Readable } from "svelte/store" +import { Store as StoreContext } from "." import { parseCellID } from "../lib/utils" +interface ValidationStore { + validation: Writable> +} + +interface DerivedValidationStore { + validationRowLookupMap: Readable> +} + +export type Store = ValidationStore & DerivedValidationStore + // Normally we would break out actions into the explicit "createActions" // function, but for validation all these actions are pure so can go into // "createStores" instead to make dependency ordering simpler -export const createStores = () => { +export const createStores = (): ValidationStore => { const validation = writable({}) return { @@ -12,12 +23,12 @@ export const createStores = () => { } } -export const deriveStores = context => { +export const deriveStores = (context: StoreContext): DerivedValidationStore => { const { validation } = context // Derive which rows have errors so that we can use that info later const validationRowLookupMap = derived(validation, $validation => { - let map = {} + const map: Record = {} Object.entries($validation).forEach(([key, error]) => { // Extract row ID from all errored cell IDs if (error) { @@ -36,10 +47,10 @@ export const deriveStores = context => { } } -export const createActions = context => { +export const createActions = (context: StoreContext) => { const { validation, focusedCellId, validationRowLookupMap } = context - const setError = (cellId, error) => { + const setError = (cellId: string | undefined, error: string) => { if (!cellId) { return } @@ -49,11 +60,11 @@ export const createActions = context => { })) } - const rowHasErrors = rowId => { + const rowHasErrors = (rowId: string) => { return get(validationRowLookupMap)[rowId]?.length > 0 } - const focusFirstRowError = rowId => { + const focusFirstRowError = (rowId: string) => { const errorCells = get(validationRowLookupMap)[rowId] const cellId = errorCells?.[0] if (cellId) { @@ -73,7 +84,7 @@ export const createActions = context => { } } -export const initialise = context => { +export const initialise = (context: StoreContext) => { const { validation, previousFocusedRowId, validationRowLookupMap } = context // Remove validation errors when changing rows diff --git a/packages/frontend-core/src/utils/index.js b/packages/frontend-core/src/utils/index.ts similarity index 100% rename from packages/frontend-core/src/utils/index.js rename to packages/frontend-core/src/utils/index.ts diff --git a/packages/frontend-core/src/utils/relatedColumns.js b/packages/frontend-core/src/utils/relatedColumns.js deleted file mode 100644 index 6e7968f70c..0000000000 --- a/packages/frontend-core/src/utils/relatedColumns.js +++ /dev/null @@ -1,103 +0,0 @@ -import { FieldType, RelationshipType } from "@budibase/types" -import { Helpers } from "@budibase/bbui" - -const columnTypeManyTypeOverrides = { - [FieldType.DATETIME]: FieldType.STRING, - [FieldType.BOOLEAN]: FieldType.STRING, - [FieldType.SIGNATURE_SINGLE]: FieldType.ATTACHMENTS, -} - -const columnTypeManyParser = { - [FieldType.DATETIME]: (value, field) => { - function parseDate(value) { - const { timeOnly, dateOnly, ignoreTimezones } = field || {} - const enableTime = !dateOnly - const parsedValue = Helpers.parseDate(value, { - timeOnly, - enableTime, - ignoreTimezones, - }) - const parsed = Helpers.getDateDisplayValue(parsedValue, { - enableTime, - timeOnly, - }) - return parsed - } - - return value.map(v => parseDate(v)) - }, - [FieldType.BOOLEAN]: value => value.map(v => !!v), - [FieldType.BB_REFERENCE_SINGLE]: value => [ - ...new Map(value.map(i => [i._id, i])).values(), - ], - [FieldType.BB_REFERENCE]: value => [ - ...new Map(value.map(i => [i._id, i])).values(), - ], - [FieldType.ARRAY]: value => Array.from(new Set(value)), -} - -export function enrichSchemaWithRelColumns(schema) { - if (!schema) { - return - } - const result = Object.keys(schema).reduce((result, fieldName) => { - const field = schema[fieldName] - result[fieldName] = field - - if (field.visible !== false && field.columns) { - const fromSingle = - field?.relationshipType === RelationshipType.ONE_TO_MANY - - for (const relColumn of Object.keys(field.columns)) { - const relField = field.columns[relColumn] - if (!relField.visible) { - continue - } - const name = `${field.name}.${relColumn}` - result[name] = { - ...relField, - name, - related: { field: fieldName, subField: relColumn }, - cellRenderType: - (!fromSingle && columnTypeManyTypeOverrides[relField.type]) || - relField.type, - } - } - } - return result - }, {}) - - return result -} - -export function getRelatedTableValues(row, field, fromField) { - const fromSingle = - fromField?.relationshipType === RelationshipType.ONE_TO_MANY - - let result = "" - - if (fromSingle) { - result = row[field.related.field]?.[0]?.[field.related.subField] - } else { - const parser = columnTypeManyParser[field.type] || (value => value) - const value = row[field.related.field] - ?.flatMap(r => r[field.related.subField]) - ?.filter(i => i !== undefined && i !== null) - result = parser(value || [], field) - if ( - [ - FieldType.STRING, - FieldType.NUMBER, - FieldType.BIGINT, - FieldType.BOOLEAN, - FieldType.DATETIME, - FieldType.LONGFORM, - FieldType.BARCODEQR, - ].includes(field.type) - ) { - result = result?.join(", ") - } - } - - return result -} diff --git a/packages/frontend-core/src/utils/relatedColumns.ts b/packages/frontend-core/src/utils/relatedColumns.ts new file mode 100644 index 0000000000..e7bd3662d3 --- /dev/null +++ b/packages/frontend-core/src/utils/relatedColumns.ts @@ -0,0 +1,129 @@ +import { Helpers } from "@budibase/bbui" +import { + FieldType, + isRelationshipField, + RelationshipType, + Row, + UIFieldSchema, +} from "@budibase/types" + +const columnTypeManyTypeOverrides: Partial> = { + [FieldType.DATETIME]: FieldType.STRING, + [FieldType.BOOLEAN]: FieldType.STRING, + [FieldType.SIGNATURE_SINGLE]: FieldType.ATTACHMENTS, +} + +const columnTypeManyParser = { + [FieldType.DATETIME]: ( + value: any[], + field: { + timeOnly?: boolean + dateOnly?: boolean + } + ) => { + function parseDate(value: any) { + const { timeOnly, dateOnly } = field || {} + const enableTime = !dateOnly + const parsedValue = Helpers.parseDate(value, { enableTime }) + const parsed = Helpers.getDateDisplayValue(parsedValue, { + enableTime, + timeOnly, + }) + return parsed + } + + return value.map(v => parseDate(v)) + }, + [FieldType.BOOLEAN]: (value: any[]) => value.map(v => !!v), + [FieldType.BB_REFERENCE_SINGLE]: (value: any[]) => [ + ...new Map(value.map(i => [i._id, i])).values(), + ], + [FieldType.BB_REFERENCE]: (value: any[]) => [ + ...new Map(value.map(i => [i._id, i])).values(), + ], + [FieldType.ARRAY]: (value: any[]) => Array.from(new Set(value)), +} + +export function enrichSchemaWithRelColumns( + schema: Record +): Record | undefined { + if (!schema) { + return + } + const result = Object.keys(schema).reduce>( + (result, fieldName) => { + const field = schema[fieldName] + result[fieldName] = field + + if ( + field.visible !== false && + isRelationshipField(field) && + field.columns + ) { + const fromSingle = + field?.relationshipType === RelationshipType.ONE_TO_MANY + + for (const relColumn of Object.keys(field.columns)) { + const relField = field.columns[relColumn] + if (!relField.visible) { + continue + } + const name = `${field.name}.${relColumn}` + result[name] = { + ...relField, + type: relField.type as any, // TODO + name, + related: { field: fieldName, subField: relColumn }, + cellRenderType: + (!fromSingle && columnTypeManyTypeOverrides[relField.type]) || + relField.type, + } + } + } + return result + }, + {} + ) + + return result +} + +export function getRelatedTableValues( + row: Row, + field: UIFieldSchema & { related: { field: string; subField: string } }, + fromField: UIFieldSchema +) { + const fromSingle = + isRelationshipField(fromField) && + fromField?.relationshipType === RelationshipType.ONE_TO_MANY + + let result = "" + + if (fromSingle) { + result = row[field.related.field]?.[0]?.[field.related.subField] + } else { + const parser = + columnTypeManyParser[field.type as keyof typeof columnTypeManyParser] || + ((value: any) => value) + const value = row[field.related.field] + ?.flatMap((r: Row) => r[field.related.subField]) + ?.filter((i: any) => i !== undefined && i !== null) + const parsed = parser(value || [], field as any) + result = parsed as any + if ( + [ + FieldType.STRING, + FieldType.NUMBER, + FieldType.BIGINT, + FieldType.BOOLEAN, + FieldType.DATETIME, + FieldType.LONGFORM, + FieldType.BARCODEQR, + ].includes(field.type) + ) { + result = parsed?.join(", ") + } + } + + return result +} diff --git a/packages/frontend-core/tsconfig.json b/packages/frontend-core/tsconfig.json index 3900034413..8ffb3f19be 100644 --- a/packages/frontend-core/tsconfig.json +++ b/packages/frontend-core/tsconfig.json @@ -1,13 +1,12 @@ { + "extends": "../../tsconfig.build.json", "compilerOptions": { "target": "ESNext", + "module": "preserve", "moduleResolution": "bundler", + "outDir": "./dist", "skipLibCheck": true, - "paths": { - "@budibase/types": ["../types/src"], - "@budibase/shared-core": ["../shared-core/src"], - "@budibase/bbui": ["../bbui/src"] - } + "allowJs": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 33e6e407dc..3058a706c1 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -456,7 +456,7 @@ export function filterAutomation(appId: string, tableId?: string): Automation { icon: "Icon", id: "a", type: AutomationStepType.TRIGGER, - event: "row:save", + event: AutomationEventType.ROW_SAVE, stepId: AutomationTriggerStepId.ROW_SAVED, inputs: { tableId: tableId!, @@ -498,7 +498,7 @@ export function updateRowAutomationWithFilters( icon: "Icon", id: "a", type: AutomationStepType.TRIGGER, - event: "row:update", + event: AutomationEventType.ROW_UPDATE, stepId: AutomationTriggerStepId.ROW_UPDATED, inputs: { tableId }, schema: TRIGGER_DEFINITIONS.ROW_UPDATED.schema, @@ -513,7 +513,7 @@ export function basicAutomationResults( return { automationId, status: AutomationStatus.SUCCESS, - trigger: "trigger", + trigger: "trigger" as any, steps: [ { stepId: AutomationActionStepId.SERVER_LOG, diff --git a/packages/types/src/documents/app/automation/automation.ts b/packages/types/src/documents/app/automation/automation.ts index 71530c7939..d56f0de879 100644 --- a/packages/types/src/documents/app/automation/automation.ts +++ b/packages/types/src/documents/app/automation/automation.ts @@ -148,6 +148,7 @@ export interface Automation extends Document { interface BaseIOStructure { type?: AutomationIOType + subtype?: AutomationIOType customType?: AutomationCustomIOType title?: string description?: string @@ -192,7 +193,7 @@ export enum AutomationStoppedReason { export interface AutomationResults { automationId?: string status?: AutomationStatus - trigger?: any + trigger?: AutomationTrigger steps: { stepId: AutomationTriggerStepId | AutomationActionStepId inputs: { diff --git a/packages/types/src/documents/app/automation/schema.ts b/packages/types/src/documents/app/automation/schema.ts index efdf60a4e2..84bfebf6bf 100644 --- a/packages/types/src/documents/app/automation/schema.ts +++ b/packages/types/src/documents/app/automation/schema.ts @@ -6,6 +6,7 @@ import { AutomationFeature, InputOutputBlock, AutomationTriggerStepId, + AutomationEventType, } from "./automation" import { CollectStepInputs, @@ -142,6 +143,7 @@ export type ActionImplementations = { export interface AutomationStepSchemaBase { name: string stepTitle?: string + event?: AutomationEventType tagline: string icon: string description: string @@ -344,7 +346,7 @@ export interface AutomationTriggerSchema< > extends AutomationStepSchemaBase { id: string type: AutomationStepType.TRIGGER - event?: string + event?: AutomationEventType cronJobId?: string stepId: TTrigger inputs: AutomationTriggerInputs & Record // The record union to be removed once the types are fixed diff --git a/packages/types/src/ui/stores/automations.ts b/packages/types/src/ui/stores/automations.ts new file mode 100644 index 0000000000..7e85ceee38 --- /dev/null +++ b/packages/types/src/ui/stores/automations.ts @@ -0,0 +1,12 @@ +export interface BranchPath { + stepIdx: number + branchIdx: number + branchStepId: string + id: string +} + +export interface BlockDefinitions { + TRIGGER: Record + CREATABLE_TRIGGER: Record + ACTION: Record +} diff --git a/packages/types/src/ui/stores/grid/datasource.ts b/packages/types/src/ui/stores/grid/datasource.ts index d7367352d5..1d9b6740a4 100644 --- a/packages/types/src/ui/stores/grid/datasource.ts +++ b/packages/types/src/ui/stores/grid/datasource.ts @@ -1,5 +1,11 @@ -export interface UIDatasource { +import { UITable, UIView } from "@budibase/types" + +export type UIDatasource = (UITable | UIView) & { type: string - id: string - tableId: string +} + +export interface UIFieldMutation { + visible?: boolean + readonly?: boolean + width?: number } diff --git a/packages/types/src/ui/stores/grid/index.ts b/packages/types/src/ui/stores/grid/index.ts index f6c3472aaa..b6a152ed73 100644 --- a/packages/types/src/ui/stores/grid/index.ts +++ b/packages/types/src/ui/stores/grid/index.ts @@ -1,2 +1,5 @@ export * from "./columns" export * from "./datasource" +export * from "./table" +export * from "./view" +export * from "./user" diff --git a/packages/types/src/ui/stores/grid/table.ts b/packages/types/src/ui/stores/grid/table.ts new file mode 100644 index 0000000000..a5a13d5fa2 --- /dev/null +++ b/packages/types/src/ui/stores/grid/table.ts @@ -0,0 +1,34 @@ +import { + BasicViewFieldMetadata, + FieldSchema, + FieldType, + RelationSchemaField, + SortOrder, + Table, + UISearchFilter, +} from "@budibase/types" + +export interface UITable extends Omit { + name: string + id: string + type: string + tableId: string + primaryDisplay?: string + sort?: { + field: string + order: SortOrder + } + queryUI: UISearchFilter + schema: Record +} + +export type UIFieldSchema = FieldSchema & + BasicViewFieldMetadata & { + related?: { field: string; subField: string } + columns?: Record + cellRenderType?: string + } + +interface UIRelationSchemaField extends RelationSchemaField { + type: FieldType +} diff --git a/packages/types/src/ui/stores/grid/user.ts b/packages/types/src/ui/stores/grid/user.ts new file mode 100644 index 0000000000..b6eb529805 --- /dev/null +++ b/packages/types/src/ui/stores/grid/user.ts @@ -0,0 +1,6 @@ +import { User } from "@budibase/types" + +export interface UIUser extends User { + sessionId: string + gridMetadata?: { focusedCellId?: string } +} diff --git a/packages/types/src/ui/stores/grid/view.ts b/packages/types/src/ui/stores/grid/view.ts new file mode 100644 index 0000000000..f81cc34aaf --- /dev/null +++ b/packages/types/src/ui/stores/grid/view.ts @@ -0,0 +1,6 @@ +import { ViewV2 } from "@budibase/types" +import { UIFieldSchema } from "./table" + +export interface UIView extends ViewV2 { + schema: Record +} diff --git a/packages/types/src/ui/stores/index.ts b/packages/types/src/ui/stores/index.ts index 7a6382c6b0..8dae68862e 100644 --- a/packages/types/src/ui/stores/index.ts +++ b/packages/types/src/ui/stores/index.ts @@ -1,2 +1,3 @@ export * from "./integration" +export * from "./automations" export * from "./grid" diff --git a/yarn.lock b/yarn.lock index a8af49581c..f59f89b87c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5946,6 +5946,11 @@ dependencies: "@types/node" "*" +"@types/shortid@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/shortid/-/shortid-2.2.0.tgz#905990fc4275f77e60ab0cd9f791b91a3d4bff04" + integrity sha512-jBG2FgBxcaSf0h662YloTGA32M8UtNbnTPekUr/eCmWXq0JWQXgNEQ/P5Gf05Cv66QZtE1Ttr83I1AJBPdzCBg== + "@types/ssh2-streams@*": version "0.1.12" resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz#e68795ba2bf01c76b93f9c9809e1f42f0eaaec5f" @@ -18639,16 +18644,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -18740,7 +18736,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -18754,13 +18750,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -20508,7 +20497,7 @@ worker-farm@1.7.0: dependencies: errno "~0.1.7" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -20526,15 +20515,6 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"