Add display name, color and descriptions to roles. Allow row CRUD via new UI
This commit is contained in:
parent
a5520a973c
commit
a81d9c6dd1
|
@ -45,10 +45,22 @@ export class Role implements RoleDoc {
|
|||
inherits?: string
|
||||
version?: string
|
||||
permissions = {}
|
||||
displayName?: string
|
||||
color?: string
|
||||
description?: string
|
||||
|
||||
constructor(id: string, name: string, permissionId: string) {
|
||||
constructor(
|
||||
id: string,
|
||||
displayName: string,
|
||||
description: string,
|
||||
color: string,
|
||||
permissionId: string
|
||||
) {
|
||||
this._id = id
|
||||
this.name = name
|
||||
this.name = id
|
||||
this.displayName = displayName
|
||||
this.color = color
|
||||
this.description = description
|
||||
this.permissionId = permissionId
|
||||
// version for managing the ID - removing the role_ when responding
|
||||
this.version = RoleIDVersion.NAME
|
||||
|
@ -63,21 +75,39 @@ export class Role implements RoleDoc {
|
|||
const BUILTIN_ROLES = {
|
||||
ADMIN: new Role(
|
||||
BUILTIN_IDS.ADMIN,
|
||||
"Admin",
|
||||
"App admin",
|
||||
"Can do everything",
|
||||
"var(--spectrum-global-color-static-red-400)",
|
||||
BuiltinPermissionID.ADMIN
|
||||
).addInheritance(BUILTIN_IDS.POWER),
|
||||
POWER: new Role(
|
||||
BUILTIN_IDS.POWER,
|
||||
"Power",
|
||||
"App power user",
|
||||
"An app user with more access",
|
||||
"var(--spectrum-global-color-static-orange-400)",
|
||||
BuiltinPermissionID.POWER
|
||||
).addInheritance(BUILTIN_IDS.BASIC),
|
||||
BASIC: new Role(
|
||||
BUILTIN_IDS.BASIC,
|
||||
"Basic",
|
||||
"App user",
|
||||
"Any logged in user",
|
||||
"var(--spectrum-global-color-static-green-400)",
|
||||
BuiltinPermissionID.WRITE
|
||||
).addInheritance(BUILTIN_IDS.PUBLIC),
|
||||
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public", BuiltinPermissionID.PUBLIC),
|
||||
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder", BuiltinPermissionID.ADMIN),
|
||||
PUBLIC: new Role(
|
||||
BUILTIN_IDS.PUBLIC,
|
||||
"Public user",
|
||||
"Accessible to anyone",
|
||||
"var(--spectrum-global-color-static-blue-400)",
|
||||
BuiltinPermissionID.PUBLIC
|
||||
),
|
||||
BUILDER: new Role(
|
||||
BUILTIN_IDS.BUILDER,
|
||||
"Builder user",
|
||||
"Users that can edit this app",
|
||||
"var(--spectrum-global-color-static-magenta-600)",
|
||||
BuiltinPermissionID.ADMIN
|
||||
),
|
||||
}
|
||||
|
||||
export function getBuiltinRoles(): { [key: string]: RoleDoc } {
|
||||
|
|
|
@ -2,29 +2,28 @@
|
|||
import { Button, Helpers, ActionButton } from "@budibase/bbui"
|
||||
import { useSvelteFlow, Position } from "@xyflow/svelte"
|
||||
import { getContext, tick } from "svelte"
|
||||
import { autoLayout } from "./layout"
|
||||
import { autoLayout, roleToNode } from "./layout"
|
||||
import { ZoomDuration } from "./constants"
|
||||
import { getSequentialName } from "helpers/duplicate"
|
||||
import { roles } from "stores/builder"
|
||||
import { Roles } from "constants/backend"
|
||||
|
||||
const { nodes, edges } = getContext("flow")
|
||||
const flow = useSvelteFlow()
|
||||
|
||||
const addRole = async () => {
|
||||
nodes.update(state => [
|
||||
...state,
|
||||
{
|
||||
id: Helpers.uuid(),
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
type: "role",
|
||||
data: {
|
||||
displayName: "New role",
|
||||
description: "Custom role",
|
||||
custom: true,
|
||||
const role = {
|
||||
name: Helpers.uuid(),
|
||||
displayName: getSequentialName($nodes, "New role ", {
|
||||
getName: x => x.data.displayName,
|
||||
}),
|
||||
color: "var(--spectrum-global-color-gray-700)",
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
])
|
||||
description: "Custom role",
|
||||
permissionId: "write",
|
||||
inherits: Roles.BASIC,
|
||||
}
|
||||
const savedRole = await roles.save(role)
|
||||
nodes.update(state => [...state, roleToNode(savedRole)])
|
||||
await doAutoLayout()
|
||||
}
|
||||
|
||||
|
@ -36,7 +35,6 @@
|
|||
flow.fitView({
|
||||
maxZoom: 1,
|
||||
duration: ZoomDuration,
|
||||
includeHiddenNodes: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { SvelteFlow, Background, BackgroundVariant } from "@xyflow/svelte"
|
||||
import "@xyflow/svelte/dist/style.css"
|
||||
import RoleNode from "./RoleNode.svelte"
|
||||
import { defaultLayout, autoLayout } from "./layout"
|
||||
import { rolesToNodes, autoLayout } from "./layout"
|
||||
import { onMount, setContext } from "svelte"
|
||||
import Controls from "./Controls.svelte"
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
|||
setContext("flow", { nodes, edges })
|
||||
|
||||
onMount(() => {
|
||||
const layout = autoLayout(defaultLayout())
|
||||
const layout = autoLayout(rolesToNodes())
|
||||
nodes.set(layout.nodes)
|
||||
edges.set(layout.edges)
|
||||
})
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
import { Roles } from "constants/backend"
|
||||
import { NodeWidth, NodeHeight } from "./constants"
|
||||
import { getContext, tick } from "svelte"
|
||||
import { autoLayout } from "./layout"
|
||||
import { autoLayout, nodeToRole } from "./layout"
|
||||
import { roles } from "stores/builder"
|
||||
|
||||
export let data
|
||||
export let isConnectable
|
||||
|
@ -27,9 +28,8 @@
|
|||
let tempDescription
|
||||
let tempColor
|
||||
|
||||
$: color = data.color || RoleUtils.getRoleColour(id)
|
||||
|
||||
const deleteNode = async () => {
|
||||
await roles.delete(nodeToRole({ id, data }))
|
||||
flow.deleteElements({
|
||||
nodes: [{ id }],
|
||||
})
|
||||
|
@ -40,11 +40,12 @@
|
|||
const openPopover = () => {
|
||||
tempDisplayName = data.displayName
|
||||
tempDescription = data.description
|
||||
tempColor = color
|
||||
tempColor = data.color
|
||||
modal.show()
|
||||
}
|
||||
|
||||
const saveChanges = () => {
|
||||
const saveChanges = async () => {
|
||||
await roles.save(nodeToRole({ id, data }))
|
||||
flow.updateNodeData(id, {
|
||||
displayName: tempDisplayName,
|
||||
description: tempDescription,
|
||||
|
@ -63,7 +64,7 @@
|
|||
<div
|
||||
class="node"
|
||||
class:selected={false}
|
||||
style={`--color:${color}; --width:${NodeWidth}px; --height:${NodeHeight}px;`}
|
||||
style={`--color:${data.color}; --width:${NodeWidth}px; --height:${NodeHeight}px;`}
|
||||
bind:this={anchor}
|
||||
>
|
||||
<div class="color" />
|
||||
|
|
|
@ -6,15 +6,36 @@ import { Roles } from "constants/backend"
|
|||
import { get } from "svelte/store"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
|
||||
// Converts a role doc into a node structure
|
||||
export const roleToNode = role => ({
|
||||
id: role._id,
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
type: "role",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
displayName: role.displayName || role.name,
|
||||
description: role.description || "Custom role",
|
||||
color: role.color || "var(--spectrum-global-color-static-magenta-400)",
|
||||
custom: !role._id.match(/[A-Z]+/),
|
||||
},
|
||||
})
|
||||
|
||||
// Converts a node structure back into a role doc
|
||||
export const nodeToRole = node => {
|
||||
const role = get(roles).find(x => x._id === node.id)
|
||||
return {
|
||||
...role,
|
||||
displayName: node.data.displayName,
|
||||
color: node.data.color,
|
||||
description: node.data.description,
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a flow compatible structure of nodes and edges from the current roles
|
||||
export const defaultLayout = () => {
|
||||
export const rolesToNodes = () => {
|
||||
const ignoredRoles = [Roles.PUBLIC]
|
||||
const $roles = get(roles)
|
||||
const descriptions = {
|
||||
[Roles.BASIC]: "Basic user",
|
||||
[Roles.POWER]: "Power user",
|
||||
[Roles.ADMIN]: "Can do everything",
|
||||
}
|
||||
|
||||
let nodes = []
|
||||
let edges = []
|
||||
|
@ -23,19 +44,11 @@ export const defaultLayout = () => {
|
|||
if (ignoredRoles.includes(role._id)) {
|
||||
continue
|
||||
}
|
||||
nodes.push({
|
||||
id: role._id,
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
type: "role",
|
||||
data: {
|
||||
displayName: role.displayName || role.name || "",
|
||||
description: descriptions[role._id] || "Custom role",
|
||||
color: role.color,
|
||||
custom: !role._id.match(/[A-Z]+/),
|
||||
},
|
||||
})
|
||||
|
||||
// Add node for this role
|
||||
nodes.push(roleToNode(role))
|
||||
|
||||
// Add edges for this role
|
||||
let inherits = []
|
||||
if (role.inherits) {
|
||||
inherits = Array.isArray(role.inherits) ? role.inherits : [role.inherits]
|
||||
|
@ -60,40 +73,6 @@ export const defaultLayout = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Converts the flow structure of ndes and edges back into an array of roles
|
||||
export const layoutToRoles = ({ nodes, edges }) => {
|
||||
// Clone and wipe existing inheritance
|
||||
let newRoles = Helpers.cloneDeep(get(roles)).map(role => {
|
||||
return { ...role, inherits: [] }
|
||||
})
|
||||
|
||||
// Copy over names and colours
|
||||
for (let node of nodes) {
|
||||
let role = newRoles.find(x => x._id === node.id)
|
||||
if (role) {
|
||||
role.name = node.data.label
|
||||
role.color = node.data.color
|
||||
} else {
|
||||
// New role
|
||||
}
|
||||
}
|
||||
|
||||
// Build inheritance
|
||||
for (let edge of edges) {
|
||||
let role = newRoles.find(x => x._id === edge.target)
|
||||
if (role) {
|
||||
role.inherits.push(edge.source)
|
||||
} else {
|
||||
// New role
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure basic is correct
|
||||
newRoles.find(x => x._id === Roles.BASIC).inherits = [Roles.BASIC]
|
||||
|
||||
return newRoles
|
||||
}
|
||||
|
||||
// Updates positions of nodes and edges into a nice graph structure
|
||||
const dagreLayout = ({ nodes, edges }) => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<script>
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { StatusLight } from "@budibase/bbui"
|
||||
import { roles } from "stores/builder"
|
||||
|
||||
export let id
|
||||
export let size = "M"
|
||||
export let disabled = false
|
||||
|
||||
$: color = RoleUtils.getRoleColour(id)
|
||||
$: color =
|
||||
$roles.find(x => x._id === id)?.color ||
|
||||
"var(--spectrum-global-color-static-magenta-400)"
|
||||
</script>
|
||||
|
||||
<StatusLight square {disabled} {size} {color} />
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) {
|
||||
return null
|
||||
}
|
||||
return RoleUtils.getRoleColour(role._id)
|
||||
return role.color || "var(--spectrum-global-color-static-magenta-400)"
|
||||
}
|
||||
|
||||
const getIcon = role => {
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
options={$roles}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
|
||||
getOptionColour={role =>
|
||||
role.color || "var(--spectrum-global-color-static-magenta-400)"}
|
||||
{placeholder}
|
||||
{error}
|
||||
/>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
let showTooltip = false
|
||||
|
||||
$: color = RoleUtils.getRoleColour(roleId)
|
||||
$: color = role.color || "var(--spectrum-global-color-static-magenta-400)"
|
||||
$: role = $roles.find(role => role._id === roleId)
|
||||
$: tooltip =
|
||||
roleId === Roles.PUBLIC
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
|
||||
export let value
|
||||
|
||||
$: role = $roles.find(x => x._id === roleId)
|
||||
|
||||
const getRoleLabel = roleId => {
|
||||
const role = $roles.find(x => x._id === roleId)
|
||||
return roleId === Constants.Roles.CREATOR
|
||||
? capitalise(Constants.Roles.CREATOR.toLowerCase())
|
||||
: role?.name || "Custom role"
|
||||
|
@ -17,7 +18,10 @@
|
|||
{#if value === Constants.Roles.CREATOR}
|
||||
Can edit
|
||||
{:else}
|
||||
<StatusLight square color={RoleUtils.getRoleColour(value)}>
|
||||
<StatusLight
|
||||
square
|
||||
color={role?.color || "var(--spectrum-global-color-static-magenta-400)"}
|
||||
>
|
||||
Can use as {getRoleLabel(value)}
|
||||
</StatusLight>
|
||||
{/if}
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "api"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { Roles } from "constants/backend"
|
||||
|
||||
const ROLE_NAMES = {
|
||||
[Roles.ADMIN]: "App admin",
|
||||
[Roles.POWER]: "App power user",
|
||||
[Roles.BASIC]: "App user",
|
||||
[Roles.PUBLIC]: "Public user",
|
||||
}
|
||||
|
||||
export function createRolesStore() {
|
||||
const { subscribe, update, set } = writable([])
|
||||
|
@ -25,16 +17,7 @@ export function createRolesStore() {
|
|||
|
||||
const actions = {
|
||||
fetch: async () => {
|
||||
let roles = await API.getRoles()
|
||||
|
||||
// Update labels
|
||||
for (let [roleId, name] of Object.entries(ROLE_NAMES)) {
|
||||
const idx = roles.findIndex(x => x._id === roleId)
|
||||
if (idx !== -1) {
|
||||
roles[idx].name = name
|
||||
}
|
||||
}
|
||||
|
||||
const roles = await API.getRoles()
|
||||
setRoles(roles)
|
||||
},
|
||||
fetchByAppId: async appId => {
|
||||
|
@ -51,7 +34,13 @@ export function createRolesStore() {
|
|||
save: async role => {
|
||||
const savedRole = await API.saveRole(role)
|
||||
await actions.fetch()
|
||||
return savedRole
|
||||
|
||||
// When saving a role we get back an _id prefixed by role_, but the API does not want this
|
||||
// in future requests
|
||||
return {
|
||||
...savedRole,
|
||||
_id: savedRole._id.replace("role_", ""),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -7,20 +7,7 @@ const RolePriorities = {
|
|||
[Roles.BASIC]: 2,
|
||||
[Roles.PUBLIC]: 1,
|
||||
}
|
||||
const RoleColours = {
|
||||
[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.BASIC]: "var(--spectrum-global-color-static-green-400)",
|
||||
[Roles.PUBLIC]: "var(--spectrum-global-color-static-blue-400)",
|
||||
}
|
||||
|
||||
export const getRolePriority = role => {
|
||||
return RolePriorities[role] ?? 0
|
||||
}
|
||||
|
||||
export const getRoleColour = roleId => {
|
||||
return (
|
||||
RoleColours[roleId] ?? "var(--spectrum-global-color-static-magenta-400)"
|
||||
)
|
||||
}
|
||||
|
|
|
@ -62,7 +62,16 @@ export async function find(ctx: UserCtx<void, FindRoleResponse>) {
|
|||
|
||||
export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
|
||||
const db = context.getAppDB()
|
||||
let { _id, name, inherits, permissionId, version } = ctx.request.body
|
||||
let {
|
||||
_id,
|
||||
name,
|
||||
displayName,
|
||||
description,
|
||||
color,
|
||||
inherits,
|
||||
permissionId,
|
||||
version,
|
||||
} = ctx.request.body
|
||||
let isCreate = false
|
||||
const isNewVersion = version === roles.RoleIDVersion.NAME
|
||||
|
||||
|
@ -88,7 +97,13 @@ export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
|
|||
ctx.throw(400, "Cannot change custom role name")
|
||||
}
|
||||
|
||||
const role = new roles.Role(_id, name, permissionId).addInheritance(inherits)
|
||||
const role = new roles.Role(
|
||||
_id,
|
||||
displayName || name,
|
||||
description || "Custom role",
|
||||
color || "var(--spectrum-global-color-static-magenta-400)",
|
||||
permissionId
|
||||
).addInheritance(inherits)
|
||||
if (ctx.request.body._rev) {
|
||||
role._rev = ctx.request.body._rev
|
||||
}
|
||||
|
|
|
@ -208,6 +208,9 @@ export function roleValidator() {
|
|||
name: Joi.string()
|
||||
.regex(/^[a-zA-Z0-9_]*$/)
|
||||
.required(),
|
||||
displayName: Joi.string().optional(),
|
||||
color: Joi.string().optional(),
|
||||
description: Joi.string().optional(),
|
||||
// this is the base permission ID (for now a built in)
|
||||
permissionId: Joi.string()
|
||||
.valid(...Object.values(permissions.BuiltinPermissionID))
|
||||
|
|
|
@ -4,6 +4,9 @@ export interface SaveRoleRequest {
|
|||
_id?: string
|
||||
_rev?: string
|
||||
name: string
|
||||
displayName?: string
|
||||
color?: string
|
||||
description?: string
|
||||
inherits: string
|
||||
permissionId: string
|
||||
version: string
|
||||
|
|
|
@ -6,4 +6,7 @@ export interface Role extends Document {
|
|||
permissions: { [key: string]: string[] }
|
||||
version?: string
|
||||
name: string
|
||||
displayName?: string
|
||||
color?: string
|
||||
description?: string
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue