diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte index 0aa43f8e04..523f00b351 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte @@ -22,7 +22,7 @@ const dispatch = createEventDispatcher() $: blockRefs = $selectedAutomation?.blockRefs || {} - $: stepNames = automation?.definition.stepNames + $: stepNames = automation?.definition.stepNames || {} $: allSteps = automation?.definition.steps || [] $: automationName = itemName || stepNames?.[block.id] || block?.name || "" $: automationNameError = getAutomationNameError(automationName) @@ -64,7 +64,7 @@ const getAutomationNameError = name => { const duplicateError = "This name already exists, please enter a unique name" - if (stepNames && editing) { + if (editing) { for (const [key, value] of Object.entries(stepNames)) { if (name !== block.name && name === value && key !== block.id) { return duplicateError 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/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/frontend-core/src/components/grid/stores/index.ts b/packages/frontend-core/src/components/grid/stores/index.ts index f2b6c973b9..d0413cb80a 100644 --- a/packages/frontend-core/src/components/grid/stores/index.ts +++ b/packages/frontend-core/src/components/grid/stores/index.ts @@ -60,7 +60,10 @@ export type Store = BaseStore & Table.Store & ViewV2.Store & NonPlus.Store & - Datasource.Store & { + Datasource.Store & + Validation.Store & + Users.Store & + Menu.Store & { // TODO while typing the rest of stores fetch: Writable filter: Writable @@ -76,6 +79,13 @@ export type Store = BaseStore & 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 => { 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/types/src/ui/stores/grid/index.ts b/packages/types/src/ui/stores/grid/index.ts index bcd7e3267d..b6a152ed73 100644 --- a/packages/types/src/ui/stores/grid/index.ts +++ b/packages/types/src/ui/stores/grid/index.ts @@ -2,3 +2,4 @@ export * from "./columns" export * from "./datasource" export * from "./table" export * from "./view" +export * from "./user" 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 } +}