@revenexx/studio-integrations 2.0.0 → 2.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 (73) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/README.md +1 -0
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -6
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +134 -131
  6. package/dist/runtime/components/Integrations/CredentialLogo.vue +1 -1
  7. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +1 -1
  8. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +6 -6
  9. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +2 -0
  10. package/dist/runtime/components/Integrations/ExpressionInput.vue +90 -11
  11. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +2 -0
  12. package/dist/runtime/components/Integrations/GroupFrameView.vue +121 -121
  13. package/dist/runtime/components/Integrations/NodeConfigField.vue +28 -9
  14. package/dist/runtime/components/Integrations/NodeInspector.vue +44 -41
  15. package/dist/runtime/components/Integrations/NodePalette.vue +52 -37
  16. package/dist/runtime/components/Integrations/RunActionsMenu.vue +6 -5
  17. package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +7 -0
  18. package/dist/runtime/components/Integrations/RunDetailPanel.vue +14 -2
  19. package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +7 -0
  20. package/dist/runtime/components/Integrations/RunsTable.vue +7 -26
  21. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -0
  22. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +10 -6
  23. package/dist/runtime/components/Integrations/WorkflowEditor.vue +224 -225
  24. package/dist/runtime/components/Integrations/WorkflowExportDialog.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/WorkflowExportDialog.vue +71 -0
  26. package/dist/runtime/components/Integrations/WorkflowExportDialog.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/WorkflowImportFileStep.d.vue.ts +8 -0
  28. package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue +95 -0
  29. package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue.d.ts +8 -0
  30. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +5 -4
  31. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +0 -7
  32. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +2 -2
  33. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +2 -3
  34. package/dist/runtime/components/Integrations/WorkflowsTable.vue +14 -8
  35. package/dist/runtime/composables/useIntegrationsNodes.js +4 -1
  36. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +3 -1
  37. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -22
  38. package/dist/runtime/composables/useNow.d.ts +17 -0
  39. package/dist/runtime/composables/useNow.js +16 -0
  40. package/dist/runtime/composables/useWorkflowFile.d.ts +21 -0
  41. package/dist/runtime/composables/useWorkflowFile.js +63 -0
  42. package/dist/runtime/data/expressionEditor.d.ts +11 -1
  43. package/dist/runtime/data/expressionEditor.js +2 -1
  44. package/dist/runtime/data/nodeIcons.d.ts +14 -0
  45. package/dist/runtime/data/nodeIcons.js +57 -14
  46. package/dist/runtime/data/nodeRunData.js +1 -1
  47. package/dist/runtime/data/payloadForm.js +1 -1
  48. package/dist/runtime/i18n/messages/de.json +42 -0
  49. package/dist/runtime/i18n/messages/en.json +42 -0
  50. package/dist/runtime/pages/integrations/audit.vue +3 -3
  51. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +2 -2
  52. package/dist/runtime/pages/integrations/index.vue +5 -10
  53. package/dist/runtime/pages/integrations/schedules.vue +13 -1
  54. package/dist/runtime/pages/integrations/templates/index.vue +40 -40
  55. package/dist/runtime/pages/integrations/workflows/[id].vue +324 -244
  56. package/dist/runtime/types/integrations.d.ts +13 -0
  57. package/dist/runtime/utils/credentialState.d.ts +0 -7
  58. package/dist/runtime/utils/credentialState.js +0 -12
  59. package/dist/runtime/utils/localize.d.ts +12 -0
  60. package/dist/runtime/utils/localize.js +1 -1
  61. package/dist/runtime/utils/paletteOrder.d.ts +29 -0
  62. package/dist/runtime/utils/paletteOrder.js +46 -0
  63. package/dist/runtime/utils/relativeTime.d.ts +29 -0
  64. package/dist/runtime/utils/relativeTime.js +23 -0
  65. package/dist/runtime/utils/runFeedQuery.d.ts +12 -6
  66. package/dist/runtime/utils/runFeedQuery.js +3 -18
  67. package/dist/runtime/utils/settingValue.d.ts +33 -0
  68. package/dist/runtime/utils/settingValue.js +8 -0
  69. package/dist/runtime/utils/statusStyle.d.ts +8 -8
  70. package/dist/runtime/utils/statusStyle.js +4 -1
  71. package/dist/runtime/utils/workflowFile.d.ts +119 -0
  72. package/dist/runtime/utils/workflowFile.js +192 -0
  73. package/package.json +8 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,146 @@
