Merge branch 'master' into type-portal-email-store

This commit is contained in:
Andrew Kingston 2025-01-06 10:01:43 +00:00 committed by GitHub
commit 69b950937b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 347 additions and 281 deletions

View File

@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.2.29",
"version": "3.2.32",
"npmClient": "yarn",
"concurrency": 20,
"command": {

View File

@ -291,8 +291,8 @@ const automationActions = (store: AutomationStore) => ({
let result: (AutomationStep | AutomationTrigger)[] = []
pathWay.forEach(path => {
const { stepIdx, branchIdx } = path
let last = result ? result[result.length - 1] : []
if (!result) {
let last = result.length ? result[result.length - 1] : []
if (!result.length) {
// Preceeding steps.
result = steps.slice(0, stepIdx + 1)
return

View File

@ -1,27 +0,0 @@
import { writable } from "svelte/store"
import { API } from "@/api"
export function createFlagsStore() {
const { subscribe, set } = writable({})
const actions = {
fetch: async () => {
const flags = await API.getFlags()
set(flags)
},
updateFlag: async (flag, value) => {
await API.updateFlag(flag, value)
await actions.fetch()
},
toggleUiFeature: async feature => {
await API.toggleUiFeature(feature)
},
}
return {
subscribe,
...actions,
}
}
export const flags = createFlagsStore()

View File

@ -0,0 +1,40 @@
import { API } from "@/api"
import { GetUserFlagsResponse } from "@budibase/types"
import { BudiStore } from "../BudiStore"
interface FlagsState {
flags: GetUserFlagsResponse
}
const INITIAL_FLAGS_STATE: FlagsState = {
flags: {},
}
export class FlagsStore extends BudiStore<FlagsState> {
constructor() {
super(INITIAL_FLAGS_STATE)
this.fetch = this.fetch.bind(this)
this.updateFlag = this.updateFlag.bind(this)
this.toggleUiFeature = this.toggleUiFeature.bind(this)
}
async fetch() {
const flags = await API.getFlags()
this.update(state => ({
...state,
flags,
}))
}
async updateFlag(flag: string, value: any) {
await API.updateFlag(flag, value)
await this.fetch()
}
async toggleUiFeature(feature: string) {
await API.toggleUiFeature(feature)
}
}
export const flags = new FlagsStore()

View File

@ -2,13 +2,19 @@ import { derived, get } from "svelte/store"
import { componentStore } from "@/stores/builder"
import { API } from "@/api"
import { BudiStore } from "../BudiStore"
import { Layout } from "@budibase/types"
export const INITIAL_LAYOUT_STATE = {
interface LayoutState {
layouts: Layout[]
selectedLayoutId: string | null
}
export const INITIAL_LAYOUT_STATE: LayoutState = {
layouts: [],
selectedLayoutId: null,
}
export class LayoutStore extends BudiStore {
export class LayoutStore extends BudiStore<LayoutState> {
constructor() {
super(INITIAL_LAYOUT_STATE)
@ -22,14 +28,14 @@ export class LayoutStore extends BudiStore {
this.store.set({ ...INITIAL_LAYOUT_STATE })
}
syncAppLayouts(pkg) {
syncAppLayouts(pkg: { layouts: Layout[] }) {
this.update(state => ({
...state,
layouts: [...pkg.layouts],
}))
}
select(layoutId) {
select(layoutId: string) {
// Check this layout exists
const state = get(this.store)
const componentState = get(componentStore)
@ -48,15 +54,15 @@ export class LayoutStore extends BudiStore {
// Select new layout
this.update(state => {
state.selectedLayoutId = layout._id
state.selectedLayoutId = layout._id!
return state
})
componentStore.select(layout.props?._id)
}
async deleteLayout(layout) {
if (!layout?._id) {
async deleteLayout(layout: Layout) {
if (!layout?._id || !layout?._rev) {
return
}
await API.deleteLayout(layout._id, layout._rev)

View File

@ -1,80 +0,0 @@
import { writable, get } from "svelte/store"
const INITIAL_PREVIEW_STATE = {
previewDevice: "desktop",
previewEventHandler: null,
showPreview: false,
selectedComponentContext: null,
}
export const createPreviewStore = () => {
const store = writable({
...INITIAL_PREVIEW_STATE,
})
const setDevice = device => {
store.update(state => {
state.previewDevice = device
return state
})
}
// Potential evt names "eject-block", "dragging-new-component"
const sendEvent = (name, payload) => {
const { previewEventHandler } = get(store)
previewEventHandler?.(name, payload)
}
const registerEventHandler = handler => {
store.update(state => {
state.previewEventHandler = handler
return state
})
}
const startDrag = component => {
sendEvent("dragging-new-component", {
dragging: true,
component,
})
}
const stopDrag = () => {
sendEvent("dragging-new-component", {
dragging: false,
})
}
//load preview?
const showPreview = isVisible => {
store.update(state => {
state.showPreview = isVisible
return state
})
}
const setSelectedComponentContext = context => {
store.update(state => {
state.selectedComponentContext = context
return state
})
}
const requestComponentContext = () => {
sendEvent("request-context")
}
return {
subscribe: store.subscribe,
setDevice,
sendEvent, //may not be required
registerEventHandler,
startDrag,
stopDrag,
showPreview,
setSelectedComponentContext,
requestComponentContext,
}
}
export const previewStore = createPreviewStore()

View File

@ -0,0 +1,90 @@
import { get } from "svelte/store"
import { BudiStore } from "../BudiStore"
type PreviewDevice = "desktop" | "tablet" | "mobile"
type PreviewEventHandler = (name: string, payload?: any) => void
type ComponentContext = Record<string, any>
interface PreviewState {
previewDevice: PreviewDevice
previewEventHandler: PreviewEventHandler | null
showPreview: boolean
selectedComponentContext: ComponentContext | null
}
const INITIAL_PREVIEW_STATE: PreviewState = {
previewDevice: "desktop",
previewEventHandler: null,
showPreview: false,
selectedComponentContext: null,
}
export class PreviewStore extends BudiStore<PreviewState> {
constructor() {
super(INITIAL_PREVIEW_STATE)
this.setDevice = this.setDevice.bind(this)
this.sendEvent = this.sendEvent.bind(this)
this.registerEventHandler = this.registerEventHandler.bind(this)
this.startDrag = this.startDrag.bind(this)
this.stopDrag = this.stopDrag.bind(this)
this.showPreview = this.showPreview.bind(this)
this.setSelectedComponentContext =
this.setSelectedComponentContext.bind(this)
this.requestComponentContext = this.requestComponentContext.bind(this)
}
setDevice(device: PreviewDevice) {
this.update(state => ({
...state,
previewDevice: device,
}))
}
// Potential evt names "eject-block", "dragging-new-component"
sendEvent(name: string, payload?: any) {
const { previewEventHandler } = get(this.store)
previewEventHandler?.(name, payload)
}
registerEventHandler(handler: PreviewEventHandler) {
this.update(state => ({
...state,
previewEventHandler: handler,
}))
}
startDrag(component: any) {
this.sendEvent("dragging-new-component", {
dragging: true,
component,
})
}
stopDrag() {
this.sendEvent("dragging-new-component", {
dragging: false,
})
}
//load preview?
showPreview(isVisible: boolean) {
this.update(state => ({
...state,
showPreview: isVisible,
}))
}
setSelectedComponentContext(context: ComponentContext) {
this.update(state => ({
...state,
selectedComponentContext: context,
}))
}
requestComponentContext() {
this.sendEvent("request-context")
}
}
export const previewStore = new PreviewStore()

View File

@ -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
})

View File

@ -0,0 +1,59 @@
import { get, derived } from "svelte/store"
import { BudiStore } from "../BudiStore"
import { UIUser } from "@budibase/types"
export class UserStore extends BudiStore<UIUser[]> {
constructor() {
super([])
}
init(users: UIUser[]) {
this.set(users)
}
updateUser(user: UIUser) {
const $users = get(this)
if (!$users.some(x => x.sessionId === user.sessionId)) {
this.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.set([])
}
}
export const userStore = new UserStore()
export const userSelectedResourceMap = derived(
userStore,
($userStore): Record<string, UIUser[]> => {
let map: Record<string, UIUser[]> = {}
$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
})

View File

@ -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
}

View File

@ -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.init(users)
}
)
})
socket.on("connect_error", err => {
console.error("Failed to connect to builder websocket:", err.message)
})
socket.on("disconnect", () => {
userStore.reset()
})
// User events
socket.onOther(SocketEvent.UserUpdate, ({ user }: { user: UIUser }) => {
userStore.updateUser(user)
})
socket.onOther(
SocketEvent.UserDisconnect,
({ sessionId }: { sessionId: string }) => {
userStore.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
}

View File

@ -26,7 +26,7 @@
: RelationshipType.MANY_TO_MANY,
}
async function searchFunction(searchParams) {
async function searchFunction(_tableId, searchParams) {
if (
subtype !== BBReferenceFieldSubType.USER &&
subtype !== BBReferenceFieldSubType.USERS

View File

@ -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)

View File

@ -174,7 +174,9 @@ class QueryRunner {
}
// needs to an array for next step
if (!Array.isArray(rows)) {
if (rows === null) {
rows = []
} else if (!Array.isArray(rows)) {
rows = [rows]
}

View File

@ -3,4 +3,5 @@ import { User } from "@budibase/types"
export interface UIUser extends User {
sessionId: string
gridMetadata?: { focusedCellId?: string }
builderMetadata?: { selectedResourceId?: string }
}

View File

@ -1,3 +1,4 @@
export * from "./integration"
export * from "./automations"
export * from "./grid"
export * from "./preview"

View File

@ -0,0 +1 @@
export type PreviewDevice = "desktop" | "tablet" | "mobile"