diff --git a/packages/builder/src/stores/builder/users.js b/packages/builder/src/stores/builder/users.js deleted file mode 100644 index 0794c63289..0000000000 --- a/packages/builder/src/stores/builder/users.js +++ /dev/null @@ -1,62 +0,0 @@ -import { writable, get, derived } from "svelte/store" - -export const createUserStore = () => { - const store = writable([]) - - const init = users => { - store.set(users) - } - - const updateUser = user => { - const $users = get(store) - if (!$users.some(x => x.sessionId === user.sessionId)) { - store.set([...$users, user]) - } else { - store.update(state => { - const index = state.findIndex(x => x.sessionId === user.sessionId) - state[index] = user - return state.slice() - }) - } - } - - const removeUser = sessionId => { - store.update(state => { - return state.filter(x => x.sessionId !== sessionId) - }) - } - - const reset = () => { - store.set([]) - } - - return { - ...store, - actions: { - init, - updateUser, - removeUser, - reset, - }, - } -} - -export const userStore = createUserStore() - -export const userSelectedResourceMap = derived(userStore, $userStore => { - let map = {} - $userStore.forEach(user => { - const resource = user.builderMetadata?.selectedResourceId - if (resource) { - if (!map[resource]) { - map[resource] = [] - } - map[resource].push(user) - } - }) - return map -}) - -export const isOnlyUser = derived(userStore, $userStore => { - return $userStore.length < 2 -}) diff --git a/packages/builder/src/stores/builder/users.ts b/packages/builder/src/stores/builder/users.ts new file mode 100644 index 0000000000..5f0c4a3701 --- /dev/null +++ b/packages/builder/src/stores/builder/users.ts @@ -0,0 +1,72 @@ +import { get, derived } from "svelte/store" +import { BudiStore } from "../BudiStore" +import { UIUser } from "@budibase/types" + +export class UserStore extends BudiStore { + actions: { + init: (users: UIUser[]) => void + updateUser: (user: UIUser) => void + removeUser: (sessionId: string) => void + reset: () => void + } + + constructor() { + super([]) + this.actions = { + init: this.init.bind(this), + updateUser: this.updateUser.bind(this), + removeUser: this.removeUser.bind(this), + reset: this.reset.bind(this), + } + } + + init(users: UIUser[]) { + this.store.set(users) + } + + updateUser(user: UIUser) { + const $users = get(this.store) + if (!$users.some(x => x.sessionId === user.sessionId)) { + this.store.set([...$users, user]) + } else { + this.update(state => { + const index = state.findIndex(x => x.sessionId === user.sessionId) + state[index] = user + return state.slice() + }) + } + } + + removeUser(sessionId: string) { + this.update(state => { + return state.filter(x => x.sessionId !== sessionId) + }) + } + + reset() { + this.store.set([]) + } +} + +export const userStore = new UserStore() + +export const userSelectedResourceMap = derived( + userStore, + ($userStore): Record => { + let map: Record = {} + $userStore.forEach(user => { + const resource = user.builderMetadata?.selectedResourceId + if (resource) { + if (!map[resource]) { + map[resource] = [] + } + map[resource].push(user) + } + }) + return map + } +) + +export const isOnlyUser = derived(userStore, $userStore => { + return $userStore.length < 2 +}) diff --git a/packages/builder/src/stores/builder/websocket.js b/packages/builder/src/stores/builder/websocket.js deleted file mode 100644 index 3944c8cba5..0000000000 --- a/packages/builder/src/stores/builder/websocket.js +++ /dev/null @@ -1,95 +0,0 @@ -import { createWebsocket } from "@budibase/frontend-core" -import { - automationStore, - userStore, - appStore, - themeStore, - navigationStore, - deploymentStore, - snippets, - datasources, - tables, - roles, -} from "@/stores/builder" -import { get } from "svelte/store" -import { auth, appsStore } from "@/stores/portal" -import { screenStore } from "./screens" -import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core" -import { notifications } from "@budibase/bbui" - -export const createBuilderWebsocket = appId => { - const socket = createWebsocket("/socket/builder") - - // Built-in events - socket.on("connect", () => { - socket.emit(BuilderSocketEvent.SelectApp, { appId }, ({ users }) => { - userStore.actions.init(users) - }) - }) - socket.on("connect_error", err => { - console.error("Failed to connect to builder websocket:", err.message) - }) - socket.on("disconnect", () => { - userStore.actions.reset() - }) - - // User events - socket.onOther(SocketEvent.UserUpdate, ({ user }) => { - userStore.actions.updateUser(user) - }) - socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => { - userStore.actions.removeUser(sessionId) - }) - socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => { - if (userId === get(auth)?.user?._id) { - appStore.update(state => ({ - ...state, - hasLock: true, - })) - } - }) - - // Data section events - socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => { - tables.replaceTable(id, table) - }) - socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => { - datasources.replaceDatasource(id, datasource) - }) - - // Role events - socket.onOther(BuilderSocketEvent.RoleChange, ({ id, role }) => { - roles.replace(id, role) - }) - - // Design section events - socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => { - screenStore.replace(id, screen) - }) - - // App events - socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => { - appStore.syncMetadata(metadata) - themeStore.syncMetadata(metadata) - navigationStore.syncMetadata(metadata) - snippets.syncMetadata(metadata) - }) - socket.onOther( - BuilderSocketEvent.AppPublishChange, - async ({ user, published }) => { - await appsStore.load() - if (published) { - await deploymentStore.load() - } - const verb = published ? "published" : "unpublished" - notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`) - } - ) - - // Automation events - socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => { - automationStore.actions.replace(id, automation) - }) - - return socket -} diff --git a/packages/builder/src/stores/builder/websocket.ts b/packages/builder/src/stores/builder/websocket.ts new file mode 100644 index 0000000000..49b8067dd8 --- /dev/null +++ b/packages/builder/src/stores/builder/websocket.ts @@ -0,0 +1,124 @@ +import { createWebsocket } from "@budibase/frontend-core" +import { + automationStore, + userStore, + appStore, + themeStore, + navigationStore, + deploymentStore, + snippets, + datasources, + tables, + roles, +} from "@/stores/builder" +import { get } from "svelte/store" +import { auth, appsStore } from "@/stores/portal" +import { screenStore } from "./screens" +import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core" +import { notifications } from "@budibase/bbui" +import { Automation, Datasource, Role, Table, UIUser } from "@budibase/types" + +export const createBuilderWebsocket = (appId: string) => { + const socket = createWebsocket("/socket/builder") + + // Built-in events + socket.on("connect", () => { + socket.emit( + BuilderSocketEvent.SelectApp, + { appId }, + ({ users }: { users: UIUser[] }) => { + userStore.actions.init(users) + } + ) + }) + socket.on("connect_error", err => { + console.error("Failed to connect to builder websocket:", err.message) + }) + socket.on("disconnect", () => { + userStore.actions.reset() + }) + + // User events + socket.onOther(SocketEvent.UserUpdate, ({ user }: { user: UIUser }) => { + userStore.actions.updateUser(user) + }) + socket.onOther( + SocketEvent.UserDisconnect, + ({ sessionId }: { sessionId: string }) => { + userStore.actions.removeUser(sessionId) + } + ) + socket.onOther( + BuilderSocketEvent.LockTransfer, + ({ userId }: { userId: string }) => { + if (userId === get(auth)?.user?._id) { + appStore.update(state => ({ + ...state, + hasLock: true, + })) + } + } + ) + + // Data section events + socket.onOther( + BuilderSocketEvent.TableChange, + ({ id, table }: { id: string; table: Table }) => { + tables.replaceTable(id, table) + } + ) + socket.onOther( + BuilderSocketEvent.DatasourceChange, + ({ id, datasource }: { id: string; datasource: Datasource }) => { + datasources.replaceDatasource(id, datasource) + } + ) + + // Role events + socket.onOther( + BuilderSocketEvent.RoleChange, + ({ id, role }: { id: string; role: Role }) => { + roles.replace(id, role) + } + ) + + // Design section events + socket.onOther( + BuilderSocketEvent.ScreenChange, + ({ id, screen }: { id: string; screen: Screen }) => { + screenStore.replace(id, screen) + } + ) + + // App events + socket.onOther( + BuilderSocketEvent.AppMetadataChange, + ({ metadata }: { metadata: any }) => { + appStore.syncMetadata(metadata) + themeStore.syncMetadata(metadata) + navigationStore.syncMetadata(metadata) + snippets.syncMetadata(metadata) + } + ) + socket.onOther( + BuilderSocketEvent.AppPublishChange, + async ({ user, published }: { user: UIUser; published: boolean }) => { + await appsStore.load() + if (published) { + await deploymentStore.load() + } + const verb = published ? "published" : "unpublished" + notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`) + } + ) + + // Automation events + socket.onOther( + BuilderSocketEvent.AutomationChange, + ({ id, automation }: { id: string; automation: Automation }) => { + automationStore.actions.replace(id, automation) + } + ) + + return socket +} diff --git a/packages/frontend-core/src/utils/websocket.js b/packages/frontend-core/src/utils/websocket.ts similarity index 78% rename from packages/frontend-core/src/utils/websocket.js rename to packages/frontend-core/src/utils/websocket.ts index dee679eaef..475b14176f 100644 --- a/packages/frontend-core/src/utils/websocket.js +++ b/packages/frontend-core/src/utils/websocket.ts @@ -1,12 +1,18 @@ -import { io } from "socket.io-client" +import { io, Socket } from "socket.io-client" import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core" import { APISessionID } from "../api" const DefaultOptions = { heartbeat: true, } +export interface ExtendedSocket extends Socket { + onOther: (event: string, callback: (data: any) => void) => void +} -export const createWebsocket = (path, options = DefaultOptions) => { +export const createWebsocket = ( + path: string, + options = DefaultOptions +): ExtendedSocket => { if (!path) { throw "A websocket path must be provided" } @@ -32,10 +38,10 @@ export const createWebsocket = (path, options = DefaultOptions) => { // Disable polling and rely on websocket only, as HTTP transport // will only work with sticky sessions which we don't have transports: ["websocket"], - }) + }) as ExtendedSocket // Set up a heartbeat that's half of the session TTL - let interval + let interval: NodeJS.Timeout | undefined if (heartbeat) { interval = setInterval(() => { socket.emit(SocketEvent.Heartbeat) @@ -48,7 +54,7 @@ export const createWebsocket = (path, options = DefaultOptions) => { // Helper utility to ignore events that were triggered due to API // changes made by us - socket.onOther = (event, callback) => { + socket.onOther = (event: string, callback: (data: any) => void) => { socket.on(event, data => { if (data?.apiSessionId !== APISessionID) { callback(data) diff --git a/packages/types/src/ui/stores/grid/user.ts b/packages/types/src/ui/stores/grid/user.ts index b6eb529805..9625b0bb80 100644 --- a/packages/types/src/ui/stores/grid/user.ts +++ b/packages/types/src/ui/stores/grid/user.ts @@ -3,4 +3,5 @@ import { User } from "@budibase/types" export interface UIUser extends User { sessionId: string gridMetadata?: { focusedCellId?: string } + builderMetadata?: { selectedResourceId?: string } }