group / user app assignment
This commit is contained in:
parent
5ce3861ef2
commit
3de2123dc4
|
@ -5,6 +5,7 @@
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import clickOutside from "../../Actions/click_outside"
|
import clickOutside from "../../Actions/click_outside"
|
||||||
|
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||||
|
|
||||||
export let inputValue
|
export let inputValue
|
||||||
export let dropdownValue
|
export let dropdownValue
|
||||||
|
@ -18,6 +19,8 @@
|
||||||
export let options = []
|
export let options = []
|
||||||
export let getOptionLabel = option => extractProperty(option, "label")
|
export let getOptionLabel = option => extractProperty(option, "label")
|
||||||
export let getOptionValue = option => extractProperty(option, "value")
|
export let getOptionValue = option => extractProperty(option, "value")
|
||||||
|
export let getOptionColour = option => extractProperty(option, "colour")
|
||||||
|
|
||||||
export let isOptionSelected = () => false
|
export let isOptionSelected = () => false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let open = false
|
let open = false
|
||||||
$: fieldText = getFieldText(value, options, placeholder)
|
$: fieldText = getFieldText(value, options, placeholder)
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let value = "Anchor"
|
export let value
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let alignRight = false
|
export let alignRight = false
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
style={value ? `background: ${value};` : ""}
|
style={value ? `background: ${value};` : ""}
|
||||||
class:placeholder={!value}
|
class:placeholder={!value}
|
||||||
>
|
>
|
||||||
<Icon name={value} />
|
<Icon name={value || "UserGroup"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if open}
|
{#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 RadioGroup } from "./Form/RadioGroup.svelte"
|
||||||
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
||||||
export { default as InputDropdown } from "./Form/InputDropdown.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 DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
||||||
export { default as Popover } from "./Popover/Popover.svelte"
|
export { default as Popover } from "./Popover/Popover.svelte"
|
||||||
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
||||||
|
|
|
@ -22,23 +22,15 @@
|
||||||
export let groupId
|
export let groupId
|
||||||
let popoverAnchor
|
let popoverAnchor
|
||||||
let popover
|
let popover
|
||||||
$: group = $groups.find(x => x._id === groupId)
|
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
let selectedUsers = []
|
let selectedUsers = []
|
||||||
|
$: group = $groups.find(x => x._id === groupId)
|
||||||
$: filteredUsers = $users.filter(
|
$: filteredUsers = $users.filter(
|
||||||
user =>
|
user =>
|
||||||
selectedUsers &&
|
selectedUsers &&
|
||||||
user?.email?.toLowerCase().includes(searchTerm.toLowerCase())
|
user?.email?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
let app_list = [
|
$: console.log(group)
|
||||||
{
|
|
||||||
access: "ADMIN",
|
|
||||||
name: "test app",
|
|
||||||
icon: "Anchor",
|
|
||||||
color: "blue",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
async function addAll() {
|
async function addAll() {
|
||||||
selectedUsers = [...selectedUsers, ...filteredUsers]
|
selectedUsers = [...selectedUsers, ...filteredUsers]
|
||||||
group.users = selectedUsers
|
group.users = selectedUsers
|
||||||
|
@ -138,9 +130,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
{#if app_list.length}
|
{#if group?.apps}
|
||||||
{#each app_list as app}
|
{#each group.apps as app}
|
||||||
<ListItem title={app.name} icon={app.icon} iconBackground={app.color}>
|
<ListItem
|
||||||
|
title={app.name}
|
||||||
|
icon={app?.icon?.name || "Apps"}
|
||||||
|
iconBackground={app?.icon?.color || ""}
|
||||||
|
>
|
||||||
<div class="title ">
|
<div class="title ">
|
||||||
<StatusLight color={RoleUtils.getRoleColour(app.access)} />
|
<StatusLight color={RoleUtils.getRoleColour(app.access)} />
|
||||||
<div style="margin-left: var(--spacing-s);">
|
<div style="margin-left: var(--spacing-s);">
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
<Icon name="WebPage" />
|
<Icon name="WebPage" />
|
||||||
|
|
||||||
<div style="margin-left: var(--spacing-l)">
|
<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"}
|
: "s"}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
icon: "",
|
icon: "",
|
||||||
color: "",
|
color: "",
|
||||||
users: [],
|
users: [],
|
||||||
|
apps: [],
|
||||||
}
|
}
|
||||||
let proPlan = true
|
let proPlan = true
|
||||||
|
|
||||||
|
@ -82,13 +83,15 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="groupTable">
|
{#if proPlan}
|
||||||
{#each $groups as group}
|
<div class="groupTable">
|
||||||
<div>
|
{#each $groups as group}
|
||||||
<UserGroupsRow {saveGroup} {deleteGroup} {group} />
|
<div>
|
||||||
</div>
|
<UserGroupsRow {saveGroup} {deleteGroup} {group} />
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -24,7 +24,8 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
import { fetchData } from "helpers"
|
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 { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
|
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
|
||||||
|
@ -38,19 +39,28 @@
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
let popover
|
let popover
|
||||||
let selectedGroups = []
|
let selectedGroups = []
|
||||||
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : ""
|
let allAppList = []
|
||||||
|
$: console.log($apps)
|
||||||
// Merge the Apps list and the roles response to get something that makes sense for the table
|
$: console.log($userFetch.data)
|
||||||
$: 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],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
$: 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
|
// Used for searching through groups in the add group popover
|
||||||
$: filteredGroups = $groups.filter(
|
$: filteredGroups = $groups.filter(
|
||||||
group =>
|
group =>
|
||||||
|
@ -58,16 +68,13 @@
|
||||||
group?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
group?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
$: appList = allAppList.filter(app => !!app.role[0])
|
|
||||||
|
|
||||||
$: userGroups = $groups.filter(x => {
|
$: userGroups = $groups.filter(x => {
|
||||||
return x.users?.some(y => {
|
return x.users?.find(y => {
|
||||||
return y._id === userId
|
return y._id === userId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const userFetch = fetchData(`/api/global/users/${userId}`)
|
const userFetch = fetchData(`/api/global/users/${userId}`)
|
||||||
const apps = fetchData(`/api/global/roles`)
|
|
||||||
async function deleteUser() {
|
async function deleteUser() {
|
||||||
try {
|
try {
|
||||||
await users.delete(userId)
|
await users.delete(userId)
|
||||||
|
@ -166,6 +173,7 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
|
await apps.load()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting User groups")
|
notifications.error("Error getting User groups")
|
||||||
}
|
}
|
||||||
|
@ -243,9 +251,7 @@
|
||||||
<div class="tableTitle">
|
<div class="tableTitle">
|
||||||
<div>
|
<div>
|
||||||
<Heading size="XS">User groups</Heading>
|
<Heading size="XS">User groups</Heading>
|
||||||
<Body size="S"
|
<Body size="S">Add or remove this user from user groups</Body>
|
||||||
>Manage apps that this User group has been assigned to</Body
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div bind:this={popoverAnchor}>
|
<div bind:this={popoverAnchor}>
|
||||||
<Button on:click={popover.show()} icon="UserGroup" cta
|
<Button on:click={popover.show()} icon="UserGroup" cta
|
||||||
|
@ -291,25 +297,27 @@
|
||||||
<div class="appsTitle">
|
<div class="appsTitle">
|
||||||
<Heading weight="light" size="XS">Apps</Heading>
|
<Heading weight="light" size="XS">Apps</Heading>
|
||||||
<div style="margin-top: var(--spacing-xs)">
|
<div style="margin-top: var(--spacing-xs)">
|
||||||
<Body size="S"
|
<Body size="S">Manage apps that this user has been assigned to</Body>
|
||||||
>Manage apps that this User group has been assigned to</Body
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
{#if appList.length}
|
{#if allAppList.length}
|
||||||
{#each appList as app}
|
{#each allAppList as app}
|
||||||
<ListItem title={app.name} icon="Apps">
|
<div on:click={$goto(`../../overview/${app.devId}`)}>
|
||||||
<div class="title ">
|
<ListItem
|
||||||
<StatusLight
|
title={app.name}
|
||||||
color={RoleUtils.getRoleColour(getHighestRole(app.role)._id)}
|
iconBackground={app?.icon?.color || ""}
|
||||||
/>
|
icon={app?.icon?.name || "Apps"}
|
||||||
<div style="margin-left: var(--spacing-s);">
|
>
|
||||||
<Body size="XS">{getHighestRole(app.role).name}</Body>
|
<div class="title ">
|
||||||
|
<StatusLight />
|
||||||
|
<div style="margin-left: var(--spacing-s);">
|
||||||
|
<Body size="XS">d</Body>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ListItem>
|
||||||
</ListItem>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<ListItem icon="Apps" title="No apps" />
|
<ListItem icon="Apps" title="No apps" />
|
||||||
|
|
|
@ -5,39 +5,33 @@
|
||||||
Label,
|
Label,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Multiselect,
|
Multiselect,
|
||||||
notifications,
|
|
||||||
InputDropdown,
|
InputDropdown,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { users, groups } from "stores/portal"
|
import { createEventDispatcher } from "svelte"
|
||||||
import analytics, { Events } from "analytics"
|
import { groups } from "stores/portal"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let disabled
|
export let disabled
|
||||||
export let showOnboardingTypeModal
|
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() {
|
function addNewInput() {
|
||||||
userData = [...userData, { email: "", role: "" }]
|
userData = [...userData, { email: "", role: "" }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setValue(e) {
|
||||||
|
userData.groups = e.detail
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onConfirm={showOnboardingTypeModal}
|
onConfirm={() => {
|
||||||
|
showOnboardingTypeModal()
|
||||||
|
dispatch("change", userData)
|
||||||
|
}}
|
||||||
size="M"
|
size="M"
|
||||||
title="Add new user"
|
title="Add new user"
|
||||||
confirmText="Add user"
|
confirmText="Add user"
|
||||||
|
@ -64,6 +58,7 @@
|
||||||
|
|
||||||
<Multiselect
|
<Multiselect
|
||||||
placeholder="Select User Groups"
|
placeholder="Select User Groups"
|
||||||
|
on:change={e => setValue(e)}
|
||||||
label="User Groups"
|
label="User Groups"
|
||||||
options={$groups}
|
options={$groups}
|
||||||
getOptionLabel={option => option.name}
|
getOptionLabel={option => option.name}
|
||||||
|
|
|
@ -1,8 +1,44 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, ModalContent, RadioGroup, Multiselect } from "@budibase/bbui"
|
import {
|
||||||
|
Body,
|
||||||
|
ModalContent,
|
||||||
|
RadioGroup,
|
||||||
|
Multiselect,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { groups } from "stores/portal"
|
import { groups } from "stores/portal"
|
||||||
|
|
||||||
import { Constants } from "@budibase/frontend-core"
|
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>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -12,13 +48,16 @@
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
|
onConfirm={showOnboardingTypeModal}
|
||||||
|
disabled={!files.length}
|
||||||
>
|
>
|
||||||
<Body size="S">Import your users email addrresses from a CSV</Body>
|
<Body size="S">Import your users email addrresses from a CSV</Body>
|
||||||
|
|
||||||
<div class="container">
|
<div class="dropzone">
|
||||||
<div class="inner">
|
<input id="file-upload" accept=".csv" type="file" on:change={handleFile} />
|
||||||
<Body size="S">Upload</Body>
|
<label for="file-upload" class:uploaded={files[0]}>
|
||||||
</div>
|
{#if files[0]}{files[0].name}{:else}Upload{/if}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RadioGroup options={Constants.BuilderRoleDescriptions} />
|
<RadioGroup options={Constants.BuilderRoleDescriptions} />
|
||||||
|
@ -49,4 +88,51 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: 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>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Body, Layout, Icon } from "@budibase/bbui"
|
import { ModalContent, Body, Layout, Icon } from "@budibase/bbui"
|
||||||
export let showConfirmationModal
|
|
||||||
|
export let chooseCreationType
|
||||||
let emailOnboardingKey = "emailOnboarding"
|
let emailOnboardingKey = "emailOnboarding"
|
||||||
let basicOnboaridngKey = "basicOnboarding"
|
let basicOnboaridngKey = "basicOnboarding"
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
confirmText="Done"
|
confirmText="Done"
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
onConfirm={() => showConfirmationModal(selectedOnboardingType)}
|
onConfirm={() => chooseCreationType(selectedOnboardingType)}
|
||||||
disabled={!selectedOnboardingType}
|
disabled={!selectedOnboardingType}
|
||||||
>
|
>
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
|
|
|
@ -33,7 +33,9 @@
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
{schema}
|
{schema}
|
||||||
data={[{ email: "test", password: "§xz§§zvzxvxzv" }]}
|
data={[
|
||||||
|
{ email: "test", password: Math.random().toString(36).slice(2, 20) },
|
||||||
|
]}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
||||||
import PasswordModal from "./_components/PasswordModal.svelte"
|
import PasswordModal from "./_components/PasswordModal.svelte"
|
||||||
import ImportUsersModal from "./_components/ImportUsersModal.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 = {
|
const schema = {
|
||||||
name: {},
|
name: {},
|
||||||
|
@ -42,6 +44,8 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: userData = []
|
||||||
|
|
||||||
const accessTypes = [
|
const accessTypes = [
|
||||||
{
|
{
|
||||||
icon: "User",
|
icon: "User",
|
||||||
|
@ -97,10 +101,51 @@
|
||||||
onboardingTypeModal.show()
|
onboardingTypeModal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function showConfirmationModal(onboardingType) {
|
async function createUserFlow() {
|
||||||
if (onboardingType === "emailOnboarding") {
|
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()
|
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 {
|
} 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()
|
passwordModal.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,7 +208,10 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={createUserModal}>
|
<Modal bind:this={createUserModal}>
|
||||||
<AddUserModal {showOnboardingTypeModal} />
|
<AddUserModal
|
||||||
|
on:change={e => (userData = e.detail)}
|
||||||
|
{showOnboardingTypeModal}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={inviteConfirmationModal}>
|
<Modal bind:this={inviteConfirmationModal}>
|
||||||
|
@ -180,7 +228,7 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={onboardingTypeModal}>
|
<Modal bind:this={onboardingTypeModal}>
|
||||||
<OnboardingTypeModal {showConfirmationModal} />
|
<OnboardingTypeModal {chooseCreationType} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={passwordModal}>
|
<Modal bind:this={passwordModal}>
|
||||||
|
@ -188,7 +236,7 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={importUsersModal}>
|
<Modal bind:this={importUsersModal}>
|
||||||
<ImportUsersModal />
|
<ImportUsersModal {showOnboardingTypeModal} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
|
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
|
||||||
|
|
|
@ -1,6 +1,75 @@
|
||||||
<script>
|
<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 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>
|
</script>
|
||||||
|
|
||||||
<div class="access-tab">
|
<div class="access-tab">
|
||||||
|
@ -11,31 +80,44 @@
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Assign users to your app and define their access here</Body
|
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>
|
||||||
</div>
|
</div>
|
||||||
<List title="User Groups">
|
<List title="User Groups">
|
||||||
<ListItem title="Design Team" icon="Brush" iconBackground="#348c6f">
|
{#each appGroups as group}
|
||||||
<RoleSelect autoWidth quiet value="POWER" />
|
<ListItem
|
||||||
</ListItem>
|
title={group.name}
|
||||||
<ListItem title="Admin Team" icon="UserAdmin" iconBackground="#843c6f">
|
icon={group.icon}
|
||||||
<RoleSelect autoWidth quiet value="ADMIN" />
|
iconBackground={group.color}
|
||||||
</ListItem>
|
>
|
||||||
|
<RoleSelect autoWidth quiet value={group.role} />
|
||||||
|
</ListItem>
|
||||||
|
{/each}
|
||||||
</List>
|
</List>
|
||||||
<List title="Users">
|
<List title="Users">
|
||||||
<ListItem title="andy@gmail.com" avatar>
|
{#each appUsers as user}
|
||||||
<RoleSelect autoWidth quiet value="BASIC" />
|
<ListItem title={user.email} avatar>
|
||||||
</ListItem>
|
<RoleSelect
|
||||||
<ListItem title="jeff@gmail.com" avatar>
|
autoWidth
|
||||||
<RoleSelect autoWidth quiet value="BASIC" />
|
quiet
|
||||||
</ListItem>
|
value={user.roles[
|
||||||
<ListItem title="tom@gmail.com" avatar>
|
Object.keys(user.roles).find(
|
||||||
<RoleSelect autoWidth quiet value="BASIC" />
|
x => extractAppId(x) === extractAppId(app.appId)
|
||||||
</ListItem>
|
)
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
{/each}
|
||||||
</List>
|
</List>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:this={assignmentModal}>
|
||||||
|
<AssignmentModal {addData} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.access-tab {
|
.access-tab {
|
||||||
max-width: 600px;
|
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,
|
subscribe: store.subscribe,
|
||||||
load,
|
load,
|
||||||
update,
|
update,
|
||||||
|
extractAppId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ export function createGroupsStore() {
|
||||||
icon: "",
|
icon: "",
|
||||||
color: "",
|
color: "",
|
||||||
users: [],
|
users: [],
|
||||||
|
apps: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = writable([DEFAULT_CONFIG])
|
const store = writable([DEFAULT_CONFIG])
|
||||||
|
|
|
@ -3,21 +3,20 @@ import { API } from "api"
|
||||||
import { update } from "lodash"
|
import { update } from "lodash"
|
||||||
|
|
||||||
export function createUsersStore() {
|
export function createUsersStore() {
|
||||||
const { subscribe, set } = writable([])
|
const store = writable([])
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const users = await API.getUsers()
|
const users = await API.getUsers()
|
||||||
set(users)
|
store.set(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invite({ email, builder, admin }) {
|
async function invite({ emails, builder, admin }) {
|
||||||
return API.inviteUser({
|
return API.inviteUsers({
|
||||||
email,
|
emails,
|
||||||
builder,
|
builder,
|
||||||
admin,
|
admin,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptInvite(inviteCode, password) {
|
async function acceptInvite(inviteCode, password) {
|
||||||
return API.acceptInvite({
|
return API.acceptInvite({
|
||||||
inviteCode,
|
inviteCode,
|
||||||
|
@ -55,12 +54,23 @@ export function createUsersStore() {
|
||||||
update(users => users.filter(user => user._id !== id))
|
update(users => users.filter(user => user._id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(data) {
|
async function save(user) {
|
||||||
await API.saveUser(data)
|
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 {
|
return {
|
||||||
subscribe,
|
subscribe: store.subscribe,
|
||||||
init,
|
init,
|
||||||
invite,
|
invite,
|
||||||
acceptInvite,
|
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.
|
* Accepts an invite to join the platform and creates a user.
|
||||||
* @param inviteCode the invite code sent in the email
|
* @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) => {
|
export const inviteAccept = async (ctx: any) => {
|
||||||
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -14,7 +14,8 @@ function buildGroupSaveValidation() {
|
||||||
color: Joi.string().required(),
|
color: Joi.string().required(),
|
||||||
icon: Joi.string().required(),
|
icon: Joi.string().required(),
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
users: Joi.array().optional()
|
users: Joi.array().optional(),
|
||||||
|
apps: Joi.array().optional()
|
||||||
}).required())
|
}).required())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,14 @@ function buildInviteValidation() {
|
||||||
}).required())
|
}).required())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildInviteMultipleValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
emails: Joi.array().required(),
|
||||||
|
userInfo: Joi.object().optional(),
|
||||||
|
}).required())
|
||||||
|
}
|
||||||
|
|
||||||
function buildInviteAcceptValidation() {
|
function buildInviteAcceptValidation() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.object({
|
||||||
|
@ -53,6 +61,19 @@ router
|
||||||
buildInviteValidation(),
|
buildInviteValidation(),
|
||||||
controller.invite
|
controller.invite
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/api/global/users/invite",
|
||||||
|
adminOnly,
|
||||||
|
buildInviteValidation(),
|
||||||
|
controller.invite
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/api/global/users/inviteMultiple",
|
||||||
|
adminOnly,
|
||||||
|
buildInviteMultipleValidation(),
|
||||||
|
controller.inviteMultiple
|
||||||
|
)
|
||||||
|
|
||||||
// non-global endpoints
|
// non-global endpoints
|
||||||
.post(
|
.post(
|
||||||
"/api/global/users/invite/accept",
|
"/api/global/users/invite/accept",
|
||||||
|
|
|
@ -185,14 +185,27 @@ exports.sendEmail = async (
|
||||||
// if there is a link code needed this will retrieve it
|
// if there is a link code needed this will retrieve it
|
||||||
const code = await getLinkCode(purpose, email, user, info)
|
const code = await getLinkCode(purpose, email, user, info)
|
||||||
const context = await getSettingsTemplateContext(purpose, code)
|
const context = await getSettingsTemplateContext(purpose, code)
|
||||||
const message = {
|
|
||||||
|
let message = {
|
||||||
from: from || config.from,
|
from: from || config.from,
|
||||||
to: email,
|
|
||||||
html: await buildEmail(purpose, email, context, {
|
html: await buildEmail(purpose, email, context, {
|
||||||
user,
|
user,
|
||||||
contents,
|
contents,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (email.length > 1) {
|
||||||
|
message = {
|
||||||
|
...message,
|
||||||
|
bcc: email,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message = {
|
||||||
|
...message,
|
||||||
|
to: email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (subject || config.subject) {
|
if (subject || config.subject) {
|
||||||
message.subject = await processString(subject || config.subject, context)
|
message.subject = await processString(subject || config.subject, context)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue