Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
D
dify
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tech
dify
Commits
8b2a63e5
Commit
8b2a63e5
authored
Mar 14, 2024
by
StyleZhang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
ae9e7acd
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
130 additions
and
36 deletions
+130
-36
page.tsx
...onLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx
+42
-1
use-edges-interactions.ts
web/app/components/workflow/hooks/use-edges-interactions.ts
+33
-8
use-nodes-interactions.ts
web/app/components/workflow/hooks/use-nodes-interactions.ts
+25
-10
index.tsx
web/app/components/workflow/index.tsx
+1
-1
node-handle.tsx
...omponents/workflow/nodes/_base/components/node-handle.tsx
+10
-14
node.tsx
web/app/components/workflow/nodes/_base/node.tsx
+1
-0
zoom-in-out.tsx
web/app/components/workflow/operator/zoom-in-out.tsx
+4
-1
types.ts
web/app/components/workflow/types.ts
+2
-0
utils.ts
web/app/components/workflow/utils.ts
+12
-1
No files found.
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx
View file @
8b2a63e5
...
@@ -3,10 +3,51 @@
...
@@ -3,10 +3,51 @@
import
{
memo
}
from
'react'
import
{
memo
}
from
'react'
import
Workflow
from
'@/app/components/workflow'
import
Workflow
from
'@/app/components/workflow'
// export function createNodesAndEdges(xNodes = 10, yNodes = 10) {
// const nodes = []
// const edges = []
// let nodeId = 1
// let recentNodeId = null
// for (let y = 0; y < yNodes; y++) {
// for (let x = 0; x < xNodes; x++) {
// const position = { x: x * 200, y: y * 50 }
// const node = {
// id: `stress-${nodeId.toString()}`,
// type: 'custom',
// data: { type: 'start', title: '开始', variables: [] },
// position,
// }
// nodes.push(node)
// if (recentNodeId && nodeId <= xNodes * yNodes) {
// edges.push({
// id: `${x}-${y}`,
// type: 'custom',
// source: `stress-${recentNodeId.toString()}`,
// target: `stress-${nodeId.toString()}`,
// })
// }
// recentNodeId = nodeId
// nodeId++
// }
// }
// return { nodes, edges }
// }
const
Page
=
()
=>
{
const
Page
=
()
=>
{
// const {
// nodes,
// edges,
// } = createNodesAndEdges()
return
(
return
(
<
div
className=
'w-full h-full overflow-x-auto'
>
<
div
className=
'w-full h-full overflow-x-auto'
>
<
Workflow
/>
<
Workflow
// nodes={nodes}
// edges={edges}
/>
</
div
>
</
div
>
)
)
}
}
...
...
web/app/components/workflow/hooks/use-edges-interactions.ts
View file @
8b2a63e5
...
@@ -6,6 +6,7 @@ import type {
...
@@ -6,6 +6,7 @@ import type {
}
from
'reactflow'
}
from
'reactflow'
import
{
useStoreApi
}
from
'reactflow'
import
{
useStoreApi
}
from
'reactflow'
import
{
useStore
}
from
'../store'
import
{
useStore
}
from
'../store'
import
type
{
Node
}
from
'../types'
import
{
useNodesSyncDraft
}
from
'./use-nodes-sync-draft'
import
{
useNodesSyncDraft
}
from
'./use-nodes-sync-draft'
export
const
useEdgesInteractions
=
()
=>
{
export
const
useEdgesInteractions
=
()
=>
{
...
@@ -55,14 +56,26 @@ export const useEdgesInteractions = () => {
...
@@ -55,14 +56,26 @@ export const useEdgesInteractions = () => {
return
return
const
{
const
{
getNodes
,
setNodes
,
edges
,
edges
,
setEdges
,
setEdges
,
}
=
store
.
getState
()
}
=
store
.
getState
()
const
newEdges
=
produce
(
edges
,
(
draft
)
=>
{
const
currentEdgeIndex
=
edges
.
findIndex
(
edge
=>
edge
.
source
===
nodeId
&&
edge
.
sourceHandle
===
branchId
)
const
index
=
draft
.
findIndex
(
edge
=>
edge
.
source
===
nodeId
&&
edge
.
sourceHandle
===
branchId
)
const
currentEdge
=
edges
[
currentEdgeIndex
]
const
newNodes
=
produce
(
getNodes
(),
(
draft
:
Node
[])
=>
{
const
sourceNode
=
draft
.
find
(
node
=>
node
.
id
===
currentEdge
.
source
)
const
targetNode
=
draft
.
find
(
node
=>
node
.
id
===
currentEdge
.
target
)
if
(
sourceNode
)
sourceNode
.
data
.
_connectedSourceHandleIds
=
sourceNode
.
data
.
_connectedSourceHandleIds
?.
filter
(
handleId
=>
handleId
!==
currentEdge
.
sourceHandle
)
if
(
index
>
-
1
)
if
(
targetNode
)
draft
.
splice
(
index
,
1
)
targetNode
.
data
.
_connectedTargetHandleIds
=
targetNode
.
data
.
_connectedTargetHandleIds
?.
filter
(
handleId
=>
handleId
!==
currentEdge
.
targetHandle
)
})
setNodes
(
newNodes
)
const
newEdges
=
produce
(
edges
,
(
draft
)
=>
{
draft
.
splice
(
currentEdgeIndex
,
1
)
})
})
setEdges
(
newEdges
)
setEdges
(
newEdges
)
handleSyncWorkflowDraft
()
handleSyncWorkflowDraft
()
...
@@ -75,14 +88,26 @@ export const useEdgesInteractions = () => {
...
@@ -75,14 +88,26 @@ export const useEdgesInteractions = () => {
return
return
const
{
const
{
getNodes
,
setNodes
,
edges
,
edges
,
setEdges
,
setEdges
,
}
=
store
.
getState
()
}
=
store
.
getState
()
const
newEdges
=
produce
(
edges
,
(
draft
)
=>
{
const
currentEdgeIndex
=
edges
.
findIndex
(
edge
=>
edge
.
selected
)
const
index
=
draft
.
findIndex
(
edge
=>
edge
.
selected
)
const
currentEdge
=
edges
[
currentEdgeIndex
]
const
newNodes
=
produce
(
getNodes
(),
(
draft
:
Node
[])
=>
{
const
sourceNode
=
draft
.
find
(
node
=>
node
.
id
===
currentEdge
.
source
)
const
targetNode
=
draft
.
find
(
node
=>
node
.
id
===
currentEdge
.
target
)
if
(
sourceNode
)
sourceNode
.
data
.
_connectedSourceHandleIds
=
sourceNode
.
data
.
_connectedSourceHandleIds
?.
filter
(
handleId
=>
handleId
!==
currentEdge
.
sourceHandle
)
if
(
index
>
-
1
)
if
(
targetNode
)
draft
.
splice
(
index
,
1
)
targetNode
.
data
.
_connectedTargetHandleIds
=
targetNode
.
data
.
_connectedTargetHandleIds
?.
filter
(
handleId
=>
handleId
!==
currentEdge
.
targetHandle
)
})
setNodes
(
newNodes
)
const
newEdges
=
produce
(
edges
,
(
draft
)
=>
{
draft
.
splice
(
currentEdgeIndex
,
1
)
})
})
setEdges
(
newEdges
)
setEdges
(
newEdges
)
handleSyncWorkflowDraft
()
handleSyncWorkflowDraft
()
...
...
web/app/components/workflow/hooks/use-nodes-interactions.ts
View file @
8b2a63e5
...
@@ -260,9 +260,8 @@ export const useNodesInteractions = () => {
...
@@ -260,9 +260,8 @@ export const useNodesInteractions = () => {
edges
,
edges
,
setEdges
,
setEdges
,
}
=
store
.
getState
()
}
=
store
.
getState
()
const
newEdges
=
produce
(
edges
,
(
draft
)
=>
{
const
newEdges
=
produce
(
edges
,
(
draft
)
=>
{
const
filtered
=
draft
.
filter
(
edge
=>
edge
.
source
!==
source
&&
edge
.
target
!==
target
)
const
filtered
=
draft
.
filter
(
edge
=>
(
edge
.
source
!==
source
&&
edge
.
sourceHandle
!==
sourceHandle
)
||
(
edge
.
target
!==
target
&&
edge
.
targetHandle
!==
targetHandle
)
)
filtered
.
push
({
filtered
.
push
({
id
:
`
${
source
}
-
${
target
}
`
,
id
:
`
${
source
}
-
${
target
}
`
,
...
@@ -292,14 +291,25 @@ export const useNodesInteractions = () => {
...
@@ -292,14 +291,25 @@ export const useNodesInteractions = () => {
setEdges
,
setEdges
,
}
=
store
.
getState
()
}
=
store
.
getState
()
const
newNodes
=
produce
(
getNodes
(),
(
draft
)
=>
{
const
nodes
=
getNodes
()
const
index
=
draft
.
findIndex
(
node
=>
node
.
id
===
nodeId
)
const
currentNodeIndex
=
nodes
.
findIndex
(
node
=>
node
.
id
===
nodeId
)
const
currentNode
=
nodes
[
currentNodeIndex
]
const
incomersIds
=
getIncomers
(
currentNode
,
nodes
,
edges
).
map
(
incomer
=>
incomer
.
id
)
const
outgoersIds
=
getOutgoers
(
currentNode
,
nodes
,
edges
).
map
(
outgoer
=>
outgoer
.
id
)
const
connectedEdges
=
getConnectedEdges
([{
id
:
nodeId
}
as
Node
],
edges
)
const
sourceEdgesHandleIds
=
connectedEdges
.
filter
(
edge
=>
edge
.
target
===
nodeId
).
map
(
edge
=>
edge
.
sourceHandle
)
const
targetEdgesHandleIds
=
connectedEdges
.
filter
(
edge
=>
edge
.
source
===
nodeId
).
map
(
edge
=>
edge
.
targetHandle
)
const
newNodes
=
produce
(
nodes
,
(
draft
:
Node
[])
=>
{
draft
.
forEach
((
node
)
=>
{
if
(
incomersIds
.
includes
(
node
.
id
))
node
.
data
.
_connectedSourceHandleIds
=
node
.
data
.
_connectedSourceHandleIds
?.
filter
(
handleId
=>
!
sourceEdgesHandleIds
.
includes
(
handleId
))
if
(
index
>
-
1
)
if
(
outgoersIds
.
includes
(
node
.
id
))
draft
.
splice
(
index
,
1
)
node
.
data
.
_connectedTargetHandleIds
=
node
.
data
.
_connectedTargetHandleIds
?.
filter
(
handleId
=>
!
targetEdgesHandleIds
.
includes
(
handleId
))
})
draft
.
splice
(
currentNodeIndex
,
1
)
})
})
setNodes
(
newNodes
)
setNodes
(
newNodes
)
const
connectedEdges
=
getConnectedEdges
([{
id
:
nodeId
}
as
Node
],
edges
)
const
newEdges
=
produce
(
edges
,
(
draft
)
=>
{
const
newEdges
=
produce
(
edges
,
(
draft
)
=>
{
return
draft
.
filter
(
edge
=>
!
connectedEdges
.
find
(
connectedEdge
=>
connectedEdge
.
id
===
edge
.
id
))
return
draft
.
filter
(
edge
=>
!
connectedEdges
.
find
(
connectedEdge
=>
connectedEdge
.
id
===
edge
.
id
))
})
})
...
@@ -325,7 +335,8 @@ export const useNodesInteractions = () => {
...
@@ -325,7 +335,8 @@ export const useNodesInteractions = () => {
setEdges
,
setEdges
,
}
=
store
.
getState
()
}
=
store
.
getState
()
const
nodes
=
getNodes
()
const
nodes
=
getNodes
()
const
currentNode
=
nodes
.
find
(
node
=>
node
.
id
===
currentNodeId
)
!
const
currentNodeIndex
=
nodes
.
findIndex
(
node
=>
node
.
id
===
currentNodeId
)
const
currentNode
=
nodes
[
currentNodeIndex
]
const
outgoers
=
getOutgoers
(
currentNode
,
nodes
,
edges
).
sort
((
a
,
b
)
=>
a
.
position
.
y
-
b
.
position
.
y
)
const
outgoers
=
getOutgoers
(
currentNode
,
nodes
,
edges
).
sort
((
a
,
b
)
=>
a
.
position
.
y
-
b
.
position
.
y
)
const
lastOutgoer
=
outgoers
[
outgoers
.
length
-
1
]
const
lastOutgoer
=
outgoers
[
outgoers
.
length
-
1
]
const
nextNode
:
Node
=
{
const
nextNode
:
Node
=
{
...
@@ -334,6 +345,7 @@ export const useNodesInteractions = () => {
...
@@ -334,6 +345,7 @@ export const useNodesInteractions = () => {
data
:
{
data
:
{
...
nodesInitialData
[
nodeType
],
...
nodesInitialData
[
nodeType
],
...(
toolDefaultValue
||
{}),
...(
toolDefaultValue
||
{}),
_connectedTargetHandleIds
:
[
'target'
],
selected
:
true
,
selected
:
true
,
},
},
position
:
{
position
:
{
...
@@ -350,9 +362,12 @@ export const useNodesInteractions = () => {
...
@@ -350,9 +362,12 @@ export const useNodesInteractions = () => {
target
:
nextNode
.
id
,
target
:
nextNode
.
id
,
targetHandle
:
'target'
,
targetHandle
:
'target'
,
}
}
const
newNodes
=
produce
(
nodes
,
(
draft
)
=>
{
const
newNodes
=
produce
(
nodes
,
(
draft
:
Node
[]
)
=>
{
draft
.
forEach
((
node
)
=>
{
draft
.
forEach
((
node
,
index
)
=>
{
node
.
data
.
selected
=
false
node
.
data
.
selected
=
false
if
(
index
===
currentNodeIndex
)
node
.
data
.
_connectedSourceHandleIds
?.
push
(
sourceHandle
)
})
})
draft
.
push
(
nextNode
)
draft
.
push
(
nextNode
)
})
})
...
...
web/app/components/workflow/index.tsx
View file @
8b2a63e5
...
@@ -182,7 +182,7 @@ const WorkflowWrap: FC<WorkflowProps> = ({
...
@@ -182,7 +182,7 @@ const WorkflowWrap: FC<WorkflowProps> = ({
return
nodes
return
nodes
if
(
data
)
if
(
data
)
return
initialNodes
(
data
.
graph
.
nodes
)
return
initialNodes
(
data
.
graph
.
nodes
,
data
.
graph
.
edges
)
return
[
startNode
]
return
[
startNode
]
},
[
data
,
nodes
,
startNode
])
},
[
data
,
nodes
,
startNode
])
...
...
web/app/components/workflow/nodes/_base/components/node-handle.tsx
View file @
8b2a63e5
import
type
{
MouseEvent
}
from
'react'
import
type
{
MouseEvent
}
from
'react'
import
{
import
{
memo
,
useCallback
,
useCallback
,
useEffect
,
useEffect
,
useState
,
useState
,
}
from
'react'
}
from
'react'
import
type
{
NodeProps
}
from
'reactflow'
import
{
import
{
Handle
,
Handle
,
Position
,
Position
,
getConnectedEdges
,
useEdges
,
}
from
'reactflow'
}
from
'reactflow'
import
{
BlockEnum
}
from
'../../../types'
import
{
BlockEnum
}
from
'../../../types'
import
type
{
Node
}
from
'../../../types'
import
type
{
Node
}
from
'../../../types'
...
@@ -22,9 +20,9 @@ type NodeHandleProps = {
...
@@ -22,9 +20,9 @@ type NodeHandleProps = {
handleId
:
string
handleId
:
string
handleClassName
?:
string
handleClassName
?:
string
nodeSelectorClassName
?:
string
nodeSelectorClassName
?:
string
}
&
Pick
<
Node
Props
,
'id'
|
'data'
>
}
&
Pick
<
Node
,
'id'
|
'data'
>
export
const
NodeTargetHandle
=
({
export
const
NodeTargetHandle
=
memo
(
({
id
,
id
,
data
,
data
,
handleId
,
handleId
,
...
@@ -33,9 +31,7 @@ export const NodeTargetHandle = ({
...
@@ -33,9 +31,7 @@ export const NodeTargetHandle = ({
}:
NodeHandleProps
)
=>
{
}:
NodeHandleProps
)
=>
{
const
[
open
,
setOpen
]
=
useState
(
false
)
const
[
open
,
setOpen
]
=
useState
(
false
)
const
{
handleNodeAddPrev
}
=
useNodesInteractions
()
const
{
handleNodeAddPrev
}
=
useNodesInteractions
()
const
edges
=
useEdges
()
const
connected
=
data
.
_connectedTargetHandleIds
?.
includes
(
handleId
)
const
connectedEdges
=
getConnectedEdges
([{
id
}
as
Node
],
edges
)
const
connected
=
connectedEdges
.
find
(
edge
=>
edge
.
targetHandle
===
handleId
&&
edge
.
target
===
id
)
const
handleOpenChange
=
useCallback
((
v
:
boolean
)
=>
{
const
handleOpenChange
=
useCallback
((
v
:
boolean
)
=>
{
setOpen
(
v
)
setOpen
(
v
)
...
@@ -85,9 +81,10 @@ export const NodeTargetHandle = ({
...
@@ -85,9 +81,10 @@ export const NodeTargetHandle = ({
</
Handle
>
</
Handle
>
</>
</>
)
)
}
})
NodeTargetHandle
.
displayName
=
'NodeTargetHandle'
export
const
NodeSourceHandle
=
({
export
const
NodeSourceHandle
=
memo
(
({
id
,
id
,
data
,
data
,
handleId
,
handleId
,
...
@@ -97,9 +94,7 @@ export const NodeSourceHandle = ({
...
@@ -97,9 +94,7 @@ export const NodeSourceHandle = ({
const
notInitialWorkflow
=
useStore
(
s
=>
s
.
notInitialWorkflow
)
const
notInitialWorkflow
=
useStore
(
s
=>
s
.
notInitialWorkflow
)
const
[
open
,
setOpen
]
=
useState
(
false
)
const
[
open
,
setOpen
]
=
useState
(
false
)
const
{
handleNodeAddNext
}
=
useNodesInteractions
()
const
{
handleNodeAddNext
}
=
useNodesInteractions
()
const
edges
=
useEdges
()
const
connected
=
data
.
_connectedSourceHandleIds
?.
includes
(
handleId
)
const
connectedEdges
=
getConnectedEdges
([{
id
}
as
Node
],
edges
)
const
connected
=
connectedEdges
.
find
(
edge
=>
edge
.
sourceHandle
===
handleId
&&
edge
.
source
===
id
)
const
handleOpenChange
=
useCallback
((
v
:
boolean
)
=>
{
const
handleOpenChange
=
useCallback
((
v
:
boolean
)
=>
{
setOpen
(
v
)
setOpen
(
v
)
},
[])
},
[])
...
@@ -150,4 +145,5 @@ export const NodeSourceHandle = ({
...
@@ -150,4 +145,5 @@ export const NodeSourceHandle = ({
</
Handle
>
</
Handle
>
</>
</>
)
)
}
})
NodeSourceHandle
.
displayName
=
'NodeSourceHandle'
web/app/components/workflow/nodes/_base/node.tsx
View file @
8b2a63e5
...
@@ -47,6 +47,7 @@ const BaseNode: FC<BaseNodeProps> = ({
...
@@ -47,6 +47,7 @@ const BaseNode: FC<BaseNodeProps> = ({
${data._runningStatus === NodeRunningStatus.Running && '!border-primary-500'}
${data._runningStatus === NodeRunningStatus.Running && '!border-primary-500'}
${data._runningStatus === NodeRunningStatus.Succeeded && '!border-[#12B76A]'}
${data._runningStatus === NodeRunningStatus.Succeeded && '!border-[#12B76A]'}
${data._runningStatus === NodeRunningStatus.Failed && '!border-[#F04438]'}
${data._runningStatus === NodeRunningStatus.Failed && '!border-[#F04438]'}
${data._runningStatus === NodeRunningStatus.Waiting && 'opacity-70'}
`
}
`
}
>
>
{
{
...
...
web/app/components/workflow/operator/zoom-in-out.tsx
View file @
8b2a63e5
...
@@ -94,7 +94,10 @@ const ZoomInOut: FC = () => {
...
@@ -94,7 +94,10 @@ const ZoomInOut: FC = () => {
placement=
'top-start'
placement=
'top-start'
open=
{
open
}
open=
{
open
}
onOpenChange=
{
setOpen
}
onOpenChange=
{
setOpen
}
offset=
{
4
}
offset=
{
{
mainAxis
:
4
,
crossAxis
:
-
2
,
}
}
>
>
<
PortalToFollowElemTrigger
asChild
onClick=
{
handleTrigger
}
>
<
PortalToFollowElemTrigger
asChild
onClick=
{
handleTrigger
}
>
<
div
className=
{
`
<
div
className=
{
`
...
...
web/app/components/workflow/types.ts
View file @
8b2a63e5
...
@@ -25,6 +25,8 @@ export type Branch = {
...
@@ -25,6 +25,8 @@ export type Branch = {
}
}
export
type
CommonNodeType
<
T
=
{}
>
=
{
export
type
CommonNodeType
<
T
=
{}
>
=
{
_connectedSourceHandleIds
?:
string
[]
_connectedTargetHandleIds
?:
string
[]
_targetBranches
?:
Branch
[]
_targetBranches
?:
Branch
[]
_isSingleRun
?:
boolean
_isSingleRun
?:
boolean
_runningStatus
?:
NodeRunningStatus
_runningStatus
?:
NodeRunningStatus
...
...
web/app/components/workflow/utils.ts
View file @
8b2a63e5
...
@@ -80,10 +80,14 @@ export const nodesLevelOrderTraverse = (
...
@@ -80,10 +80,14 @@ export const nodesLevelOrderTraverse = (
}
}
}
}
export
const
initialNodes
=
(
nodes
:
Node
[])
=>
{
export
const
initialNodes
=
(
nodes
:
Node
[]
,
edges
:
Edge
[]
)
=>
{
return
nodes
.
map
((
node
)
=>
{
return
nodes
.
map
((
node
)
=>
{
node
.
type
=
'custom'
node
.
type
=
'custom'
const
connectedEdges
=
getConnectedEdges
([
node
],
edges
)
node
.
data
.
_connectedSourceHandleIds
=
connectedEdges
.
filter
(
edge
=>
edge
.
source
===
node
.
id
).
map
(
edge
=>
edge
.
sourceHandle
||
'source'
)
node
.
data
.
_connectedTargetHandleIds
=
connectedEdges
.
filter
(
edge
=>
edge
.
target
===
node
.
id
).
map
(
edge
=>
edge
.
targetHandle
||
'target'
)
if
(
node
.
data
.
type
===
BlockEnum
.
IfElse
)
{
if
(
node
.
data
.
type
===
BlockEnum
.
IfElse
)
{
node
.
data
.
_targetBranches
=
[
node
.
data
.
_targetBranches
=
[
{
{
...
@@ -170,3 +174,10 @@ export const canRunBySingle = (nodeType: BlockEnum) => {
...
@@ -170,3 +174,10 @@ export const canRunBySingle = (nodeType: BlockEnum) => {
||
nodeType
===
BlockEnum
.
HttpRequest
||
nodeType
===
BlockEnum
.
HttpRequest
||
nodeType
===
BlockEnum
.
Tool
||
nodeType
===
BlockEnum
.
Tool
}
}
type
ConnectedSourceOrTargetNodesChange
=
{
type
:
'add'
|
'remove'
edge
:
Edge
}[]
export
const
getConnectedSourceOrTargetNodesChangeList
=
(
changes
:
ConnectedSourceOrTargetNodesChange
,
nodes
:
Node
[],
edges
:
Edge
[])
=>
{
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment