Merge branch 'master' of github.com:Budibase/budibase into contributor-enhancements

This commit is contained in:
Andrew Kingston 2024-12-30 15:38:24 +00:00
commit c47b7d5adf
No known key found for this signature in database
13 changed files with 251 additions and 91 deletions

View File

@ -22,7 +22,7 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: blockRefs = $selectedAutomation?.blockRefs || {} $: blockRefs = $selectedAutomation?.blockRefs || {}
$: stepNames = automation?.definition.stepNames $: stepNames = automation?.definition.stepNames || {}
$: allSteps = automation?.definition.steps || [] $: allSteps = automation?.definition.steps || []
$: automationName = itemName || stepNames?.[block.id] || block?.name || "" $: automationName = itemName || stepNames?.[block.id] || block?.name || ""
$: automationNameError = getAutomationNameError(automationName) $: automationNameError = getAutomationNameError(automationName)
@ -64,7 +64,7 @@
const getAutomationNameError = name => { const getAutomationNameError = name => {
const duplicateError = const duplicateError =
"This name already exists, please enter a unique name" "This name already exists, please enter a unique name"
if (stepNames && editing) { if (editing) {
for (const [key, value] of Object.entries(stepNames)) { for (const [key, value] of Object.entries(stepNames)) {
if (name !== block.name && name === value && key !== block.id) { if (name !== block.name && name === value && key !== block.id) {
return duplicateError return duplicateError

View File

@ -1,7 +1,54 @@
import { API } from "api" import { API } from "api"
import { BudiStore } from "../BudiStore" 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: "", appId: "",
name: "", name: "",
url: "", url: "",
@ -34,23 +81,27 @@ export const INITIAL_APP_META_STATE = {
routes: {}, routes: {},
} }
export class AppMetaStore extends BudiStore { export class AppMetaStore extends BudiStore<AppMetaState> {
constructor() { constructor() {
super(INITIAL_APP_META_STATE) super(INITIAL_APP_META_STATE)
} }
reset() { reset(): void {
this.store.set({ ...INITIAL_APP_META_STATE }) this.store.set({ ...INITIAL_APP_META_STATE })
} }
syncAppPackage(pkg) { syncAppPackage(pkg: {
application: App
clientLibPath: string
hasLock: boolean
}): void {
const { application: app, clientLibPath, hasLock } = pkg const { application: app, clientLibPath, hasLock } = pkg
this.update(state => ({ this.update(state => ({
...state, ...state,
name: app.name, name: app.name,
appId: app.appId, appId: app.appId,
url: app.url, url: app.url || "",
hasLock, hasLock,
clientLibPath, clientLibPath,
libraries: app.componentLibraries, libraries: app.componentLibraries,
@ -58,8 +109,8 @@ export class AppMetaStore extends BudiStore {
appInstance: app.instance, appInstance: app.instance,
revertableVersion: app.revertableVersion, revertableVersion: app.revertableVersion,
upgradableVersion: app.upgradableVersion, upgradableVersion: app.upgradableVersion,
usedPlugins: app.usedPlugins, usedPlugins: app.usedPlugins || null,
icon: app.icon || {}, icon: app.icon,
features: { features: {
...INITIAL_APP_META_STATE.features, ...INITIAL_APP_META_STATE.features,
...app.features, ...app.features,
@ -70,7 +121,7 @@ export class AppMetaStore extends BudiStore {
})) }))
} }
syncClientFeatures(features) { syncClientFeatures(features: Partial<ClientFeatures>): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
clientFeatures: { clientFeatures: {
@ -80,14 +131,14 @@ export class AppMetaStore extends BudiStore {
})) }))
} }
syncClientTypeSupportPresets(typeSupportPresets) { syncClientTypeSupportPresets(typeSupportPresets: TypeSupportPresets): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
typeSupportPresets, typeSupportPresets,
})) }))
} }
async syncAppRoutes() { async syncAppRoutes(): Promise<void> {
const resp = await API.fetchAppRoutes() const resp = await API.fetchAppRoutes()
this.update(state => ({ this.update(state => ({
...state, ...state,
@ -96,7 +147,7 @@ export class AppMetaStore extends BudiStore {
} }
// Returned from socket // Returned from socket
syncMetadata(metadata) { syncMetadata(metadata: { name: string; url: string; icon?: AppIcon }): void {
const { name, url, icon } = metadata const { name, url, icon } = metadata
this.update(state => ({ this.update(state => ({
...state, ...state,

View File

@ -1,10 +1,28 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { createBuilderWebsocket } from "./websocket.js" import { createBuilderWebsocket } from "./websocket.js"
import { Socket } from "socket.io-client"
import { BuilderSocketEvent } from "@budibase/shared-core" import { BuilderSocketEvent } from "@budibase/shared-core"
import { BudiStore } from "../BudiStore.js" import { BudiStore } from "../BudiStore.js"
import { TOUR_KEYS } from "components/portal/onboarding/tours.js" import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
import { App } from "@budibase/types"
export const INITIAL_BUILDER_STATE = { interface BuilderState {
previousTopNavPath: Record<string, string>
highlightedSetting: {
key: string
type: "info" | string
} | null
propertyFocus: string | null
builderSidePanel: boolean
onboarding: boolean
tourNodes: Record<string, HTMLElement> | null
tourKey: string | null
tourStepKey: string | null
hoveredComponentId: string | null
websocket?: Socket
}
export const INITIAL_BUILDER_STATE: BuilderState = {
previousTopNavPath: {}, previousTopNavPath: {},
highlightedSetting: null, highlightedSetting: null,
propertyFocus: null, propertyFocus: null,
@ -16,7 +34,9 @@ export const INITIAL_BUILDER_STATE = {
hoveredComponentId: null, hoveredComponentId: null,
} }
export class BuilderStore extends BudiStore { export class BuilderStore extends BudiStore<BuilderState> {
websocket?: Socket
constructor() { constructor() {
super({ ...INITIAL_BUILDER_STATE }) super({ ...INITIAL_BUILDER_STATE })
@ -32,11 +52,9 @@ export class BuilderStore extends BudiStore {
this.registerTourNode = this.registerTourNode.bind(this) this.registerTourNode = this.registerTourNode.bind(this)
this.destroyTourNode = this.destroyTourNode.bind(this) this.destroyTourNode = this.destroyTourNode.bind(this)
this.startBuilderOnboarding = this.startBuilderOnboarding.bind(this) this.startBuilderOnboarding = this.startBuilderOnboarding.bind(this)
this.websocket
} }
init(app) { init(app: App): void {
if (!app?.appId) { if (!app?.appId) {
console.error("BuilderStore: No appId supplied for websocket") console.error("BuilderStore: No appId supplied for websocket")
return return
@ -46,45 +64,46 @@ export class BuilderStore extends BudiStore {
} }
} }
refresh() { refresh(): void {
this.store.set(this.store.get()) const currentState = get(this.store)
this.store.set(currentState)
} }
reset() { reset(): void {
this.store.set({ ...INITIAL_BUILDER_STATE }) this.store.set({ ...INITIAL_BUILDER_STATE })
this.websocket?.disconnect() this.websocket?.disconnect()
this.websocket = null this.websocket = undefined
} }
highlightSetting(key, type) { highlightSetting(key?: string, type?: string): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
highlightedSetting: key ? { key, type: type || "info" } : null, highlightedSetting: key ? { key, type: type || "info" } : null,
})) }))
} }
propertyFocus(key) { propertyFocus(key: string | null): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
propertyFocus: key, propertyFocus: key,
})) }))
} }
showBuilderSidePanel() { showBuilderSidePanel(): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
builderSidePanel: true, builderSidePanel: true,
})) }))
} }
hideBuilderSidePanel() { hideBuilderSidePanel(): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
builderSidePanel: false, builderSidePanel: false,
})) }))
} }
setPreviousTopNavPath(route, url) { setPreviousTopNavPath(route: string, url: string): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
previousTopNavPath: { previousTopNavPath: {
@ -94,13 +113,13 @@ export class BuilderStore extends BudiStore {
})) }))
} }
selectResource(id) { selectResource(id: string): void {
this.websocket.emit(BuilderSocketEvent.SelectResource, { this.websocket?.emit(BuilderSocketEvent.SelectResource, {
resourceId: id, resourceId: id,
}) })
} }
registerTourNode(tourStepKey, node) { registerTourNode(tourStepKey: string, node: HTMLElement): void {
this.update(state => { this.update(state => {
const update = { const update = {
...state, ...state,
@ -113,7 +132,7 @@ export class BuilderStore extends BudiStore {
}) })
} }
destroyTourNode(tourStepKey) { destroyTourNode(tourStepKey: string): void {
const store = get(this.store) const store = get(this.store)
if (store.tourNodes?.[tourStepKey]) { if (store.tourNodes?.[tourStepKey]) {
const nodes = { ...store.tourNodes } const nodes = { ...store.tourNodes }
@ -125,7 +144,7 @@ export class BuilderStore extends BudiStore {
} }
} }
startBuilderOnboarding() { startBuilderOnboarding(): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
onboarding: true, onboarding: true,
@ -133,19 +152,19 @@ export class BuilderStore extends BudiStore {
})) }))
} }
endBuilderOnboarding() { endBuilderOnboarding(): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
onboarding: false, onboarding: false,
})) }))
} }
setTour(tourKey) { setTour(tourKey?: string | null): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
tourStepKey: null, tourStepKey: null,
tourNodes: null, tourNodes: null,
tourKey: tourKey, tourKey: tourKey || null,
})) }))
} }
} }