1
1
  # @revenexx/studio-integrations
2
2
 
3
+ ## 2.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - deb3bf6: A setting takes a value mapped from an earlier step only where its node says it may (PO-548). Every choice, number and switch used to offer the `{{ }}` switch whatever the node declared — so XML's parse-or-build, a decision somebody has to make, could be mapped; now only a setting that declares `expressionAllowed` offers it. And one that does keeps its own control — the list, the number box, the switch — with the `{{ }}` switch beside it, instead of being drawn as the mapping box from the start. A value mapped into a setting before this stays shown, with the switch back to the regular control.
8
+
9
+ ## 2.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 6ff6833: A workflow can be exported to a file and imported again, into the same environment or
14
+ another: Export as file in a workflow's row actions and in the editor's actions menu, Import
15
+ from file in the New workflow dialog, and Import from file… in the editor to replace a
16
+ workflow's content. The file never carries credentials, secret values or a webhook's
17
+ address; an import creates a paused workflow, or replaces the editor's content as
18
+ unsaved changes that keep each webhook's address and leave an active workflow running
19
+ until a valid version is saved (PO-525).
20
+ - 9721bc2: PO-537: inside a palette folder, the nodes stand in the order their package gives
21
+ them rather than in the alphabetical order of their identifiers.
22
+
23
+ A node that states its place (`paletteOrder` in its manifest) comes first; the rest
24
+ follow in the order the package lists them, which the Integrations API now reports
25
+ as `package_position`; a node for which neither is known still falls back to its
26
+ identifier. So "Send Email" leads Brevo's Email folder instead of "Cancel Scheduled
27
+ Email".
28
+
29
+ Where a folder holds nodes of several packages, each package orders only its own:
30
+ no node takes a place another package's node held. Against an API that does not
31
+ report `package_position` yet, folders keep today's order.
32
+
33
+ - 1820289: A run's record now shows what the run returned per step: one block for each step, under the name it carries on the canvas, in the order of the steps in the workflow — the way the dialog after a run already shows it. Before, the record printed the result as one object keyed by internal node ids, in an order the run never took. A result the workflow's steps cannot account for, because the workflow has changed since the run, say, is still shown whole.
34
+ - 6cc8b71: A node's optional settings fold only after its last required setting. Before, every optional setting from the fourth on was folded wherever it stood, so a setting between two required ones vanished from the middle of the form, and one "Show n optional settings" control opened settings in two separate places. Everything up to the last required setting is now always shown, and the fold is one tail at the end — Brevo's Double Opt-in, for one, now shows "Never on lists" and folds only "Attributes".
35
+ - 664725b: A mapped value in a node's setting can now be changed where it stands. Click it
36
+ to select it, then click it again or press Enter, and it opens as the expression
37
+ it stands for;
38
+ once the caret or the focus leaves it, it reads as its source again. Until now it
39
+ could only be deleted and written again, which for a condition meant retyping the
40
+ whole expression to correct one character (PO-544).
41
+ - 7fd02ea: The node palette's folders now stand alphabetically on every level, in the
42
+ language the studio is shown in and regardless of case. Until now a folder stood
43
+ where its first node's internal identifier fell in the catalogue, so Test Nodes
44
+ appeared between PostBin and Shipcloud. SFTP is no longer pinned directly after
45
+ FTP; the nodes inside a folder keep the order their package gives them (PO-545).
46
+ - 7525758: A Core folder in the node palette is one folder in every language (PO-546). The studio now names the Core folders it knows — Flow, Data, Text & Formats, Files, Web, Messaging, FTP / SFTP — from its own translations, by their English name, so a node that names no folder, or one an older release left registered with an out-of-date name, no longer opens a second folder (_Data_ beside _Daten_, _Flow_ beside _Ablauf_). Every one of them has its own icon. A path of two levels no longer files a node under Integrations — only an app package or a logo does — so Core can nest a folder; a subfolder without a logo shows the first image its nodes carry.
47
+
48
+ ### Patch Changes
49
+
50
+ - ddcbdbd: The Schedules page says why a schedule will not fire, in the Next run column: its
51
+ workflow is inactive or not built, its schedule cannot be read, its timezone is
52
+ unknown, or its rhythm never comes round. Until now every one of these read as a
53
+ bare dash beside a live badge. A paused schedule and one with no schedule set keep
54
+ the dash, because the row already says so (PO-261).
55
+ - ebbc148: Nothing a user sees changes. The module now declares `@solar-icons/vue`, which its
56
+ components already import through `@solar-icons/nuxt`. A host that typechecks and
57
+ sees every `#solar-icons` import reported as a missing export has to declare
58
+ `@solar-icons/vue` itself: the type declaration is generated into the host's own
59
+ `.nuxt`, so it is resolved from the host's dependencies, not this module's (PO-281).
60
+ - fd5a78e: Nothing a user sees changes. The module is linted now, and the first run's fixes ship
61
+ in it: unused imports and dead helpers removed, the pattern that finds `${{ … }}` in a
62
+ setting no longer slows down on long unclosed input, and the OAuth popup renders one
63
+ root element (PO-305).
64
+ - d9f4742: Cancelling a run from its row in either run list now asks first, the way terminating
65
+ already did and the way the run's record asks before both. A cancelled run cannot be
66
+ resumed, only retried from the start, so a slip in the row's menu was as final as a
67
+ terminate (PO-336).
68
+ - 271de84: A run the platform reports as `canceled` — Temporal's spelling — or in another letter case (`COMPLETED`) now reads the same in the Runs list as at its own record. The list used to draw such a run as _failed_ while the record called it _cancelled_ or _succeeded_; both now read the platform's status through one table.
69
+ - 90d9ae3: "How long ago" reads the same everywhere it is shown: the dashboard's Latest executions and
70
+ Needs attention panels, the runs list's Started column and a credential's last
71
+ check. Under ten seconds is "just now", from there to a minute it counts seconds
72
+ ("40s ago") — the dashboard used to say "just now" for the whole minute — and on
73
+ the dashboard and the runs list that count keeps moving between realtime events.
74
+ From thirty days on it is a date in the studio's language instead of a count like
75
+ "412d ago". A timestamp that cannot be read shows "—"; the dashboard used to print
76
+ "NaNd ago" (PO-393).
77
+ - 60ef22a: The Shipcloud package's node icons draw as icons rather than as grey boxes.
78
+
79
+ A node declares its icon as an `mdi:` name and the studio renders Lucide, so the
80
+ two vocabularies are bridged by a table. A name in neither the table nor Lucide's
81
+ own spelling silently becomes a generic box — which is what the Shipcloud package
82
+ got for its nodes. Eight names now map: `truck-delivery`,
83
+ `package-variant-closed-plus`, `map-marker-path`, `package-variant-remove`,
84
+ `truck-check-outline`, `file-document-outline`, `file-plus-outline` and
85
+ `map-marker-radius-outline`.
86
+
87
+ The table also has a test now, including one that fails on an entry pointing at a
88
+ Lucide component that does not exist: a wrong entry looks exactly like a missing
89
+ one, and neither says anything.
90
+
91
+ - 78e99d6: A number typed into a setting's mapped-value mode is kept as a number, not as text
92
+ (PO-521).
93
+
94
+ **A numeric setting has two boxes and only one of them kept the number.** The plain
95
+ one wrote what was typed as a number; the box beside it — the one that also takes a
96
+ value mapped from elsewhere in the workflow — wrote the raw text through. Brightness
97
+ 50 was stored as `"50"`, the workflow saved without a word, and the run stopped with
98
+ `Config field 'brightness' must be of type number.` Nothing in that field could fix
99
+ it, because the field itself is what produced the text. Reported from the Philips Hue
100
+ **Set Light** node, where every value the author touched was text and the one they
101
+ left at its default was a number.
102
+
103
+ The rule now lives in one place: a numeric setting keeps the number, an emptied box
104
+ is no value rather than `''`, a mapped value is left exactly as it was typed, and
105
+ text denoting no number is kept as typed so the operator sees what they wrote rather
106
+ than finding the box silently emptied. Text in one of the three base notations —
107
+ `0x10`, `0b101`, `0o17` — counts as no number here, since the platform refuses it on
108
+ the way in and storing 16, 5 or 15 would put a value in the workflow nobody typed.
109
+
110
+ **When the conversion happens differs by box, and has to.** The plain one converts as
111
+ it is typed. The mapped-value box shows a document rather than a line of text, and it
112
+ re-projects that document whenever it disagrees with the value it is given — so it
113
+ stores what was typed and converts when it is left. Converting there on every
114
+ keystroke would rebuild the document under the operator's cursor: a number typed over
115
+ what the box already held would keep only its last digit, and `0.4` could never be
116
+ typed at all, because half-way through it is `0.` — a number worth `0`, whose text has
117
+ no dot in it. The range a
118
+ setting allows is read through the same rule, so a workflow **saved before this
119
+ change** — its number still stored as text — is marked in the dialog instead of
120
+ reading as sound until the run refuses it.
121
+
122
+ A yes/no setting has the same shape of defect and is deliberately untouched; it is
123
+ recorded in the Gaps of `specs/node-configuration.md`, with why.
124
+
125
+ The platform half of PO-521 ships separately: cross-validation now reads a numeric
126
+ setting's value as the number it denotes, which is what lets the workflows already
127
+ saved this way run again without anything rewriting them.
128
+
129
+ - 2632bae: A credential type's logo sits in a smaller box: 8 px larger than the symbol instead of
130
+ 16, so 24 px on the Credentials list instead of 32, with the type picker and the
131
+ credential dialogs shrinking in step. The generic key shown for a type without a logo
132
+ shrinks with it (PO-527).
133
+ - 1159568: PO-542: nodes whose icon the studio could not translate no longer draw as an
134
+ empty box.
135
+
136
+ Node packages name their icons in one vocabulary and the studio draws another, so
137
+ each name goes through a table; a name missing from it fell back to the generic box
138
+ without a word. The table now covers the names the packages ship today — all of
139
+ Philips Hue and PostBin, sixteen Brevo nodes, the notifications nodes, and one each
140
+ in core and DeepL. The four chat brands (Discord, Microsoft Teams, Telegram,
141
+ WhatsApp) have no counterpart in the drawing set and get a neutral messaging
142
+ symbol; their vendor logo still marks them.
143
+
3
144
  ## 2.0.0
4
145
 
5
146
  ### Major Changes
package/README.md CHANGED
@@ -86,6 +86,7 @@ cockpit monorepo under `packages/studio-integrations`).
86
86
  ```bash
87
87
  pnpm install # runs the module `prepare` (nuxt-module-build)
88
88
  pnpm test # vitest
89
+ pnpm lint # eslint — `pnpm lint --fix` fixes what has a fix, layout above all
89
90
  pnpm build # dist/
90
91
  pnpm changeset # record what your change does to the version
91
92
  ```
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
3
  "configKey": "studioIntegrations",
4
- "version": "2.0.0",
4
+ "version": "2.2.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -149,16 +149,16 @@ async function submit() {
149
149
  {{ t("integration.credentials.createDialog.noTypeAvailable") }}
150
150
  </p>
151
151
  <button
152
- v-for="t in allowedTypes"
153
- :key="t.slug"
152
+ v-for="type in allowedTypes"
153
+ :key="type.slug"
154
154
  type="button"
155
155
  class="flex w-full items-center gap-2 rounded-lg border border-black/[0.08] p-3 text-left transition hover:border-black/20 hover:bg-muted/30"
156
- @click="pickType(t)"
156
+ @click="pickType(type)"
157
157
  >
158
- <IntegrationsCredentialLogo :type="t" :size="18" />
158
+ <IntegrationsCredentialLogo :type="type" :size="18" />
159
159
  <div class="min-w-0 truncate text-sm">
160
- <span class="font-semibold">{{ resolveLocalized(t.name) }}</span>
161
- <span class="ml-1.5 text-muted-foreground">({{ authKindLabel(t.auth_kind) }})</span>
160
+ <span class="font-semibold">{{ resolveLocalized(type.name) }}</span>
161
+ <span class="ml-1.5 text-muted-foreground">({{ authKindLabel(type.auth_kind) }})</span>
162
162
  </div>
163
163
  </button>
164
164
  </div>
@@ -31,7 +31,9 @@ import {
31
31
  } from "@revenexx/studio";
32
32
  import { toast } from "../../utils/notify";
33
33
  import { apiRefusal, errorReason } from "../../utils/apiError";
34
- import { credentialState, relativeTime } from "../../utils/credentialState";
34
+ import { credentialState } from "../../utils/credentialState";
35
+ import { relativeTime } from "../../utils/relativeTime";
36
+ import { useNow } from "../../composables/useNow";
35
37
  import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
36
38
  import { resolveLocalized } from "../../utils/localize";
37
39
  const props = defineProps({
@@ -41,6 +43,7 @@ const props = defineProps({
41
43
  });
42
44
  const emit = defineEmits(["update:open", "changed"]);
43
45
  const t = useIntegrationsT();
46
+ const now = useNow();
44
47
  const { update, remove, test, oauthAuthorizeUrl, load } = useIntegrationsCredentials();
45
48
  const tab = ref("credentials");
46
49
  const tabs = computed(() => [
@@ -204,167 +207,167 @@ async function connect() {
204
207
  scrolling pane and the block below it are siblings, as they were when the
205
208
  pane was the whole tab, so nothing about the spacing around them moves. -->
206
209
  <template v-if="tab === 'credentials'">
207
- <div class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
208
- <!-- Connection state — the headline answer: are these credentials correct? -->
209
- <div class="rounded-lg border p-3" :class="state.toneClass">
210
- <div class="flex items-center gap-2">
211
- <CheckCircle2 v-if="state.key === 'connected'" :size="16" class="shrink-0" />
212
- <XCircle v-else-if="state.key === 'failed'" :size="16" class="shrink-0" />
213
- <span class="font-medium">{{ state.label }}</span>
214
- <span v-if="state.at" class="text-sm opacity-70">· verified {{ relativeTime(state.at) }}</span>
215
- <div class="flex-1" />
216
- <Button
217
- v-if="isOAuthAuthcode"
218
- size="sm"
219
- variant="outline"
220
- class="bg-white/60"
221
- :disabled="connecting"
222
- @click="connect"
223
- >
224
- <Loader2 v-if="connecting" :size="13" class="mr-1 animate-spin" />
225
- {{ t("integration.credentials.detail.action.reconnect") }}
226
- </Button>
227
- </div>
228
- <!-- The stored sentence, and only while nothing fresher is on screen. The
210
+ <div class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
211
+ <!-- Connection state — the headline answer: are these credentials correct? -->
212
+ <div class="rounded-lg border p-3" :class="state.toneClass">
213
+ <div class="flex items-center gap-2">
214
+ <CheckCircle2 v-if="state.key === 'connected'" :size="16" class="shrink-0" />
215
+ <XCircle v-else-if="state.key === 'failed'" :size="16" class="shrink-0" />
216
+ <span class="font-medium">{{ state.label }}</span>
217
+ <span v-if="state.at" class="text-sm opacity-70">· verified {{ relativeTime(state.at, now) }}</span>
218
+ <div class="flex-1" />
219
+ <Button
220
+ v-if="isOAuthAuthcode"
221
+ size="sm"
222
+ variant="outline"
223
+ class="bg-white/60"
224
+ :disabled="connecting"
225
+ @click="connect"
226
+ >
227
+ <Loader2 v-if="connecting" :size="13" class="mr-1 animate-spin" />
228
+ {{ t("integration.credentials.detail.action.reconnect") }}
229
+ </Button>
230
+ </div>
231
+ <!-- The stored sentence, and only while nothing fresher is on screen. The
229
232
  live box below shows the result of a test just run, and the reload that
230
233
  follows it puts the very same sentence here — so both would print it
231
234
  twice, and past the length the platform truncates at, twice differently.
232
235
  The freshly returned one wins, being whole (specs/credential-test.md
233
236
  AC-1). -->
234
- <p v-if="state.message && !liveTest" class="mt-1 text-sm break-words opacity-90">
235
- {{ state.message }}
236
- </p>
237
- <p v-else-if="state.key === 'untested'" class="mt-1 text-sm opacity-80">
238
- {{ t("integration.credentials.detail.state.untestedHint") }}
239
- </p>
240
- </div>
237
+ <p v-if="state.message && !liveTest" class="mt-1 text-sm break-words opacity-90">
238
+ {{ state.message }}
239
+ </p>
240
+ <p v-else-if="state.key === 'untested'" class="mt-1 text-sm opacity-80">
241
+ {{ t("integration.credentials.detail.state.untestedHint") }}
242
+ </p>
243
+ </div>
241
244
 
242
- <!-- Editable fields -->
243
- <div class="space-y-3">
244
- <div class="space-y-1.5">
245
- <Label for="detail-cred-name">{{ t("integration.credentials.shared.field.name.label") }}</Label>
246
- <Input
247
- id="detail-cred-name"
248
- v-model="form.name"
249
- class="bg-white"
250
- :aria-invalid="fieldErrors.name ? 'true' : void 0"
251
- :aria-describedby="fieldErrors.name ? 'detail-cred-name-error' : void 0"
252
- />
253
- <p v-if="fieldErrors.name" id="detail-cred-name-error" class="text-sm text-rose-700">
254
- {{ fieldErrors.name }}
255
- </p>
256
- </div>
245
+ <!-- Editable fields -->
246
+ <div class="space-y-3">
247
+ <div class="space-y-1.5">
248
+ <Label for="detail-cred-name">{{ t("integration.credentials.shared.field.name.label") }}</Label>
249
+ <Input
250
+ id="detail-cred-name"
251
+ v-model="form.name"
252
+ class="bg-white"
253
+ :aria-invalid="fieldErrors.name ? 'true' : void 0"
254
+ :aria-describedby="fieldErrors.name ? 'detail-cred-name-error' : void 0"
255
+ />
256
+ <p v-if="fieldErrors.name" id="detail-cred-name-error" class="text-sm text-rose-700">
257
+ {{ fieldErrors.name }}
258
+ </p>
259
+ </div>
257
260
 
258
- <!-- "Use revenexx …" source picker (SFTP / Mail / API key) — mounted
261
+ <!-- "Use revenexx …" source picker (SFTP / Mail / API key) — mounted
259
262
  only for the types it serves, which is tidiness rather than
260
263
  protection: it renders nothing for any other type anyway. It used
261
264
  to be protection, because the component resolved cockpit-only
262
265
  sources at setup and mounting it elsewhere took this form down;
263
266
  since PO-222 it asks instead (`utils/cockpitSources.ts`). -->
264
- <IntegrationsCredentialRevenexxSource
265
- v-if="hasRevenexxSource(type?.slug)"
266
- :type="type"
267
- @update:active="(v) => {
267
+ <IntegrationsCredentialRevenexxSource
268
+ v-if="hasRevenexxSource(type?.slug)"
269
+ :type="type"
270
+ @update:active="(v) => {
268
271
  sourceActive = v;
269
272
  clearRefusal();
270
273
  }"
271
- @apply="(c) => Object.assign(form.config, c)"
272
- />
274
+ @apply="(c) => Object.assign(form.config, c)"
275
+ />
273
276
 
274
- <!-- Each label names its own control (`for`/`id`): without it a
277
+ <!-- Each label names its own control (`for`/`id`): without it a
275
278
  screen reader reads the field as unlabelled, and the label text
276
279
  cannot be used to reach it at all. -->
277
- <div v-for="f in fieldsOf(type)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
278
- <Label :for="`detail-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
280
+ <div v-for="f in fieldsOf(type)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
281
+ <Label :for="`detail-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
279
282
 
280
- <Input
281
- v-if="f.type === 'secret'"
282
- :id="`detail-field-${f.key}`"
283
- :model-value="form.config[f.key] ?? ''"
284
- type="password"
285
- autocomplete="new-password"
286
- :placeholder="t('integration.credentials.shared.field.secret.placeholder')"
287
- class="bg-white"
288
- v-bind="configFieldAria(f.key)"
289
- @update:model-value="(v) => form.config[f.key] = v"
290
- />
291
- <Input
292
- v-else-if="f.type === 'number'"
293
- :id="`detail-field-${f.key}`"
294
- type="number"
295
- :model-value="form.config[f.key] ?? ''"
296
- class="bg-white"
297
- v-bind="configFieldAria(f.key)"
298
- @update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
299
- />
300
- <div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
301
- <Switch :id="`detail-field-${f.key}`" :model-value="Boolean(form.config[f.key])" v-bind="configFieldAria(f.key)" @update:model-value="(v) => form.config[f.key] = v" />
302
- </div>
303
- <Select
304
- v-else-if="f.type === 'select'"
305
- :model-value="form.config[f.key] ?? ''"
306
- @update:model-value="(v) => form.config[f.key] = v"
307
- >
308
- <SelectTrigger :id="`detail-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
309
- <SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
310
- </SelectTrigger>
311
- <SelectContent>
312
- <SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
313
- {{ resolveLocalized(opt.label) }}
314
- </SelectItem>
315
- </SelectContent>
316
- </Select>
317
- <Input
318
- v-else
319
- :id="`detail-field-${f.key}`"
320
- :model-value="form.config[f.key] ?? ''"
321
- class="bg-white"
322
- v-bind="configFieldAria(f.key)"
323
- @update:model-value="(v) => form.config[f.key] = v"
324
- />
283
+ <Input
284
+ v-if="f.type === 'secret'"
285
+ :id="`detail-field-${f.key}`"
286
+ :model-value="form.config[f.key] ?? ''"
287
+ type="password"
288
+ autocomplete="new-password"
289
+ :placeholder="t('integration.credentials.shared.field.secret.placeholder')"
290
+ class="bg-white"
291
+ v-bind="configFieldAria(f.key)"
292
+ @update:model-value="(v) => form.config[f.key] = v"
293
+ />
294
+ <Input
295
+ v-else-if="f.type === 'number'"
296
+ :id="`detail-field-${f.key}`"
297
+ type="number"
298
+ :model-value="form.config[f.key] ?? ''"
299
+ class="bg-white"
300
+ v-bind="configFieldAria(f.key)"
301
+ @update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
302
+ />
303
+ <div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
304
+ <Switch :id="`detail-field-${f.key}`" :model-value="Boolean(form.config[f.key])" v-bind="configFieldAria(f.key)" @update:model-value="(v) => form.config[f.key] = v" />
305
+ </div>
306
+ <Select
307
+ v-else-if="f.type === 'select'"
308
+ :model-value="form.config[f.key] ?? ''"
309
+ @update:model-value="(v) => form.config[f.key] = v"
310
+ >
311
+ <SelectTrigger :id="`detail-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
312
+ <SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
313
+ </SelectTrigger>
314
+ <SelectContent>
315
+ <SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
316
+ {{ resolveLocalized(opt.label) }}
317
+ </SelectItem>
318
+ </SelectContent>
319
+ </Select>
320
+ <Input
321
+ v-else
322
+ :id="`detail-field-${f.key}`"
323
+ :model-value="form.config[f.key] ?? ''"
324
+ class="bg-white"
325
+ v-bind="configFieldAria(f.key)"
326
+ @update:model-value="(v) => form.config[f.key] = v"
327
+ />
325
328
 
326
- <p v-if="fieldErrors[`config.${f.key}`]" :id="`detail-field-${f.key}-error`" class="text-sm text-rose-700">
327
- {{ fieldErrors[`config.${f.key}`] }}
328
- </p>
329
- </div>
329
+ <p v-if="fieldErrors[`config.${f.key}`]" :id="`detail-field-${f.key}-error`" class="text-sm text-rose-700">
330
+ {{ fieldErrors[`config.${f.key}`] }}
331
+ </p>
332
+ </div>
330
333
 
331
- <!-- Live test feedback (from the test the operator just ran) -->
332
- <div
333
- v-if="liveTest"
334
- class="rounded-md px-3 py-2 text-sm"
335
- :class="liveTest.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
336
- >
337
- {{ liveTest.ok ? liveTest.message ?? t("integration.credentials.shared.test.ok") : liveTest.error ?? liveTest.message ?? t("integration.credentials.shared.test.failed") }}
338
- </div>
334
+ <!-- Live test feedback (from the test the operator just ran) -->
335
+ <div
336
+ v-if="liveTest"
337
+ class="rounded-md px-3 py-2 text-sm"
338
+ :class="liveTest.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
339
+ >
340
+ {{ liveTest.ok ? liveTest.message ?? t("integration.credentials.shared.test.ok") : liveTest.error ?? liveTest.message ?? t("integration.credentials.shared.test.failed") }}
341
+ </div>
339
342
 
340
- </div>
343
+ </div>
341
344
 
342
- </div>
345
+ </div>
343
346
 
344
- <!-- Outside the scrolling pane on purpose, the way the create dialog has it:
347
+ <!-- Outside the scrolling pane on purpose, the way the create dialog has it:
345
348
  a refusal puts the operator in the first marked field, which scrolls that
346
349
  pane back to the top — and a summary rendered at the end of the form would
347
350
  be carried out of sight by the very focus meant to make the refusal
348
351
  visible. What is marked at a field is not repeated here; what has no field
349
352
  to be marked at would otherwise be silent. -->
350
- <div v-if="summaryReasons.length" role="alert" class="mt-3 space-y-1 rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
351
- <!-- Keyed by position: two undrawn keys can be refused with the same
353
+ <div v-if="summaryReasons.length" role="alert" class="mt-3 space-y-1 rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
354
+ <!-- Keyed by position: two undrawn keys can be refused with the same
352
355
  sentence, and a duplicate key is how one of them goes unrendered. -->
353
- <p v-for="(reason, i) in summaryReasons" :key="i">{{ reason }}</p>
354
- </div>
356
+ <p v-for="(reason, i) in summaryReasons" :key="i">{{ reason }}</p>
357
+ </div>
355
358
 
356
- <div class="flex items-center gap-2 pt-3">
357
- <div class="flex-1" />
358
- <Button variant="outline" :disabled="testing || saving" @click="runTest">
359
- <Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
360
- <TestIcon v-else :size="14" class="mr-1" />
361
- {{ t("integration.credentials.shared.action.testConnection") }}
362
- </Button>
363
- <Button :disabled="saving || !form.name.trim()" @click="save">
364
- <Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
365
- {{ t("integration.common.action.save") }}
366
- </Button>
367
- </div>
359
+ <div class="flex items-center gap-2 pt-3">
360
+ <div class="flex-1" />
361
+ <Button variant="outline" :disabled="testing || saving" @click="runTest">
362
+ <Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
363
+ <TestIcon v-else :size="14" class="mr-1" />
364
+ {{ t("integration.credentials.shared.action.testConnection") }}
365
+ </Button>
366
+ <Button :disabled="saving || !form.name.trim()" @click="save">
367
+ <Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
368
+ {{ t("integration.common.action.save") }}
369
+ </Button>
370
+ </div>
368
371
  </template>
369
372
 
370
373
  <!-- Tab 2: Details -->
@@ -5,7 +5,7 @@ const props = defineProps({
5
5
  type: { type: [Object, null], required: false },
6
6
  size: { type: Number, required: false }
7
7
  });
8
- const box = computed(() => (props.size ?? 16) + 16);
8
+ const box = computed(() => (props.size ?? 16) + 8);
9
9
  const glyph = computed(() => props.size ?? 16);
10
10
  const logoSrc = computed(() => {
11
11
  const imgs = props.type?.images;
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, reactive, ref, useId, watch } from "vue";
2
+ import { computed, ref, useId, watch } from "vue";
3
3
  import {
4
4
  AddCircle as Plus,
5
5
  CheckCircle as Check,
@@ -51,16 +51,16 @@ const filtered = computed(() => {
51
51
  {{ t("integration.credentials.typePicker.empty", { query }) }}
52
52
  </p>
53
53
  <button
54
- v-for="t in filtered"
55
- :key="t.slug"
54
+ v-for="type in filtered"
55
+ :key="type.slug"
56
56
  type="button"
57
57
  class="flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left transition hover:bg-muted/50"
58
- @click="emit('pick', t)"
58
+ @click="emit('pick', type)"
59
59
  >
60
- <IntegrationsCredentialLogo :type="t" :size="16" />
60
+ <IntegrationsCredentialLogo :type="type" :size="16" />
61
61
  <div class="min-w-0 flex-1 truncate text-sm">
62
- <span class="font-medium">{{ resolveLocalized(t.name) || t.slug }}</span>
63
- <span class="ml-1.5 text-muted-foreground">({{ authKindLabel(t.auth_kind) }})</span>
62
+ <span class="font-medium">{{ resolveLocalized(type.name) || type.slug }}</span>
63
+ <span class="ml-1.5 text-muted-foreground">({{ authKindLabel(type.auth_kind) }})</span>
64
64
  </div>
65
65
  </button>
66
66
  </div>
@@ -21,7 +21,9 @@ type __VLS_Props = {
21
21
  };
22
22
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
23
23
  "update:modelValue": (value: string) => any;
24
+ blur: () => any;
24
25
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
26
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
27
+ onBlur?: (() => any) | undefined;
26
28
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
27
29
  export default _default;