231 lines
5.2 KiB
Svelte
231 lines
5.2 KiB
Svelte
<script>
|
|
import { Handle, Position } from "@xyflow/svelte"
|
|
import {
|
|
Icon,
|
|
Input,
|
|
ColorPicker,
|
|
Modal,
|
|
ModalContent,
|
|
FieldLabel,
|
|
} from "@budibase/bbui"
|
|
import { Roles } from "constants/backend"
|
|
import { NodeWidth, NodeHeight } from "./constants"
|
|
import { getContext } from "svelte"
|
|
import { roles } from "stores/builder"
|
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
|
|
export let data
|
|
export let id
|
|
export let selected
|
|
export let isConnectable
|
|
|
|
const { dragging, updateRole, deleteRole } = getContext("flow")
|
|
|
|
let anchor
|
|
let modal
|
|
let tempDisplayName
|
|
let tempDescription
|
|
let tempColor
|
|
let deleteModal
|
|
|
|
$: nameError = validateName(tempDisplayName, $roles)
|
|
$: descriptionError = validateDescription(tempDescription)
|
|
$: invalid = nameError || descriptionError
|
|
$: targetClasses = `target${!$dragging ? " hidden" : ""}`
|
|
|
|
const validateName = (name, roles) => {
|
|
if (!name?.length) {
|
|
return "Please enter a name"
|
|
}
|
|
if (roles.some(x => x.uiMetadata.displayName === name && x._id !== id)) {
|
|
return "That name is already used by another role"
|
|
}
|
|
return null
|
|
}
|
|
|
|
const validateDescription = description => {
|
|
if (!description?.length) {
|
|
return "Please enter a name"
|
|
}
|
|
return null
|
|
}
|
|
|
|
const openPopover = e => {
|
|
e.stopPropagation()
|
|
tempDisplayName = data.displayName
|
|
tempDescription = data.description
|
|
tempColor = data.color
|
|
modal.show()
|
|
}
|
|
|
|
const saveChanges = () => {
|
|
updateRole(id, {
|
|
displayName: tempDisplayName,
|
|
description: tempDescription,
|
|
color: tempColor,
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
class="node"
|
|
class:selected
|
|
style={`--color:${data.color}; --width:${NodeWidth}px; --height:${NodeHeight}px;`}
|
|
bind:this={anchor}
|
|
>
|
|
<div class="color" />
|
|
<div class="content">
|
|
<div class="text">
|
|
<div class="name">
|
|
{data.displayName}
|
|
</div>
|
|
{#if data.description}
|
|
<div class="description" title={data.description}>
|
|
{data.description}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{#if data.custom}
|
|
<div class="buttons">
|
|
<Icon size="S" name="Edit" hoverable on:click={openPopover} />
|
|
<Icon size="S" name="Delete" hoverable on:click={deleteModal?.show} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{#if isConnectable}
|
|
<Handle
|
|
type="target"
|
|
position={Position.Left}
|
|
class={targetClasses}
|
|
isConnectable={$dragging}
|
|
/>
|
|
<Handle type="source" position={Position.Right} />
|
|
{/if}
|
|
</div>
|
|
|
|
<ConfirmDialog
|
|
bind:this={deleteModal}
|
|
title={`Delete ${data.displayName}`}
|
|
body="Are you sure you want to delete this role? This can't be undone."
|
|
okText="Delete"
|
|
onOk={async () => await deleteRole(id)}
|
|
/>
|
|
|
|
<Modal bind:this={modal}>
|
|
<ModalContent
|
|
title={`Edit ${data.displayName}`}
|
|
confirmText="Save"
|
|
onConfirm={saveChanges}
|
|
disabled={invalid}
|
|
>
|
|
<Input
|
|
label="Name"
|
|
value={tempDisplayName}
|
|
error={nameError}
|
|
on:change={e => (tempDisplayName = e.detail)}
|
|
/>
|
|
<Input
|
|
label="Description"
|
|
value={tempDescription}
|
|
error={descriptionError}
|
|
on:change={e => (tempDescription = e.detail)}
|
|
/>
|
|
<div>
|
|
<FieldLabel label="Color" />
|
|
<ColorPicker value={tempColor} on:change={e => (tempColor = e.detail)} />
|
|
</div>
|
|
</ModalContent>
|
|
</Modal>
|
|
|
|
<style>
|
|
/* Node styles */
|
|
.node {
|
|
position: relative;
|
|
background: var(--spectrum-global-color-gray-100);
|
|
border-radius: 4px;
|
|
width: var(--width);
|
|
height: var(--height);
|
|
display: flex;
|
|
flex-direction: row;
|
|
box-sizing: border-box;
|
|
cursor: pointer;
|
|
transition: background 130ms ease-out;
|
|
}
|
|
.node:hover {
|
|
background: var(--spectrum-global-color-gray-200);
|
|
}
|
|
.node.selected {
|
|
background: var(--spectrum-global-color-blue-100);
|
|
cursor: grab;
|
|
}
|
|
.color {
|
|
border-top-left-radius: 4px;
|
|
border-bottom-left-radius: 4px;
|
|
height: 100%;
|
|
width: 10px;
|
|
flex: 0 0 10px;
|
|
background: var(--color);
|
|
}
|
|
|
|
/* Main container */
|
|
.content {
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
flex-direction: row;
|
|
border: 1px solid var(--border-color);
|
|
border-left-width: 0;
|
|
border-top-right-radius: 4px;
|
|
border-bottom-right-radius: 4px;
|
|
padding: 12px;
|
|
gap: 6px;
|
|
}
|
|
.node.selected .content {
|
|
border-color: var(--spectrum-global-color-blue-100);
|
|
}
|
|
|
|
/* Text */
|
|
.text {
|
|
width: 0;
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: stretch;
|
|
}
|
|
.name,
|
|
.description {
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
}
|
|
.description {
|
|
color: var(--spectrum-global-color-gray-600);
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Icons */
|
|
.buttons {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.buttons :global(.spectrum-Icon) {
|
|
color: var(--spectrum-global-color-gray-600);
|
|
}
|
|
|
|
/* Handlers */
|
|
.node :global(.svelte-flow__handle) {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-width: 2px;
|
|
}
|
|
.node :global(.svelte-flow__handle.target) {
|
|
background: var(--background-color);
|
|
}
|
|
.node :global(.svelte-flow__handle.hidden) {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|