Add animated curly brackets
This commit is contained in:
parent
66f6e91245
commit
4de91d4e3a
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import { BaseEdge } from "@xyflow/svelte"
|
||||
import { NodeWidth, GridResolution } from "./constants"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let sourceX
|
||||
export let sourceY
|
||||
|
||||
const { bounds } = getContext("flow")
|
||||
const BracketWidth = GridResolution * 2
|
||||
const BracketHeight = $bounds.height / 2 + GridResolution * 2
|
||||
|
||||
$: path = getCurlyBracePath(
|
||||
sourceX + BracketWidth,
|
||||
sourceY - BracketHeight,
|
||||
sourceX + BracketWidth,
|
||||
sourceY + BracketHeight
|
||||
)
|
||||
|
||||
const getCurlyBracePath = (x1, y1, x2, y2) => {
|
||||
const w = 2 // Thickness
|
||||
const q = 2 // Intensity
|
||||
const i = 28 // Inner radius strenth (lower is stronger)
|
||||
const j = 32 // Outer radius strength (higher is stronger)
|
||||
|
||||
// Calculate unit vector
|
||||
var dx = x1 - x2
|
||||
var dy = y1 - y2
|
||||
var len = Math.sqrt(dx * dx + dy * dy)
|
||||
dx = dx / len
|
||||
dy = dy / len
|
||||
|
||||
// Path control points
|
||||
const qx1 = x1 + q * w * dy - j
|
||||
const qy1 = y1 - q * w * dx
|
||||
const qx2 = x1 - 0.25 * len * dx + (1 - q) * w * dy - i
|
||||
const qy2 = y1 - 0.25 * len * dy - (1 - q) * w * dx
|
||||
const tx1 = x1 - 0.5 * len * dx + w * dy - BracketWidth
|
||||
const ty1 = y1 - 0.5 * len * dy - w * dx
|
||||
const qx3 = x2 + q * w * dy - j
|
||||
const qy3 = y2 - q * w * dx
|
||||
const qx4 = x1 - 0.75 * len * dx + (1 - q) * w * dy - i
|
||||
const qy4 = y1 - 0.75 * len * dy - (1 - q) * w * dx
|
||||
|
||||
return `M ${x1} ${y1} Q ${qx1} ${qy1} ${qx2} ${qy2} T ${tx1} ${ty1} M ${x2} ${y2} Q ${qx3} ${qy3} ${qx4} ${qy4} T ${tx1} ${ty1}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{...$$props}
|
||||
{path}
|
||||
style="--width:{NodeWidth}px; --x:{sourceX}px; --y:{sourceY}px;"
|
||||
/>
|
||||
|
||||
<style>
|
||||
:global(#basic-bracket) {
|
||||
animation-timing-function: linear(1, 0);
|
||||
}
|
||||
:global(#admin-bracket) {
|
||||
transform: scale(-1, 1) translateX(calc(var(--width) + 8px));
|
||||
transform-origin: var(--x) var(--y);
|
||||
}
|
||||
</style>
|
|
@ -11,11 +11,13 @@
|
|||
import "@xyflow/svelte/dist/style.css"
|
||||
import RoleNode from "./RoleNode.svelte"
|
||||
import RoleEdge from "./RoleEdge.svelte"
|
||||
import BracketEdge from "./BracketEdge.svelte"
|
||||
import {
|
||||
autoLayout,
|
||||
getAdminPosition,
|
||||
getBasicPosition,
|
||||
rolesToLayout,
|
||||
nodeToRole,
|
||||
} from "./utils"
|
||||
import { setContext, tick } from "svelte"
|
||||
import Controls from "./Controls.svelte"
|
||||
|
@ -81,7 +83,7 @@
|
|||
name: roleId,
|
||||
uiMetadata: {
|
||||
displayName: getSequentialName($roles, "New role ", {
|
||||
getName: x => x.uiMetadata.displayName,
|
||||
getName: role => role.uiMetadata.displayName,
|
||||
}),
|
||||
color: "var(--spectrum-global-color-gray-700)",
|
||||
description: "Custom role",
|
||||
|
@ -108,7 +110,7 @@
|
|||
if (metadata) {
|
||||
flow.updateNodeData(roleId, metadata)
|
||||
}
|
||||
await roles.save(nodeToRole(node))
|
||||
await roles.save(nodeToRole({ node, edges: $edges }))
|
||||
}
|
||||
|
||||
const deleteRole = async roleId => {
|
||||
|
@ -119,11 +121,11 @@
|
|||
}
|
||||
|
||||
const deleteEdge = async edgeId => {
|
||||
const edge = $edges.find(x => x.id === edgeId)
|
||||
const edge = $edges.find(edge => edge.id === edgeId)
|
||||
if (!edge) {
|
||||
return
|
||||
}
|
||||
edges.set($edges.filter(x => x.id !== edgeId))
|
||||
edges.set($edges.filter(edge => edge.id !== edgeId))
|
||||
await updateRole(edge.target)
|
||||
}
|
||||
|
||||
|
@ -158,7 +160,7 @@
|
|||
{edges}
|
||||
snapGrid={[GridResolution, GridResolution]}
|
||||
nodeTypes={{ role: RoleNode }}
|
||||
edgeTypes={{ role: RoleEdge }}
|
||||
edgeTypes={{ role: RoleEdge, bracket: BracketEdge }}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
fitViewOptions={{ maxZoom: MaxAutoZoom }}
|
||||
defaultEdgeOptions={{ type: "role", animated: true, selectable: false }}
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
ModalContent,
|
||||
FieldLabel,
|
||||
} from "@budibase/bbui"
|
||||
import { Roles } from "constants/backend"
|
||||
import { NodeWidth, NodeHeight, NodeVSpacing } from "./constants"
|
||||
import { NodeWidth, NodeHeight } from "./constants"
|
||||
import { getContext } from "svelte"
|
||||
import { roles } from "stores/builder"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
@ -70,6 +69,7 @@
|
|||
<div
|
||||
class="node"
|
||||
class:selected
|
||||
class:custom={data.custom}
|
||||
style={`--color:${data.color}; --width:${NodeWidth}px; --height:${NodeHeight}px;`}
|
||||
bind:this={anchor}
|
||||
>
|
||||
|
@ -92,24 +92,14 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isConnectable}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
class={targetClasses}
|
||||
isConnectable={$dragging}
|
||||
/>
|
||||
<Handle type="source" position={Position.Right} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if id === Roles.BASIC || id === Roles.ADMIN}
|
||||
<div
|
||||
class="bounds"
|
||||
class:flip={id === Roles.ADMIN}
|
||||
style="--height:{$bounds.height}px; --spacing:{NodeVSpacing}px;"
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
class={targetClasses}
|
||||
isConnectable={isConnectable && $dragging}
|
||||
/>
|
||||
{/if}
|
||||
<Handle type="source" position={Position.Right} {isConnectable} />
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={deleteModal}
|
||||
|
@ -222,7 +212,7 @@
|
|||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
/* Handlers */
|
||||
/* Handles */
|
||||
.node :global(.svelte-flow__handle) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
@ -235,34 +225,8 @@
|
|||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Bounds brackets */
|
||||
.bounds {
|
||||
height: calc(var(--height) + 2 * var(--spacing));
|
||||
position: absolute;
|
||||
width: calc(var(--spacing) / 1.5);
|
||||
border: 2px dashed var(--edge-color);
|
||||
border-right: none;
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing));
|
||||
transform: translateY(-50%);
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
.bounds.flip {
|
||||
left: calc(-1 * var(--spacing));
|
||||
transform: translateY(-50%) translateX(-100%) scale(-1, 1);
|
||||
}
|
||||
.bounds::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(-1 * var(--spacing));
|
||||
height: 0;
|
||||
border-top: 2px dashed var(--edge-color);
|
||||
width: var(--spacing);
|
||||
box-sizing: border-box;
|
||||
transform: translateY(-50%);
|
||||
.node:not(.custom) :global(.svelte-flow__handle) {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,5 +3,5 @@ export const MaxAutoZoom = 1.2
|
|||
export const GridResolution = 20
|
||||
export const NodeHeight = GridResolution * 3
|
||||
export const NodeWidth = GridResolution * 12
|
||||
export const NodeHSpacing = GridResolution * 4
|
||||
export const NodeHSpacing = GridResolution * 6
|
||||
export const NodeVSpacing = GridResolution * 2
|
||||
|
|
|
@ -8,19 +8,36 @@ import {
|
|||
} from "./constants"
|
||||
import { getNodesBounds, Position } from "@xyflow/svelte"
|
||||
import { Roles } from "constants/backend"
|
||||
import { roles } from "stores/builder"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
// Gets the position of the basic role
|
||||
export const getBasicPosition = bounds => ({
|
||||
x: bounds.x - NodeHSpacing - NodeWidth,
|
||||
x: bounds.x - GridResolution * 4 - NodeWidth,
|
||||
y: bounds.y + bounds.height / 2 - NodeHeight / 2,
|
||||
})
|
||||
|
||||
// Gets the position of the admin role
|
||||
export const getAdminPosition = bounds => ({
|
||||
x: bounds.x + bounds.width + NodeHSpacing,
|
||||
x: bounds.x + bounds.width + GridResolution * 4,
|
||||
y: bounds.y + bounds.height / 2 - NodeHeight / 2,
|
||||
})
|
||||
|
||||
// Filters out invalid nodes and edges
|
||||
const preProcessLayout = ({ nodes, edges }) => {
|
||||
const ignoredRoles = [Roles.PUBLIC, Roles.POWER]
|
||||
const edglessRoles = [...ignoredRoles, Roles.BASIC, Roles.ADMIN]
|
||||
return {
|
||||
nodes: nodes.filter(node => !ignoredRoles.includes(node.id)),
|
||||
edges: edges.filter(edge => {
|
||||
return (
|
||||
!edglessRoles.includes(edge.source) &&
|
||||
!edglessRoles.includes(edge.target)
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// Updates positions of nodes and edges into a nice graph structure
|
||||
export const dagreLayout = ({ nodes, edges }) => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
|
@ -46,33 +63,34 @@ export const dagreLayout = ({ nodes, edges }) => {
|
|||
y: Math.round((pos.y - NodeHeight / 2) / GridResolution) * GridResolution,
|
||||
}
|
||||
})
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
const postProcessLayout = ({ nodes, edges }) => {
|
||||
// Reposition basic and admin to bound the custom nodes
|
||||
const bounds = getNodesBounds(nodes.filter(node => node.data.custom))
|
||||
nodes.find(x => x.id === Roles.BASIC).position = getBasicPosition(bounds)
|
||||
nodes.find(x => x.id === Roles.ADMIN).position = getAdminPosition(bounds)
|
||||
|
||||
// Add custom edges for basic and admin brackets
|
||||
edges.push({
|
||||
id: "basic-bracket",
|
||||
source: Roles.BASIC,
|
||||
target: Roles.ADMIN,
|
||||
type: "bracket",
|
||||
})
|
||||
edges.push({
|
||||
id: "admin-bracket",
|
||||
source: Roles.ADMIN,
|
||||
target: Roles.BASIC,
|
||||
type: "bracket",
|
||||
})
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
// Adds additional edges as needed to the flow structure to ensure compatibility with BB role logic
|
||||
const sanitiseLayout = ({ nodes, edges }) => {
|
||||
const ignoredRoles = [Roles.PUBLIC, Roles.POWER]
|
||||
const edglessRoles = [...ignoredRoles, Roles.BASIC, Roles.ADMIN]
|
||||
return {
|
||||
nodes: nodes.filter(node => !ignoredRoles.includes(node.id)),
|
||||
edges: edges.filter(edge => {
|
||||
return (
|
||||
!edglessRoles.includes(edge.source) &&
|
||||
!edglessRoles.includes(edge.target)
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically lays out the graph, sanitising and enriching the structure
|
||||
export const autoLayout = ({ nodes, edges }) => {
|
||||
return dagreLayout(sanitiseLayout({ nodes, edges }))
|
||||
return postProcessLayout(dagreLayout(preProcessLayout({ nodes, edges })))
|
||||
}
|
||||
|
||||
// Converts a role doc into a node structure
|
||||
|
@ -99,27 +117,22 @@ export const roleToNode = role => {
|
|||
}
|
||||
|
||||
// Converts a node structure back into a role doc
|
||||
export const nodeToRole = node => {
|
||||
const role = $roles.find(x => x._id === node.id)
|
||||
const inherits = $edges.filter(x => x.target === node.id).map(x => x.source)
|
||||
// TODO save inherits array
|
||||
return {
|
||||
...role,
|
||||
inherits,
|
||||
uiMetadata: {
|
||||
displayName: node.data.displayName,
|
||||
color: node.data.color,
|
||||
description: node.data.description,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const nodeToRole = ({ node, edges }) => ({
|
||||
...get(roles).find(role => role._id === node.id),
|
||||
inherits: edges.filter(x => x.target === node.id).map(x => x.source),
|
||||
uiMetadata: {
|
||||
displayName: node.data.displayName,
|
||||
color: node.data.color,
|
||||
description: node.data.description,
|
||||
},
|
||||
})
|
||||
|
||||
// Builds a layout from an array of roles
|
||||
// Builds a default layout from an array of roles
|
||||
export const rolesToLayout = roles => {
|
||||
let nodes = []
|
||||
let edges = []
|
||||
|
||||
// Remove some builtins
|
||||
// Add all nodes and edges
|
||||
for (let role of roles) {
|
||||
// Add node for this role
|
||||
nodes.push(roleToNode(role))
|
||||
|
|
Loading…
Reference in New Issue