@revenexx/studio-integrations 0.5.1 → 0.6.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 (129) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +17 -4
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
  7. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
  8. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  9. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
  10. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  11. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
  13. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  14. package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
  15. package/dist/runtime/components/Integrations/Layout.vue +21 -2
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
  18. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  19. package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  21. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
  23. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  24. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  25. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  26. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  27. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  28. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  29. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  30. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  31. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
  32. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  33. package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
  34. package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
  35. package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
  36. package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
  38. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  39. package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
  40. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  41. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  42. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
  43. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  44. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
  45. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  46. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
  47. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  48. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  49. package/dist/runtime/components/Integrations/WorkflowEditor.vue +167 -46
  50. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  51. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
  52. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  53. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
  54. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  55. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  56. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
  57. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  58. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
  59. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
  60. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  61. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
  62. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  63. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
  64. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
  65. package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
  66. package/dist/runtime/composables/useCredentialTypes.js +4 -4
  67. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  68. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
  69. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  70. package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
  71. package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
  72. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  73. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  74. package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
  75. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
  76. package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
  77. package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
  78. package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
  79. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  80. package/dist/runtime/composables/useLoadGate.js +8 -0
  81. package/dist/runtime/data/branchReorder.d.ts +37 -0
  82. package/dist/runtime/data/branchReorder.js +71 -0
  83. package/dist/runtime/data/handleId.d.ts +23 -0
  84. package/dist/runtime/data/handleId.js +6 -0
  85. package/dist/runtime/data/integrationTemplates.d.ts +39 -0
  86. package/dist/runtime/data/integrationTemplates.js +15 -0
  87. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  88. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  89. package/dist/runtime/data/nodeIcons.js +73 -0
  90. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  91. package/dist/runtime/data/nodeInputData.js +4 -0
  92. package/dist/runtime/data/payloadForm.d.ts +9 -2
  93. package/dist/runtime/data/payloadForm.js +27 -3
  94. package/dist/runtime/data/portTone.d.ts +17 -0
  95. package/dist/runtime/data/portTone.js +7 -0
  96. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  97. package/dist/runtime/data/webhookAuth.js +19 -0
  98. package/dist/runtime/data/workflowIconCatalog.js +2 -5
  99. package/dist/runtime/pages/integrations/audit.vue +91 -6
  100. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
  101. package/dist/runtime/pages/integrations/credentials.vue +119 -21
  102. package/dist/runtime/pages/integrations/index.vue +49 -44
  103. package/dist/runtime/pages/integrations/runs.vue +1 -1
  104. package/dist/runtime/pages/integrations/schedules.vue +51 -7
  105. package/dist/runtime/pages/integrations/secrets.vue +29 -7
  106. package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
  107. package/dist/runtime/pages/integrations/templates/index.vue +57 -86
  108. package/dist/runtime/pages/integrations/workflows/[id].vue +196 -8
  109. package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
  110. package/dist/runtime/types/integrations.d.ts +65 -6
  111. package/dist/runtime/utils/apiError.d.ts +47 -0
  112. package/dist/runtime/utils/apiError.js +32 -0
  113. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  114. package/dist/runtime/utils/cockpitSources.js +14 -0
  115. package/dist/runtime/utils/loadGate.d.ts +61 -0
  116. package/dist/runtime/utils/loadGate.js +22 -0
  117. package/dist/runtime/utils/localize.d.ts +29 -0
  118. package/dist/runtime/utils/localize.js +4 -0
  119. package/dist/runtime/utils/notify.d.ts +59 -0
  120. package/dist/runtime/utils/notify.js +21 -0
  121. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  122. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  123. package/dist/runtime/utils/schedule.d.ts +141 -0
  124. package/dist/runtime/utils/schedule.js +207 -0
  125. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  126. package/dist/runtime/utils/validationFindings.js +53 -0
  127. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  128. package/dist/runtime/utils/whenIdle.js +12 -0
  129. package/package.json +8 -2
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, reactive, ref, watch } from "vue";
2
+ import { computed, reactive, ref, useId, watch } from "vue";
3
3
  import {
4
4
  AddCircle as Plus,
5
5
  CheckCircle as Check,
@@ -23,16 +23,14 @@ import {
23
23
  SelectValue
24
24
  } from "@revenexx/studio";
25
25
  import { toast } from "vue-sonner";
26
+ import { REVENEXX_SOURCE_KIND_BY_SLUG } from "../../utils/revenexxCredentialSource";
27
+ import { cockpitSources } from "../../utils/cockpitSources";
26
28
  const props = defineProps({
27
29
  type: { type: [Object, null], required: true }
28
30
  });
29
31
  const emit = defineEmits(["update:active", "apply"]);
30
- const KIND_BY_SLUG = {
31
- "revenexx:sftp": "sftp",
32
- "revenexx:smtp": "smtp",
33
- "revenexx:api": "api"
34
- };
35
- const kind = computed(() => props.type ? KIND_BY_SLUG[props.type.slug] ?? null : null);
32
+ const kind = computed(() => props.type ? REVENEXX_SOURCE_KIND_BY_SLUG[props.type.slug] ?? null : null);
33
+ const uid = useId();
36
34
  const SWITCH_LABEL = {
37
35
  sftp: "Use revenexx SFTP Servers",
38
36
  smtp: "Use revenexx Mail Servers",
@@ -43,11 +41,18 @@ const NEW_LABEL = {
43
41
  smtp: "New mail credential",
44
42
  api: "New API key"
45
43
  };
46
- const { tenants, onlyTenantSlug, load: loadTenants } = useConsoleTenants();
47
- const { markets, activeCode, isGlobal, GLOBAL_MARKET_CODE, fetchMarkets } = useActiveMarket();
48
- const sftp = useConsoleSftp();
49
- const mail = useMailAdmin();
50
- const consoleKeys = useConsoleApiKeys();
44
+ const sources = cockpitSources();
45
+ const available = sources !== null;
46
+ const tenants = computed(() => sources?.tenants.tenants.value ?? []);
47
+ const onlyTenantSlug = computed(() => sources?.tenants.onlyTenantSlug.value ?? null);
48
+ const loadTenants = () => sources?.tenants.load() ?? Promise.resolve();
49
+ const markets = computed(() => sources?.market.markets.value ?? []);
50
+ const activeCode = computed(() => sources?.market.activeCode.value ?? "");
51
+ const fetchMarkets = () => sources?.market.fetchMarkets() ?? Promise.resolve();
52
+ const GLOBAL_MARKET_CODE = sources?.market.GLOBAL_MARKET_CODE ?? "__global__";
53
+ const sftp = sources?.sftp ?? null;
54
+ const mail = sources?.mail ?? null;
55
+ const consoleKeys = sources?.keys ?? null;
51
56
  const config = useRuntimeConfig();
52
57
  const sftpHost = config.public.sftpHost ?? "share.revenexx.com";
53
58
  const sftpPort = Number(config.public.sftpPort ?? 42);
@@ -67,12 +72,13 @@ watch(kind, () => {
67
72
  selectedId.value = "";
68
73
  applied.value = null;
69
74
  });
70
- const sftpOptions = computed(() => sftp.accounts.value.filter((a) => (a.is_active || a.status === "active") && a.login_username).map((a) => ({ value: String(a.id), label: a.login_username })));
75
+ const sftpOptions = computed(() => (sftp?.accounts.value ?? []).filter((a) => (a.is_active || a.status === "active") && a.login_username).map((a) => ({ value: String(a.id), label: a.login_username })));
71
76
  const marketOptions = computed(() => [
72
77
  { code: GLOBAL_MARKET_CODE, label: "Global (tenant baseline)" },
73
78
  ...markets.value.map((m) => ({ code: m.code, label: `${m.name} (${m.code})` }))
74
79
  ]);
75
80
  async function ensureLists() {
81
+ if (!sftp || !mail) return;
76
82
  loadingList.value = true;
77
83
  try {
78
84
  await loadTenants();
@@ -98,6 +104,7 @@ function marketSlug() {
98
104
  return marketCode.value === GLOBAL_MARKET_CODE ? "" : marketCode.value;
99
105
  }
100
106
  async function applySftp() {
107
+ if (!sftp) return;
101
108
  const t = tenant.value;
102
109
  const acc = sftp.accounts.value.find((a) => String(a.id) === selectedId.value);
103
110
  if (!t || !acc) return;
@@ -122,9 +129,14 @@ async function applySftp() {
122
129
  }
123
130
  }
124
131
  async function applySmtp() {
132
+ if (!mail) return;
125
133
  applying.value = true;
126
134
  try {
127
135
  const cred = await mail.getCredential(selectedId.value);
136
+ if (!cred) {
137
+ toast.error("That mail credential is no longer there.");
138
+ return;
139
+ }
128
140
  emit("apply", {
129
141
  host: mailSmtpHost,
130
142
  port: mailSmtpPort,
@@ -165,6 +177,7 @@ function openCreate() {
165
177
  }
166
178
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
167
179
  async function createSftp() {
180
+ if (!sftp) return;
168
181
  const t = tenant.value;
169
182
  if (!t) {
170
183
  toast.error("No tenant selected.");
@@ -195,9 +208,14 @@ async function createSftp() {
195
208
  }
196
209
  }
197
210
  async function createSmtp() {
211
+ if (!mail) return;
198
212
  creating.value = true;
199
213
  try {
200
214
  const cred = await mail.createCredential({ name: createName.value.trim() || "SMTP", type: "SMTP" });
215
+ if (!cred) {
216
+ toast.error("The mail credential was not returned \u2014 pick it from the list once it appears.");
217
+ return;
218
+ }
201
219
  emit("apply", {
202
220
  host: mailSmtpHost,
203
221
  port: mailSmtpPort,
@@ -217,6 +235,7 @@ async function createSmtp() {
217
235
  }
218
236
  }
219
237
  async function createApiKey() {
238
+ if (!consoleKeys) return;
220
239
  creating.value = true;
221
240
  try {
222
241
  const { secret } = await consoleKeys.create(createName.value.trim() || "Integrations key", ["*"]);
@@ -244,19 +263,37 @@ function submitCreate() {
244
263
  <div class="flex items-center justify-between gap-3">
245
264
  <div class="min-w-0">
246
265
  <div class="text-sm font-medium">{{ SWITCH_LABEL[kind] }}</div>
266
+ <!-- Two sentences, one per state. Saying WHERE the sources are missing is
267
+ the point: without it an operator in the dev studio or another host
268
+ reads a dead switch and cannot tell a broken feature from one that
269
+ belongs somewhere else. The manual fields stay on screen either way. -->
247
270
  <p class="text-xs text-muted-foreground">
248
- Reuse an existing revenexx resource instead of typing access data by hand.
271
+ <template v-if="available">
272
+ Reuse an existing revenexx resource instead of typing access data by hand.
273
+ </template>
274
+ <template v-else>
275
+ Not available here — the managed revenexx resources live in the cockpit.
276
+ Enter the access data by hand instead.
277
+ </template>
249
278
  </p>
250
279
  </div>
251
- <Switch :model-value="active" @update:model-value="(v) => active = v" />
280
+ <!-- Disabled rather than hidden: a control that vanishes leaves nothing to
281
+ read the explanation from, and the operator cannot tell whether the
282
+ feature exists at all. -->
283
+ <Switch
284
+ :aria-label="SWITCH_LABEL[kind]"
285
+ :disabled="!available"
286
+ :model-value="active"
287
+ @update:model-value="(v) => active = v"
288
+ />
252
289
  </div>
253
290
 
254
291
  <div v-if="active" class="mt-3 space-y-3">
255
292
  <!-- Tenant (preselected) -->
256
293
  <div class="space-y-1.5">
257
- <Label>Tenant</Label>
294
+ <Label :for="`${uid}-tenant`">Tenant</Label>
258
295
  <Select :model-value="tenant ?? ''" @update:model-value="(v) => tenant = v">
259
- <SelectTrigger class="bg-white">
296
+ <SelectTrigger :id="`${uid}-tenant`" class="bg-white">
260
297
  <SelectValue placeholder="Select tenant" />
261
298
  </SelectTrigger>
262
299
  <SelectContent>
@@ -269,9 +306,9 @@ function submitCreate() {
269
306
 
270
307
  <!-- Market scope — only the Revenexx API credential is market-scoped -->
271
308
  <div v-if="kind === 'api'" class="space-y-1.5">
272
- <Label>Market</Label>
309
+ <Label :for="`${uid}-market`">Market</Label>
273
310
  <Select :model-value="marketCode" @update:model-value="(v) => marketCode = v">
274
- <SelectTrigger class="bg-white">
311
+ <SelectTrigger :id="`${uid}-market`" class="bg-white">
275
312
  <SelectValue placeholder="Select market" />
276
313
  </SelectTrigger>
277
314
  <SelectContent>
@@ -284,10 +321,10 @@ function submitCreate() {
284
321
 
285
322
  <!-- Resource select (SFTP / Mail) -->
286
323
  <div v-if="kind === 'sftp' || kind === 'smtp'" class="space-y-1.5">
287
- <Label>{{ kind === "sftp" ? "SFTP account" : "Mail credential" }}</Label>
324
+ <Label :for="`${uid}-resource`">{{ kind === "sftp" ? "SFTP account" : "Mail credential" }}</Label>
288
325
  <div class="flex items-center gap-2">
289
326
  <Select :model-value="selectedId" class="flex-1" @update:model-value="(v) => onSelect(v)">
290
- <SelectTrigger class="bg-white">
327
+ <SelectTrigger :id="`${uid}-resource`" class="bg-white">
291
328
  <SelectValue :placeholder="loadingList ? 'Loading\u2026' : 'Select one'" />
292
329
  </SelectTrigger>
293
330
  <SelectContent>
@@ -2,17 +2,12 @@
2
2
  import { computed, ref } from "vue";
3
3
  import { Magnifier as Search } from "#solar-icons";
4
4
  import { Input } from "@revenexx/studio";
5
+ import { resolveLocalized } from "../../utils/localize";
5
6
  const props = defineProps({
6
7
  types: { type: Array, required: true }
7
8
  });
8
9
  const emit = defineEmits(["pick"]);
9
10
  const query = ref("");
10
- function loc(v) {
11
- if (!v) return "";
12
- if (typeof v === "string") return v;
13
- const o = v;
14
- return o.en ?? o.de ?? Object.values(o)[0] ?? "";
15
- }
16
11
  function authKindLabel(k) {
17
12
  switch (k) {
18
13
  case "api-key":
@@ -33,7 +28,7 @@ function authKindLabel(k) {
33
28
  const filtered = computed(() => {
34
29
  const q = query.value.trim().toLowerCase();
35
30
  if (!q) return props.types;
36
- return props.types.filter((t) => loc(t.name).toLowerCase().includes(q));
31
+ return props.types.filter((t) => resolveLocalized(t.name).toLowerCase().includes(q));
37
32
  });
38
33
  </script>
39
34
 
@@ -43,6 +38,7 @@ const filtered = computed(() => {
43
38
  <Search :size="15" class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
44
39
  <Input
45
40
  v-model="query"
41
+ aria-label="Search for an app"
46
42
  class="bg-white pl-9"
47
43
  placeholder="Search for an app…"
48
44
  autofocus
@@ -62,7 +58,7 @@ const filtered = computed(() => {
62
58
  >
63
59
  <IntegrationsCredentialLogo :type="t" :size="16" />
64
60
  <div class="min-w-0 flex-1 truncate text-sm">
65
- <span class="font-medium">{{ loc(t.name) || t.slug }}</span>
61
+ <span class="font-medium">{{ resolveLocalized(t.name) || t.slug }}</span>
66
62
  <span class="ml-1.5 text-muted-foreground">({{ authKindLabel(t.auth_kind) }})</span>
67
63
  </div>
68
64
  </button>
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  modelValue: string;
3
+ inputId?: string;
3
4
  };
4
5
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
6
  "update:modelValue": (value: string) => any;
@@ -2,7 +2,8 @@
2
2
  import { computed, onMounted, ref } from "vue";
3
3
  import { Input } from "@revenexx/studio";
4
4
  const props = defineProps({
5
- modelValue: { type: String, required: true }
5
+ modelValue: { type: String, required: true },
6
+ inputId: { type: String, required: false }
6
7
  });
7
8
  const emit = defineEmits(["update:modelValue"]);
8
9
  const catalog = typeof useDefinedEvents === "function" ? useDefinedEvents() : { emits: ref([]), listens: ref([]), loading: ref(false), load: async () => {
@@ -34,6 +35,7 @@ function pick(topic) {
34
35
  <template>
35
36
  <div class="relative">
36
37
  <Input
38
+ :id="props.inputId"
37
39
  :model-value="modelValue"
38
40
  placeholder="customer.created"
39
41
  class="h-8 text-sm bg-white font-mono"
@@ -46,7 +48,7 @@ function pick(topic) {
46
48
  class="absolute left-0 right-0 top-full z-30 mt-1 max-h-56 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 shadow-md"
47
49
  >
48
50
  <div v-if="catalog.loading.value && !filtered.length" class="px-2.5 py-1.5 text-sm text-muted-foreground">
49
- Loading event catalog…
51
+ Loading event catalogue…
50
52
  </div>
51
53
  <button
52
54
  v-for="o in filtered"
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  modelValue: string;
3
+ inputId?: string;
3
4
  };
4
5
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
6
  "update:modelValue": (value: string) => any;
@@ -10,6 +10,10 @@ type __VLS_Props = {
10
10
  placeholder?: string;
11
11
  variables?: Record<string, WorkflowVariable>;
12
12
  upstreamOutputs?: UpstreamOutput[];
13
+ /** Draw the editor as at fault — the field above owns the reason. */
14
+ invalid?: boolean;
15
+ /** The setting's label, so the editable region has an accessible name. */
16
+ label?: string;
13
17
  };
14
18
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
19
  "update:modelValue": (value: string) => any;
@@ -12,7 +12,9 @@ const props = defineProps({
12
12
  modelValue: { type: null, required: true },
13
13
  placeholder: { type: String, required: false },
14
14
  variables: { type: Object, required: false },
15
- upstreamOutputs: { type: Array, required: false }
15
+ upstreamOutputs: { type: Array, required: false },
16
+ invalid: { type: Boolean, required: false },
17
+ label: { type: String, required: false }
16
18
  });
17
19
  const emit = defineEmits(["update:modelValue"]);
18
20
  const text = computed(() => String(props.modelValue ?? ""));
@@ -78,7 +80,11 @@ const editor = new Editor({
78
80
  content: textToDoc(text.value),
79
81
  editorProps: {
80
82
  // Single line: swallow Enter (paragraph splits would break the field).
81
- handleKeyDown: (_view, event) => event.key === "Enter" && !event.isComposing
83
+ handleKeyDown: (_view, event) => event.key === "Enter" && !event.isComposing,
84
+ // A contenteditable region is not a text box to assistive technology
85
+ // unless it says so, and its label sits in a sibling element — so it would
86
+ // otherwise be an anonymous blob to a screen reader.
87
+ attributes: { role: "textbox", ...props.label ? { "aria-label": props.label } : {} }
82
88
  },
83
89
  onUpdate({ editor: ed }) {
84
90
  const json = ed.getJSON();
@@ -143,6 +149,12 @@ const sections = computed(() => {
143
149
  return out;
144
150
  });
145
151
  const hasSuggestions = computed(() => sections.value.some((s) => s.items.length > 0));
152
+ const query = ref("");
153
+ const filteredSections = computed(() => {
154
+ const q = query.value.trim().toLowerCase();
155
+ if (!q) return sections.value;
156
+ return sections.value.map((s) => ({ ...s, items: s.items.filter((i) => i.label.toLowerCase().includes(q) || i.expr.toLowerCase().includes(q)) })).filter((s) => s.items.length > 0);
157
+ });
146
158
  const open = ref(false);
147
159
  const wrapRef = ref(null);
148
160
  function toggle() {
@@ -163,6 +175,9 @@ function onKeydown(e) {
163
175
  function onDocMousedown(e) {
164
176
  if (wrapRef.value && !wrapRef.value.contains(e.target)) open.value = false;
165
177
  }
178
+ watch(open, (isOpen) => {
179
+ if (!isOpen) query.value = "";
180
+ });
166
181
  onMounted(() => document.addEventListener("mousedown", onDocMousedown));
167
182
  onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown));
168
183
  function insert(expr) {
@@ -174,7 +189,10 @@ function insert(expr) {
174
189
  <template>
175
190
  <div ref="wrapRef" class="relative" @focusout="onFocusOut">
176
191
  <div class="flex items-center gap-1">
177
- <div class="expr-editor relative h-8 min-w-0 flex-1 overflow-hidden rounded-md border border-black/[0.08] bg-white px-2 font-mono text-sm transition focus-within:border-black/[0.16]">
192
+ <div
193
+ class="expr-editor relative h-8 min-w-0 flex-1 overflow-hidden rounded-md border bg-white px-2 text-sm transition focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-ring"
194
+ :class="invalid ? 'border-rose-400' : 'border-black/[0.08]'"
195
+ >
178
196
  <span
179
197
  v-if="!text"
180
198
  class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 truncate text-muted-foreground/60"
@@ -203,34 +221,45 @@ function insert(expr) {
203
221
 
204
222
  <div
205
223
  v-if="open && hasSuggestions"
206
- class="absolute right-0 top-full z-50 mt-1 max-h-64 w-72 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 text-sm shadow-lg"
224
+ class="absolute right-0 top-full z-50 mt-1 flex max-h-72 w-80 flex-col rounded-md border border-black/[0.08] bg-white text-sm shadow-lg"
207
225
  >
208
- <template v-for="section in sections" :key="section.heading">
209
- <div
210
- v-if="section.items.length"
211
- class="px-2 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"
212
- >
213
- {{ section.heading }}
214
- </div>
215
- <button
216
- v-for="item in section.items"
217
- :key="item.expr"
218
- type="button"
219
- class="block w-full px-2 py-1 text-left transition hover:bg-muted/50"
220
- @mousedown.prevent="insert(item.expr)"
226
+ <div class="shrink-0 border-b border-black/[0.06] p-1">
227
+ <input
228
+ v-model="query"
229
+ aria-label="Filter available data"
230
+ placeholder="Filter…"
231
+ class="w-full rounded px-1.5 py-1 text-sm outline-none placeholder:text-muted-foreground/60 focus:ring-2 focus:ring-primary/40"
232
+ @mousedown.stop
221
233
  >
222
- <div class="truncate text-muted-foreground">
223
- {{ item.label }}
224
- </div>
225
- <div class="truncate font-mono text-emerald-700">
226
- {{ item.expr }}
234
+ </div>
235
+ <div class="min-h-0 flex-1 overflow-y-auto py-1">
236
+ <p v-if="!filteredSections.length" class="px-2 py-1.5 text-muted-foreground">
237
+ Nothing matches “{{ query }}”.
238
+ </p>
239
+ <template v-for="section in filteredSections" :key="section.heading">
240
+ <div class="px-2 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
241
+ {{ section.heading }}
227
242
  </div>
228
- </button>
229
- </template>
243
+ <button
244
+ v-for="item in section.items"
245
+ :key="item.expr"
246
+ type="button"
247
+ class="block w-full px-2 py-1 text-left transition hover:bg-muted/50"
248
+ @mousedown.prevent="insert(item.expr)"
249
+ >
250
+ <div class="truncate text-muted-foreground">
251
+ {{ item.label }}
252
+ </div>
253
+ <div class="truncate font-mono text-emerald-700">
254
+ {{ item.expr }}
255
+ </div>
256
+ </button>
257
+ </template>
258
+ </div>
230
259
  </div>
231
260
  </div>
232
261
  </template>
233
262
 
234
263
  <style>
235
- .expr-editor .ProseMirror{align-items:center;display:flex;height:100%;outline:none;overflow-x:auto;scrollbar-width:none}.expr-editor .ProseMirror p{line-height:30px;margin:0;white-space:pre}.expr-pill{background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.25);border-radius:6px;color:#047857;display:inline-block;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;line-height:18px;margin:0 1px;padding:0 6px;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:baseline;white-space:nowrap}.expr-pill.ProseMirror-selectednode{outline:2px solid rgba(16,185,129,.5)}
264
+ .expr-editor .ProseMirror{align-items:center;display:flex;height:100%;outline:none;overflow-x:auto;scrollbar-width:none}.expr-editor .ProseMirror p{line-height:30px;margin:0;white-space:pre}.expr-pill{align-items:center;background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.25);border-radius:6px;color:#047857;display:inline-flex;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;height:20px;line-height:1;margin:0 2px;padding:0 7px;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}.expr-pill.ProseMirror-selectednode{outline:2px solid rgba(16,185,129,.5)}
236
265
  </style>
@@ -10,6 +10,10 @@ type __VLS_Props = {
10
10
  placeholder?: string;
11
11
  variables?: Record<string, WorkflowVariable>;
12
12
  upstreamOutputs?: UpstreamOutput[];
13
+ /** Draw the editor as at fault — the field above owns the reason. */
14
+ invalid?: boolean;
15
+ /** The setting's label, so the editable region has an accessible name. */
16
+ label?: string;
13
17
  };
14
18
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
19
  "update:modelValue": (value: string) => any;
@@ -3,6 +3,7 @@ import { computed, nextTick, ref } from "vue";
3
3
  import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
5
  import { ChevronsDownUp, ChevronsUpDown, Trash2, Ungroup } from "lucide-vue-next";
6
+ import { resolveDisplayName } from "../../utils/localize";
6
7
  const props = defineProps({
7
8
  id: { type: String, required: true },
8
9
  data: { type: Object, required: true },
@@ -10,20 +11,16 @@ const props = defineProps({
10
11
  });
11
12
  defineEmits(["updateNodeInternals"]);
12
13
  const GROUP_COLORS = [
13
- { bg: "rgba(254,243,199,0.45)", border: "#fcd34d", chip: "#f59e0b" },
14
- { bg: "rgba(255,237,213,0.45)", border: "#fdba74", chip: "#f97316" },
15
- { bg: "rgba(255,228,230,0.45)", border: "#fda4af", chip: "#f43f5e" },
16
- { bg: "rgba(220,252,231,0.45)", border: "#86efac", chip: "#10b981" },
17
- { bg: "rgba(219,234,254,0.45)", border: "#93c5fd", chip: "#3b82f6" },
18
- { bg: "rgba(237,233,254,0.45)", border: "#c4b5fd", chip: "#8b5cf6" },
19
- { bg: "rgba(245,245,244,0.6)", border: "#d6d3d1", chip: "#78716c" }
14
+ { name: "Amber", bg: "rgba(254,243,199,0.45)", border: "#fcd34d", chip: "#f59e0b" },
15
+ { name: "Orange", bg: "rgba(255,237,213,0.45)", border: "#fdba74", chip: "#f97316" },
16
+ { name: "Rose", bg: "rgba(255,228,230,0.45)", border: "#fda4af", chip: "#f43f5e" },
17
+ { name: "Green", bg: "rgba(220,252,231,0.45)", border: "#86efac", chip: "#10b981" },
18
+ { name: "Blue", bg: "rgba(219,234,254,0.45)", border: "#93c5fd", chip: "#3b82f6" },
19
+ { name: "Violet", bg: "rgba(237,233,254,0.45)", border: "#c4b5fd", chip: "#8b5cf6" },
20
+ { name: "Gray", bg: "rgba(245,245,244,0.6)", border: "#d6d3d1", chip: "#78716c" }
20
21
  ];
21
22
  const color = computed(() => GROUP_COLORS[props.data.group.color ?? 4] ?? GROUP_COLORS[4]);
22
- function loc(v) {
23
- if (!v) return "";
24
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
25
- }
26
- const label = computed(() => loc(props.data.group.label) || "Group");
23
+ const label = computed(() => resolveDisplayName(props.data.group.label) || "Group");
27
24
  const collapsed = computed(() => props.data.group.collapsed === true);
28
25
  const editable = computed(() => !!props.data.actions);
29
26
  const editing = ref(false);
@@ -99,6 +96,8 @@ function onResizeEnd() {
99
96
  class="size-4.5 rounded-full border transition hover:scale-110"
100
97
  :class="(data.group.color ?? 4) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
101
98
  :style="{ backgroundColor: c.chip, borderColor: c.border, width: '16px', height: '16px' }"
99
+ :title="c.name"
100
+ :aria-label="c.name"
102
101
  @click="data.actions?.setColor(i)"
103
102
  />
104
103
  <div class="mx-0.5 h-4 w-px bg-black/10" />
@@ -184,6 +183,7 @@ function onResizeEnd() {
184
183
  v-if="editing"
185
184
  ref="inputEl"
186
185
  v-model="draft"
186
+ aria-label="Group name"
187
187
  class="nodrag w-40 rounded border border-black/[0.1] bg-white px-1 py-0.5 text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-primary/40"
188
188
  @blur="commitRename"
189
189
  @keydown.enter.prevent="commitRename"
@@ -5,6 +5,13 @@ const { focused } = useIntegrationsFocus();
5
5
 
6
6
  <template>
7
7
  <div class="flex h-[calc(100%-0.75rem)] mb-3 mr-3 ml-1 overflow-hidden rounded-lg border border-gray-400/10 outline-1 outline-white/30">
8
+ <!-- First thing in the tab order: jumps past the sidebar into the page. -->
9
+ <a
10
+ href="#integrations-main"
11
+ class="sr-only focus:not-sr-only focus:absolute focus:left-3 focus:top-3 focus:z-50 focus:rounded-md focus:bg-white focus:px-3 focus:py-1.5 focus:text-sm focus:font-medium focus:ring-2 focus:ring-primary/40"
12
+ >
13
+ Skip to content
14
+ </a>
8
15
  <div
9
16
  class="shrink-0 overflow-hidden transition-[width] duration-200 ease-out border-r border-black/[0.06] bg-white"
10
17
  :class="focused ? 'w-0 !border-r-0' : 'w-64'"
@@ -13,8 +20,20 @@ const { focused } = useIntegrationsFocus();
13
20
  <Sidebar />
14
21
  </div>
15
22
  </div>
16
- <div class="flex-1 min-w-0 overflow-y-auto bg-white/40 backdrop-blur-sm">
23
+ <!-- `tabindex="-1"` is what makes the skip link actually skip: a <main> is
24
+ not focusable on its own, so the browser would move the fragment but
25
+ leave focus on the link — and the next Tab would walk into the sidebar
26
+ the user just asked to be past. It stays out of the tab order (-1, not
27
+ 0); only the fragment and a script can put focus here — which is also
28
+ why it draws no focus ring: a ring around the whole page would be noise
29
+ on a region nobody tabs to, and the promise of a visible mark is about
30
+ the fields, buttons and links inside it. -->
31
+ <main
32
+ id="integrations-main"
33
+ tabindex="-1"
34
+ class="flex-1 min-w-0 overflow-y-auto bg-white/40 backdrop-blur-sm outline-none"
35
+ >
17
36
  <slot />
18
- </div>
37
+ </main>
19
38
  </div>
20
39
  </template>
@@ -2,6 +2,7 @@
2
2
  import { computed, ref } from "vue";
3
3
  import { ChevronDown } from "lucide-vue-next";
4
4
  import { Input } from "@revenexx/studio";
5
+ import { resolveLocalized } from "../../utils/localize";
5
6
  const props = defineProps({
6
7
  slug: { type: String, required: true },
7
8
  version: { type: String, required: true },
@@ -10,10 +11,6 @@ const props = defineProps({
10
11
  modelValue: { type: null, required: true }
11
12
  });
12
13
  const emit = defineEmits(["update:modelValue"]);
13
- function labelText(v) {
14
- if (!v) return "";
15
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
16
- }
17
14
  const search = ref("");
18
15
  const focused = ref(false);
19
16
  const q = useNodeConfigResolve({
@@ -33,14 +30,14 @@ const currentKnown = computed(
33
30
  );
34
31
  const currentLabel = computed(() => {
35
32
  const hit = options.value.find((o) => String(o.value) === currentValue.value);
36
- return hit ? labelText(hit.label) || String(hit.value) : currentValue.value;
33
+ return hit ? resolveLocalized(hit.label) || String(hit.value) : currentValue.value;
37
34
  });
38
35
  const listOpen = computed(() => focused.value && !q.gated.value);
39
36
  const visibleOptions = computed(() => {
40
37
  const term = search.value.trim().toLowerCase();
41
38
  if (!focused.value || !term) return options.value;
42
39
  const hits = options.value.filter(
43
- (o) => (labelText(o.label) || "").toLowerCase().includes(term) || String(o.value).toLowerCase().includes(term)
40
+ (o) => (resolveLocalized(o.label) || "").toLowerCase().includes(term) || String(o.value).toLowerCase().includes(term)
44
41
  );
45
42
  return hits.length ? hits : options.value;
46
43
  });
@@ -66,7 +63,7 @@ const visibleMultiOptions = computed(() => {
66
63
  const f = multiFilter.value.trim().toLowerCase();
67
64
  if (!f) return options.value;
68
65
  return options.value.filter(
69
- (o) => (labelText(o.label) || String(o.value)).toLowerCase().includes(f) || String(o.value).toLowerCase().includes(f)
66
+ (o) => (resolveLocalized(o.label) || String(o.value)).toLowerCase().includes(f) || String(o.value).toLowerCase().includes(f)
70
67
  );
71
68
  });
72
69
  function toggleMulti(optValue) {
@@ -109,10 +106,11 @@ function toggleMulti(optValue) {
109
106
  <Input
110
107
  v-if="options.length > 10"
111
108
  v-model="multiFilter"
109
+ :aria-label="`Filter the options for ${resolveLocalized(field.label)}`"
112
110
  placeholder="Filter options"
113
111
  class="mb-1.5 h-7 bg-white text-sm"
114
112
  />
115
- <div class="flex flex-wrap gap-1">
113
+ <div role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
116
114
  <span v-if="visibleMultiOptions.length === 0" class="text-sm text-muted-foreground">
117
115
  {{ options.length === 0 ? "no options available" : "no options match the filter" }}
118
116
  </span>
@@ -125,7 +123,7 @@ function toggleMulti(optValue) {
125
123
  :class="selectedArray.includes(String(opt.value)) ? 'bg-emerald-500/15 text-emerald-700 border-emerald-500/30' : 'bg-white text-muted-foreground hover:text-foreground'"
126
124
  @click="toggleMulti(String(opt.value))"
127
125
  >
128
- {{ labelText(opt.label) || String(opt.value) }}
126
+ {{ resolveLocalized(opt.label) || String(opt.value) }}
129
127
  </button>
130
128
  <!-- stored values no longer offered — shown read-only so they stay visible -->
131
129
  <span
@@ -144,7 +142,8 @@ function toggleMulti(optValue) {
144
142
  <div class="relative">
145
143
  <Input
146
144
  :model-value="focused ? search : currentLabel"
147
- :placeholder="labelText(field.placeholder) || 'Pick one'"
145
+ :aria-label="resolveLocalized(field.label)"
146
+ :placeholder="resolveLocalized(field.placeholder) || 'Pick one'"
148
147
  class="h-8 bg-white pr-7 text-sm"
149
148
  @update:model-value="(v) => {
150
149
  search = String(v);
@@ -168,7 +167,7 @@ function toggleMulti(optValue) {
168
167
  :class="String(opt.value) === currentValue ? 'bg-emerald-500/10 text-emerald-800' : ''"
169
168
  @mousedown.prevent="pick(String(opt.value))"
170
169
  >
171
- <span class="min-w-0 truncate">{{ labelText(opt.label) || String(opt.value) }}</span>
170
+ <span class="min-w-0 truncate">{{ resolveLocalized(opt.label) || String(opt.value) }}</span>
172
171
  </button>
173
172
  <div v-if="!q.isFetching.value && visibleOptions.length === 0" class="px-2.5 py-1.5 text-sm text-muted-foreground">
174
173
  {{ search ? 'nothing matches "' + search + '"' : "no options available" }}
@@ -2,6 +2,7 @@
2
2
  import { computed } from "vue";
3
3
  import { Button } from "@revenexx/studio";
4
4
  import { IntegrationsApiError } from "../../utils/apiError";
5
+ import { resolveLocalized } from "../../utils/localize";
5
6
  const props = defineProps({
6
7
  slug: { type: String, required: true },
7
8
  version: { type: String, required: true },
@@ -22,10 +23,6 @@ const q = useNodeConfigResolve({
22
23
  dependsOn: () => props.field.dependsOn
23
24
  });
24
25
  const resolvedFields = computed(() => q.data.value?.fields ?? []);
25
- function labelText(v) {
26
- if (!v) return "";
27
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
28
- }
29
26
  </script>
30
27
 
31
28
  <template>
@@ -67,7 +64,7 @@ function labelText(v) {
67
64
  class="space-y-3 rounded-md border border-black/[0.08] bg-muted/10 p-3 m-0"
68
65
  >
69
66
  <legend class="px-1 text-mono text-muted-foreground">
70
- {{ labelText(field.label) }}
67
+ {{ resolveLocalized(field.label) }}
71
68
  </legend>
72
69
  <template v-for="f in resolvedFields" :key="f.key">
73
70
  <!-- collision guard: a resolved key that shadows a static config field is skipped -->
@@ -14,6 +14,8 @@ type __VLS_Props = {
14
14
  output: string;
15
15
  fields?: string[];
16
16
  }>;
17
+ /** Why the last save refused this setting, if it did. */
18
+ refused?: string;
17
19
  };
18
20
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
21
  set: (key: string, value: unknown) => any;