diff --git a/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte b/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte
index 1be019b83e..589df5c599 100644
--- a/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte
+++ b/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte
@@ -11,7 +11,10 @@
ActionMenu,
MenuItem,
Modal,
+ Pagination,
} from "@budibase/bbui"
+ import { fetchData } from "@budibase/frontend-core"
+ import { API } from "api"
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
import { createPaginationStore } from "helpers/pagination"
import { users, apps, groups, auth, features } from "stores/portal"
@@ -28,6 +31,18 @@
export let groupId
+ const fetchGroupUsers = fetchData({
+ API,
+ datasource: {
+ type: "groupUser",
+ },
+ options: {
+ query: {
+ groupId,
+ },
+ },
+ })
+
$: userSchema = {
email: {
width: "1fr",
@@ -71,16 +86,15 @@
let popover
let searchTerm = ""
let prevSearch = undefined
- let pageInfo = createPaginationStore()
+ let searchUsersPageInfo = createPaginationStore()
let loaded = false
let editModal, deleteModal
$: scimEnabled = $features.isScimEnabled
$: readonly = !$auth.isAdmin || scimEnabled
- $: page = $pageInfo.page
- $: fetchUsers(page, searchTerm)
+ $: page = $searchUsersPageInfo.page
+ $: searchUsers(page, searchTerm)
$: group = $groups.find(x => x._id === groupId)
- $: filtered = $users.data
$: groupApps = $apps
.filter(app =>
groups.actions
@@ -97,20 +111,20 @@
}
}
- async function fetchUsers(page, search) {
- if ($pageInfo.loading) {
+ async function searchUsers(page, search) {
+ if ($searchUsersPageInfo.loading) {
return
}
// need to remove the page if they've started searching
if (search && !prevSearch) {
- pageInfo.reset()
+ searchUsersPageInfo.reset()
page = undefined
}
prevSearch = search
try {
- pageInfo.loading()
+ searchUsersPageInfo.loading()
await users.search({ page, email: search })
- pageInfo.fetched($users.hasNextPage, $users.nextPage)
+ searchUsersPageInfo.fetched($users.hasNextPage, $users.nextPage)
} catch (error) {
notifications.error("Error getting user list")
}
@@ -136,6 +150,7 @@
const removeUser = async id => {
await groups.actions.removeUser(groupId, id)
+ fetchGroupUsers.refresh()
}
const removeApp = async app => {
@@ -203,15 +218,21 @@
labelKey="email"
selected={group.users?.map(user => user._id)}
list={$users.data}
- on:select={e => groups.actions.addUser(groupId, e.detail)}
- on:deselect={e => groups.actions.removeUser(groupId, e.detail)}
+ on:select={async e => {
+ await groups.actions.addUser(groupId, e.detail)
+ fetchGroupUsers.getInitialData()
+ }}
+ on:deselect={async e => {
+ await groups.actions.removeUser(groupId, e.detail)
+ fetchGroupUsers.getInitialData()
+ }}
/>
This user group doesn't have any users
+
+
diff --git a/packages/builder/src/stores/portal/groups.js b/packages/builder/src/stores/portal/groups.js
index eda3961e2b..c7a54c7e6d 100644
--- a/packages/builder/src/stores/portal/groups.js
+++ b/packages/builder/src/stores/portal/groups.js
@@ -28,7 +28,7 @@ export function createGroupsStore() {
// on the backend anyway
if (get(licensing).groupsEnabled) {
const groups = await API.getGroups()
- store.set(groups)
+ store.set(groups.data)
}
},
diff --git a/packages/frontend-core/src/api/groups.js b/packages/frontend-core/src/api/groups.js
index c27f11e0ea..cbc5bfd72a 100644
--- a/packages/frontend-core/src/api/groups.js
+++ b/packages/frontend-core/src/api/groups.js
@@ -52,6 +52,20 @@ export const buildGroupsEndpoints = API => {
})
},
+ /**
+ * Gets a group users by the group id
+ */
+ getGroupUsers: async ({ id, bookmark }) => {
+ let url = `/api/global/groups/${id}/users?`
+ if (bookmark) {
+ url += `bookmark=${bookmark}`
+ }
+
+ return await API.get({
+ url,
+ })
+ },
+
/**
* Adds users to a group
* @param groupId The group to update
diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js
index f68b37dcca..18a00c08d5 100644
--- a/packages/frontend-core/src/fetch/DataFetch.js
+++ b/packages/frontend-core/src/fetch/DataFetch.js
@@ -362,13 +362,35 @@ export default class DataFetch {
return
}
this.store.update($store => ({ ...$store, loading: true }))
- const { rows, info, error } = await this.getPage()
+ const { rows, info, error, cursor } = await this.getPage()
+
+ let { cursors } = get(this.store)
+ const { pageNumber } = get(this.store)
+
+ if (!rows.length && pageNumber > 0) {
+ // If the full page is gone but we have previous pages, navigate to the previous page
+ this.store.update($store => ({
+ ...$store,
+ loading: false,
+ cursors: cursors.slice(0, pageNumber),
+ }))
+ return await this.prevPage()
+ }
+
+ const currentNextCursor = cursors[pageNumber + 1]
+ if (currentNextCursor != cursor) {
+ // If the current cursor changed, all the next pages need to be updated, so we mark them as stale
+ cursors = cursors.slice(0, pageNumber + 1)
+ cursors[pageNumber + 1] = cursor
+ }
+
this.store.update($store => ({
...$store,
rows,
info,
loading: false,
error,
+ cursors,
}))
}
diff --git a/packages/frontend-core/src/fetch/GroupUserFetch.js b/packages/frontend-core/src/fetch/GroupUserFetch.js
new file mode 100644
index 0000000000..b0ca9a5388
--- /dev/null
+++ b/packages/frontend-core/src/fetch/GroupUserFetch.js
@@ -0,0 +1,50 @@
+import { get } from "svelte/store"
+import DataFetch from "./DataFetch.js"
+import { TableNames } from "../constants"
+
+export default class GroupUserFetch extends DataFetch {
+ constructor(opts) {
+ super({
+ ...opts,
+ datasource: {
+ tableId: TableNames.USERS,
+ },
+ })
+ }
+
+ determineFeatureFlags() {
+ return {
+ supportsSearch: true,
+ supportsSort: false,
+ supportsPagination: true,
+ }
+ }
+
+ async getDefinition() {
+ return {
+ schema: {},
+ }
+ }
+
+ async getData() {
+ const { query, cursor } = get(this.store)
+ try {
+ const res = await this.API.getGroupUsers({
+ id: query.groupId,
+ bookmark: cursor,
+ })
+
+ return {
+ rows: res?.users || [],
+ hasNextPage: res?.hasNextPage || false,
+ cursor: res?.bookmark || null,
+ }
+ } catch (error) {
+ return {
+ rows: [],
+ hasNextPage: false,
+ error,
+ }
+ }
+ }
+}
diff --git a/packages/frontend-core/src/fetch/fetchData.js b/packages/frontend-core/src/fetch/fetchData.js
index 4974816496..c4968eabc0 100644
--- a/packages/frontend-core/src/fetch/fetchData.js
+++ b/packages/frontend-core/src/fetch/fetchData.js
@@ -6,6 +6,7 @@ import NestedProviderFetch from "./NestedProviderFetch.js"
import FieldFetch from "./FieldFetch.js"
import JSONArrayFetch from "./JSONArrayFetch.js"
import UserFetch from "./UserFetch.js"
+import GroupUserFetch from "./GroupUserFetch.js"
const DataFetchMap = {
table: TableFetch,
@@ -13,6 +14,7 @@ const DataFetchMap = {
query: QueryFetch,
link: RelationshipFetch,
user: UserFetch,
+ groupUser: GroupUserFetch,
// Client specific datasource types
provider: NestedProviderFetch,
diff --git a/packages/pro b/packages/pro
index 3a6dba1a72..4d640bfa74 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit 3a6dba1a7292384553900a284fa63422ee070bfa
+Subproject commit 4d640bfa74945d2e8675af0022fba42c738679f0
diff --git a/packages/types/src/documents/global/userGroup.ts b/packages/types/src/documents/global/userGroup.ts
index fedd8426f0..b74e59c020 100644
--- a/packages/types/src/documents/global/userGroup.ts
+++ b/packages/types/src/documents/global/userGroup.ts
@@ -1,3 +1,4 @@
+import { PaginationResponse } from "../../api"
import { Document } from "../document"
export interface UserGroup extends Document {
@@ -21,3 +22,15 @@ export interface GroupUser {
export interface UserGroupRoles {
[key: string]: string
}
+
+export interface SearchGroupRequest {}
+export interface SearchGroupResponse {
+ data: UserGroup[]
+}
+
+export interface SearchUserGroupResponse extends PaginationResponse {
+ users: {
+ _id: any
+ email: any
+ }[]
+}
diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts
index 2cccd3be6a..58b3b7e5bc 100644
--- a/packages/types/src/sdk/db.ts
+++ b/packages/types/src/sdk/db.ts
@@ -65,6 +65,7 @@ export type DatabaseQueryOpts = {
key?: string
keys?: string[]
group?: boolean
+ startkey_docid?: string
}
export const isDocument = (doc: any): doc is Document => {
diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts
index d9ebc87517..33335379c0 100644
--- a/packages/worker/src/api/controllers/global/users.ts
+++ b/packages/worker/src/api/controllers/global/users.ts
@@ -69,9 +69,11 @@ const bulkCreate = async (users: User[], groupIds: string[]) => {
return await userSdk.bulkCreate(users, groupIds)
}
-export const bulkUpdate = async (ctx: any) => {
+export const bulkUpdate = async (
+ ctx: Ctx
+) => {
const currentUserId = ctx.user._id
- const input = ctx.request.body as BulkUserRequest
+ const input = ctx.request.body
let created, deleted
try {
if (input.create) {
@@ -83,7 +85,7 @@ export const bulkUpdate = async (ctx: any) => {
} catch (err: any) {
ctx.throw(err.status || 400, err?.message || err)
}
- ctx.body = { created, deleted } as BulkUserResponse
+ ctx.body = { created, deleted }
}
const parseBooleanParam = (param: any) => {
@@ -184,15 +186,15 @@ export const destroy = async (ctx: any) => {
}
}
-export const getAppUsers = async (ctx: any) => {
- const body = ctx.request.body as SearchUsersRequest
+export const getAppUsers = async (ctx: Ctx) => {
+ const body = ctx.request.body
const users = await userSdk.getUsersByAppAccess(body?.appId)
ctx.body = { data: users }
}
-export const search = async (ctx: any) => {
- const body = ctx.request.body as SearchUsersRequest
+export const search = async (ctx: Ctx) => {
+ const body = ctx.request.body
if (body.paginated === false) {
await getAppUsers(ctx)
@@ -238,8 +240,8 @@ export const tenantUserLookup = async (ctx: any) => {
/*
Encapsulate the app user onboarding flows here.
*/
-export const onboardUsers = async (ctx: any) => {
- const request = ctx.request.body as InviteUsersRequest | BulkUserRequest
+export const onboardUsers = async (ctx: Ctx) => {
+ const request = ctx.request.body
const isBulkCreate = "create" in request
const emailConfigured = await isEmailConfigured()
@@ -255,7 +257,7 @@ export const onboardUsers = async (ctx: any) => {
} else if (emailConfigured) {
onboardingResponse = await inviteMultiple(ctx)
} else if (!emailConfigured) {
- const inviteRequest = ctx.request.body as InviteUsersRequest
+ const inviteRequest = ctx.request.body
let createdPasswords: any = {}
@@ -295,10 +297,10 @@ export const onboardUsers = async (ctx: any) => {
}
}
-export const invite = async (ctx: any) => {
- const request = ctx.request.body as InviteUserRequest
+export const invite = async (ctx: Ctx) => {
+ const request = ctx.request.body
- let multiRequest = [request] as InviteUsersRequest
+ let multiRequest = [request]
const response = await userSdk.invite(multiRequest)
// explicitly throw for single user invite
@@ -318,8 +320,8 @@ export const invite = async (ctx: any) => {
}
}
-export const inviteMultiple = async (ctx: any) => {
- const request = ctx.request.body as InviteUsersRequest
+export const inviteMultiple = async (ctx: Ctx) => {
+ const request = ctx.request.body
ctx.body = await userSdk.invite(request)
}
@@ -424,7 +426,6 @@ export const inviteAccept = async (
if (err.code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
// explicitly re-throw limit exceeded errors
ctx.throw(400, err)
- return
}
console.warn("Error inviting user", err)
ctx.throw(400, "Unable to create new user, invitation invalid.")