@revenexx/studio-integrations 0.7.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +633 -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/DeadLettersPanel.vue +2 -6
  7. package/dist/runtime/components/Integrations/ExpressionInput.vue +5 -0
  8. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
  9. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +16 -3
  10. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
  11. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +1 -0
  12. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
  13. package/dist/runtime/components/Integrations/NodeConfigField.vue +2 -0
  14. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
  15. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  16. package/dist/runtime/components/Integrations/NodeInspector.vue +135 -10
  17. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  18. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
  19. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +34 -8
  20. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
  21. package/dist/runtime/components/Integrations/NodePalette.vue +12 -8
  22. package/dist/runtime/components/Integrations/RunDetailPanel.vue +9 -6
  23. package/dist/runtime/components/Integrations/RunsTable.vue +2 -2
  24. package/dist/runtime/components/Integrations/StickyNoteView.vue +45 -11
  25. package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
  26. package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
  27. package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
  28. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -3
  29. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +12 -3
  30. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +5 -4
  31. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
  32. package/dist/runtime/components/Integrations/WorkflowEditor.vue +79 -12
  33. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
  34. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
  35. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +13 -3
  36. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
  37. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +13 -0
  38. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +95 -29
  39. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +13 -0
  40. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +11 -10
  41. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
  42. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +37 -26
  43. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
  44. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -5
  45. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +53 -14
  46. package/dist/runtime/components/Integrations/WorkflowsTable.vue +9 -4
  47. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
  48. package/dist/runtime/composables/useIntegrationsCredentials.js +2 -2
  49. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +9 -2
  50. package/dist/runtime/composables/useIntegrationsDeadLetters.js +27 -18
  51. package/dist/runtime/composables/useIntegrationsNodes.js +11 -1
  52. package/dist/runtime/composables/useIntegrationsRuns.d.ts +30 -1
  53. package/dist/runtime/composables/useIntegrationsRuns.js +70 -29
  54. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +7 -14
  55. package/dist/runtime/composables/useIntegrationsSchedules.js +80 -35
  56. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +30 -3
  57. package/dist/runtime/composables/useIntegrationsTriggers.js +18 -7
  58. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
  59. package/dist/runtime/composables/useIntegrationsWorkflows.js +25 -4
  60. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  61. package/dist/runtime/data/integrationNodeCatalog.d.ts +22 -1
  62. package/dist/runtime/data/integrationNodeCatalog.js +8 -4
  63. package/dist/runtime/data/loopBody.d.ts +129 -0
  64. package/dist/runtime/data/loopBody.js +76 -0
  65. package/dist/runtime/data/nodeInputData.d.ts +58 -1
  66. package/dist/runtime/data/nodeInputData.js +30 -5
  67. package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
  68. package/dist/runtime/data/triggerGlyphs.js +11 -0
  69. package/dist/runtime/i18n/messages/de.json +62 -33
  70. package/dist/runtime/i18n/messages/en.json +32 -3
  71. package/dist/runtime/pages/integrations/credentials.vue +4 -1
  72. package/dist/runtime/pages/integrations/index.vue +32 -34
  73. package/dist/runtime/pages/integrations/runs.vue +22 -10
  74. package/dist/runtime/pages/integrations/schedules.vue +64 -7
  75. package/dist/runtime/pages/integrations/templates/[slug].vue +13 -5
  76. package/dist/runtime/pages/integrations/workflows/[id].vue +193 -104
  77. package/dist/runtime/types/integrations.d.ts +64 -1
  78. package/dist/runtime/utils/cockpitSources.d.ts +22 -3
  79. package/dist/runtime/utils/credentialState.d.ts +6 -1
  80. package/dist/runtime/utils/credentialState.js +1 -1
  81. package/dist/runtime/utils/localize.d.ts +34 -0
  82. package/dist/runtime/utils/localize.js +22 -7
  83. package/dist/runtime/utils/pollBudget.d.ts +1 -1
  84. package/dist/runtime/utils/servedPaths.d.ts +22 -0
  85. package/dist/runtime/utils/servedPaths.js +3 -0
  86. package/dist/runtime/utils/statusStyle.d.ts +52 -0
  87. package/dist/runtime/utils/statusStyle.js +26 -0
  88. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
+ import { AltArrowLeft as ArrowLeft, Bolt as Zap, Magnifier as Search } from "#solar-icons";
4
+ import { triggerGlyph } from "../../data/triggerGlyphs";
3
5
  import * as Lucide from "lucide-vue-next";
4
6
  import { INTEGRATION_NODE_CATALOG, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
5
- import { nodeIcon } from "../../data/nodeIcons";
6
7
  import { resolveLocalized } from "../../utils/localize";
7
8
  const { load: loadNodes } = useIntegrationsNodes();
8
9
  onMounted(() => {
@@ -13,7 +14,6 @@ const emit = defineEmits(["insert"]);
13
14
  const search = ref("");
14
15
  const searchInput = ref(null);
15
16
  defineExpose({ focusSearch: () => searchInput.value?.focus() });
16
- const iconFor = nodeIcon;
17
17
  const CORE_GROUP_BY_CATEGORY = {
18
18
  control: "Flow",
19
19
  transform: "Data",
@@ -76,11 +76,14 @@ function isIntegration(m) {
76
76
  function makeGroup(label) {
77
77
  return { label, children: /* @__PURE__ */ new Map(), leaves: [], count: 0 };
78
78
  }
79
+ function isExecutable(m) {
80
+ return m.__hasBundle !== false;
81
+ }
79
82
  const latestManifests = computed(() => {
80
83
  const seen = /* @__PURE__ */ new Set();
81
84
  const out = [];
82
85
  for (const m of INTEGRATION_NODE_CATALOG) {
83
- if (seen.has(m.slug)) continue;
86
+ if (seen.has(m.slug) || !isExecutable(m)) continue;
84
87
  seen.add(m.slug);
85
88
  out.push(m);
86
89
  }
@@ -89,6 +92,7 @@ const latestManifests = computed(() => {
89
92
  const versionsBySlug = computed(() => {
90
93
  const map = /* @__PURE__ */ new Map();
91
94
  for (const m of INTEGRATION_NODE_CATALOG) {
95
+ if (!isExecutable(m)) continue;
92
96
  const list = map.get(m.slug);
93
97
  if (list) list.push(m);
94
98
  else map.set(m.slug, [m]);
@@ -282,14 +286,14 @@ const searching = computed(() => search.value.trim().length > 0);
282
286
  :title="t('integration.canvas.palette.action.back')"
283
287
  @click="popLevel"
284
288
  >
285
- <Lucide.ArrowLeft :size="14" />
289
+ <ArrowLeft :size="14" />
286
290
  </button>
287
291
  <div class="min-w-0 flex-1 truncate text-mono text-muted-foreground">
288
292
  {{ levelTitle }}
289
293
  </div>
290
294
  </div>
291
295
  <div class="relative">
292
- <Lucide.Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
296
+ <Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
293
297
  <input
294
298
  ref="searchInput"
295
299
  v-model="search"
@@ -333,7 +337,7 @@ const searching = computed(() => search.value.trim().length > 0);
333
337
  @click="insertTrigger(tpl)"
334
338
  >
335
339
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
336
- <component :is="iconFor(tpl.icon)" :size="13" />
340
+ <component :is="triggerGlyph(tpl.type)" :size="13" />
337
341
  </span>
338
342
  <div class="min-w-0">
339
343
  <div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
@@ -397,7 +401,7 @@ const searching = computed(() => search.value.trim().length > 0);
397
401
  @click="insertTrigger(tpl)"
398
402
  >
399
403
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
400
- <component :is="iconFor(tpl.icon)" :size="13" />
404
+ <component :is="triggerGlyph(tpl.type)" :size="13" />
401
405
  </span>
402
406
  <div class="min-w-0">
403
407
  <div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
@@ -417,7 +421,7 @@ const searching = computed(() => search.value.trim().length > 0);
417
421
  @click="pushPath(['__triggers'])"
418
422
  >
419
423
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-emerald-600/80">
420
- <Lucide.Zap :size="13" />
424
+ <Zap :size="13" />
421
425
  </span>
422
426
  <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ t("integration.canvas.palette.group.triggers") }}</span>
423
427
  <span class="text-sm tabular-nums text-muted-foreground">{{ TRIGGER_TEMPLATES.length }}</span>
@@ -2,7 +2,7 @@
2
2
  import { computed, onUnmounted, ref, watch } from "vue";
3
3
  import { Download as DownloadIcon, RefreshCircle as RefreshIcon } from "#solar-icons";
4
4
  import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@revenexx/studio";
5
- import { runStatusLabel, statusClass } from "../../utils/statusStyle";
5
+ import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
6
6
  import { IntegrationsApiError } from "../../utils/apiError";
7
7
  import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
8
8
  const props = defineProps({
@@ -270,9 +270,12 @@ function timelineBarClass(row) {
270
270
  }
271
271
  }
272
272
  const timelineDetail = ref(null);
273
+ function timelineRowStatusWord(row) {
274
+ if (row.durationMs == null) return "running";
275
+ return row.ok ? "succeeded" : "failed";
276
+ }
273
277
  function timelineRowStatus(row) {
274
- if (row.durationMs == null) return t("integration.runs.status.running");
275
- return t(row.ok ? "integration.runs.status.succeeded" : "integration.runs.status.failed");
278
+ return runStatusLabel(timelineRowStatusWord(row));
276
279
  }
277
280
  function fmtClock(ms) {
278
281
  try {
@@ -583,7 +586,7 @@ async function confirmAction() {
583
586
  <div>
584
587
  <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.status") }}</div>
585
588
  <div class="mt-0.5">
586
- <Badge variant="outline" :class="['border text-xs font-medium uppercase', statusClass(dStatus ?? run.status)]">
589
+ <Badge variant="outline" :class="['border text-xs font-medium uppercase', runStatusClass(dStatus ?? run.status)]">
587
590
  {{ runStatusLabel(dStatus ?? run.status) }}
588
591
  </Badge>
589
592
  </div>
@@ -699,7 +702,7 @@ async function confirmAction() {
699
702
  class="w-full flex items-center gap-3 px-3 py-2 text-left"
700
703
  @click="toggleStep(i)"
701
704
  >
702
- <Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0', statusClass(st.status)]">{{ runStatusLabel(st.status) }}</Badge>
705
+ <Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0', runStatusClass(st.status)]">{{ runStatusLabel(st.status) }}</Badge>
703
706
  <div class="min-w-0 flex-1">
704
707
  <div class="font-mono text-xs truncate">{{ st.node_slug || st.node_id }}</div>
705
708
  <div class="text-[10px] text-muted-foreground truncate">
@@ -802,7 +805,7 @@ async function confirmAction() {
802
805
  <Badge
803
806
  v-if="timelineDetail"
804
807
  variant="outline"
805
- :class="['border text-xs font-medium uppercase', statusClass(timelineRowStatus(timelineDetail))]"
808
+ :class="['border text-xs font-medium uppercase', runStatusClass(timelineRowStatusWord(timelineDetail))]"
806
809
  >{{ timelineRowStatus(timelineDetail) }}</Badge>
807
810
  <span v-if="timelineDetail" class="text-sm tabular-nums text-muted-foreground">{{ fmtMs(timelineDetail.durationMs) }}</span>
808
811
  </DialogTitle>
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, onMounted, onUnmounted, ref } from "vue";
3
3
  import { Badge } from "@revenexx/studio";
4
- import { RUN_STATUS_WORDS, normalisedRunStatus, runStatusLabel, statusClass } from "../../utils/statusStyle";
4
+ import { RUN_STATUS_WORDS, normalisedRunStatus, runStatusClass, runStatusLabel } from "../../utils/statusStyle";
5
5
  import { TRIGGER_TYPE_WORDS, triggerTypeLabel } from "../../data/integrationNodeCatalog";
6
6
  const props = defineProps({
7
7
  runs: { type: Array, required: true },
@@ -99,7 +99,7 @@ const columns = computed(() => [
99
99
  @row-click="(r) => openRun(r)"
100
100
  >
101
101
  <template #cell-status="{ row: run }">
102
- <Badge variant="outline" :class="['border text-xs font-medium uppercase', statusClass(run.status)]">
102
+ <Badge variant="outline" :class="['border text-xs font-medium uppercase', runStatusClass(run.status)]">
103
103
  {{ runStatusLabel(run.status) }}
104
104
  </Badge>
105
105
  </template>
@@ -100,24 +100,58 @@ function onHoverLeave() {
100
100
  }
101
101
  const toolbarVisible = computed(() => editable.value && !editing.value && (hovered.value || !!props.selected));
102
102
  function escapeHtml(s) {
103
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
103
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
104
104
  }
105
105
  function inline(s) {
106
106
  return s.replace(/`([^`\n]+)`/g, '<code class="rounded bg-black/[0.07] px-1 py-px font-mono text-[0.85em]">$1</code>').replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="font-medium underline decoration-current/40 underline-offset-2 hover:decoration-current">$1</a>').replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
107
107
  }
108
+ const NOTE_HEADINGS = [
109
+ { tag: "h2", cls: "m-0 text-base font-semibold" },
110
+ { tag: "h3", cls: "m-0 text-sm font-semibold" },
111
+ { tag: "h4", cls: "m-0 text-sm font-medium" }
112
+ ];
113
+ const HEADING_RE = /^(#{1,3})\s+(.+)$/;
114
+ const UL_RE = /^[-*]\s+(.+)$/;
115
+ const OL_RE = /^(\d+)\.\s+(.+)$/;
116
+ function renderList(tag, items) {
117
+ const li = items.map(({ marker, text }) => {
118
+ const numeric = tag === "ol" ? " tabular-nums" : "";
119
+ const hidden = tag === "ul" ? ' aria-hidden="true"' : "";
120
+ const ordinal = tag === "ol" ? ` value="${Number.parseInt(marker, 10)}"` : "";
121
+ return `<li${ordinal} class="flex gap-1.5"><span${hidden} class="select-none${numeric}">${marker}</span><span class="min-w-0">${text}</span></li>`;
122
+ }).join("");
123
+ return `<${tag} role="list" class="m-0 list-none p-0 pl-1">${li}</${tag}>`;
124
+ }
108
125
  function renderMarkdownLite(src) {
109
- return escapeHtml(src).split("\n").map((line) => {
110
- const h = /^(#{1,3})\s+(.+)$/.exec(line);
126
+ const lines = escapeHtml(src).split("\n");
127
+ const out = [];
128
+ let i = 0;
129
+ while (i < lines.length) {
130
+ const line = lines[i] ?? "";
131
+ const h = HEADING_RE.exec(line);
111
132
  if (h) {
112
- const cls = h[1].length === 1 ? "text-base font-semibold" : h[1].length === 2 ? "text-sm font-semibold" : "text-sm font-medium";
113
- return `<div class="${cls}">${inline(h[2])}</div>`;
133
+ const { tag, cls } = NOTE_HEADINGS[(h[1] ?? "#").length - 1] ?? NOTE_HEADINGS[2];
134
+ out.push(`<${tag} class="${cls}">${inline(h[2] ?? "")}</${tag}>`);
135
+ i++;
136
+ continue;
114
137
  }
115
- const ul = /^[-*]\s+(.+)$/.exec(line);
116
- if (ul) return `<div class="flex gap-1.5 pl-1"><span class="select-none">\u2022</span><span class="min-w-0">${inline(ul[1])}</span></div>`;
117
- const ol = /^(\d+)\.\s+(.+)$/.exec(line);
118
- if (ol) return `<div class="flex gap-1.5 pl-1"><span class="select-none tabular-nums">${ol[1]}.</span><span class="min-w-0">${inline(ol[2])}</span></div>`;
119
- return line ? `<div>${inline(line)}</div>` : "<div>&nbsp;</div>";
120
- }).join("");
138
+ const listRe = UL_RE.test(line) ? UL_RE : OL_RE.test(line) ? OL_RE : null;
139
+ if (listRe) {
140
+ const tag = listRe === UL_RE ? "ul" : "ol";
141
+ const items = [];
142
+ while (i < lines.length) {
143
+ const m = listRe.exec(lines[i] ?? "");
144
+ if (!m) break;
145
+ items.push({ marker: tag === "ul" ? "\u2022" : `${m[1]}.`, text: inline(m[m.length - 1] ?? "") });
146
+ i++;
147
+ }
148
+ out.push(renderList(tag, items));
149
+ continue;
150
+ }
151
+ out.push(line ? `<div>${inline(line)}</div>` : "<div>&nbsp;</div>");
152
+ i++;
153
+ }
154
+ return out.join("");
121
155
  }
122
156
  const rendered = computed(() => renderMarkdownLite(props.data.sticky.content));
123
157
  function onViewMouseDown(e) {
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One shortcut into a surface the *host* provides — Import & Export, the SFTP
3
+ * server, the event stream, inbound webhooks. None of the four is a page of this
4
+ * module: they are the cockpit's, and this card is the studio's landing page
5
+ * pointing at them.
6
+ *
7
+ * So the card has two states, and which one it is in is not its own decision:
8
+ *
9
+ * - **`available`** — the host has the surface. The card is a link and behaves
10
+ * exactly as it did before PO-223.
11
+ * - **not available** — the card stays on screen, looks the same, and is a button
12
+ * that explains where the surface lives instead of leading into a route the host
13
+ * does not serve. Before PO-223 it was a link into nothing: in the playground
14
+ * and in the standalone dev studio all four led to a 404.
15
+ *
16
+ * **Staying on screen is the point**, and it is the call the managed credential
17
+ * sources already made (`CredentialRevenexxSource.vue`, PO-222): a control that
18
+ * vanishes leaves nothing to read the explanation from, and the reader cannot tell
19
+ * a broken feature from one that belongs somewhere else.
20
+ *
21
+ * **No `aria-label`, on purpose** — like `TemplateCard`, the card takes its name
22
+ * from what it shows. The state is carried by the *role*: "Import & Export,
23
+ * button" instead of "link" is what says to a screen reader that this does not
24
+ * navigate, and it says it without announcing something no sighted reader can see
25
+ * (`specs/accessibility.md` AC-1 names the on-screen text as the source of a name).
26
+ *
27
+ * Who decides `available`, and why it is the router rather than a guess about the
28
+ * host, is `docs/host-surfaces.md`.
29
+ *
30
+ * The two branches draw the same body twice rather than sharing it: `as-child`
31
+ * wants a real element as its child, and the alternatives — a wrapper element
32
+ * around the card, or a second component for eight lines — cost either the grid
33
+ * layout or a file. **Keep the two bodies identical.**
34
+ */
35
+ import type { Component } from 'vue';
36
+ type __VLS_Props = {
37
+ /** What the surface is called, in the reader's language. */
38
+ label: string;
39
+ /** One line on what it is for. Shown in both states. */
40
+ hint: string;
41
+ icon: Component;
42
+ /** Where the surface lives in a host that has it. */
43
+ to: string;
44
+ /** Whether this host has it. See the comment above; no default on purpose. */
45
+ available: boolean;
46
+ /** What the popover says when it does not. */
47
+ note: string;
48
+ };
49
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
50
+ export default _default;
@@ -0,0 +1,55 @@
1
+ <script setup>
2
+ import { ArrowRight } from "#solar-icons";
3
+ import { Popover, PopoverContent, PopoverTrigger } from "@revenexx/studio";
4
+ defineProps({
5
+ label: { type: String, required: true },
6
+ hint: { type: String, required: true },
7
+ icon: { type: null, required: true },
8
+ to: { type: String, required: true },
9
+ available: { type: Boolean, required: true },
10
+ note: { type: String, required: true }
11
+ });
12
+ const CARD_CLASS = "group rounded-lg border border-black/[0.06] bg-white p-3 transition hover:border-black/20";
13
+ </script>
14
+
15
+ <template>
16
+ <NuxtLink v-if="available" :to="to" :class="CARD_CLASS">
17
+ <div class="flex items-center gap-2">
18
+ <span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
19
+ <component :is="icon" :size="12" />
20
+ </span>
21
+ <span class="min-w-0 flex-1 truncate text-sm font-semibold">{{ label }}</span>
22
+ <ArrowRight :size="11" class="text-muted-foreground transition-transform group-hover:translate-x-0.5" />
23
+ </div>
24
+ <p class="mt-2 text-sm text-muted-foreground">
25
+ {{ hint }}
26
+ </p>
27
+ </NuxtLink>
28
+
29
+ <Popover v-else>
30
+ <PopoverTrigger as-child>
31
+ <!-- No `id` here: `PopoverTrigger` puts reka-ui's generated `triggerId` on
32
+ its child and `PopoverContentImpl` points the opened dialog's
33
+ `aria-labelledby` at exactly that id, so the popover is named by this
34
+ card. Ours would replace it (reka's `Slot` merges with the child's
35
+ props winning) and leave the dialog naming nothing. -->
36
+ <button type="button" :class="[CARD_CLASS, 'w-full text-left']">
37
+ <div class="flex items-center gap-2">
38
+ <span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
39
+ <component :is="icon" :size="12" />
40
+ </span>
41
+ <span class="min-w-0 flex-1 truncate text-sm font-semibold">{{ label }}</span>
42
+ <ArrowRight :size="11" class="text-muted-foreground transition-transform group-hover:translate-x-0.5" />
43
+ </div>
44
+ <p class="mt-2 text-sm text-muted-foreground">
45
+ {{ hint }}
46
+ </p>
47
+ </button>
48
+ </PopoverTrigger>
49
+ <PopoverContent align="start" class="w-72 rounded-lg border border-black/[0.06] bg-white p-3 shadow-lg">
50
+ <p class="text-sm text-muted-foreground">
51
+ {{ note }}
52
+ </p>
53
+ </PopoverContent>
54
+ </Popover>
55
+ </template>
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One shortcut into a surface the *host* provides — Import & Export, the SFTP
3
+ * server, the event stream, inbound webhooks. None of the four is a page of this
4
+ * module: they are the cockpit's, and this card is the studio's landing page
5
+ * pointing at them.
6
+ *
7
+ * So the card has two states, and which one it is in is not its own decision:
8
+ *
9
+ * - **`available`** — the host has the surface. The card is a link and behaves
10
+ * exactly as it did before PO-223.
11
+ * - **not available** — the card stays on screen, looks the same, and is a button
12
+ * that explains where the surface lives instead of leading into a route the host
13
+ * does not serve. Before PO-223 it was a link into nothing: in the playground
14
+ * and in the standalone dev studio all four led to a 404.
15
+ *
16
+ * **Staying on screen is the point**, and it is the call the managed credential
17
+ * sources already made (`CredentialRevenexxSource.vue`, PO-222): a control that
18
+ * vanishes leaves nothing to read the explanation from, and the reader cannot tell
19
+ * a broken feature from one that belongs somewhere else.
20
+ *
21
+ * **No `aria-label`, on purpose** — like `TemplateCard`, the card takes its name
22
+ * from what it shows. The state is carried by the *role*: "Import & Export,
23
+ * button" instead of "link" is what says to a screen reader that this does not
24
+ * navigate, and it says it without announcing something no sighted reader can see
25
+ * (`specs/accessibility.md` AC-1 names the on-screen text as the source of a name).
26
+ *
27
+ * Who decides `available`, and why it is the router rather than a guess about the
28
+ * host, is `docs/host-surfaces.md`.
29
+ *
30
+ * The two branches draw the same body twice rather than sharing it: `as-child`
31
+ * wants a real element as its child, and the alternatives — a wrapper element
32
+ * around the card, or a second component for eight lines — cost either the grid
33
+ * layout or a file. **Keep the two bodies identical.**
34
+ */
35
+ import type { Component } from 'vue';
36
+ type __VLS_Props = {
37
+ /** What the surface is called, in the reader's language. */
38
+ label: string;
39
+ /** One line on what it is for. Shown in both states. */
40
+ hint: string;
41
+ icon: Component;
42
+ /** Where the surface lives in a host that has it. */
43
+ to: string;
44
+ /** Whether this host has it. See the comment above; no default on purpose. */
45
+ available: boolean;
46
+ /** What the popover says when it does not. */
47
+ note: string;
48
+ };
49
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
50
+ export default _default;
@@ -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. */