Skip to content
Open
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
25 changes: 25 additions & 0 deletions src/main/frontend/app/hooks/use-handle-types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { getHandleTypes } from './use-handle-types'

describe('getHandleTypes', () => {
it('returns empty array when typesAllowed is undefined', () => {
expect(getHandleTypes()).toEqual([])
})

it('returns empty array when typesAllowed is empty', () => {
expect(getHandleTypes({})).toEqual([])
})

it('returns all forwards as-is', () => {
const result = getHandleTypes({ success: {}, exception: {}, failure: {} })
expect(result).toContain('success')
expect(result).toContain('exception')
expect(result).toContain('failure')
})

it('maps wildcard * to custom', () => {
const result = getHandleTypes({ '*': {}, exception: {} })
expect(result).toContain('custom')
expect(result).toContain('exception')
expect(result).not.toContain('*')
})
})
23 changes: 6 additions & 17 deletions src/main/frontend/app/hooks/use-handle-types.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import { useMemo } from 'react'
import type { ElementProperty } from '@frankframework/doc-library-core'

export function useHandleTypes(typesAllowed?: Record<string, ElementProperty>) {
return useMemo(() => {
// Always include the 'success' handle, using a Set to avoid duplicates
const handles = new Set<string>(['success'])

if (!typesAllowed) return [...handles]

if ('*' in typesAllowed) {
handles.add('custom')
}
export function getHandleTypes(typesAllowed?: Record<string, ElementProperty>): string[] {
if (!typesAllowed) return []

for (const type of Object.keys(typesAllowed)) {
if (type !== '*') {
handles.add(type)
}
}
return Object.keys(typesAllowed).flatMap((type) => (type === '*' ? ['custom'] : [type]))
}

return [...handles]
}, [typesAllowed])
export function useHandleTypes(typesAllowed?: Record<string, ElementProperty>) {
return useMemo(() => getHandleTypes(typesAllowed), [typesAllowed])
}
30 changes: 20 additions & 10 deletions src/main/frontend/app/routes/studio/canvas/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@
import { NodeContextMenuContext, useNodeContextMenu } from './node-context-menu-context'
import StickyNoteComponent, { type StickyNote } from '~/routes/studio/canvas/nodetypes/sticky-note'
import useTabStore, { type TabData } from '~/stores/tab-store'
import { convertAdapterXmlToJson, getAdapterFromConfiguration } from '~/routes/studio/xml-to-json-parser'
import {
convertAdapterXmlToJson,
getAdapterFromConfiguration,
type ResolveForwards,
} from '~/routes/studio/xml-to-json-parser'
import { exportFlowToXml, replaceAdapterInXml } from '~/routes/studio/flow-to-xml-parser'
import useNodeContextStore from '~/stores/node-context-store'
import CreateNodeModal from '~/components/flow/create-node-modal'
import { useFFDoc } from '@frankframework/doc-library-react'
import type { ElementDetails } from '@frankframework/doc-library-core'
import { getDefaultSourceHandles, resolveForwardsWithInheritance } from '~/utils/frankdoc-utils'
import { useProjectStore } from '~/stores/project-store'
import {
clearConfigurationFileCache,
Expand Down Expand Up @@ -338,7 +343,7 @@
logApiError('Failed to save XML', error as Error)
setIdle()
}
}, [])

Check warning on line 346 in src/main/frontend/app/routes/studio/canvas/flow.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

React Hook useCallback has missing dependencies: 'setIdle', 'setSaved', and 'setSaving'. Either include them or remove the dependency array

const autosaveEnabled = useSettingsStore((s) => s.general.autoSave.enabled)
const autosaveDelay = useSettingsStore((s) => s.general.autoSave.delayMs)
Expand Down Expand Up @@ -992,6 +997,14 @@
[elements],
)

const resolveForwards = useCallback(
(subtype: string) => resolveForwardsWithInheritance(lookupFrankElement(subtype)),
[lookupFrankElement],
)

const importForwardsResolverRef = useRef<ResolveForwards>(resolveForwards)
importForwardsResolverRef.current = resolveForwards