View File

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

View File

@ -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<ContextMenuState>({ ...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()

View File

@ -1,11 +1,12 @@
import { writable } from "svelte/store" import { writable, type Writable } from "svelte/store"
import { API } from "api" import { API } from "api"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { DeploymentProgressResponse } from "@budibase/types"
export const createDeploymentStore = () => { export const createDeploymentStore = () => {
let store = writable([]) let store: Writable<DeploymentProgressResponse[]> = writable([])
const load = async () => { const load = async (): Promise<void> => {
try { try {
store.set(await API.getAppDeployments()) store.set(await API.getAppDeployments())
} catch (err) { } catch (err) {

View File

@ -65,7 +65,7 @@ describe("Builder store", () => {
ctx.test.builderStore.reset() ctx.test.builderStore.reset()
expect(disconnected).toBe(true) expect(disconnected).toBe(true)
expect(ctx.test.store).toStrictEqual(INITIAL_BUILDER_STATE) 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 => { it("Attempt to emit a resource select event to the websocket on select", ctx => {

View File

@ -60,7 +60,10 @@ export type Store = BaseStore &
Table.Store & Table.Store &
ViewV2.Store & ViewV2.Store &
NonPlus.Store & NonPlus.Store &
Datasource.Store & { Datasource.Store &
Validation.Store &
Users.Store &
Menu.Store & {
// TODO while typing the rest of stores // TODO while typing the rest of stores
fetch: Writable<any> fetch: Writable<any>
filter: Writable<any> filter: Writable<any>
@ -76,6 +79,13 @@ export type Store = BaseStore &
dispatch: (event: string, data: any) => any dispatch: (event: string, data: any) => any
notifications: Writable<any> notifications: Writable<any>
schemaOverrides: Writable<any> schemaOverrides: Writable<any>
focusedCellId: Writable<any>
previousFocusedRowId: Writable<string>
gridID: string
selectedRows: Writable<any>
selectedRowCount: Writable<any>
selectedCellMap: Writable<any>
selectedCellCount: Writable<any>
} }
export const attachStores = (context: Store): Store => { export const attachStores = (context: Store): Store => {

View File

@ -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" import { parseCellID } from "../lib/utils"
interface MenuStoreData {
left: number
top: number
visible: boolean
multiRowMode: boolean
multiCellMode: boolean
}
interface MenuStore {
menu: Writable<MenuStoreData>
}
export type Store = MenuStore
export const createStores = () => { export const createStores = () => {
const menu = writable({ const menu = writable<MenuStoreData>({
left: 0, left: 0,
top: 0, top: 0,
visible: false, visible: false,
@ -14,7 +30,7 @@ export const createStores = () => {
} }
} }
export const createActions = context => { export const createActions = (context: StoreContext) => {
const { const {
menu, menu,
focusedCellId, focusedCellId,
@ -25,7 +41,7 @@ export const createActions = context => {
selectedCellCount, selectedCellCount,
} = context } = context
const open = (cellId, e) => { const open = (cellId: string, e: MouseEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@ -37,7 +53,7 @@ export const createActions = context => {
} }
// Compute bounds of cell relative to outer data node // 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() const dataBounds = dataNode.getBoundingClientRect()
// Check if there are multiple rows selected, and if this is one of them // Check if there are multiple rows selected, and if this is one of them

View File

@ -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 { helpers } from "@budibase/shared-core"
import { Store as StoreContext } from "."
import { UIUser } from "@budibase/types"
export const createStores = () => { interface UIEnrichedUser extends UIUser {
const users = writable([]) color: string
label: string
}
interface UsersStore {
users: Writable<UIUser[]>
}
interface DerivedUsersStore {
userCellMap: Readable<Record<string, UIUser>>
}
interface ActionUserStore {
users: UsersStore["users"] &
Readable<UIEnrichedUser[]> & {
actions: {
updateUser: (user: UIUser) => void
removeUser: (sessionId: string) => void
}
}
}
export type Store = DerivedUsersStore & ActionUserStore
export const createStores = (): UsersStore => {
const users = writable<UIUser[]>([])
const enrichedUsers = derived(users, $users => { const enrichedUsers = derived(users, $users => {
return $users.map(user => ({ return $users.map<UIEnrichedUser>(user => ({
...user, ...user,
color: helpers.getUserColor(user), color: helpers.getUserColor(user),
label: helpers.getUserLabel(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 const { users, focusedCellId } = context
// Generate a lookup map of cell ID to the user that has it selected, to make // 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( const userCellMap = derived(
[users, focusedCellId], [users, focusedCellId],
([$users, $focusedCellId]) => { ([$users, $focusedCellId]) => {
let map = {} let map: Record<string, UIUser> = {}
$users.forEach(user => { $users.forEach(user => {
const cellId = user.gridMetadata?.focusedCellId const cellId = user.gridMetadata?.focusedCellId
if (cellId && cellId !== $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 { users } = context
const updateUser = user => { const updateUser = (user: UIUser) => {
const $users = get(users) const $users = get(users)
if (!$users.some(x => x.sessionId === user.sessionId)) { if (!$users.some(x => x.sessionId === user.sessionId)) {
users.set([...$users, user]) users.set([...$users, user])
@ -60,7 +87,7 @@ export const createActions = context => {
} }
} }
const removeUser = sessionId => { const removeUser = (sessionId: string) => {
users.update(state => { users.update(state => {
return state.filter(x => x.sessionId !== sessionId) return state.filter(x => x.sessionId !== sessionId)
}) })

View File

@ -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" import { parseCellID } from "../lib/utils"
interface ValidationStore {
validation: Writable<Record<string, string>>
}
interface DerivedValidationStore {
validationRowLookupMap: Readable<Record<string, string[]>>
}
export type Store = ValidationStore & DerivedValidationStore
// Normally we would break out actions into the explicit "createActions" // Normally we would break out actions into the explicit "createActions"
// function, but for validation all these actions are pure so can go into // function, but for validation all these actions are pure so can go into
// "createStores" instead to make dependency ordering simpler // "createStores" instead to make dependency ordering simpler
export const createStores = () => { export const createStores = (): ValidationStore => {
const validation = writable({}) const validation = writable({})
return { return {
@ -12,12 +23,12 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const deriveStores = (context: StoreContext): DerivedValidationStore => {
const { validation } = context const { validation } = context
// Derive which rows have errors so that we can use that info later // Derive which rows have errors so that we can use that info later
const validationRowLookupMap = derived(validation, $validation => { const validationRowLookupMap = derived(validation, $validation => {
let map = {} const map: Record<string, string[]> = {}
Object.entries($validation).forEach(([key, error]) => { Object.entries($validation).forEach(([key, error]) => {
// Extract row ID from all errored cell IDs // Extract row ID from all errored cell IDs
if (error) { 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 { validation, focusedCellId, validationRowLookupMap } = context
const setError = (cellId, error) => { const setError = (cellId: string | undefined, error: string) => {
if (!cellId) { if (!cellId) {
return return
} }
@ -49,11 +60,11 @@ export const createActions = context => {
})) }))
} }
const rowHasErrors = rowId => { const rowHasErrors = (rowId: string) => {
return get(validationRowLookupMap)[rowId]?.length > 0 return get(validationRowLookupMap)[rowId]?.length > 0
} }
const focusFirstRowError = rowId => { const focusFirstRowError = (rowId: string) => {
const errorCells = get(validationRowLookupMap)[rowId] const errorCells = get(validationRowLookupMap)[rowId]
const cellId = errorCells?.[0] const cellId = errorCells?.[0]
if (cellId) { if (cellId) {
@ -73,7 +84,7 @@ export const createActions = context => {
} }
} }
export const initialise = context => { export const initialise = (context: StoreContext) => {
const { validation, previousFocusedRowId, validationRowLookupMap } = context const { validation, previousFocusedRowId, validationRowLookupMap } = context
// Remove validation errors when changing rows // Remove validation errors when changing rows

View File

@ -2,3 +2,4 @@ export * from "./columns"
export * from "./datasource" export * from "./datasource"
export * from "./table" export * from "./table"
export * from "./view" export * from "./view"
export * from "./user"

View File

@ -0,0 +1,6 @@
import { User } from "@budibase/types"
export interface UIUser extends User {
sessionId: string
gridMetadata?: { focusedCellId?: string }
}