@revenexx/studio-integrations 1.2.0 → 1.4.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 (82) hide show
  1. package/CHANGELOG.md +581 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
  4. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
  5. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
  6. package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
  7. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  8. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +15 -1
  9. package/dist/runtime/components/Integrations/NodeConfigField.vue +158 -49
  10. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +15 -1
  11. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
  12. package/dist/runtime/components/Integrations/NodeInspector.vue +136 -81
  13. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
  14. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  15. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  16. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  17. package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
  18. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  19. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  20. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  21. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
  22. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
  23. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
  24. package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
  25. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
  26. package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
  27. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
  28. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
  29. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
  30. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
  31. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
  32. package/dist/runtime/components/Integrations/WorkflowEditor.vue +200 -54
  33. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
  34. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
  35. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
  36. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
  37. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  38. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  39. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  40. package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
  41. package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
  42. package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
  43. package/dist/runtime/composables/useCredentialTypes.js +3 -9
  44. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
  45. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  46. package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
  47. package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
  48. package/dist/runtime/composables/useIntegrationsTemplates.js +10 -8
  49. package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
  50. package/dist/runtime/composables/useIntegrationsWorkflows.js +11 -7
  51. package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
  52. package/dist/runtime/data/canvasShortcuts.js +68 -0
  53. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  54. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  55. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  56. package/dist/runtime/data/nodeVersionMove.js +65 -0
  57. package/dist/runtime/i18n/messages/de.json +83 -20
  58. package/dist/runtime/i18n/messages/en.json +83 -20
  59. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
  60. package/dist/runtime/pages/integrations/workflows/[id].vue +158 -53
  61. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  62. package/dist/runtime/types/integrations.d.ts +44 -1
  63. package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
  64. package/dist/runtime/utils/apiEnvelope.js +20 -0
  65. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  66. package/dist/runtime/utils/buildSettle.js +29 -0
  67. package/dist/runtime/utils/configDefaults.d.ts +18 -0
  68. package/dist/runtime/utils/configDefaults.js +7 -0
  69. package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
  70. package/dist/runtime/utils/errorPathAdvice.js +19 -0
  71. package/dist/runtime/utils/notify.d.ts +2 -2
  72. package/dist/runtime/utils/runEnvelope.d.ts +12 -0
  73. package/dist/runtime/utils/runEnvelope.js +2 -1
  74. package/dist/runtime/utils/selectOptions.d.ts +48 -0
  75. package/dist/runtime/utils/selectOptions.js +19 -0
  76. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  77. package/dist/runtime/utils/settingCondition.js +84 -0
  78. package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
  79. package/dist/runtime/utils/shortcutLabel.js +48 -0
  80. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  81. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  82. package/package.json +1 -1
@@ -21,7 +21,6 @@ const {
21
21
  getStackTrace,
22
22
  cancelRun,
23
23
  terminateRun,
24
- retryRun,
25
24
  resumeRun
26
25
  } = useIntegrationsRuns();
27
26
  const DETAILS_POLL_MS = 3e3;
@@ -41,8 +40,10 @@ const detailsError = ref(null);
41
40
  const steps = ref([]);
42
41
  const stepsLoading = ref(false);
43
42
  const stepsError = ref(null);
44
- const stepsWithPayloads = ref(false);
45
43
  const expandedStep = ref(null);
44
+ const payloadsLoading = ref(false);
45
+ const payloadsError = ref(null);
46
+ const payloadCache = ref(/* @__PURE__ */ new Map());
46
47
  const historyLoading = ref(false);
47
48
  const historyError = ref(null);
48
49
  const stackEnabled = ref(false);
