Merge pull request #15634 from Budibase/fix/group-ui-issues
Group UI improvements
This commit is contained in:
commit
c13dfbf76a
|
@ -11,6 +11,7 @@
|
||||||
export let active = false
|
export let active = false
|
||||||
export let inactive = false
|
export let inactive = false
|
||||||
export let hoverable = false
|
export let hoverable = false
|
||||||
|
export let outlineColor = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
class:spectrum-Label--seafoam={seafoam}
|
class:spectrum-Label--seafoam={seafoam}
|
||||||
class:spectrum-Label--active={active}
|
class:spectrum-Label--active={active}
|
||||||
class:spectrum-Label--inactive={inactive}
|
class:spectrum-Label--inactive={inactive}
|
||||||
|
style={outlineColor ? `border: 2px solid ${outlineColor}` : ""}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -28,6 +28,9 @@
|
||||||
<svg
|
<svg
|
||||||
on:contextmenu
|
on:contextmenu
|
||||||
on:click
|
on:click
|
||||||
|
on:mouseover
|
||||||
|
on:mouseleave
|
||||||
|
on:focus
|
||||||
class:hoverable
|
class:hoverable
|
||||||
class:disabled
|
class:disabled
|
||||||
class="spectrum-Icon spectrum-Icon--size{size}"
|
class="spectrum-Icon spectrum-Icon--size{size}"
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
.main {
|
.main {
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
|
export let groups = []
|
||||||
|
function tooltip(groups) {
|
||||||
|
const sortedNames = groups
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map(group => group.name)
|
||||||
|
return `Member of ${helpers.lists.punctuateList(sortedNames)}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="icon">
|
||||||
|
<Icon
|
||||||
|
name="Info"
|
||||||
|
size="XS"
|
||||||
|
color="grey"
|
||||||
|
hoverable
|
||||||
|
tooltip={tooltip(groups)}
|
||||||
|
tooltipPosition="top"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon {
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -37,6 +37,7 @@
|
||||||
import { emailValidator } from "@/helpers/validation"
|
import { emailValidator } from "@/helpers/validation"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
import InfoDisplay from "../design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
import InfoDisplay from "../design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||||
|
import BuilderGroupPopover from "./BuilderGroupPopover.svelte"
|
||||||
|
|
||||||
let query = null
|
let query = null
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
@ -197,12 +198,19 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const update = await users.get(user._id)
|
const update = await users.get(user._id)
|
||||||
|
const newRoles = {
|
||||||
|
...update.roles,
|
||||||
|
[prodAppId]: role,
|
||||||
|
}
|
||||||
|
// make sure no undefined/null roles (during removal)
|
||||||
|
for (let [appId, role] of Object.entries(newRoles)) {
|
||||||
|
if (!role) {
|
||||||
|
delete newRoles[appId]
|
||||||
|
}
|
||||||
|
}
|
||||||
await users.save({
|
await users.save({
|
||||||
...update,
|
...update,
|
||||||
roles: {
|
roles: newRoles,
|
||||||
...update.roles,
|
|
||||||
[prodAppId]: role,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
await searchUsers(query, $builderStore.builderSidePanel, loaded)
|
await searchUsers(query, $builderStore.builderSidePanel, loaded)
|
||||||
}
|
}
|
||||||
|
@ -539,6 +547,10 @@
|
||||||
creationAccessType = Constants.Roles.CREATOR
|
creationAccessType = Constants.Roles.CREATOR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const itemCountText = (word, count) => {
|
||||||
|
return `${count} ${word}${count !== 1 ? "s" : ""}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeyDown} />
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
|
@ -701,13 +713,11 @@
|
||||||
>
|
>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<GroupIcon {group} size="S" />
|
<GroupIcon {group} size="S" />
|
||||||
<div>
|
<div class="group-name">
|
||||||
{group.name}
|
{group.name}
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-entity-meta">
|
<div class="auth-entity-meta">
|
||||||
{`${group.users?.length} user${
|
{itemCountText("user", group.users?.length)}
|
||||||
group.users?.length != 1 ? "s" : ""
|
|
||||||
}`}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-entity-access">
|
<div class="auth-entity-access">
|
||||||
|
@ -741,16 +751,33 @@
|
||||||
<div class="auth-entity-access-title">Access</div>
|
<div class="auth-entity-access-title">Access</div>
|
||||||
</div>
|
</div>
|
||||||
{#each allUsers as user}
|
{#each allUsers as user}
|
||||||
|
{@const userGroups = sdk.users.getUserAppGroups(
|
||||||
|
$appStore.appId,
|
||||||
|
user,
|
||||||
|
$groups
|
||||||
|
)}
|
||||||
<div class="auth-entity">
|
<div class="auth-entity">
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<div class="user-email" title={user.email}>
|
<div class="user-groups">
|
||||||
{user.email}
|
<div class="user-email" title={user.email}>
|
||||||
|
{user.email}
|
||||||
|
</div>
|
||||||
|
{#if userGroups.length}
|
||||||
|
<div class="group-info">
|
||||||
|
<div class="auth-entity-meta">
|
||||||
|
{itemCountText("group", userGroups.length)}
|
||||||
|
</div>
|
||||||
|
<BuilderGroupPopover groups={userGroups} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-entity-access" class:muted={user.group}>
|
<div class="auth-entity-access" class:muted={user.group}>
|
||||||
<RoleSelect
|
<RoleSelect
|
||||||
footer={getRoleFooter(user)}
|
footer={getRoleFooter(user)}
|
||||||
placeholder={false}
|
placeholder={userGroups?.length
|
||||||
|
? "Controlled by group"
|
||||||
|
: false}
|
||||||
value={parseRole(user)}
|
value={parseRole(user)}
|
||||||
allowRemove={user.role && !user.group}
|
allowRemove={user.role && !user.group}
|
||||||
allowPublic={false}
|
allowPublic={false}
|
||||||
|
@ -915,6 +942,7 @@
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-entity-access {
|
.auth-entity-access {
|
||||||
|
@ -931,7 +959,7 @@
|
||||||
|
|
||||||
.auth-entity,
|
.auth-entity,
|
||||||
.auth-entity-header {
|
.auth-entity-header {
|
||||||
padding: 0px var(--spacing-xl);
|
padding: 0 var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-entity,
|
.auth-entity,
|
||||||
|
@ -946,15 +974,17 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
color: var(--spectrum-global-color-gray-900);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-entity .user-email {
|
.auth-entity .user-email,
|
||||||
text-overflow: ellipsis;
|
.group-name {
|
||||||
white-space: nowrap;
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--spectrum-global-color-gray-900);
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
#builder-side-panel-container {
|
#builder-side-panel-container {
|
||||||
|
@ -1048,4 +1078,23 @@
|
||||||
.alert {
|
.alert {
|
||||||
padding: 0 var(--spacing-xl);
|
padding: 0 var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-groups {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
justify-content: end;
|
||||||
|
width: 60px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -98,7 +98,9 @@
|
||||||
$: privileged = sdk.users.isAdminOrGlobalBuilder(user)
|
$: privileged = sdk.users.isAdminOrGlobalBuilder(user)
|
||||||
$: nameLabel = getNameLabel(user)
|
$: nameLabel = getNameLabel(user)
|
||||||
$: filteredGroups = getFilteredGroups(internalGroups, searchTerm)
|
$: filteredGroups = getFilteredGroups(internalGroups, searchTerm)
|
||||||
$: availableApps = getAvailableApps($appsStore.apps, privileged, user?.roles)
|
$: availableApps = user
|
||||||
|
? getApps(user, sdk.users.userAppAccessList(user, $groups || []))
|
||||||
|
: []
|
||||||
$: userGroups = $groups.filter(x => {
|
$: userGroups = $groups.filter(x => {
|
||||||
return x.users?.find(y => {
|
return x.users?.find(y => {
|
||||||
return y._id === userId
|
return y._id === userId
|
||||||
|
@ -107,23 +109,19 @@
|
||||||
$: globalRole = users.getUserRole(user)
|
$: globalRole = users.getUserRole(user)
|
||||||
$: isTenantOwner = tenantOwner?.email && tenantOwner.email === user?.email
|
$: isTenantOwner = tenantOwner?.email && tenantOwner.email === user?.email
|
||||||
|
|
||||||
const getAvailableApps = (appList, privileged, roles) => {
|
const getApps = (user, appIds) => {
|
||||||
let availableApps = appList.slice()
|
let availableApps = $appsStore.apps
|
||||||
if (!privileged) {
|
.slice()
|
||||||
availableApps = availableApps.filter(x => {
|
.filter(app =>
|
||||||
let roleKeys = Object.keys(roles || {})
|
appIds.find(id => id === appsStore.getProdAppID(app.devId))
|
||||||
return roleKeys.concat(user?.builder?.apps).find(y => {
|
)
|
||||||
return x.appId === appsStore.extractAppId(y)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return availableApps.map(app => {
|
return availableApps.map(app => {
|
||||||
const prodAppId = appsStore.getProdAppID(app.devId)
|
const prodAppId = appsStore.getProdAppID(app.devId)
|
||||||
return {
|
return {
|
||||||
name: app.name,
|
name: app.name,
|
||||||
devId: app.devId,
|
devId: app.devId,
|
||||||
icon: app.icon,
|
icon: app.icon,
|
||||||
role: getRole(prodAppId, roles),
|
role: getRole(prodAppId, user),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -136,7 +134,7 @@
|
||||||
return groups.filter(group => group.name?.toLowerCase().includes(search))
|
return groups.filter(group => group.name?.toLowerCase().includes(search))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRole = (prodAppId, roles) => {
|
const getRole = (prodAppId, user) => {
|
||||||
if (privileged) {
|
if (privileged) {
|
||||||
return Constants.Roles.ADMIN
|
return Constants.Roles.ADMIN
|
||||||
}
|
}
|
||||||
|
@ -145,7 +143,21 @@
|
||||||
return Constants.Roles.CREATOR
|
return Constants.Roles.CREATOR
|
||||||
}
|
}
|
||||||
|
|
||||||
return roles[prodAppId]
|
if (user?.roles[prodAppId]) {
|
||||||
|
return user.roles[prodAppId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if access via group for creator
|
||||||
|
const foundGroup = $groups?.find(
|
||||||
|
group => group.roles[prodAppId] || group.builder?.apps[prodAppId]
|
||||||
|
)
|
||||||
|
if (foundGroup.builder?.apps[prodAppId]) {
|
||||||
|
return Constants.Roles.CREATOR
|
||||||
|
}
|
||||||
|
// can't tell how groups will control role
|
||||||
|
if (foundGroup.roles[prodAppId]) {
|
||||||
|
return Constants.Roles.GROUP
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNameLabel = user => {
|
const getNameLabel = user => {
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if value === Constants.Roles.CREATOR}
|
{#if value === Constants.Roles.GROUP}
|
||||||
|
Controlled by group
|
||||||
|
{:else if value === Constants.Roles.CREATOR}
|
||||||
Can edit
|
Can edit
|
||||||
{:else}
|
{:else}
|
||||||
<StatusLight
|
<StatusLight
|
||||||
|
|
|
@ -128,7 +128,7 @@
|
||||||
$auth.user?.email === user.email
|
$auth.user?.email === user.email
|
||||||
? false
|
? false
|
||||||
: true,
|
: true,
|
||||||
apps: [...new Set(Object.keys(user.roles))],
|
apps: sdk.users.userAppAccessList(user, $groups),
|
||||||
access: role.sortOrder,
|
access: role.sortOrder,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -106,6 +106,7 @@ export const Roles = {
|
||||||
PUBLIC: "PUBLIC",
|
PUBLIC: "PUBLIC",
|
||||||
BUILDER: "BUILDER",
|
BUILDER: "BUILDER",
|
||||||
CREATOR: "CREATOR",
|
CREATOR: "CREATOR",
|
||||||
|
GROUP: "GROUP",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventPublishType = {
|
export const EventPublishType = {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit e3843dd4eaced68ae063355b77df200dbc789c98
|
Subproject commit b28dbd549284cf450be7f25ad85aadf614d08f0b
|
|
@ -6,3 +6,4 @@ export * as cron from "./cron"
|
||||||
export * as schema from "./schema"
|
export * as schema from "./schema"
|
||||||
export * as views from "./views"
|
export * as views from "./views"
|
||||||
export * as roles from "./roles"
|
export * as roles from "./roles"
|
||||||
|
export * as lists from "./lists"
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export function punctuateList(list: string[]) {
|
||||||
|
if (list.length === 0) return ""
|
||||||
|
if (list.length === 1) return list[0]
|
||||||
|
if (list.length === 2) return list.join(" and ")
|
||||||
|
return list.slice(0, -1).join(", ") + " and " + list[list.length - 1]
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
User,
|
User,
|
||||||
InternalTable,
|
InternalTable,
|
||||||
|
UserGroup,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getProdAppID } from "./applications"
|
import { getProdAppID } from "./applications"
|
||||||
import * as _ from "lodash/fp"
|
import * as _ from "lodash/fp"
|
||||||
|
@ -129,3 +130,30 @@ export function containsUserID(value: string | undefined): boolean {
|
||||||
}
|
}
|
||||||
return value.includes(`${DocumentType.USER}${SEPARATOR}`)
|
return value.includes(`${DocumentType.USER}${SEPARATOR}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUserGroups(user: User, groups?: UserGroup[]) {
|
||||||
|
return (
|
||||||
|
groups?.filter(group => group.users?.find(u => u._id === user._id)) || []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserAppGroups(
|
||||||
|
appId: string,
|
||||||
|
user: User,
|
||||||
|
groups?: UserGroup[]
|
||||||
|
) {
|
||||||
|
const prodAppId = getProdAppID(appId)
|
||||||
|
const userGroups = getUserGroups(user, groups)
|
||||||
|
return userGroups.filter(group =>
|
||||||
|
Object.keys(group.roles || {}).find(app => app === prodAppId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userAppAccessList(user: User, groups?: UserGroup[]) {
|
||||||
|
const userGroups = getUserGroups(user, groups)
|
||||||
|
const userGroupApps = userGroups.flatMap(userGroup =>
|
||||||
|
Object.keys(userGroup.roles || {})
|
||||||
|
)
|
||||||
|
const fullList = [...Object.keys(user.roles), ...userGroupApps]
|
||||||
|
return [...new Set(fullList)]
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue