@revenexx/studio-integrations 2.0.0 → 2.1.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 +135 -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 +20 -5
  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,140 @@
1
1
  # @revenexx/studio-integrations
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6ff6833: A workflow can be exported to a file and imported again, into the same environment or
8
+ another: Export as file in a workflow's row actions and in the editor's actions menu, Import
9
+ from file in the New workflow dialog, and Import from file… in the editor to replace a
10
+ workflow's content. The file never carries credentials, secret values or a webhook's
11
+ address; an import creates a paused workflow, or replaces the editor's content as
12
+ unsaved changes that keep each webhook's address and leave an active workflow running
13
+ until a valid version is saved (PO-525).
14
+ - 9721bc2: PO-537: inside a palette folder, the nodes stand in the order their package gives
15
+ them rather than in the alphabetical order of their identifiers.
16
+
17
+ A node that states its place (`paletteOrder` in its manifest) comes first; the rest
18
+ follow in the order the package lists them, which the Integrations API now reports
19
+ as `package_position`; a node for which neither is known still falls back to its
20
+ identifier. So "Send Email" leads Brevo's Email folder instead of "Cancel Scheduled
21
+ Email".
22
+
23
+ Where a folder holds nodes of several packages, each package orders only its own:
24
+ no node takes a place another package's node held. Against an API that does not
25
+ report `package_position` yet, folders keep today's order.
26
+
27
+ - 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.
28
+ - 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".
29
+ - 664725b: A mapped value in a node's setting can now be changed where it stands. Click it
30
+ to select it, then click it again or press Enter, and it opens as the expression
31
+ it stands for;
32
+ once the caret or the focus leaves it, it reads as its source again. Until now it
33
+ could only be deleted and written again, which for a condition meant retyping the
34
+ whole expression to correct one character (PO-544).
35
+ - 7fd02ea: The node palette's folders now stand alphabetically on every level, in the
36
+ language the studio is shown in and regardless of case. Until now a folder stood
37
+ where its first node's internal identifier fell in the catalogue, so Test Nodes
38
+ appeared between PostBin and Shipcloud. SFTP is no longer pinned directly after
39
+ FTP; the nodes inside a folder keep the order their package gives them (PO-545).
40
+ - 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.
41
+
42
+ ### Patch Changes
43
+
44
+ - ddcbdbd: The Schedules page says why a schedule will not fire, in the Next run column: its
45
+ workflow is inactive or not built, its schedule cannot be read, its timezone is
46
+ unknown, or its rhythm never comes round. Until now every one of these read as a
47
+ bare dash beside a live badge. A paused schedule and one with no schedule set keep
48
+ the dash, because the row already says so (PO-261).
49
+ - ebbc148: Nothing a user sees changes. The module now declares `@solar-icons/vue`, which its
50
+ components already import through `@solar-icons/nuxt`. A host that typechecks and
51
+ sees every `#solar-icons` import reported as a missing export has to declare
52
+ `@solar-icons/vue` itself: the type declaration is generated into the host's own
53
+ `.nuxt`, so it is resolved from the host's dependencies, not this module's (PO-281).
54
+ - fd5a78e: Nothing a user sees changes. The module is linted now, and the first run's fixes ship
55
+ in it: unused imports and dead helpers removed, the pattern that finds `${{ … }}` in a
56
+ setting no longer slows down on long unclosed input, and the OAuth popup renders one
57
+ root element (PO-305).
58
+ - d9f4742: Cancelling a run from its row in either run list now asks first, the way terminating
59
+ already did and the way the run's record asks before both. A cancelled run cannot be
60
+ resumed, only retried from the start, so a slip in the row's menu was as final as a
61
+ terminate (PO-336).
62
+ - 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.
63
+ - 90d9ae3: "How long ago" reads the same everywhere it is shown: the dashboard's Latest executions and
64
+ Needs attention panels, the runs list's Started column and a credential's last
65
+ check. Under ten seconds is "just now", from there to a minute it counts seconds
66
+ ("40s ago") — the dashboard used to say "just now" for the whole minute — and on
67
+ the dashboard and the runs list that count keeps moving between realtime events.
68
+ From thirty days on it is a date in the studio's language instead of a count like
69
+ "412d ago". A timestamp that cannot be read shows "—"; the dashboard used to print
70
+ "NaNd ago" (PO-393).
71
+ - 60ef22a: The Shipcloud package's node icons draw as icons rather than as grey boxes.
72
+
73
+ A node declares its icon as an `mdi:` name and the studio renders Lucide, so the
74
+ two vocabularies are bridged by a table. A name in neither the table nor Lucide's
75
+ own spelling silently becomes a generic box — which is what the Shipcloud package
76
+ got for its nodes. Eight names now map: `truck-delivery`,
77
+ `package-variant-closed-plus`, `map-marker-path`, `package-variant-remove`,
78
+ `truck-check-outline`, `file-document-outline`, `file-plus-outline` and
79
+ `map-marker-radius-outline`.
80
+
81
+ The table also has a test now, including one that fails on an entry pointing at a
82
+ Lucide component that does not exist: a wrong entry looks exactly like a missing
83
+ one, and neither says anything.
84
+
85
+ - 78e99d6: A number typed into a setting's mapped-value mode is kept as a number, not as text
86
+ (PO-521).
87
+
88
+ **A numeric setting has two boxes and only one of them kept the number.** The plain
89
+ one wrote what was typed as a number; the box beside it — the one that also takes a
90
+ value mapped from elsewhere in the workflow — wrote the raw text through. Brightness
91
+ 50 was stored as `"50"`, the workflow saved without a word, and the run stopped with
92
+ `Config field 'brightness' must be of type number.` Nothing in that field could fix
93
+ it, because the field itself is what produced the text. Reported from the Philips Hue
94
+ **Set Light** node, where every value the author touched was text and the one they
95
+ left at its default was a number.
96
+
97
+ The rule now lives in one place: a numeric setting keeps the number, an emptied box
98
+ is no value rather than `''`, a mapped value is left exactly as it was typed, and
99
+ text denoting no number is kept as typed so the operator sees what they wrote rather
100
+ than finding the box silently emptied. Text in one of the three base notations —
101
+ `0x10`, `0b101`, `0o17` — counts as no number here, since the platform refuses it on
102
+ the way in and storing 16, 5 or 15 would put a value in the workflow nobody typed.
103
+
104
+ **When the conversion happens differs by box, and has to.** The plain one converts as
105
+ it is typed. The mapped-value box shows a document rather than a line of text, and it
106
+ re-projects that document whenever it disagrees with the value it is given — so it
107
+ stores what was typed and converts when it is left. Converting there on every
108
+ keystroke would rebuild the document under the operator's cursor: a number typed over
109
+ what the box already held would keep only its last digit, and `0.4` could never be
110
+ typed at all, because half-way through it is `0.` — a number worth `0`, whose text has
111
+ no dot in it. The range a
112
+ setting allows is read through the same rule, so a workflow **saved before this
113
+ change** — its number still stored as text — is marked in the dialog instead of
114
+ reading as sound until the run refuses it.
115
+
116
+ A yes/no setting has the same shape of defect and is deliberately untouched; it is
117
+ recorded in the Gaps of `specs/node-configuration.md`, with why.
118
+
119
+ The platform half of PO-521 ships separately: cross-validation now reads a numeric
120
+ setting's value as the number it denotes, which is what lets the workflows already
121
+ saved this way run again without anything rewriting them.
122
+
123
+ - 2632bae: A credential type's logo sits in a smaller box: 8 px larger than the symbol instead of
124
+ 16, so 24 px on the Credentials list instead of 32, with the type picker and the
125
+ credential dialogs shrinking in step. The generic key shown for a type without a logo
126
+ shrinks with it (PO-527).
127
+ - 1159568: PO-542: nodes whose icon the studio could not translate no longer draw as an
128
+ empty box.
129
+
130
+ Node packages name their icons in one vocabulary and the studio draws another, so
131
+ each name goes through a table; a name missing from it fell back to the generic box
132
+ without a word. The table now covers the names the packages ship today — all of
133
+ Philips Hue and PostBin, sixteen Brevo nodes, the notifications nodes, and one each
134
+ in core and DeepL. The four chat brands (Discord, Microsoft Teams, Telegram,
135
+ WhatsApp) have no counterpart in the drawing set and get a neutral messaging
136
+ symbol; their vendor logo still marks them.
137
+
3
138
  ## 2.0.0
4
139
 
5
140
  ### 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.1.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;