@revenexx/studio-integrations 1.3.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 (58) hide show
  1. package/CHANGELOG.md +426 -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/NodeConfigField.d.vue.ts +10 -0
  8. package/dist/runtime/components/Integrations/NodeConfigField.vue +94 -49
  9. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +10 -0
  10. package/dist/runtime/components/Integrations/NodeInspector.vue +37 -60
  11. package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
  12. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
  13. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
  14. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
  15. package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
  16. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
  17. package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
  18. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
  19. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
  20. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
  21. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
  22. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
  23. package/dist/runtime/components/Integrations/WorkflowEditor.vue +165 -56
  24. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
  25. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
  26. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
  27. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
  28. package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
  29. package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
  30. package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
  31. package/dist/runtime/composables/useCredentialTypes.js +3 -9
  32. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
  33. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  34. package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
  35. package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
  36. package/dist/runtime/composables/useIntegrationsTemplates.js +5 -8
  37. package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
  38. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -7
  39. package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
  40. package/dist/runtime/data/canvasShortcuts.js +68 -0
  41. package/dist/runtime/i18n/messages/de.json +30 -16
  42. package/dist/runtime/i18n/messages/en.json +30 -16
  43. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
  44. package/dist/runtime/pages/integrations/workflows/[id].vue +128 -33
  45. package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
  46. package/dist/runtime/utils/apiEnvelope.js +20 -0
  47. package/dist/runtime/utils/configDefaults.d.ts +18 -0
  48. package/dist/runtime/utils/configDefaults.js +7 -0
  49. package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
  50. package/dist/runtime/utils/errorPathAdvice.js +19 -0
  51. package/dist/runtime/utils/notify.d.ts +2 -2
  52. package/dist/runtime/utils/runEnvelope.d.ts +12 -0
  53. package/dist/runtime/utils/runEnvelope.js +2 -1
  54. package/dist/runtime/utils/selectOptions.d.ts +48 -0
  55. package/dist/runtime/utils/selectOptions.js +19 -0
  56. package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
  57. package/dist/runtime/utils/shortcutLabel.js +48 -0
  58. package/package.json +1 -1
@@ -9,6 +9,8 @@ import {
9
9
  ContextMenuShortcut,
10
10
  ContextMenuTrigger
11
11
  } from "@revenexx/studio";
