Merge pull request #11596 from Budibase/feat/per-app-builder-fe
Per App Builder Frontend
This commit is contained in:
commit
28f9664297
|
@ -2,8 +2,9 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import FancyField from "./FancyField.svelte"
|
import FancyField from "./FancyField.svelte"
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import Popover from "../Popover/Popover.svelte"
|
|
||||||
import FancyFieldLabel from "./FancyFieldLabel.svelte"
|
import FancyFieldLabel from "./FancyFieldLabel.svelte"
|
||||||
|
import StatusLight from "../StatusLight/StatusLight.svelte"
|
||||||
|
import Picker from "../Form/Core/Picker.svelte"
|
||||||
|
|
||||||
export let label
|
export let label
|
||||||
export let value
|
export let value
|
||||||
|
@ -11,18 +12,30 @@
|
||||||
export let error = null
|
export let error = null
|
||||||
export let validate = null
|
export let validate = null
|
||||||
export let options = []
|
export let options = []
|
||||||
|
export let isOptionEnabled = () => true
|
||||||
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 getOptionSubtitle = option => extractProperty(option, "subtitle")
|
||||||
|
export let getOptionColour = () => null
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
let popover
|
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|
||||||
$: placeholder = !value
|
$: placeholder = !value
|
||||||
$: selectedLabel = getSelectedLabel(value)
|
$: selectedLabel = getSelectedLabel(value)
|
||||||
|
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||||
|
|
||||||
|
const getFieldAttribute = (getAttribute, value, options) => {
|
||||||
|
// Wait for options to load if there is a value but no options
|
||||||
|
if (!options?.length) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const index = options.findIndex(
|
||||||
|
(option, idx) => getOptionValue(option, idx) === value
|
||||||
|
)
|
||||||
|
return index !== -1 ? getAttribute(options[index], index) : null
|
||||||
|
}
|
||||||
const extractProperty = (value, property) => {
|
const extractProperty = (value, property) => {
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
return value[property]
|
return value[property]
|
||||||
|
@ -64,46 +77,45 @@
|
||||||
<FancyFieldLabel {placeholder}>{label}</FancyFieldLabel>
|
<FancyFieldLabel {placeholder}>{label}</FancyFieldLabel>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if fieldColour}
|
||||||
|
<span class="align">
|
||||||
|
<StatusLight square color={fieldColour} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="value" class:placeholder>
|
<div class="value" class:placeholder>
|
||||||
{selectedLabel || ""}
|
{selectedLabel || ""}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="arrow">
|
<div class="align arrow-alignment">
|
||||||
<Icon name="ChevronDown" />
|
<Icon name="ChevronDown" />
|
||||||
</div>
|
</div>
|
||||||
</FancyField>
|
</FancyField>
|
||||||
|
|
||||||
<Popover
|
<div id="picker-wrapper">
|
||||||
anchor={wrapper}
|
<Picker
|
||||||
align="left"
|
customAnchor={wrapper}
|
||||||
portalTarget={document.documentElement}
|
onlyPopover={true}
|
||||||
bind:this={popover}
|
bind:open
|
||||||
{open}
|
{error}
|
||||||
on:close={() => (open = false)}
|
{disabled}
|
||||||
useAnchorWidth={true}
|
{options}
|
||||||
maxWidth={null}
|
{getOptionLabel}
|
||||||
>
|
{getOptionValue}
|
||||||
<div class="popover-content">
|
{getOptionSubtitle}
|
||||||
{#if options.length}
|
{getOptionColour}
|
||||||
{#each options as option, idx}
|
{isOptionEnabled}
|
||||||
<div
|
isPlaceholder={value == null || value === ""}
|
||||||
class="popover-option"
|
placeholderOption={placeholder === false ? null : placeholder}
|
||||||
tabindex="0"
|
onSelectOption={onChange}
|
||||||
on:click={() => onChange(getOptionValue(option, idx))}
|
isOptionSelected={option => option === value}
|
||||||
>
|
/>
|
||||||
<span class="option-text">
|
</div>
|
||||||
{getOptionLabel(option, idx)}
|
|
||||||
</span>
|
|
||||||
{#if value === getOptionValue(option, idx)}
|
|
||||||
<Icon name="Checkmark" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
#picker-wrapper :global(.spectrum-Picker) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.value {
|
.value {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
@ -118,30 +130,23 @@
|
||||||
width: 0;
|
width: 0;
|
||||||
transform: translateY(9px);
|
transform: translateY(9px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.align {
|
||||||
|
display: block;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 17px;
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
transition: transform 130ms ease-out, opacity 130ms ease-out;
|
||||||
|
transform: translateY(9px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-alignment {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
.value.placeholder {
|
.value.placeholder {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.popover-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 7px 0;
|
|
||||||
}
|
|
||||||
.popover-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 7px 16px;
|
|
||||||
transition: background 130ms ease-out;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
.popover-option:hover {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
import Icon from "../../Icon/Icon.svelte"
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||||
import Popover from "../../Popover/Popover.svelte"
|
import Popover from "../../Popover/Popover.svelte"
|
||||||
|
import Tags from "../../Tags/Tags.svelte"
|
||||||
|
import Tag from "../../Tags/Tag.svelte"
|
||||||
|
|
||||||
export let id = null
|
export let id = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -26,6 +28,7 @@
|
||||||
export let getOptionIcon = () => null
|
export let getOptionIcon = () => null
|
||||||
export let useOptionIconImage = false
|
export let useOptionIconImage = false
|
||||||
export let getOptionColour = () => null
|
export let getOptionColour = () => null
|
||||||
|
export let getOptionSubtitle = () => null
|
||||||
export let open = false
|
export let open = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
|
@ -37,7 +40,7 @@
|
||||||
export let customPopoverHeight
|
export let customPopoverHeight
|
||||||
export let align = "left"
|
export let align = "left"
|
||||||
export let footer = null
|
export let footer = null
|
||||||
|
export let customAnchor = null
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let searchTerm = null
|
let searchTerm = null
|
||||||
|
@ -99,7 +102,7 @@
|
||||||
bind:this={button}
|
bind:this={button}
|
||||||
>
|
>
|
||||||
{#if fieldIcon}
|
{#if fieldIcon}
|
||||||
{#if !useOptionIconImage}
|
{#if !useOptionIconImage}x
|
||||||
<span class="option-extra icon">
|
<span class="option-extra icon">
|
||||||
<Icon size="S" name={fieldIcon} />
|
<Icon size="S" name={fieldIcon} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -139,9 +142,8 @@
|
||||||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={button}
|
anchor={customAnchor ? customAnchor : button}
|
||||||
align={align || "left"}
|
align={align || "left"}
|
||||||
bind:this={popover}
|
bind:this={popover}
|
||||||
{open}
|
{open}
|
||||||
|
@ -215,8 +217,21 @@
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="spectrum-Menu-itemLabel">
|
<span class="spectrum-Menu-itemLabel">
|
||||||
|
{#if getOptionSubtitle(option, idx)}
|
||||||
|
<span class="subtitle-text"
|
||||||
|
>{getOptionSubtitle(option, idx)}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{getOptionLabel(option, idx)}
|
{getOptionLabel(option, idx)}
|
||||||
</span>
|
</span>
|
||||||
|
{#if option.tag}
|
||||||
|
<span class="option-tag">
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">{option.tag}</Tag>
|
||||||
|
</Tags>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
|
@ -242,6 +257,17 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtitle-text {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
top: 10px;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
.spectrum-Picker-label.auto-width {
|
.spectrum-Picker-label.auto-width {
|
||||||
margin-right: var(--spacing-xs);
|
margin-right: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
@ -321,4 +347,12 @@
|
||||||
.option-extra.icon.field-icon {
|
.option-extra.icon.field-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.option-tag {
|
||||||
|
margin: 0 var(--spacing-m) 0 var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-tag :global(.spectrum-Tags-item > .spectrum-Icon) {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
export let sort = false
|
export let sort = false
|
||||||
export let align
|
export let align
|
||||||
export let footer = null
|
export let footer = null
|
||||||
|
export let tag = null
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
|
@ -83,6 +83,7 @@
|
||||||
{isOptionEnabled}
|
{isOptionEnabled}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{sort}
|
{sort}
|
||||||
|
{tag}
|
||||||
isPlaceholder={value == null || value === ""}
|
isPlaceholder={value == null || value === ""}
|
||||||
placeholderOption={placeholder === false ? null : placeholder}
|
placeholderOption={placeholder === false ? null : placeholder}
|
||||||
isOptionSelected={option => option === value}
|
isOptionSelected={option => option === value}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
export let customPopoverHeight
|
export let customPopoverHeight
|
||||||
export let align
|
export let align
|
||||||
export let footer = null
|
export let footer = null
|
||||||
|
export let tag = null
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
value = e.detail
|
value = e.detail
|
||||||
|
@ -61,6 +61,7 @@
|
||||||
{isOptionEnabled}
|
{isOptionEnabled}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{customPopoverHeight}
|
{customPopoverHeight}
|
||||||
|
{tag}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select, FancySelect } from "@budibase/bbui"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
|
||||||
import { Constants, RoleUtils } from "@budibase/frontend-core"
|
import { Constants, RoleUtils } from "@budibase/frontend-core"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let error
|
export let error
|
||||||
|
@ -15,17 +18,43 @@
|
||||||
export let align
|
export let align
|
||||||
export let footer = null
|
export let footer = null
|
||||||
export let allowedRoles = null
|
export let allowedRoles = null
|
||||||
|
export let allowCreator = false
|
||||||
|
export let fancySelect = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const RemoveID = "remove"
|
const RemoveID = "remove"
|
||||||
|
|
||||||
$: options = getOptions($roles, allowPublic, allowRemove, allowedRoles)
|
$: options = getOptions(
|
||||||
|
$roles,
|
||||||
const getOptions = (roles, allowPublic, allowRemove, allowedRoles) => {
|
allowPublic,
|
||||||
|
allowRemove,
|
||||||
|
allowedRoles,
|
||||||
|
allowCreator
|
||||||
|
)
|
||||||
|
const getOptions = (
|
||||||
|
roles,
|
||||||
|
allowPublic,
|
||||||
|
allowRemove,
|
||||||
|
allowedRoles,
|
||||||
|
allowCreator
|
||||||
|
) => {
|
||||||
if (allowedRoles?.length) {
|
if (allowedRoles?.length) {
|
||||||
return roles.filter(role => allowedRoles.includes(role._id))
|
return roles.filter(role => allowedRoles.includes(role._id))
|
||||||
}
|
}
|
||||||
let newRoles = [...roles]
|
let newRoles = [...roles]
|
||||||
|
|
||||||
|
if (allowCreator) {
|
||||||
|
newRoles = [
|
||||||
|
{
|
||||||
|
_id: Constants.Roles.CREATOR,
|
||||||
|
name: "Creator",
|
||||||
|
tag:
|
||||||
|
!$licensing.perAppBuildersEnabled &&
|
||||||
|
capitalise(Constants.PlanType.BUSINESS),
|
||||||
|
},
|
||||||
|
...newRoles,
|
||||||
|
]
|
||||||
|
}
|
||||||
if (allowRemove) {
|
if (allowRemove) {
|
||||||
newRoles = [
|
newRoles = [
|
||||||
...newRoles,
|
...newRoles,
|
||||||
|
@ -64,7 +93,29 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
{#if fancySelect}
|
||||||
|
<FancySelect
|
||||||
|
{autoWidth}
|
||||||
|
{quiet}
|
||||||
|
{disabled}
|
||||||
|
{align}
|
||||||
|
{footer}
|
||||||
|
bind:value
|
||||||
|
on:change={onChange}
|
||||||
|
{options}
|
||||||
|
label="Access on this app"
|
||||||
|
getOptionLabel={role => role.name}
|
||||||
|
getOptionValue={role => role._id}
|
||||||
|
getOptionColour={getColor}
|
||||||
|
getOptionIcon={getIcon}
|
||||||
|
isOptionEnabled={option =>
|
||||||
|
option._id !== Constants.Roles.CREATOR ||
|
||||||
|
$licensing.perAppBuildersEnabled}
|
||||||
|
{placeholder}
|
||||||
|
{error}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Select
|
||||||
{autoWidth}
|
{autoWidth}
|
||||||
{quiet}
|
{quiet}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -77,6 +128,10 @@
|
||||||
getOptionValue={role => role._id}
|
getOptionValue={role => role._id}
|
||||||
getOptionColour={getColor}
|
getOptionColour={getColor}
|
||||||
getOptionIcon={getIcon}
|
getOptionIcon={getIcon}
|
||||||
|
isOptionEnabled={option =>
|
||||||
|
option._id !== Constants.Roles.CREATOR ||
|
||||||
|
$licensing.perAppBuildersEnabled}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{error}
|
{error}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -26,6 +26,9 @@ export const capitalise = s => {
|
||||||
|
|
||||||
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
||||||
|
|
||||||
|
export const lowercaseExceptFirst = s =>
|
||||||
|
s.charAt(0) + s.substring(1).toLowerCase()
|
||||||
|
|
||||||
export const get_name = s => (!s ? "" : last(s.split("/")))
|
export const get_name = s => (!s ? "" : last(s.split("/")))
|
||||||
|
|
||||||
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
|
Divider,
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
Input,
|
Input,
|
||||||
clickOutside,
|
clickOutside,
|
||||||
notifications,
|
notifications,
|
||||||
ActionButton,
|
|
||||||
CopyInput,
|
CopyInput,
|
||||||
Modal,
|
Modal,
|
||||||
|
FancyForm,
|
||||||
|
FancyInput,
|
||||||
|
Button,
|
||||||
|
FancySelect,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
|
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
|
||||||
import { fetchData, Constants, Utils } from "@budibase/frontend-core"
|
import {
|
||||||
|
fetchData,
|
||||||
|
Constants,
|
||||||
|
Utils,
|
||||||
|
RoleUtils,
|
||||||
|
} from "@budibase/frontend-core"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import GroupIcon from "../../../portal/users/groups/_components/GroupIcon.svelte"
|
import GroupIcon from "../../../portal/users/groups/_components/GroupIcon.svelte"
|
||||||
|
@ -26,10 +35,15 @@
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let inviting = false
|
let inviting = false
|
||||||
let searchFocus = false
|
let searchFocus = false
|
||||||
|
let invitingFlow = false
|
||||||
// Initially filter entities without app access
|
// Initially filter entities without app access
|
||||||
// Show all when false
|
// Show all when false
|
||||||
let filterByAppAccess = true
|
let filterByAppAccess = false
|
||||||
|
let email
|
||||||
|
let error
|
||||||
|
let form
|
||||||
|
let creationRoleType = Constants.BudibaseRoles.AppUser
|
||||||
|
let creationAccessType = Constants.Roles.BASIC
|
||||||
|
|
||||||
let appInvites = []
|
let appInvites = []
|
||||||
let filteredInvites = []
|
let filteredInvites = []
|
||||||
|
@ -40,8 +54,7 @@
|
||||||
let userLimitReachedModal
|
let userLimitReachedModal
|
||||||
|
|
||||||
let inviteFailureResponse = ""
|
let inviteFailureResponse = ""
|
||||||
|
$: validEmail = emailValidator(email) === true
|
||||||
$: queryIsEmail = emailValidator(query) === true
|
|
||||||
$: prodAppId = apps.getProdAppID($store.appId)
|
$: prodAppId = apps.getProdAppID($store.appId)
|
||||||
$: promptInvite = showInvite(
|
$: promptInvite = showInvite(
|
||||||
filteredInvites,
|
filteredInvites,
|
||||||
|
@ -50,7 +63,6 @@
|
||||||
query
|
query
|
||||||
)
|
)
|
||||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
const showInvite = (invites, users, groups, query) => {
|
const showInvite = (invites, users, groups, query) => {
|
||||||
return !invites?.length && !users?.length && !groups?.length && query
|
return !invites?.length && !users?.length && !groups?.length && query
|
||||||
}
|
}
|
||||||
|
@ -66,9 +78,9 @@
|
||||||
if (!filterByAppAccess && !query) {
|
if (!filterByAppAccess && !query) {
|
||||||
filteredInvites =
|
filteredInvites =
|
||||||
appInvites.length > 100 ? appInvites.slice(0, 100) : [...appInvites]
|
appInvites.length > 100 ? appInvites.slice(0, 100) : [...appInvites]
|
||||||
|
filteredInvites.sort(sortInviteRoles)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredInvites = appInvites.filter(invite => {
|
filteredInvites = appInvites.filter(invite => {
|
||||||
const inviteInfo = invite.info?.apps
|
const inviteInfo = invite.info?.apps
|
||||||
if (!query && inviteInfo && prodAppId) {
|
if (!query && inviteInfo && prodAppId) {
|
||||||
|
@ -76,8 +88,8 @@
|
||||||
}
|
}
|
||||||
return invite.email.includes(query)
|
return invite.email.includes(query)
|
||||||
})
|
})
|
||||||
|
filteredInvites.sort(sortInviteRoles)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: filterByAppAccess, prodAppId, filterInvites(query)
|
$: filterByAppAccess, prodAppId, filterInvites(query)
|
||||||
$: if (searchFocus === true) {
|
$: if (searchFocus === true) {
|
||||||
filterByAppAccess = false
|
filterByAppAccess = false
|
||||||
|
@ -107,24 +119,66 @@
|
||||||
})
|
})
|
||||||
await usersFetch.refresh()
|
await usersFetch.refresh()
|
||||||
|
|
||||||
filteredUsers = $usersFetch.rows.map(user => {
|
filteredUsers = $usersFetch.rows
|
||||||
const isAdminOrBuilder = sdk.users.isAdminOrBuilder(user, prodAppId)
|
.filter(user => !user?.admin?.global) // filter out global admins
|
||||||
let role = undefined
|
.map(user => {
|
||||||
if (isAdminOrBuilder) {
|
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(
|
||||||
|
user,
|
||||||
|
prodAppId
|
||||||
|
)
|
||||||
|
const isAppBuilder = sdk.users.hasAppBuilderPermissions(user, prodAppId)
|
||||||
|
let role
|
||||||
|
if (isAdminOrGlobalBuilder) {
|
||||||
role = Constants.Roles.ADMIN
|
role = Constants.Roles.ADMIN
|
||||||
|
} else if (isAppBuilder) {
|
||||||
|
role = Constants.Roles.CREATOR
|
||||||
} else {
|
} else {
|
||||||
const appRole = Object.keys(user.roles).find(x => x === prodAppId)
|
const appRole = user.roles[prodAppId]
|
||||||
if (appRole) {
|
if (appRole) {
|
||||||
role = user.roles[appRole]
|
role = appRole
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
role,
|
role,
|
||||||
isAdminOrBuilder,
|
isAdminOrGlobalBuilder,
|
||||||
|
isAppBuilder,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.sort(sortRoles)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortInviteRoles = (a, b) => {
|
||||||
|
const aEmpty =
|
||||||
|
!a.info?.appBuilders?.length && Object.keys(a.info.apps).length === 0
|
||||||
|
const bEmpty =
|
||||||
|
!b.info?.appBuilders?.length && Object.keys(b.info.apps).length === 0
|
||||||
|
|
||||||
|
if (aEmpty && !bEmpty) return 1
|
||||||
|
if (!aEmpty && bEmpty) return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortRoles = (a, b) => {
|
||||||
|
const roleA = a.role
|
||||||
|
const roleB = b.role
|
||||||
|
|
||||||
|
const priorityA = RoleUtils.getRolePriority(roleA)
|
||||||
|
const priorityB = RoleUtils.getRolePriority(roleB)
|
||||||
|
|
||||||
|
if (roleA === undefined && roleB !== undefined) {
|
||||||
|
return 1
|
||||||
|
} else if (roleA !== undefined && roleB === undefined) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priorityA < priorityB) {
|
||||||
|
return 1
|
||||||
|
} else if (priorityA > priorityB) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
||||||
|
@ -160,6 +214,12 @@
|
||||||
if (user.role === role) {
|
if (user.role === role) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (user.isAppBuilder) {
|
||||||
|
await removeAppBuilder(user._id, prodAppId)
|
||||||
|
}
|
||||||
|
if (role === Constants.Roles.CREATOR) {
|
||||||
|
await removeAppBuilder(user._id, prodAppId)
|
||||||
|
}
|
||||||
await updateAppUser(user, role)
|
await updateAppUser(user, role)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
@ -189,6 +249,9 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (group?.builder?.apps.includes(prodAppId)) {
|
||||||
|
await removeGroupAppBuilder(group._id)
|
||||||
|
}
|
||||||
await updateAppGroup(group, role)
|
await updateAppGroup(group, role)
|
||||||
} catch {
|
} catch {
|
||||||
notifications.error("Group update failed")
|
notifications.error("Group update failed")
|
||||||
|
@ -225,12 +288,15 @@
|
||||||
return nameMatch
|
return nameMatch
|
||||||
})
|
})
|
||||||
.map(enrichGroupRole)
|
.map(enrichGroupRole)
|
||||||
|
.sort(sortRoles)
|
||||||
}
|
}
|
||||||
|
|
||||||
const enrichGroupRole = group => {
|
const enrichGroupRole = group => {
|
||||||
return {
|
return {
|
||||||
...group,
|
...group,
|
||||||
role: group.roles?.[
|
role: group?.builder?.apps.includes(prodAppId)
|
||||||
|
? Constants.Roles.CREATOR
|
||||||
|
: group.roles?.[
|
||||||
groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
|
groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -245,7 +311,6 @@
|
||||||
$: filteredGroups = searchGroups(enrichedGroups, query)
|
$: filteredGroups = searchGroups(enrichedGroups, query)
|
||||||
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
||||||
$: allUsers = [...filteredUsers, ...groupUsers]
|
$: allUsers = [...filteredUsers, ...groupUsers]
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Create pseudo users from the "users" attribute on app groups.
|
Create pseudo users from the "users" attribute on app groups.
|
||||||
These users will appear muted in the UI and show the ROLE
|
These users will appear muted in the UI and show the ROLE
|
||||||
|
@ -291,21 +356,28 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function inviteUser() {
|
async function inviteUser() {
|
||||||
if (!queryIsEmail) {
|
if (!validEmail) {
|
||||||
notifications.error("Email is not valid")
|
notifications.error("Email is not valid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newUserEmail = query + ""
|
const newUserEmail = email + ""
|
||||||
inviting = true
|
inviting = true
|
||||||
|
|
||||||
const payload = [
|
const payload = [
|
||||||
{
|
{
|
||||||
email: newUserEmail,
|
email: newUserEmail,
|
||||||
builder: false,
|
builder: !!creationRoleType === Constants.BudibaseRoles.Admin,
|
||||||
admin: false,
|
admin: !!creationRoleType === Constants.BudibaseRoles.Admin,
|
||||||
apps: { [prodAppId]: Constants.Roles.BASIC },
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (creationAccessType === Constants.Roles.CREATOR) {
|
||||||
|
payload[0].appBuilders = [prodAppId]
|
||||||
|
} else {
|
||||||
|
payload[0].apps = {
|
||||||
|
[prodAppId]: creationAccessType,
|
||||||
|
}
|
||||||
|
}
|
||||||
let userInviteResponse
|
let userInviteResponse
|
||||||
try {
|
try {
|
||||||
userInviteResponse = await users.onboard(payload)
|
userInviteResponse = await users.onboard(payload)
|
||||||
|
@ -317,16 +389,23 @@
|
||||||
return userInviteResponse
|
return userInviteResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openInviteFlow = () => {
|
||||||
|
$licensing.userLimitReached
|
||||||
|
? userLimitReachedModal.show()
|
||||||
|
: (invitingFlow = true)
|
||||||
|
}
|
||||||
|
|
||||||
const onInviteUser = async () => {
|
const onInviteUser = async () => {
|
||||||
|
form.validate()
|
||||||
userOnboardResponse = await inviteUser()
|
userOnboardResponse = await inviteUser()
|
||||||
const originalQuery = query + ""
|
const originalQuery = email + ""
|
||||||
query = null
|
email = null
|
||||||
|
|
||||||
const newUser = userOnboardResponse?.successful.find(
|
const newUser = userOnboardResponse?.successful.find(
|
||||||
user => user.email === originalQuery
|
user => user.email === originalQuery
|
||||||
)
|
)
|
||||||
if (newUser) {
|
if (newUser) {
|
||||||
query = originalQuery
|
email = originalQuery
|
||||||
notifications.success(
|
notifications.success(
|
||||||
userOnboardResponse.created
|
userOnboardResponse.created
|
||||||
? "User created successfully"
|
? "User created successfully"
|
||||||
|
@ -344,16 +423,27 @@
|
||||||
notifications.error(inviteFailureResponse)
|
notifications.error(inviteFailureResponse)
|
||||||
}
|
}
|
||||||
userOnboardResponse = null
|
userOnboardResponse = null
|
||||||
|
invitingFlow = false
|
||||||
|
// trigger reload of the users
|
||||||
|
query = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateUserInvite = async (invite, role) => {
|
const onUpdateUserInvite = async (invite, role) => {
|
||||||
await users.updateInvite({
|
let updateBody = {
|
||||||
code: invite.code,
|
code: invite.code,
|
||||||
apps: {
|
apps: {
|
||||||
...invite.apps,
|
...invite.apps,
|
||||||
[prodAppId]: role,
|
[prodAppId]: role,
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if (role === Constants.Roles.CREATOR) {
|
||||||
|
updateBody.appBuilders = [...(updateBody.appBuilders ?? []), prodAppId]
|
||||||
|
delete updateBody?.apps?.[prodAppId]
|
||||||
|
} else if (role !== Constants.Roles.CREATOR && invite?.appBuilders) {
|
||||||
|
invite.appBuilders = []
|
||||||
|
}
|
||||||
|
await users.updateInvite(updateBody)
|
||||||
await filterInvites(query)
|
await filterInvites(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,6 +463,22 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addAppBuilder = async userId => {
|
||||||
|
await users.addAppBuilder(userId, prodAppId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAppBuilder = async userId => {
|
||||||
|
await users.removeAppBuilder(userId, prodAppId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addGroupAppBuilder = async groupId => {
|
||||||
|
await groups.actions.addGroupAppBuilder(groupId, prodAppId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeGroupAppBuilder = async groupId => {
|
||||||
|
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
|
||||||
|
}
|
||||||
|
|
||||||
const initSidePanel = async sidePaneOpen => {
|
const initSidePanel = async sidePaneOpen => {
|
||||||
if (sidePaneOpen === true) {
|
if (sidePaneOpen === true) {
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
|
@ -383,27 +489,17 @@
|
||||||
$: initSidePanel($store.builderSidePanel)
|
$: initSidePanel($store.builderSidePanel)
|
||||||
|
|
||||||
function handleKeyDown(evt) {
|
function handleKeyDown(evt) {
|
||||||
if (evt.key === "Enter" && queryIsEmail && !inviting) {
|
if (evt.key === "Enter" && validEmail && !inviting) {
|
||||||
onInviteUser()
|
onInviteUser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userTitle = user => {
|
|
||||||
if (sdk.users.isAdmin(user)) {
|
|
||||||
return "Admin"
|
|
||||||
} else if (sdk.users.isBuilder(user, prodAppId)) {
|
|
||||||
return "Developer"
|
|
||||||
} else {
|
|
||||||
return "App user"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRoleFooter = user => {
|
const getRoleFooter = user => {
|
||||||
if (user.group) {
|
if (user.group) {
|
||||||
const role = $roles.find(role => role._id === user.role)
|
const role = $roles.find(role => role._id === user.role)
|
||||||
return `This user has been given ${role?.name} access from the ${user.group} group`
|
return `This user has been given ${role?.name} access from the ${user.group} group`
|
||||||
}
|
}
|
||||||
if (user.isAdminOrBuilder) {
|
if (user.isAdminOrGlobalBuilder) {
|
||||||
return "This user's role grants admin access to all apps"
|
return "This user's role grants admin access to all apps"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@ -423,7 +519,19 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="builder-side-panel-header">
|
<div class="builder-side-panel-header">
|
||||||
<Heading size="S">Users</Heading>
|
<div
|
||||||
|
on:click={() => {
|
||||||
|
invitingFlow = false
|
||||||
|
}}
|
||||||
|
class="header"
|
||||||
|
>
|
||||||
|
{#if invitingFlow}
|
||||||
|
<Icon name="BackAndroid" />
|
||||||
|
{/if}
|
||||||
|
<Heading size="S">{invitingFlow ? "Invite new user" : "Users"}</Heading>
|
||||||
|
</div>
|
||||||
|
<div class="header">
|
||||||
|
<Button on:click={openInviteFlow} size="S" cta>Invite user</Button>
|
||||||
<Icon
|
<Icon
|
||||||
color="var(--spectrum-global-color-gray-600)"
|
color="var(--spectrum-global-color-gray-600)"
|
||||||
name="RailRightClose"
|
name="RailRightClose"
|
||||||
|
@ -436,6 +544,8 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if !invitingFlow}
|
||||||
<div class="search" class:focused={searchFocus}>
|
<div class="search" class:focused={searchFocus}>
|
||||||
<span class="search-input">
|
<span class="search-input">
|
||||||
<Input
|
<Input
|
||||||
|
@ -455,15 +565,11 @@
|
||||||
class="search-input-icon"
|
class="search-input-icon"
|
||||||
class:searching={query || !filterByAppAccess}
|
class:searching={query || !filterByAppAccess}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (!filterByAppAccess) {
|
|
||||||
filterByAppAccess = true
|
|
||||||
}
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
query = null
|
query = null
|
||||||
userOnboardResponse = null
|
userOnboardResponse = null
|
||||||
filterByAppAccess = true
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||||
|
@ -476,20 +582,11 @@
|
||||||
<div class="invite-header">
|
<div class="invite-header">
|
||||||
<Heading size="XS">No user found</Heading>
|
<Heading size="XS">No user found</Heading>
|
||||||
<div class="invite-directions">
|
<div class="invite-directions">
|
||||||
Add a valid email to invite a new user
|
Try searching a different email or <span
|
||||||
</div>
|
class="underlined"
|
||||||
</div>
|
on:click={openInviteFlow}>invite a new user</span
|
||||||
<div class="invite-form">
|
|
||||||
<span>{query || ""}</span>
|
|
||||||
<ActionButton
|
|
||||||
icon="UserAdd"
|
|
||||||
disabled={!queryIsEmail || inviting}
|
|
||||||
on:click={$licensing.userLimitReached
|
|
||||||
? userLimitReachedModal.show
|
|
||||||
: onInviteUser}
|
|
||||||
>
|
>
|
||||||
Add user
|
</div>
|
||||||
</ActionButton>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -512,9 +609,12 @@
|
||||||
<div class="auth-entity-access">
|
<div class="auth-entity-access">
|
||||||
<RoleSelect
|
<RoleSelect
|
||||||
placeholder={false}
|
placeholder={false}
|
||||||
value={invite.info.apps?.[prodAppId]}
|
value={invite.info?.appBuilders?.includes(prodAppId)
|
||||||
|
? Constants.Roles.CREATOR
|
||||||
|
: invite.info.apps?.[prodAppId]}
|
||||||
allowRemove={invite.info.apps?.[prodAppId]}
|
allowRemove={invite.info.apps?.[prodAppId]}
|
||||||
allowPublic={false}
|
allowPublic={false}
|
||||||
|
allowCreator={true}
|
||||||
quiet={true}
|
quiet={true}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
onUpdateUserInvite(invite, e.detail)
|
onUpdateUserInvite(invite, e.detail)
|
||||||
|
@ -567,8 +667,13 @@
|
||||||
allowRemove={group.role}
|
allowRemove={group.role}
|
||||||
allowPublic={false}
|
allowPublic={false}
|
||||||
quiet={true}
|
quiet={true}
|
||||||
|
allowCreator={true}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
|
if (e.detail === Constants.Roles.CREATOR) {
|
||||||
|
addGroupAppBuilder(group._id)
|
||||||
|
} else {
|
||||||
onUpdateGroup(group, e.detail)
|
onUpdateGroup(group, e.detail)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
on:remove={() => {
|
on:remove={() => {
|
||||||
onUpdateGroup(group)
|
onUpdateGroup(group)
|
||||||
|
@ -594,9 +699,6 @@
|
||||||
<div class="user-email" title={user.email}>
|
<div class="user-email" title={user.email}>
|
||||||
{user.email}
|
{user.email}
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-entity-meta">
|
|
||||||
{userTitle(user)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-entity-access" class:muted={user.group}>
|
<div class="auth-entity-access" class:muted={user.group}>
|
||||||
<RoleSelect
|
<RoleSelect
|
||||||
|
@ -605,16 +707,22 @@
|
||||||
value={user.role}
|
value={user.role}
|
||||||
allowRemove={user.role && !user.group}
|
allowRemove={user.role && !user.group}
|
||||||
allowPublic={false}
|
allowPublic={false}
|
||||||
|
allowCreator={true}
|
||||||
quiet={true}
|
quiet={true}
|
||||||
|
on:addcreator={() => {}}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
|
if (e.detail === Constants.Roles.CREATOR) {
|
||||||
|
addAppBuilder(user._id)
|
||||||
|
} else {
|
||||||
onUpdateUser(user, e.detail)
|
onUpdateUser(user, e.detail)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
on:remove={() => {
|
on:remove={() => {
|
||||||
onUpdateUser(user)
|
onUpdateUser(user)
|
||||||
}}
|
}}
|
||||||
autoWidth
|
autoWidth
|
||||||
align="right"
|
align="right"
|
||||||
allowedRoles={user.isAdminOrBuilder
|
allowedRoles={user.isAdminOrGlobalBuilder
|
||||||
? [Constants.Roles.ADMIN]
|
? [Constants.Roles.ADMIN]
|
||||||
: null}
|
: null}
|
||||||
/>
|
/>
|
||||||
|
@ -631,8 +739,8 @@
|
||||||
<div class="invite-header">
|
<div class="invite-header">
|
||||||
<Heading size="XS">User added!</Heading>
|
<Heading size="XS">User added!</Heading>
|
||||||
<div class="invite-directions">
|
<div class="invite-directions">
|
||||||
Email invites are not available without SMTP configuration. Here is
|
Email invites are not available without SMTP configuration. Here
|
||||||
the password that has been generated for this user.
|
is the password that has been generated for this user.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -644,6 +752,67 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Divider />
|
||||||
|
<div class="body">
|
||||||
|
<Layout gap="L" noPadding>
|
||||||
|
<div class="user-invite-form">
|
||||||
|
<FancyForm bind:this={form}>
|
||||||
|
<FancyInput
|
||||||
|
disabled={false}
|
||||||
|
label="Email"
|
||||||
|
value={email}
|
||||||
|
on:change={e => {
|
||||||
|
email = e.detail
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
if (!email) {
|
||||||
|
return "Please enter an email"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}}
|
||||||
|
{error}
|
||||||
|
/>
|
||||||
|
<FancySelect
|
||||||
|
bind:value={creationRoleType}
|
||||||
|
options={sdk.users.isAdmin($auth.user)
|
||||||
|
? Constants.BudibaseRoleOptionsNew
|
||||||
|
: Constants.BudibaseRoleOptionsNew.filter(
|
||||||
|
option => option.value !== Constants.BudibaseRoles.Admin
|
||||||
|
)}
|
||||||
|
label="Role"
|
||||||
|
/>
|
||||||
|
{#if creationRoleType !== Constants.BudibaseRoles.Admin}
|
||||||
|
<RoleSelect
|
||||||
|
placeholder={false}
|
||||||
|
bind:value={creationAccessType}
|
||||||
|
allowPublic={false}
|
||||||
|
allowCreator={true}
|
||||||
|
quiet={true}
|
||||||
|
autoWidth
|
||||||
|
align="right"
|
||||||
|
fancySelect
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</FancyForm>
|
||||||
|
{#if creationRoleType === Constants.BudibaseRoles.Admin}
|
||||||
|
<div class="admin-info">
|
||||||
|
<Icon name="Info" />
|
||||||
|
Admins will get full access to all apps and settings
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<span class="add-user">
|
||||||
|
<Button
|
||||||
|
newStyles
|
||||||
|
cta
|
||||||
|
disabled={!email?.length}
|
||||||
|
on:click={onInviteUser}>Add user</Button
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<Modal bind:this={userLimitReachedModal}>
|
<Modal bind:this={userLimitReachedModal}>
|
||||||
<UpgradeModal {isOwner} />
|
<UpgradeModal {isOwner} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -659,6 +828,27 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-user {
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-info {
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
padding: var(--spacing-l) var(--spacing-l) var(--spacing-l) var(--spacing-l);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
height: 30px;
|
||||||
|
background-color: var(--background-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.underlined {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -746,12 +936,6 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invite-form {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
#builder-side-panel-container .search {
|
#builder-side-panel-container .search {
|
||||||
padding-top: var(--spacing-m);
|
padding-top: var(--spacing-m);
|
||||||
padding-bottom: var(--spacing-m);
|
padding-bottom: var(--spacing-m);
|
||||||
|
@ -798,6 +982,16 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-invite-form {
|
||||||
|
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
return publishedApps
|
return publishedApps
|
||||||
}
|
}
|
||||||
return publishedApps.filter(app => {
|
return publishedApps.filter(app => {
|
||||||
if (sdk.users.isBuilder(user, app.appId)) {
|
if (sdk.users.isBuilder(user, app.prodId)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (!Object.keys(user?.roles).length && user?.userGroups) {
|
if (!Object.keys(user?.roles).length && user?.userGroups) {
|
||||||
|
@ -142,7 +142,12 @@
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
{#each userApps as app (app.appId)}
|
{#each userApps as app (app.appId)}
|
||||||
<a class="app" target="_blank" href={getUrl(app)}>
|
<a
|
||||||
|
class="app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
href={getUrl(app)}
|
||||||
|
>
|
||||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||||
<div class="app-info">
|
<div class="app-info">
|
||||||
<Heading size="XS">{app.name}</Heading>
|
<Heading size="XS">{app.name}</Heading>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
import GroupIcon from "./_components/GroupIcon.svelte"
|
import GroupIcon from "./_components/GroupIcon.svelte"
|
||||||
import GroupUsers from "./_components/GroupUsers.svelte"
|
import GroupUsers from "./_components/GroupUsers.svelte"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let groupId
|
export let groupId
|
||||||
|
|
||||||
|
@ -45,7 +46,7 @@
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let editModal, deleteModal
|
let editModal, deleteModal
|
||||||
|
$: console.log(group)
|
||||||
$: scimEnabled = $features.isScimEnabled
|
$: scimEnabled = $features.isScimEnabled
|
||||||
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||||
$: group = $groups.find(x => x._id === groupId)
|
$: group = $groups.find(x => x._id === groupId)
|
||||||
|
@ -57,8 +58,11 @@
|
||||||
)
|
)
|
||||||
.map(app => ({
|
.map(app => ({
|
||||||
...app,
|
...app,
|
||||||
role: group?.roles?.[apps.getProdAppID(app.devId)],
|
role: group?.builder?.apps.includes(apps.getProdAppID(app.devId))
|
||||||
|
? Constants.Roles.CREATOR
|
||||||
|
: group?.roles?.[apps.getProdAppID(app.devId)],
|
||||||
}))
|
}))
|
||||||
|
$: console.log(groupApps)
|
||||||
$: {
|
$: {
|
||||||
if (loaded && !group?._id) {
|
if (loaded && !group?._id) {
|
||||||
$goto("./")
|
$goto("./")
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
export let row
|
||||||
|
$: count = getCount(Object.keys(value || {}).length)
|
||||||
|
|
||||||
$: count = Object.keys(value || {}).length
|
const getCount = () => {
|
||||||
|
return sdk.users.hasAppBuilderPermissions(row)
|
||||||
|
? row.builder.apps.length +
|
||||||
|
Object.keys(row.roles || {}).filter(appId =>
|
||||||
|
row.builder.apps.includes(appId)
|
||||||
|
).length
|
||||||
|
: value?.length || 0
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="align">
|
<div class="align">
|
||||||
|
|
|
@ -89,7 +89,7 @@
|
||||||
$: scimEnabled = $features.isScimEnabled
|
$: scimEnabled = $features.isScimEnabled
|
||||||
$: isSSO = !!user?.provider
|
$: isSSO = !!user?.provider
|
||||||
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||||
$: privileged = sdk.users.isAdminOrBuilder(user)
|
$: privileged = sdk.users.isAdminOrGlobalBuilder(user)
|
||||||
$: nameLabel = getNameLabel(user)
|
$: nameLabel = getNameLabel(user)
|
||||||
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
||||||
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
||||||
|
@ -98,17 +98,14 @@
|
||||||
return y._id === userId
|
return y._id === userId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
$: globalRole = sdk.users.isAdmin(user)
|
$: globalRole = sdk.users.isAdmin(user) ? "admin" : "appUser"
|
||||||
? "admin"
|
|
||||||
: sdk.users.isBuilder(user)
|
|
||||||
? "developer"
|
|
||||||
: "appUser"
|
|
||||||
|
|
||||||
const getAvailableApps = (appList, privileged, roles) => {
|
const getAvailableApps = (appList, privileged, roles) => {
|
||||||
let availableApps = appList.slice()
|
let availableApps = appList.slice()
|
||||||
if (!privileged) {
|
if (!privileged) {
|
||||||
availableApps = availableApps.filter(x => {
|
availableApps = availableApps.filter(x => {
|
||||||
return Object.keys(roles || {}).find(y => {
|
let roleKeys = Object.keys(roles || {})
|
||||||
|
return roleKeys.concat(user?.builder?.apps).find(y => {
|
||||||
return x.appId === apps.extractAppId(y)
|
return x.appId === apps.extractAppId(y)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -119,7 +116,7 @@
|
||||||
name: app.name,
|
name: app.name,
|
||||||
devId: app.devId,
|
devId: app.devId,
|
||||||
icon: app.icon,
|
icon: app.icon,
|
||||||
role: privileged ? Constants.Roles.ADMIN : roles[prodAppId],
|
role: getRole(prodAppId, roles),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -132,6 +129,18 @@
|
||||||
return groups.filter(group => group.name?.toLowerCase().includes(search))
|
return groups.filter(group => group.name?.toLowerCase().includes(search))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRole = (prodAppId, roles) => {
|
||||||
|
if (privileged) {
|
||||||
|
return Constants.Roles.ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user?.builder?.apps?.includes(prodAppId)) {
|
||||||
|
return Constants.Roles.CREATOR
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles[prodAppId]
|
||||||
|
}
|
||||||
|
|
||||||
const getNameLabel = user => {
|
const getNameLabel = user => {
|
||||||
const { firstName, lastName, email } = user || {}
|
const { firstName, lastName, email } = user || {}
|
||||||
if (!firstName && !lastName) {
|
if (!firstName && !lastName) {
|
||||||
|
|
|
@ -2,12 +2,16 @@
|
||||||
import { StatusLight } from "@budibase/bbui"
|
import { StatusLight } from "@budibase/bbui"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const getRoleLabel = roleId => {
|
const getRoleLabel = roleId => {
|
||||||
const role = $roles.find(x => x._id === roleId)
|
const role = $roles.find(x => x._id === roleId)
|
||||||
return role?.name || "Custom role"
|
return roleId === Constants.Roles.CREATOR
|
||||||
|
? capitalise(Constants.Roles.CREATOR.toLowerCase())
|
||||||
|
: role?.name || "Custom role"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,22 @@
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let row
|
export let row
|
||||||
|
$: console.log(row)
|
||||||
$: priviliged = sdk.users.isAdminOrBuilder(row)
|
$: priviliged = sdk.users.isAdminOrBuilder(row)
|
||||||
$: count = priviliged ? $apps.length : value?.length || 0
|
$: count = getCount(row)
|
||||||
|
|
||||||
|
const getCount = () => {
|
||||||
|
if (priviliged) {
|
||||||
|
return $apps.length
|
||||||
|
} else {
|
||||||
|
return sdk.users.hasAppBuilderPermissions(row)
|
||||||
|
? row.builder.apps.length +
|
||||||
|
Object.keys(row.roles || {}).filter(appId =>
|
||||||
|
row.builder.apps.includes(appId)
|
||||||
|
).length
|
||||||
|
: value?.length || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="align">
|
<div class="align">
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
export let row
|
export let row
|
||||||
|
|
||||||
const TooltipMap = {
|
const TooltipMap = {
|
||||||
appUser: "Only has access to published apps",
|
appUser: "Only has access to assigned apps",
|
||||||
developer: "Access to the app builder",
|
developer: "Access to the app builder",
|
||||||
admin: "Full access",
|
admin: "Full access",
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,19 @@ export function createGroupsStore() {
|
||||||
},
|
},
|
||||||
|
|
||||||
getGroupAppIds: group => {
|
getGroupAppIds: group => {
|
||||||
return Object.keys(group?.roles || {})
|
let groupAppIds = Object.keys(group?.roles || {})
|
||||||
|
if (group?.builder?.apps) {
|
||||||
|
groupAppIds = groupAppIds.concat(group.builder.apps)
|
||||||
|
}
|
||||||
|
return groupAppIds
|
||||||
|
},
|
||||||
|
|
||||||
|
addGroupAppBuilder: async (groupId, appId) => {
|
||||||
|
return await API.addGroupAppBuilder({ groupId, appId })
|
||||||
|
},
|
||||||
|
|
||||||
|
removeGroupAppBuilder: async (groupId, appId) => {
|
||||||
|
return await API.removeGroupAppBuilder({ groupId, appId })
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,6 +125,10 @@ export const createLicensingStore = () => {
|
||||||
const syncAutomationsEnabled = license.features.includes(
|
const syncAutomationsEnabled = license.features.includes(
|
||||||
Constants.Features.SYNC_AUTOMATIONS
|
Constants.Features.SYNC_AUTOMATIONS
|
||||||
)
|
)
|
||||||
|
const perAppBuildersEnabled = license.features.includes(
|
||||||
|
Constants.Features.APP_BUILDERS
|
||||||
|
)
|
||||||
|
|
||||||
const isViewPermissionsEnabled = license.features.includes(
|
const isViewPermissionsEnabled = license.features.includes(
|
||||||
Constants.Features.VIEW_PERMISSIONS
|
Constants.Features.VIEW_PERMISSIONS
|
||||||
)
|
)
|
||||||
|
@ -144,6 +148,7 @@ export const createLicensingStore = () => {
|
||||||
enforceableSSO,
|
enforceableSSO,
|
||||||
syncAutomationsEnabled,
|
syncAutomationsEnabled,
|
||||||
isViewPermissionsEnabled,
|
isViewPermissionsEnabled,
|
||||||
|
perAppBuildersEnabled,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -112,12 +112,16 @@ export function createUsersStore() {
|
||||||
return await API.saveUser(user)
|
return await API.saveUser(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addAppBuilder(userId, appId) {
|
||||||
|
return await API.addAppBuilder({ userId, appId })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAppBuilder(userId, appId) {
|
||||||
|
return await API.removeAppBuilder({ userId, appId })
|
||||||
|
}
|
||||||
|
|
||||||
const getUserRole = user =>
|
const getUserRole = user =>
|
||||||
sdk.users.isAdmin(user)
|
sdk.users.isAdminOrGlobalBuilder(user) ? "admin" : "appUser"
|
||||||
? "admin"
|
|
||||||
: sdk.users.isBuilder(user)
|
|
||||||
? "developer"
|
|
||||||
: "appUser"
|
|
||||||
|
|
||||||
const refreshUsage =
|
const refreshUsage =
|
||||||
fn =>
|
fn =>
|
||||||
|
@ -139,6 +143,8 @@ export function createUsersStore() {
|
||||||
getInvites,
|
getInvites,
|
||||||
updateInvite,
|
updateInvite,
|
||||||
getUserCountByApp,
|
getUserCountByApp,
|
||||||
|
addAppBuilder,
|
||||||
|
removeAppBuilder,
|
||||||
// any operation that adds or deletes users
|
// any operation that adds or deletes users
|
||||||
acceptInvite,
|
acceptInvite,
|
||||||
create: refreshUsage(create),
|
create: refreshUsage(create),
|
||||||
|
|
|
@ -104,5 +104,27 @@ export const buildGroupsEndpoints = API => {
|
||||||
removeAppsFromGroup: async (groupId, appArray) => {
|
removeAppsFromGroup: async (groupId, appArray) => {
|
||||||
return updateGroupResource(groupId, "apps", "remove", appArray)
|
return updateGroupResource(groupId, "apps", "remove", appArray)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add app builder to group
|
||||||
|
* @param groupId The group to update
|
||||||
|
* @param appId The app id where the builder will be added
|
||||||
|
*/
|
||||||
|
addGroupAppBuilder: async ({ groupId, appId }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/global/groups/${groupId}/app/${appId}/builder`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove app builder from group
|
||||||
|
* @param groupId The group to update
|
||||||
|
* @param appId The app id where the builder will be removed
|
||||||
|
*/
|
||||||
|
removeGroupAppBuilder: async ({ groupId, appId }) => {
|
||||||
|
return await API.delete({
|
||||||
|
url: `/api/global/groups/${groupId}/app/${appId}/builder`,
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,13 +156,14 @@ export const buildUserEndpoints = API => ({
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: "/api/global/users/onboard",
|
url: "/api/global/users/onboard",
|
||||||
body: payload.map(invite => {
|
body: payload.map(invite => {
|
||||||
const { email, admin, builder, apps } = invite
|
const { email, admin, builder, apps, appBuilders } = invite
|
||||||
return {
|
return {
|
||||||
email,
|
email,
|
||||||
userInfo: {
|
userInfo: {
|
||||||
admin: admin ? { global: true } : undefined,
|
admin: admin ? { global: true } : undefined,
|
||||||
builder: builder ? { global: true } : undefined,
|
builder: builder ? { global: true } : undefined,
|
||||||
apps: apps ? apps : undefined,
|
apps: apps ? apps : undefined,
|
||||||
|
appBuilders,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -175,10 +176,12 @@ export const buildUserEndpoints = API => ({
|
||||||
* @param invite the invite code sent in the email
|
* @param invite the invite code sent in the email
|
||||||
*/
|
*/
|
||||||
updateUserInvite: async invite => {
|
updateUserInvite: async invite => {
|
||||||
|
console.log(invite)
|
||||||
await API.post({
|
await API.post({
|
||||||
url: `/api/global/users/invite/update/${invite.code}`,
|
url: `/api/global/users/invite/update/${invite.code}`,
|
||||||
body: {
|
body: {
|
||||||
apps: invite.apps,
|
apps: invite.apps,
|
||||||
|
appBuilders: invite.appBuilders,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -250,4 +253,26 @@ export const buildUserEndpoints = API => ({
|
||||||
url: `/api/global/users/count/${appId}`,
|
url: `/api/global/users/count/${appId}`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a per app builder to the selected app
|
||||||
|
* @param appId the applications id
|
||||||
|
* @param userId The id of the user to add as a builder
|
||||||
|
*/
|
||||||
|
addAppBuilder: async ({ userId, appId }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/global/users/${userId}/app/${appId}/builder`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a per app builder to the selected app
|
||||||
|
* @param appId the applications id
|
||||||
|
* @param userId The id of the user to remove as a builder
|
||||||
|
*/
|
||||||
|
removeAppBuilder: async ({ userId, appId }) => {
|
||||||
|
return await API.delete({
|
||||||
|
url: `/api/global/users/${userId}/app/${appId}/builder`,
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,11 +24,23 @@ export const BudibaseRoles = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BudibaseRoleOptions = [
|
export const BudibaseRoleOptions = [
|
||||||
{ label: "App User", value: BudibaseRoles.AppUser },
|
{ label: "Member", value: BudibaseRoles.AppUser },
|
||||||
{ label: "Developer", value: BudibaseRoles.Developer },
|
|
||||||
{ label: "Admin", value: BudibaseRoles.Admin },
|
{ label: "Admin", value: BudibaseRoles.Admin },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const BudibaseRoleOptionsNew = [
|
||||||
|
{
|
||||||
|
label: "Admin",
|
||||||
|
value: "admin",
|
||||||
|
subtitle: "Has full access to all apps and settings in your account",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Member",
|
||||||
|
value: "appUser",
|
||||||
|
subtitle: "Can only view apps they have access to",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export const BuilderRoleDescriptions = [
|
export const BuilderRoleDescriptions = [
|
||||||
{
|
{
|
||||||
value: BudibaseRoles.AppUser,
|
value: BudibaseRoles.AppUser,
|
||||||
|
@ -70,6 +82,7 @@ export const Roles = {
|
||||||
BASIC: "BASIC",
|
BASIC: "BASIC",
|
||||||
PUBLIC: "PUBLIC",
|
PUBLIC: "PUBLIC",
|
||||||
BUILDER: "BUILDER",
|
BUILDER: "BUILDER",
|
||||||
|
CREATOR: "CREATOR",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Themes = [
|
export const Themes = [
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
import { Roles } from "../constants"
|
import { Roles } from "../constants"
|
||||||
|
|
||||||
const RolePriorities = {
|
const RolePriorities = {
|
||||||
[Roles.ADMIN]: 4,
|
[Roles.ADMIN]: 5,
|
||||||
|
[Roles.CREATOR]: 4,
|
||||||
[Roles.POWER]: 3,
|
[Roles.POWER]: 3,
|
||||||
[Roles.BASIC]: 2,
|
[Roles.BASIC]: 2,
|
||||||
[Roles.PUBLIC]: 1,
|
[Roles.PUBLIC]: 1,
|
||||||
}
|
}
|
||||||
const RoleColours = {
|
const RoleColours = {
|
||||||
[Roles.ADMIN]: "var(--spectrum-global-color-static-red-400)",
|
[Roles.ADMIN]: "var(--spectrum-global-color-static-red-400)",
|
||||||
|
[Roles.CREATOR]: "var(--spectrum-global-color-static-magenta-600)",
|
||||||
[Roles.POWER]: "var(--spectrum-global-color-static-orange-400)",
|
[Roles.POWER]: "var(--spectrum-global-color-static-orange-400)",
|
||||||
[Roles.BASIC]: "var(--spectrum-global-color-static-green-400)",
|
[Roles.BASIC]: "var(--spectrum-global-color-static-green-400)",
|
||||||
[Roles.PUBLIC]: "var(--spectrum-global-color-static-blue-400)",
|
[Roles.PUBLIC]: "var(--spectrum-global-color-static-blue-400)",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRolePriority = roleId => {
|
export const getRolePriority = role => {
|
||||||
return RolePriorities[roleId] ?? 0
|
return RolePriorities[role] ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRoleColour = roleId => {
|
export const getRoleColour = roleId => {
|
||||||
|
|
|
@ -35,6 +35,13 @@ export function isAdminOrBuilder(
|
||||||
return isBuilder(user, appId) || isAdmin(user)
|
return isBuilder(user, appId) || isAdmin(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAdminOrGlobalBuilder(
|
||||||
|
user: User | ContextUser,
|
||||||
|
appId?: string
|
||||||
|
): boolean {
|
||||||
|
return isGlobalBuilder(user) || isAdmin(user)
|
||||||
|
}
|
||||||
|
|
||||||
// check if they are a builder within an app (not necessarily a global builder)
|
// check if they are a builder within an app (not necessarily a global builder)
|
||||||
export function hasAppBuilderPermissions(user?: User | ContextUser): boolean {
|
export function hasAppBuilderPermissions(user?: User | ContextUser): boolean {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|
|
@ -266,14 +266,17 @@ export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
|
||||||
|
|
||||||
// Temp password to be passed to the user.
|
// Temp password to be passed to the user.
|
||||||
createdPasswords[invite.email] = password
|
createdPasswords[invite.email] = password
|
||||||
|
let builder: { global: boolean; apps?: string[] } = { global: false }
|
||||||
|
if (invite.userInfo.appBuilders) {
|
||||||
|
builder.apps = invite.userInfo.appBuilders
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
email: invite.email,
|
email: invite.email,
|
||||||
password,
|
password,
|
||||||
forceResetPassword: true,
|
forceResetPassword: true,
|
||||||
roles: invite.userInfo.apps,
|
roles: invite.userInfo.apps,
|
||||||
admin: { global: false },
|
admin: { global: false },
|
||||||
builder: { global: false },
|
builder,
|
||||||
tenantId: tenancy.getTenantId(),
|
tenantId: tenancy.getTenantId(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -368,6 +371,15 @@ export const updateInvite = async (ctx: any) => {
|
||||||
...invite,
|
...invite,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!updateBody?.appBuilders || !updateBody.appBuilders?.length) {
|
||||||
|
updated.info.appBuilders = []
|
||||||
|
} else {
|
||||||
|
updated.info.appBuilders = [
|
||||||
|
...(invite.info.appBuilders ?? []),
|
||||||
|
...updateBody.appBuilders,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
if (!updateBody?.apps || !Object.keys(updateBody?.apps).length) {
|
if (!updateBody?.apps || !Object.keys(updateBody?.apps).length) {
|
||||||
updated.info.apps = []
|
updated.info.apps = []
|
||||||
} else {
|
} else {
|
||||||
|
@ -392,7 +404,7 @@ export const inviteAccept = async (
|
||||||
// info is an extension of the user object that was stored by global
|
// info is an extension of the user object that was stored by global
|
||||||
const { email, info }: any = await checkInviteCode(inviteCode)
|
const { email, info }: any = await checkInviteCode(inviteCode)
|
||||||
const user = await tenancy.doInTenant(info.tenantId, async () => {
|
const user = await tenancy.doInTenant(info.tenantId, async () => {
|
||||||
let request = {
|
let request: any = {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
password,
|
password,
|
||||||
|
@ -400,9 +412,14 @@ export const inviteAccept = async (
|
||||||
roles: info.apps,
|
roles: info.apps,
|
||||||
tenantId: info.tenantId,
|
tenantId: info.tenantId,
|
||||||
}
|
}
|
||||||
|
let builder: { global: boolean; apps?: string[] } = { global: false }
|
||||||
|
|
||||||
|
if (info.appBuilders) {
|
||||||
|
builder.apps = info.appBuilders
|
||||||
|
request.builder = builder
|
||||||
|
delete info.appBuilders
|
||||||
|
}
|
||||||
delete info.apps
|
delete info.apps
|
||||||
|
|
||||||
request = {
|
request = {
|
||||||
...request,
|
...request,
|
||||||
...info,
|
...info,
|
||||||
|
|
Loading…
Reference in New Issue