Merge branch 'master' into fix/automation-query-rows-table-with-spaces

This commit is contained in:
Michael Drury 2025-01-13 11:26:54 +00:00 committed by GitHub
commit 39f2ad021d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 118 additions and 97 deletions

View File

@ -442,13 +442,11 @@
const onUpdateUserInvite = async (invite, role) => { const onUpdateUserInvite = async (invite, role) => {
let updateBody = { let updateBody = {
code: invite.code,
apps: { apps: {
...invite.apps, ...invite.apps,
[prodAppId]: role, [prodAppId]: role,
}, },
} }
if (role === Constants.Roles.CREATOR) { if (role === Constants.Roles.CREATOR) {
updateBody.builder = updateBody.builder || {} updateBody.builder = updateBody.builder || {}
updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId] updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId]
@ -456,7 +454,7 @@
} else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) { } else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) {
invite.builder.apps = [] invite.builder.apps = []
} }
await users.updateInvite(updateBody) await users.updateInvite(invite.code, updateBody)
await filterInvites(query) await filterInvites(query)
} }
@ -470,8 +468,7 @@
let updated = { ...invite } let updated = { ...invite }
delete updated.info.apps[prodAppId] delete updated.info.apps[prodAppId]
return await users.updateInvite({ return await users.updateInvite(updated.code, {
code: updated.code,
apps: updated.apps, apps: updated.apps,
}) })
} }

View File

@ -191,8 +191,14 @@
? "View errors" ? "View errors"
: "View error"} : "View error"}
on:dismiss={async () => { on:dismiss={async () => {
await automationStore.actions.clearLogErrors({ appId }) const automationId = Object.keys(automationErrors[appId] || {})[0]
if (automationId) {
await automationStore.actions.clearLogErrors({
appId,
automationId,
})
await appsStore.load() await appsStore.load()
}
}} }}
message={automationErrorMessage(appId)} message={automationErrorMessage(appId)}
/> />

View File

@ -52,7 +52,7 @@
] ]
const removeUser = async id => { const removeUser = async id => {
await groups.actions.removeUser(groupId, id) await groups.removeUser(groupId, id)
fetchGroupUsers.refresh() fetchGroupUsers.refresh()
} }

View File

@ -251,6 +251,7 @@
passwordModal.show() passwordModal.show()
await fetch.refresh() await fetch.refresh()
} catch (error) { } catch (error) {
console.error(error)
notifications.error("Error creating user") notifications.error("Error creating user")
} }
} }

View File