@@ -82,6 +83,7 @@ const temporalWorkflowId = computed(() => pickStr(runData.value, ["temporal_work
82
83
  const temporalRunId = computed(() => pickStr(runData.value, ["temporal_run_id", "temporalRunId"]) ?? props.run.temporalRunId ?? null);
83
84
  const hasTemporal = computed(() => !!temporalWorkflowId.value && !!temporalRunId.value);
84
85
  const runResult = computed(() => pick(runData.value, ["result"]));
86
+ const MAX_RESULT_LINKS = 50;
85
87
  const resultLinks = computed(() => {
86
88
  const out = [];
87
89
  const seen = /* @__PURE__ */ new Set();
@@ -100,7 +102,7 @@ const resultLinks = computed(() => {
100
102
  }
101
103
  };
102
104
  walk(runResult.value, []);
103
- return out.slice(0, 8);
105
+ return out.length > MAX_RESULT_LINKS ? [] : out;
104
106
  });
105
107
  const hasResult = computed(() => runResult.value != null && (typeof runResult.value !== "object" || Object.keys(runResult.value).length > 0));
106
108
  const isTerminal = computed(() => {
@@ -303,6 +305,24 @@ async function loadDetails() {
303
305
  detailsLoading.value = false;
304
306
  }
305
307
  }
308
+ function stepKey(st, i) {
309
+ return `${st.node_id}-${st.scheduled_event_id ?? i}`;
310
+ }
311
+ function cachedPayload(st) {
312
+ return st.scheduled_event_id === null ? void 0 : payloadCache.value.get(st.scheduled_event_id);
313
+ }
314
+ function hasPayload(st) {
315
+ return cachedPayload(st) !== void 0;
316
+ }
317
+ function withCachedPayload(st) {
318
+ const cached = cachedPayload(st);
319
+ if (!cached) return st;
320
+ return { ...st, input: cached.input, output: cached.output, config: cached.config, branch: cached.branch };
321
+ }
322
+ function openStep() {
323
+ if (expandedStep.value === null) return null;
324
+ return steps.value.find((st, i) => stepKey(st, i) === expandedStep.value) ?? null;
325
+ }
306
326
  async function loadSteps() {
307
327
  if (!hasTemporal.value) {
308
328
  steps.value = [];
@@ -312,22 +332,55 @@ async function loadSteps() {
312
332
  stepsLoading.value = true;
313
333
  stepsError.value = null;
314
334
  try {
315
- const res = await getSteps(props.run.workflowId, props.run.id, stepsWithPayloads.value);
316
- steps.value = res?.steps ?? [];
335
+ const res = await getSteps(props.run.workflowId, props.run.id, false);
336
+ steps.value = (res?.steps ?? []).map(withCachedPayload);
317
337
  } catch (e) {
318
338
  stepsError.value = stepsErrMsg(e);
319
339
  steps.value = [];
320
340
  } finally {
321
341
  stepsLoading.value = false;
322
342
  }
343
+ void loadPayloads(false);
344
+ }
345
+ async function loadPayloads(opened) {
346
+ if (!hasTemporal.value || payloadsLoading.value) return;
347
+ const st = openStep();
348
+ if (!st) return;
349
+ const cached = cachedPayload(st);
350
+ if (cached && cached.status === st.status) return;
351
+ if (!cached && !opened) return;
352
+ const runId = props.run.id;
353
+ const readFor = expandedStep.value;
354
+ payloadsLoading.value = true;
355
+ payloadsError.value = null;
356
+ try {
357
+ const res = await getSteps(props.run.workflowId, runId, true);
358
+ if (props.run.id !== runId) return;
359
+ const next = new Map(payloadCache.value);
360
+ for (const row of res?.steps ?? []) {
361
+ if (row.scheduled_event_id === null) continue;
362
+ next.set(row.scheduled_event_id, {
363
+ status: row.status,
364
+ input: row.input,
365
+ output: row.output,
366
+ config: row.config,
367
+ branch: row.branch
368
+ });
369
+ }
370
+ payloadCache.value = next;
371
+ steps.value = steps.value.map(withCachedPayload);
372
+ } catch (e) {
373
+ if (props.run.id !== runId) return;
374
+ payloadsError.value = stepsErrMsg(e);
375
+ } finally {
376
+ if (props.run.id === runId) payloadsLoading.value = false;
377
+ }
378
+ if (expandedStep.value !== readFor) void loadPayloads(true);
323
379
  }
324
- function toggleStepPayloads() {
325
- stepsWithPayloads.value = !stepsWithPayloads.value;
326
- expandedStep.value = null;
327
- void loadSteps();
328
- }
329
- function toggleStep(i) {
330
- expandedStep.value = expandedStep.value === i ? null : i;
380
+ function toggleStep(st, i) {
381
+ const key = stepKey(st, i);
382
+ expandedStep.value = expandedStep.value === key ? null : key;
383
+ if (expandedStep.value !== null) void loadPayloads(true);
331
384
  }
332
385
  async function loadStack() {
333
386
  stackEnabled.value = true;
@@ -422,8 +475,10 @@ function resetState() {
422
475
  detailsError.value = null;
423
476
  steps.value = [];
424
477
  stepsError.value = null;
425
- stepsWithPayloads.value = false;
426
478
  expandedStep.value = null;
479
+ payloadsLoading.value = false;
480
+ payloadsError.value = null;
481
+ payloadCache.value = /* @__PURE__ */ new Map();
427
482
  historyError.value = null;
428
483
  timelineEvents.value = [];
429
484
  timelineDetail.value = null;
@@ -441,8 +496,6 @@ function actionLabel(a) {
441
496
  return t("integration.runs.action.cancel");
442
497
  case "terminate":
443
498
  return t("integration.runs.action.terminate");
444
- case "retry":
445
- return t("integration.runs.action.retry");
446
499
  case "resume":
447
500
  return t("integration.runs.action.resume");
448
501
  }
@@ -453,8 +506,6 @@ function confirmQuestion(a) {
453
506
  return t("integration.runs.confirm.cancel");
454
507
  case "terminate":
455
508
  return t("integration.runs.confirm.terminate");
456
- case "retry":
457
- return t("integration.runs.confirm.retry");
458
509
  case "resume":
459
510
  return t("integration.runs.confirm.resume");
460
511
  }
@@ -465,8 +516,6 @@ function actionErrorFallback(a) {
465
516
  return t("integration.runs.error.cancel");
466
517
  case "terminate":
467
518
  return t("integration.runs.error.terminate");
468
- case "retry":
469
- return t("integration.runs.error.retry");
470
519
  case "resume":
471
520
  return t("integration.runs.error.resume");
472
521
  }
@@ -488,6 +537,10 @@ watch(() => props.run.id, () => {
488
537
  resetState();
489
538
  void loadAll().then(startPollingIfNeeded);
490
539
  }, { immediate: true });
540
+ watch(apiStatusRaw, (now, before) => {
541
+ if (!now || !before || now === before) return;
542
+ emit("changed");
543
+ });
491
544
  onUnmounted(stopPolling);
492
545
  function arm(a) {
493
546
  armed.value = a;
@@ -511,15 +564,12 @@ async function confirmAction() {
511
564
  case "terminate":
512
565
  res = await terminateRun(props.run.workflowId, props.run.id);
513
566
  break;
514
- case "retry":
515
- res = await retryRun(props.run.workflowId, props.run.id);
516
- break;
517
567
  case "resume":
518
568
  res = await resumeRun(props.run.workflowId, props.run.id);
519
569
  break;
520
570
  }
521
571
  const newId = pickStr(res, ["id"]);
522
- actionNotice.value = (a === "retry" || a === "resume") && newId && newId !== String(props.run.id) ? t("integration.runs.notice.started", { id: newId }) : t("integration.runs.notice.requested", { action: actionLabel(a) });
572
+ actionNotice.value = a === "resume" && newId && newId !== String(props.run.id) ? t("integration.runs.notice.started", { id: newId }) : t("integration.runs.notice.requested", { action: actionLabel(a) });
523
573
  armed.value = null;
524
574
  await loadAll();
525
575
  startPollingIfNeeded();
@@ -537,7 +587,7 @@ async function confirmAction() {
537
587
  <!-- ── Details view: overview strip + result. ─────────────────────────── -->
538
588
  <template v-if="view === 'details'">
539
589
  <!-- Toolbar — only the status-gated stop/resume actions; hidden entirely
540
- for a terminal run (Retry sits in the page header, data auto-polls). -->
590
+ for a terminal run (Run again sits in the page header, data auto-polls). -->
541
591
  <div v-if="hasActions" class="flex items-center gap-2">
542
592
  <!-- confirm step: one click arms, second confirms -->
543
593
  <template v-if="armed">
@@ -668,10 +718,6 @@ async function confirmAction() {
668
718
  <section v-if="hasTemporal" class="rounded-md border border-black/[0.06] bg-white p-4 space-y-3">
669
719
  <div class="flex items-center gap-2 border-b border-black/[0.06] pb-2">
670
720
  <h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.steps.title") }}</h4>
671
- <div class="flex-1" />
672
- <Button variant="outline" size="sm" :disabled="stepsLoading" @click="toggleStepPayloads">
673
- {{ stepsWithPayloads ? t("integration.runs.detail.steps.action.hidePayloads") : t("integration.runs.detail.steps.action.showPayloads") }}
674
- </Button>
675
721
  </div>
676
722
 
677
723
  <p v-if="stepsError" class="text-rose-700">{{ stepsError }}</p>
@@ -688,7 +734,7 @@ async function confirmAction() {
688
734
  <button
689
735
  type="button"
690
736
  class="w-full flex items-center gap-3 px-3 py-2 text-left"
691
- @click="toggleStep(i)"
737
+ @click="toggleStep(st, i)"
692
738
  >
693
739
  <Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0', runStatusClass(st.status)]">{{ runStatusLabel(st.status) }}</Badge>
694
740
  <div class="min-w-0 flex-1">
@@ -698,13 +744,13 @@ async function confirmAction() {
698
744
  </div>
699
745
  </div>
700
746
  <span class="text-xs text-muted-foreground tabular-nums shrink-0">{{ fmtMs(st.duration_ms) }}</span>
701
- <span class="text-[10px] text-muted-foreground shrink-0">{{ expandedStep === i ? "\u25BC" : "\u25B6" }}</span>
747
+ <span class="text-[10px] text-muted-foreground shrink-0">{{ expandedStep === stepKey(st, i) ? "\u25BC" : "\u25B6" }}</span>
702
748
  </button>
703
749
  <div v-if="st.error" class="px-3 pb-2 -mt-1">
704
750
  <div class="rounded-md border border-rose-500/20 bg-rose-500/5 px-2 py-1 text-xs text-rose-700 break-words">{{ st.error }}</div>
705
751
  </div>
706
- <div v-if="expandedStep === i" class="px-3 pb-3 space-y-2">
707
- <template v-if="stepsWithPayloads">
752
+ <div v-if="expandedStep === stepKey(st, i)" class="px-3 pb-3 space-y-2">
753
+ <template v-if="hasPayload(st)">
708
754
  <div class="space-y-1">
709
755
  <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.steps.field.input") }}</div>
710
756
  <pre class="rounded-md bg-muted/40 p-3 text-xs overflow-x-auto max-h-40">{{ st.input === void 0 ? "\u2014" : json(st.input) }}</pre>
@@ -714,8 +760,9 @@ async function confirmAction() {
714
760
  <pre class="rounded-md bg-muted/40 p-3 text-xs overflow-x-auto max-h-40">{{ st.output === void 0 ? "\u2014" : json(st.output) }}</pre>
715
761
  </div>
716
762
  </template>
763
+ <p v-else-if="payloadsError" class="text-xs text-rose-700">{{ payloadsError }}</p>
717
764
  <p v-else class="text-xs text-muted-foreground">
718
- {{ t("integration.runs.detail.steps.payloadHint") }}
765
+ {{ t("integration.runs.detail.steps.payloadsLoading") }}
719
766
  </p>
720
767
  </div>
721
768
  </li>
@@ -0,0 +1,16 @@
1
+ import type { WorkflowStateRole, WorkflowStateVisibility } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ open: boolean;
4
+ /** Fixed by the `state-ref` field that opened this. */
5
+ role: WorkflowStateRole;
6
+ /** Names the workflow already declares, so a collision is caught while typing. */
7
+ taken: string[];
8
+ };
9
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:open": (args_0: boolean) => any;
11
+ declared: (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
14
+ onDeclared?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ export default _default;
@@ -0,0 +1,111 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import {
4
+ Button,
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ Input,
12
+ Label,
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue
18
+ } from "@revenexx/studio";
19
+ import { stateNamespaceErrorKey } from "../../utils/stateNamespaceName";
20
+ const props = defineProps({
21
+ open: { type: Boolean, required: true },
22
+ role: { type: String, required: true },
23
+ taken: { type: Array, required: true }
24
+ });
25
+ const emit = defineEmits(["update:open", "declared"]);
26
+ const t = useIntegrationsT();
27
+ const name = ref("");
28
+ const visibility = ref("private");
29
+ watch(() => props.open, (isOpen) => {
30
+ if (!isOpen) return;
31
+ name.value = "";
32
+ visibility.value = "private";
33
+ });
34
+ const error = computed(() => {
35
+ const key = stateNamespaceErrorKey(name.value, props.taken);
36
+ return key ? t(key) : null;
37
+ });
38
+ function declare() {
39
+ const value = name.value.trim();
40
+ if (!value || error.value) return;
41
+ emit("declared", value, props.role, visibility.value);
42
+ emit("update:open", false);
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
48
+ <DialogContent class="max-w-[94vw] sm:max-w-lg">
49
+ <DialogHeader>
50
+ <DialogTitle>{{ t("integration.nodeConfig.state.create.title") }}</DialogTitle>
51
+ <DialogDescription class="text-sm">
52
+ {{ t("integration.nodeConfig.state.create.description", { role: t(`integration.workflows.state.role.${role}.label`) }) }}
53
+ </DialogDescription>
54
+ </DialogHeader>
55
+
56
+ <div class="space-y-3">
57
+ <div class="space-y-1">
58
+ <Label for="state-create-name" class="text-mono text-muted-foreground">
59
+ {{ t("integration.workflows.state.field.newName.label") }}
60
+ </Label>
61
+ <Input
62
+ id="state-create-name"
63
+ v-model="name"
64
+ class="h-8 bg-white font-mono text-sm"
65
+ :placeholder="t('integration.workflows.state.field.newName.placeholder')"
66
+ @keydown.enter.prevent="declare"
67
+ />
68
+ <p v-if="error" class="text-xs text-rose-600">{{ error }}</p>
69
+ <p class="text-xs text-muted-foreground">{{ t(`integration.workflows.state.role.${role}.hint`) }}</p>
70
+ </div>
71
+
72
+ <div class="space-y-1">
73
+ <Label for="state-create-visibility" class="text-mono text-muted-foreground">
74
+ {{ t("integration.workflows.state.field.visibility.label") }}
75
+ </Label>
76
+ <Select v-model="visibility">
77
+ <SelectTrigger
78
+ id="state-create-visibility"
79
+ :aria-label="t('integration.workflows.state.field.visibility.label')"
80
+ class="h-8 bg-white text-sm"
81
+ >
82
+ <SelectValue />
83
+ </SelectTrigger>
84
+ <SelectContent>
85
+ <SelectItem value="private">
86
+ {{ t("integration.workflows.state.visibility.private.label") }}
87
+ </SelectItem>
88
+ <SelectItem value="shared">
89
+ {{ t("integration.workflows.state.visibility.shared.label") }}
90
+ </SelectItem>
91
+ </SelectContent>
92
+ </Select>
93
+ <!-- The same consequence the workflow-level dialog states, in the same
94
+ place relative to the control that chooses it. -->
95
+ <p class="text-xs text-muted-foreground">
96
+ {{ t(`integration.workflows.state.visibility.${visibility}.hint`) }}
97
+ </p>
98
+ </div>
99
+ </div>
100
+
101
+ <DialogFooter>
102
+ <Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
103
+ {{ t("integration.common.action.cancel") }}
104
+ </Button>
105
+ <Button size="sm" class="h-8" :disabled="!name.trim() || !!error" @click="declare">
106
+ {{ t("integration.workflows.state.action.add") }}
107
+ </Button>
108
+ </DialogFooter>
109
+ </DialogContent>
110
+ </Dialog>
111
+ </template>
@@ -0,0 +1,16 @@
1
+ import type { WorkflowStateRole, WorkflowStateVisibility } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ open: boolean;
4
+ /** Fixed by the `state-ref` field that opened this. */
5
+ role: WorkflowStateRole;
6
+ /** Names the workflow already declares, so a collision is caught while typing. */
7
+ taken: string[];
8
+ };
9
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:open": (args_0: boolean) => any;
11
+ declared: (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
14
+ onDeclared?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ export default _default;
@@ -5,10 +5,6 @@ type __VLS_Props = {
5
5
  density: 'full' | 'compact';
6
6
  /** Given → the card is a link there. Omitted → a button that emits `select`. */
7
7
  to?: string;
8
- /** This card is the one being acted on: it says so and cannot be pressed again. */
9
- busy?: boolean;
10
- /** Another card on the surface is being acted on, so this one is out of reach. */
11
- disabled?: boolean;
12
8
  };
13
9
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
10
  select: () => any;
@@ -7,9 +7,7 @@ import TemplateFlowPreview from "./TemplateFlowPreview.vue";
7
7
  const props = defineProps({
8
8
  template: { type: Object, required: true },
9
9
  density: { type: String, required: true },
10
- to: { type: String, required: false },
11
- busy: { type: Boolean, required: false },
12
- disabled: { type: Boolean, required: false }
10
+ to: { type: String, required: false }
13
11
  });
14
12
  const emit = defineEmits(["select"]);
15
13
  const t = useIntegrationsT();
@@ -40,8 +38,8 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
40
38
  <template>
41
39
  <component
42
40
  :is="to ? NuxtLink : 'button'"
43
- v-bind="to ? { to } : { type: 'button', disabled: disabled || busy }"
44
- class="flex flex-col overflow-hidden rounded-lg border border-black/[0.06] bg-white text-left transition hover:border-black/20 hover:shadow-sm disabled:opacity-60"
41
+ v-bind="to ? { to } : { type: 'button' }"
42
+ class="flex flex-col overflow-hidden rounded-lg border border-black/[0.06] bg-white text-left transition hover:border-black/20 hover:shadow-sm"
45
43
  @click="to ? void 0 : emit('select')"
46
44
  >
47
45
  <!-- The picture: the flow this template builds, drawn from its own
@@ -87,7 +85,6 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
87
85
  <span v-if="packageLabel" :class="[CHIP, chipSize, 'bg-violet-100 text-violet-700']">{{ packageLabel }}</span>
88
86
  </div>
89
87
 
90
- <Loader2 v-if="busy" :size="13" class="absolute right-1.5 top-1.5 animate-spin text-muted-foreground" />
91
88
  </div>
92
89
 
93
90
  <div class="flex min-h-0 flex-1 flex-col" :class="full ? 'p-4' : 'p-3'">
@@ -5,10 +5,6 @@ type __VLS_Props = {
5
5
  density: 'full' | 'compact';
6
6
  /** Given → the card is a link there. Omitted → a button that emits `select`. */
7
7
  to?: string;
8
- /** This card is the one being acted on: it says so and cannot be pressed again. */
9
- busy?: boolean;
10
- /** Another card on the surface is being acted on, so this one is out of reach. */
11
- disabled?: boolean;
12
8
  };
13
9
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
10
  select: () => any;
@@ -0,0 +1,25 @@
1
+ type __VLS_Props = {
2
+ slug: string;
3
+ };
4
+ declare var __VLS_69: {
5
+ installing: any;
6
+ };
7
+ type __VLS_Slots = {} & {
8
+ secondary?: (props: typeof __VLS_69) => any;
9
+ };
10
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ installed: (id: number) => any;
12
+ "update:loading": (loading: boolean) => any;
13
+ "update:installing": (installing: boolean) => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
+ onInstalled?: ((id: number) => any) | undefined;
16
+ "onUpdate:loading"?: ((loading: boolean) => any) | undefined;
17
+ "onUpdate:installing"?: ((installing: boolean) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
20
+ export default _default;
21
+ type __VLS_WithSlots<T, S> = T & {
22
+ new (): {
23
+ $slots: S;
24
+ };
25
+ };
@@ -0,0 +1,198 @@
1
+ <script setup>
2
+ import { computed, onBeforeUnmount, reactive, ref, watch } from "vue";
3
+ import { RefreshCircle as Loader2 } from "#solar-icons";
4
+ import {
5
+ Badge,
6
+ Button,
7
+ DialogFooter,
8
+ Input,
9
+ Label,
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue
15
+ } from "@revenexx/studio";
16
+ import { errorReason } from "../../utils/apiError";
17
+ const SELECT_LATER = "__later__";
18
+ const props = defineProps({
19
+ slug: { type: String, required: true }
20
+ });
21
+ const emit = defineEmits(["installed", "update:loading", "update:installing"]);
22
+ const t = useIntegrationsT();
23
+ const { get, load, fetchDetail, requirements, instantiate } = useIntegrationsTemplates();
24
+ const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
25
+ const { ofType, load: loadCredentials } = useIntegrationsCredentials();
26
+ const reqs = ref(null);
27
+ const loading = ref(true);
28
+ const installing = ref(false);
29
+ const error = ref(null);
30
+ const name = ref("");
31
+ const bindings = reactive({});
32
+ let run = 0;
33
+ watch(() => props.slug, async (slug) => {
34
+ const mine = ++run;
35
+ loading.value = true;
36
+ reqs.value = null;
37
+ error.value = null;
38
+ name.value = "";
39
+ for (const k of Object.keys(bindings)) delete bindings[k];
40
+ await load();
41
+ if (mine !== run) return;
42
+ if (name.value === "") name.value = get(slug)?.name ?? "";
43
+ await Promise.all([fetchDetail(slug), loadCredentials(), loadSecrets()]);
44
+ if (mine !== run) return;
45
+ const answer = await requirements(slug);
46
+ if (mine !== run) return;
47
+ reqs.value = answer;
48
+ loading.value = false;
49
+ }, { immediate: true });
50
+ watch(loading, (v) => emit("update:loading", v), { immediate: true });
51
+ watch(installing, (v) => emit("update:installing", v));
52
+ onBeforeUnmount(() => emit("update:loading", false));
53
+ const tpl = computed(() => get(props.slug));
54
+ const credentialReqs = computed(() => reqs.value?.credentialTypes ?? []);
55
+ const secretReqs = computed(() => reqs.value?.secrets ?? []);
56
+ const secretChecks = computed(() => {
57
+ const fromApi = secretReqs.value;
58
+ if (fromApi.length) {
59
+ return fromApi.map((s) => ({
60
+ key: s.key,
61
+ label: s.key ?? t("integration.templates.detail.secrets.chosenAtInstall"),
62
+ present: s.exists || (s.key ? secretKeys.value.includes(s.key) : false)
63
+ }));
64
+ }
65
+ return (tpl.value?.requiredSecrets ?? []).map((key) => ({
66
+ key,
67
+ label: key,
68
+ present: secretKeys.value.includes(key)
69
+ }));
70
+ });
71
+ function nodesFor(usedBy) {
72
+ return [...new Set(usedBy.map((u) => u.nodeId))];
73
+ }
74
+ async function confirmInstall() {
75
+ installing.value = true;
76
+ error.value = null;
77
+ try {
78
+ const credentialBindings = {};
79
+ for (const [nodeId, credId] of Object.entries(bindings)) {
80
+ if (credId && credId !== SELECT_LATER) credentialBindings[nodeId] = credId;
81
+ }
82
+ const id = await instantiate(props.slug, {
83
+ name: name.value.trim(),
84
+ credentialBindings
85
+ });
86
+ if (!Number.isFinite(id)) throw new Error(t("integration.templates.detail.install.error.fallback"));
87
+ emit("installed", id);
88
+ } catch (e) {
89
+ error.value = errorReason(e, t("integration.templates.detail.install.error.fallback"));
90
+ } finally {
91
+ installing.value = false;
92
+ }
93
+ }
94
+ </script>
95
+
96
+ <template>
97
+ <div>
98
+ <div class="space-y-4 py-1 max-h-[28rem] overflow-y-auto pr-1">
99
+ <div class="space-y-1.5">
100
+ <Label for="tpl-install-name">{{ t("integration.templates.detail.install.nameLabel") }}</Label>
101
+ <Input id="tpl-install-name" v-model="name" class="bg-white" :placeholder="t('integration.templates.detail.install.namePlaceholder')" />
102
+ </div>
103
+
104
+ <div v-if="loading" class="flex items-center gap-2 py-4 text-sm text-muted-foreground">
105
+ <Loader2 :size="14" class="animate-spin" />
106
+ {{ t("integration.templates.detail.install.state.loading") }}
107
+ </div>
108
+
109
+ <template v-else>
110
+ <!-- Credentials -->
111
+ <div class="space-y-2">
112
+ <div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
113
+ {{ t("integration.templates.detail.credentials.title") }}
114
+ </div>
115
+ <p v-if="!credentialReqs.length" class="text-sm text-muted-foreground italic">
116
+ {{ t("integration.templates.detail.install.noCredentials") }}
117
+ </p>
118
+ <div v-for="req in credentialReqs" :key="req.slug" class="space-y-2">
119
+ <div class="flex items-center gap-2">
120
+ <span class="text-sm font-medium">{{ req.name ?? req.slug }}</span>
121
+ <Badge v-if="!req.registered" variant="outline" class="bg-rose-500/10 border-rose-500/20 text-rose-700 text-sm">
122
+ {{ t("integration.templates.detail.install.notRegistered") }}
123
+ </Badge>
124
+ <Badge v-else-if="!ofType(req.slug).length" variant="outline" class="bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
125
+ {{ t("integration.templates.detail.install.noneYet") }}
126
+ </Badge>
127
+ </div>
128
+
129
+ <p v-if="!ofType(req.slug).length" class="rounded-md border-l-2 border-black/[0.08] bg-muted/30 px-2.5 py-1.5 text-sm text-muted-foreground">
130
+ {{ t("integration.templates.detail.install.noCredentialsOfType") }}
131
+ <NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
132
+ {{ t("integration.templates.detail.install.orInstallLater") }}
133
+ </p>
134
+ <div v-for="nodeId in nodesFor(req.usedBy)" v-else :key="nodeId" class="space-y-1">
135
+ <Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">{{ t("integration.templates.detail.install.nodeLabel", { id: nodeId }) }}</Label>
136
+ <Select
137
+ :model-value="bindings[nodeId] ?? SELECT_LATER"
138
+ @update:model-value="(v) => {
139
+ bindings[nodeId] = v ?? SELECT_LATER;
140
+ }"
141
+ >
142
+ <SelectTrigger :id="`tpl-cred-${nodeId}`" class="bg-white">
143
+ <SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
144
+ </SelectTrigger>
145
+ <SelectContent>
146
+ <SelectItem :value="SELECT_LATER">{{ t("integration.templates.detail.install.selectLater") }}</SelectItem>
147
+ <SelectItem v-for="c in ofType(req.slug)" :key="c.id" :value="c.id">
148
+ {{ c.name }}
149
+ </SelectItem>
150
+ </SelectContent>
151
+ </Select>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Secrets -->
157
+ <div v-if="secretChecks.length" class="space-y-2">
158
+ <div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
159
+ {{ t("integration.templates.detail.install.secretsHeader") }}
160
+ </div>
161
+ <p class="text-sm text-muted-foreground">
162
+ {{ t("integration.templates.detail.install.secretsDescription") }}
163
+ <NuxtLink to="/integrations/secrets" class="text-primary hover:underline">{{ t("integration.nav.item.secrets") }}</NuxtLink> {{ t("integration.templates.detail.install.secretsDescriptionSuffix") }}
164
+ </p>
165
+ <ul class="space-y-1">
166
+ <li
167
+ v-for="(s, i) in secretChecks"
168
+ :key="s.key ?? `dlg-secret-${i}`"
169
+ class="flex items-center justify-between rounded-md border border-black/[0.06] bg-muted/20 px-2 py-1 text-sm"
170
+ >
171
+ <code class="font-mono">{{ s.label }}</code>
172
+ <Badge variant="outline" :class="s.present ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm' : 'bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm'">
173
+ {{ s.present ? t("integration.templates.detail.install.secretStored") : t("integration.templates.detail.secrets.missing") }}
174
+ </Badge>
175
+ </li>
176
+ </ul>
177
+ </div>
178
+ </template>
179
+
180
+ <div v-if="error" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
181
+ {{ error }}
182
+ </div>
183
+ </div>
184
+
185
+ <DialogFooter>
186
+ <!-- Cancel here, back to the picker there: what the secondary control does
187
+ is the caller's, and only the caller knows what it goes back to. -->
188
+ <slot name="secondary" :installing="installing" />
189
+ <!-- Also closed while the step is still checking: a reader who confirmed
190
+ in that window would get the unwired copy the short route used to
191
+ produce, which is the outcome this step exists to replace. -->
192
+ <Button :disabled="installing || loading || !name.trim()" @click="confirmInstall">
193
+ <Loader2 v-if="installing" :size="14" class="mr-1 animate-spin" />
194
+ {{ installing ? t("integration.templates.detail.install.submitting") : t("integration.templates.detail.install.submit") }}
195
+ </Button>
196
+ </DialogFooter>
197
+ </div>
198
+ </template>