@live-change/flow-frontend 0.9.226 → 0.9.228
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/README.md +33 -0
- package/e2e/calculation.test.ts +247 -0
- package/e2e/e2eSuite.ts +8 -0
- package/e2e/env.ts +86 -0
- package/e2e/kitchen-sink.test.ts +85 -0
- package/e2e/runner.ts +10 -0
- package/e2e/withBrowser.ts +5 -0
- package/front/components.d.ts +25 -0
- package/front/locales/en.js +27 -0
- package/front/locales/en.json +5 -0
- package/front/src/App.vue +38 -12
- package/front/src/NavBar.vue +31 -0
- package/front/src/components/Edge.vue +1 -1
- package/front/src/components/EdgeEndHandle.vue +1 -1
- package/front/src/components/Flow.vue +5 -4
- package/front/src/components/Node.vue +1 -1
- package/front/src/components/NodeHandle.vue +1 -1
- package/front/src/components/NodePort.vue +1 -2
- package/front/src/components/index.js +10 -10
- package/front/src/config.js +90 -0
- package/front/src/demo/DemoEdge.vue +38 -0
- package/front/src/demo/DemoNodeCard.vue +86 -0
- package/front/src/demo/DemoPort.vue +34 -0
- package/front/src/demo/EnrichNode.vue +37 -0
- package/front/src/demo/FlowKitchenSink.vue +165 -0
- package/front/src/demo/OutputNode.vue +45 -0
- package/front/src/demo/SourceNode.vue +49 -0
- package/front/src/demo/calculation/AddNode.vue +35 -0
- package/front/src/demo/calculation/ConstantNode.vue +61 -0
- package/front/src/demo/calculation/FlowCalculationDemo.vue +300 -0
- package/front/src/demo/calculation/MultiplyNode.vue +35 -0
- package/front/src/demo/calculation/PowerNode.vue +62 -0
- package/front/src/demo/flowTestId.js +3 -0
- package/front/src/demo/runDemoGraph.js +76 -0
- package/front/src/demo/seedGraph.js +38 -0
- package/front/src/entry-client.js +2 -2
- package/front/src/entry-server.js +2 -1
- package/front/src/router.js +20 -12
- package/package.json +44 -37
- package/server/app.config.js +61 -0
- package/server/calculation/calculation.js +20 -0
- package/server/calculation/config.js +15 -0
- package/server/calculation/definition.js +11 -0
- package/server/calculation/index.js +12 -0
- package/server/calculation/ops.js +69 -0
- package/server/calculation/run.js +69 -0
- package/server/calculation/runGraph.js +85 -0
- package/server/calculation/tests/run.test.js +73 -0
- package/server/calculation/triggers.js +95 -0
- package/server/calculation/wire.js +30 -0
- package/server/init.js +1 -6
- package/server/services.list.js +11 -0
- package/server/start.js +39 -0
- package/server/services.config.js +0 -26
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DemoNodeCard :node="node" title="Source" icon="pi-database" @delete="deleteNode">
|
|
3
|
+
<div class="demo-node-section">Sample record</div>
|
|
4
|
+
<div class="flex items-center p-1">
|
|
5
|
+
<Textarea v-model="recordJson" class="w-full text-sm mx-2" rows="6" autoResize />
|
|
6
|
+
<DemoPort :x="1" :node="node" portId="out" />
|
|
7
|
+
</div>
|
|
8
|
+
<div v-if="jsonError" class="text-red-600 text-xs px-3 pb-1">{{ jsonError }}</div>
|
|
9
|
+
</DemoNodeCard>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
|
|
14
|
+
import DemoNodeCard from "./DemoNodeCard.vue"
|
|
15
|
+
import DemoPort from "./DemoPort.vue"
|
|
16
|
+
import { useFlow } from "../components/index.js"
|
|
17
|
+
|
|
18
|
+
import { computed, defineProps, ref, toRefs } from "vue"
|
|
19
|
+
|
|
20
|
+
const props = defineProps({
|
|
21
|
+
node: {
|
|
22
|
+
type: Object,
|
|
23
|
+
required: true
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const { node } = toRefs(props)
|
|
28
|
+
const flow = useFlow()
|
|
29
|
+
const jsonError = ref('')
|
|
30
|
+
|
|
31
|
+
const recordJson = computed({
|
|
32
|
+
get() {
|
|
33
|
+
return JSON.stringify(node.value.record ?? {}, null, 2)
|
|
34
|
+
},
|
|
35
|
+
set(value) {
|
|
36
|
+
try {
|
|
37
|
+
node.value.record = JSON.parse(value)
|
|
38
|
+
jsonError.value = ''
|
|
39
|
+
} catch (err) {
|
|
40
|
+
jsonError.value = 'Invalid JSON'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
function deleteNode() {
|
|
46
|
+
flow.deleteNode(node.value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DemoNodeCard :node="node" title="Add" icon="pi-plus" @delete="deleteNode"
|
|
3
|
+
:data-testid="`node-${node.logicType}`">
|
|
4
|
+
<div class="demo-node-section">Sum</div>
|
|
5
|
+
<div class="flex items-center p-1">
|
|
6
|
+
<DemoPort :x="-1" :node="node" portId="in" />
|
|
7
|
+
<div class="flex-1 mx-2 text-sm">N inputs</div>
|
|
8
|
+
<DemoPort :x="1" :node="node" portId="out" />
|
|
9
|
+
</div>
|
|
10
|
+
</DemoNodeCard>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
|
|
15
|
+
import DemoNodeCard from "../DemoNodeCard.vue"
|
|
16
|
+
import DemoPort from "../DemoPort.vue"
|
|
17
|
+
import { useFlow } from "../../components/index.js"
|
|
18
|
+
|
|
19
|
+
import { defineProps, toRefs } from "vue"
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
node: {
|
|
23
|
+
type: Object,
|
|
24
|
+
required: true
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const { node } = toRefs(props)
|
|
29
|
+
const flow = useFlow()
|
|
30
|
+
|
|
31
|
+
function deleteNode() {
|
|
32
|
+
flow.deleteNode(node.value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DemoNodeCard :node="node" title="Constant" icon="pi-hashtag" @delete="deleteNode"
|
|
3
|
+
:data-testid="`node-${node.logicType}`">
|
|
4
|
+
<div class="demo-node-section">Value</div>
|
|
5
|
+
<div class="flex items-center p-1">
|
|
6
|
+
<div class="flex-1 mx-2">
|
|
7
|
+
<InputNumber v-if="editable" v-model="editable.value" class="w-full" :max-fraction-digits="6"
|
|
8
|
+
data-testid="constant-value" />
|
|
9
|
+
</div>
|
|
10
|
+
<DemoPort :x="1" :node="node" portId="out" />
|
|
11
|
+
</div>
|
|
12
|
+
</DemoNodeCard>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup>
|
|
16
|
+
|
|
17
|
+
import DemoNodeCard from "../DemoNodeCard.vue"
|
|
18
|
+
import DemoPort from "../DemoPort.vue"
|
|
19
|
+
import { useFlow } from "../../components/index.js"
|
|
20
|
+
import { synchronized } from '@live-change/vue3-components'
|
|
21
|
+
import { useApi } from '@live-change/vue3-ssr'
|
|
22
|
+
import InputNumber from 'primevue/inputnumber'
|
|
23
|
+
|
|
24
|
+
import { computed, defineProps, toRefs } from "vue"
|
|
25
|
+
|
|
26
|
+
const props = defineProps({
|
|
27
|
+
node: {
|
|
28
|
+
type: Object,
|
|
29
|
+
required: true
|
|
30
|
+
},
|
|
31
|
+
constant: {
|
|
32
|
+
type: Object,
|
|
33
|
+
default: null
|
|
34
|
+
},
|
|
35
|
+
calculationId: {
|
|
36
|
+
type: String,
|
|
37
|
+
default: null
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const { node } = toRefs(props)
|
|
42
|
+
const flow = useFlow()
|
|
43
|
+
const api = useApi()
|
|
44
|
+
|
|
45
|
+
const source = computed(() => props.constant)
|
|
46
|
+
const sync = synchronized({
|
|
47
|
+
source,
|
|
48
|
+
update: (params) => api.command(['calculation', 'updateConstant'], params),
|
|
49
|
+
identifiers: computed(() => ({
|
|
50
|
+
constant: node.value.logic,
|
|
51
|
+
calculation: props.calculationId || props.constant?.calculation
|
|
52
|
+
})),
|
|
53
|
+
recursive: true
|
|
54
|
+
})
|
|
55
|
+
const { value: editable } = sync
|
|
56
|
+
|
|
57
|
+
function deleteNode() {
|
|
58
|
+
flow.deleteNode(node.value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
</script>
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full h-screen flex flex-col">
|
|
3
|
+
<NavBar />
|
|
4
|
+
<div class="bg-surface-0 dark:bg-surface-900 py-2 px-4 shadow flex items-center gap-2 z-10 flex-wrap">
|
|
5
|
+
<Button label="Add constant" icon="pi pi-plus" size="small" data-testid="add-constant"
|
|
6
|
+
:disabled="!graphId" @click="addOp('Constant')" />
|
|
7
|
+
<Button label="Add add" icon="pi pi-plus" size="small" severity="secondary" data-testid="add-add"
|
|
8
|
+
:disabled="!graphId" @click="addOp('Add')" />
|
|
9
|
+
<Button label="Add multiply" icon="pi pi-plus" size="small" severity="secondary"
|
|
10
|
+
data-testid="add-multiply" :disabled="!graphId" @click="addOp('Multiply')" />
|
|
11
|
+
<Button label="Add power" icon="pi pi-plus" size="small" severity="secondary" data-testid="add-power"
|
|
12
|
+
:disabled="!graphId" @click="addOp('Power')" />
|
|
13
|
+
<Button label="Run" icon="pi pi-play" size="small" severity="success" data-testid="run-calculation"
|
|
14
|
+
:disabled="!calculationId" @click="run" />
|
|
15
|
+
<span v-if="runError" class="text-red-600 text-sm ml-2">{{ runError }}</span>
|
|
16
|
+
<span v-if="calculationId" data-testid="calculation-id" :data-id="calculationId" class="hidden">
|
|
17
|
+
{{ calculationId }}
|
|
18
|
+
</span>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="flex flex-1 min-h-0">
|
|
21
|
+
<Flow class="flex-1 grow">
|
|
22
|
+
<template #default>
|
|
23
|
+
<component
|
|
24
|
+
v-for="node of nodes"
|
|
25
|
+
:key="node.id"
|
|
26
|
+
:is="nodeComponent(node)"
|
|
27
|
+
:node="node"
|
|
28
|
+
:constant="constantById(node.logic)"
|
|
29
|
+
:power="powerById(node.logic)"
|
|
30
|
+
:calculation-id="calculationId"
|
|
31
|
+
class="absolute"
|
|
32
|
+
:style="{ left: (node.position?.x || 0) + 'px', top: (node.position?.y || 0) + 'px' }"
|
|
33
|
+
/>
|
|
34
|
+
</template>
|
|
35
|
+
<template #foreground-edges>
|
|
36
|
+
<DemoEdge v-for="edge of canvasEdges" :key="edge.id" :edge="edge" />
|
|
37
|
+
</template>
|
|
38
|
+
<template #free-edge="{ freeEdge }">
|
|
39
|
+
<DemoEdge v-if="freeEdge" :edge="freeEdge" />
|
|
40
|
+
</template>
|
|
41
|
+
</Flow>
|
|
42
|
+
<aside class="w-80 bg-surface-100 dark:bg-surface-800 border-l border-gray-200 overflow-auto p-3">
|
|
43
|
+
<div class="font-semibold mb-2">Last run</div>
|
|
44
|
+
<pre class="text-xs m-0 whitespace-pre-wrap" data-testid="run-results">{{ lastRunText }}</pre>
|
|
45
|
+
</aside>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup>
|
|
51
|
+
|
|
52
|
+
import Flow from '../../components/Flow.vue'
|
|
53
|
+
import { useFlow } from '../../components/index.js'
|
|
54
|
+
import NavBar from '../../NavBar.vue'
|
|
55
|
+
import DemoEdge from '../DemoEdge.vue'
|
|
56
|
+
import ConstantNode from './ConstantNode.vue'
|
|
57
|
+
import AddNode from './AddNode.vue'
|
|
58
|
+
import MultiplyNode from './MultiplyNode.vue'
|
|
59
|
+
import PowerNode from './PowerNode.vue'
|
|
60
|
+
|
|
61
|
+
import { computed, ref, watch } from 'vue'
|
|
62
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
63
|
+
import { usePath, live, useApi, useClient } from '@live-change/vue3-ssr'
|
|
64
|
+
import { synchronizedList } from '@live-change/vue3-components'
|
|
65
|
+
|
|
66
|
+
const nodeComponentByType = {
|
|
67
|
+
calculation_Constant: ConstantNode,
|
|
68
|
+
calculation_Add: AddNode,
|
|
69
|
+
calculation_Multiply: MultiplyNode,
|
|
70
|
+
calculation_Power: PowerNode
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const deleteActionByType = {
|
|
74
|
+
calculation_Constant: { name: 'deleteConstant', key: 'constant' },
|
|
75
|
+
calculation_Add: { name: 'deleteAdd', key: 'add' },
|
|
76
|
+
calculation_Multiply: { name: 'deleteMultiply', key: 'multiply' },
|
|
77
|
+
calculation_Power: { name: 'deletePower', key: 'power' }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const path = usePath()
|
|
81
|
+
const api = useApi()
|
|
82
|
+
const client = useClient()
|
|
83
|
+
const route = useRoute()
|
|
84
|
+
const router = useRouter()
|
|
85
|
+
|
|
86
|
+
const creating = ref(false)
|
|
87
|
+
const lastRun = ref(null)
|
|
88
|
+
const runError = ref('')
|
|
89
|
+
const nextNodeSlot = ref(0)
|
|
90
|
+
|
|
91
|
+
const calculations = await live(computed(() => client.value?.session
|
|
92
|
+
? path.calculation.sessionOwnedCalculations({ session: client.value.session })
|
|
93
|
+
: null))
|
|
94
|
+
|
|
95
|
+
const calculationId = computed(() => route.params.calculation || calculations.value?.[0]?.id || null)
|
|
96
|
+
|
|
97
|
+
watch(calculationId, () => {
|
|
98
|
+
nextNodeSlot.value = 0
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
watch([calculations, () => client.value?.session, () => route.params.calculation], async () => {
|
|
102
|
+
if(typeof window == 'undefined') return
|
|
103
|
+
if(route.params.calculation) return
|
|
104
|
+
const session = client.value?.session
|
|
105
|
+
if(!session) return
|
|
106
|
+
const list = calculations.value || []
|
|
107
|
+
if(list.length) {
|
|
108
|
+
await router.replace({ name: 'calculationDetail', params: { calculation: list[0].id } })
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if(creating.value) return
|
|
112
|
+
creating.value = true
|
|
113
|
+
try {
|
|
114
|
+
const id = await api.command(['calculation', 'createCalculation'], { session })
|
|
115
|
+
await router.replace({ name: 'calculationDetail', params: { calculation: id } })
|
|
116
|
+
} finally {
|
|
117
|
+
creating.value = false
|
|
118
|
+
}
|
|
119
|
+
}, { immediate: true })
|
|
120
|
+
|
|
121
|
+
const graph = await live(computed(() => calculationId.value
|
|
122
|
+
? path.flow.graph({
|
|
123
|
+
ownerType: 'calculation_Calculation',
|
|
124
|
+
owner: calculationId.value
|
|
125
|
+
})
|
|
126
|
+
: null))
|
|
127
|
+
|
|
128
|
+
const graphId = computed(() => graph.value?.id || null)
|
|
129
|
+
|
|
130
|
+
function rowObjectId(row) {
|
|
131
|
+
return row?.to || row?.id
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const nodeRows = await live(computed(() => graphId.value
|
|
135
|
+
? path.flow.graphOwnedNodes({
|
|
136
|
+
graphType: 'flow_Graph',
|
|
137
|
+
graph: graphId.value
|
|
138
|
+
})
|
|
139
|
+
: null))
|
|
140
|
+
|
|
141
|
+
const edgeRows = await live(computed(() => graphId.value
|
|
142
|
+
? path.flow.graphOwnedEdges({
|
|
143
|
+
graphType: 'flow_Graph',
|
|
144
|
+
graph: graphId.value
|
|
145
|
+
})
|
|
146
|
+
: null))
|
|
147
|
+
|
|
148
|
+
const constants = await live(computed(() => calculationId.value
|
|
149
|
+
? path.calculation.calculationOwnedConstants({ calculation: calculationId.value })
|
|
150
|
+
: null))
|
|
151
|
+
|
|
152
|
+
const powers = await live(computed(() => calculationId.value
|
|
153
|
+
? path.calculation.calculationOwnedPowers({ calculation: calculationId.value })
|
|
154
|
+
: null))
|
|
155
|
+
|
|
156
|
+
const nodeSource = computed(() => (nodeRows.value || []).map(row => ({
|
|
157
|
+
...row,
|
|
158
|
+
id: rowObjectId(row)
|
|
159
|
+
})))
|
|
160
|
+
const syncNodes = synchronizedList({
|
|
161
|
+
source: nodeSource,
|
|
162
|
+
update: (params) => api.command(['flow', 'updateNode'], params),
|
|
163
|
+
objectIdentifiers: (object) => ({
|
|
164
|
+
graphType: object.graphType,
|
|
165
|
+
graph: object.graph,
|
|
166
|
+
logicType: object.logicType,
|
|
167
|
+
logic: object.logic
|
|
168
|
+
}),
|
|
169
|
+
recursive: true
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const nodes = syncNodes.value
|
|
173
|
+
|
|
174
|
+
const canvasEdges = computed(() => (edgeRows.value || []).map(edge => ({
|
|
175
|
+
id: rowObjectId(edge),
|
|
176
|
+
src: { node: edge.source, port: edge.sourcePort },
|
|
177
|
+
dest: { node: edge.destination, port: edge.destinationPort },
|
|
178
|
+
connection: edge.connection
|
|
179
|
+
})))
|
|
180
|
+
|
|
181
|
+
function nodeById(nodeId) {
|
|
182
|
+
return nodes.value.find(node => node.id == nodeId)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isConnectable(fromNode, fromPort, toNode, toPort) {
|
|
186
|
+
const fromId = typeof fromNode == 'string' ? fromNode : fromNode?.id
|
|
187
|
+
const toId = typeof toNode == 'string' ? toNode : toNode?.id
|
|
188
|
+
if(fromId && toId && fromId === toId) return false
|
|
189
|
+
if(!fromPort || !toPort) return true
|
|
190
|
+
return fromPort !== toPort
|
|
191
|
+
&& (fromPort === 'in' || fromPort === 'out')
|
|
192
|
+
&& (toPort === 'in' || toPort === 'out')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function connect(newEdge) {
|
|
196
|
+
if(newEdge.src.port !== 'out' && newEdge.dest.port === 'out') {
|
|
197
|
+
const tmp = newEdge.src
|
|
198
|
+
newEdge.src = newEdge.dest
|
|
199
|
+
newEdge.dest = tmp
|
|
200
|
+
}
|
|
201
|
+
const srcNode = nodeById(newEdge.src.node)
|
|
202
|
+
const destNode = nodeById(newEdge.dest.node)
|
|
203
|
+
if(!srcNode || !destNode || !graphId.value) return
|
|
204
|
+
const wireId = await api.command(['calculation', 'createWire'], {
|
|
205
|
+
sourceType: srcNode.logicType,
|
|
206
|
+
source: srcNode.logic,
|
|
207
|
+
destinationType: destNode.logicType,
|
|
208
|
+
destination: destNode.logic,
|
|
209
|
+
sourcePort: newEdge.src.port,
|
|
210
|
+
destinationPort: newEdge.dest.port
|
|
211
|
+
})
|
|
212
|
+
await api.command(['flow', 'createEdge'], {
|
|
213
|
+
graphType: 'flow_Graph',
|
|
214
|
+
graph: graphId.value,
|
|
215
|
+
sourceType: 'flow_Node',
|
|
216
|
+
source: newEdge.src.node,
|
|
217
|
+
destinationType: 'flow_Node',
|
|
218
|
+
destination: newEdge.dest.node,
|
|
219
|
+
connectionType: 'calculation_Wire',
|
|
220
|
+
connection: wireId,
|
|
221
|
+
sourcePort: newEdge.src.port,
|
|
222
|
+
destinationPort: newEdge.dest.port
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function deleteLogicNode(node) {
|
|
227
|
+
const spec = deleteActionByType[node.logicType]
|
|
228
|
+
if(!spec) return
|
|
229
|
+
await api.command(['calculation', spec.name], { [spec.key]: node.logic })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function deleteWireEdge(edge) {
|
|
233
|
+
if(!edge.connection) return
|
|
234
|
+
await api.command(['calculation', 'deleteWire'], { wire: edge.connection })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const flow = useFlow({
|
|
238
|
+
nodes,
|
|
239
|
+
edges: canvasEdges,
|
|
240
|
+
width: 1600,
|
|
241
|
+
height: 900,
|
|
242
|
+
edgeConnectDistance: 48,
|
|
243
|
+
isConnectable,
|
|
244
|
+
connect,
|
|
245
|
+
deleteNode: deleteLogicNode,
|
|
246
|
+
deleteEdge: deleteWireEdge
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
function nodeComponent(node) {
|
|
250
|
+
return nodeComponentByType[node.logicType]
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function constantById(id) {
|
|
254
|
+
const row = (constants.value || []).find(row => rowObjectId(row) == id || row.id == id)
|
|
255
|
+
if(!row) return null
|
|
256
|
+
return { ...row, id: rowObjectId(row) }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function powerById(id) {
|
|
260
|
+
const row = (powers.value || []).find(row => rowObjectId(row) == id || row.id == id)
|
|
261
|
+
if(!row) return null
|
|
262
|
+
return { ...row, id: rowObjectId(row) }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function addOp(kind) {
|
|
266
|
+
if(!calculationId.value || !graphId.value) return
|
|
267
|
+
const createName = 'create' + kind
|
|
268
|
+
const logicType = 'calculation_' + kind
|
|
269
|
+
const logic = await api.command(['calculation', createName], {
|
|
270
|
+
calculation: calculationId.value
|
|
271
|
+
})
|
|
272
|
+
const slot = nextNodeSlot.value
|
|
273
|
+
nextNodeSlot.value = slot + 1
|
|
274
|
+
await api.command(['flow', 'setNode'], {
|
|
275
|
+
graphType: 'flow_Graph',
|
|
276
|
+
graph: graphId.value,
|
|
277
|
+
logicType,
|
|
278
|
+
logic,
|
|
279
|
+
position: {
|
|
280
|
+
x: 80 + (slot % 4) * 360,
|
|
281
|
+
y: 80 + Math.floor(slot / 4) * 260
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function run() {
|
|
287
|
+
if(!calculationId.value) return
|
|
288
|
+
const result = await api.command(['calculation', 'runCalculation'], {
|
|
289
|
+
calculation: calculationId.value
|
|
290
|
+
})
|
|
291
|
+
lastRun.value = result
|
|
292
|
+
runError.value = result?.error || ''
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const lastRunText = computed(() => {
|
|
296
|
+
if(!lastRun.value) return 'Press Run to execute the calculation graph.'
|
|
297
|
+
return JSON.stringify(lastRun.value, null, 2)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DemoNodeCard :node="node" title="Multiply" icon="pi-times" @delete="deleteNode"
|
|
3
|
+
:data-testid="`node-${node.logicType}`">
|
|
4
|
+
<div class="demo-node-section">Product</div>
|
|
5
|
+
<div class="flex items-center p-1">
|
|
6
|
+
<DemoPort :x="-1" :node="node" portId="in" />
|
|
7
|
+
<div class="flex-1 mx-2 text-sm">N inputs</div>
|
|
8
|
+
<DemoPort :x="1" :node="node" portId="out" />
|
|
9
|
+
</div>
|
|
10
|
+
</DemoNodeCard>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
|
|
15
|
+
import DemoNodeCard from "../DemoNodeCard.vue"
|
|
16
|
+
import DemoPort from "../DemoPort.vue"
|
|
17
|
+
import { useFlow } from "../../components/index.js"
|
|
18
|
+
|
|
19
|
+
import { defineProps, toRefs } from "vue"
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
node: {
|
|
23
|
+
type: Object,
|
|
24
|
+
required: true
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const { node } = toRefs(props)
|
|
29
|
+
const flow = useFlow()
|
|
30
|
+
|
|
31
|
+
function deleteNode() {
|
|
32
|
+
flow.deleteNode(node.value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DemoNodeCard :node="node" title="Power" icon="pi-sort-amount-up" @delete="deleteNode"
|
|
3
|
+
:data-testid="`node-${node.logicType}`">
|
|
4
|
+
<div class="demo-node-section">Exponent</div>
|
|
5
|
+
<div class="flex items-center p-1">
|
|
6
|
+
<DemoPort :x="-1" :node="node" portId="in" />
|
|
7
|
+
<div class="flex-1 mx-2">
|
|
8
|
+
<InputNumber v-if="editable" v-model="editable.exponent" class="w-full" :max-fraction-digits="6"
|
|
9
|
+
data-testid="power-exponent" />
|
|
10
|
+
</div>
|
|
11
|
+
<DemoPort :x="1" :node="node" portId="out" />
|
|
12
|
+
</div>
|
|
13
|
+
</DemoNodeCard>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
|
|
18
|
+
import DemoNodeCard from "../DemoNodeCard.vue"
|
|
19
|
+
import DemoPort from "../DemoPort.vue"
|
|
20
|
+
import { useFlow } from "../../components/index.js"
|
|
21
|
+
import { synchronized } from '@live-change/vue3-components'
|
|
22
|
+
import { useApi } from '@live-change/vue3-ssr'
|
|
23
|
+
import InputNumber from 'primevue/inputnumber'
|
|
24
|
+
|
|
25
|
+
import { computed, defineProps, toRefs } from "vue"
|
|
26
|
+
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
node: {
|
|
29
|
+
type: Object,
|
|
30
|
+
required: true
|
|
31
|
+
},
|
|
32
|
+
power: {
|
|
33
|
+
type: Object,
|
|
34
|
+
default: null
|
|
35
|
+
},
|
|
36
|
+
calculationId: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: null
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const { node } = toRefs(props)
|
|
43
|
+
const flow = useFlow()
|
|
44
|
+
const api = useApi()
|
|
45
|
+
|
|
46
|
+
const source = computed(() => props.power)
|
|
47
|
+
const sync = synchronized({
|
|
48
|
+
source,
|
|
49
|
+
update: (params) => api.command(['calculation', 'updatePower'], params),
|
|
50
|
+
identifiers: computed(() => ({
|
|
51
|
+
power: node.value.logic,
|
|
52
|
+
calculation: props.calculationId || props.power?.calculation
|
|
53
|
+
})),
|
|
54
|
+
recursive: true
|
|
55
|
+
})
|
|
56
|
+
const { value: editable } = sync
|
|
57
|
+
|
|
58
|
+
function deleteNode() {
|
|
59
|
+
flow.deleteNode(node.value)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
</script>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
function portKind(portId) {
|
|
2
|
+
if (typeof portId != 'string') return null
|
|
3
|
+
if (portId === 'in' || portId.endsWith('/in')) return 'in'
|
|
4
|
+
if (portId === 'out' || portId.endsWith('/out')) return 'out'
|
|
5
|
+
return null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeLink(edge) {
|
|
9
|
+
const srcKind = portKind(edge.src?.port)
|
|
10
|
+
const destKind = portKind(edge.dest?.port)
|
|
11
|
+
if (srcKind === 'out' && destKind === 'in') {
|
|
12
|
+
return { from: edge.src.node, to: edge.dest.node }
|
|
13
|
+
}
|
|
14
|
+
if (srcKind === 'in' && destKind === 'out') {
|
|
15
|
+
return { from: edge.dest.node, to: edge.src.node }
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function executeNode(node, inputRecords) {
|
|
21
|
+
if (node.type === 'source') {
|
|
22
|
+
return { ...(node.record || {}) }
|
|
23
|
+
}
|
|
24
|
+
const base = Object.assign({}, ...inputRecords)
|
|
25
|
+
if (node.type === 'enrich') {
|
|
26
|
+
const field = node.field || 'field'
|
|
27
|
+
return { ...base, [field]: node.value }
|
|
28
|
+
}
|
|
29
|
+
return base
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function runDemoGraph(nodes, edges) {
|
|
33
|
+
const nodeById = new Map(nodes.map(node => [node.id, node]))
|
|
34
|
+
const incoming = new Map(nodes.map(node => [node.id, []]))
|
|
35
|
+
const outgoing = new Map(nodes.map(node => [node.id, []]))
|
|
36
|
+
|
|
37
|
+
for (const edge of edges) {
|
|
38
|
+
const link = normalizeLink(edge)
|
|
39
|
+
if (!link) continue
|
|
40
|
+
if (!nodeById.has(link.from) || !nodeById.has(link.to)) continue
|
|
41
|
+
incoming.get(link.to).push(link.from)
|
|
42
|
+
outgoing.get(link.from).push(link.to)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const indegree = new Map(nodes.map(node => [node.id, incoming.get(node.id).length]))
|
|
46
|
+
const queue = nodes.filter(node => indegree.get(node.id) === 0).map(node => node.id)
|
|
47
|
+
const order = []
|
|
48
|
+
while (queue.length) {
|
|
49
|
+
const id = queue.shift()
|
|
50
|
+
order.push(id)
|
|
51
|
+
for (const nextId of outgoing.get(id)) {
|
|
52
|
+
indegree.set(nextId, indegree.get(nextId) - 1)
|
|
53
|
+
if (indegree.get(nextId) === 0) queue.push(nextId)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (order.length !== nodes.length) {
|
|
58
|
+
return { error: 'Cycle in the graph', traces: [], outputs: [] }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const values = new Map()
|
|
62
|
+
const traces = []
|
|
63
|
+
for (const id of order) {
|
|
64
|
+
const node = nodeById.get(id)
|
|
65
|
+
const inputRecords = incoming.get(id).map(fromId => values.get(fromId)).filter(Boolean)
|
|
66
|
+
const record = executeNode(node, inputRecords)
|
|
67
|
+
values.set(id, record)
|
|
68
|
+
traces.push({ nodeId: id, type: node.type, record })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const outputs = nodes
|
|
72
|
+
.filter(node => node.type === 'output')
|
|
73
|
+
.map(node => ({ id: node.id, record: values.get(node.id) }))
|
|
74
|
+
|
|
75
|
+
return { traces, outputs }
|
|
76
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const STORAGE_KEY = 'flow-frontend-demo-graph'
|
|
2
|
+
|
|
3
|
+
export function createSeedGraph() {
|
|
4
|
+
return {
|
|
5
|
+
nodes: [
|
|
6
|
+
{
|
|
7
|
+
id: 'source',
|
|
8
|
+
type: 'source',
|
|
9
|
+
position: { x: 80, y: 140 },
|
|
10
|
+
record: { sku: 'ABC-12', qty: 2, nip: '1234567890' }
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'enrich',
|
|
14
|
+
type: 'enrich',
|
|
15
|
+
position: { x: 420, y: 140 },
|
|
16
|
+
field: 'vatRate',
|
|
17
|
+
value: 23
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'output',
|
|
21
|
+
type: 'output',
|
|
22
|
+
position: { x: 760, y: 140 }
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
edges: [
|
|
26
|
+
{
|
|
27
|
+
id: 'e-source-enrich',
|
|
28
|
+
src: { node: 'source', port: 'out' },
|
|
29
|
+
dest: { node: 'enrich', port: 'in' }
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'e-enrich-output',
|
|
33
|
+
src: { node: 'enrich', port: 'out' },
|
|
34
|
+
dest: { node: 'output', port: 'in' }
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serverEntry } from '@live-change/frontend-base/server-entry.js'
|
|
2
2
|
import App from './App.vue'
|
|
3
3
|
import { createRouter } from './router'
|
|
4
|
+
import config from './config.js'
|
|
4
5
|
|
|
5
|
-
const render = serverEntry(App, createRouter)
|
|
6
|
+
const render = serverEntry(App, createRouter, config)
|
|
6
7
|
export { render }
|