Merge pull request #15313 from Budibase/screen-store-ts-conversion

Screen store TS conversion
This commit is contained in:
deanhannigan 2025-01-27 12:28:22 +00:00 committed by GitHub
commit 3de4cc3e60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 322 additions and 242 deletions

View File

@ -49,7 +49,12 @@ export class ComponentTreeNodesStore extends BudiStore<OpenNodesState> {
// Will ensure all parents of a node are expanded so that it is visible in the tree // Will ensure all parents of a node are expanded so that it is visible in the tree
makeNodeVisible(componentId: string) { makeNodeVisible(componentId: string) {
const selectedScreen: Screen = get(selectedScreenStore) const selectedScreen: Screen | undefined = get(selectedScreenStore)
if (!selectedScreen) {
console.error("Invalid node " + componentId)
return {}
}
const path = findComponentPath(selectedScreen.props, componentId) const path = findComponentPath(selectedScreen.props, componentId)

View File

@ -33,7 +33,16 @@ import { Utils } from "@budibase/frontend-core"
import { Component, FieldType, Screen, Table } from "@budibase/types" import { Component, FieldType, Screen, Table } from "@budibase/types"
import { utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"
interface ComponentDefinition { export interface ComponentState {
components: Record<string, ComponentDefinition>
customComponents: string[]
selectedComponentId?: string
componentToPaste?: Component
settingsCache: Record<string, ComponentSetting[]>
selectedScreenId?: string | null
}
export interface ComponentDefinition {
component: string component: string
name: string name: string
friendlyName?: string friendlyName?: string
@ -41,9 +50,11 @@ interface ComponentDefinition {
settings?: ComponentSetting[] settings?: ComponentSetting[]
features?: Record<string, boolean> features?: Record<string, boolean>
typeSupportPresets?: Record<string, any> typeSupportPresets?: Record<string, any>
legalDirectChildren: string[]
illegalChildren: string[]
} }
interface ComponentSetting { export interface ComponentSetting {
key: string key: string
type: string type: string
section?: string section?: string
@ -54,20 +65,9 @@ interface ComponentSetting {
settings?: ComponentSetting[] settings?: ComponentSetting[]
} }
interface ComponentState {
components: Record<string, ComponentDefinition>
customComponents: string[]
selectedComponentId: string | null
componentToPaste?: Component | null
settingsCache: Record<string, ComponentSetting[]>
selectedScreenId?: string | null
}
export const INITIAL_COMPONENTS_STATE: ComponentState = { export const INITIAL_COMPONENTS_STATE: ComponentState = {
components: {}, components: {},
customComponents: [], customComponents: [],
selectedComponentId: null,
componentToPaste: null,
settingsCache: {}, settingsCache: {},
} }
@ -440,6 +440,11 @@ export class ComponentStore extends BudiStore<ComponentState> {
* @returns * @returns
*/ */
createInstance(componentName: string, presetProps: any, parent: any) { createInstance(componentName: string, presetProps: any, parent: any) {
const screen = get(selectedScreen)
if (!screen) {
throw "A valid screen must be selected"
}
const definition = this.getDefinition(componentName) const definition = this.getDefinition(componentName)
if (!definition) { if (!definition) {
return null return null
@ -461,7 +466,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
// Standard post processing // Standard post processing
this.enrichEmptySettings(instance, { this.enrichEmptySettings(instance, {
parent, parent,
screen: get(selectedScreen), screen,
useDefaultValues: true, useDefaultValues: true,
}) })
@ -481,7 +486,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
// Add step name to form steps // Add step name to form steps
if (componentName.endsWith("/formstep")) { if (componentName.endsWith("/formstep")) {
const parentForm = findClosestMatchingComponent( const parentForm = findClosestMatchingComponent(
get(selectedScreen).props, screen.props,
get(selectedComponent)._id, get(selectedComponent)._id,
(component: Component) => component._component.endsWith("/form") (component: Component) => component._component.endsWith("/form")
) )
@ -541,7 +546,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
// Find the selected component // Find the selected component
let selectedComponentId = state.selectedComponentId let selectedComponentId = state.selectedComponentId
if (selectedComponentId?.startsWith(`${screen._id}-`)) { if (selectedComponentId?.startsWith(`${screen._id}-`)) {
selectedComponentId = screen.props._id || null selectedComponentId = screen.props._id
} }
const currentComponent = findComponent( const currentComponent = findComponent(
screen.props, screen.props,
@ -652,7 +657,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
// Determine the next component to select, and select it before deletion // Determine the next component to select, and select it before deletion
// to avoid an intermediate state of no component selection // to avoid an intermediate state of no component selection
const state = get(this.store) const state = get(this.store)
let nextId: string | null = "" let nextId = ""
if (state.selectedComponentId === component._id) { if (state.selectedComponentId === component._id) {
nextId = this.getNext() nextId = this.getNext()
if (!nextId) { if (!nextId) {
@ -739,7 +744,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
if (!state.componentToPaste) { if (!state.componentToPaste) {
return return
} }
let newComponentId: string | null = "" let newComponentId = ""
// Remove copied component if cutting, regardless if pasting works // Remove copied component if cutting, regardless if pasting works
let componentToPaste = cloneDeep(state.componentToPaste) let componentToPaste = cloneDeep(state.componentToPaste)
@ -841,6 +846,9 @@ export class ComponentStore extends BudiStore<ComponentState> {
const state = get(this.store) const state = get(this.store)
const componentId = state.selectedComponentId const componentId = state.selectedComponentId
const screen = get(selectedScreen) const screen = get(selectedScreen)
if (!screen) {
throw "A valid screen must be selected"
}
const parent = findComponentParent(screen.props, componentId) const parent = findComponentParent(screen.props, componentId)
const index = parent?._children.findIndex( const index = parent?._children.findIndex(
(x: Component) => x._id === componentId (x: Component) => x._id === componentId
@ -890,6 +898,9 @@ export class ComponentStore extends BudiStore<ComponentState> {
const component = get(selectedComponent) const component = get(selectedComponent)
const componentId = component?._id const componentId = component?._id
const screen = get(selectedScreen) const screen = get(selectedScreen)
if (!screen) {
throw "A valid screen must be selected"
}
const parent = findComponentParent(screen.props, componentId) const parent = findComponentParent(screen.props, componentId)
const index = parent?._children.findIndex( const index = parent?._children.findIndex(
(x: Component) => x._id === componentId (x: Component) => x._id === componentId
@ -1156,7 +1167,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
} }
async handleEjectBlock(componentId: string, ejectedDefinition: Component) { async handleEjectBlock(componentId: string, ejectedDefinition: Component) {
let nextSelectedComponentId: string | null = null let nextSelectedComponentId: string | undefined
await screenStore.patch((screen: Screen) => { await screenStore.patch((screen: Screen) => {
const block = findComponent(screen.props, componentId) const block = findComponent(screen.props, componentId)
@ -1192,7 +1203,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
(x: Component) => x._id === componentId (x: Component) => x._id === componentId
) )
parent._children[index] = ejectedDefinition parent._children[index] = ejectedDefinition
nextSelectedComponentId = ejectedDefinition._id ?? null nextSelectedComponentId = ejectedDefinition._id
}, null) }, null)
// Select new root component // Select new root component

View File

@ -3,7 +3,7 @@ import { appStore } from "./app.js"
import { componentStore, selectedComponent } from "./components" import { componentStore, selectedComponent } from "./components"
import { navigationStore } from "./navigation.js" import { navigationStore } from "./navigation.js"
import { themeStore } from "./theme.js" import { themeStore } from "./theme.js"
import { screenStore, selectedScreen, sortedScreens } from "./screens.js" import { screenStore, selectedScreen, sortedScreens } from "./screens"
import { builderStore } from "./builder.js" import { builderStore } from "./builder.js"
import { hoverStore } from "./hover.js" import { hoverStore } from "./hover.js"
import { previewStore } from "./preview.js" import { previewStore } from "./preview.js"

View File

@ -100,6 +100,11 @@ export const screenComponentErrors = derived(
...reduceBy("_id", $queries.list), ...reduceBy("_id", $queries.list),
} }
if (!$selectedScreen) {
// Skip validation if a screen is not selected.
return {}
}
return getInvalidDatasources($selectedScreen, datasources) return getInvalidDatasources($selectedScreen, datasources)
} }
) )

View File

@ -13,15 +13,32 @@ import {
import { createHistoryStore } from "@/stores/builder/history" import { createHistoryStore } from "@/stores/builder/history"
import { API } from "@/api" import { API } from "@/api"
import { BudiStore } from "../BudiStore" import { BudiStore } from "../BudiStore"
import {
FetchAppPackageResponse,
DeleteScreenResponse,
Screen,
Component,
SaveScreenResponse,
} from "@budibase/types"
import { ComponentDefinition } from "./components"
export const INITIAL_SCREENS_STATE = { interface ScreenState {
screens: [], screens: Screen[]
selectedScreenId: null, selectedScreenId?: string
} }
export class ScreenStore extends BudiStore { export const initialScreenState: ScreenState = {
screens: [],
}
// Review the nulls
export class ScreenStore extends BudiStore<ScreenState> {
history: any
delete: any
save: any
constructor() { constructor() {
super(INITIAL_SCREENS_STATE) super(initialScreenState)
// Bind scope // Bind scope
this.select = this.select.bind(this) this.select = this.select.bind(this)
@ -38,14 +55,16 @@ export class ScreenStore extends BudiStore {
this.removeCustomLayout = this.removeCustomLayout.bind(this) this.removeCustomLayout = this.removeCustomLayout.bind(this)
this.history = createHistoryStore({ this.history = createHistoryStore({
getDoc: id => get(this.store).screens?.find(screen => screen._id === id), getDoc: (id: string) =>
get(this.store).screens?.find(screen => screen._id === id),
selectDoc: this.select, selectDoc: this.select,
beforeAction: () => {},
afterAction: () => { afterAction: () => {
// Ensure a valid component is selected // Ensure a valid component is selected
if (!get(selectedComponent)) { if (!get(selectedComponent)) {
this.update(state => ({ this.update(state => ({
...state, ...state,
selectedComponentId: get(this.store).selected?.props._id, selectedComponentId: get(selectedScreen)?.props._id,
})) }))
} }
}, },
@ -59,14 +78,14 @@ export class ScreenStore extends BudiStore {
* Reset entire store back to base config * Reset entire store back to base config
*/ */
reset() { reset() {
this.store.set({ ...INITIAL_SCREENS_STATE }) this.store.set({ ...initialScreenState })
} }
/** /**
* Replace ALL store screens with application package screens * Replace ALL store screens with application package screens
* @param {object} pkg * @param {FetchAppPackageResponse} pkg
*/ */
syncAppScreens(pkg) { syncAppScreens(pkg: FetchAppPackageResponse) {
this.update(state => ({ this.update(state => ({
...state, ...state,
screens: [...pkg.screens], screens: [...pkg.screens],
@ -79,7 +98,7 @@ export class ScreenStore extends BudiStore {
* @param {string} screenId * @param {string} screenId
* @returns * @returns
*/ */
select(screenId) { select(screenId: string) {
// Check this screen exists // Check this screen exists
const state = get(this.store) const state = get(this.store)
const screen = state.screens.find(screen => screen._id === screenId) const screen = state.screens.find(screen => screen._id === screenId)
@ -103,18 +122,18 @@ export class ScreenStore extends BudiStore {
* Recursively parses the entire screen doc and checks for components * Recursively parses the entire screen doc and checks for components
* violating illegal child configurations. * violating illegal child configurations.
* *
* @param {object} screen * @param {Screen} screen
* @throws Will throw an error containing the name of the component causing * @throws Will throw an error containing the name of the component causing
* the invalid screen state * the invalid screen state
*/ */
validate(screen) { validate(screen: Screen) {
// Recursive function to find any illegal children in component trees // Recursive function to find any illegal children in component trees
const findIllegalChild = ( const findIllegalChild = (
component, component: Component,
illegalChildren = [], illegalChildren: string[] = [],
legalDirectChildren = [] legalDirectChildren: string[] = []
) => { ): string | undefined => {
const type = component._component const type: string = component._component
if (illegalChildren.includes(type)) { if (illegalChildren.includes(type)) {
return type return type
@ -137,7 +156,13 @@ export class ScreenStore extends BudiStore {
illegalChildren = [] illegalChildren = []
} }
const definition = componentStore.getDefinition(component._component) const definition: ComponentDefinition | null =
componentStore.getDefinition(component._component)
if (definition == null) {
throw `Invalid defintion ${component._component}`
}
// Reset whitelist for direct children // Reset whitelist for direct children
legalDirectChildren = [] legalDirectChildren = []
if (definition?.legalDirectChildren?.length) { if (definition?.legalDirectChildren?.length) {
@ -172,7 +197,7 @@ export class ScreenStore extends BudiStore {
const illegalChild = findIllegalChild(screen.props) const illegalChild = findIllegalChild(screen.props)
if (illegalChild) { if (illegalChild) {
const def = componentStore.getDefinition(illegalChild) const def = componentStore.getDefinition(illegalChild)
throw `You can't place a ${def.name} here` throw `You can't place a ${def?.name} here`
} }
} }
@ -180,10 +205,9 @@ export class ScreenStore extends BudiStore {
* Core save method. If creating a new screen, the store will sync the target * Core save method. If creating a new screen, the store will sync the target
* screen id to ensure that it is selected in the builder * screen id to ensure that it is selected in the builder
* *
* @param {object} screen * @param {Screen} screen The screen being modified/created
* @returns {object}
*/ */
async saveScreen(screen) { async saveScreen(screen: Screen) {
const appState = get(appStore) const appState = get(appStore)
// Validate screen structure if the app supports it // Validate screen structure if the app supports it
@ -228,9 +252,9 @@ export class ScreenStore extends BudiStore {
/** /**
* After saving a screen, sync plugins and routes to the appStore * After saving a screen, sync plugins and routes to the appStore
* @param {object} savedScreen * @param {Screen} savedScreen
*/ */
async syncScreenData(savedScreen) { async syncScreenData(savedScreen: Screen) {
const appState = get(appStore) const appState = get(appStore)
// If plugins changed we need to fetch the latest app metadata // If plugins changed we need to fetch the latest app metadata
let usedPlugins = appState.usedPlugins let usedPlugins = appState.usedPlugins
@ -256,28 +280,32 @@ export class ScreenStore extends BudiStore {
* This is slightly better than just a traditional "patch" endpoint and this * This is slightly better than just a traditional "patch" endpoint and this
* supports deeply mutating the current doc rather than just appending data. * supports deeply mutating the current doc rather than just appending data.
*/ */
sequentialScreenPatch = Utils.sequential(async (patchFn, screenId) => { sequentialScreenPatch = Utils.sequential(
const state = get(this.store) async (patchFn: (screen: Screen) => any, screenId: string) => {
const screen = state.screens.find(screen => screen._id === screenId) const state = get(this.store)
if (!screen) { const screen = state.screens.find(screen => screen._id === screenId)
return if (!screen) {
} return
let clone = cloneDeep(screen) }
const result = patchFn(clone) let clone = cloneDeep(screen)
const result = patchFn(clone)
// An explicit false result means skip this change // An explicit false result means skip this change
if (result === false) { if (result === false) {
return return
}
return this.save(clone)
} }
return this.save(clone) )
})
/** /**
* @param {function} patchFn * @param {Function} patchFn the patch action to be applied
* @param {string | null} screenId * @param {string | null} screenId
* @returns
*/ */
async patch(patchFn, screenId) { async patch(
patchFn: (screen: Screen) => any,
screenId?: string | null
): Promise<SaveScreenResponse | void> {
// Default to the currently selected screen // Default to the currently selected screen
if (!screenId) { if (!screenId) {
const state = get(this.store) const state = get(this.store)
@ -294,11 +322,11 @@ export class ScreenStore extends BudiStore {
* the screen supplied. If no screen is provided, the target has * the screen supplied. If no screen is provided, the target has
* been removed by another user and will be filtered from the store. * been removed by another user and will be filtered from the store.
* Used to marshal updates for the websocket * Used to marshal updates for the websocket
* @param {string} screenId *
* @param {object} screen * @param {string} screenId the target screen id
* @returns * @param {Screen} screen the replacement screen
*/ */
async replace(screenId, screen) { async replace(screenId: string, screen: Screen) {
if (!screenId) { if (!screenId) {
return return
} }
@ -334,20 +362,27 @@ export class ScreenStore extends BudiStore {
* Any deleted screens will then have their routes/links purged * Any deleted screens will then have their routes/links purged
* *
* Wrapped by {@link delete} * Wrapped by {@link delete}
* @param {object | array} screens * @param {Screen | Screen[]} screens
* @returns
*/ */
async deleteScreen(screens) { async deleteScreen(screens: Screen | Screen[]) {
const screensToDelete = Array.isArray(screens) ? screens : [screens] const screensToDelete = Array.isArray(screens) ? screens : [screens]
// Build array of promises to speed up bulk deletions // Build array of promises to speed up bulk deletions
let promises = [] let promises: Promise<DeleteScreenResponse>[] = []
let deleteUrls = [] let deleteUrls: string[] = []
screensToDelete.forEach(screen => {
// Delete the screen // In this instance _id will have been set
promises.push(API.deleteScreen(screen._id, screen._rev)) // Underline the expectation that _id and _rev will be set after filtering
// Remove links to this screen screensToDelete
deleteUrls.push(screen.routing.route) .filter(
}) (screen): screen is Screen & { _id: string; _rev: string } =>
!!screen._id || !!screen._rev
)
.forEach(screen => {
// Delete the screen
promises.push(API.deleteScreen(screen._id, screen._rev))
// Remove links to this screen
deleteUrls.push(screen.routing.route)
})
await Promise.all(promises) await Promise.all(promises)
await navigationStore.deleteLink(deleteUrls) await navigationStore.deleteLink(deleteUrls)
const deletedIds = screensToDelete.map(screen => screen._id) const deletedIds = screensToDelete.map(screen => screen._id)
@ -359,12 +394,15 @@ export class ScreenStore extends BudiStore {
}) })
// Deselect the current screen if it was deleted // Deselect the current screen if it was deleted
if (deletedIds.includes(state.selectedScreenId)) { if (
state.selectedScreenId = null state.selectedScreenId &&
componentStore.update(state => ({ deletedIds.includes(state.selectedScreenId)
...state, ) {
selectedComponentId: null, delete state.selectedScreenId
})) componentStore.update(state => {
delete state.selectedComponentId
return state
})
} }
// Update routing // Update routing
@ -375,7 +413,6 @@ export class ScreenStore extends BudiStore {
return state return state
}) })
return null
} }
/** /**
@ -384,18 +421,17 @@ export class ScreenStore extends BudiStore {
* After a successful update, this method ensures that there is only * After a successful update, this method ensures that there is only
* ONE home screen per user Role. * ONE home screen per user Role.
* *
* @param {object} screen * @param {Screen} screen
* @param {string} name e.g "routing.homeScreen" or "showNavigation" * @param {string} name e.g "routing.homeScreen" or "showNavigation"
* @param {any} value * @param {any} value
* @returns
*/ */
async updateSetting(screen, name, value) { async updateSetting(screen: Screen, name: string, value: any) {
if (!screen || !name) { if (!screen || !name) {
return return
} }
// Apply setting update // Apply setting update
const patchFn = screen => { const patchFn = (screen: Screen) => {
if (!screen) { if (!screen) {
return false return false
} }
@ -422,7 +458,7 @@ export class ScreenStore extends BudiStore {
) )
}) })
if (otherHomeScreens.length && updatedScreen.routing.homeScreen) { if (otherHomeScreens.length && updatedScreen.routing.homeScreen) {
const patchFn = screen => { const patchFn = (screen: Screen) => {
screen.routing.homeScreen = false screen.routing.homeScreen = false
} }
for (let otherHomeScreen of otherHomeScreens) { for (let otherHomeScreen of otherHomeScreens) {
@ -432,11 +468,11 @@ export class ScreenStore extends BudiStore {
} }
// Move to layouts store // Move to layouts store
async removeCustomLayout(screen) { async removeCustomLayout(screen: Screen) {
// Pull relevant settings from old layout, if required // Pull relevant settings from old layout, if required
const layout = get(layoutStore).layouts.find(x => x._id === screen.layoutId) const layout = get(layoutStore).layouts.find(x => x._id === screen.layoutId)
const patchFn = screen => { const patchFn = (screen: Screen) => {
screen.layoutId = null delete screen.layoutId
screen.showNavigation = layout?.props.navigation !== "None" screen.showNavigation = layout?.props.navigation !== "None"
screen.width = layout?.props.width || "Large" screen.width = layout?.props.width || "Large"
} }
@ -446,11 +482,14 @@ export class ScreenStore extends BudiStore {
/** /**
* Parse the entire screen component tree and ensure settings are valid * Parse the entire screen component tree and ensure settings are valid
* and up-to-date. Ensures stability after a product update. * and up-to-date. Ensures stability after a product update.
* @param {object} screen * @param {Screen} screen
*/ */
async enrichEmptySettings(screen) { async enrichEmptySettings(screen: Screen) {
// Flatten the recursive component tree // Flatten the recursive component tree
const components = findAllMatchingComponents(screen.props, x => x) const components = findAllMatchingComponents(
screen.props,
(x: Component) => x
)
// Iterate over all components and run checks // Iterate over all components and run checks
components.forEach(component => { components.forEach(component => {

View File

@ -3,7 +3,7 @@ import { get, writable } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
import { componentStore, appStore } from "@/stores/builder" import { componentStore, appStore } from "@/stores/builder"
import { INITIAL_SCREENS_STATE, ScreenStore } from "@/stores/builder/screens" import { initialScreenState, ScreenStore } from "@/stores/builder/screens"
import { import {
getScreenFixture, getScreenFixture,
getComponentFixture, getComponentFixture,
@ -73,7 +73,7 @@ describe("Screens store", () => {
vi.clearAllMocks() vi.clearAllMocks()
const screenStore = new ScreenStore() const screenStore = new ScreenStore()
ctx.test = { ctx.bb = {
get store() { get store() {
return get(screenStore) return get(screenStore)
}, },
@ -81,74 +81,76 @@ describe("Screens store", () => {
} }
}) })
it("Create base screen store with defaults", ctx => { it("Create base screen store with defaults", ({ bb }) => {
expect(ctx.test.store).toStrictEqual(INITIAL_SCREENS_STATE) expect(bb.store).toStrictEqual(initialScreenState)
}) })
it("Syncs all screens from the app package", ctx => { it("Syncs all screens from the app package", ({ bb }) => {
expect(ctx.test.store.screens.length).toBe(0) expect(bb.store.screens.length).toBe(0)
const screens = Array(2) const screens = Array(2)
.fill() .fill()
.map(() => getScreenFixture().json()) .map(() => getScreenFixture().json())
ctx.test.screenStore.syncAppScreens({ screens }) bb.screenStore.syncAppScreens({ screens })
expect(ctx.test.store.screens).toStrictEqual(screens) expect(bb.store.screens).toStrictEqual(screens)
}) })
it("Reset the screen store back to the default state", ctx => { it("Reset the screen store back to the default state", ({ bb }) => {
expect(ctx.test.store.screens.length).toBe(0) expect(bb.store.screens.length).toBe(0)
const screens = Array(2) const screens = Array(2)
.fill() .fill()
.map(() => getScreenFixture().json()) .map(() => getScreenFixture().json())
ctx.test.screenStore.syncAppScreens({ screens }) bb.screenStore.syncAppScreens({ screens })
expect(ctx.test.store.screens).toStrictEqual(screens) expect(bb.store.screens).toStrictEqual(screens)
ctx.test.screenStore.update(state => ({ bb.screenStore.update(state => ({
...state, ...state,
selectedScreenId: screens[0]._id, selectedScreenId: screens[0]._id,
})) }))
ctx.test.screenStore.reset() bb.screenStore.reset()
expect(ctx.test.store).toStrictEqual(INITIAL_SCREENS_STATE) expect(bb.store).toStrictEqual(initialScreenState)
}) })
it("Marks a valid screen as selected", ctx => { it("Marks a valid screen as selected", ({ bb }) => {
const screens = Array(2) const screens = Array(2)
.fill() .fill()
.map(() => getScreenFixture().json()) .map(() => getScreenFixture().json())
ctx.test.screenStore.syncAppScreens({ screens }) bb.screenStore.syncAppScreens({ screens })
expect(ctx.test.store.screens.length).toBe(2) expect(bb.store.screens.length).toBe(2)
ctx.test.screenStore.select(screens[0]._id) bb.screenStore.select(screens[0]._id)
expect(ctx.test.store.selectedScreenId).toEqual(screens[0]._id) expect(bb.store.selectedScreenId).toEqual(screens[0]._id)
}) })
it("Skip selecting a screen if it is not present", ctx => { it("Skip selecting a screen if it is not present", ({ bb }) => {
const screens = Array(2) const screens = Array(2)
.fill() .fill()
.map(() => getScreenFixture().json()) .map(() => getScreenFixture().json())
ctx.test.screenStore.syncAppScreens({ screens }) bb.screenStore.syncAppScreens({ screens })
expect(ctx.test.store.screens.length).toBe(2) expect(bb.store.screens.length).toBe(2)
ctx.test.screenStore.select("screen_abc") bb.screenStore.select("screen_abc")
expect(ctx.test.store.selectedScreenId).toBeNull() expect(bb.store.selectedScreenId).toBeUndefined()
}) })
it("Approve a valid empty screen config", ctx => { it("Approve a valid empty screen config", ({ bb }) => {
const coreScreen = getScreenFixture() const coreScreen = getScreenFixture()
ctx.test.screenStore.validate(coreScreen.json()) bb.screenStore.validate(coreScreen.json())
}) })
it("Approve a valid screen config with one component and no illegal children", ctx => { it("Approve a valid screen config with one component and no illegal children", ({
bb,
}) => {
const coreScreen = getScreenFixture() const coreScreen = getScreenFixture()
const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`) const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`)
@ -157,12 +159,12 @@ describe("Screens store", () => {
const defSpy = vi.spyOn(componentStore, "getDefinition") const defSpy = vi.spyOn(componentStore, "getDefinition")
defSpy.mockReturnValueOnce(COMPONENT_DEFINITIONS.formblock) defSpy.mockReturnValueOnce(COMPONENT_DEFINITIONS.formblock)
ctx.test.screenStore.validate(coreScreen.json()) bb.screenStore.validate(coreScreen.json())
expect(defSpy).toHaveBeenCalled() expect(defSpy).toHaveBeenCalled()
}) })
it("Reject an attempt to nest invalid components", ctx => { it("Reject an attempt to nest invalid components", ({ bb }) => {
const coreScreen = getScreenFixture() const coreScreen = getScreenFixture()
const formOne = getComponentFixture(`${COMP_PREFIX}/form`) const formOne = getComponentFixture(`${COMP_PREFIX}/form`)
@ -178,14 +180,14 @@ describe("Screens store", () => {
return defMap[comp] return defMap[comp]
}) })
expect(() => ctx.test.screenStore.validate(coreScreen.json())).toThrowError( expect(() => bb.screenStore.validate(coreScreen.json())).toThrowError(
`You can't place a ${COMPONENT_DEFINITIONS.form.name} here` `You can't place a ${COMPONENT_DEFINITIONS.form.name} here`
) )
expect(defSpy).toHaveBeenCalled() expect(defSpy).toHaveBeenCalled()
}) })
it("Reject an attempt to deeply nest invalid components", ctx => { it("Reject an attempt to deeply nest invalid components", ({ bb }) => {
const coreScreen = getScreenFixture() const coreScreen = getScreenFixture()
const formOne = getComponentFixture(`${COMP_PREFIX}/form`) const formOne = getComponentFixture(`${COMP_PREFIX}/form`)
@ -210,14 +212,16 @@ describe("Screens store", () => {
return defMap[comp] return defMap[comp]
}) })
expect(() => ctx.test.screenStore.validate(coreScreen.json())).toThrowError( expect(() => bb.screenStore.validate(coreScreen.json())).toThrowError(
`You can't place a ${COMPONENT_DEFINITIONS.form.name} here` `You can't place a ${COMPONENT_DEFINITIONS.form.name} here`
) )
expect(defSpy).toHaveBeenCalled() expect(defSpy).toHaveBeenCalled()
}) })
it("Save a brand new screen and add it to the store. No validation", async ctx => { it("Save a brand new screen and add it to the store. No validation", async ({
bb,
}) => {
const coreScreen = getScreenFixture() const coreScreen = getScreenFixture()
const formOne = getComponentFixture(`${COMP_PREFIX}/form`) const formOne = getComponentFixture(`${COMP_PREFIX}/form`)
@ -225,7 +229,7 @@ describe("Screens store", () => {
appStore.set({ features: { componentValidation: false } }) appStore.set({ features: { componentValidation: false } })
expect(ctx.test.store.screens.length).toBe(0) expect(bb.store.screens.length).toBe(0)
const newDocId = getScreenDocId() const newDocId = getScreenDocId()
const newDoc = { ...coreScreen.json(), _id: newDocId } const newDoc = { ...coreScreen.json(), _id: newDocId }
@ -235,15 +239,15 @@ describe("Screens store", () => {
vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({ vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({
routes: [], routes: [],
}) })
await ctx.test.screenStore.save(coreScreen.json()) await bb.screenStore.save(coreScreen.json())
expect(saveSpy).toHaveBeenCalled() expect(saveSpy).toHaveBeenCalled()
expect(ctx.test.store.screens.length).toBe(1) expect(bb.store.screens.length).toBe(1)
expect(ctx.test.store.screens[0]).toStrictEqual(newDoc) expect(bb.store.screens[0]).toStrictEqual(newDoc)
expect(ctx.test.store.selectedScreenId).toBe(newDocId) expect(bb.store.selectedScreenId).toBe(newDocId)
// The new screen should be selected // The new screen should be selected
expect(get(componentStore).selectedComponentId).toBe( expect(get(componentStore).selectedComponentId).toBe(
@ -251,7 +255,7 @@ describe("Screens store", () => {
) )
}) })
it("Sync an updated screen to the screen store on save", async ctx => { it("Sync an updated screen to the screen store on save", async ({ bb }) => {
const existingScreens = Array(4) const existingScreens = Array(4)
.fill() .fill()
.map(() => { .map(() => {
@ -261,7 +265,7 @@ describe("Screens store", () => {
return screenDoc return screenDoc
}) })
ctx.test.screenStore.update(state => ({ bb.screenStore.update(state => ({
...state, ...state,
screens: existingScreens.map(screen => screen.json()), screens: existingScreens.map(screen => screen.json()),
})) }))
@ -279,16 +283,18 @@ describe("Screens store", () => {
}) })
// Saved the existing screen having modified it. // Saved the existing screen having modified it.
await ctx.test.screenStore.save(existingScreens[2].json()) await bb.screenStore.save(existingScreens[2].json())
expect(routeSpy).toHaveBeenCalled() expect(routeSpy).toHaveBeenCalled()
expect(saveSpy).toHaveBeenCalled() expect(saveSpy).toHaveBeenCalled()
// On save, the screen is spliced back into the store with the saved content // On save, the screen is spliced back into the store with the saved content
expect(ctx.test.store.screens[2]).toStrictEqual(existingScreens[2].json()) expect(bb.store.screens[2]).toStrictEqual(existingScreens[2].json())
}) })
it("Sync API data to relevant stores on save. Updated plugins", async ctx => { it("Sync API data to relevant stores on save. Updated plugins", async ({
bb,
}) => {
const coreScreen = getScreenFixture() const coreScreen = getScreenFixture()
const newDocId = getScreenDocId() const newDocId = getScreenDocId()
@ -318,7 +324,7 @@ describe("Screens store", () => {
routes: [], routes: [],
}) })
await ctx.test.screenStore.syncScreenData(newDoc) await bb.screenStore.syncScreenData(newDoc)
expect(routeSpy).toHaveBeenCalled() expect(routeSpy).toHaveBeenCalled()
expect(appPackageSpy).toHaveBeenCalled() expect(appPackageSpy).toHaveBeenCalled()
@ -326,7 +332,9 @@ describe("Screens store", () => {
expect(get(appStore).usedPlugins).toStrictEqual(plugins) expect(get(appStore).usedPlugins).toStrictEqual(plugins)
}) })
it("Sync API updates to relevant stores on save. Plugins unchanged", async ctx => { it("Sync API updates to relevant stores on save. Plugins unchanged", async ({
bb,
}) => {
const coreScreen = getScreenFixture() const coreScreen = getScreenFixture()
const newDocId = getScreenDocId() const newDocId = getScreenDocId()
@ -343,7 +351,7 @@ describe("Screens store", () => {
routes: [], routes: [],
}) })
await ctx.test.screenStore.syncScreenData(newDoc) await bb.screenStore.syncScreenData(newDoc)
expect(routeSpy).toHaveBeenCalled() expect(routeSpy).toHaveBeenCalled()
expect(appPackageSpy).not.toHaveBeenCalled() expect(appPackageSpy).not.toHaveBeenCalled()
@ -352,46 +360,48 @@ describe("Screens store", () => {
expect(get(appStore).usedPlugins).toStrictEqual([plugin]) expect(get(appStore).usedPlugins).toStrictEqual([plugin])
}) })
it("Proceed to patch if appropriate config are supplied", async ctx => { it("Proceed to patch if appropriate config are supplied", async ({ bb }) => {
vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch").mockImplementation( vi.spyOn(bb.screenStore, "sequentialScreenPatch").mockImplementation(() => {
() => { return false
return false })
}
)
const noop = () => {} const noop = () => {}
await ctx.test.screenStore.patch(noop, "test") await bb.screenStore.patch(noop, "test")
expect(ctx.test.screenStore.sequentialScreenPatch).toHaveBeenCalledWith( expect(bb.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
noop, noop,
"test" "test"
) )
}) })
it("Return from the patch if all valid config are not present", async ctx => { it("Return from the patch if all valid config are not present", async ({
vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch") bb,
await ctx.test.screenStore.patch() }) => {
expect(ctx.test.screenStore.sequentialScreenPatch).not.toBeCalled() vi.spyOn(bb.screenStore, "sequentialScreenPatch")
await bb.screenStore.patch()
expect(bb.screenStore.sequentialScreenPatch).not.toBeCalled()
}) })
it("Acquire the currently selected screen on patch, if not specified", async ctx => { it("Acquire the currently selected screen on patch, if not specified", async ({
vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch") bb,
await ctx.test.screenStore.patch() }) => {
vi.spyOn(bb.screenStore, "sequentialScreenPatch")
await bb.screenStore.patch()
const noop = () => {} const noop = () => {}
ctx.test.screenStore.update(state => ({ bb.screenStore.update(state => ({
...state, ...state,
selectedScreenId: "screen_123", selectedScreenId: "screen_123",
})) }))
await ctx.test.screenStore.patch(noop) await bb.screenStore.patch(noop)
expect(ctx.test.screenStore.sequentialScreenPatch).toHaveBeenCalledWith( expect(bb.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
noop, noop,
"screen_123" "screen_123"
) )
}) })
// Used by the websocket // Used by the websocket
it("Ignore a call to replace if no screenId is provided", ctx => { it("Ignore a call to replace if no screenId is provided", ({ bb }) => {
const existingScreens = Array(4) const existingScreens = Array(4)
.fill() .fill()
.map(() => { .map(() => {
@ -400,14 +410,16 @@ describe("Screens store", () => {
screenDoc._json._id = existingDocId screenDoc._json._id = existingDocId
return screenDoc.json() return screenDoc.json()
}) })
ctx.test.screenStore.syncAppScreens({ screens: existingScreens }) bb.screenStore.syncAppScreens({ screens: existingScreens })
ctx.test.screenStore.replace() bb.screenStore.replace()
expect(ctx.test.store.screens).toStrictEqual(existingScreens) expect(bb.store.screens).toStrictEqual(existingScreens)
}) })
it("Remove a screen from the store if a single screenId is supplied", ctx => { it("Remove a screen from the store if a single screenId is supplied", ({
bb,
}) => {
const existingScreens = Array(4) const existingScreens = Array(4)
.fill() .fill()
.map(() => { .map(() => {
@ -416,17 +428,17 @@ describe("Screens store", () => {
screenDoc._json._id = existingDocId screenDoc._json._id = existingDocId
return screenDoc.json() return screenDoc.json()
}) })
ctx.test.screenStore.syncAppScreens({ screens: existingScreens }) bb.screenStore.syncAppScreens({ screens: existingScreens })
ctx.test.screenStore.replace(existingScreens[1]._id) bb.screenStore.replace(existingScreens[1]._id)
const filtered = existingScreens.filter( const filtered = existingScreens.filter(
screen => screen._id != existingScreens[1]._id screen => screen._id != existingScreens[1]._id
) )
expect(ctx.test.store.screens).toStrictEqual(filtered) expect(bb.store.screens).toStrictEqual(filtered)
}) })
it("Replace an existing screen with a new version of itself", ctx => { it("Replace an existing screen with a new version of itself", ({ bb }) => {
const existingScreens = Array(4) const existingScreens = Array(4)
.fill() .fill()
.map(() => { .map(() => {
@ -436,7 +448,7 @@ describe("Screens store", () => {
return screenDoc return screenDoc
}) })
ctx.test.screenStore.update(state => ({ bb.screenStore.update(state => ({
...state, ...state,
screens: existingScreens.map(screen => screen.json()), screens: existingScreens.map(screen => screen.json()),
})) }))
@ -444,15 +456,14 @@ describe("Screens store", () => {
const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`) const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`)
existingScreens[2].addChild(formBlock) existingScreens[2].addChild(formBlock)
ctx.test.screenStore.replace( bb.screenStore.replace(existingScreens[2]._id, existingScreens[2].json())
existingScreens[2]._id,
existingScreens[2].json()
)
expect(ctx.test.store.screens.length).toBe(4) expect(bb.store.screens.length).toBe(4)
}) })
it("Add a screen when attempting to replace one not present in the store", ctx => { it("Add a screen when attempting to replace one not present in the store", ({
bb,
}) => {
const existingScreens = Array(4) const existingScreens = Array(4)
.fill() .fill()
.map(() => { .map(() => {
@ -462,7 +473,7 @@ describe("Screens store", () => {
return screenDoc return screenDoc
}) })
ctx.test.screenStore.update(state => ({ bb.screenStore.update(state => ({
...state, ...state,
screens: existingScreens.map(screen => screen.json()), screens: existingScreens.map(screen => screen.json()),
})) }))
@ -470,13 +481,13 @@ describe("Screens store", () => {
const newScreenDoc = getScreenFixture() const newScreenDoc = getScreenFixture()
newScreenDoc._json._id = getScreenDocId() newScreenDoc._json._id = getScreenDocId()
ctx.test.screenStore.replace(newScreenDoc._json._id, newScreenDoc.json()) bb.screenStore.replace(newScreenDoc._json._id, newScreenDoc.json())
expect(ctx.test.store.screens.length).toBe(5) expect(bb.store.screens.length).toBe(5)
expect(ctx.test.store.screens[4]).toStrictEqual(newScreenDoc.json()) expect(bb.store.screens[4]).toStrictEqual(newScreenDoc.json())
}) })
it("Delete a single screen and remove it from the store", async ctx => { it("Delete a single screen and remove it from the store", async ({ bb }) => {
const existingScreens = Array(3) const existingScreens = Array(3)
.fill() .fill()
.map(() => { .map(() => {
@ -486,14 +497,14 @@ describe("Screens store", () => {
return screenDoc return screenDoc
}) })
ctx.test.screenStore.update(state => ({ bb.screenStore.update(state => ({
...state, ...state,
screens: existingScreens.map(screen => screen.json()), screens: existingScreens.map(screen => screen.json()),
})) }))
const deleteSpy = vi.spyOn(API, "deleteScreen") const deleteSpy = vi.spyOn(API, "deleteScreen")
await ctx.test.screenStore.delete(existingScreens[2].json()) await bb.screenStore.delete(existingScreens[2].json())
vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({ vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({
routes: [], routes: [],
@ -501,13 +512,15 @@ describe("Screens store", () => {
expect(deleteSpy).toBeCalled() expect(deleteSpy).toBeCalled()
expect(ctx.test.store.screens.length).toBe(2) expect(bb.store.screens.length).toBe(2)
// Just confirm that the routes at are being initialised // Just confirm that the routes at are being initialised
expect(get(appStore).routes).toEqual([]) expect(get(appStore).routes).toEqual([])
}) })
it("Upon delete, reset selected screen and component ids if the screen was selected", async ctx => { it("Upon delete, reset selected screen and component ids if the screen was selected", async ({
bb,
}) => {
const existingScreens = Array(3) const existingScreens = Array(3)
.fill() .fill()
.map(() => { .map(() => {
@ -517,7 +530,7 @@ describe("Screens store", () => {
return screenDoc return screenDoc
}) })
ctx.test.screenStore.update(state => ({ bb.screenStore.update(state => ({
...state, ...state,
screens: existingScreens.map(screen => screen.json()), screens: existingScreens.map(screen => screen.json()),
selectedScreenId: existingScreens[2]._json._id, selectedScreenId: existingScreens[2]._json._id,
@ -528,14 +541,16 @@ describe("Screens store", () => {
selectedComponentId: existingScreens[2]._json._id, selectedComponentId: existingScreens[2]._json._id,
})) }))
await ctx.test.screenStore.delete(existingScreens[2].json()) await bb.screenStore.delete(existingScreens[2].json())
expect(ctx.test.store.screens.length).toBe(2) expect(bb.store.screens.length).toBe(2)
expect(get(componentStore).selectedComponentId).toBeNull() expect(get(componentStore).selectedComponentId).toBeUndefined()
expect(ctx.test.store.selectedScreenId).toBeNull() expect(bb.store.selectedScreenId).toBeUndefined()
}) })
it("Delete multiple is not supported and should leave the store unchanged", async ctx => { it("Delete multiple is not supported and should leave the store unchanged", async ({
bb,
}) => {
const existingScreens = Array(3) const existingScreens = Array(3)
.fill() .fill()
.map(() => { .map(() => {
@ -547,7 +562,7 @@ describe("Screens store", () => {
const storeScreens = existingScreens.map(screen => screen.json()) const storeScreens = existingScreens.map(screen => screen.json())
ctx.test.screenStore.update(state => ({ bb.screenStore.update(state => ({
...state, ...state,
screens: existingScreens.map(screen => screen.json()), screens: existingScreens.map(screen => screen.json()),
})) }))
@ -556,42 +571,40 @@ describe("Screens store", () => {
const deleteSpy = vi.spyOn(API, "deleteScreen") const deleteSpy = vi.spyOn(API, "deleteScreen")
await ctx.test.screenStore.delete(targets) await bb.screenStore.delete(targets)
expect(deleteSpy).not.toHaveBeenCalled() expect(deleteSpy).not.toHaveBeenCalled()
expect(ctx.test.store.screens.length).toBe(3) expect(bb.store.screens.length).toBe(3)
expect(ctx.test.store.screens).toStrictEqual(storeScreens) expect(bb.store.screens).toStrictEqual(storeScreens)
}) })
it("Update a screen setting", async ctx => { it("Update a screen setting", async ({ bb }) => {
const screenDoc = getScreenFixture() const screenDoc = getScreenFixture()
const existingDocId = getScreenDocId() const existingDocId = getScreenDocId()
screenDoc._json._id = existingDocId screenDoc._json._id = existingDocId
await ctx.test.screenStore.update(state => ({ await bb.screenStore.update(state => ({
...state, ...state,
screens: [screenDoc.json()], screens: [screenDoc.json()],
})) }))
const patchedDoc = screenDoc.json() const patchedDoc = screenDoc.json()
const patchSpy = vi const patchSpy = vi
.spyOn(ctx.test.screenStore, "patch") .spyOn(bb.screenStore, "patch")
.mockImplementation(async patchFn => { .mockImplementation(async patchFn => {
patchFn(patchedDoc) patchFn(patchedDoc)
return return
}) })
await ctx.test.screenStore.updateSetting( await bb.screenStore.updateSetting(patchedDoc, "showNavigation", false)
patchedDoc,
"showNavigation",
false
)
expect(patchSpy).toBeCalled() expect(patchSpy).toBeCalled()
expect(patchedDoc.showNavigation).toBe(false) expect(patchedDoc.showNavigation).toBe(false)
}) })
it("Ensure only one homescreen per role after updating setting. All screens same role", async ctx => { it("Ensure only one homescreen per role after updating setting. All screens same role", async ({
bb,
}) => {
const existingScreens = Array(3) const existingScreens = Array(3)
.fill() .fill()
.map(() => { .map(() => {
@ -611,23 +624,21 @@ describe("Screens store", () => {
// Set the 2nd screen as the home screen // Set the 2nd screen as the home screen
storeScreens[1].routing.homeScreen = true storeScreens[1].routing.homeScreen = true
await ctx.test.screenStore.update(state => ({ await bb.screenStore.update(state => ({
...state, ...state,
screens: storeScreens, screens: storeScreens,
})) }))
const patchSpy = vi const patchSpy = vi
.spyOn(ctx.test.screenStore, "patch") .spyOn(bb.screenStore, "patch")
.mockImplementation(async (patchFn, screenId) => { .mockImplementation(async (patchFn, screenId) => {
const target = ctx.test.store.screens.find( const target = bb.store.screens.find(screen => screen._id === screenId)
screen => screen._id === screenId
)
patchFn(target) patchFn(target)
await ctx.test.screenStore.replace(screenId, target) await bb.screenStore.replace(screenId, target)
}) })
await ctx.test.screenStore.updateSetting( await bb.screenStore.updateSetting(
storeScreens[0], storeScreens[0],
"routing.homeScreen", "routing.homeScreen",
true true
@ -637,13 +648,15 @@ describe("Screens store", () => {
expect(patchSpy).toBeCalledTimes(2) expect(patchSpy).toBeCalledTimes(2)
// The new homescreen for BASIC // The new homescreen for BASIC
expect(ctx.test.store.screens[0].routing.homeScreen).toBe(true) expect(bb.store.screens[0].routing.homeScreen).toBe(true)
// The previous home screen for the BASIC role is now unset // The previous home screen for the BASIC role is now unset
expect(ctx.test.store.screens[1].routing.homeScreen).toBe(false) expect(bb.store.screens[1].routing.homeScreen).toBe(false)
}) })
it("Ensure only one homescreen per role when updating screen setting. Multiple screen roles", async ctx => { it("Ensure only one homescreen per role when updating screen setting. Multiple screen roles", async ({
bb,
}) => {
const expectedRoles = [ const expectedRoles = [
Constants.Roles.BASIC, Constants.Roles.BASIC,
Constants.Roles.POWER, Constants.Roles.POWER,
@ -675,30 +688,24 @@ describe("Screens store", () => {
sorted[9].routing.homeScreen = true sorted[9].routing.homeScreen = true
// Set screens state // Set screens state
await ctx.test.screenStore.update(state => ({ await bb.screenStore.update(state => ({
...state, ...state,
screens: sorted, screens: sorted,
})) }))
const patchSpy = vi const patchSpy = vi
.spyOn(ctx.test.screenStore, "patch") .spyOn(bb.screenStore, "patch")
.mockImplementation(async (patchFn, screenId) => { .mockImplementation(async (patchFn, screenId) => {
const target = ctx.test.store.screens.find( const target = bb.store.screens.find(screen => screen._id === screenId)
screen => screen._id === screenId
)
patchFn(target) patchFn(target)
await ctx.test.screenStore.replace(screenId, target) await bb.screenStore.replace(screenId, target)
}) })
// ADMIN homeScreen updated from 0 to 2 // ADMIN homeScreen updated from 0 to 2
await ctx.test.screenStore.updateSetting( await bb.screenStore.updateSetting(sorted[2], "routing.homeScreen", true)
sorted[2],
"routing.homeScreen",
true
)
const results = ctx.test.store.screens.reduce((acc, screen) => { const results = bb.store.screens.reduce((acc, screen) => {
if (screen.routing.homeScreen) { if (screen.routing.homeScreen) {
acc[screen.routing.roleId] = acc[screen.routing.roleId] || [] acc[screen.routing.roleId] = acc[screen.routing.roleId] || []
acc[screen.routing.roleId].push(screen) acc[screen.routing.roleId].push(screen)
@ -706,7 +713,7 @@ describe("Screens store", () => {
return acc return acc
}, {}) }, {})
const screens = ctx.test.store.screens const screens = bb.store.screens
// Should still only be one of each homescreen // Should still only be one of each homescreen
expect(results[Constants.Roles.ADMIN].length).toBe(1) expect(results[Constants.Roles.ADMIN].length).toBe(1)
expect(screens[2].routing.homeScreen).toBe(true) expect(screens[2].routing.homeScreen).toBe(true)
@ -724,74 +731,80 @@ describe("Screens store", () => {
expect(patchSpy).toBeCalledTimes(2) expect(patchSpy).toBeCalledTimes(2)
}) })
it("Sequential patch check. Exit if the screenId is not valid.", async ctx => { it("Sequential patch check. Exit if the screenId is not valid.", async ({
bb,
}) => {
const screenDoc = getScreenFixture() const screenDoc = getScreenFixture()
const existingDocId = getScreenDocId() const existingDocId = getScreenDocId()
screenDoc._json._id = existingDocId screenDoc._json._id = existingDocId
const original = screenDoc.json() const original = screenDoc.json()
await ctx.test.screenStore.update(state => ({ await bb.screenStore.update(state => ({
...state, ...state,
screens: [original], screens: [original],
})) }))
const saveSpy = vi const saveSpy = vi
.spyOn(ctx.test.screenStore, "save") .spyOn(bb.screenStore, "save")
.mockImplementation(async () => { .mockImplementation(async () => {
return return
}) })
// A screen with this Id does not exist // A screen with this Id does not exist
await ctx.test.screenStore.sequentialScreenPatch(() => {}, "123") await bb.screenStore.sequentialScreenPatch(() => {}, "123")
expect(saveSpy).not.toBeCalled() expect(saveSpy).not.toBeCalled()
}) })
it("Sequential patch check. Exit if the patchFn result is false", async ctx => { it("Sequential patch check. Exit if the patchFn result is false", async ({
bb,
}) => {
const screenDoc = getScreenFixture() const screenDoc = getScreenFixture()
const existingDocId = getScreenDocId() const existingDocId = getScreenDocId()
screenDoc._json._id = existingDocId screenDoc._json._id = existingDocId
const original = screenDoc.json() const original = screenDoc.json()
// Set screens state // Set screens state
await ctx.test.screenStore.update(state => ({ await bb.screenStore.update(state => ({
...state, ...state,
screens: [original], screens: [original],
})) }))
const saveSpy = vi const saveSpy = vi
.spyOn(ctx.test.screenStore, "save") .spyOn(bb.screenStore, "save")
.mockImplementation(async () => { .mockImplementation(async () => {
return return
}) })
// Returning false from the patch will abort the save // Returning false from the patch will abort the save
await ctx.test.screenStore.sequentialScreenPatch(() => { await bb.screenStore.sequentialScreenPatch(() => {
return false return false
}, "123") }, "123")
expect(saveSpy).not.toBeCalled() expect(saveSpy).not.toBeCalled()
}) })
it("Sequential patch check. Patch applied and save requested", async ctx => { it("Sequential patch check. Patch applied and save requested", async ({
bb,
}) => {
const screenDoc = getScreenFixture() const screenDoc = getScreenFixture()
const existingDocId = getScreenDocId() const existingDocId = getScreenDocId()
screenDoc._json._id = existingDocId screenDoc._json._id = existingDocId
const original = screenDoc.json() const original = screenDoc.json()
await ctx.test.screenStore.update(state => ({ await bb.screenStore.update(state => ({
...state, ...state,
screens: [original], screens: [original],
})) }))
const saveSpy = vi const saveSpy = vi
.spyOn(ctx.test.screenStore, "save") .spyOn(bb.screenStore, "save")
.mockImplementation(async () => { .mockImplementation(async () => {
return return
}) })
await ctx.test.screenStore.sequentialScreenPatch(screen => { await bb.screenStore.sequentialScreenPatch(screen => {
screen.name = "updated" screen.name = "updated"
}, existingDocId) }, existingDocId)

View File

@ -16,7 +16,14 @@ import { auth, appsStore } from "@/stores/portal"
import { screenStore } from "./screens" import { screenStore } from "./screens"
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core" import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { Automation, Datasource, Role, Table, UIUser } from "@budibase/types" import {
Automation,
Datasource,
Role,
Table,
UIUser,
Screen,
} from "@budibase/types"
export const createBuilderWebsocket = (appId: string) => { export const createBuilderWebsocket = (appId: string) => {
const socket = createWebsocket("/socket/builder") const socket = createWebsocket("/socket/builder")

View File

@ -8,7 +8,7 @@ export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
* Utility to wrap an async function and ensure all invocations happen * Utility to wrap an async function and ensure all invocations happen
* sequentially. * sequentially.
* @param fn the async function to run * @param fn the async function to run
* @return {Promise} a sequential version of the function * @return {Function} a sequential version of the function
*/ */
export const sequential = fn => { export const sequential = fn => {
let queue = [] let queue = []