group / user app assignment
This commit is contained in:
parent
ce1fe5e600
commit
a84b36cc54
|
@ -5,6 +5,7 @@
|
|||
import { fly } from "svelte/transition"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import clickOutside from "../../Actions/click_outside"
|
||||
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||
|
||||
export let inputValue
|
||||
export let dropdownValue
|
||||
|
@ -18,6 +19,8 @@
|
|||
export let options = []
|
||||
export let getOptionLabel = option => extractProperty(option, "label")
|
||||
export let getOptionValue = option => extractProperty(option, "value")
|
||||
export let getOptionColour = option => extractProperty(option, "colour")
|
||||
|
||||
export let isOptionSelected = () => false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
export let autoWidth = false
|
||||
export let autocomplete = false
|
||||
export let sort = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let open = false
|
||||
$: fieldText = getFieldText(value, options, placeholder)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import Icon from "../Icon/Icon.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let value = "Anchor"
|
||||
export let value
|
||||
export let size = "M"
|
||||
export let alignRight = false
|
||||
|
||||
|
@ -59,7 +59,7 @@
|
|||
style={value ? `background: ${value};` : ""}
|
||||
class:placeholder={!value}
|
||||
>
|
||||
<Icon name={value} />
|
||||
<Icon name={value || "UserGroup"} />
|
||||
</div>
|
||||
</div>
|
||||
{#if open}
|
||||
|
|
|
@ -24,6 +24,7 @@ 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 PickerDropdown } from "./Form/PickerDropdown.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"
|
||||
|
|
|
@ -22,23 +22,15 @@
|
|||
export let groupId
|
||||
let popoverAnchor
|
||||
let popover
|
||||
$: group = $groups.find(x => x._id === groupId)
|
||||
let searchTerm = ""
|
||||
let selectedUsers = []
|
||||
$: group = $groups.find(x => x._id === groupId)
|
||||
$: filteredUsers = $users.filter(
|
||||
user =>
|
||||
selectedUsers &&
|
||||
user?.email?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
let app_list = [
|
||||
{
|
||||
access: "ADMIN",
|
||||
name: "test app",
|
||||
icon: "Anchor",
|
||||
color: "blue",
|
||||
},
|
||||
]
|
||||
|
||||
$: console.log(group)
|
||||
async function addAll() {
|
||||
selectedUsers = [...selectedUsers, ...filteredUsers]
|
||||
group.users = selectedUsers
|
||||
|
@ -138,9 +130,13 @@
|
|||
</div>
|
||||
|
||||
<List>
|
||||
{#if app_list.length}
|
||||
{#each app_list as app}
|
||||
<ListItem title={app.name} icon={app.icon} iconBackground={app.color}>
|
||||
{#if group?.apps}
|
||||
{#each group.apps as app}
|
||||
<ListItem
|
||||
title={app.name}
|
||||
icon={app?.icon?.name || "Apps"}
|
||||
iconBackground={app?.icon?.color || ""}
|
||||
>
|
||||
<div class="title ">
|
||||
<StatusLight color={RoleUtils.getRoleColour(app.access)} />
|
||||
<div style="margin-left: var(--spacing-s);">
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
<Icon name="WebPage" />
|
||||
|
||||
<div style="margin-left: var(--spacing-l)">
|
||||
{parseInt(group.appCount) || 0} app{parseInt(group.appCount) === 1
|
||||
{parseInt(group?.apps?.length) || 0} app{parseInt(group?.apps?.length) === 1
|
||||
? ""
|
||||
: "s"}
|
||||
</div>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
icon: "",
|
||||
color: "",
|
||||
users: [],
|
||||
apps: [],
|
||||
}
|
||||
let proPlan = true
|
||||
|
||||
|
@ -82,13 +83,15 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="groupTable">
|
||||
{#each $groups as group}
|
||||
<div>
|
||||
<UserGroupsRow {saveGroup} {deleteGroup} {group} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if proPlan}
|
||||
<div class="groupTable">
|
||||
{#each $groups as group}
|
||||
<div>
|
||||
<UserGroupsRow {saveGroup} {deleteGroup} {group} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
import { onMount } from "svelte"
|
||||
|
||||
import { fetchData } from "helpers"
|
||||
import { users, auth, groups } from "stores/portal"
|
||||
import { users, auth, groups, apps } from "stores/portal"
|
||||
import { roles } from "stores/backend"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
|
||||
|
@ -38,19 +39,28 @@
|
|||
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
|
||||
const role = $apps?.data?.[id].roles.find(role => role._id === roleId)
|
||||
return {
|
||||
...$apps?.data?.[id],
|
||||
_id: id,
|
||||
role: [role],
|
||||
}
|
||||
})
|
||||
let allAppList = []
|
||||
$: console.log($apps)
|
||||
$: console.log($userFetch.data)
|
||||
|
||||
$: allAppList = $apps
|
||||
.filter(x => {
|
||||
if ($userFetch.data?.roles) {
|
||||
return Object.keys($userFetch.data.roles).find(y => {
|
||||
return x.appId === y
|
||||
})
|
||||
}
|
||||
})
|
||||
.map(app => {
|
||||
let roles = Object.keys($userFetch.data.roles).filter(id => {
|
||||
return id === app.appId
|
||||
})
|
||||
return {
|
||||
...app,
|
||||
roles,
|
||||
}
|
||||
})
|
||||
$: console.log(allAppList)
|
||||
// Used for searching through groups in the add group popover
|
||||
$: filteredGroups = $groups.filter(
|
||||
group =>
|
||||
|
@ -58,16 +68,13 @@
|
|||
group?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
$: appList = allAppList.filter(app => !!app.role[0])
|
||||
|
||||
$: userGroups = $groups.filter(x => {
|
||||
return x.users?.some(y => {
|
||||
return x.users?.find(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)
|
||||
|
@ -166,6 +173,7 @@
|
|||
onMount(async () => {
|
||||
try {
|
||||
await groups.actions.init()
|
||||
await apps.load()
|
||||
} catch (error) {
|
||||
notifications.error("Error getting User groups")
|
||||
}
|
||||
|
@ -243,9 +251,7 @@
|
|||
<div class="tableTitle">
|
||||
<div>
|
||||
<Heading size="XS">User groups</Heading>
|
||||
<Body size="S"
|
||||
>Manage apps that this User group has been assigned to</Body
|
||||
>
|
||||
<Body size="S">Add or remove this user from user groups</Body>
|
||||
</div>
|
||||
<div bind:this={popoverAnchor}>
|
||||
<Button on:click={popover.show()} icon="UserGroup" cta
|
||||
|
@ -291,25 +297,27 @@
|
|||
<div class="appsTitle">
|
||||
<Heading weight="light" size="XS">Apps</Heading>
|
||||
<div style="margin-top: var(--spacing-xs)">
|
||||
<Body size="S"
|
||||
>Manage apps that this User group has been assigned to</Body
|
||||
>
|
||||
<Body size="S">Manage apps that this user has been assigned to</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<List>
|
||||
{#if appList.length}
|
||||
{#each appList as app}
|
||||
<ListItem title={app.name} icon="Apps">
|
||||
<div class="title ">
|
||||
<StatusLight
|
||||
color={RoleUtils.getRoleColour(getHighestRole(app.role)._id)}
|
||||
/>
|
||||
<div style="margin-left: var(--spacing-s);">
|
||||
<Body size="XS">{getHighestRole(app.role).name}</Body>
|
||||
{#if allAppList.length}
|
||||
{#each allAppList as app}
|
||||
<div on:click={$goto(`../../overview/${app.devId}`)}>
|
||||
<ListItem
|
||||
title={app.name}
|
||||
iconBackground={app?.icon?.color || ""}
|
||||
icon={app?.icon?.name || "Apps"}
|
||||
>
|
||||
<div class="title ">
|
||||
<StatusLight />
|
||||
<div style="margin-left: var(--spacing-s);">
|
||||
<Body size="XS">d</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListItem>
|
||||
</ListItem>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="Apps" title="No apps" />
|
||||
|
|
|
@ -5,39 +5,33 @@
|
|||
Label,
|
||||
ModalContent,
|
||||
Multiselect,
|
||||
notifications,
|
||||
InputDropdown,
|
||||
} from "@budibase/bbui"
|
||||
import { users, groups } from "stores/portal"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { groups } from "stores/portal"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
export let disabled
|
||||
export let showOnboardingTypeModal
|
||||
const options = ["Email onboarding", "Basic onboarding"]
|
||||
let selected = options[0]
|
||||
let builder, admin
|
||||
|
||||
$: userData = [{ email: "", role: "", error: null }]
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: userData = [{ email: "", role: "", groups: [], error: null }]
|
||||
|
||||
/*
|
||||
async function createUserFlow() {
|
||||
try {
|
||||
const res = await users.invite({ email: "", builder, admin })
|
||||
notifications.success(res.message)
|
||||
analytics.captureEvent(Events.USER.INVITE, { type: selected })
|
||||
} catch (error) {
|
||||
notifications.error("Error inviting user")
|
||||
}
|
||||
}
|
||||
*/
|
||||
function addNewInput() {
|
||||
userData = [...userData, { email: "", role: "" }]
|
||||
}
|
||||
|
||||
function setValue(e) {
|
||||
userData.groups = e.detail
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
onConfirm={showOnboardingTypeModal}
|
||||
onConfirm={() => {
|
||||
showOnboardingTypeModal()
|
||||
dispatch("change", userData)
|
||||
}}
|
||||
size="M"
|
||||
title="Add new user"
|
||||
confirmText="Add user"
|
||||
|
@ -64,6 +58,7 @@
|
|||
|
||||
<Multiselect
|
||||
placeholder="Select User Groups"
|
||||
on:change={e => setValue(e)}
|
||||
label="User Groups"
|
||||
options={$groups}
|
||||
getOptionLabel={option => option.name}
|
||||
|
|
|
@ -1,8 +1,44 @@
|
|||
<script>
|
||||
import { Body, ModalContent, RadioGroup, Multiselect } from "@budibase/bbui"
|
||||
import {
|
||||
Body,
|
||||
ModalContent,
|
||||
RadioGroup,
|
||||
Multiselect,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { groups } from "stores/portal"
|
||||
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
const BYTES_IN_MB = 1000000
|
||||
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
|
||||
|
||||
export let showOnboardingTypeModal
|
||||
let files = []
|
||||
|
||||
let csvString = undefined
|
||||
|
||||
function parseCsv() {}
|
||||
|
||||
async function handleFile(evt) {
|
||||
const fileArray = Array.from(evt.target.files)
|
||||
if (fileArray.some(file => file.size >= FILE_SIZE_LIMIT)) {
|
||||
notifications.error(
|
||||
`Files cannot exceed ${
|
||||
FILE_SIZE_LIMIT / BYTES_IN_MB
|
||||
}MB. Please try again with smaller files.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Read CSV as plain text to upload alongside schema
|
||||
let reader = new FileReader()
|
||||
reader.addEventListener("load", function (e) {
|
||||
csvString = e.target.result
|
||||
files = fileArray
|
||||
})
|
||||
reader.readAsText(fileArray[0])
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
@ -12,13 +48,16 @@
|
|||
showCancelButton={false}
|
||||
cancelText="Cancel"
|
||||
showCloseIcon={false}
|
||||
onConfirm={showOnboardingTypeModal}
|
||||
disabled={!files.length}
|
||||
>
|
||||
<Body size="S">Import your users email addrresses from a CSV</Body>
|
||||
|
||||
<div class="container">
|
||||
<div class="inner">
|
||||
<Body size="S">Upload</Body>
|
||||
</div>
|
||||
<div class="dropzone">
|
||||
<input id="file-upload" accept=".csv" type="file" on:change={handleFile} />
|
||||
<label for="file-upload" class:uploaded={files[0]}>
|
||||
{#if files[0]}{files[0].name}{:else}Upload{/if}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<RadioGroup options={Constants.BuilderRoleDescriptions} />
|
||||
|
@ -49,4 +88,51 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.uploaded {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
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;
|
||||
outline: none;
|
||||
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
|
||||
-webkit-box-align: center;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
background-color: var(--grey-2);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: normal;
|
||||
border: var(--border-transparent);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { ModalContent, Body, Layout, Icon } from "@budibase/bbui"
|
||||
export let showConfirmationModal
|
||||
|
||||
export let chooseCreationType
|
||||
let emailOnboardingKey = "emailOnboarding"
|
||||
let basicOnboaridngKey = "basicOnboarding"
|
||||
|
||||
|
@ -13,7 +14,7 @@
|
|||
confirmText="Done"
|
||||
cancelText="Cancel"
|
||||
showCloseIcon={false}
|
||||
onConfirm={() => showConfirmationModal(selectedOnboardingType)}
|
||||
onConfirm={() => chooseCreationType(selectedOnboardingType)}
|
||||
disabled={!selectedOnboardingType}
|
||||
>
|
||||
<Layout noPadding gap="S">
|
||||
|
|
|
@ -33,7 +33,9 @@
|
|||
|
||||
<Table
|
||||
{schema}
|
||||
data={[{ email: "test", password: "§xz§§zvzxvxzv" }]}
|
||||
data={[
|
||||
{ email: "test", password: Math.random().toString(36).slice(2, 20) },
|
||||
]}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
||||
import PasswordModal from "./_components/PasswordModal.svelte"
|
||||
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
||||
import analytics, { Events } from "analytics"
|
||||
import TriggerAutomation from "../../../../../components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/TriggerAutomation.svelte"
|
||||
|
||||
const schema = {
|
||||
name: {},
|
||||
|
@ -42,6 +44,8 @@
|
|||
},
|
||||
}
|
||||
|
||||
$: userData = []
|
||||
|
||||
const accessTypes = [
|
||||
{
|
||||
icon: "User",
|
||||
|
@ -97,10 +101,51 @@
|
|||
onboardingTypeModal.show()
|
||||
}
|
||||
|
||||
function showConfirmationModal(onboardingType) {
|
||||
if (onboardingType === "emailOnboarding") {
|
||||
async function createUserFlow() {
|
||||
let emails = userData.map(x => x.email)
|
||||
try {
|
||||
const res = await users.invite({
|
||||
emails: emails,
|
||||
builder: true,
|
||||
admin: true,
|
||||
})
|
||||
notifications.success(res.message)
|
||||
analytics.captureEvent(Events.USER.INVITE, { type: "Email onboarding" })
|
||||
inviteConfirmationModal.show()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
notifications.error("Error inviting user")
|
||||
}
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
try {
|
||||
await users.create({
|
||||
email: $email,
|
||||
password,
|
||||
builder,
|
||||
admin,
|
||||
forceResetPassword: true,
|
||||
})
|
||||
notifications.success("Successfully created user")
|
||||
} catch (error) {
|
||||
notifications.error("Error creating user")
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseCreationType(onboardingType) {
|
||||
if (onboardingType === "emailOnboarding") {
|
||||
createUserFlow()
|
||||
} else {
|
||||
let newUser = await users.create({
|
||||
email: "auser5@test.com",
|
||||
password: Math.random().toString(36).slice(2, 20),
|
||||
builder: true,
|
||||
admin: true,
|
||||
forceResetPassword: true,
|
||||
})
|
||||
console.log(newUser)
|
||||
|
||||
passwordModal.show()
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +208,10 @@
|
|||
</Layout>
|
||||
|
||||
<Modal bind:this={createUserModal}>
|
||||
<AddUserModal {showOnboardingTypeModal} />
|
||||
<AddUserModal
|
||||
on:change={e => (userData = e.detail)}
|
||||
{showOnboardingTypeModal}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={inviteConfirmationModal}>
|
||||
|
@ -180,7 +228,7 @@
|
|||
</Modal>
|
||||
|
||||
<Modal bind:this={onboardingTypeModal}>
|
||||
<OnboardingTypeModal {showConfirmationModal} />
|
||||
<OnboardingTypeModal {chooseCreationType} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={passwordModal}>
|
||||
|
@ -188,7 +236,7 @@
|
|||
</Modal>
|
||||
|
||||
<Modal bind:this={importUsersModal}>
|
||||
<ImportUsersModal />
|
||||
<ImportUsersModal {showOnboardingTypeModal} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
|
||||
|
|
|
@ -1,6 +1,75 @@
|
|||
<script>
|
||||
import { Layout, Heading, Body, Button, List, ListItem } from "@budibase/bbui"
|
||||
import {
|
||||
Layout,
|
||||
Heading,
|
||||
Body,
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
Modal,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
import RoleSelect from "components/common/RoleSelect.svelte"
|
||||
import { users, groups, apps } from "stores/portal"
|
||||
import AssignmentModal from "./AssignmentModal.svelte"
|
||||
|
||||
export let app
|
||||
|
||||
let assignmentModal
|
||||
let appGroups = []
|
||||
let appUsers = []
|
||||
|
||||
$: appUsers = $users.filter(x => {
|
||||
return Object.keys(x.roles).some(y => {
|
||||
return extractAppId(y) === extractAppId(app.appId)
|
||||
})
|
||||
})
|
||||
$: appGroups = $groups.filter(x => {
|
||||
return x.apps.find(y => {
|
||||
return y.appId === app.appId
|
||||
})
|
||||
})
|
||||
function extractAppId(id) {
|
||||
const split = id?.split("_") || []
|
||||
return split.length ? split[split.length - 1] : null
|
||||
}
|
||||
|
||||
async function addData(appData) {
|
||||
let gr_prefix = "gr"
|
||||
let us_prefix = "us"
|
||||
appData.forEach(async data => {
|
||||
if (data.id.startsWith(gr_prefix)) {
|
||||
let matchedGroup = $groups.find(group => {
|
||||
return group._id === data.id
|
||||
})
|
||||
matchedGroup.apps.push(app)
|
||||
|
||||
groups.actions.save(matchedGroup)
|
||||
} else if (data.id.startsWith(us_prefix)) {
|
||||
let matchedUser = $users.find(user => {
|
||||
return user._id === data.id
|
||||
})
|
||||
|
||||
let newUser = {
|
||||
...matchedUser,
|
||||
roles: { [app.appId]: data.role },
|
||||
}
|
||||
|
||||
await users.save(newUser)
|
||||
}
|
||||
})
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
await users.init()
|
||||
await groups.actions.init()
|
||||
await apps.load()
|
||||
} catch (error) {
|
||||
notifications.error("Error")
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="access-tab">
|
||||
|
@ -11,31 +80,44 @@
|
|||
<Body size="S">
|
||||
Assign users to your app and define their access here</Body
|
||||
>
|
||||
<Button icon="User" cta>Assign users</Button>
|
||||
<Button on:click={assignmentModal.show} icon="User" cta
|
||||
>Assign users</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<List title="User Groups">
|
||||
<ListItem title="Design Team" icon="Brush" iconBackground="#348c6f">
|
||||
<RoleSelect autoWidth quiet value="POWER" />
|
||||
</ListItem>
|
||||
<ListItem title="Admin Team" icon="UserAdmin" iconBackground="#843c6f">
|
||||
<RoleSelect autoWidth quiet value="ADMIN" />
|
||||
</ListItem>
|
||||
{#each appGroups as group}
|
||||
<ListItem
|
||||
title={group.name}
|
||||
icon={group.icon}
|
||||
iconBackground={group.color}
|
||||
>
|
||||
<RoleSelect autoWidth quiet value={group.role} />
|
||||
</ListItem>
|
||||
{/each}
|
||||
</List>
|
||||
<List title="Users">
|
||||
<ListItem title="andy@gmail.com" avatar>
|
||||
<RoleSelect autoWidth quiet value="BASIC" />
|
||||
</ListItem>
|
||||
<ListItem title="jeff@gmail.com" avatar>
|
||||
<RoleSelect autoWidth quiet value="BASIC" />
|
||||
</ListItem>
|
||||
<ListItem title="tom@gmail.com" avatar>
|
||||
<RoleSelect autoWidth quiet value="BASIC" />
|
||||
</ListItem>
|
||||
{#each appUsers as user}
|
||||
<ListItem title={user.email} avatar>
|
||||
<RoleSelect
|
||||
autoWidth
|
||||
quiet
|
||||
value={user.roles[
|
||||
Object.keys(user.roles).find(
|
||||
x => extractAppId(x) === extractAppId(app.appId)
|
||||
)
|
||||
]}
|
||||
/>
|
||||
</ListItem>
|
||||
{/each}
|
||||
</List>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<Modal bind:this={assignmentModal}>
|
||||
<AssignmentModal {addData} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.access-tab {
|
||||
max-width: 600px;
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import { ModalContent, PickerDropdown, ActionButton } from "@budibase/bbui"
|
||||
import { users, groups, apps } from "stores/portal"
|
||||
import { roles } from "stores/backend"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
export let addData
|
||||
$: optionSections = {
|
||||
groups: {
|
||||
data: $groups,
|
||||
getLabel: group => group.name,
|
||||
getValue: group => group._id,
|
||||
getIcon: group => group.icon,
|
||||
getColour: group => group.color,
|
||||
},
|
||||
users: {
|
||||
data: $users,
|
||||
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: "" }]
|
||||
}
|
||||
|
||||
$: console.log(appData)
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
size="M"
|
||||
title="Assign users to your app"
|
||||
confirmText="Done"
|
||||
cancelText="Cancel"
|
||||
onConfirm={() => addData(appData)}
|
||||
showCloseIcon={false}
|
||||
>
|
||||
{#each appData as input, index}
|
||||
<PickerDropdown
|
||||
autocomplete
|
||||
primaryOptions={optionSections}
|
||||
primaryPlaceholder={"Search Users"}
|
||||
secondaryOptions={$roles}
|
||||
bind:primaryValue={input.id}
|
||||
bind:secondaryValue={input.role}
|
||||
getPrimaryOptionLabel={group => 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}
|
||||
|
||||
<div>
|
||||
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
|
||||
</div>
|
||||
</ModalContent>
|
|
@ -78,6 +78,7 @@ export function createAppStore() {
|
|||
subscribe: store.subscribe,
|
||||
load,
|
||||
update,
|
||||
extractAppId,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export function createGroupsStore() {
|
|||
icon: "",
|
||||
color: "",
|
||||
users: [],
|
||||
apps: [],
|
||||
}
|
||||
|
||||
const store = writable([DEFAULT_CONFIG])
|
||||
|
|
|
@ -3,21 +3,20 @@ import { API } from "api"
|
|||
import { update } from "lodash"
|
||||
|
||||
export function createUsersStore() {
|
||||
const { subscribe, set } = writable([])
|
||||
const store = writable([])
|
||||
|
||||
async function init() {
|
||||
const users = await API.getUsers()
|
||||
set(users)
|
||||
store.set(users)
|
||||
}
|
||||
|
||||
async function invite({ email, builder, admin }) {
|
||||
return API.inviteUser({
|
||||
email,
|
||||
async function invite({ emails, builder, admin }) {
|
||||
return API.inviteUsers({
|
||||
emails,
|
||||
builder,
|
||||
admin,
|
||||
})
|
||||
}
|
||||
|
||||
async function acceptInvite(inviteCode, password) {
|
||||
return API.acceptInvite({
|
||||
inviteCode,
|
||||
|
@ -55,12 +54,23 @@ export function createUsersStore() {
|
|||
update(users => users.filter(user => user._id !== id))
|
||||
}
|
||||
|
||||
async function save(data) {
|
||||
await API.saveUser(data)
|
||||
async function save(user) {
|
||||
const response = await API.saveUser(user)
|
||||
user._id = response._id
|
||||
user._rev = response._rev
|
||||
store.update(state => {
|
||||
const currentIdx = state.findIndex(user => user._id === user._id)
|
||||
if (currentIdx >= 0) {
|
||||
state.splice(currentIdx, 1, user)
|
||||
} else {
|
||||
state.push(user)
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
subscribe: store.subscribe,
|
||||
init,
|
||||
invite,
|
||||
acceptInvite,
|
||||
|
|
|
@ -81,6 +81,25 @@ 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
|
||||
*/
|
||||
inviteUsers: async ({ emails, builder, admin }) => {
|
||||
return await API.post({
|
||||
url: "/api/global/users/inviteMultiple",
|
||||
body: {
|
||||
emails,
|
||||
userInfo: {
|
||||
admin: admin ? { global: true } : undefined,
|
||||
builder: builder ? { global: true } : undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Accepts an invite to join the platform and creates a user.
|
||||
* @param inviteCode the invite code sent in the email
|
||||
|
|
|
@ -187,6 +187,35 @@ export const invite = async (ctx: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const inviteMultiple = async (ctx: any) => {
|
||||
let { emails, userInfo } = ctx.request.body
|
||||
let existing = false
|
||||
let existingEmail
|
||||
for (let email of emails) {
|
||||
if (await getGlobalUserByEmail(email)) {
|
||||
existing = true
|
||||
existingEmail = email
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
ctx.throw(400, `${existingEmail} already exists`)
|
||||
}
|
||||
if (!userInfo) {
|
||||
userInfo = {}
|
||||
}
|
||||
userInfo.tenantId = getTenantId()
|
||||
const opts: any = {
|
||||
subject: "{{ company }} platform invitation",
|
||||
info: userInfo,
|
||||
}
|
||||
await sendEmail(emails, EmailTemplatePurpose.INVITATION, opts)
|
||||
ctx.body = {
|
||||
message: "Invitations have been sent.",
|
||||
}
|
||||
}
|
||||
|
||||
export const inviteAccept = async (ctx: any) => {
|
||||
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
||||
try {
|
||||
|
|
|
@ -14,7 +14,8 @@ function buildGroupSaveValidation() {
|
|||
color: Joi.string().required(),
|
||||
icon: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
users: Joi.array().optional()
|
||||
users: Joi.array().optional(),
|
||||
apps: Joi.array().optional()
|
||||
}).required())
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,14 @@ function buildInviteValidation() {
|
|||
}).required())
|
||||
}
|
||||
|
||||
function buildInviteMultipleValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
emails: Joi.array().required(),
|
||||
userInfo: Joi.object().optional(),
|
||||
}).required())
|
||||
}
|
||||
|
||||
function buildInviteAcceptValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
|
@ -53,6 +61,19 @@ router
|
|||
buildInviteValidation(),
|
||||
controller.invite
|
||||
)
|
||||
.post(
|
||||
"/api/global/users/invite",
|
||||
adminOnly,
|
||||
buildInviteValidation(),
|
||||
controller.invite
|
||||
)
|
||||
.post(
|
||||
"/api/global/users/inviteMultiple",
|
||||
adminOnly,
|
||||
buildInviteMultipleValidation(),
|
||||
controller.inviteMultiple
|
||||
)
|
||||
|
||||
// non-global endpoints
|
||||
.post(
|
||||
"/api/global/users/invite/accept",
|
||||
|
|
|
@ -185,14 +185,27 @@ exports.sendEmail = async (
|
|||
// if there is a link code needed this will retrieve it
|
||||
const code = await getLinkCode(purpose, email, user, info)
|
||||
const context = await getSettingsTemplateContext(purpose, code)
|
||||
const message = {
|
||||
|
||||
let message = {
|
||||
from: from || config.from,
|
||||
to: email,
|
||||
html: await buildEmail(purpose, email, context, {
|
||||
user,
|
||||
contents,
|
||||
}),
|
||||
}
|
||||
|
||||
if (email.length > 1) {
|
||||
message = {
|
||||
...message,
|
||||
bcc: email,
|
||||
}
|
||||
} else {
|
||||
message = {
|
||||
...message,
|
||||
to: email,
|
||||
}
|
||||
}
|
||||
|
||||
if (subject || config.subject) {
|
||||
message.subject = await processString(subject || config.subject, context)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue