add user groups UI

This commit is contained in:
Peter Clement 2022-06-14 14:57:34 +01:00
parent b3227fe51e
commit d529d7456f
8 changed files with 530 additions and 4 deletions

View File

@ -1,5 +1,5 @@
<script> <script>
import { createEventDispatcher } from "svelte" //import { createEventDispatcher } from "svelte"
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import clickOutside from "../Actions/click_outside" import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
@ -11,7 +11,7 @@
let open = false let open = false
const dispatch = createEventDispatcher() // const dispatch = createEventDispatcher()
const iconList = [ const iconList = [
{ {
@ -44,11 +44,12 @@
], ],
}, },
] ]
/*
const onChange = value => { const onChange = value => {
dispatch("change", value) dispatch("change", value)
open = false open = false
} }
*/
</script> </script>
<div class="container"> <div class="container">

View File

@ -52,6 +52,11 @@
href: "/builder/portal/manage/users", href: "/builder/portal/manage/users",
heading: "Manage", heading: "Manage",
}, },
{
title: "User Groups",
href: "/builder/portal/manage/groups",
},
{ title: "Auth", href: "/builder/portal/manage/auth" }, { title: "Auth", href: "/builder/portal/manage/auth" },
{ title: "Email", href: "/builder/portal/manage/email" }, { title: "Email", href: "/builder/portal/manage/email" },
{ {

View File

@ -9,10 +9,14 @@
$redirect("../") $redirect("../")
} }
} }
$: wide =
$page.path.includes("email/:template") ||
($page.path.includes("groups") && !$page.path.includes(":groupId"))
</script> </script>
{#if $auth.isAdmin} {#if $auth.isAdmin}
<Page maxWidth="90ch" wide={$page.path.includes("email/:template")}> <Page maxWidth="90ch" {wide}>
<slot /> <slot />
</Page> </Page>
{/if} {/if}

View File

@ -0,0 +1,211 @@
<script>
import { goto } from "@roxi/routify"
import {
ActionButton,
Button,
Layout,
Heading,
Body,
Icon,
Popover,
Search,
Divider,
Detail,
} from "@budibase/bbui"
import UserRow from "./_components/UserRow.svelte"
import { users } from "stores/portal"
import { onMount } from "svelte"
let popoverAnchor
let popover
let searchTerm = ""
let selectedUsers = []
$: filteredUsers = $users.filter(user =>
user?.email?.toLowerCase().includes(searchTerm.toLowerCase())
)
let group = {
_id: "gr_123456",
color: "green",
icon: "Anchor",
name: "Core Team",
userCount: 5,
appCount: 2,
}
let groupUsers = [
{
email: "peter@budibase.com",
access: "Developer",
},
]
/*
function getGroup() {
return
}
*/
function selectUser(id) {
let user = selectedUsers.find(user_id => user_id === id)
if (user) {
selectedUsers = selectedUsers.filter(id => id !== user)
} else {
selectedUsers = [...selectedUsers, id]
}
}
onMount(() => {
console.log($users)
})
</script>
<Layout noPadding>
<div>
<ActionButton on:click={() => $goto("../groups")} size="S" icon="ArrowLeft">
Back
</ActionButton>
</div>
<div class="header">
<div class="title">
<div style="background: {group.color};" class="circle">
<div>
<Icon size="M" name={group.icon} />
</div>
</div>
<div class="text-padding">
<Heading>{group.name}</Heading>
</div>
</div>
<div bind:this={popoverAnchor}>
<Button on:click={popover.show()} icon="UserAdd" cta>Add User</Button>
</div>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<div style="padding: var(--spacing-m)">
<Search placeholder="Search" bind:value={searchTerm} />
<div class="users-header header">
<div>
<Detail
>{filteredUsers.length} User{filteredUsers.length === 1
? ""
: "s"}</Detail
>
</div>
<div>
<ActionButton emphasized size="S">Add all</ActionButton>
</div>
</div>
<Divider noMargin />
<div>
{#each filteredUsers as user}
<div
on:click={selectUser(user._id)}
style="padding-bottom: var(--spacing-m)"
class="user-selection"
>
<div>
{user.email}
</div>
{#if selectedUsers.includes(user._id)}
<div>
<Icon
color="var(--spectrum-global-color-blue-600);"
name="Checkmark"
/>
</div>
{/if}
</div>
{/each}
</div>
</div>
</Popover>
</div>
<div class="usersTable">
{#if groupUsers.length}
{#each groupUsers as user}
<div>
<UserRow {user} />
</div>
{/each}
{:else}
<div>
<div class="title header text-padding">
<Icon name="UserGroup" />
<div class="text-padding">
<Body size="S">You have no users in this team</Body>
</div>
</div>
</div>
{/if}
</div>
</Layout>
<style>
.text-padding {
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;
}
.title {
display: flex;
}
.circle {
border-radius: 50%;
height: 30px;
color: white;
font-weight: bold;
display: inline-block;
font-size: 1.2em;
width: 30px;
}
.circle > div {
padding: calc(1.5 * var(--spacing-xs)) var(--spacing-xs);
}
.usersTable {
display: grid;
grid-template-rows: auto;
align-items: center;
border-bottom: 1px solid var(--spectrum-alias-border-color-mid);
border-left: 1px solid var(--spectrum-alias-border-color-mid);
background: var(--spectrum-global-color-gray-50);
}
.usersTable :global(> div) {
background: var(--bg-color);
height: 70px;
display: grid;
align-items: center;
grid-gap: var(--spacing-xl);
grid-template-columns: 0.1fr 0.6fr 2fr 0.4fr;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 var(--spacing-s);
border-top: 1px solid var(--spectrum-alias-border-color-mid);
border-right: 1px solid var(--spectrum-alias-border-color-mid);
}
</style>

View File

@ -0,0 +1,92 @@
<script>
import { Button, Icon, Body } from "@budibase/bbui"
import { goto } from "@roxi/routify"
export let group
let { icon, color, name, userCount, appCount } = group
</script>
<div class="title">
<div class="name" style="display: flex; margin-left: var(--spacing-xl)">
<div style="background: {color};" class="circle">
<div>
<Icon size="M" name={icon} />
</div>
</div>
<div class="name" data-cy="app-name-link"><Body size="S">{name}</Body></div>
</div>
</div>
<div class="desktop tableElement">
<Icon name="User" />
<div style="margin-left: var(--spacing-l">
{parseInt(userCount)} app{parseInt(userCount) === 1 ? "" : "s"}
</div>
</div>
<div class="desktop tableElement">
<Icon name="WebPage" />
<div style="margin-left: var(--spacing-l">
{parseInt(appCount)} app{parseInt(appCount) === 1 ? "" : "s"}
</div>
</div>
<div>
<div class="app-row-actions">
<div>
<Button on:click={() => $goto(`./${group._id}`)} size="S" cta
>Manage</Button
>
</div>
</div>
</div>
<style>
.app-row-actions {
display: flex;
float: right;
margin-right: var(--spacing-xl);
}
.name {
grid-gap: var(--spacing-xl);
grid-template-columns: 75px 75px;
align-items: center;
}
.circle {
border-radius: 50%;
height: 30px;
color: white;
font-weight: bold;
display: inline-block;
font-size: 1.2em;
width: 30px;
}
.tableElement {
display: flex;
}
.circle > div {
padding: calc(1.5 * var(--spacing-xs)) var(--spacing-xs);
}
.name {
text-decoration: none;
overflow: hidden;
}
.name :global(.spectrum-Heading) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-left: calc(1.5 * var(--spacing-xl));
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
transition: color 130ms ease;
}
@media (max-width: 640px) {
.desktop {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,55 @@
<script>
import { Icon, Body, Avatar } from "@budibase/bbui"
export let user
function removeFromGroup() {}
</script>
<div class="title">
<div class="name" style="display: flex; margin-left: var(--spacing-xl)">
<div>
<Avatar size="L" initials="PC" />
</div>
</div>
</div>
<div class="desktop">
<div style="display: flex; align-items: center;">
<Body size="M">{user.email}</Body>
<div style="opacity: 0.5; margin: var(--spacing-xs) 0 0 var(--spacing-m)">
<Body size="XS">{user.access}</Body>
</div>
</div>
</div>
<div class="desktop" />
<div><Icon on:click={removeFromGroup} hoverable size="L" name="Close" /></div>
<style>
.name {
grid-gap: var(--spacing-xl);
grid-template-columns: 75px 75px;
align-items: center;
}
.name {
text-decoration: none;
overflow: hidden;
}
.name :global(.spectrum-Heading) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-left: calc(1.5 * var(--spacing-xl));
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
transition: color 130ms ease;
}
@media (max-width: 640px) {
.desktop {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,3 @@
<div style="float: right;">
<slot />
</div>

View File

@ -0,0 +1,155 @@
<script>
import {
Layout,
Heading,
ColorPicker,
Body,
Button,
Modal,
ModalContent,
Input,
Tag,
Tags,
IconPicker,
} from "@budibase/bbui"
import UserGroupsRow from "./_components/UserGroupsRow.svelte"
let modal
let selectedColor
let selectedIcon
let proPlan = true
let userGroupData = [
{
_id: "gr_123456",
color: "green",
icon: "Anchor",
name: "Core Team",
userCount: 5,
appCount: 2,
},
{
_id: "gr_45678",
color: "red",
icon: "Beaker",
name: "QA Team",
userCount: 3,
appCount: 7,
},
]
</script>
<Layout noPadding>
<Layout gap="XS" noPadding>
<div style="display: flex;">
<Heading size="M">User groups</Heading>
{#if !proPlan}
<Tags>
<div class="tags">
<div class="tag">
<Tag icon="LockClosed">Pro plan</Tag>
</div>
</div>
</Tags>
{/if}
</div>
<Body>Easily assign and manage your users access with User Groups</Body>
</Layout>
<div class="align-buttons">
<Button
icon={proPlan ? "UserGroup" : ""}
cta={proPlan}
on:click={() => modal.show()}
>{proPlan ? "Create user group" : "Upgrade Account"}</Button
>
{#if !proPlan}
<Button
secondary
on:click={() => {
window.open("https://budibase.com/pricing/", "_blank")
}}>View Plans</Button
>
{/if}
</div>
<div class="groupTable">
{#each userGroupData as group}
<div>
<UserGroupsRow {group} />
</div>
{/each}
</div>
</Layout>
<Modal bind:this={modal}>
<ModalContent size="M" title="Create User Group" confirmText="Save">
<Input label="Team name" />
<div class="modal-format">
<div class="modal-inner">
<Body size="XS">Icon</Body>
<div class="modal-spacing">
<IconPicker bind:value={selectedIcon} />
</div>
</div>
<div class="modal-inner">
<Body size="XS">Color</Body>
<div class="modal-spacing">
<ColorPicker
bind:value={selectedColor}
on:change={e => (selectedColor = e.detail)}
/>
</div>
</div>
</div>
</ModalContent>
</Modal>
<style>
.modal-format {
display: flex;
justify-content: space-between;
width: 40%;
}
.modal-inner {
display: flex;
align-items: center;
}
.modal-spacing {
margin-left: var(--spacing-l);
}
.align-buttons {
display: flex;
column-gap: var(--spacing-xl);
}
.tag {
margin-top: var(--spacing-xs);
margin-left: var(--spacing-m);
}
.groupTable {
display: grid;
grid-template-rows: auto;
align-items: center;
border-bottom: 1px solid var(--spectrum-alias-border-color-mid);
border-left: 1px solid var(--spectrum-alias-border-color-mid);
background: var(--spectrum-global-color-gray-50);
}
.groupTable :global(> div) {
background: var(--bg-color);
height: 70px;
display: grid;
align-items: center;
grid-gap: var(--spacing-xl);
grid-template-columns: 2fr 2fr 2fr auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 var(--spacing-s);
border-top: 1px solid var(--spectrum-alias-border-color-mid);
border-right: 1px solid var(--spectrum-alias-border-color-mid);
}
</style>