Merge branch 'develop' of github.com:Budibase/budibase into feature/table-fetching-frontend

This commit is contained in:
Michael Drury 2023-06-06 10:09:50 +01:00
commit 465856e8c9
21 changed files with 187 additions and 111 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.6.19-alpha.52", "version": "2.6.24-alpha.0",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/backend-core", "packages/backend-core",

View File

@ -86,6 +86,7 @@ const getCurrentIdentity = async (): Promise<Identity> => {
installationId, installationId,
tenantId, tenantId,
environment, environment,
realTenantId: context.getTenantId(),
hostInfo: userContext.hostInfo, hostInfo: userContext.hostInfo,
} }
} else { } else {

View File

@ -1,15 +1,18 @@
import { createWebsocket } from "@budibase/frontend-core" import { createWebsocket } from "@budibase/frontend-core"
import { userStore } from "builderStore" import { userStore, store } from "builderStore"
import { datasources, tables } from "stores/backend" import { datasources, tables } from "stores/backend"
import { get } from "svelte/store"
import { auth } from "stores/portal"
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core" import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
import { notifications } from "@budibase/bbui"
export const createBuilderWebsocket = appId => { export const createBuilderWebsocket = appId => {
const socket = createWebsocket("/socket/builder") const socket = createWebsocket("/socket/builder")
// Built-in events // Built-in events
socket.on("connect", () => { socket.on("connect", () => {
socket.emit(BuilderSocketEvent.SelectApp, appId, response => { socket.emit(BuilderSocketEvent.SelectApp, { appId }, ({ users }) => {
userStore.actions.init(response.users) userStore.actions.init(users)
}) })
}) })
socket.on("connect_error", err => { socket.on("connect_error", err => {
@ -20,8 +23,21 @@ export const createBuilderWebsocket = appId => {
}) })
// User events // User events
socket.onOther(SocketEvent.UserUpdate, userStore.actions.updateUser) socket.onOther(SocketEvent.UserUpdate, ({ user }) => {
socket.onOther(SocketEvent.UserDisconnect, userStore.actions.removeUser) userStore.actions.updateUser(user)
})
socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => {
userStore.actions.removeUser(sessionId)
})
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
if (userId === get(auth)?.user?._id) {
notifications.success("You can now edit screens and automations")
store.update(state => ({
...state,
hasLock: true,
}))
}
})
// Table events // Table events
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => { socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {

View File

@ -1,13 +0,0 @@
const getUserInitials = user => {
if (user.firstName && user.lastName) {
return user.firstName[0] + user.lastName[0]
} else if (user.firstName) {
return user.firstName[0]
} else if (user.email) {
return user.email[0]
}
return "U"
}
export default getUserInitials

View File

@ -44,14 +44,14 @@
let config = {} let config = {}
let updated = false let updated = false
$: onConfigUpdate(config, mounted) $: onConfigUpdate(config)
$: init = Object.keys(config).length > 0 $: initialised = Object.keys(config).length > 0
$: isCloud = $admin.cloud $: isCloud = $admin.cloud
$: brandingEnabled = $licensing.brandingEnabled $: brandingEnabled = $licensing.brandingEnabled
const onConfigUpdate = () => { const onConfigUpdate = () => {
if (!mounted || updated || !init) { if (!mounted || updated || !initialised) {
return return
} }
updated = true updated = true
@ -122,34 +122,27 @@
return response return response
} }
async function saveConfig() { async function saveFiles() {
saving = true
if (logoFile) { if (logoFile) {
const logoResp = await uploadLogo(logoFile) const logoResp = await uploadLogo(logoFile)
if (logoResp.url) { if (logoResp.url) {
config = {
...config,
logoUrl: logoResp.url,
}
logoFile = null logoFile = null
logoPreview = null logoPreview = null
} }
config.logoUrl = undefined
} }
if (faviconFile) { if (faviconFile) {
const faviconResp = await uploadFavicon(faviconFile) const faviconResp = await uploadFavicon(faviconFile)
if (faviconResp.url) { if (faviconResp.url) {
config = {
...config,
faviconUrl: faviconResp.url,
}
faviconFile = null faviconFile = null
faviconPreview = null faviconPreview = null
} }
config.faviconUrl = undefined
} }
}
// Trim function trimFields() {
const userStrings = [ const userStrings = [
"metaTitle", "metaTitle",
"platformTitle", "platformTitle",
@ -168,11 +161,18 @@
...config, ...config,
...trimmed, ...trimmed,
} }
}
async function saveConfig() {
saving = true
await saveFiles()
trimFields()
try { try {
// Update settings // Update settings
await organisation.save(config) await organisation.save(config)
await organisation.init() await init()
notifications.success("Branding settings updated") notifications.success("Branding settings updated")
} catch (e) { } catch (e) {
console.error("Branding updated failed", e) console.error("Branding updated failed", e)
@ -182,9 +182,10 @@
saving = false saving = false
} }
onMount(async () => { async function init() {
await organisation.init() if (!$organisation.loaded) {
await organisation.init()
}
config = { config = {
faviconUrl: $organisation.faviconUrl, faviconUrl: $organisation.faviconUrl,
logoUrl: $organisation.logoUrl, logoUrl: $organisation.logoUrl,
@ -197,6 +198,10 @@
metaImageUrl: $organisation.metaImageUrl, metaImageUrl: $organisation.metaImageUrl,
metaTitle: $organisation.metaTitle, metaTitle: $organisation.metaTitle,
} }
}
onMount(async () => {
await init()
mounted = true mounted = true
}) })
</script> </script>
@ -262,6 +267,7 @@
faviconFile = e.detail faviconFile = e.detail
faviconPreview = null faviconPreview = null
} else { } else {
faviconFile = null
clone.faviconUrl = "" clone.faviconUrl = ""
} }
config = clone config = clone
@ -408,7 +414,11 @@
Upgrade Upgrade
</Button> </Button>
{/if} {/if}
<Button on:click={saveConfig} cta disabled={saving || !updated || !init}> <Button
on:click={saveConfig}
cta
disabled={saving || !updated || !$organisation.loaded}
>
Save Save
</Button> </Button>
</div> </div>

View File

@ -31,7 +31,6 @@
import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte" import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte"
import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte" import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte"
import ScimBanner from "../_components/SCIMBanner.svelte" import ScimBanner from "../_components/SCIMBanner.svelte"
import { helpers } from "@budibase/shared-core"
export let userId export let userId
@ -91,7 +90,6 @@
$: readonly = !$auth.isAdmin || scimEnabled $: readonly = !$auth.isAdmin || scimEnabled
$: privileged = user?.admin?.global || user?.builder?.global $: privileged = user?.admin?.global || user?.builder?.global
$: nameLabel = getNameLabel(user) $: nameLabel = getNameLabel(user)
$: initials = helpers.getUserInitials(user)
$: filteredGroups = getFilteredGroups($groups, searchTerm) $: filteredGroups = getFilteredGroups($groups, searchTerm)
$: availableApps = getAvailableApps($apps, privileged, user?.roles) $: availableApps = getAvailableApps($apps, privileged, user?.roles)
$: userGroups = $groups.filter(x => { $: userGroups = $groups.filter(x => {

View File

@ -2,7 +2,6 @@ import { derived, writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { admin } from "stores/portal" import { admin } from "stores/portal"
import analytics from "analytics" import analytics from "analytics"
import getUserInitials from "helpers/userInitials.js"
export function createAuthStore() { export function createAuthStore() {
const auth = writable({ const auth = writable({
@ -14,12 +13,10 @@ export function createAuthStore() {
postLogout: false, postLogout: false,
}) })
const store = derived(auth, $store => { const store = derived(auth, $store => {
let initials = null
let isAdmin = false let isAdmin = false
let isBuilder = false let isBuilder = false
if ($store.user) { if ($store.user) {
const user = $store.user const user = $store.user
initials = getUserInitials(user)
isAdmin = !!user.admin?.global isAdmin = !!user.admin?.global
isBuilder = !!user.builder?.global isBuilder = !!user.builder?.global
} }
@ -30,7 +27,6 @@ export function createAuthStore() {
tenantSet: $store.tenantSet, tenantSet: $store.tenantSet,
loaded: $store.loaded, loaded: $store.loaded,
postLogout: $store.postLogout, postLogout: $store.postLogout,
initials,
isAdmin, isAdmin,
isBuilder, isBuilder,
isSSO: !!$store.user?.provider, isSSO: !!$store.user?.provider,

View File

@ -23,6 +23,7 @@ const DEFAULT_CONFIG = {
oidcCallbackUrl: "", oidcCallbackUrl: "",
googleCallbackUrl: "", googleCallbackUrl: "",
isSSOEnforced: false, isSSOEnforced: false,
loaded: false,
} }
export function createOrganisationStore() { export function createOrganisationStore() {
@ -32,7 +33,7 @@ export function createOrganisationStore() {
async function init() { async function init() {
const tenantId = get(auth).tenantId const tenantId = get(auth).tenantId
const settingsConfigDoc = await API.getTenantConfig(tenantId) const settingsConfigDoc = await API.getTenantConfig(tenantId)
set({ ...DEFAULT_CONFIG, ...settingsConfigDoc.config }) set({ ...DEFAULT_CONFIG, ...settingsConfigDoc.config, loaded: true })
} }
async function save(config) { async function save(config) {
@ -43,6 +44,10 @@ export function createOrganisationStore() {
delete storeConfig.googleDatasourceConfigured delete storeConfig.googleDatasourceConfigured
delete storeConfig.oidcCallbackUrl delete storeConfig.oidcCallbackUrl
delete storeConfig.googleCallbackUrl delete storeConfig.googleCallbackUrl
// delete internal store field
delete storeConfig.loaded
await API.saveConfig({ await API.saveConfig({
type: "settings", type: "settings",
config: { ...storeConfig, ...config }, config: { ...storeConfig, ...config },

View File

@ -11,10 +11,13 @@ export const createGridWebsocket = context => {
return return
} }
// Identify which table we are editing // Identify which table we are editing
socket.emit(GridSocketEvent.SelectTable, tableId, response => { socket.emit(
// handle initial connection info GridSocketEvent.SelectTable,
users.set(response.users) { tableId },
}) ({ users: gridUsers }) => {
users.set(gridUsers)
}
)
} }
// Built-in events // Built-in events
@ -26,29 +29,29 @@ export const createGridWebsocket = context => {
}) })
// User events // User events
socket.onOther(SocketEvent.UserUpdate, user => { socket.onOther(SocketEvent.UserUpdate, ({ user }) => {
users.actions.updateUser(user) users.actions.updateUser(user)
}) })
socket.onOther(SocketEvent.UserDisconnect, user => { socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => {
users.actions.removeUser(user) users.actions.removeUser(sessionId)
}) })
// Row events // Row events
socket.onOther(GridSocketEvent.RowChange, async data => { socket.onOther(GridSocketEvent.RowChange, async ({ id, row }) => {
if (data.id) { if (id) {
rows.actions.replaceRow(data.id, data.row) rows.actions.replaceRow(id, row)
} else if (data.row.id) { } else if (row.id) {
// Handle users table edge cased // Handle users table edge cased
await rows.actions.refreshRow(data.row.id) await rows.actions.refreshRow(row.id)
} }
}) })
// Table events // Table events
socket.onOther(GridSocketEvent.TableChange, data => { socket.onOther(GridSocketEvent.TableChange, ({ table: newTable }) => {
// Only update table if one exists. If the table was deleted then we don't // Only update table if one exists. If the table was deleted then we don't
// want to know - let the builder navigate away // want to know - let the builder navigate away
if (data.table) { if (newTable) {
table.set(data.table) table.set(newTable)
} }
}) })
@ -57,7 +60,7 @@ export const createGridWebsocket = context => {
// Notify selected cell changes // Notify selected cell changes
focusedCellId.subscribe($focusedCellId => { focusedCellId.subscribe($focusedCellId => {
socket.emit(GridSocketEvent.SelectCell, $focusedCellId) socket.emit(GridSocketEvent.SelectCell, { cellId: $focusedCellId })
}) })
return () => socket?.disconnect() return () => socket?.disconnect()

@ -1 +1 @@
Subproject commit cd06642b860111aa1bd3443ee10076ca3abf03c3 Subproject commit 01fbc8670021c5a275c2a1a36ee18b984eeafad5

View File

@ -14,6 +14,7 @@ import {
SearchFilters, SearchFilters,
Table, Table,
} from "@budibase/types" } from "@budibase/types"
import { db as dbCore } from "@budibase/backend-core"
enum SortOrder { enum SortOrder {
ASCENDING = "ascending", ASCENDING = "ascending",
@ -121,7 +122,11 @@ function typeCoercion(filters: SearchFilters, table: Table) {
const searchParam = filters[key] const searchParam = filters[key]
if (typeof searchParam === "object") { if (typeof searchParam === "object") {
for (let [property, value] of Object.entries(searchParam)) { for (let [property, value] of Object.entries(searchParam)) {
const column = table.schema[property] // We need to strip numerical prefixes here, so that we can look up
// the correct field name in the schema
const columnName = dbCore.removeKeyNumbering(property)
const column = table.schema[columnName]
// convert string inputs // convert string inputs
if (!column || typeof value !== "string") { if (!column || typeof value !== "string") {
continue continue

View File

@ -38,6 +38,9 @@ const SCHEMA: Integration = {
type: "password", type: "password",
required: true, required: true,
}, },
role: {
type: "string",
},
warehouse: { warehouse: {
type: "string", type: "string",
required: true, required: true,

View File

@ -7,7 +7,7 @@ import {
InternalTables, InternalTables,
} from "../../db/utils" } from "../../db/utils"
import { isEqual } from "lodash" import { isEqual } from "lodash"
import { ContextUser, UserMetadata, User } from "@budibase/types" import { ContextUser, UserMetadata, User, Database } from "@budibase/types"
export function combineMetadataAndUser( export function combineMetadataAndUser(
user: ContextUser, user: ContextUser,
@ -51,8 +51,10 @@ export function combineMetadataAndUser(
return null return null
} }
export async function rawUserMetadata() { export async function rawUserMetadata(db?: Database) {
const db = context.getAppDB() if (!db) {
db = context.getAppDB()
}
return ( return (
await db.allDocs( await db.allDocs(
getUserMetadataParams(null, { getUserMetadataParams(null, {
@ -64,30 +66,36 @@ export async function rawUserMetadata() {
export async function syncGlobalUsers() { export async function syncGlobalUsers() {
// sync user metadata // sync user metadata
const db = context.getAppDB() const dbs = [context.getDevAppDB(), context.getProdAppDB()]
const resp = await Promise.all([getGlobalUsers(), rawUserMetadata()]) for (let db of dbs) {
const users = resp[0] as User[] if (!(await db.exists())) {
const metadata = resp[1] as UserMetadata[]
const toWrite = []
for (let user of users) {
const combined = combineMetadataAndUser(user, metadata)
if (combined) {
toWrite.push(combined)
}
}
let foundEmails: string[] = []
for (let data of metadata) {
if (!data._id) {
continue continue
} }
const alreadyExisting = data.email && foundEmails.indexOf(data.email) !== -1 const resp = await Promise.all([getGlobalUsers(), rawUserMetadata(db)])
const globalId = getGlobalIDFromUserMetadataID(data._id) const users = resp[0] as User[]
if (!users.find(user => user._id === globalId) || alreadyExisting) { const metadata = resp[1] as UserMetadata[]
toWrite.push({ ...data, _deleted: true }) const toWrite = []
for (let user of users) {
const combined = combineMetadataAndUser(user, metadata)
if (combined) {
toWrite.push(combined)
}
} }
if (data.email) { let foundEmails: string[] = []
foundEmails.push(data.email) for (let data of metadata) {
if (!data._id) {
continue
}
const alreadyExisting =
data.email && foundEmails.indexOf(data.email) !== -1
const globalId = getGlobalIDFromUserMetadataID(data._id)
if (!users.find(user => user._id === globalId) || alreadyExisting) {
toWrite.push({ ...data, _deleted: true })
}
if (data.email) {
foundEmails.push(data.email)
}
} }
await db.bulkDocs(toWrite)
} }
await db.bulkDocs(toWrite)
} }

View File

@ -122,11 +122,8 @@ export async function getGlobalUsers(
delete user.forceResetPassword delete user.forceResetPassword
return user return user
}) })
if (!appId) {
return globalUsers
}
if (opts?.noProcessing) { if (opts?.noProcessing || !appId) {
return globalUsers return globalUsers
} else { } else {
// pass in the groups, meaning we don't actually need to retrieve them for // pass in the groups, meaning we don't actually need to retrieve them for

View File

@ -5,7 +5,7 @@ import http from "http"
import Koa from "koa" import Koa from "koa"
import { Datasource, Table, SocketSession, ContextUser } from "@budibase/types" import { Datasource, Table, SocketSession, ContextUser } from "@budibase/types"
import { gridSocket } from "./index" import { gridSocket } from "./index"
import { clearLock } from "../utilities/redis" import { clearLock, updateLock } from "../utilities/redis"
import { Socket } from "socket.io" import { Socket } from "socket.io"
import { BuilderSocketEvent } from "@budibase/shared-core" import { BuilderSocketEvent } from "@budibase/shared-core"
@ -16,7 +16,7 @@ export default class BuilderSocket extends BaseSocket {
async onConnect(socket?: Socket) { async onConnect(socket?: Socket) {
// Initial identification of selected app // Initial identification of selected app
socket?.on(BuilderSocketEvent.SelectApp, async (appId, callback) => { socket?.on(BuilderSocketEvent.SelectApp, async ({ appId }, callback) => {
await this.joinRoom(socket, appId) await this.joinRoom(socket, appId)
// Reply with all users in current room // Reply with all users in current room
@ -26,7 +26,8 @@ export default class BuilderSocket extends BaseSocket {
} }
async onDisconnect(socket: Socket) { async onDisconnect(socket: Socket) {
// Remove app lock from this user if they have no other connections // Remove app lock from this user if they have no other connections,
// and transfer it to someone else if possible
try { try {
// @ts-ignore // @ts-ignore
const session: SocketSession = socket.data const session: SocketSession = socket.data
@ -36,9 +37,26 @@ export default class BuilderSocket extends BaseSocket {
return _id === otherSession._id && sessionId !== otherSession.sessionId return _id === otherSession._id && sessionId !== otherSession.sessionId
}) })
if (!hasOtherSession && room) { if (!hasOtherSession && room) {
// Clear the lock from this user since they had no other sessions
// @ts-ignore // @ts-ignore
const user: ContextUser = { _id: socket.data._id } const user: ContextUser = { _id: socket.data._id }
await clearLock(room, user) await clearLock(room, user)
// Transfer lock ownership to the next oldest user
let otherSessions = sessions.filter(x => x._id !== _id).slice()
otherSessions.sort((a, b) => {
return a.connectedAt < b.connectedAt ? -1 : 1
})
const nextSession = otherSessions[0]
if (nextSession) {
const { _id, email, firstName, lastName } = nextSession
// @ts-ignore
const nextUser: ContextUser = { _id, email, firstName, lastName }
await updateLock(room, nextUser)
this.io.to(room).emit(BuilderSocketEvent.LockTransfer, {
userId: _id,
})
}
} }
} catch (e) { } catch (e) {
// This is fine, just means this user didn't hold the lock // This is fine, just means this user didn't hold the lock

View File

@ -15,7 +15,7 @@ export default class GridSocket extends BaseSocket {
async onConnect(socket: Socket) { async onConnect(socket: Socket) {
// Initial identification of connected spreadsheet // Initial identification of connected spreadsheet
socket.on(GridSocketEvent.SelectTable, async (tableId, callback) => { socket.on(GridSocketEvent.SelectTable, async ({ tableId }, callback) => {
await this.joinRoom(socket, tableId) await this.joinRoom(socket, tableId)
// Reply with all users in current room // Reply with all users in current room
@ -24,7 +24,7 @@ export default class GridSocket extends BaseSocket {
}) })
// Handle users selecting a new cell // Handle users selecting a new cell
socket.on(GridSocketEvent.SelectCell, cellId => { socket.on(GridSocketEvent.SelectCell, ({ cellId }) => {
this.updateUser(socket, { focusedCellId: cellId }) this.updateUser(socket, { focusedCellId: cellId })
}) })
} }

View File

@ -77,6 +77,7 @@ export class BaseSocket {
firstName, firstName,
lastName, lastName,
sessionId: socket.id, sessionId: socket.id,
connectedAt: Date.now(),
} }
next() next()
} }
@ -173,7 +174,9 @@ export class BaseSocket {
) )
const prunedSessionIds = sessionIds.filter((id, idx) => { const prunedSessionIds = sessionIds.filter((id, idx) => {
if (!sessionsExist[idx]) { if (!sessionsExist[idx]) {
this.io.to(room).emit(SocketEvent.UserDisconnect, sessionIds[idx]) this.io.to(room).emit(SocketEvent.UserDisconnect, {
sessionId: sessionIds[idx],
})
return false return false
} }
return true return true
@ -216,7 +219,9 @@ export class BaseSocket {
} }
// Notify other users // Notify other users
socket.to(room).emit(SocketEvent.UserUpdate, user) socket.to(room).emit(SocketEvent.UserUpdate, {
user,
})
} }
// Disconnects a socket from its current room // Disconnects a socket from its current room
@ -242,7 +247,7 @@ export class BaseSocket {
) )
// Notify other users // Notify other users
socket.to(room).emit(SocketEvent.UserDisconnect, sessionId) socket.to(room).emit(SocketEvent.UserDisconnect, { sessionId })
} }
// Updates a connected user's metadata, assuming a room change is not required. // Updates a connected user's metadata, assuming a room change is not required.

View File

@ -85,6 +85,7 @@ export enum BuilderSocketEvent {
SelectApp = "SelectApp", SelectApp = "SelectApp",
TableChange = "TableChange", TableChange = "TableChange",
DatasourceChange = "DatasourceChange", DatasourceChange = "DatasourceChange",
LockTransfer = "LockTransfer",
} }
export const SocketSessionTTL = 60 export const SocketSessionTTL = 60

View File

@ -35,7 +35,10 @@ export const getUserInitials = (user: User) => {
let initials = "" let initials = ""
initials += user.firstName ? user.firstName[0] : "" initials += user.firstName ? user.firstName[0] : ""
initials += user.lastName ? user.lastName[0] : "" initials += user.lastName ? user.lastName[0] : ""
return initials === "" ? user.email[0] : initials if (initials !== "") {
return initials
}
return user.email?.[0] || "U"
} }
/** /**

View File

@ -5,4 +5,5 @@ export interface SocketSession {
lastName?: string lastName?: string
sessionId: string sessionId: string
room?: string room?: string
connectedAt: number
} }

View File

@ -23,6 +23,7 @@ import {
isSettingsConfig, isSettingsConfig,
isSMTPConfig, isSMTPConfig,
OIDCConfigs, OIDCConfigs,
SettingsBrandingConfig,
SettingsInnerConfig, SettingsInnerConfig,
SSOConfig, SSOConfig,
SSOConfigType, SSOConfigType,
@ -142,13 +143,29 @@ async function hasActivatedConfig(ssoConfigs?: SSOConfigs) {
return !!Object.values(ssoConfigs).find(c => c?.activated) return !!Object.values(ssoConfigs).find(c => c?.activated)
} }
async function verifySettingsConfig(config: SettingsInnerConfig) { async function verifySettingsConfig(
config: SettingsInnerConfig & SettingsBrandingConfig,
existingConfig?: SettingsInnerConfig & SettingsBrandingConfig
) {
if (config.isSSOEnforced) { if (config.isSSOEnforced) {
const valid = await hasActivatedConfig() const valid = await hasActivatedConfig()
if (!valid) { if (!valid) {
throw new Error("Cannot enforce SSO without an activated configuration") throw new Error("Cannot enforce SSO without an activated configuration")
} }
} }
// always preserve file attributes
// these should be set via upload instead
// only allow for deletion by checking empty string to bypass this behaviour
if (existingConfig && config.logoUrl !== "") {
config.logoUrl = existingConfig.logoUrl
config.logoUrlEtag = existingConfig.logoUrlEtag
}
if (existingConfig && config.faviconUrl !== "") {
config.faviconUrl = existingConfig.faviconUrl
config.faviconUrlEtag = existingConfig.faviconUrlEtag
}
} }
async function verifySSOConfig(type: SSOConfigType, config: SSOConfig) { async function verifySSOConfig(type: SSOConfigType, config: SSOConfig) {
@ -198,7 +215,7 @@ export async function save(ctx: UserCtx<Config>) {
await email.verifyConfig(config) await email.verifyConfig(config)
break break
case ConfigType.SETTINGS: case ConfigType.SETTINGS:
await verifySettingsConfig(config) await verifySettingsConfig(config, existingConfig?.config)
break break
case ConfigType.GOOGLE: case ConfigType.GOOGLE:
await verifyGoogleConfig(config) await verifyGoogleConfig(config)
@ -320,14 +337,15 @@ export async function publicSettings(
) )
} }
if (branding.faviconUrl && branding.faviconUrl !== "") { // enrich the favicon url - empty url means deleted
// @ts-ignore const faviconUrl =
config.faviconUrl = objectStore.getGlobalFileUrl( branding.faviconUrl && branding.faviconUrl !== ""
"settings", ? objectStore.getGlobalFileUrl(
"faviconUrl", "settings",
branding.faviconUrl "faviconUrl",
) branding.faviconUrlEtag
} )
: undefined
// google // google
const googleConfig = await configs.getGoogleConfig() const googleConfig = await configs.getGoogleConfig()
@ -352,6 +370,7 @@ export async function publicSettings(
config: { config: {
...config, ...config,
...branding, ...branding,
...{ faviconUrl },
google, google,
googleDatasourceConfigured, googleDatasourceConfigured,
oidc, oidc,