Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/main/frontend/app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@
--palette-parameters: #1389ff;
--palette-monitoring: #00ff84;
--palette-transactionalstorages: #fdc300;

/* Label styling */
--label-components: #edfeea;
--label-category: #feeaf0;
--label-eip: #eaf0fe;
--label-type: #feeaf0;
--label-deprecated: #a8a8a8;

--label-border-components: #a5f994;
--label-border-category: #f994b3;
--label-border-eip: #94b3f9;
--label-border-type: #f994b3;
--label-border-deprecated: #444444;
}

[data-theme='dark'] {
Expand Down Expand Up @@ -104,6 +117,19 @@
--palette-parameters: #0d4a87;
--palette-monitoring: #01924c;
--palette-transactionalstorages: #9c7800;

/* Label styling */
--label-components: #20930c;
--label-category: #bf063e;
--label-eip: #123ea2;
--label-type: #bf063e;
--label-deprecated: #444444;

--label-border-components: #89be7e;
--label-border-category: #e37294;
--label-border-eip: #5174c7;
--label-border-type: #e37294;
--label-border-deprecated: #b6b6b6;
}
}

Expand Down
142 changes: 142 additions & 0 deletions src/main/frontend/app/routes/studio/context/element-hover-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import type { ElementDetails } from '~/types/ff-doc.types'
import { getFirstLabelGroup } from '~/utils/flow-utils'
import ExternalLinkIcon from '../../../../icons/solar/External Link.svg?react'

interface ElementHoverCardProps {
anchorRect: DOMRect
element: ElementDetails
isLocked: boolean
onComplete?: () => void
onUnlock?: () => void
}

export default function ElementHoverCard({
anchorRect,
element,
isLocked,
onComplete,
onUnlock,
}: ElementHoverCardProps) {
const ref = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 })
const [fillWidth, setFillWidth] = useState(0)
const offset = 10 // distance between anchor and tooltip
const [labelGroup, label] = getFirstLabelGroup(element.labels)
const route = element.labels ? [labelGroup, label, element.name].join('/') : element.className
const frankdocUrl = `https://frankdoc.frankframework.org/#/${route}`

useLayoutEffect(() => {
if (!ref.current || !anchorRect) return

const tooltip = ref.current
const tooltipHeight = tooltip.offsetHeight
const tooltipWidth = tooltip.offsetWidth

const viewportHeight = window.innerHeight
const margin = 8 // space from top/bottom of screen

// Desired centered position
const centeredTop = anchorRect.top + anchorRect.height / 2 - tooltipHeight / 2

// Clamp within viewport
const clampedTop = Math.min(Math.max(centeredTop, margin), viewportHeight - tooltipHeight - margin)

const left = anchorRect.left - tooltipWidth - offset

setPosition({
top: clampedTop,
left,
})
}, [anchorRect])

useEffect(() => {
const timeout = setTimeout(() => {
setFillWidth(100)
}, 50)
return () => clearTimeout(timeout)
}, [onComplete])

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (!isLocked) return
if (!ref.current) return

if (!ref.current.contains(event.target as Node)) {
onUnlock?.()
}
}

document.addEventListener('pointerdown', handleClickOutside, true)

return () => {
document.removeEventListener('pointerdown', handleClickOutside, true)
}
}, [isLocked, onUnlock])

return createPortal(
<div
ref={ref}
style={{
position: 'fixed',
top: position.top,
left: position.left - offset,
pointerEvents: isLocked ? 'auto' : 'none',
zIndex: 50,
}}
className="border-border bg-background flex max-h-[70vh] max-w-[40vw] flex-col rounded-md border text-sm shadow-lg"
>
<div className="bg-backdrop h-[4px] w-full flex-shrink-0">
<div
className="bg-foreground-active h-full transition-all duration-1000 ease-linear"
style={{ width: `${fillWidth}%` }}
onTransitionEnd={() => {
if (fillWidth === 100) {
onComplete?.()
}
}}
/>
</div>
<div className="flex-1 overflow-x-auto overflow-y-auto p-5">
<p className="text-foreground-muted">{element.labels.Components}</p>
<h2 className="mb-2 font-semibold">
<a
href={frankdocUrl}
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground-active hover:underline"
>
{element.name}
<ExternalLinkIcon className="ml-3 inline h-4 w-4" />
</a>
</h2>
<div className="py-2">
{element.deprecated && (
<span
key={'deprecated'}
className="mr-1 rounded-full border border-[var(--label-border-deprecated)] bg-[var(--label-deprecated)] p-2"
>
Deprecated
</span>
)}
{element.labels &&
Object.entries(element.labels).map(([key, value]) => (
<span
key={key}
className={'mr-1 rounded-full border p-2'}
style={{
backgroundColor: `var(--label-${key.toLowerCase()})`,
borderColor: `var(--label-border-${key.toLowerCase()})`,
}}
>
{value}
</span>
))}
</div>
{element.description && <p className="text-sm" dangerouslySetInnerHTML={{ __html: element.description }} />}
</div>
</div>,
document.body,
)
}
44 changes: 41 additions & 3 deletions src/main/frontend/app/routes/studio/context/sorted-elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ElementDetails } from '@frankframework/ff-doc'
import { getElementTypeFromName } from '../node-translator-module'
import DangerIcon from '../../../../icons/solar/Danger Triangle.svg?react'
import { DeprecatedListPopover } from './deprecated-list-popover'
import ElementHoverCard from './element-hover-card'

