diff --git a/packages/bbui/src/Form/Core/InputDropdown.svelte b/packages/bbui/src/Form/Core/InputDropdown.svelte new file mode 100644 index 0000000000..c9ca70dff9 --- /dev/null +++ b/packages/bbui/src/Form/Core/InputDropdown.svelte @@ -0,0 +1,215 @@ + + +
+
+ +
+
+ + {#if open} +
(open = false)} + transition:fly|local={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + > +
    + {#each options as option, idx} +
  • onPick(getOptionValue(option, idx))} + > + + {getOptionLabel(option, idx)} + + +
  • + {/each} +
+
+ {/if} +
+
+ + diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index 3eb1add267..9dd5a25a4f 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -13,6 +13,7 @@ export let readonly = false export let autocomplete = false export let sort = false + export let autoWidth = false const dispatch = createEventDispatcher() $: selectedLookupMap = getSelectedLookupMap(value) @@ -85,4 +86,5 @@ {getOptionValue} onSelectOption={toggleOption} {sort} + {autoWidth} /> diff --git a/packages/bbui/src/Form/InputDropdown.svelte b/packages/bbui/src/Form/InputDropdown.svelte new file mode 100644 index 0000000000..73516ea37c --- /dev/null +++ b/packages/bbui/src/Form/InputDropdown.svelte @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/bbui/src/Form/Multiselect.svelte b/packages/bbui/src/Form/Multiselect.svelte index 957dcccddf..7bcf22aa06 100644 --- a/packages/bbui/src/Form/Multiselect.svelte +++ b/packages/bbui/src/Form/Multiselect.svelte @@ -14,7 +14,7 @@ export let getOptionLabel = option => option export let getOptionValue = option => option export let sort = false - + export let autoWidth = false const dispatch = createEventDispatcher() const onChange = e => { value = e.detail @@ -33,6 +33,7 @@ {sort} {getOptionLabel} {getOptionValue} + {autoWidth} on:change={onChange} on:click /> diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index 29d9772d33..b6dea0638b 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -445,7 +445,7 @@ width: 100%; border-radius: 0; display: grid; - overflow: auto; + overflow: visible; } /* Header */ @@ -513,7 +513,7 @@ z-index: 3; } .spectrum-Table-headCell .title { - overflow: hidden; + overflow: visible; text-overflow: ellipsis; } .spectrum-Table-headCell:hover .spectrum-Table-editIcon { diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js index ea7dd88020..6a4373df92 100644 --- a/packages/bbui/src/index.js +++ b/packages/bbui/src/index.js @@ -23,6 +23,7 @@ export { default as Icon, directions } from "./Icon/Icon.svelte" export { default as Toggle } from "./Form/Toggle.svelte" export { default as RadioGroup } from "./Form/RadioGroup.svelte" export { default as Checkbox } from "./Form/Checkbox.svelte" +export { default as InputDropdown } from "./Form/InputDropdown.svelte" export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte" export { default as Popover } from "./Popover/Popover.svelte" export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte" @@ -71,6 +72,7 @@ export { default as ListItem } from "./List/ListItem.svelte" // Renderers export { default as BoldRenderer } from "./Table/BoldRenderer.svelte" export { default as CodeRenderer } from "./Table/CodeRenderer.svelte" +export { default as InternalRenderer } from "./Table/InternalRenderer.svelte" // Typography export { default as Body } from "./Typography/Body.svelte" diff --git a/packages/builder/src/components/settings/UserGroupPicker.svelte b/packages/builder/src/components/settings/UserGroupPicker.svelte new file mode 100644 index 0000000000..8e67331b09 --- /dev/null +++ b/packages/builder/src/components/settings/UserGroupPicker.svelte @@ -0,0 +1,73 @@ + + +
+ +
+
+ {filtered.length} {title}{filtered.length === 1 ? "" : "s"} +
+
+ Add all +
+
+ +
+ {#each filtered as item} +
+
+ {item[key]} +
+ + {#if selected.includes(item._id)} +
+ +
+ {/if} +
+ {/each} +
+
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/_layout.svelte b/packages/builder/src/pages/builder/portal/manage/_layout.svelte index b6c2897730..a63195a214 100644 --- a/packages/builder/src/pages/builder/portal/manage/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/manage/_layout.svelte @@ -12,7 +12,7 @@ $: wide = $page.path.includes("email/:template") || - $page.path.includes("users") || + ($page.path.includes("users") && !$page.path.includes(":userId")) || ($page.path.includes("groups") && !$page.path.includes(":groupId")) diff --git a/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte index b3ee52ffcf..08610e0e23 100644 --- a/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte @@ -8,14 +8,13 @@ Body, Icon, Popover, - Search, - Divider, - Detail, notifications, List, ListItem, StatusLight, } from "@budibase/bbui" + import UserGroupPicker from "components/settings/UserGroupPicker.svelte" + import { users, apps, groups } from "stores/portal" import { onMount } from "svelte" import { RoleUtils } from "@budibase/frontend-core" @@ -47,8 +46,9 @@ } async function selectUser(id) { - let selectedUser = selectedUsers.find(user_id => user_id === id) + let selectedUser = selectedUsers.includes(id) let enrichedUser = $users.find(user => user._id === id) + if (selectedUser) { selectedUsers = selectedUsers.filter(id => id !== selectedUser) let newUsers = group.users.filter(user => user._id !== id) @@ -57,6 +57,7 @@ selectedUsers = [...selectedUsers, id] group.users.push(enrichedUser) } + await groups.actions.save(group) } @@ -97,55 +98,24 @@ -
- -
-
- {filteredUsers.length} User{filteredUsers.length === 1 - ? "" - : "s"} -
-
- Add all -
-
- -
- {#each filteredUsers as user} -
-
- {user.email} -
- - {#if selectedUsers.includes(user._id)} -
- -
- {/if} -
- {/each} -
-
+
{#if group?.users.length} {#each group.users as user} - removeUser(user._id)} + on:click={() => removeUser(user?._id)} hoverable size="L" name="Close" @@ -180,7 +150,7 @@ {/each} {:else} - + {/if} @@ -190,24 +160,6 @@ margin-left: var(--spacing-l); } - .users-header { - align-items: center; - padding: var(--spacing-m) 0 var(--spacing-m) 0; - display: flex; - justify-content: space-between; - } - - .user-selection { - align-items: end; - display: flex; - justify-content: space-between; - cursor: pointer; - } - - .user-selection > :first-child { - padding-top: var(--spacing-m); - } - .header { display: flex; justify-content: space-between; diff --git a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte index a8cb340465..f095cf8a90 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte @@ -2,43 +2,44 @@ import { goto } from "@roxi/routify" import { ActionButton, + ActionMenu, + Avatar, Button, Layout, Heading, Body, - Divider, Label, + List, + ListItem, + Icon, Input, + MenuItem, + Popover, Select, - Toggle, Modal, - Table, ModalContent, notifications, + StatusLight, } from "@budibase/bbui" + import { onMount } from "svelte" + import { fetchData } from "helpers" - import { users, auth } from "stores/portal" + import { users, auth, groups } from "stores/portal" + import { Constants } from "@budibase/frontend-core" - import TagsRenderer from "./_components/RolesTagsTableRenderer.svelte" - - import UpdateRolesModal from "./_components/UpdateRolesModal.svelte" import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte" + import { RoleUtils } from "@budibase/frontend-core" + import UserGroupPicker from "components/settings/UserGroupPicker.svelte" export let userId let deleteUserModal - let editRolesModal let resetPasswordModal - - const roleSchema = { - name: { displayName: "App" }, - role: {}, - } - - const noRoleSchema = { - name: { displayName: "App" }, - } - + let popoverAnchor + let searchTerm = "" + let popover + let selectedGroups = [] $: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : "" + // Merge the Apps list and the roles response to get something that makes sense for the table $: allAppList = Object.keys($apps?.data).map(id => { const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId @@ -50,19 +51,23 @@ } }) - $: appList = allAppList.filter(app => !!app.role[0]) - $: noRoleAppList = allAppList - .filter(app => !app.role[0]) - .map(app => { - delete app.role - return app - }) + // Used for searching through groups in the add group popover + $: filteredGroups = $groups.filter( + group => + selectedGroups && + group?.name?.toLowerCase().includes(searchTerm.toLowerCase()) + ) - let selectedApp + $: appList = allAppList.filter(app => !!app.role[0]) + + $: userGroups = $groups.filter(x => { + return x.users?.some(y => { + return y._id === userId + }) + }) const userFetch = fetchData(`/api/global/users/${userId}`) const apps = fetchData(`/api/global/roles`) - async function deleteUser() { try { await users.delete(userId) @@ -73,8 +78,17 @@ } } - let toggleDisabled = false - + function getHighestRole(roles) { + let highestRole + let highestRoleNumber = 0 + roles.forEach(role => { + let roleNumber = RoleUtils.getRolePriority(role._id) + if (roleNumber > highestRoleNumber) { + highestRole = role + } + }) + return highestRole + } async function updateUserFirstName(evt) { try { await users.save({ ...$userFetch?.data, firstName: evt.target.value }) @@ -84,6 +98,13 @@ } } + async function removeGroup(id) { + let updatedGroup = $groups.find(x => x._id === id) + let newUsers = updatedGroup.users.filter(user => user._id !== userId) + updatedGroup.users = newUsers + groups.actions.save(updatedGroup) + } + async function updateUserLastName(evt) { try { await users.save({ ...$userFetch?.data, lastName: evt.target.value }) @@ -93,6 +114,30 @@ } } + async function updateUserRole() { + return + } + + async function addGroup(groupId) { + let selectedGroup = selectedGroups.includes(groupId) + let newUser = $users.find(user => user._id === userId) + let group = $groups.find(group => group._id === groupId) + + if (selectedGroup) { + selectedGroups = selectedGroups.filter(id => id === selectedGroup) + let newUsers = group.users.filter(user => user._id !== newUser._id) + group.users = newUsers + } else { + selectedGroups = [...selectedGroups, groupId] + group.users.push(newUser) + } + + await groups.actions.save(group) + } + + function addAll() {} + + /* async function toggleFlag(flagName, detail) { toggleDisabled = true try { @@ -104,6 +149,7 @@ toggleDisabled = false } + async function toggleBuilderAccess({ detail }) { return toggleFlag("builder", detail) } @@ -116,38 +162,56 @@ selectedApp = detail editRolesModal.show() } +*/ + onMount(async () => { + try { + await groups.actions.init() + } catch (error) { + notifications.error("Error getting User groups") + } + }) - +
- $goto("./")} - quiet - size="S" - icon="BackAndroid" - > - Back to users + $goto("./")} size="S" icon="ArrowLeft"> + Back
- User: {$userFetch?.data?.email} - - Change user settings and update their app roles. Also contains the ability - to delete the user as well as force reset their password. -
- + +
+
+
+ +
+ {$userFetch?.data?.firstName + + " " + + $userFetch?.data?.lastName} + {$userFetch?.data?.email} +
+
+
+
+ + + + + Force Password Reset + Delete + +
+
+
- General
-
- - -
-
- - {#if userId !== $auth.user._id}
- - -
-
- - + + - -
-
- - + + + + {#each userData as input, index} + + {/each} +
+ Add email
-
- - -
-
+ + + option.name} + getOptionValue={option => option.name} + /> diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte index 5611b20e99..b4f9edd590 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte @@ -21,6 +21,7 @@ diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte index d212bb6f6a..255e63a611 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte @@ -25,6 +25,7 @@ .align { display: flex; align-items: center; + overflow: hidden; } .spacing { diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/OnboardingTypeModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/OnboardingTypeModal.svelte new file mode 100644 index 0000000000..089721348e --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/OnboardingTypeModal.svelte @@ -0,0 +1,107 @@ + + + showConfirmationModal(selectedOnboardingType)} + disabled={!selectedOnboardingType} +> + +
{ + selectedOnboardingType = emailOnboardingKey + }} + > +
+ +
+ Send email invites +
+
+
+ {#if selectedOnboardingType == emailOnboardingKey} +
+ +
+ {/if} +
+
+ +
{ + selectedOnboardingType = basicOnboaridngKey + }} + > +
+ +
+ Generate passwords for each user +
+
+
+ {#if selectedOnboardingType == basicOnboaridngKey} +
+ +
+ {/if} +
+
+
+
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordCopyRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordCopyRenderer.svelte new file mode 100644 index 0000000000..00e0c6eeab --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordCopyRenderer.svelte @@ -0,0 +1,15 @@ + + +
+ {value} +
+ +
+
+ + 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 new file mode 100644 index 0000000000..26738f34c8 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte @@ -0,0 +1,61 @@ + + + + All your new users can be accessed through the autogenerated passwords. + Make not of these passwords or download the csv + +
+
+ + +
+ Passwords CSV +
+
+
+ + + + + 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 f25501f90b..5182614789 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 @@ -8,19 +8,16 @@ ] -
-
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/SettingsTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/SettingsTableRenderer.svelte index 0ead895893..37ce58a0b7 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/SettingsTableRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/SettingsTableRenderer.svelte @@ -2,7 +2,10 @@ import { Icon, ActionMenu, MenuItem } from "@budibase/bbui" -
+
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 af3e745161..6fd674f605 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/index.svelte @@ -7,6 +7,7 @@ Table, Layout, Modal, + ModalContent, Icon, notifications, } from "@budibase/bbui" @@ -20,11 +21,17 @@ import SettingsTableRenderer from "./_components/SettingsTableRenderer.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" const schema = { name: {}, email: {}, - role: { noPropagation: true, sortable: false }, + role: { + noPropagation: true, + sortable: false, + }, userGroups: { sortable: false, displayName: "User groups" }, apps: {}, settings: { @@ -52,6 +59,12 @@ let search let email + let createUserModal, + basicOnboardingModal, + inviteConfirmationModal, + onboardingTypeModal, + passwordModal, + importUsersModal $: filteredUsers = $users .filter(user => user.email.includes(search || "")) @@ -80,10 +93,16 @@ } }) - let createUserModal - let basicOnboardingModal = function openBasicOnboardingModal() { - createUserModal.hide() - basicOnboardingModal.show() + function showOnboardingTypeModal() { + onboardingTypeModal.show() + } + + function showConfirmationModal(onboardingType) { + if (onboardingType === "emailOnboarding") { + inviteConfirmationModal.show() + } else { + passwordModal.show() + } } onMount(async () => { @@ -120,7 +139,9 @@ icon="UserAdd" cta>Add Users - +
$goto(`./${detail._id}`)} @@ -142,8 +163,34 @@ - + + + + + Your users should now recieve an email invite to get access to their + Budibase account + + + + + + + + + + + + + +