Skip to content

Commit 5ae8986

Browse files
committed
feat(flows): flow list on CnIndexPage, toolbar-driven editor hosts (ADR-096)
/flows was the one page in the app that looked like a different product — the deprecated CnFlowIndexPage bare table with no title, search, pagination, view modes or row actions. It is now an ordinary CnIndexPage over the same shared flow store (:objects from useFlowStore, app-scoped), with a New flow header action, an Edit row action, and the honest status column (Disabled / Enabled / 'Enabled, but has no owner — it will not start'). The built-in Add button is off: it opens the schema form dialog, and a flow is not an OpenRegister object. FlowDetailPage now hosts the editor toolbar's save/run events (route swap to the minted id on first save); FlowDetailSidebar is a plain wrapper — Save/Run moved onto the canvas toolbar in @conduction/nextcloud-vue.
1 parent 767e397 commit 5ae8986

4 files changed

Lines changed: 158 additions & 45 deletions

File tree

l10n/en.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ OC.L10N.register(
44
"(no value needed)" : "(no value needed)",
55
"(not set — the next run requests an unfiltered fetch)" : "(not set — the next run requests an unfiltered fetch)",
66
"A dry run executes all the synchronization logic but saves nothing: no contract is written and the target system is not touched." : "A dry run executes all the synchronization logic but saves nothing: no contract is written and the target system is not touched.",
7+
"A flow runs a series of steps when something happens — an object changes, a schedule fires, or you run it yourself." : "A flow runs a series of steps when something happens — an object changes, a schedule fires, or you run it yourself.",
78
"A matched event either POSTs to the sink above (Webhook), runs a synchronization, or runs a job. All three are tracked, retried, and dead-letterable the same way." : "A matched event either POSTs to the sink above (Webhook), runs a synchronization, or runs a job. All three are tracked, retried, and dead-letterable the same way.",
89
"A name is required." : "A name is required.",
910
"A signing secret is configured (hidden)." : "A signing secret is configured (hidden).",
@@ -340,6 +341,7 @@ OC.L10N.register(
340341
"Enable pass-through in the Options tab to add unset rules." : "Enable pass-through in the Options tab to add unset rules.",
341342
"Enabled" : "Enabled",
342343
"Enabled (cron/endpoint/event triggers run this flow; a manual Run always works)" : "Enabled (cron/endpoint/event triggers run this flow; a manual Run always works)",
344+
"Enabled, but has no owner — it will not start" : "Enabled, but has no owner — it will not start",
343345
"End day" : "End day",
344346
"End month" : "End month",
345347
"End time" : "End time",
@@ -495,6 +497,7 @@ OC.L10N.register(
495497
"Flow run ended with status: {status}" : "Flow run ended with status: {status}",
496498
"Flow run failed" : "Flow run failed",
497499
"Flow run triggered" : "Flow run triggered",
500+
"Flows" : "Flows",
498501
"Follow-ups" : "Follow-ups",
499502
"For JWT / JWT-ZGW the rule only checks the signed bearer; no extra fields are required." : "For JWT / JWT-ZGW the rule only checks the signed bearer; no extra fields are required.",
500503
"For security, saved API keys are never displayed. Leave the fields below empty to keep the existing keys unchanged. Only enter keys here to REPLACE all existing keys — saving with keys entered overwrites the stored set." : "For security, saved API keys are never displayed. Leave the fields below empty to keep the existing keys unchanged. Only enter keys here to REPLACE all existing keys — saving with keys entered overwrites the stored set.",
@@ -691,6 +694,7 @@ OC.L10N.register(
691694
"Name is required" : "Name is required",
692695
"Name must contain at least one letter or number" : "Name must contain at least one letter or number",
693696
"Never" : "Never",
697+
"New flow" : "New flow",
694698
"Next" : "Next",
695699
"Next Run" : "Next Run",
696700
"Next run" : "Next run",
@@ -990,6 +994,7 @@ OC.L10N.register(
990994
"Save to register" : "Save to register",
991995
"Saving failed" : "Saving failed",
992996
"Saving…" : "Saving…",
997+
"Schedule" : "Schedule",
993998
"Schema" : "Schema",
994999
"Schema (required)" : "Schema (required)",
9951000
"Schema ID" : "Schema ID",
@@ -1210,6 +1215,7 @@ OC.L10N.register(
12101215
"Transform" : "Transform",
12111216
"Transformation rules" : "Transformation rules",
12121217
"Transforms each source record into the target shape." : "Transforms each source record into the target shape.",
1218+
"Trigger" : "Trigger",
12131219
"Trigger a run to see its trace here." : "Trigger a run to see its trace here.",
12141220
"Trigger: {trigger}" : "Trigger: {trigger}",
12151221
"Trusted root CA (PKIoverheid Private Root CA, PEM)" : "Trusted root CA (PKIoverheid Private Root CA, PEM)",

src/views/Flow/FlowDetailPage.vue

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,41 @@
2121
@spec openspec/specs/flow-orchestration/spec.md#REQ-017
2222
-->
2323
<template>
24-
<CnFlowDetail :id="$route.params.id" app="openconnector" />
24+
<CnFlowDetail :id="$route.params.id" app="openconnector" @save="onSave" @run="onRun" />
2525
</template>
2626

2727
<script>
28-
import { CnFlowDetail } from '@conduction/nextcloud-vue'
28+
import { CnFlowDetail, useFlowStore } from '@conduction/nextcloud-vue'
2929
3030
export default {
3131
name: 'FlowDetailPage',
3232
components: { CnFlowDetail },
33+
34+
setup() {
35+
return { store: useFlowStore() }
36+
},
37+
38+
methods: {
39+
/**
40+
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
41+
* @return {Promise<void>}
42+
*/
43+
async onSave() {
44+
const saved = await this.store.save()
45+
// A newly created flow gets its id from the server, so the route has
46+
// to catch up or a reload would land back on `new`.
47+
if (saved?.id && this.$route.params.id === 'new') {
48+
this.$router.replace(`/flows/${saved.id}`)
49+
}
50+
},
51+
52+
/**
53+
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
54+
* @return {Promise<void>}
55+
*/
56+
async onRun() {
57+
await this.store.run({})
58+
},
59+
},
3360
}
3461
</script>

src/views/Flow/FlowDetailSidebar.vue

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,48 +9,15 @@
99
@spec openspec/specs/flow-orchestration/spec.md#REQ-017
1010
-->
1111
<template>
12-
<CnFlowSidebar @save="onSave" @run="onRun" />
12+
<CnFlowSidebar />
1313
</template>
1414

1515
<script>
16-
import { CnFlowSidebar, useFlowStore } from '@conduction/nextcloud-vue'
16+
import { CnFlowSidebar } from '@conduction/nextcloud-vue'
1717
1818
export default {
1919
name: 'FlowDetailSidebar',
2020
2121
components: { CnFlowSidebar },
22-
23-
/**
24-
* Expose the shared flow store to the sidebar canvas.
25-
*
26-
* @return {object} The setup bindings.
27-
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
28-
*/
29-
setup() {
30-
return { store: useFlowStore() }
31-
},
32-
33-
methods: {
34-
/**
35-
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
36-
* @return {Promise<void>}
37-
*/
38-
async onSave() {
39-
const saved = await this.store.save()
40-
// A newly created flow gets its id from the server, so the route has
41-
// to catch up or a reload would land back on `new`.
42-
if (saved?.id && this.$route.params.id === 'new') {
43-
this.$router.replace(`/flows/${saved.id}`)
44-
}
45-
},
46-
47-
/**
48-
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
49-
* @return {Promise<void>}
50-
*/
51-
async onRun() {
52-
await this.store.run({})
53-
},
54-
},
5522
}
5623
</script>

src/views/Flow/FlowsIndex.vue

Lines changed: 121 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,146 @@
33
<!--
44
Flows — OpenConnector's scoped view over the one shared flow store.
55
6-
Passes `app="openconnector"`: this app sees only its own flows. OpenRegister's
7-
own Flows page passes no app filter and shows every app's flows (that surface
8-
is the fleet-wide one; this is the leaf-app one). Mirrors
9-
openregister/src/views/flows/FlowsIndex.vue exactly.
6+
Passes `app: 'openconnector'`: this app sees only its own flows.
7+
OpenRegister's own Flows page passes no app filter and shows every app's
8+
flows (that surface is the fleet-wide one; this is the leaf-app one).
9+
10+
Rendered on CnIndexPage per ADR-096 — a flow list is an ordinary index
11+
surface. The SOURCE is external (`:objects` from useFlowStore) because a
12+
flow is not an OpenRegister object, so there is no register/schema pair for
13+
a `type:index` page to bind. Both built-in row actions are replaced: the
14+
built-in Edit opens the schema-driven form dialog (nothing to render without
15+
a schema), and a flow has no read-only detail page for View — the canvas IS
16+
the flow.
1017
1118
@spec openspec/specs/flow-orchestration/spec.md#REQ-017
1219
-->
1320
<template>
14-
<CnFlowIndexPage app="openconnector" @open="openFlow" @create="createFlow" />
21+
<CnIndexPage
22+
:title="t('openconnector', 'Flows')"
23+
:description="t('openconnector', 'A flow runs a series of steps when something happens — an object changes, a schedule fires, or you run it yourself.')"
24+
:columns="columns"
25+
:objects="rows"
26+
:loading="store.loading"
27+
:selectable="false"
28+
:showAdd="false"
29+
:showViewAction="false"
30+
:showEditAction="false"
31+
:actions="rowActions"
32+
rowClickToView
33+
@rowClick="openFlow">
34+
<template #header-actions>
35+
<NcButton variant="primary" @click="createFlow">
36+
<template #icon>
37+
<Plus :size="20" />
38+
</template>
39+
{{ t('openconnector', 'New flow') }}
40+
</NcButton>
41+
</template>
42+
</CnIndexPage>
1543
</template>
1644

1745
<script>
18-
import { CnFlowIndexPage } from '@conduction/nextcloud-vue'
46+
import { CnIndexPage, useFlowStore } from '@conduction/nextcloud-vue'
47+
import { NcButton } from '@nextcloud/vue'
48+
import Pencil from 'vue-material-design-icons/Pencil.vue'
49+
import Plus from 'vue-material-design-icons/Plus.vue'
1950
2051
export default {
2152
name: 'FlowsIndex',
2253
23-
components: { CnFlowIndexPage },
54+
components: {
55+
CnIndexPage,
56+
NcButton,
57+
// Pencil is deliberately NOT registered: it is passed as an icon
58+
// COMPONENT in `rowActions`, never used as a tag in this template.
59+
Plus,
60+
},
61+
62+
setup() {
63+
return { store: useFlowStore() }
64+
},
65+
66+
computed: {
67+
/**
68+
* @return {Array<object>} The row-action menu: Edit, and only Edit.
69+
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
70+
*/
71+
rowActions() {
72+
return [
73+
{
74+
label: this.t('openconnector', 'Edit'),
75+
icon: Pencil,
76+
handler: (row) => this.openFlow(row),
77+
},
78+
]
79+
},
80+
81+
/**
82+
* @return {Array<object>} The column definitions.
83+
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
84+
*/
85+
columns() {
86+
return [
87+
{ key: 'name', label: this.t('openconnector', 'Name') },
88+
{ key: 'description', label: this.t('openconnector', 'Description') },
89+
{ key: 'trigger', label: this.t('openconnector', 'Trigger') },
90+
{ key: 'cron', label: this.t('openconnector', 'Schedule') },
91+
{ key: 'statusLabel', label: this.t('openconnector', 'Status') },
92+
]
93+
},
94+
95+
/**
96+
* The flows with the status rendered for display.
97+
*
98+
* @return {Array<object>} The rows.
99+
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
100+
*/
101+
rows() {
102+
return (this.store.flows || []).map((flow) => ({
103+
...flow,
104+
statusLabel: this.statusLabel(flow),
105+
}))
106+
},
107+
},
108+
109+
created() {
110+
this.store.load({ app: 'openconnector' })
111+
},
24112
25113
methods: {
114+
/**
115+
* Enabled and dispatchable are NOT the same thing: a trigger fires with
116+
* no acting user, so a flow with no owner has no identity to run as and
117+
* will not start however enabled it looks.
118+
*
119+
* @param {object} flow The flow.
120+
* @return {string} The label.
121+
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
122+
*/
123+
statusLabel(flow) {
124+
if (!flow.enabled) {
125+
return this.t('openconnector', 'Disabled')
126+
}
127+
if (!flow.owner) {
128+
return this.t('openconnector', 'Enabled, but has no owner — it will not start')
129+
}
130+
131+
return this.t('openconnector', 'Enabled')
132+
},
133+
26134
/**
27135
* @param {object} flow The activated flow.
28136
* @spec openspec/specs/flow-orchestration/spec.md#REQ-017
29137
* @return {void}
30138
*/
31139
openFlow(flow) {
32-
this.$router.push(`/flows/${flow.id}`)
140+
const id = flow?.id || flow?.uuid
141+
if (!id) {
142+
return
143+
}
144+
145+
this.$router.push(`/flows/${id}`)
33146
},
34147
35148
/**

0 commit comments

Comments
 (0)