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

This commit is contained in:
Andrew Kingston 2025-01-06 14:59:12 +00:00 committed by GitHub
commit 997a4ef624
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 155 additions and 141 deletions

View File

@ -2,19 +2,24 @@ import { get } from "svelte/store"
import { BudiStore } from "../BudiStore" import { BudiStore } from "../BudiStore"
import { previewStore } from "@/stores/builder" import { previewStore } from "@/stores/builder"
interface BuilderHoverStore {
hoverTimeout?: NodeJS.Timeout
componentId: string | null
}
export const INITIAL_HOVER_STATE = { export const INITIAL_HOVER_STATE = {
componentId: null, componentId: null,
} }
export class HoverStore extends BudiStore { export class HoverStore extends BudiStore<BuilderHoverStore> {
hoverTimeout hoverTimeout?: NodeJS.Timeout
constructor() { constructor() {
super({ ...INITIAL_HOVER_STATE }) super({ ...INITIAL_HOVER_STATE })
this.hover = this.hover.bind(this) this.hover = this.hover.bind(this)
} }
hover(componentId, notifyClient = true) { hover(componentId: string, notifyClient = true) {
clearTimeout(this.hoverTimeout) clearTimeout(this.hoverTimeout)
if (componentId) { if (componentId) {
this.processHover(componentId, notifyClient) this.processHover(componentId, notifyClient)
@ -25,7 +30,7 @@ export class HoverStore extends BudiStore {
} }
} }
processHover(componentId, notifyClient) { processHover(componentId: string, notifyClient?: boolean) {
if (componentId === get(this.store).componentId) { if (componentId === get(this.store).componentId) {
return return
} }

View File

@ -2,27 +2,22 @@ import { get } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { appStore } from "@/stores/builder" import { appStore } from "@/stores/builder"
import { BudiStore } from "../BudiStore" import { BudiStore } from "../BudiStore"
import { AppNavigation, AppNavigationLink, UIObject } from "@budibase/types"
interface BuilderNavigationStore extends AppNavigation {}
export const INITIAL_NAVIGATION_STATE = { export const INITIAL_NAVIGATION_STATE = {
navigation: "Top", navigation: "Top",
links: [], links: [],
title: null,
sticky: null,
hideLogo: null,
logoUrl: null,
hideTitle: null,
textAlign: "Left", textAlign: "Left",
navBackground: null,
navWidth: null,
navTextColor: null,
} }
export class NavigationStore extends BudiStore { export class NavigationStore extends BudiStore<BuilderNavigationStore> {
constructor() { constructor() {
super(INITIAL_NAVIGATION_STATE) super(INITIAL_NAVIGATION_STATE)
} }
syncAppNavigation(nav) { syncAppNavigation(nav: AppNavigation) {
this.update(state => ({ this.update(state => ({
...state, ...state,
...nav, ...nav,
@ -33,15 +28,17 @@ export class NavigationStore extends BudiStore {
this.store.set({ ...INITIAL_NAVIGATION_STATE }) this.store.set({ ...INITIAL_NAVIGATION_STATE })
} }
async save(navigation) { async save(navigation: AppNavigation) {
const appId = get(appStore).appId const appId = get(appStore).appId
const app = await API.saveAppMetadata(appId, { navigation }) const app = await API.saveAppMetadata(appId, { navigation })
this.syncAppNavigation(app.navigation) if (app.navigation) {
this.syncAppNavigation(app.navigation)
}
} }
async saveLink(url, title, roleId) { async saveLink(url: string, title: string, roleId: string) {
const navigation = get(this.store) const navigation = get(this.store)
let links = [...(navigation?.links ?? [])] let links: AppNavigationLink[] = [...(navigation?.links ?? [])]
// Skip if we have an identical link // Skip if we have an identical link
if (links.find(link => link.url === url && link.text === title)) { if (links.find(link => link.url === url && link.text === title)) {
@ -60,7 +57,7 @@ export class NavigationStore extends BudiStore {
}) })
} }
async deleteLink(urls) { async deleteLink(urls: string[] | string) {
const navigation = get(this.store) const navigation = get(this.store)
let links = navigation?.links let links = navigation?.links
if (!links?.length) { if (!links?.length) {
@ -86,7 +83,7 @@ export class NavigationStore extends BudiStore {
}) })
} }
syncMetadata(metadata) { syncMetadata(metadata: UIObject) {
const { navigation } = metadata const { navigation } = metadata
this.syncAppNavigation(navigation) this.syncAppNavigation(navigation)
} }

View File

@ -6,18 +6,29 @@ import { automationStore } from "./automations"
import { API } from "@/api" import { API } from "@/api"
import { getSequentialName } from "@/helpers/duplicate" import { getSequentialName } from "@/helpers/duplicate"
const initialState = {} interface RowAction {
id: string
name: string
tableId: string
allowedSources?: string[]
}
export class RowActionStore extends BudiStore { interface RowActionState {
[tableId: string]: RowAction[]
}
const initialState: RowActionState = {}
export class RowActionStore extends BudiStore<RowActionState> {
constructor() { constructor() {
super(initialState) super(initialState)
} }
reset = () => { reset = () => {
this.store.set(initialState) this.set(initialState)
} }
refreshRowActions = async sourceId => { refreshRowActions = async (sourceId: string) => {
if (!sourceId) { if (!sourceId) {
return return
} }
@ -34,26 +45,30 @@ export class RowActionStore extends BudiStore {
// Fetch row actions for this table // Fetch row actions for this table
const res = await API.rowActions.fetch(tableId) const res = await API.rowActions.fetch(tableId)
const actions = Object.values(res || {}) const actions = Object.values(res || {}) as RowAction[]
this.update(state => ({ this.update(state => ({
...state, ...state,
[tableId]: actions, [tableId]: actions,
})) }))
} }
createRowAction = async (tableId, viewId, name) => { createRowAction = async (tableId: string, viewId?: string, name?: string) => {
if (!tableId) { if (!tableId) {
return return
} }
// Get a unique name for this action // Get a unique name for this action
if (!name) { if (!name) {
const existingRowActions = get(this.store)[tableId] || [] const existingRowActions = get(this)[tableId] || []
name = getSequentialName(existingRowActions, "New row action ", { name = getSequentialName(existingRowActions, "New row action ", {
getName: x => x.name, getName: x => x.name,
}) })
} }
if (!name) {
throw new Error("Failed to generate a unique name for the row action")
}
// Create the action // Create the action
const res = await API.rowActions.create(tableId, name) const res = await API.rowActions.create(tableId, name)
@ -73,41 +88,35 @@ export class RowActionStore extends BudiStore {
return res return res
} }
enableView = async (tableId, rowActionId, viewId) => { enableView = async (tableId: string, rowActionId: string, viewId: string) => {
await API.rowActions.enableView(tableId, rowActionId, viewId) await API.rowActions.enableView(tableId, rowActionId, viewId)
await this.refreshRowActions(tableId) await this.refreshRowActions(tableId)
} }
disableView = async (tableId, rowActionId, viewId) => { disableView = async (
tableId: string,
rowActionId: string,
viewId: string
) => {
await API.rowActions.disableView(tableId, rowActionId, viewId) await API.rowActions.disableView(tableId, rowActionId, viewId)
await this.refreshRowActions(tableId) await this.refreshRowActions(tableId)
} }
rename = async (tableId, rowActionId, name) => { delete = async (tableId: string, rowActionId: string) => {
await API.rowActions.update({
tableId,
rowActionId,
name,
})
await this.refreshRowActions(tableId)
automationStore.actions.fetch()
}
delete = async (tableId, rowActionId) => {
await API.rowActions.delete(tableId, rowActionId) await API.rowActions.delete(tableId, rowActionId)
await this.refreshRowActions(tableId) await this.refreshRowActions(tableId)
// We don't need to refresh automations as we can only delete row actions // We don't need to refresh automations as we can only delete row actions
// from the automations store, so we already handle the state update there // from the automations store, so we already handle the state update there
} }
trigger = async (sourceId, rowActionId, rowId) => { trigger = async (sourceId: string, rowActionId: string, rowId: string) => {
await API.rowActions.trigger(sourceId, rowActionId, rowId) await API.rowActions.trigger(sourceId, rowActionId, rowId)
} }
} }
const store = new RowActionStore() const store = new RowActionStore()
const derivedStore = derived(store, $store => { const derivedStore = derived<RowActionStore, RowActionState>(store, $store => {
let map = {} const map: RowActionState = {}
// Generate an entry for every view as well // Generate an entry for every view as well
Object.keys($store || {}).forEach(tableId => { Object.keys($store || {}).forEach(tableId => {
@ -115,7 +124,7 @@ const derivedStore = derived(store, $store => {
map[tableId] = $store[tableId] map[tableId] = $store[tableId]
for (let action of $store[tableId]) { for (let action of $store[tableId]) {
const otherSources = (action.allowedSources || []).filter( const otherSources = (action.allowedSources || []).filter(
sourceId => sourceId !== tableId (sourceId: string) => sourceId !== tableId
) )
for (let source of otherSources) { for (let source of otherSources) {
map[source] ??= [] map[source] ??= []

View File

@ -1,35 +0,0 @@
import { writable, get } from "svelte/store"
import { API } from "@/api"
import { appStore } from "./app"
const createsnippets = () => {
const store = writable([])
const syncMetadata = metadata => {
store.set(metadata?.snippets || [])
}
const saveSnippet = async updatedSnippet => {
const snippets = [
...get(store).filter(snippet => snippet.name !== updatedSnippet.name),
updatedSnippet,
]
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
syncMetadata(app)
}
const deleteSnippet = async snippetName => {
const snippets = get(store).filter(snippet => snippet.name !== snippetName)
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
syncMetadata(app)
}
return {
...store,
syncMetadata,
saveSnippet,
deleteSnippet,
}
}
export const snippets = createsnippets()

View File

@ -0,0 +1,32 @@
import { get } from "svelte/store"
import { API } from "@/api"
import { appStore } from "./app"
import { BudiStore } from "../BudiStore"
import { Snippet, UpdateAppResponse } from "@budibase/types"
export class SnippetStore extends BudiStore<Snippet[]> {
constructor() {
super([])
}
syncMetadata = (metadata: UpdateAppResponse) => {
this.set(metadata?.snippets || [])
}
saveSnippet = async (updatedSnippet: Snippet) => {
const snippets = [
...get(this).filter(snippet => snippet.name !== updatedSnippet.name),
updatedSnippet,
]
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
this.syncMetadata(app)
}
deleteSnippet = async (snippetName: string) => {
const snippets = get(this).filter(snippet => snippet.name !== snippetName)
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
this.syncMetadata(app)
}
}
export const snippets = new SnippetStore()

View File

@ -1,58 +0,0 @@
import { writable, get } from "svelte/store"
import { API } from "@/api"
import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
export const createThemeStore = () => {
const store = writable({
theme: DefaultAppTheme,
customTheme: {},
})
const syncAppTheme = app => {
store.update(state => {
const theme = ensureValidTheme(app.theme, DefaultAppTheme)
return {
...state,
theme,
customTheme: app.customTheme,
}
})
}
const save = async (theme, appId) => {
const app = await API.saveAppMetadata(appId, { theme })
store.update(state => {
state.theme = app.theme
return state
})
}
const saveCustom = async (theme, appId) => {
const updated = { ...get(store).customTheme, ...theme }
const app = await API.saveAppMetadata(appId, { customTheme: updated })
store.update(state => {
state.customTheme = app.customTheme
return state
})
}
const syncMetadata = metadata => {
const { theme, customTheme } = metadata
store.update(state => ({
...state,
theme: ensureValidTheme(theme, DefaultAppTheme),
customTheme,
}))
}
return {
subscribe: store.subscribe,
update: store.update,
syncMetadata,
syncAppTheme,
save,
saveCustom,
}
}
export const themeStore = createThemeStore()

View File

@ -0,0 +1,58 @@
import { get } from "svelte/store"
import { API } from "@/api"
import { BudiStore } from "../BudiStore"
import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
import { App, UpdateAppResponse, Theme, AppCustomTheme } from "@budibase/types"
interface ThemeState {
theme: Theme
customTheme: AppCustomTheme
}
export class ThemeStore extends BudiStore<ThemeState> {
constructor() {
super({
theme: DefaultAppTheme,
customTheme: {},
})
}
syncAppTheme = (app: App) => {
this.update(state => {
const theme = ensureValidTheme(app.theme, DefaultAppTheme)
return {
...state,
theme,
customTheme: app.customTheme || {},
}
})
}
save = async (theme: Theme, appId: string) => {
const app = await API.saveAppMetadata(appId, { theme })
this.update(state => ({
...state,
theme: ensureValidTheme(app.theme, DefaultAppTheme),
}))
}
saveCustom = async (theme: Partial<AppCustomTheme>, appId: string) => {
const updated = { ...get(this).customTheme, ...theme }
const app = await API.saveAppMetadata(appId, { customTheme: updated })
this.update(state => ({
...state,
customTheme: app.customTheme || {},
}))
}
syncMetadata = (metadata: UpdateAppResponse) => {
const { theme, customTheme } = metadata
this.update(state => ({
...state,
theme: ensureValidTheme(theme, DefaultAppTheme),
customTheme: customTheme || {},
}))
}
}
export const themeStore = new ThemeStore()

View File

@ -1,4 +1,4 @@
import { User, Document, Plugin, Snippet } from "../" import { User, Document, Plugin, Snippet, Theme } from "../"
import { SocketSession } from "../../sdk" import { SocketSession } from "../../sdk"
export type AppMetadataErrors = { [key: string]: string[] } export type AppMetadataErrors = { [key: string]: string[] }
@ -14,7 +14,7 @@ export interface App extends Document {
instance: AppInstance instance: AppInstance
tenantId: string tenantId: string
status: string status: string
theme?: string theme?: Theme
customTheme?: AppCustomTheme customTheme?: AppCustomTheme
revertableVersion?: string revertableVersion?: string
lockedBy?: User lockedBy?: User
@ -37,8 +37,8 @@ export interface AppInstance {
export interface AppNavigation { export interface AppNavigation {
navigation: string navigation: string
title: string title?: string
navWidth: string navWidth?: string
sticky?: boolean sticky?: boolean
hideLogo?: boolean hideLogo?: boolean
logoUrl?: string logoUrl?: string
@ -46,6 +46,7 @@ export interface AppNavigation {
navBackground?: string navBackground?: string
navTextColor?: string navTextColor?: string
links?: AppNavigationLink[] links?: AppNavigationLink[]
textAlign?: string
} }
export interface AppNavigationLink { export interface AppNavigationLink {
@ -53,6 +54,8 @@ export interface AppNavigationLink {
url: string url: string
id?: string id?: string
roleId?: string roleId?: string
type?: string
subLinks?: AppNavigationLink[]
} }
export interface AppCustomTheme { export interface AppCustomTheme {

View File

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

View File

@ -0,0 +1,2 @@
// type purely to capture structures that the type is unknown, but maybe known later
export type UIObject = Record<string, any>