12
+ import { primaryShortcut } from "../../data/canvasShortcuts";
13
+ import { formatShortcut } from "../../utils/shortcutLabel";
12
14
  const props = defineProps({
13
15
  kind: { type: String, required: true },
14
16
  selectionCount: { type: Number, required: true },
@@ -18,44 +20,24 @@ const props = defineProps({
18
20
  });
19
21
  const emit = defineEmits(["action", "update:open"]);
20
22
  const t = useIntegrationsT();
21
- const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
22
- function formatShortcut(tokens) {
23
- const parts = tokens.split("+");
24
- const out = [];
25
- for (const raw of parts) {
26
- const t2 = raw.toLowerCase();
27
- if (t2 === "mod") out.push(isMac ? "\u2318" : "Ctrl");
28
- else if (t2 === "shift") out.push("\u21E7");
29
- else if (t2 === "alt") out.push(isMac ? "\u2325" : "Alt");
30
- else if (t2 === "enter") out.push("\u21B5");
31
- else if (t2 === "backspace") out.push("\u232B");
32
- else if (t2 === "delete") out.push("Del");
33
- else if (t2 === "escape") out.push("Esc");
34
- else if (t2 === "tab") out.push("Tab");
35
- else if (t2 === "space") out.push("Space");
36
- else if (t2 === "f2") out.push("F2");
37
- else out.push(t2.toUpperCase());
38
- }
39
- return isMac ? out.join("") : out.join("+");
40
- }
41
23
  const allSelected = computed(() => props.totalNodes > 0 && props.selectionCount >= props.totalNodes);
42
24
  const sections = computed(() => {
43
25
  if (props.kind === "node") {
44
26
  return [
45
27
  [
46
- { id: "open", label: t("integration.canvas.contextMenu.item.open"), shortcut: "enter" },
47
- { id: "rename", label: t("integration.canvas.contextMenu.item.rename"), shortcut: "f2" }
28
+ { id: "open", label: t("integration.canvas.contextMenu.item.open"), shortcut: primaryShortcut("open") },
29
+ { id: "rename", label: t("integration.canvas.contextMenu.item.rename"), shortcut: primaryShortcut("rename") }
48
30
  ],
49
31
  [
50
- { id: "copy", label: t("integration.canvas.contextMenu.item.copy"), shortcut: "mod+c", disabled: props.targetIsTrigger },
51
- { id: "duplicate", label: t("integration.canvas.contextMenu.item.duplicate"), shortcut: "mod+d", disabled: props.targetIsTrigger }
32
+ { id: "copy", label: t("integration.canvas.contextMenu.item.copy"), shortcut: primaryShortcut("copy"), disabled: props.targetIsTrigger },
33
+ { id: "duplicate", label: t("integration.canvas.contextMenu.item.duplicate"), shortcut: primaryShortcut("duplicate"), disabled: props.targetIsTrigger }
52
34
  ],
53
35
  [
54
- { id: "run", label: t("integration.canvas.contextMenu.item.runWorkflow"), shortcut: "mod+enter", disabled: props.runBusy },
55
- { id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: "shift+alt+t" }
36
+ { id: "run", label: t("integration.canvas.contextMenu.item.runWorkflow"), shortcut: primaryShortcut("run"), disabled: props.runBusy },
37
+ { id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: primaryShortcut("tidyUp") }
56
38
  ],
57
39
  [
58
- { id: "delete", label: t("integration.common.action.delete"), shortcut: "backspace", danger: true }
40
+ { id: "delete", label: t("integration.common.action.delete"), shortcut: primaryShortcut("delete"), danger: true }
59
41
  ]
60
42
  ];
61
43
  }
@@ -63,32 +45,32 @@ const sections = computed(() => {
63
45
  const n = props.selectionCount;
64
46
  return [
65
47
  [
66
- { id: "copy", label: t("integration.canvas.contextMenu.item.copyNodes", n), shortcut: "mod+c" },
67
- { id: "duplicate", label: t("integration.canvas.contextMenu.item.duplicateNodes", n), shortcut: "mod+d" }
48
+ { id: "copy", label: t("integration.canvas.contextMenu.item.copyNodes", n), shortcut: primaryShortcut("copy") },
49
+ { id: "duplicate", label: t("integration.canvas.contextMenu.item.duplicateNodes", n), shortcut: primaryShortcut("duplicate") }
68
50
  ],
69
51
  [
70
- { id: "group", label: t("integration.canvas.contextMenu.item.groupNodes", n), shortcut: "mod+g" },
71
- { id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: "shift+alt+t" },
72
- { id: "select-all", label: t("integration.canvas.contextMenu.item.selectAll"), shortcut: "mod+a", disabled: allSelected.value },
73
- { id: "clear-selection", label: t("integration.canvas.contextMenu.item.clearSelection"), shortcut: "escape" }
52
+ { id: "group", label: t("integration.canvas.contextMenu.item.groupNodes", n), shortcut: primaryShortcut("group") },
53
+ { id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: primaryShortcut("tidyUp") },
54
+ { id: "select-all", label: t("integration.canvas.contextMenu.item.selectAll"), shortcut: primaryShortcut("selectAll"), disabled: allSelected.value },
55
+ { id: "clear-selection", label: t("integration.canvas.contextMenu.item.clearSelection"), shortcut: primaryShortcut("clearSelection") }
74
56
  ],
75
57
  [
76
- { id: "delete", label: t("integration.canvas.contextMenu.item.deleteNodes", n), shortcut: "backspace", danger: true }
58
+ { id: "delete", label: t("integration.canvas.contextMenu.item.deleteNodes", n), shortcut: primaryShortcut("delete"), danger: true }
77
59
  ]
78
60
  ];
79
61
  }
80
62
  return [
81
63
  [
82
- { id: "add-node", label: t("integration.canvas.contextMenu.item.addNode"), shortcut: "tab" },
83
- { id: "add-sticky", label: t("integration.canvas.contextMenu.item.addSticky"), shortcut: "shift+s" },
84
- { id: "paste", label: t("integration.canvas.contextMenu.item.paste"), shortcut: "mod+v" }
64
+ { id: "add-node", label: t("integration.canvas.contextMenu.item.addNode"), shortcut: primaryShortcut("addNode") },
65
+ { id: "add-sticky", label: t("integration.canvas.contextMenu.item.addSticky"), shortcut: primaryShortcut("addSticky") },
66
+ { id: "paste", label: t("integration.canvas.contextMenu.item.paste"), shortcut: primaryShortcut("paste") }
85
67
  ],
86
68
  [
87
- { id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: "shift+alt+t" },
88
- { id: "fit-view", label: t("integration.canvas.contextMenu.item.fitView"), shortcut: "1" }
69
+ { id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: primaryShortcut("tidyUp") },
70
+ { id: "fit-view", label: t("integration.canvas.contextMenu.item.fitView"), shortcut: primaryShortcut("fitView") }
89
71
  ],
90
72
  [
91
- { id: "select-all", label: t("integration.canvas.contextMenu.item.selectAll"), shortcut: "mod+a", disabled: allSelected.value || props.totalNodes === 0 }
73
+ { id: "select-all", label: t("integration.canvas.contextMenu.item.selectAll"), shortcut: primaryShortcut("selectAll"), disabled: allSelected.value || props.totalNodes === 0 }
92
74
  ]
93
75
  ];
94
76
  });
@@ -17,11 +17,11 @@ type __VLS_Slots = {} & {
17
17
  default?: (props: typeof __VLS_14) => any;
18
18
  };
19
19
  declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
20
- action: (id: MenuActionId) => any;
21
20
  "update:open": (open: boolean) => any;
21
+ action: (id: MenuActionId) => any;
22
22
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
23
- onAction?: ((id: MenuActionId) => any) | undefined;
24
23
  "onUpdate:open"?: ((open: boolean) => any) | undefined;
24
+ onAction?: ((id: MenuActionId) => any) | undefined;
25
25
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
26
26
  declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
27
27
  export default _default;
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
- import { RefreshCircle as Loader2, Magnifier as Search, MagicStick as Sparkles } from "#solar-icons";
3
+ import { AltArrowLeft as ArrowLeft, RefreshCircle as Loader2, Magnifier as Search, MagicStick as Sparkles } from "#solar-icons";
4
4
  import { triggerGlyph } from "../../data/triggerGlyphs";
5
5
  import * as Lucide from "lucide-vue-next";
6
6
  import {
@@ -19,10 +19,10 @@ import {
19
19
  SelectValue,
20
20
  Textarea
21
21
  } from "@revenexx/studio";
22
- import { toast } from "vue-sonner";
23
22
  import { TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
24
23
  import LoadingAnnouncement from "./LoadingAnnouncement.vue";
25
24
  import TemplateCard from "./TemplateCard.vue";
25
+ import TemplateInstallStep from "./TemplateInstallStep.vue";
26
26
  import WorkflowIconPicker from "./WorkflowIconPicker.vue";
27
27
  import WorkflowTagsInput from "./WorkflowTagsInput.vue";
28
28
  import { resolveLocalized } from "../../utils/localize";
@@ -31,7 +31,7 @@ const open = defineModel("open", { type: Boolean, ...{ default: false } });
31
31
  const t = useIntegrationsT();
32
32
  const router = useRouter();
33
33
  const { create, workflows } = useIntegrationsWorkflows();
34
- const { templates, loading: templatesLoading, error: templatesError, load: loadTemplates, ensureDetails, instantiate } = useIntegrationsTemplates();
34
+ const { templates, loading: templatesLoading, error: templatesError, load: loadTemplates, ensureDetails } = useIntegrationsTemplates();
35
35
  const mode = ref("blank");
36
36
  const modeTabs = computed(() => [
37
37
  { key: "blank", label: t("integration.workflows.create.tab.blank") },
@@ -72,6 +72,7 @@ watch(open, (v) => {
72
72
  icon.value = null;
73
73
  error.value = null;
74
74
  templateSearch.value = "";
75
+ pickedSlug.value = null;
75
76
  void loadTemplates();
76
77
  });
77
78
  async function submit() {
@@ -109,8 +110,14 @@ async function submit() {
109
110
  submitting.value = false;
110
111
  }
111
112
  }
112
- const instantiatingSlug = ref(null);
113
+ const pickedSlug = ref(null);
113
114
  const templateSearch = ref("");
115
+ const stepLoading = ref(false);
116
+ const stepInstalling = ref(false);
117
+ watch(mode, (m) => {
118
+ if (m !== "template") pickedSlug.value = null;
119
+ });
120
+ const pickedTemplate = computed(() => templates.value.find((tpl) => tpl.slug === pickedSlug.value) ?? null);
114
121
  watch(() => templates.value.length, (n) => {
115
122
  if (n) void ensureDetails(templates.value.map((tpl) => tpl.slug));
116
123
  }, { immediate: true });
@@ -119,19 +126,9 @@ const filteredTemplates = computed(() => {
119
126
  if (!q) return templates.value;
120
127
  return templates.value.filter((tpl) => `${tpl.name} ${tpl.description} ${tpl.category}`.toLowerCase().includes(q));
121
128
  });
122
- async function useTemplate(slug) {
123
- if (instantiatingSlug.value) return;
124
- instantiatingSlug.value = slug;
125
- try {
126
- const id = await instantiate(slug);
127
- if (id == null) throw new Error("The template could not be instantiated.");
128
- open.value = false;
129
- await router.push(`/integrations/workflows/${id}`);
130
- } catch (e) {
131
- toast.error(errorReason(e, t("integration.workflows.create.toast.templateError")));
132
- } finally {
133
- instantiatingSlug.value = null;
134
- }
129
+ async function installed(id) {
130
+ open.value = false;
131
+ await router.push(`/integrations/workflows/${id}`);
135
132
  }
136
133
  </script>
137
134
 
@@ -154,8 +151,8 @@ async function useTemplate(slug) {
154
151
  there (PO-251). -->
155
152
  <LoadingAnnouncement
156
153
  :label="t('integration.workflows.create.tab.template')"
157
- :loading="mode === 'template' && templatesLoading && !templates.length"
158
- :text="t('integration.workflows.create.template.state.loading')"
154
+ :loading="mode === 'template' && (stepLoading || templatesLoading && !templates.length)"
155
+ :text="stepLoading ? t('integration.templates.detail.install.state.loading') : t('integration.workflows.create.template.state.loading')"
159
156
  />
160
157
 
161
158
  <!-- ── Blank ─────────────────────────────────────────────────────────── -->
@@ -265,58 +262,90 @@ async function useTemplate(slug) {
265
262
 
266
263
  <!-- ── From template ─────────────────────────────────────────────────── -->
267
264
  <div v-else class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1">
268
- <div class="relative">
269
- <Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
270
- <input
271
- v-model="templateSearch"
272
- type="text"
273
- :aria-label="t('integration.workflows.create.template.search.aria')"
274
- :placeholder="t('integration.workflows.create.template.search.placeholder')"
275
- class="h-8 w-full rounded-md border border-black/[0.08] bg-white pl-7 pr-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/40"
265
+ <!-- The install step, once a template is picked: the same one the
266
+ template's own page opens, so both routes create the same workflow. -->
267
+ <template v-if="pickedSlug">
268
+ <div class="flex items-center gap-2">
269
+ <!-- Closed while the install is in flight: the step would unmount
270
+ under it, the request would land anyway, and the editor would
271
+ open on a workflow nobody was told was being created — twice
272
+ over if a second template were picked in that window. -->
273
+ <Button variant="outline" class="h-8 shrink-0 text-sm" :disabled="stepInstalling" @click="pickedSlug = null">
274
+ <ArrowLeft :size="13" />
275
+ {{ t("integration.workflows.create.template.backToPicker") }}
276
+ </Button>
277
+ <span class="truncate text-sm font-medium">{{ pickedTemplate?.name }}</span>
278
+ </div>
279
+ <p class="text-sm text-muted-foreground">
280
+ {{ t("integration.templates.detail.install.description") }}
281
+ </p>
282
+
283
+ <TemplateInstallStep
284
+ :slug="pickedSlug"
285
+ @installed="installed"
286
+ @update:loading="stepLoading = $event"
287
+ @update:installing="stepInstalling = $event"
276
288
  >
277
- </div>
289
+ <template #secondary="{ installing }">
290
+ <Button variant="outline" :disabled="installing" @click="open = false">
291
+ {{ t("integration.common.action.cancel") }}
292
+ </Button>
293
+ </template>
294
+ </TemplateInstallStep>
295
+ </template>
296
+
297
+ <template v-else>
298
+ <div class="relative">
299
+ <Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
300
+ <input
301
+ v-model="templateSearch"
302
+ type="text"
303
+ :aria-label="t('integration.workflows.create.template.search.aria')"
304
+ :placeholder="t('integration.workflows.create.template.search.placeholder')"
305
+ class="h-8 w-full rounded-md border border-black/[0.08] bg-white pl-7 pr-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/40"
306
+ >
307
+ </div>
278
308
 
279
- <div v-if="templatesLoading && !templates.length" aria-hidden="true" class="grid gap-2 p-1 sm:grid-cols-2">
280
- <div v-for="n in 6" :key="`sk-tpl-${n}`" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
281
- <div class="flex items-center gap-2">
282
- <div class="size-7 rounded-md bg-black/[0.07] animate-pulse" />
283
- <div class="h-3 w-1/2 rounded bg-black/[0.07] animate-pulse" />
309
+ <div v-if="templatesLoading && !templates.length" aria-hidden="true" class="grid gap-2 p-1 sm:grid-cols-2">
310
+ <div v-for="n in 6" :key="`sk-tpl-${n}`" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
311
+ <div class="flex items-center gap-2">
312
+ <div class="size-7 rounded-md bg-black/[0.07] animate-pulse" />
313
+ <div class="h-3 w-1/2 rounded bg-black/[0.07] animate-pulse" />
314
+ </div>
315
+ <div class="h-3 w-full rounded bg-black/[0.05] animate-pulse" />
316
+ <div class="h-3 w-2/3 rounded bg-black/[0.05] animate-pulse" />
284
317
  </div>
285
- <div class="h-3 w-full rounded bg-black/[0.05] animate-pulse" />
286
- <div class="h-3 w-2/3 rounded bg-black/[0.05] animate-pulse" />
287
318
  </div>
288
- </div>
289
- <div v-else-if="templatesError" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
290
- {{ templatesError }}
291
- </div>
292
- <p v-else-if="!templates.length" class="p-6 text-center text-sm text-muted-foreground">
293
- {{ t("integration.workflows.create.template.empty") }}
294
- </p>
295
- <p v-else-if="!filteredTemplates.length" class="p-6 text-center text-sm text-muted-foreground">
296
- {{ t("integration.workflows.create.template.noMatch", { query: templateSearch }) }}
297
- </p>
319
+ <div v-else-if="templatesError" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
320
+ {{ templatesError }}
321
+ </div>
322
+ <p v-else-if="!templates.length" class="p-6 text-center text-sm text-muted-foreground">
323
+ {{ t("integration.workflows.create.template.empty") }}
324
+ </p>
325
+ <p v-else-if="!filteredTemplates.length" class="p-6 text-center text-sm text-muted-foreground">
326
+ {{ t("integration.workflows.create.template.noMatch", { query: templateSearch }) }}
327
+ </p>
298
328
 
299
- <div v-else class="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
300
- <TemplateCard
301
- v-for="tpl in filteredTemplates"
302
- :key="tpl.slug"
303
- :template="tpl"
304
- density="compact"
305
- :busy="instantiatingSlug === tpl.slug"
306
- :disabled="!!instantiatingSlug"
307
- @select="useTemplate(tpl.slug)"
308
- />
309
- </div>
329
+ <div v-else class="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
330
+ <TemplateCard
331
+ v-for="tpl in filteredTemplates"
332
+ :key="tpl.slug"
333
+ :template="tpl"
334
+ density="compact"
335
+ @select="pickedSlug = tpl.slug"
336
+ />
337
+ </div>
310
338
 
311
- <div class="flex items-center justify-between pt-1">
312
- <NuxtLink to="/integrations/templates" class="inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary" @click="open = false">
313
- <Sparkles :size="13" />
314
- {{ t("integration.workflows.create.template.browseAll") }}
315
- </NuxtLink>
316
- <Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
317
- {{ t("integration.common.action.cancel") }}
318
- </Button>
319
- </div>
339
+ <div class="flex items-center justify-between pt-1">
340
+ <NuxtLink to="/integrations/templates" class="inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary" @click="open = false">
341
+ <Sparkles :size="13" />
342
+ {{ t("integration.workflows.create.template.browseAll") }}
343
+ </NuxtLink>
344
+ <Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
345
+ {{ t("integration.common.action.cancel") }}
346
+ </Button>
347
+ </div>
348
+ </template>
320
349
  </div>
321
350
  </DialogContent>
322
351
  </Dialog>
@@ -42,17 +42,51 @@ type __VLS_Props = {
42
42
  * id that is on the canvas is in the blob by construction.
43
43
  */
44
44
  declare function revealNode(id: string): void;
45
+ /**
46
+ * Point at a node without opening it — for a reader who has to look at the
47
+ * card's *surroundings* rather than at its settings.
48
+ *
49
+ * The deliberate difference from {@link revealNode} is the missing selection,
50
+ * and with it the missing settings lightbox. That is the whole reason there are
51
+ * two functions: the validation banner sends someone to a field at fault, and
52
+ * the dialog is where that field is. The error-path advice sends them to a port
53
+ * with nothing on it, and the question they have to answer — where should this
54
+ * failure go? — is answered by the graph around the card, which a dialog over
55
+ * the canvas covers up.
56
+ *
57
+ * Marked with the same pulse a newly created card gets, because the job is the
58
+ * same one: say which of thirty cards is the one meant.
59
+ */
60
+ declare function showNodeOnCanvas(id: string): void;
61
+ /**
62
+ * Hang a Stop-and-Error step off a node's error output and wire it up
63
+ * (specs/workflow-advice.md AC-3).
64
+ *
65
+ * The advice strip can tell an author their error output leads nowhere; on its
66
+ * own that is a reprimand without a remedy, because the repair — drop a node,
67
+ * pick the right one, connect it, write an expression quoting the failing step's
68
+ * message — is four steps of tedium they have to already know. Every template we
69
+ * ship does exactly this, twenty-five times over, so the shape is settled and
70
+ * worth one click.
71
+ *
72
+ * Leaves the result unsaved and unopened, like every other insertion here: the
73
+ * new step is highlighted and the author decides whether to keep it. Deleting it
74
+ * is the undo, because this editor has no undo stack to hook into.
75
+ */
76
+ declare function attachErrorStop(sourceNodeId: string, port: string): void;
45
77
  declare const _default: import("vue").DefineComponent<__VLS_Props, {
46
78
  revealNode: typeof revealNode;
79
+ showNodeOnCanvas: typeof showNodeOnCanvas;
80
+ attachErrorStop: typeof attachErrorStop;
47
81
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
48
- change: (blob: WorkflowBlob) => any;
49
82
  run: () => any;
50
83
  toggleActive: () => any;
84
+ change: (blob: WorkflowBlob) => any;
51
85
  openSettings: () => any;
52
86
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
53
- onChange?: ((blob: WorkflowBlob) => any) | undefined;
54
87
  onRun?: (() => any) | undefined;
55
88
  onToggleActive?: (() => any) | undefined;
89
+ onChange?: ((blob: WorkflowBlob) => any) | undefined;
56
90
  onOpenSettings?: (() => any) | undefined;
57
91
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
92
  export default _default;