@ -1,41 +1,71 @@
import { writable } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { update } from "lodash"
import { licensing } from "." import { licensing } from "."
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
import {
DeleteInviteUsersRequest,
InviteUsersRequest,
SearchUsersRequest,
SearchUsersResponse,
UpdateInviteRequest,
User,
UserIdentifier,
UnsavedUser,
} from "@budibase/types"
import { BudiStore } from "../BudiStore"
export function createUsersStore() { interface UserInfo {
const { subscribe, set } = writable({}) email: string
password: string
forceResetPassword?: boolean
role: keyof typeof Constants.BudibaseRoles
}
// opts can contain page and search params type UserState = SearchUsersResponse & SearchUsersRequest
async function search(opts = {}) {
class UserStore extends BudiStore<UserState> {
constructor() {
super({
data: [],
})
}
async search(opts: SearchUsersRequest = {}) {
const paged = await API.searchUsers(opts) const paged = await API.searchUsers(opts)
set({ this.set({
...paged, ...paged,
...opts, ...opts,
}) })
return paged return paged
} }
async function get(userId) { async get(userId: string) {
try { try {
return await API.getUser(userId) return await API.getUser(userId)
} catch (err) { } catch (err) {
return null return null
} }
} }
const fetch = async () => {
async fetch() {
return await API.getUsers() return await API.getUsers()
} }
// One or more users. async onboard(payload: InviteUsersRequest) {
async function onboard(payload) {
return await API.onboardUsers(payload) return await API.onboardUsers(payload)
} }
async function invite(payload) { async invite(
const users = payload.map(user => { payload: {
admin?: boolean
builder?: boolean
creator?: boolean
email: string
apps?: any[]
groups?: any[]
}[]
) {
const users: InviteUsersRequest = payload.map(user => {
let builder = undefined let builder = undefined
if (user.admin || user.builder) { if (user.admin || user.builder) {
builder = { global: true } builder = { global: true }
@ -55,11 +85,16 @@ export function createUsersStore() {
return API.inviteUsers(users) return API.inviteUsers(users)
} }
async function removeInvites(payload) { async removeInvites(payload: DeleteInviteUsersRequest) {
return API.removeUserInvites(payload) return API.removeUserInvites(payload)
} }
async function acceptInvite(inviteCode, password, firstName, lastName) { async acceptInvite(
inviteCode: string,
password: string,
firstName: string,
lastName?: string
) {
return API.acceptInvite({ return API.acceptInvite({
inviteCode, inviteCode,
password, password,
@ -68,21 +103,25 @@ export function createUsersStore() {
}) })
} }
async function fetchInvite(inviteCode) { async fetchInvite(inviteCode: string) {
return API.getUserInvite(inviteCode) return API.getUserInvite(inviteCode)
} }
async function getInvites() { async getInvites() {
return API.getUserInvites() return API.getUserInvites()
} }
async function updateInvite(invite) { async updateInvite(code: string, invite: UpdateInviteRequest) {
return API.updateUserInvite(invite.code, invite) return API.updateUserInvite(code, invite)
} }
async function create(data) { async getUserCountByApp(appId: string) {
let mappedUsers = data.users.map(user => { return await API.getUserCountByApp(appId)
const body = { }
async create(data: { users: UserInfo[]; groups: any[] }) {
let mappedUsers: UnsavedUser[] = data.users.map((user: any) => {
const body: UnsavedUser = {
email: user.email, email: user.email,
password: user.password, password: user.password,
roles: {}, roles: {},
@ -92,17 +131,17 @@ export function createUsersStore() {
} }
switch (user.role) { switch (user.role) {
case "appUser": case Constants.BudibaseRoles.AppUser:
body.builder = { global: false } body.builder = { global: false }
body.admin = { global: false } body.admin = { global: false }
break break
case "developer": case Constants.BudibaseRoles.Developer:
body.builder = { global: true } body.builder = { global: true }
break break
case "creator": case Constants.BudibaseRoles.Creator:
body.builder = { creator: true, global: false } body.builder = { creator: true, global: false }
break break
case "admin": case Constants.BudibaseRoles.Admin:
body.admin = { global: true } body.admin = { global: true }
body.builder = { global: true } body.builder = { global: true }
break break
@ -111,43 +150,47 @@ export function createUsersStore() {
return body return body
}) })
const response = await API.createUsers(mappedUsers, data.groups) const response = await API.createUsers(mappedUsers, data.groups)
licensing.setQuotaUsage()
// re-search from first page // re-search from first page
await search() await this.search()
return response return response
} }
async function del(id) { async delete(id: string) {
await API.deleteUser(id) await API.deleteUser(id)
update(users => users.filter(user => user._id !== id)) licensing.setQuotaUsage()
} }
async function getUserCountByApp(appId) { async bulkDelete(users: UserIdentifier[]) {
return await API.getUserCountByApp(appId) const res = API.deleteUsers(users)
licensing.setQuotaUsage()
return res
} }
async function bulkDelete(users) { async save(user: User) {
return API.deleteUsers(users) const res = await API.saveUser(user)
licensing.setQuotaUsage()
return res
} }
async function save(user) { async addAppBuilder(userId: string, appId: string) {
return await API.saveUser(user)
}
async function addAppBuilder(userId, appId) {
return await API.addAppBuilder(userId, appId) return await API.addAppBuilder(userId, appId)
} }
async function removeAppBuilder(userId, appId) { async removeAppBuilder(userId: string, appId: string) {
return await API.removeAppBuilder(userId, appId) return await API.removeAppBuilder(userId, appId)
} }
async function getAccountHolder() { async getAccountHolder() {
return await API.getAccountHolder() return await API.getAccountHolder()
} }
const getUserRole = user => { getUserRole(user?: User & { tenantOwnerEmail?: string }) {
if (user && user.email === user.tenantOwnerEmail) { if (!user) {
return Constants.BudibaseRoles.AppUser
}
if (user.email === user.tenantOwnerEmail) {
return Constants.BudibaseRoles.Owner return Constants.BudibaseRoles.Owner
} else if (sdk.users.isAdmin(user)) { } else if (sdk.users.isAdmin(user)) {
return Constants.BudibaseRoles.Admin return Constants.BudibaseRoles.Admin
@ -159,38 +202,6 @@ export function createUsersStore() {
return Constants.BudibaseRoles.AppUser return Constants.BudibaseRoles.AppUser
} }
} }
const refreshUsage =
fn =>
async (...args) => {
const response = await fn(...args)
await licensing.setQuotaUsage()
return response
}
return {
subscribe,
search,
get,
getUserRole,
fetch,
invite,
onboard,
fetchInvite,
getInvites,
removeInvites,
updateInvite,
getUserCountByApp,
addAppBuilder,
removeAppBuilder,
// any operation that adds or deletes users
acceptInvite,
create: refreshUsage(create),
save: refreshUsage(save),
bulkDelete: refreshUsage(bulkDelete),
delete: refreshUsage(del),
getAccountHolder,
}
} }
export const users = createUsersStore() export const users = new UserStore()

View File

@ -21,11 +21,12 @@ import {
SaveUserResponse, SaveUserResponse,
SearchUsersRequest, SearchUsersRequest,
SearchUsersResponse, SearchUsersResponse,
UnsavedUser,
UpdateInviteRequest, UpdateInviteRequest,
UpdateInviteResponse, UpdateInviteResponse,
UpdateSelfMetadataRequest, UpdateSelfMetadataRequest,
UpdateSelfMetadataResponse, UpdateSelfMetadataResponse,
User, UserIdentifier,
} from "@budibase/types" } from "@budibase/types"
import { BaseAPIClient } from "./types" import { BaseAPIClient } from "./types"
@ -38,14 +39,9 @@ export interface UserEndpoints {
createAdminUser: ( createAdminUser: (
user: CreateAdminUserRequest user: CreateAdminUserRequest
) => Promise<CreateAdminUserResponse> ) => Promise<CreateAdminUserResponse>
saveUser: (user: User) => Promise<SaveUserResponse> saveUser: (user: UnsavedUser) => Promise<SaveUserResponse>
deleteUser: (userId: string) => Promise<DeleteUserResponse> deleteUser: (userId: string) => Promise<DeleteUserResponse>
deleteUsers: ( deleteUsers: (users: UserIdentifier[]) => Promise<BulkUserDeleted | undefined>
users: Array<{
userId: string
email: string
}>
) => Promise<BulkUserDeleted | undefined>
onboardUsers: (data: InviteUsersRequest) => Promise<InviteUsersResponse> onboardUsers: (data: InviteUsersRequest) => Promise<InviteUsersResponse>
getUserInvite: (code: string) => Promise<CheckInviteResponse> getUserInvite: (code: string) => Promise<CheckInviteResponse>
getUserInvites: () => Promise<GetUserInvitesResponse> getUserInvites: () => Promise<GetUserInvitesResponse>
@ -60,7 +56,7 @@ export interface UserEndpoints {
getAccountHolder: () => Promise<LookupAccountHolderResponse> getAccountHolder: () => Promise<LookupAccountHolderResponse>
searchUsers: (data: SearchUsersRequest) => Promise<SearchUsersResponse> searchUsers: (data: SearchUsersRequest) => Promise<SearchUsersResponse>
createUsers: ( createUsers: (
users: User[], users: UnsavedUser[],
groups: any[] groups: any[]
) => Promise<BulkUserCreated | undefined> ) => Promise<BulkUserCreated | undefined>
updateUserInvite: ( updateUserInvite: (

View File

@ -22,6 +22,8 @@ export interface UserDetails {
password?: string password?: string
} }
export type UnsavedUser = Omit<User, "tenantId">
export interface BulkUserRequest { export interface BulkUserRequest {
delete?: { delete?: {
users: Array<{ users: Array<{
@ -31,7 +33,7 @@ export interface BulkUserRequest {
} }
create?: { create?: {
roles?: any[] roles?: any[]
users: User[] users: UnsavedUser[]
groups: any[] groups: any[]
} }
} }
@ -124,7 +126,7 @@ export interface AcceptUserInviteRequest {
inviteCode: string inviteCode: string
password: string password: string
firstName: string firstName: string
lastName: string lastName?: string
} }
export interface AcceptUserInviteResponse { export interface AcceptUserInviteResponse {

View File

@ -33,6 +33,7 @@ import {
SaveUserResponse, SaveUserResponse,
SearchUsersRequest, SearchUsersRequest,
SearchUsersResponse, SearchUsersResponse,
UnsavedUser,
UpdateInviteRequest, UpdateInviteRequest,
UpdateInviteResponse, UpdateInviteResponse,
User, User,
@ -49,6 +50,7 @@ import {
tenancy, tenancy,
db, db,
locks, locks,
context,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users" import { checkAnyUserExists } from "../../../utilities/users"
import { isEmailConfigured } from "../../../utilities/email" import { isEmailConfigured } from "../../../utilities/email"
@ -66,10 +68,11 @@ const generatePassword = (length: number) => {
.slice(0, length) .slice(0, length)
} }
export const save = async (ctx: UserCtx<User, SaveUserResponse>) => { export const save = async (ctx: UserCtx<UnsavedUser, SaveUserResponse>) => {
try { try {
const currentUserId = ctx.user?._id const currentUserId = ctx.user?._id
const requestUser = ctx.request.body const tenantId = context.getTenantId()
const requestUser: User = { ...ctx.request.body, tenantId }
// Do not allow the account holder role to be changed // Do not allow the account holder role to be changed
if ( if (
@ -151,7 +154,12 @@ export const bulkUpdate = async (
let created, deleted let created, deleted
try { try {
if (input.create) { if (input.create) {
created = await bulkCreate(input.create.users, input.create.groups) const tenantId = context.getTenantId()
const users: User[] = input.create.users.map(user => ({
...user,
tenantId,
}))
created = await bulkCreate(users, input.create.groups)
} }
if (input.delete) { if (input.delete) {
deleted = await bulkDelete(input.delete.users, currentUserId) deleted = await bulkDelete(input.delete.users, currentUserId)