interface Properties {
type: string
Expand All @@ -19,6 +20,9 @@ export default function SortedElements({ type, items, onDragStart, searchTerm }:
const [isExpanded, setIsExpanded] = useState(false)
const gradientEnabled = useSettingsStore((state) => state.studio.gradient)
const { setDraggedName } = useNodeContextStore((state) => state)
const [hoveredRect, setHoveredRect] = useState<DOMRect | null>(null)
const [hoveredElement, setHoveredElement] = useState<ElementDetails | null>(null)
const [lockedElement, setLockedElement] = useState<ElementDetails | null>(null)

const toggleExpansion = () => {
setIsExpanded(!isExpanded)
Expand All @@ -38,8 +42,12 @@ export default function SortedElements({ type, items, onDragStart, searchTerm }:
onClick={toggleExpansion}
className="text-foreground-muted hover:text-foreground-active flex w-full cursor-pointer items-center gap-1 text-left text-sm font-semibold capitalize"
>
{shouldExpand ? <ArrowDownIcon className="fill-current" /> : <ArrowRightIcon className="fill-current" />}
{type}
{shouldExpand ? (
<ArrowDownIcon className="h-4 w-4 fill-current" />
) : (
<ArrowRightIcon className="h-4 w-4 fill-current" />
)}
<span className="min-w-0 truncate">{type}</span>
</button>

{shouldExpand && (
Expand All @@ -52,8 +60,24 @@ export default function SortedElements({ type, items, onDragStart, searchTerm }:
key={value.name}
className="border-border m-2 flex cursor-move items-center justify-between rounded border p-4"
draggable
onDragStart={onDragStart(value)}
onDragStart={(event) => {
setHoveredRect(null)
setHoveredElement(null)
setLockedElement(null)
onDragStart(value)(event)
}}
onDragEnd={() => setDraggedName(null)}
onMouseEnter={(event) => {
setLockedElement(null) // unlock previous element
const rect = event.currentTarget.getBoundingClientRect()
setHoveredRect(rect)
setHoveredElement(value)
}}
onMouseLeave={() => {
if (lockedElement?.name === value.name) return
setHoveredElement(null)
setHoveredRect(null)
}}
style={{
background: gradientEnabled
? `radial-gradient(
Expand All @@ -79,6 +103,20 @@ export default function SortedElements({ type, items, onDragStart, searchTerm }:
})}
</div>
)}
{(hoveredRect && hoveredElement) || lockedElement ? (
<ElementHoverCard
key={(lockedElement ?? hoveredElement)!.name}
anchorRect={hoveredRect!}
element={lockedElement ?? hoveredElement!}
isLocked={!!lockedElement}
onComplete={() => setLockedElement(hoveredElement)}
onUnlock={() => {
setLockedElement(null)
setHoveredElement(null)
setHoveredRect(null)
}}
/>
) : null}
</div>
)
}
9 changes: 9 additions & 0 deletions src/main/frontend/app/utils/flow-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ export function cloneWithRemappedIds<T>(value: T, idMap: Map<string, string>, ge
return value
}

// Helper function from frank-doc https://github.com/frankframework/frank-doc/blob/master/frank-doc-frontend/src/app/app.service.ts
export function getFirstLabelGroup(filters: Record<string, string> | undefined): [string, string] {
const defaultLabelGroup: [string, string] = ['-', '-']
if (!filters) return defaultLabelGroup
const labelGroups = Object.entries(filters)
if (labelGroups.length === 0) return defaultLabelGroup
return labelGroups[0]
}

/** Converts the tagname of a non capitalized element that has a classname attribute to the last part of said classname, e.g.:
* <pipe name="uploadFiles" className="org.frankframework.pipes.ForEachChildElementPipe" />
* Becomes <ForEachChildElementPipe name="uploadFiles" />
Expand Down
1 change: 1 addition & 0 deletions src/main/frontend/icons/solar/External Link.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading