@revenexx/studio-integrations 1.1.0 → 1.2.0

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +520 -0
  2. package/README.md +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
  7. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
  8. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  9. package/dist/runtime/components/Integrations/ExpressionInput.vue +77 -22
  10. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  11. package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
  12. package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
  13. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
  14. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
  15. package/dist/runtime/components/Integrations/NodeConfigField.vue +23 -0
  16. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  17. package/dist/runtime/components/Integrations/NodeInspector.vue +25 -32
  18. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  19. package/dist/runtime/components/Integrations/RunActionsMenu.vue +5 -3
  20. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  21. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  22. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  23. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  24. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  25. package/dist/runtime/components/Integrations/RunsTable.vue +143 -20
  26. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  27. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  28. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  29. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  30. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  31. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  32. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  33. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  34. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  35. package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -28
  36. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  37. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  38. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  39. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  40. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  41. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  42. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  43. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  44. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  45. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  46. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  47. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  48. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  49. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  50. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  51. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  52. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  53. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  54. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  55. package/dist/runtime/composables/useIntegrationsTemplates.js +2 -5
  56. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  57. package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -7
  58. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  59. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  60. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  61. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  62. package/dist/runtime/data/expressionEditor.js +11 -5
  63. package/dist/runtime/data/handleId.d.ts +61 -1
  64. package/dist/runtime/data/handleId.js +8 -1
  65. package/dist/runtime/data/payloadForm.js +3 -2
  66. package/dist/runtime/i18n/messages/de.json +30 -11
  67. package/dist/runtime/i18n/messages/en.json +27 -8
  68. package/dist/runtime/pages/integrations/audit.vue +7 -6
  69. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  70. package/dist/runtime/pages/integrations/index.vue +70 -12
  71. package/dist/runtime/pages/integrations/runs.vue +116 -7
  72. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  73. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  74. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  75. package/dist/runtime/pages/integrations/workflows/[id].vue +177 -28
  76. package/dist/runtime/types/integrations.d.ts +5 -0
  77. package/dist/runtime/utils/apiError.d.ts +21 -1
  78. package/dist/runtime/utils/apiError.js +1 -1
  79. package/dist/runtime/utils/clipboard.d.ts +33 -0
  80. package/dist/runtime/utils/clipboard.js +11 -0
  81. package/dist/runtime/utils/deepClone.d.ts +41 -0
  82. package/dist/runtime/utils/deepClone.js +3 -0
  83. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  84. package/dist/runtime/utils/formatDuration.js +13 -0
  85. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  86. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  87. package/dist/runtime/utils/notify.d.ts +1 -1
  88. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  89. package/dist/runtime/utils/runEnvelope.js +12 -0
  90. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  91. package/dist/runtime/utils/runFeedQuery.js +66 -0
  92. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  93. package/dist/runtime/utils/runOutputs.js +10 -0
  94. package/dist/runtime/utils/statusStyle.d.ts +24 -0
  95. package/dist/runtime/utils/statusStyle.js +1 -0
  96. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  97. package/dist/runtime/utils/validationFindings.js +18 -3
  98. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,525 @@
1
1
  # @revenexx/studio-integrations
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3f56bfa: A studio surface that is waiting now says so in words, not only as a shimmer.
8
+
9
+ Seven surfaces drew placeholder blocks while they loaded and said nothing at all —
10
+ which is exactly what they say when there is genuinely nothing to show. Those two
11
+ states ask for opposite reactions: hold on, or go and do something about it. The
12
+ shimmer that separates them for a sighted reader is not information a screen reader
13
+ can reach, so for one reader every one of these surfaces was silent in the same way,
14
+ whether it was loading or empty.
15
+
16
+ The template gallery had been given words of its own; the other six now have them
17
+ too: the dashboard's four panels, the missed-runs list on the Runs page and inside a
18
+ workflow, the version history, the New-workflow dialog's template picker, and the
19
+ dialog that imports a platform SFTP account. Each says what _it_ is waiting for,
20
+ under its own name, and each stops saying it when the answer arrives. Nothing changes
21
+ on screen for anybody who could already see the shimmer.
22
+
23
+ The dashboard's key figures had the sharper half of the problem, and it was worse
24
+ than silence: while the tenant's workflows were still on their way, each tile
25
+ announced itself as "Active workflows: 0" — a figure counted from an empty list,
26
+ which does not read as waiting but as an answer. A tile is now named by what it
27
+ counts until it has counted it.
28
+
29
+ Behind it, one small component owns the announcement — the region that is always in
30
+ the DOM with only its text changing, the naming that keeps several on one page apart,
31
+ and the placeholders' removal from the accessibility tree — so the seventh surface to
32
+ need it cannot get a different version of it than the first.
33
+
34
+ The eight lists that draw the shared table are not in this: their placeholder rows
35
+ come from `@revenexx/studio-shared` and are carried separately.
36
+
37
+ - 69c9e95: The tenant-wide Runs page now pages and narrows on the platform instead of in the
38
+ browser, so a tenant's whole run history is reachable rather than only its newest
39
+ hundred runs.
40
+
41
+ What changes for anyone using the page. Older runs are reached a page at a time,
42
+ and the count under the table is the number of runs that match rather than the
43
+ number of rows loaded — it used to say "of 100" on a tenant with thousands.
44
+ Narrowing by outcome or trigger kind, and searching by workflow name, are applied
45
+ to the history: "what failed last night" no longer searches a window that on a busy
46
+ tenant can be under an hour, and a narrowing applied from a later page starts again
47
+ at the first one. The Missed runs tab pages the same way, where it previously read
48
+ one page and offered no way past it.
49
+
50
+ Two things are deliberately withheld rather than left to mislead. The columns no
51
+ longer offer a sort, because `GET /runs` orders newest first and takes no ordering
52
+ parameter — a sort here would have reordered one page. And the filter menu shows no
53
+ count per option, because a count of the rows in hand would describe the page
54
+ rather than the history the option narrows.
55
+
56
+ The filter control itself is unchanged in shape: the same menu, the same words, one
57
+ value per dimension.
58
+
59
+ Fixed on the way: the outcome filter sent a status the platform does not have.
60
+ The studio's "failed" stands for several of the platform's statuses, and the
61
+ expansion carried `timed_out`, which its `RunStatus` does not list — so choosing
62
+ "failed" answered `422 The selected status.2 is invalid` instead of narrowing.
63
+ The vocabulary now mirrors the enum, the end-to-end mocks refuse what the service
64
+ refuses, and the suite walks every option of every filter rather than a sample.
65
+
66
+ From review: a search taken back inside the debounce window no longer commits
67
+ itself, a read the platform refuses is now said instead of leaving the previous
68
+ answer standing under the new question, a narrowing that matches nothing says so
69
+ rather than claiming the tenant has never run a workflow, the search box is no
70
+ longer left narrowing the list invisibly after a visit elsewhere, the sidebar's
71
+ Runs badge keeps converging while a filter is applied, and the Missed runs tab
72
+ opens on the newest firings when the dashboard links to it.
73
+
74
+ - 1b2a938: The Runs list can be read in an order other than newest first. Ordering by how
75
+ long a run took answers "which of these is slow?", which the page could not
76
+ answer at all: the headers that used to offer it sorted the hundred runs the
77
+ browser was holding, and were withdrawn when the list moved onto the platform.
78
+
79
+ Two of the six columns offer a sort — when the run started, and how long it took
80
+ — and each orders the whole history rather than the page on screen.
81
+ A run the column cannot describe, one that never started or one still running,
82
+ is listed last whichever way the list is read; asking for another order returns
83
+ to the first page, because re-ordering re-numbers them.
84
+
85
+ The other four headers stay fixed. Three are the platform's shape: ordering by
86
+ the workflow's name would need a join on the path the list pages on, the trigger
87
+ kind has four values so it groups rather than orders, and the run id orders by
88
+ nothing a reader would mean.
89
+
90
+ The outcome column is ours. The platform can order by it, and this page does not
91
+ offer it: it would order by the word the platform stores while the column shows
92
+ the studio's own translated word, so the header would sit over an order the
93
+ reader cannot see — and in German not even an alphabetical one.
94
+
95
+ Needs an integrations service that serves `?sort=` on `GET /v1/runs` (PO-371).
96
+ Against an older one the headers are offered and the platform refuses them.
97
+
98
+ - 19829bf: A workflow can be deleted from the page it is open on. Until now the only way to
99
+ get rid of a workflow was to leave the editor, find it again in the list of all
100
+ of them and use its row menu — the editor offered duplicating, pausing and
101
+ saving, and no way to discard the thing it had open.
102
+
103
+ The header carries an actions menu, and Duplicate has moved into it beside
104
+ Delete. The buttons that remain — Cancel, Save — act on the unsaved work in front
105
+ of the operator; the two in the menu make and unmake the workflow that work
106
+ belongs to. Delete is not a button for a reason: it is the one control there that
107
+ cannot be undone, and it has no business a mis-click away from Save. The question
108
+ it asks is the list's own, unchanged: it names the workflow and says that the
109
+ revision history goes permanently with it.
110
+
111
+ Unsaved work on the canvas raises no second question. The draft belonged to a
112
+ workflow that has stopped existing, so the studio takes it along and moves to the
113
+ workflows list rather than offering to keep work that has nowhere left to go.
114
+
115
+ Both surfaces now say so when the platform refuses a deletion: the question stays
116
+ open carrying the reason, and the workflow stays where it is. The list used to
117
+ fail silently here — the dialog simply stopped, over a row that was still there.
118
+
119
+ - b85a8e0: A run started from the workflow editor now ends on what it returned, whichever mode
120
+ the workflow runs in.
121
+
122
+ Pressing Run on a workflow that does not wait for its result used to answer "Run #99
123
+ started" and stop there — the number was the whole answer, and reading what the run
124
+ actually did meant leaving for the run's own record and watching it fill in. The
125
+ studio now stays with that run: the dialog says where it has got to while it is
126
+ going, and shows what it returned when it ends. The mode a workflow runs in decides
127
+ who waits, not whether the author gets an answer.
128
+
129
+ What the result looks like has changed with it. It used to be the run object printed
130
+ whole — two Temporal identifiers, two re-run references and four timestamps around
131
+ the value somebody pressed Run to see, keyed by the node's internal id. Each value
132
+ now stands under the name its node carries on the canvas, with the run's number,
133
+ status and duration on one line above them; the record itself is still one click
134
+ away, where the rest of it belongs. A returned value that is not a map of this
135
+ workflow's nodes is shown whole rather than filed under a name it may not belong to.
136
+
137
+ The way into the run's record is offered from the moment the run starts, so waiting
138
+ here is never the only option. The studio stops following once the run is terminal,
139
+ once the author closes the result, and after a few reads that cannot be answered —
140
+ the confirmation and the way in stand either way.
141
+
142
+ Behind it: one duration formatter for the runs table, the run record and this
143
+ dialog, which had three identical private copies, and one place that decides what a
144
+ node is called, so the result cannot name a node differently from the canvas.
145
+
146
+ - 1704552: A node setting declared as taking several lines is now drawn with room for them.
147
+
148
+ A node type can say that one of its settings runs to more than one line — a mail
149
+ body, a failure message, a list of instructions one per line — and every layer but
150
+ this one carried the declaration: the node contract has held it since it was
151
+ written, and twelve fields across three published packages set it. The editor's own
152
+ field type did not list it, so it was dropped on arrival and every such setting was
153
+ drawn as the same one-line box. The help text under one of them told the author to
154
+ write one instruction per line at a control that refused the second one.
155
+
156
+ Both controls a typed setting can land in now honour it. The plain box becomes a
157
+ text area, and the box that maps values in from earlier steps takes Enter as a new
158
+ line — with expressions going on working line by line, which matters because that is
159
+ where most of the twelve fields land: a notification mail assembled from an upstream
160
+ value is the ordinary case. Such a setting opens at four lines and grows with what is
161
+ typed until ten, after which it scrolls.
162
+
163
+ Nothing has to be edited in a node package for this: the twelve existing
164
+ declarations start working as they were written. A setting that says nothing about
165
+ lines is drawn exactly as before, and a kind of setting with no multi-line meaning —
166
+ a dropdown — ignores the declaration rather than being made to honour it.
167
+
168
+ ### Patch Changes
169
+
170
+ - c94c96e: One place now spells the identifier of a connection point on the canvas, in both
171
+ directions.
172
+
173
+ Nothing changes on screen or in a saved workflow: the ids are byte-identical to the
174
+ ones the canvas drew before, and they were never persisted in a workflow anyway — only
175
+ port names are. What changes is that the twelve places which assembled an id by hand,
176
+ across the node card, the trigger, the group frame, the editor and the two panels that
177
+ rebuild a canvas from a run, now ask one module for it, and the four dot positions are
178
+ declared once instead of six times — including inside the reader, whose own parse is
179
+ built from that one list rather than repeating it.
180
+
181
+ Why it was worth doing while nothing was wrong: the module that reads an id was alone
182
+ there, so a fifth dot position would have had the writers produce a name the reader had
183
+ never heard of, handed back as though it were a port. That is not an empty value, so
184
+ the fallback every call site carries could not have caught it, and the editor would
185
+ have saved an output no node has. The round-trip test now exercises the writers and the
186
+ exported side list rather than a string built in the test, so the two halves are proved
187
+ together.
188
+
189
+ - 31e9a46: The controls the canvas offers on hover are big enough to hit.
190
+
191
+ Deleting a node was the smallest and hardest of them to press, which is the wrong way
192
+ round for the most irreversible thing the editor offers: the button measured 23 by 24
193
+ pixels, a pixel under the minimum a pointer target is allowed to have, and it missed by
194
+ construction rather than by accident — the height was fixed at 24 while the width was
195
+ left to grow from the padding around an 11-pixel glyph. Duplicate beside it, and the
196
+ trigger's delete, were the same.
197
+
198
+ The canvas also disagreed with itself about the size of a toolbar control, in four
199
+ sizes: those two rows, the pair offered on a connection at 20, a sticky note's colour
200
+ swatches at 20, and a group frame's swatches at 16 — the last of them 16 rather than the
201
+ 18 their styling claimed, because an inline size was quietly overriding it. Everything a
202
+ hover reveals on a node, a trigger, a sticky note or a group frame is now 24 by 24 with
203
+ a 13-pixel glyph, so a reader learns one target and keeps it.
204
+
205
+ The toolbar on a connection is raised with the rest and is the one that is still not
206
+ promised: it is drawn in the canvas's own coordinates rather than over the top, so its
207
+ 24 pixels shrink as the operator zooms out. Better than the 20 it was, and a size is not
208
+ the fix — that is carried separately.
209
+
210
+ - 1c461da: A workflow's own Runs tab no longer draws sort headers that do nothing, and the
211
+ tenant-wide Runs page opens on its first page.
212
+
213
+ On a workflow's Runs tab the column headers looked sortable and were not. Started
214
+ and Duration drew a hover state and a sort icon, took the click and did nothing
215
+ with it — the arrow did not move either, and the tab opened with no column marked
216
+ at all. The headers came back with the tenant-wide list's ordering, which the
217
+ platform performs; this tab renders the same table and the platform cannot order
218
+ its feed.
219
+
220
+ They are no longer offered. The alternative was to make them sort what the browser
221
+ holds, and that is a page: this tab pages on the server, so "order by duration"
222
+ would have put the slowest of the twenty-five rows on screen at the top and said
223
+ nothing about the rest of the history — which is the sentence the tenant-wide list
224
+ withdrew its own headers for until the platform could order the whole thing. The
225
+ runs stand newest first, as they always did, and what would bring an order back is
226
+ recorded in the spec rather than mimed by a control.
227
+
228
+ Two things on the tenant-wide Runs page ride along. Arriving at it now opens on the
229
+ first page: the search box comes back empty whatever was searched for, so a page
230
+ number kept from last time was a position inside a narrowing nothing on screen
231
+ still showed — the outcome and trigger kind are drawn back onto the menu and still
232
+ survive. And arriving with a search still in force now costs one read of the list
233
+ rather than the two it used to.
234
+
235
+ - fae7446: A duration now reads the same wherever the studio prints one.
236
+
237
+ The same step used to say `1.50s` on a run's Details tab and `1.5 s` on its Replay
238
+ tab one click away, and a step that took a minute said `1.1m` in the first place and
239
+ `1m 3s` in the second — which reads as two different figures rather than two
240
+ spellings of one. The dashboard spelled it a third way again — two decimals whatever
241
+ the value, so a run of 900 ms read `0.90s` there and `900ms` in the list it links to.
242
+ Five surfaces printed a duration, across three copies of the arithmetic.
243
+
244
+ They now share one, and its form is `900ms`, `1.5s`, `1m 3s`: the unit against the
245
+ number, as in the `10d ago` beside it, a tenth of a second where one was measured, and
246
+ minutes with their seconds above one — where `1.1m` for a 63-second step was the least
247
+ readable of the three candidates and the second decimal of `1.50s` was precision nobody
248
+ acts on.
249
+
250
+ **A run's duration loses its decimal place entirely, because there was never anything in
251
+ it.** The studio derives it from the two timestamps the platform records to the second,
252
+ so `4.00s` was three characters claiming a precision the platform does not store: that
253
+ run reads `4s` now, and one that took a minute reads `1m`. A step keeps its tenth —
254
+ there the milliseconds are measured, so `3.4s` says something `3s` would not. The replay
255
+ panel loses the space before its unit, and the dashboard stops printing seconds for a
256
+ figure that is not in seconds.
257
+
258
+ Two figures that could not come out right before are gone with it: a duration just
259
+ under a minute printed as `60.0s` — a minute said the other way — and the replay
260
+ panel's own arithmetic could produce `1m 60s`.
261
+
262
+ A duration still takes no language, so German is served `1.5s` rather than `1,5 s`.
263
+ That gap is now written down in one place rather than in four.
264
+
265
+ - d63594c: A refused save now states the reason and not the service's count of everything else it
266
+ found — everywhere, rather than on the two credential surfaces it was reported from.
267
+
268
+ The service refuses a save with one message and a tally: "The name has already been
269
+ taken. (and 2 more errors)". That count is honest arithmetic and misleading copy — it
270
+ counts by validation rule, so one missing field the service checks twice is two of them,
271
+ and it counts things no form draws. PO-275 removed it where it was reported. Creating a
272
+ workflow, creating a credential from the node editor's own lightbox and provisioning a
273
+ revenexx source kept showing it, because each had written the error handling out by hand
274
+ and only the credentials page knew the count was there.
275
+
276
+ The secret form gains a fallback it never had: a refusal whose message was empty drew an
277
+ empty sentence there, because nothing stood behind the message at all.
278
+
279
+ Underneath that: one function now answers what an error should say. It had three
280
+ implementations — the one that knew about the count, a byte-identical private
281
+ `captureError` in six composables, and the cast spelled inline at thirty-seven call
282
+ sites. `refusalReason` is `errorReason`, because a function named for refusals was
283
+ answering "could not load the audit log" in half its callers, and the name is why nobody
284
+ reached for it there. The count of call sites is the reason the sweep had to be done
285
+ twice: the same contract was spelled `(e as { message?: string })?.message`,
286
+ `(e as Error).message`, `(e as Error)?.message` and as an `instanceof` guard with the
287
+ message in each branch, and a search for one of those finds one of those.
288
+
289
+ One behaviour changes on the read paths, and it is small: an error whose message is
290
+ nothing but whitespace used to be drawn as an empty sentence and now falls back to the
291
+ surface's own wording.
292
+
293
+ - b0fcc32: Connecting a credential by OAuth now runs through one handshake instead of three copies
294
+ of it.
295
+
296
+ The **Connect** button exists in three places — a row in the credentials list, the
297
+ credential detail dialog, and the create dialog straight after a credential is made —
298
+ and each carried its own copy of the whole dance:
299
+ open the provider's window, listen for the message the return page posts back, and poll
300
+ once a second in case the operator closes the window instead. The two copies agreed, so
301
+ nothing was wrong on screen; what it cost is that the prerequisite for the message path
302
+ ever firing was documented in only one of them, and a fix to one button did not reach
303
+ the other.
304
+
305
+ Three things that were wrong in both copies are fixed by having one:
306
+
307
+ - **The poll kept running after the message arrived.** The listener removed itself and
308
+ reloaded, but the one-second timer stayed up until the window closed, and then
309
+ reloaded a second time. Harmless while the reload is idempotent, which is the sort of
310
+ thing that stops being true quietly.
311
+ - **No copy stopped when its caller went away.** Closing the detail dialog, or leaving
312
+ the page, while the provider's window was still open left a timer and a listener
313
+ running — and the detail dialog is held open by a prop rather than by `v-if`, so
314
+ closing it unmounts nothing. An attempt begun there could announce a connection, and
315
+ refresh the row, for whichever credential the operator opened next.
316
+ - **A second attempt while one was outstanding left the first still watching**, so a
317
+ window the operator had stopped looking at could still trigger a reload.
318
+ - **The completion message was accepted from any sender.** The landing page posts it to
319
+ our own origin, but nothing checked where it came from — and the window we open holds
320
+ a live `window.opener` on us, so a provider page could have announced a connection
321
+ that never happened. The origin is checked now.
322
+ - **A window the browser refuses to open left the poll running for ever.** `window.open`
323
+ returns `null`, `null?.closed` is `undefined`, and neither path could ever settle.
324
+ Nothing is installed in that case now. The operator is still told nothing, which is
325
+ what all three copies did — that is PO-399.
326
+
327
+ What an operator sees is unchanged: the connection is announced when the provider
328
+ answers, and closing the window refreshes quietly without claiming anything — because
329
+ a closed window says nothing about whether the authorisation went through.
330
+
331
+ - f6cb6e2: The deep copy every editing surface takes now comes from one module, and that module
332
+ says what the copy keeps.
333
+
334
+ Nothing changes on screen. Opening a workflow, editing a node, opening the variables
335
+ dialog, duplicating a workflow and building a manual run's payload all begin by copying
336
+ what they are about to let somebody edit, so an unsaved change can be dropped rather
337
+ than undone. That copy was written out eight times — as the same private one-line
338
+ `clone<T>` in the canvas and in the node inspector, and as a bare
339
+ `JSON.parse(JSON.stringify(…))` in six more places. All eight produced the same value,
340
+ and they still do: this is the same round trip, moved.
341
+
342
+ What was missing is the sentence underneath them. The round trip is safe **because a
343
+ workflow blob is JSON** — that is the entire guarantee, and it was written down nowhere,
344
+ so nothing said which inputs it holds for. A caller reaching for the same expression
345
+ with editor state that has not been through the API — a resolved sample, a cached
346
+ schema, anything holding a `Date`, a `Map` or an `undefined` — would lose those values
347
+ silently, in whichever of the eight places they copied from. The guarantee, and the
348
+ five things the round trip does not survive, are now in one doc comment with a unit
349
+ test that pins each of them.
350
+
351
+ `structuredClone` is deliberately not used, and the reason is in the module: it
352
+ preserves all five, which sounds strictly better and is a behaviour change — a field
353
+ dropped today because JSON cannot carry it would start surviving, and something
354
+ downstream may be relying on that loss. Swapping the mechanism is now one edit, and it
355
+ wants its own proof.
356
+
357
+ - 2271245: What the canvas does with a connection dot's side is now three exhaustive maps instead
358
+ of ternary chains scattered across five components.
359
+
360
+ A dot sits on one of four sides, and three separate answers follow from that: where Vue
361
+ Flow draws it, which side an edge arriving from it should land on, and where along the
362
+ side it is spread when a node has several outputs. Each was a chain of `if`s ending in
363
+ an `else`, inside a component: the position one in both the node card and the trigger
364
+ card, and the opposite-side one in the editor and in both read-only canvases.
365
+
366
+ Nothing changes on screen — the four sides get the same answers. What changes is what
367
+ happens to a fifth. An `else` gives a name it has never heard the last branch's answer:
368
+ a `centre` dot would have been drawn on the **left**, told that `right` faces it, and
369
+ spread along the horizontal axis — three plausible answers, no type error, and nothing
370
+ to notice. The maps are `Record<Side, …>`, so adding a name to `SIDES` stops all three
371
+ compiling until they say what it means.
372
+
373
+ This is the follow-up PO-258 wrote down for itself. Its note said three functions were
374
+ duplicated, and named them; two were, and one of those was hiding under a second name
375
+ (`opposite`, not `oppositeSide`) in the two read-only canvases. The note now says so.
376
+
377
+ - 759d21f: A copy control now says what actually happened, and all seven of them say it the same way.
378
+
379
+ The audit log's copy control announced "Copied" the instant it was pressed rather than
380
+ after the copy succeeded. `writeText` returns a promise, and a browser can refuse it —
381
+ a page that is not focused, a context that is not secure, a permission the operator
382
+ declined. The message had already gone up by then, and nothing contradicted it: the
383
+ operator believed they had the entry on their clipboard and pasted whatever was there
384
+ before. It could also do nothing at all — the control guarded on `navigator.clipboard`
385
+ existing and skipped the whole block when it did not, so on a browser without the API
386
+ the button neither copied nor complained.
387
+
388
+ Both are gone. A refused copy now says so, in English and German.
389
+
390
+ One more had the same defect and was found in review: the expression chip in the node
391
+ inspector, which copies an expression when no field is focused. It discarded the promise,
392
+ short-circuited to nothing when the clipboard API was absent, and said "Expression
393
+ copied" either way. It now reports a failure and keeps the useful half of the hint —
394
+ that clicking into a field inserts the expression directly, which still works when the
395
+ clipboard does not.
396
+
397
+ The other five — node config, rotated secret, webhook URL, a variable's expression,
398
+ nodes copied on the canvas — already behaved correctly, each with its own copy of the
399
+ await-and-catch. All seven now go through one helper, so the next one cannot be written
400
+ the wrong way by having nothing to copy from. Each keeps its own two
401
+ messages: copying a node's config, a webhook URL and an audit entry are three different
402
+ things to have failed, and one message covering all of them would name none of them.
403
+
404
+ - 70bbddc: A saved workflow stops claiming its build is queued once it is not.
405
+
406
+ Pressing Save queues a bundle build, and the editor put "Build queued…" on the toolbar
407
+ and left it there — the build finishes on the platform within a second or two, and the
408
+ chip stayed up for as long as the page was open. Reloading showed everything saved. A
409
+ chip that never clears is read as a save that did not happen, and it was reported as
410
+ exactly that.
411
+
412
+ What resolved it before was a platform announcement over the event bus, and nothing
413
+ else — so a host that does not have that channel, one where it is down, and a
414
+ connection that dropped in the second after the save all produced the same permanently
415
+ wrong screen. The announcement is still the fast path; under it, every way into a queued
416
+ build now watches it out, on the same interval and the same ceiling the Run button
417
+ already used when it had to wait a build out: an ordinary save, the resend-as-paused
418
+ after the deactivation dialog, putting an earlier version back, and opening a workflow
419
+ whose build is already running. That last one is the reload an operator reaches for
420
+ when the chip looks stuck, and it used to land on a chip that was stuck for good. A
421
+ build that fails reaches its notice the same way.
422
+
423
+ - 47fedc8: The validation banner and the card's badge now name a setting the way the node's own form names it.
424
+
425
+ A workflow that cannot be activated yet is reported in an amber banner listing the
426
+ settings still to fix, one line per node. Those settings were named by their internal
427
+ key, taken from the path the service reports the finding at — so an operator read
428
+ "Bestellung anlegen — credentials, companyId, customerId" and then opened the node to
429
+ find three fields called **Revenexx-Anmeldedaten**, **Firma** and **Kunde**. The same
430
+ fields, named two ways, on one screen, and in English inside an otherwise German
431
+ interface.
432
+
433
+ The banner asks the node's manifest now, which is where those names already live and
434
+ what the settings form has always rendered. Where there is no name to resolve — a
435
+ package that is gone, or a workflow older than the field it was saved with — the key
436
+ still stands: worse to read, but never the wrong field.
437
+
438
+ The badge on the card — the amber count whose hover lists the settings at fault — says
439
+ the same, and resolves it where it is drawn rather than upstream: the node card already
440
+ watches the manifest catalogue, so a name that arrives late redraws that one card instead
441
+ of re-syncing the whole canvas.
442
+
443
+ The marker inside the node's own settings form is deliberately unchanged. It is keyed by
444
+ the config key, which is how a message finds its input; a translated key would stop
445
+ matching the field it belongs to.
446
+
447
+ - d5838d6: The result of a run started by hand can be read when it is long.
448
+
449
+ The dialog that shows what a run returned capped its width and not its height, so it
450
+ grew as tall as its content: a result of two node outputs came out half a screen taller
451
+ than the screen, ran off the top and the bottom at once, and what stuck out was clipped
452
+ rather than made reachable — including the buttons at its foot, so the only way out was
453
+ the corner cross.
454
+
455
+ The body of the dialog now scrolls, as it already does in the create-workflow,
456
+ node-configuration and SFTP-import dialogs, and a single node's output may take a little
457
+ less of the screen so that two of them plus the footer stay navigable. Nothing about
458
+ what is shown changes.
459
+
460
+ Measured rather than eyeballed: with two outputs of a hundred entries each, the dialog
461
+ came out at 1106 pixels in a 900-pixel window before, and inside the window after.
462
+
463
+ - ea5b275: Picking the same node from the palette again lands it beside the last one instead of on top of it.
464
+
465
+ Clicking a row in the editor's node palette dropped the card in the centre of the view,
466
+ and the centre of the view does not move between two clicks. So five picks in a row
467
+ produced five nodes on one set of coordinates and one visible card — the workflow had
468
+ grown, the canvas said it had not, and the only way to find out was to drag the top card
469
+ away and see the next one underneath it.
470
+
471
+ A click-insert now steps off a spot the canvas already occupies, down and to the right in
472
+ grid steps, until it finds one that is clear. It reads the canvas each time rather than
473
+ counting the clicks so far, which is what makes a spot freed by dragging a card away
474
+ usable again — the next pick goes back to the centre of the view. The empty group frame
475
+ from the control pane, which is centred the same way and stacked the same way, goes
476
+ through the same helper.
477
+
478
+ Notes are deliberately left as they are. A note is drawn behind the cards because it
479
+ belongs over the nodes it explains, so a rule that keeps it clear of everything on the
480
+ canvas would push the first note of a workflow away from the middle of the view — two
481
+ notes in a row still land on one spot, and `specs/sticky-notes.md` says so.
482
+
483
+ Dragging a node out of the palette is deliberately unchanged: that gesture already shows a
484
+ ghost at the exact spot the card will land, so nudging the drop would make the ghost lie.
485
+
486
+ ## 1.1.1
487
+
488
+ ### Patch Changes
489
+
490
+ - 77ea454: A workflow's Runs tab no longer puts the operator back on the newest page while
491
+ they are paging. The tab's own refresh used to consult the page that last arrived
492
+ rather than the page that was asked for, so a refresh falling due between "show me
493
+ the older runs" and those runs arriving won the race and undid the click. It now
494
+ leaves a pending page alone, and a refresh that was already on its way no longer
495
+ draws over the page the operator asked for.
496
+ - ddb95eb: The `{ }` button beside a node setting is no longer announced as its own glyph.
497
+ It now carries a name saying what it does and what it opens ("Insert expression —
498
+ available data for this setting", and the German equivalent), reports whether the
499
+ list of available data is open, and points at that list while it is there.
500
+
501
+ It can also be operated from the keyboard at all now: it acted on a mouse press
502
+ alone, so Enter and Space did nothing, and Escape while it had focus closed the whole
503
+ node configuration dialog instead of the list. Nothing changes for a pointer.
504
+
505
+ - ec8f025: A run row no longer offers actions the platform will refuse. A run that has been
506
+ accepted but not started yet has no execution to act on, and the API answers a
507
+ cancel, a terminate or a resume for it with a 409 — so the row now withholds all
508
+ three, the way the run's own record already does, instead of turning the click into
509
+ an error toast.
510
+
511
+ Two German words on the same menu are fixed with it: the row's "Retry" and the
512
+ record's were shipped untranslated, and the entry that opens a run's replay said
513
+ "Wiederholen", which reads as an offer to run it again. They now read "Erneut
514
+ ausführen" and "Wiedergabe" — the word the replay surface itself already uses.
515
+
516
+ - 0de6228: A run's outcome is no longer cut off or drawn over the workflow name beside it. The
517
+ Status column in both lists of runs — the Runs page and the dashboard's latest runs —
518
+ was a fixed width measured against the English words, so the German
519
+ "fehlgeschlagen" ran out of its cell; the dashboard's own column was narrower still,
520
+ and even "erfolgreich" overflowed it. Both now share one track that keeps a floor and
521
+ grows to the widest word on the page, in whatever language the studio is read.
522
+
3
523
  ## 1.1.0
4
524
 
5
525
  ### Minor Changes
package/README.md CHANGED
@@ -92,7 +92,9 @@ pnpm changeset # record what your change does to the version
92
92
 
93
93
  Branch off `main` as `feature/<desc>`, `fix/<desc>` or `chore/<desc>` — GitHub refuses
94
94
  the first push of any other name, and `main` itself takes a reviewed pull request with
95
- CI green: [`docs/branch-protection.md`](docs/branch-protection.md).
95
+ CI green: [`docs/branch-protection.md`](docs/branch-protection.md). A separate,
96
+ non-blocking `Security Scan` reports secrets and dependency advisories:
97
+ [`docs/security-scanning.md`](docs/security-scanning.md).
96
98
 
97
99
  Hosts to develop against:
98
100
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
3
  "configKey": "studioIntegrations",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -20,6 +20,7 @@ import {
20
20
  } from "@revenexx/studio";
21
21
  import { toast } from "vue-sonner";
22
22
  import { resolveLocalized } from "../../utils/localize";
23
+ import { errorReason } from "../../utils/apiError";
23
24
  const props = defineProps({
24
25
  open: { type: Boolean, required: true },
25
26
  credentialType: { type: [String, Array], required: false }
@@ -102,24 +103,11 @@ async function runTest() {
102
103
  testing.value = false;
103
104
  }
104
105
  }
106
+ const { open: openAuthorisation } = useOAuthConnect(() => {
107
+ void load(true);
108
+ });
105
109
  function connect(id) {
106
- return oauthAuthorizeUrl(id).then((authorizeUrl) => {
107
- const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
108
- const onMessage = (ev) => {
109
- if (ev.data?.type === "integrations-oauth-complete") {
110
- window.removeEventListener("message", onMessage);
111
- void load(true);
112
- }
113
- };
114
- window.addEventListener("message", onMessage);
115
- const poll = window.setInterval(() => {
116
- if (popup?.closed) {
117
- window.clearInterval(poll);
118
- window.removeEventListener("message", onMessage);
119
- void load(true);
120
- }
121
- }, 1e3);
122
- });
110
+ return oauthAuthorizeUrl(id).then(openAuthorisation);
123
111
  }
124
112
  async function submit() {
125
113
  if (!pickedType.value || !form.name.trim()) return;
@@ -138,7 +126,7 @@ async function submit() {
138
126
  emit("created", created.id);
139
127
  emit("update:open", false);
140
128
  } catch (e) {
141
- toast.error(e?.message ?? t("integration.credentials.createDialog.error.create"));
129
+ toast.error(errorReason(e, t("integration.credentials.createDialog.error.create")));
142
130
  } finally {
143
131
  saving.value = false;
144
132
  }
@@ -30,7 +30,7 @@ import {
30
30
  SelectValue
31
31
  } from "@revenexx/studio";
32
32
  import { toast } from "../../utils/notify";
33
- import { apiFieldErrors, refusalReason } from "../../utils/apiError";
33
+ import { apiFieldErrors, errorReason } from "../../utils/apiError";
34
34
  import { credentialState, relativeTime } from "../../utils/credentialState";
35
35
  import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
36
36
  import { resolveLocalized } from "../../utils/localize";
@@ -122,7 +122,7 @@ async function save() {
122
122
  toast.success(t("integration.credentials.toast.updated"));
123
123
  } catch (e) {
124
124
  fieldErrors.value = apiFieldErrors(e);
125
- saveError.value = refusalReason(e, t("integration.credentials.error.save"));
125
+ saveError.value = errorReason(e, t("integration.credentials.error.save"));
126
126
  toast.error(saveError.value);
127
127
  } finally {
128
128
  saving.value = false;
@@ -139,36 +139,26 @@ async function destroy() {
139
139
  emit("changed");
140
140
  emit("update:open", false);
141
141
  } catch (e) {
142
- toast.error(e?.message ?? t("integration.credentials.error.delete"));
142
+ toast.error(errorReason(e, t("integration.credentials.error.delete")));
143
143
  } finally {
144
144
  deleting.value = false;
145
145
  }
146
146
  }
147
+ const { open: openAuthorisation, cancel: cancelAuthorisation } = useOAuthConnect(async (outcome) => {
148
+ await load(true);
149
+ emit("changed");
150
+ if (outcome === "completed") toast.success(t("integration.credentials.toast.connected"));
151
+ });
152
+ watch(() => props.open, (open) => {
153
+ if (!open) cancelAuthorisation();
154
+ });
147
155
  async function connect() {
148
156
  if (!props.credential) return;
149
157
  connecting.value = true;
150
158
  try {
151
- const authorizeUrl = await oauthAuthorizeUrl(props.credential.id);
152
- const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
153
- const onMessage = async (ev) => {
154
- if (ev.data?.type === "integrations-oauth-complete") {
155
- window.removeEventListener("message", onMessage);
156
- await load(true);
157
- emit("changed");
158
- toast.success(t("integration.credentials.toast.connected"));
159
- }
160
- };
161
- window.addEventListener("message", onMessage);
162
- const poll = window.setInterval(async () => {
163
- if (popup?.closed) {
164
- window.clearInterval(poll);
165
- window.removeEventListener("message", onMessage);
166
- await load(true);
167
- emit("changed");
168
- }
169
- }, 1e3);
159
+ openAuthorisation(await oauthAuthorizeUrl(props.credential.id));
170
160
  } catch (e) {
171
- toast.error(e?.message ?? t("integration.credentials.error.oauthConnect"));
161
+ toast.error(errorReason(e, t("integration.credentials.error.oauthConnect")));
172
162
  } finally {
173
163
  connecting.value = false;
174
164
  }