const deselectOtherNodes = useCallback(
(nodeId: string) => {
const flowNodes = reactFlow.getNodes()
Expand Down Expand Up @@ -1220,7 +1233,7 @@
setParentId(null)
}

function addNodeAtPosition(

Check warning on line 1236 in src/main/frontend/app/routes/studio/canvas/flow.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed
position: { x: number; y: number },
elementName: string,
sourceInfo?: { nodeId: string | null; handleId: string | null; handleType: 'source' | 'target' | null },
Expand All @@ -1241,6 +1254,8 @@
const width = nodeType === 'exitNode' ? FlowConfig.EXIT_DEFAULT_WIDTH : FlowConfig.NODE_DEFAULT_WIDTH
const height = nodeType === 'exitNode' ? FlowConfig.EXIT_DEFAULT_HEIGHT : FlowConfig.NODE_MIN_HEIGHT

const sourceHandles = getDefaultSourceHandles(resolveForwards(elementName))

const newNode: FrankNodeType = {
id: newId.toString(),
position: {
Expand All @@ -1251,7 +1266,7 @@
subtype: elementName,
type: elementType,
name: ``,
sourceHandles: [{ type: 'success', index: 1 }],
sourceHandles,
children: [],
},
type: nodeType,
Expand Down Expand Up @@ -1487,7 +1502,7 @@
tab.adapterPosition,
)
if (!adapter) return
const adapterJson = await convertAdapterXmlToJson(adapter)
const adapterJson = await convertAdapterXmlToJson(adapter, importForwardsResolverRef.current)

flowStore.setEdges(adapterJson.edges)
flowStore.setNodes(adapterJson.nodes)
Expand Down Expand Up @@ -1733,10 +1748,7 @@
position={pendingCompactConnection.position}
onClose={() => setPendingCompactConnection(null)}
onSelect={handleCompactHandleSelect}
typesAllowed={
(elements as Record<string, ElementDetails> | null)?.[pendingCompactConnection.sourceNodeSubtype]
?.forwards
}
typesAllowed={resolveForwards(pendingCompactConnection.sourceNodeSubtype)}
/>
)}

Expand All @@ -1746,9 +1758,7 @@
position={pendingEdgeDrop.position}
onClose={() => setPendingEdgeDrop(null)}
onSelect={handleEdgeDropHandleSelect}
typesAllowed={
(elements as Record<string, ElementDetails> | null)?.[pendingEdgeDrop.sourceNodeSubtype]?.forwards
}
typesAllowed={resolveForwards(pendingEdgeDrop.sourceNodeSubtype)}
/>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,34 @@ interface HandleProperties {
typesAllowed?: Record<string, ElementProperty>
}

