diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.js index 44dc4f2d3e..30869da68e 100644 --- a/packages/backend-core/src/security/roles.js +++ b/packages/backend-core/src/security/roles.js @@ -203,15 +203,24 @@ exports.getAllRoles = async appId => { if (appId) { return doWithDB(appId, internal) } else { - return internal(getAppDB()) + let appDB + try { + appDB = getAppDB() + } catch (error) { + // We don't have any apps, so we'll just use the built-in roles + } + return internal(appDB) } async function internal(db) { - const body = await db.allDocs( - getRoleParams(null, { - include_docs: true, - }) - ) - let roles = body.rows.map(row => row.doc) + let roles = [] + if (db) { + const body = await db.allDocs( + getRoleParams(null, { + include_docs: true, + }) + ) + roles = body.rows.map(row => row.doc) + } const builtinRoles = exports.getBuiltinRoles() // need to combine builtin with any DB record of them (for sake of permissions) diff --git a/packages/bbui/src/Form/Core/PickerDropdown.svelte b/packages/bbui/src/Form/Core/PickerDropdown.svelte index fbe43717ba..28cb2b2a4e 100644 --- a/packages/bbui/src/Form/Core/PickerDropdown.svelte +++ b/packages/bbui/src/Form/Core/PickerDropdown.svelte @@ -15,7 +15,6 @@ export let id = null export let placeholder = "Choose an option or type" export let disabled = false - export let readonly = false export let updateOnChange = true export let error = null export let secondaryOptions = [] @@ -35,6 +34,7 @@ export let isOptionSelected = () => false export let isPlaceholder = false export let placeholderOption = null + export let showClearIcon = true const dispatch = createEventDispatcher() let primaryOpen = false @@ -50,17 +50,11 @@ } const updateValue = newValue => { - if (readonly) { - return - } dispatch("change", newValue) } const onClickSecondary = () => { dispatch("click") - if (readonly) { - return - } secondaryOpen = true } @@ -80,24 +74,15 @@ } const onBlur = event => { - if (readonly) { - return - } focus = false updateValue(event.target.value) } const onInput = event => { - if (readonly || !updateOnChange) { - return - } updateValue(event.target.value) } const updateValueOnEnter = event => { - if (readonly) { - return - } if (event.key === "Enter") { updateValue(event.target.value) } @@ -140,11 +125,12 @@ value={primaryLabel || ""} placeholder={placeholder || ""} {disabled} - {readonly} + readonly class="spectrum-Textfield-input spectrum-InputGroup-input" class:labelPadding={iconData} + class:open={primaryOpen} /> - {#if primaryValue} + {#if primaryValue && showClearIcon} + {/if} {#if showConfirmButton} diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index c929e02d86..01a2ca4835 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -503,12 +503,6 @@ .spectrum-Table-headCell--alignRight { justify-content: flex-end; } - .spectrum-Table-headCell--divider { - padding-right: var(--cell-padding); - } - .spectrum-Table-headCell--divider + .spectrum-Table-headCell { - padding-left: var(--cell-padding); - } .spectrum-Table-headCell--edit { position: sticky; left: 0; @@ -580,13 +574,6 @@ background-color: var(--table-bg); z-index: auto; } - .spectrum-Table-cell--divider { - padding-right: var(--cell-padding); - } - .spectrum-Table-cell--divider + .spectrum-Table-cell { - padding-left: var(--cell-padding); - } - .spectrum-Table-cell--edit { position: sticky; left: 0; diff --git a/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte index 3d662ed556..e70a0aa042 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte @@ -5,6 +5,7 @@ export let selectedRows export let deleteRows + export let item = "row" const dispatch = createEventDispatcher() let modal @@ -14,12 +15,14 @@ modal?.hide() dispatch("updaterows") } + + $: text = `${item}${selectedRows?.length === 1 ? "" : "s"}` Are you sure you want to delete {selectedRows.length} - row{selectedRows.length > 1 ? "s" : ""}? + {text}? diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index 0d05e170e0..a089664d2e 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -27,7 +27,6 @@ import { AppStatus } from "constants" import Logo from "assets/bb-space-man.svg" import AccessFilter from "./_components/AcessFilter.svelte" - import { Constants } from "@budibase/frontend-core" let sortBy = "name" let template @@ -69,10 +68,6 @@ $: unlocked = lockedApps?.length === 0 $: automationErrors = getAutomationErrors(enrichedApps) - $: hasGroupsLicense = $auth.user?.license.features.includes( - Constants.Features.USER_GROUPS - ) - const enrichApps = (apps, user, sortBy) => { const enrichedApps = apps.map(app => ({ ...app, @@ -360,7 +355,7 @@ {/if}
- {#if hasGroupsLicense && $groups.length} + {#if $auth.groupsEnabled && $groups.length} {/if} -
-
- - -
- - {#if userId !== $auth.user._id} + + + Details +
- - +
+
+ +
- {/if} -
+
+ + +
+ + {#if userId !== $auth.user._id} +
+ + @@ -95,11 +92,11 @@ options={Constants.BuilderRoleDescriptions} /> - {#if hasGroupsLicense} + {#if $auth.groupsEnabled} option.name} getOptionValue={option => option._id} @@ -122,14 +119,12 @@ label { font-family: var(--font-sans); - cursor: pointer; font-weight: 600; box-sizing: border-box; overflow: hidden; border-radius: var(--border-radius-s); color: var(--ink); padding: var(--spacing-m) var(--spacing-l); - transition: all 0.2s ease 0s; display: inline-flex; text-rendering: optimizeLegibility; min-width: auto; @@ -141,10 +136,15 @@ align-items: center; justify-content: center; width: 100%; - background-color: var(--grey-2); - font-size: var(--font-size-xs); + background: var(--spectrum-global-color-gray-200); + font-size: 12px; line-height: normal; border: var(--border-transparent); + transition: background-color 130ms ease-out; + } + label:hover { + background: var(--spectrum-global-color-gray-300); + cursor: pointer; } input[type="file"] { diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte index 01dac8c222..02501f2de0 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte @@ -49,10 +49,10 @@ cancelText="Cancel" showCloseIcon={false} > - All your new users can be accessed through the autogenerated passwords. - Make not of these passwords or download the csv + + All your new users can be accessed through the autogenerated passwords. Take + note of these passwords or download the CSV file. +
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte index 4f481d374c..fe7acee6c4 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte @@ -3,14 +3,20 @@ import { Constants } from "@budibase/frontend-core" export let row - $: value = - Constants.BbRoles.find(x => x.value === users.getUserRole(row))?.label || - "Not Available" + + const TooltipMap = { + appUser: "Only has access to published apps", + developer: "Access to the app builder", + admin: "Full access", + } + + $: role = Constants.BudibaseRoleOptions.find( + x => x.value === users.getUserRole(row) + ) + $: value = role?.label || "Not available" + $: tooltip = TooltipMap[role?.value] || "" -
+
{value}
- - diff --git a/packages/builder/src/pages/builder/portal/manage/users/index.svelte b/packages/builder/src/pages/builder/portal/manage/users/index.svelte index b6cac9ece3..73cf5e26fa 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/index.svelte @@ -8,11 +8,10 @@ Layout, Modal, ModalContent, - Icon, + Search, notifications, Pagination, - Search, - Label, + Divider, } from "@budibase/bbui" import AddUserModal from "./_components/AddUserModal.svelte" import { users, groups, auth } from "stores/portal" @@ -20,69 +19,42 @@ import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte" import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte" import AppsTableRenderer from "./_components/AppsTableRenderer.svelte" - import NameTableRenderer from "./_components/NameTableRenderer.svelte" import RoleTableRenderer from "./_components/RoleTableRenderer.svelte" import { goto } from "@roxi/routify" import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte" import PasswordModal from "./_components/PasswordModal.svelte" import ImportUsersModal from "./_components/ImportUsersModal.svelte" import { createPaginationStore } from "helpers/pagination" - import { Constants } from "@budibase/frontend-core" import { get } from "svelte/store" + import { Constants } from "@budibase/frontend-core" - const accessTypes = [ - { - icon: "User", - description: "App user - Only has access to published apps", - }, - { - icon: "Hammer", - description: "Developer - Access to the app builder", - }, - { - icon: "Draw", - description: "Admin - Full access", - }, - ] - - //let email let enrichedUsers = [] let createUserModal, inviteConfirmationModal, onboardingTypeModal, passwordModal, importUsersModal - let pageInfo = createPaginationStore() let prevEmail = undefined, searchEmail = undefined - let selectedRows = [] let customRenderers = [ { column: "userGroups", component: GroupsTableRenderer }, { column: "apps", component: AppsTableRenderer }, - { column: "name", component: NameTableRenderer }, { column: "role", component: RoleTableRenderer }, ] - $: hasGroupsLicense = $auth.user?.license.features.includes( - Constants.Features.USER_GROUPS - ) - $: schema = { - name: {}, email: {}, role: { sortable: false, }, - ...(hasGroupsLicense && { - userGroups: { sortable: false, displayName: "User groups" }, + ...($auth.groupsEnabled && { + userGroups: { sortable: false, displayName: "Groups" }, }), apps: {}, } - $: userData = [] - $: page = $pageInfo.page $: fetchUsers(page, searchEmail) $: { @@ -105,6 +77,7 @@ } }) } + const showOnboardingTypeModal = async addUsersData => { userData = await removingDuplicities(addUsersData) if (!userData?.users?.length) return @@ -113,13 +86,13 @@ } async function createUserFlow() { - let emails = userData?.users?.map(x => x.email) || [] + const payload = userData?.users?.map(user => ({ + email: user.email, + builder: user.role === Constants.BudibaseRoles.Developer, + admin: user.role === Constants.BudibaseRoles.Admin, + })) try { - const res = await users.invite({ - emails: emails, - builder: false, - admin: false, - }) + const res = await users.invite(payload) notifications.success(res.message) inviteConfirmationModal.show() } catch (error) { @@ -232,23 +205,13 @@ } - + Users Add users and control who gets access to your published apps - -
- {#each accessTypes as type} -
- -
- {type.description} -
-
- {/each} -
- + +
- - -
- - -
- {#if selectedRows.length > 0} - - {/if} + Import users +
- $goto(`./${detail._id}`)} - {schema} - bind:selectedRows - data={enrichedUsers} - allowEditColumns={false} - allowEditRows={false} - allowSelectRows={true} - showHeaderBorder={false} - {customRenderers} - /> - +
$goto(`./${detail._id}`)} + {schema} + bind:selectedRows + data={enrichedUsers} + allowEditColumns={false} + allowEditRows={false} + allowSelectRows={true} + showHeaderBorder={false} + {customRenderers} + /> + @@ -325,28 +296,22 @@ display: flex; flex-direction: row; justify-content: flex-end; - margin-top: var(--spacing-xl); } - .field { + .controls { display: flex; - align-items: center; flex-direction: row; - grid-gap: var(--spacing-m); - margin-left: auto; + justify-content: space-between; + align-items: center; } - - .field > :global(*) + :global(*) { - margin-left: var(--spacing-m); - } - - .access-description { + .controls-right { display: flex; - margin-top: var(--spacing-xl); - opacity: 0.8; + flex-direction: row; + justify-content: flex-end; + align-items: center; + gap: var(--spacing-xl); } - - .access-text { - margin-left: var(--spacing-m); + .controls-right :global(.spectrum-Search) { + width: 200px; } diff --git a/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte b/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte index 565dfc7aa2..5e327a8743 100644 --- a/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte +++ b/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte @@ -17,10 +17,10 @@ import { users, groups, apps, auth } from "stores/portal" import AssignmentModal from "./AssignmentModal.svelte" import { createPaginationStore } from "helpers/pagination" - import { Constants } from "@budibase/frontend-core" import { roles } from "stores/backend" export let app + let assignmentModal let appGroups = [] let appUsers = [] @@ -28,14 +28,9 @@ search = undefined let pageInfo = createPaginationStore() let fixedAppId + $: page = $pageInfo.page - - $: hasGroupsLicense = $auth.user?.license.features.includes( - Constants.Features.USER_GROUPS - ) - $: fixedAppId = apps.getProdAppID(app.devId) - $: appGroups = $groups.filter(x => { return x.apps.includes(app.appId) }) @@ -161,7 +156,7 @@ > - {#if hasGroupsLicense && appGroups.length} + {#if $auth.groupsEnabled && appGroups.length} {#each appGroups as group} { + return !group.apps.find(appId => { + return appId === app.appId + }) + }) + $: valid = + appData?.length && !appData?.some(x => !x.id?.length || !x.role?.length) + $: optionSections = { + ...($auth.groupsEnabled && + filteredGroups.length && { + ["User groups"]: { + data: filteredGroups, + getLabel: group => group.name, + getValue: group => group._id, + getIcon: group => group.icon, + getColour: group => group.color, + }, + }), + users: { + data: availableUsers, + getLabel: user => user.email, + getValue: user => user._id, + getIcon: user => user.icon, + getColour: user => user.color, + }, + } + + const getAvailableUsers = (allUsers, appUsers, newUsers) => { + return (allUsers.data || []).filter(user => { + // Filter out assigned users + if (appUsers.find(x => x._id === user._id)) { + return false + } + + // Filter out new users which are going to be assigned + return !newUsers.find(x => x.id === user._id) + }) + } + async function fetchUsers(page, search) { if ($pageInfo.loading) { return @@ -39,36 +82,13 @@ } } - $: filteredGroups = $groups.filter(group => { - return !group.apps.find(appId => { - return appId === app.appId - }) - }) - - $: optionSections = { - ...(filteredGroups.length && { - groups: { - data: filteredGroups, - getLabel: group => group.name, - getValue: group => group._id, - getIcon: group => group.icon, - getColour: group => group.color, - }, - }), - users: { - data: $users.data.filter(u => !appUsers.find(x => x._id === u._id)), - getLabel: user => user.email, - getValue: user => user._id, - getIcon: user => user.icon, - getColour: user => user.color, - }, - } - - $: appData = [{ id: "", role: "" }] - function addNewInput() { appData = [...appData, { id: "", role: "" }] } + + const removeItem = index => { + appData = appData.filter((x, idx) => idx !== index) + } addData(appData)} showCloseIcon={false} + disabled={!valid} > - - {#each appData as input, index} - group.name} - getPrimaryOptionValue={group => group.name} - getPrimaryOptionIcon={group => group.icon} - getPrimaryOptionColour={group => group.colour} - getSecondaryOptionLabel={role => role.name} - getSecondaryOptionValue={role => role._id} - getSecondaryOptionColour={role => RoleUtils.getRoleColour(role._id)} - /> - {/each} - + {#if appData?.length} + + {#each appData as input, index} +
+
+ group.name} + getPrimaryOptionValue={group => group.name} + getPrimaryOptionIcon={group => group.icon} + getPrimaryOptionColour={group => group.colour} + getSecondaryOptionLabel={role => role.name} + getSecondaryOptionValue={role => role._id} + getSecondaryOptionColour={role => + RoleUtils.getRoleColour(role._id)} + /> +
+
+ removeItem(index)} + /> +
+
+ {/each} +
+ {/if}
Add email
+ + diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index d6f4fc140f..8ac19ab785 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -2,6 +2,8 @@ import { derived, writable, get } from "svelte/store" import { API } from "api" import { admin } from "stores/portal" import analytics from "analytics" +import { FEATURE_FLAGS } from "helpers/featureFlags" +import { Constants } from "@budibase/frontend-core" export function createAuthStore() { const auth = writable({ @@ -10,11 +12,13 @@ export function createAuthStore() { tenantSet: false, loaded: false, postLogout: false, + groupsEnabled: false, }) const store = derived(auth, $store => { let initials = null let isAdmin = false let isBuilder = false + let groupsEnabled = false if ($store.user) { const user = $store.user if (user.firstName) { @@ -29,6 +33,9 @@ export function createAuthStore() { } isAdmin = !!user.admin?.global isBuilder = !!user.builder?.global + groupsEnabled = + user?.license.features.includes(Constants.Features.USER_GROUPS) && + user?.featureFlags.includes(FEATURE_FLAGS.USER_GROUPS) } return { user: $store.user, @@ -39,6 +46,7 @@ export function createAuthStore() { initials, isAdmin, isBuilder, + groupsEnabled, } }) diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.js index 490d1bc9f6..7fc3704e98 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.js @@ -26,12 +26,8 @@ export function createUsersStore() { return await API.getUsers() } - async function invite({ emails, builder, admin }) { - return API.inviteUsers({ - emails, - builder, - admin, - }) + async function invite(payload) { + return API.inviteUsers(payload) } async function acceptInvite(inviteCode, password) { return API.acceptInvite({ diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js index 17223a80e6..653376aa55 100644 --- a/packages/frontend-core/src/api/user.js +++ b/packages/frontend-core/src/api/user.js @@ -141,20 +141,18 @@ export const buildUserEndpoints = API => ({ /** * Invites multiple users to the current tenant. - * @param email An array of email addresses - * @param builder whether the user should be a global builder - * @param admin whether the user should be a global admin + * @param users An array of users to invite */ - inviteUsers: async ({ emails, builder, admin }) => { + inviteUsers: async users => { return await API.post({ - url: "/api/global/users/inviteMultiple", - body: { - emails, + url: "/api/global/users/multi/invite", + body: users.map(user => ({ + email: user.email, userInfo: { - admin: admin ? { global: true } : undefined, - builder: builder ? { global: true } : undefined, + admin: user.admin ? { global: true } : undefined, + builder: user.admin || user.builder ? { global: true } : undefined, }, - }, + })), }) }, diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js index 77765f8d6e..4ad4f0fef8 100644 --- a/packages/frontend-core/src/constants.js +++ b/packages/frontend-core/src/constants.js @@ -60,25 +60,31 @@ export const TableNames = { USERS: "ta_users", } -export const BbRoles = [ - { label: "App User", value: "appUser" }, - { label: "Developer", value: "developer" }, - { label: "Admin", value: "admin" }, +export const BudibaseRoles = { + AppUser: "appUser", + Developer: "developer", + Admin: "admin", +} + +export const BudibaseRoleOptions = [ + { label: "App User", value: BudibaseRoles.AppUser }, + { label: "Developer", value: BudibaseRoles.Developer }, + { label: "Admin", value: BudibaseRoles.Admin }, ] export const BuilderRoleDescriptions = [ { - value: "appUser", + value: BudibaseRoles.AppUser, icon: "User", label: "App user - Only has access to published apps", }, { - value: "developer", + value: BudibaseRoles.Developer, icon: "Hammer", label: "Developer - Access to the app builder", }, { - value: "admin", + value: BudibaseRoles.Admin, icon: "Draw", label: "Admin - Full access", }, diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 30bf78efc6..1f9af3514b 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -214,13 +214,13 @@ export const invite = async (ctx: any) => { } export const inviteMultiple = async (ctx: any) => { - let { emails, userInfo } = ctx.request.body + let users = ctx.request.body let existing = false let existingEmail - for (let email of emails) { - if (await usersCore.getGlobalUserByEmail(email)) { + for (let user of users) { + if (await usersCore.getGlobalUserByEmail(user.email)) { existing = true - existingEmail = email + existingEmail = user.email break } } @@ -228,17 +228,18 @@ export const inviteMultiple = async (ctx: any) => { if (existing) { ctx.throw(400, `${existingEmail} already exists`) } - if (!userInfo) { - userInfo = {} - } - userInfo.tenantId = tenancy.getTenantId() - const opts: any = { - subject: "{{ company }} platform invitation", - info: userInfo, - } - for (let i = 0; i < emails.length; i++) { - await sendEmail(emails[i], EmailTemplatePurpose.INVITATION, opts) + for (let i = 0; i < users.length; i++) { + let userInfo = users[i].userInfo + if (!userInfo) { + userInfo = {} + } + userInfo.tenantId = tenancy.getTenantId() + const opts: any = { + subject: "{{ company }} platform invitation", + info: userInfo, + } + await sendEmail(users[i].email, EmailTemplatePurpose.INVITATION, opts) } ctx.body = { diff --git a/packages/worker/src/api/routes/global/users.js b/packages/worker/src/api/routes/global/users.js index 4d0144d6e0..0fc479df39 100644 --- a/packages/worker/src/api/routes/global/users.js +++ b/packages/worker/src/api/routes/global/users.js @@ -32,10 +32,12 @@ function buildInviteValidation() { function buildInviteMultipleValidation() { // prettier-ignore - return joiValidator.body(Joi.object({ - emails: Joi.array().required(), - userInfo: Joi.object().optional(), - }).required()) + return joiValidator.body(Joi.array().required().items( + Joi.object({ + email: Joi.string(), + userInfo: Joi.object().optional(), + }) + )) } function buildInviteAcceptValidation() { @@ -79,7 +81,7 @@ router controller.invite ) .post( - "/api/global/users/inviteMultiple", + "/api/global/users/multi/invite", adminOnly, buildInviteMultipleValidation(), controller.inviteMultiple