Merge branch 'master' of github.com:Budibase/budibase
This commit is contained in:
commit
2cf8dbabd2
|
@ -11,6 +11,7 @@
|
|||
export let active = false
|
||||
export let inactive = false
|
||||
export let hoverable = false
|
||||
export let outlineColor = null
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
@ -29,6 +30,7 @@
|
|||
class:spectrum-Label--seafoam={seafoam}
|
||||
class:spectrum-Label--active={active}
|
||||
class:spectrum-Label--inactive={inactive}
|
||||
style={outlineColor ? `border: 2px solid ${outlineColor}` : ""}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
|
|
|
@ -28,6 +28,9 @@
|
|||
<svg
|
||||
on:contextmenu
|
||||
on:click
|
||||
on:mouseover
|
||||
on:mouseleave
|
||||
on:focus
|
||||
class:hoverable
|
||||
class:disabled
|
||||
class="spectrum-Icon spectrum-Icon--size{size}"
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
overflow-x: hidden;
|
||||
}
|
||||
.main {
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.content {
|
||||
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 { fly } from "svelte/transition"
|
||||
import InfoDisplay from "../design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||
import BuilderGroupPopover from "./BuilderGroupPopover.svelte"
|
||||
|
||||
let query = null
|
||||
let loaded = false
|
||||
|
@ -197,12 +198,19 @@
|
|||
return
|
||||
}
|
||||
const update = await users.get(user._id)
|
||||
await users.save({
|
||||
...update,
|
||||
roles: {
|
||||
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({
|
||||
...update,
|
||||
roles: newRoles,
|
||||
})
|
||||
await searchUsers(query, $builderStore.builderSidePanel, loaded)
|
||||
}
|
||||
|
@ -539,6 +547,10 @@
|
|||
creationAccessType = Constants.Roles.CREATOR
|
||||
}
|
||||
}
|
||||
|
||||
const itemCountText = (word, count) => {
|
||||
return `${count} ${word}${count !== 1 ? "s" : ""}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
@ -701,13 +713,11 @@
|
|||
>
|
||||
<div class="details">
|
||||
<GroupIcon {group} size="S" />
|
||||
<div>
|
||||
<div class="group-name">
|
||||
{group.name}
|
||||
</div>
|
||||
<div class="auth-entity-meta">
|
||||
{`${group.users?.length} user${
|
||||
group.users?.length != 1 ? "s" : ""
|
||||
}`}
|
||||
{itemCountText("user", group.users?.length)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access">
|
||||
|
@ -741,16 +751,33 @@
|
|||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each allUsers as user}
|
||||
{@const userGroups = sdk.users.getUserAppGroups(
|
||||
$appStore.appId,
|
||||
user,
|
||||
$groups
|
||||
)}
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-groups">
|
||||
<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 class="auth-entity-access" class:muted={user.group}>
|
||||
<RoleSelect
|
||||
footer={getRoleFooter(user)}
|
||||
placeholder={false}
|
||||
placeholder={userGroups?.length
|
||||
? "Controlled by group"
|
||||
: false}
|
||||
value={parseRole(user)}
|
||||
allowRemove={user.role && !user.group}
|
||||
allowPublic={false}
|
||||
|
@ -915,6 +942,7 @@
|
|||
color: var(--spectrum-global-color-gray-600);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.auth-entity-access {
|
||||
|
@ -931,7 +959,7 @@
|
|||
|
||||
.auth-entity,
|
||||
.auth-entity-header {
|
||||
padding: 0px var(--spacing-xl);
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
|
||||
.auth-entity,
|
||||
|
@ -946,15 +974,17 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-entity .user-email {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.auth-entity .user-email,
|
||||
.group-name {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#builder-side-panel-container {
|
||||
|
@ -1048,4 +1078,23 @@
|
|||
.alert {
|
||||
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>
|
||||
|
|
|
@ -98,7 +98,9 @@
|
|||
$: privileged = sdk.users.isAdminOrGlobalBuilder(user)
|
||||
$: nameLabel = getNameLabel(user)
|
||||
$: filteredGroups = getFilteredGroups(internalGroups, searchTerm)
|
||||
$: availableApps = getAvailableApps($appsStore.apps, privileged, user?.roles)
|
||||
$: availableApps = user
|
||||
? getApps(user, sdk.users.userAppAccessList(user, $groups || []))
|
||||
: []
|
||||
$: userGroups = $groups.filter(x => {
|
||||
return x.users?.find(y => {
|
||||
return y._id === userId
|
||||
|
@ -107,23 +109,19 @@
|
|||
$: globalRole = users.getUserRole(user)
|
||||
$: isTenantOwner = tenantOwner?.email && tenantOwner.email === user?.email
|
||||
|
||||
const getAvailableApps = (appList, privileged, roles) => {
|
||||
let availableApps = appList.slice()
|
||||
if (!privileged) {
|
||||
availableApps = availableApps.filter(x => {
|
||||
let roleKeys = Object.keys(roles || {})
|
||||
return roleKeys.concat(user?.builder?.apps).find(y => {
|
||||
return x.appId === appsStore.extractAppId(y)
|
||||
})
|
||||
})
|
||||
}
|
||||
const getApps = (user, appIds) => {
|
||||
let availableApps = $appsStore.apps
|
||||
.slice()
|
||||
.filter(app =>
|
||||
appIds.find(id => id === appsStore.getProdAppID(app.devId))
|
||||
)
|
||||
return availableApps.map(app => {
|
||||
const prodAppId = appsStore.getProdAppID(app.devId)
|
||||
return {
|
||||
name: app.name,
|
||||
devId: app.devId,
|
||||
icon: app.icon,
|
||||
role: getRole(prodAppId, roles),
|
||||
role: getRole(prodAppId, user),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -136,7 +134,7 @@
|
|||
return groups.filter(group => group.name?.toLowerCase().includes(search))
|
||||
}
|
||||
|
||||
const getRole = (prodAppId, roles) => {
|
||||
const getRole = (prodAppId, user) => {
|
||||
if (privileged) {
|
||||
return Constants.Roles.ADMIN
|
||||
}
|
||||
|
@ -145,7 +143,21 @@
|
|||
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 => {
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#if value === Constants.Roles.CREATOR}
|
||||
{#if value === Constants.Roles.GROUP}
|
||||
Controlled by group
|
||||
{:else if value === Constants.Roles.CREATOR}
|
||||
Can edit
|
||||
{:else}
|
||||
<StatusLight
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
$auth.user?.email === user.email
|
||||
? false
|
||||
: true,
|
||||
apps: [...new Set(Object.keys(user.roles))],
|
||||
apps: sdk.users.userAppAccessList(user, $groups),
|
||||
access: role.sortOrder,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -106,6 +106,7 @@ export const Roles = {
|
|||
PUBLIC: "PUBLIC",
|
||||
BUILDER: "BUILDER",
|
||||
CREATOR: "CREATOR",
|
||||
GROUP: "GROUP",
|
||||
}
|
||||
|
||||
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 views from "./views"
|
||||
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,
|
||||
User,
|
||||
InternalTable,
|
||||
UserGroup,
|
||||
} from "@budibase/types"
|
||||
import { getProdAppID } from "./applications"
|
||||
import * as _ from "lodash/fp"
|
||||
|
@ -129,3 +130,30 @@ export function containsUserID(value: string | undefined): boolean {
|
|||
}
|
||||
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