const HANDLE_TYPE_COLOURS: Record<string, string> = {
const SEMANTIC_COLOURS: Record<string, string> = {
success: '#68D250',
failure: '#E84E4E',
exception: '#424242',
timeout: '#F2A900',
error: '#ff7605ff',
default: '#1B97D1',
}

const GREEN_BAND_START = 95
const GREEN_BAND_SIZE = 70

function colourFromName(type: string): string {
let hash = 0
for (const character of type) {
hash = (hash * 31 + (character.codePointAt(0) ?? 0)) % (360 - GREEN_BAND_SIZE)
}
const hue = hash < GREEN_BAND_START ? hash : hash + GREEN_BAND_SIZE
return `hsl(${hue}, 65%, 52%)`
}

export function translateHandleTypeToColour(type: string): string {
const normalized = type.toLowerCase()

for (const [suffix, colour] of Object.entries(HANDLE_TYPE_COLOURS)) {
if (normalized.endsWith(suffix)) {
return colour
}
for (const [suffix, colour] of Object.entries(SEMANTIC_COLOURS)) {
if (normalized.endsWith(suffix)) return colour
}

return HANDLE_TYPE_COLOURS.default
return colourFromName(normalized)
}

export function CustomHandle(properties: Readonly<HandleProperties>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import { DeprecatedPopover } from './components/deprecated-popover'
import { showWarningToast } from '~/components/toast'
import { useHandleTypes } from '~/hooks/use-handle-types'
import { resolveForwardsWithInheritance } from '~/utils/frankdoc-utils'
import AddSubcomponentModal from '~/components/flow/add-subcomponent-modal'
import { useFrankConfigXsd } from '~/providers/frankconfig-xsd-provider'
import {
Expand Down Expand Up @@ -85,7 +86,13 @@
if (!elements) return null
const recordElements = elements as Record<string, ElementDetails>

return Object.values(recordElements).find((element) => element.name === properties.data.subtype) ?? null
const element = Object.values(recordElements).find((element) => element.name === properties.data.subtype) ?? null
if (!element) return element

return {
...element,
forwards: resolveForwardsWithInheritance(element),
}
}, [elements, properties.data.subtype])

const isDeprecated = frankElement?.deprecated
Expand Down Expand Up @@ -346,7 +353,7 @@

addChild(properties.id, child)
},
[

Check warning on line 356 in src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

React Hook useCallback has missing dependencies: 'setAttributes' and 'setNodeId'. Either include them or remove the dependency array
properties.id,
addChild,
setIsNewNode,
Expand Down
75 changes: 75 additions & 0 deletions src/main/frontend/app/routes/studio/xml-to-json-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { extractSourceHandles, type ResolveForwards } from './xml-to-json-parser'
import type { ElementProperty } from '@frankframework/doc-library-core'

function elementFromXml(xml: string): Element {
const document_ = new DOMParser().parseFromString(xml, 'text/xml')
return document_.documentElement
}

const FORWARDS: Record<string, Record<string, ElementProperty>> = {
EchoPipe: { success: {}, exception: {} },
IfPipe: { exception: {}, '*': {}, else: {} },
SwitchPipe: { exception: {}, notFound: {}, empty: {}, '*': {} },
}

const resolveForwards: ResolveForwards = (subtype) => FORWARDS[subtype]

describe('extractSourceHandles', () => {
it('creates a handle per <forward> for any element type', () => {
const element = elementFromXml(
'<IfPipe name="x"><forward name="exception" path="" /><forward name="then" path="" /><forward name="else" path="" /></IfPipe>',
)
expect(extractSourceHandles(element, resolveForwards)).toEqual([
{ type: 'exception', index: 1 },
{ type: 'then', index: 2 },
{ type: 'else', index: 3 },
])
})

it('does NOT add an implicit success handle to a routing pipe (IfPipe)', () => {
const element = elementFromXml(
'<IfPipe name="x"><forward name="exception" path="" /><forward name="then" path="" /><forward name="else" path="" /></IfPipe>',
)
const handles = extractSourceHandles(element, resolveForwards)
expect(handles.some((handle) => handle.type === 'success')).toBe(false)
})

it('adds the implicit success fall-through handle for a FixedForwardPipe subclass with only an exception forward', () => {
const element = elementFromXml('<EchoPipe name="x"><forward name="exception" path="" /></EchoPipe>')
expect(extractSourceHandles(element, resolveForwards)).toEqual([
{ type: 'exception', index: 1 },
{ type: 'success', index: 2 },
])
})

it('keeps an explicit success forward without duplicating it', () => {
const element = elementFromXml('<EchoPipe name="x"><forward name="success" path="" /></EchoPipe>')
expect(extractSourceHandles(element, resolveForwards)).toEqual([{ type: 'success', index: 1 }])
})

it('uses the FrankDoc default handle when no forwards are declared (EchoPipe -> success)', () => {
const element = elementFromXml('<EchoPipe name="x" />')
expect(extractSourceHandles(element, resolveForwards)).toEqual([{ type: 'success', index: 1 }])
})

it('uses the FrankDoc default handle when no forwards are declared (SwitchPipe -> first routing forward)', () => {
const element = elementFromXml('<SwitchPipe name="x" />')
expect(extractSourceHandles(element, resolveForwards)).toEqual([{ type: 'notFound', index: 1 }])
})

it('falls back to a single success handle when no FrankDoc resolver is provided', () => {
const element = elementFromXml('<SwitchPipe name="x" />')
expect(extractSourceHandles(element)).toEqual([{ type: 'success', index: 1 }])
})

it('keeps adding the implicit success handle without a resolver (legacy behaviour)', () => {
const element = elementFromXml(
'<IfPipe name="x"><forward name="then" path="" /><forward name="else" path="" /></IfPipe>',
)
expect(extractSourceHandles(element)).toEqual([
{ type: 'then', index: 1 },
{ type: 'else', index: 2 },
{ type: 'success', index: 3 },
])
})
})
Loading
Loading