@revenexx/studio-integrations 0.7.0 → 1.0.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 (71) hide show
  1. package/CHANGELOG.md +416 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +2 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +4 -1
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +8 -2
  6. package/dist/runtime/components/Integrations/ExpressionInput.vue +5 -0
  7. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
  8. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +16 -3
  9. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
  10. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +1 -0
  11. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
  12. package/dist/runtime/components/Integrations/NodeConfigField.vue +2 -0
  13. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
  14. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  15. package/dist/runtime/components/Integrations/NodeInspector.vue +95 -10
  16. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  17. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
  18. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +34 -8
  19. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
  20. package/dist/runtime/components/Integrations/NodePalette.vue +12 -8
  21. package/dist/runtime/components/Integrations/StickyNoteView.vue +45 -11
  22. package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
  23. package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
  24. package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
  25. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -3
  26. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +12 -3
  27. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +5 -4
  28. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
  29. package/dist/runtime/components/Integrations/WorkflowEditor.vue +52 -11
  30. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
  31. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
  32. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -2
  33. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
  34. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +59 -28
  35. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +3 -4
  36. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
  37. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +37 -26
  38. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
  39. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -5
  40. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +53 -14
  41. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
  42. package/dist/runtime/composables/useIntegrationsCredentials.js +2 -2
  43. package/dist/runtime/composables/useIntegrationsNodes.js +11 -1
  44. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
  45. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
  46. package/dist/runtime/composables/useIntegrationsTriggers.js +18 -7
  47. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
  48. package/dist/runtime/composables/useIntegrationsWorkflows.js +25 -4
  49. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  50. package/dist/runtime/data/integrationNodeCatalog.d.ts +22 -1
  51. package/dist/runtime/data/integrationNodeCatalog.js +8 -4
  52. package/dist/runtime/data/nodeInputData.d.ts +58 -1
  53. package/dist/runtime/data/nodeInputData.js +30 -5
  54. package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
  55. package/dist/runtime/data/triggerGlyphs.js +11 -0
  56. package/dist/runtime/i18n/messages/de.json +20 -3
  57. package/dist/runtime/i18n/messages/en.json +20 -3
  58. package/dist/runtime/pages/integrations/credentials.vue +4 -1
  59. package/dist/runtime/pages/integrations/index.vue +16 -17
  60. package/dist/runtime/pages/integrations/templates/[slug].vue +13 -5
  61. package/dist/runtime/pages/integrations/workflows/[id].vue +193 -104
  62. package/dist/runtime/types/integrations.d.ts +25 -1
  63. package/dist/runtime/utils/cockpitSources.d.ts +22 -3
  64. package/dist/runtime/utils/credentialState.d.ts +6 -1
  65. package/dist/runtime/utils/credentialState.js +1 -1
  66. package/dist/runtime/utils/localize.d.ts +34 -0
  67. package/dist/runtime/utils/localize.js +22 -7
  68. package/dist/runtime/utils/pollBudget.d.ts +1 -1
  69. package/dist/runtime/utils/servedPaths.d.ts +22 -0
  70. package/dist/runtime/utils/servedPaths.js +3 -0
  71. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, resolveComponent } from "vue";
3
- import { RefreshCircle as Loader2, ClockCircle as Clock, Global as Webhook, Play, Bolt as Zap } from "#solar-icons";
3
+ import { RefreshCircle as Loader2 } from "#solar-icons";
4
+ import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
4
5
  import { templateCategoryLabel, templatePackageLabel } from "../../data/integrationTemplates";
5
6
  import TemplateFlowPreview from "./TemplateFlowPreview.vue";
6
7
  const props = defineProps({
@@ -17,7 +18,6 @@ const full = computed(() => props.density === "full");
17
18
  const banner = computed(
18
19
  () => full.value ? props.template.images.find((i) => i.category === "banner")?.src ?? null : null
19
20
  );
20
- const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
21
21
  const TRIGGER_LABEL_KEY = {
22
22
  schedule: "integration.templates.card.trigger.schedule",
23
23
  webhook: "integration.templates.trigger.webhook",
@@ -104,7 +104,7 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
104
104
  :key="triggerType"
105
105
  class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase"
106
106
  >
107
- <component :is="TRIGGER_ICON[triggerType] ?? Zap" :size="10" />
107
+ <component :is="glyphForTrigger(triggerType)" :size="10" />
108
108
  {{ triggerTypeLabel(triggerType) }}
109
109
  </span>
110
110
  <span class="flex-1" />
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
- import { computed } from "vue";
2
+ import { computed, onMounted } from "vue";
3
3
  import * as Lucide from "lucide-vue-next";
4
+ import { triggerGlyph } from "../../data/triggerGlyphs";
4
5
  import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
5
6
  import { resolveLocalized } from "../../utils/localize";
6
7
  import { describeCron } from "../../utils/schedule";
@@ -12,6 +13,10 @@ const props = defineProps({
12
13
  showErrorBranches: { type: Boolean, required: false, default: false }
13
14
  });
14
15
  const t = useIntegrationsT();
16
+ const { load: loadNodeCatalog } = useIntegrationsNodes();
17
+ onMounted(() => {
18
+ void loadNodeCatalog();
19
+ });
15
20
  const STOP_SLUG = "revenexx:stop-and-error";
16
21
  const NODE_W = 220;
17
22
  const NODE_H = 58;
@@ -37,7 +42,11 @@ const boxes = computed(() => {
37
42
  subtitle,
38
43
  type: t2.type,
39
44
  manifest: null,
40
- icon: iconFor(template?.icon ?? "Zap")
45
+ // The trigger's glyph from the one map, like every other trigger box. This
46
+ // resolved `TRIGGER_TEMPLATES`' icon name ('Zap', 'Play', …) out of Lucide
47
+ // instead — the same route the palette's tiles took — so the preview drew a
48
+ // Lucide bolt where the card it previews draws Solar's (PO-329).
49
+ icon: triggerGlyph(t2.type)
41
50
  });
42
51
  });
43
52
  blob.nodes.forEach((n, i) => {
@@ -162,7 +171,7 @@ const bounds = computed(() => {
162
171
  >
163
172
  <div class="flex flex-1 items-center gap-2.5 px-3 py-2">
164
173
  <span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-emerald-500 text-white shadow-sm">
165
- <component :is="b.icon" :size="15" :stroke-width="2.5" />
174
+ <component :is="b.icon" :size="15" />
166
175
  </span>
167
176
  <div class="min-w-0 flex-1">
168
177
  <div class="truncate text-sm font-semibold leading-tight text-foreground">{{ b.label }}</div>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
- import { RefreshCircle as Loader2, MagicStick as Sparkles } from "#solar-icons";
3
+ import { RefreshCircle as Loader2, Magnifier as Search, MagicStick as Sparkles } from "#solar-icons";
4
+ import { triggerGlyph } from "../../data/triggerGlyphs";
4
5
  import * as Lucide from "lucide-vue-next";
5
6
  import {
6
7
  Badge,
@@ -179,7 +180,7 @@ async function useTemplate(slug) {
179
180
  <SelectValue>
180
181
  <span class="flex min-w-0 items-center gap-2">
181
182
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
182
- <component :is="iconFor(selectedTrigger?.icon)" :size="13" />
183
+ <component :is="triggerGlyph(selectedTrigger?.type)" :size="13" />
183
184
  </span>
184
185
  <span class="shrink-0 text-sm font-medium">{{ resolveLocalized(selectedTrigger?.name) }}</span>
185
186
  <span class="truncate text-sm text-muted-foreground">{{ resolveLocalized(selectedTrigger?.description) }}</span>
@@ -190,7 +191,7 @@ async function useTemplate(slug) {
190
191
  <SelectItem v-for="t in TRIGGER_TEMPLATES" :key="t.id" :value="t.type">
191
192
  <span class="flex items-center gap-2.5 py-0.5">
192
193
  <span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
193
- <component :is="iconFor(t.icon)" :size="14" />
194
+ <component :is="triggerGlyph(t.type)" :size="14" />
194
195
  </span>
195
196
  <span class="min-w-0">
196
197
  <span class="block text-sm font-medium">{{ resolveLocalized(t.name) }}</span>
@@ -250,7 +251,7 @@ async function useTemplate(slug) {
250
251
  <!-- ── From template ─────────────────────────────────────────────────── -->
251
252
  <div v-else class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1">
252
253
  <div class="relative">
253
- <Lucide.Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
254
+ <Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
254
255
  <input
255
256
  v-model="templateSearch"
256
257
  type="text"
@@ -4,6 +4,22 @@ import '@vue-flow/minimap/dist/style.css';
4
4
  import type { Workflow, WorkflowBlob } from '../../types/integrations.js';
5
5
  type __VLS_Props = {
6
6
  workflow: Workflow;
7
+ /**
8
+ * Unsaved work the page is still holding for this workflow, read ONCE, at
9
+ * mount (behaviour specs/workflow-save.md).
10
+ *
11
+ * The editor is mounted per tab, so a trip to the Runs tab and back seeded a
12
+ * fresh working copy from the SAVED blob while the page still held the draft:
13
+ * the canvas showed one graph, the header said "Unsaved changes" about
14
+ * another, and Save wrote the one nobody could see. Seeding from the draft is
15
+ * what makes a move inside the workflow keep the work.
16
+ *
17
+ * Deliberately not watched. The page's draft is a copy of what this editor
18
+ * last emitted, so following it would re-seed the canvas — and drop the
19
+ * selection, the undo stack and any open dialog with it — on the user's own
20
+ * keystrokes.
21
+ */
22
+ draft?: WorkflowBlob | null;
7
23
  /** Busy flag for the control-pane Play button (a run is being triggered). */
8
24
  runBusy?: boolean;
9
25
  /** The run is queued behind a bundle build — busy, but worth saying why. */
@@ -56,6 +56,7 @@ import { findingsByNodeField } from "../../utils/validationFindings";
56
56
  import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
57
57
  const props = defineProps({
58
58
  workflow: { type: Object, required: true },
59
+ draft: { type: [Object, null], required: false },
59
60
  runBusy: { type: Boolean, required: false },
60
61
  runWaiting: { type: Boolean, required: false },
61
62
  overlayOpen: { type: Boolean, required: false }
@@ -63,7 +64,7 @@ const props = defineProps({
63
64
  const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
64
65
  const t = useIntegrationsT();
65
66
  const { keys: secretKeys } = useIntegrationsSecrets();
66
- const blob = ref(clone(props.workflow.blob));
67
+ const blob = ref(clone(props.draft ?? props.workflow.blob));
67
68
  watch(() => props.workflow.id, () => {
68
69
  blob.value = clone(props.workflow.blob);
69
70
  syncFromBlob();
@@ -469,22 +470,49 @@ flow.onEdgeUpdate(({ edge, connection }) => {
469
470
  syncFromBlob();
470
471
  push();
471
472
  });
472
- flow.onEdgesChange((changes) => {
473
+ let pendingEdgeRemovals = [];
474
+ let pendingNodeRemovals = [];
475
+ let removalFlushQueued = false;
476
+ function queueRemovalFlush() {
477
+ if (removalFlushQueued) return;
478
+ removalFlushQueued = true;
479
+ void nextTick(flushRemovals);
480
+ }
481
+ function flushRemovals() {
482
+ removalFlushQueued = false;
483
+ const edges = pendingEdgeRemovals;
484
+ const nodes = pendingNodeRemovals;
485
+ pendingEdgeRemovals = [];
486
+ pendingNodeRemovals = [];
487
+ const goneNodes = new Set(nodes);
473
488
  let mutated = false;
489
+ for (const c of edges) {
490
+ if (goneNodes.has(c.source) || goneNodes.has(c.target)) continue;
491
+ blob.value.edges = blob.value.edges.filter((e) => e.id !== c.id);
492
+ mutated = true;
493
+ }
494
+ if (nodes.length) {
495
+ dropFromBlob(nodes);
496
+ mutated = true;
497
+ }
498
+ if (mutated) {
499
+ syncFromBlob();
500
+ push();
501
+ }
502
+ }
503
+ flow.onEdgesChange((changes) => {
474
504
  for (const c of changes) {
475
505
  if (c.type === "remove") {
476
- blob.value.edges = blob.value.edges.filter((e) => e.id !== c.id);
477
- mutated = true;
506
+ pendingEdgeRemovals.push({ id: c.id, source: c.source, target: c.target });
478
507
  }
479
508
  }
480
- if (mutated) push();
509
+ if (pendingEdgeRemovals.length) queueRemovalFlush();
481
510
  });
482
511
  flow.onNodesChange((changes) => {
483
512
  const removed = changes.filter((c) => c.type === "remove").map((c) => c.id);
484
513
  if (!removed.length) return;
485
- void nextTick(() => {
486
- for (const id of removed) removeBlobNode(id);
487
- });
514
+ pendingNodeRemovals.push(...removed);
515
+ queueRemovalFlush();
488
516
  });
489
517
  function stripFromGroups(drop) {
490
518
  if (!blob.value.groups?.length) return;
@@ -541,7 +569,7 @@ function addSticky(at) {
541
569
  syncFromBlob();
542
570
  push();
543
571
  }
544
- function removeBlobNodes(ids) {
572
+ function dropFromBlob(ids) {
545
573
  if (!ids.length) return;
546
574
  const drop = new Set(ids);
547
575
  stripFromGroups(drop);
@@ -553,6 +581,10 @@ function removeBlobNodes(ids) {
553
581
  if (blob.value.triggerPositions?.[id]) delete blob.value.triggerPositions[id];
554
582
  }
555
583
  if (selectedId.value && drop.has(selectedId.value)) selectedId.value = null;
584
+ }
585
+ function removeBlobNodes(ids) {
586
+ if (!ids.length) return;
587
+ dropFromBlob(ids);
556
588
  syncFromBlob();
557
589
  push();
558
590
  }
@@ -779,14 +811,14 @@ const selectedUpstreamOutputs = computed(() => {
779
811
  return blob.value.edges.filter((e) => e.to.nodeId === id).flatMap((e) => {
780
812
  const source = blob.value.nodes.find((n) => n.id === e.from.nodeId);
781
813
  if (!source) return [];
782
- const portDef = (findManifest(source.nodeSlug, source.nodeVersion)?.outputs ?? []).find((o) => o.name === e.from.output);
814
+ const port = portsForNode(source, findManifest(source.nodeSlug, source.nodeVersion), dynamicOutputs.value[source.id]).find((p) => p.name === e.from.output);
783
815
  return [{
784
816
  nodeId: source.id,
785
817
  // A node's own name is text the user typed — never trimmed, never read
786
818
  // as JSON. The manifest strings below it get the normalising resolver.
787
819
  nodeName: resolveDisplayName(source.name) || source.nodeSlug,
788
820
  output: e.from.output,
789
- fields: portDef?.fields
821
+ fields: port?.fields.map((f) => f.name)
790
822
  }];
791
823
  });
792
824
  });
@@ -1213,6 +1245,14 @@ async function focusPalette() {
1213
1245
  paletteRef.value?.focusSearch();
1214
1246
  }
1215
1247
  const lightboxOpen = computed(() => !!selection.value);
1248
+ watch(lightboxOpen, (open) => {
1249
+ if (!open) renameIntent.value = false;
1250
+ });
1251
+ function onConfigOpenAutoFocus(event) {
1252
+ if (renameIntent.value) return;
1253
+ event.preventDefault();
1254
+ event.target?.focus();
1255
+ }
1216
1256
  const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
1217
1257
  const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
1218
1258
  useCanvasShortcuts(() => ({
@@ -1790,6 +1830,7 @@ const edgeTypes = {
1790
1830
  form as a strip down the left of an empty workbench. -->
1791
1831
  <DialogContent
1792
1832
  :show-close-button="false"
1833
+ @open-auto-focus="onConfigOpenAutoFocus"
1793
1834
  class="flex max-w-none flex-col gap-0 overflow-hidden rounded-lg p-0 sm:max-w-none"
1794
1835
  :class="selectionIsTrigger ? 'h-[min(520px,86vh)] w-[min(600px,94vw)]' : 'h-[min(680px,86vh)] w-[min(1100px,94vw)]'"
1795
1836
  >
@@ -4,6 +4,22 @@ import '@vue-flow/minimap/dist/style.css';
4
4
  import type { Workflow, WorkflowBlob } from '../../types/integrations.js';
5
5
  type __VLS_Props = {
6
6
  workflow: Workflow;
7
+ /**
8
+ * Unsaved work the page is still holding for this workflow, read ONCE, at
9
+ * mount (behaviour specs/workflow-save.md).
10
+ *
11
+ * The editor is mounted per tab, so a trip to the Runs tab and back seeded a
12
+ * fresh working copy from the SAVED blob while the page still held the draft:
13
+ * the canvas showed one graph, the header said "Unsaved changes" about
14
+ * another, and Save wrote the one nobody could see. Seeding from the draft is
15
+ * what makes a move inside the workflow keep the work.
16
+ *
17
+ * Deliberately not watched. The page's draft is a copy of what this editor
18
+ * last emitted, so following it would re-seed the canvas — and drop the
19
+ * selection, the undo stack and any open dialog with it — on the user's own
20
+ * keystrokes.
21
+ */
22
+ draft?: WorkflowBlob | null;
7
23
  /** Busy flag for the control-pane Play button (a run is being triggered). */
8
24
  runBusy?: boolean;
9
25
  /** The run is queued behind a bundle build — busy, but worth saying why. */
@@ -3,6 +3,12 @@ import '@vue-flow/core/dist/theme-default.css';
3
3
  import type { Workflow } from '../../types/integrations.js';
4
4
  type __VLS_Props = {
5
5
  workflow: Workflow;
6
+ /**
7
+ * The editor has unsaved work the page is holding. Nothing here shows it — the
8
+ * canvas behind this tab is not mounted — but a restore is what destroys it, so
9
+ * the question this panel asks has to name it (specs/workflow-save.md).
10
+ */
11
+ draftPending?: boolean;
6
12
  };
7
13
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
14
  restored: () => any;
@@ -20,7 +20,8 @@ import NodeDynamicOutputsResolver from "./NodeDynamicOutputsResolver.vue";
20
20
  import { findManifest } from "../../data/integrationNodeCatalog";
21
21
  import { useIntegrationsRevisions } from "../../composables/useIntegrationsRevisions";
22
22
  const props = defineProps({
23
- workflow: { type: Object, required: true }
23
+ workflow: { type: Object, required: true },
24
+ draftPending: { type: Boolean, required: false }
24
25
  });
25
26
  const emit = defineEmits(["restored"]);
26
27
  const revisionsApi = useIntegrationsRevisions();
@@ -225,7 +226,8 @@ async function select(revision) {
225
226
  }
226
227
  }
227
228
  async function restoreRevision(rev) {
228
- if (!confirm(t("integration.history.confirm.restore", { revision: rev.revision }))) return;
229
+ const question = props.draftPending ? t("integration.history.confirm.restoreWithDraft", { revision: rev.revision }) : t("integration.history.confirm.restore", { revision: rev.revision });
230
+ if (!confirm(question)) return;
229
231
  restoring.value = rev.revision;
230
232
  try {
231
233
  await revisionsApi.restore(props.workflow.id, rev.revision);
@@ -3,6 +3,12 @@ import '@vue-flow/core/dist/theme-default.css';
3
3
  import type { Workflow } from '../../types/integrations.js';
4
4
  type __VLS_Props = {
5
5
  workflow: Workflow;
6
+ /**
7
+ * The editor has unsaved work the page is holding. Nothing here shows it — the
8
+ * canvas behind this tab is not mounted — but a restore is what destroys it, so
9
+ * the question this panel asks has to name it (specs/workflow-save.md).
10
+ */
11
+ draftPending?: boolean;
6
12
  };
7
13
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
14
  restored: () => any;
@@ -3,8 +3,8 @@ import { computed, inject, nextTick, ref, watch } from "vue";
3
3
  import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
5
  import * as Lucide from "lucide-vue-next";
6
- import { Copy, TrashBinMinimalistic as TrashBin } from "#solar-icons";
7
- import { NODE_CATEGORIES, findManifest } from "../../data/integrationNodeCatalog";
6
+ import { Copy, DangerTriangle as AlertTriangle, RefreshCircle as Loader2, TrashBinMinimalistic as TrashBin } from "#solar-icons";
7
+ import { NODE_CATEGORIES, findManifest, isVersionExecutable } from "../../data/integrationNodeCatalog";
8
8
  import { isTerminalManifest } from "../../data/nodeInputData";
9
9
  import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
10
10
  const props = defineProps({
@@ -20,7 +20,6 @@ const category = computed(() => NODE_CATEGORIES.find((c) => c.key === manifest.v
20
20
  const title = computed(
21
21
  () => resolveDisplayName(props.data.name) || resolveLocalized(manifest.value?.name) || props.data.nodeSlug
22
22
  );
23
- const inputRequired = computed(() => manifest.value?.input?.required !== false);
24
23
  const invalidFields = computed(() => props.data.__invalidFields ?? []);
25
24
  const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, { fields: invalidFields.value.join(", ") }));
26
25
  const notes = computed(() => (props.data.notes ?? "").trim());
@@ -31,11 +30,15 @@ const notePlacement = ref(Position.Bottom);
31
30
  const noteMaxHeight = ref(0);
32
31
  let noteTimer = null;
33
32
  const notePanelId = computed(() => `node-note-${props.id}`);
33
+ function handleTitle(p) {
34
+ return p.hint ? p.label + " \u2014 " + p.hint : p.label;
35
+ }
34
36
  function portsFromOutputs(list) {
35
37
  const ports = [];
36
38
  for (const o of list) {
37
39
  if (o.name) {
38
- ports.push({ name: o.name, kind: o.kind, label: resolveLocalized(o.label) || o.name });
40
+ const hint = resolveLocalized(o.description);
41
+ ports.push({ name: o.name, kind: o.kind, label: resolveLocalized(o.label) || o.name, ...hint ? { hint } : {} });
39
42
  } else if (o.sourceFromConfig) {
40
43
  const raw = props.data.config?.[o.sourceFromConfig];
41
44
  const values = Array.isArray(raw) ? raw : [];
@@ -160,6 +163,8 @@ function releaseNote() {
160
163
  }, 250);
161
164
  }
162
165
  const nodeActions = inject("workflow-node-actions", null);
166
+ const unbuildableVersion = computed(() => nodeActions !== null && !isVersionExecutable(props.data.nodeSlug, props.data.nodeVersion));
167
+ const unbuildableLabel = computed(() => t("integration.canvas.node.badge.noBuiltCode", { version: props.data.nodeVersion }));
163
168
  const canConfigure = computed(() => typeof nodeActions?.configure === "function");
164
169
  function configure() {
165
170
  nodeActions?.configure?.(props.id);
@@ -226,17 +231,15 @@ function remove() {
226
231
 
227
232
  <!-- Input: ONE logical input, attachable from the left or the top — both
228
233
  dots feed the same `__in` port. Clearly visible, ring + shadow. -->
229
- <template v-if="inputRequired">
230
- <Handle
231
- v-for="side in IN_SIDES"
232
- :id="`__in__${side}`"
233
- :key="`in-${side}`"
234
- type="target"
235
- :position="sideToPosition(side)"
236
- :title="t('integration.canvas.node.handle.input')"
237
- class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-slate-700"
238
- />
239
- </template>
234
+ <Handle
235
+ v-for="side in IN_SIDES"
236
+ :id="`__in__${side}`"
237
+ :key="`in-${side}`"
238
+ type="target"
239
+ :position="sideToPosition(side)"
240
+ :title="t('integration.canvas.node.handle.input')"
241
+ class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-slate-700"
242
+ />
240
243
 
241
244
  <!-- Output handles: right + bottom, distributed per side. Color encodes the
242
245
  port semantics (true/false/error/branch/default), matching the edges. -->
@@ -247,7 +250,7 @@ function remove() {
247
250
  :key="`${p.name}-${side}`"
248
251
  type="source"
249
252
  :position="sideToPosition(side)"
250
- :title="p.label"
253
+ :title="handleTitle(p)"
251
254
  :style="distributedStyle(side, i, outputs.length)"
252
255
  class="!size-3 !rounded-full !border-2 !border-white !shadow-sm transition-all hover:!size-3.5"
253
256
  :class="portTone(p).handle"
@@ -265,19 +268,43 @@ function remove() {
265
268
  showPortLabels ? 'pb-3.5' : ''
266
269
  ]"
267
270
  >
268
- <!-- Incomplete settings at the last save: the count, amber, at the corner
269
- the input handles leave free. Hoverable on purpose — the count alone
270
- doesn't say what is wrong, so the title spells it out and lists the
271
- fields; `nodrag` keeps that hover from starting a card drag. -->
271
+ <!-- The card's marks, in one row at the corner the input handles leave free.
272
+ A row rather than two absolutely-placed badges: the amber one's width
273
+ follows its count, so any fixed offset for a second badge overlaps it as
274
+ soon as the count reaches two digits. Hoverable on purpose — a symbol or
275
+ a bare count at a corner is a riddle, so each carries its own title;
276
+ `nodrag` keeps that hover from starting a card drag. -->
272
277
  <span
273
- v-if="invalidFields.length"
274
- role="img"
275
- :aria-label="invalidLabel"
276
- :title="invalidLabel"
277
- class="nodrag absolute -left-2 -top-2 z-10 flex cursor-help items-center gap-0.5 rounded-full bg-amber-500 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
278
+ v-if="invalidFields.length || unbuildableVersion"
279
+ class="nodrag absolute -left-2 -top-2 z-10 flex items-center gap-1"
278
280
  >
279
- <Lucide.TriangleAlert :size="9" :stroke-width="2.5" />
280
- {{ invalidFields.length }}
281
+ <span
282
+ v-if="invalidFields.length"
283
+ role="img"
284
+ :aria-label="invalidLabel"
285
+ :title="invalidLabel"
286
+ class="flex cursor-help items-center gap-0.5 rounded-full bg-amber-500 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
287
+ >
288
+ <!-- `weight="Bold"` for the same reason the Lucide draw carried
289
+ `stroke-width="2.5"`: 9px of white on amber. Solar's default
290
+ BoldDuotone washes out at that size against the pill. -->
291
+ <AlertTriangle :size="9" weight="Bold" />
292
+ {{ invalidFields.length }}
293
+ </span>
294
+ <!-- The pinned version cannot be run. Rose rather than amber, and no
295
+ count: amber on this card means unfinished work of the user's own —
296
+ settings still to fill in — while this is the platform missing a piece,
297
+ which the user cannot fix by typing. Hence the stronger colour despite
298
+ being the one that does NOT block activation. -->
299
+ <span
300
+ v-if="unbuildableVersion"
301
+ role="img"
302
+ :aria-label="unbuildableLabel"
303
+ :title="unbuildableLabel"
304
+ class="flex cursor-help items-center rounded-full bg-rose-600 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
305
+ >
306
+ <AlertTriangle :size="9" weight="Bold" />
307
+ </span>
281
308
  </span>
282
309
  <!-- Always-visible edit affordance: single click opens the config lightbox.
283
310
  (Double-click on the whole card works too.) Hidden in read-only hosts
@@ -330,7 +357,11 @@ function remove() {
330
357
  v-if="resolvePending"
331
358
  class="pointer-events-none absolute bottom-1 left-1/2 flex -translate-x-1/2 select-none items-center gap-1 whitespace-nowrap text-[9px] font-semibold uppercase tracking-wide text-muted-foreground"
332
359
  >
333
- <Lucide.LoaderCircle :size="9" class="animate-spin" />
360
+ <!-- `weight="Bold"` for the badge's reason one size down: the default
361
+ BoldDuotone draws a filled disc across the whole viewBox, and a disc
362
+ turning looks like a disc standing still. Bold knocks the arrows out
363
+ of the ring, so 9px of it still reads as spinning. -->
364
+ <Loader2 :size="9" weight="Bold" class="animate-spin" />
334
365
  {{ t("integration.canvas.node.port.resolving") }}
335
366
  </span>
336
367
  <template v-else>
@@ -11,9 +11,7 @@ import {
11
11
  AltArrowRight as Next,
12
12
  Play,
13
13
  Pause,
14
- RefreshCircle as Loader2,
15
- ClockCircle as Clock,
16
- LinkRound as Webhook
14
+ RefreshCircle as Loader2
17
15
  } from "#solar-icons";
18
16
  import { Badge, Button } from "@revenexx/studio";
19
17
  import { toast } from "vue-sonner";
@@ -26,6 +24,7 @@ import StickyNoteView from "./StickyNoteView.vue";
26
24
  import GroupFrameView from "./GroupFrameView.vue";
27
25
  import WorkflowEdgeView from "./WorkflowEdgeView.vue";
28
26
  import { findManifest } from "../../data/integrationNodeCatalog";
27
+ import { triggerGlyph } from "../../data/triggerGlyphs";
29
28
  import { SAFETY_POLL_MS } from "../../utils/pollBudget";
30
29
  import { runStatusLabel, statusClass } from "../../utils/statusStyle";
31
30
  const props = defineProps({
@@ -408,7 +407,7 @@ function pretty(v) {
408
407
  }
409
408
  }
410
409
  function triggerIcon(type) {
411
- return type === "schedule" ? Clock : type === "webhook" ? Webhook : Play;
410
+ return triggerGlyph(type);
412
411
  }
413
412
  const activeStepTitle = computed(() => {
414
413
  const s = activeStep.value;
@@ -4,33 +4,32 @@ export interface ExecutionModeOption {
4
4
  label: string;
5
5
  hint: string;
6
6
  }
7
+ /** What this dialog edits — the values as they stand, saved or not. */
8
+ export interface WorkflowSettingsValues {
9
+ name: string;
10
+ description: string;
11
+ tags: string[];
12
+ icon: WorkflowIcon | null;
13
+ executionMode: string;
14
+ }
7
15
  type __VLS_Props = {
8
16
  open: boolean;
9
17
  workflow: Workflow;
10
18
  modes: ExecutionModeOption[];
11
- modeDraft: string;
12
- savingMode?: boolean;
13
- savingMeta?: boolean;
19
+ /**
20
+ * The values to edit: the editor's unsaved ones where it has any, the saved ones
21
+ * otherwise. Reopening the dialog therefore shows what the operator last entered
22
+ * rather than what the platform still holds.
23
+ */
24
+ current: WorkflowSettingsValues;
14
25
  /** Tag suggestions (tags already used across the tenant's workflows). */
15
26
  tagSuggestions?: string[];
16
27
  };
17
28
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
18
29
  "update:open": (args_0: boolean) => any;
19
- "change-mode": (mode: string) => any;
20
- "save-meta": (meta: {
21
- name: string;
22
- description: string;
23
- tags: string[];
24
- icon: WorkflowIcon | null;
25
- }) => any;
30
+ apply: (values: WorkflowSettingsValues) => any;
26
31
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
27
32
  "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
28
- "onChange-mode"?: ((mode: string) => any) | undefined;
29
- "onSave-meta"?: ((meta: {
30
- name: string;
31
- description: string;
32
- tags: string[];
33
- icon: WorkflowIcon | null;
34
- }) => any) | undefined;
33
+ onApply?: ((values: WorkflowSettingsValues) => any) | undefined;
35
34
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
35
  export default _default;