@revenexx/studio-integrations 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +17 -4
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
  7. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
  8. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  9. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
  10. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  11. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
  13. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  14. package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
  15. package/dist/runtime/components/Integrations/Layout.vue +21 -2
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
  18. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  19. package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  21. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
  23. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  24. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  25. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  26. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  27. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  28. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  29. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  30. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  31. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
  32. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  33. package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
  34. package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
  35. package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
  36. package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
  38. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  39. package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
  40. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  41. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  42. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
  43. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  44. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
  45. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  46. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
  47. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  48. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
  49. package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
  50. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
  51. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
  52. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  53. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
  54. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  55. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  56. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
  57. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  58. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
  59. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
  60. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  61. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
  62. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  63. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
  64. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
  65. package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
  66. package/dist/runtime/composables/useCredentialTypes.js +4 -4
  67. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  68. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
  69. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  70. package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
  71. package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
  72. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  73. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  74. package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
  75. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
  76. package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
  77. package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
  78. package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
  79. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  80. package/dist/runtime/composables/useLoadGate.js +8 -0
  81. package/dist/runtime/data/branchReorder.d.ts +37 -0
  82. package/dist/runtime/data/branchReorder.js +71 -0
  83. package/dist/runtime/data/handleId.d.ts +23 -0
  84. package/dist/runtime/data/handleId.js +6 -0
  85. package/dist/runtime/data/integrationTemplates.d.ts +39 -0
  86. package/dist/runtime/data/integrationTemplates.js +15 -0
  87. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  88. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  89. package/dist/runtime/data/nodeIcons.js +73 -0
  90. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  91. package/dist/runtime/data/nodeInputData.js +4 -0
  92. package/dist/runtime/data/payloadForm.d.ts +9 -2
  93. package/dist/runtime/data/payloadForm.js +27 -3
  94. package/dist/runtime/data/portTone.d.ts +17 -0
  95. package/dist/runtime/data/portTone.js +7 -0
  96. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  97. package/dist/runtime/data/webhookAuth.js +19 -0
  98. package/dist/runtime/data/workflowIconCatalog.js +2 -5
  99. package/dist/runtime/pages/integrations/audit.vue +91 -6
  100. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
  101. package/dist/runtime/pages/integrations/credentials.vue +119 -21
  102. package/dist/runtime/pages/integrations/index.vue +49 -44
  103. package/dist/runtime/pages/integrations/runs.vue +1 -1
  104. package/dist/runtime/pages/integrations/schedules.vue +51 -7
  105. package/dist/runtime/pages/integrations/secrets.vue +29 -7
  106. package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
  107. package/dist/runtime/pages/integrations/templates/index.vue +57 -86
  108. package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
  109. package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
  110. package/dist/runtime/types/integrations.d.ts +65 -6
  111. package/dist/runtime/utils/apiError.d.ts +47 -0
  112. package/dist/runtime/utils/apiError.js +32 -0
  113. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  114. package/dist/runtime/utils/cockpitSources.js +14 -0
  115. package/dist/runtime/utils/loadGate.d.ts +61 -0
  116. package/dist/runtime/utils/loadGate.js +22 -0
  117. package/dist/runtime/utils/localize.d.ts +29 -0
  118. package/dist/runtime/utils/localize.js +4 -0
  119. package/dist/runtime/utils/notify.d.ts +59 -0
  120. package/dist/runtime/utils/notify.js +21 -0
  121. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  122. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  123. package/dist/runtime/utils/schedule.d.ts +141 -0
  124. package/dist/runtime/utils/schedule.js +207 -0
  125. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  126. package/dist/runtime/utils/validationFindings.js +53 -0
  127. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  128. package/dist/runtime/utils/whenIdle.js +12 -0
  129. package/package.json +8 -2
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, ref, watch } from "vue";
2
+ import { computed, ref, useId, watch } from "vue";
3
3
  import { Paperclip, TrashBinMinimalistic as Trash2 } from "#solar-icons";
4
4
  import { Input, Switch, Textarea } from "@revenexx/studio";
5
5
  import { buildPayload, payloadFieldsFromBlob, splitPayload } from "../../data/payloadForm";
@@ -8,6 +8,8 @@ const props = defineProps({
8
8
  });
9
9
  const model = defineModel({ type: String, ...{ default: "{}" } });
10
10
  const fields = computed(() => payloadFieldsFromBlob(props.blob));
11
+ const uid = useId();
12
+ const fieldId = (path) => `${uid}-${path}`;
11
13
  const textValues = ref({});
12
14
  const files = ref({});
13
15
  const extras = ref({});
@@ -96,17 +98,24 @@ function prettyBytes(n) {
96
98
  <span class="text-xs font-medium text-muted-foreground">
97
99
  {{ expert ? "Trigger payload (JSON)" : "Trigger payload" }}
98
100
  </span>
101
+ <!-- The Switch is a `button role="switch"`, and HTML-AAM does not name a
102
+ button from a wrapping <label> — Chromium does, other engines need
103
+ not. The explicit name is what the promise rests on. -->
99
104
  <label class="flex items-center gap-1.5 text-xs text-muted-foreground">
100
- <Switch :model-value="expert" @update:model-value="toggleExpert" />
105
+ <Switch aria-label="Expert (JSON)" :model-value="expert" @update:model-value="toggleExpert" />
101
106
  Expert (JSON)
102
107
  </label>
103
108
  </div>
104
- <label v-else class="text-xs font-medium text-muted-foreground">Trigger payload (JSON)</label>
109
+ <span v-else class="text-xs font-medium text-muted-foreground">Trigger payload (JSON)</span>
105
110
 
106
111
  <!-- ── Form mode: fields derived from the blob's payload references ──── -->
107
112
  <div v-if="!expert && fields.length" class="space-y-3">
108
113
  <div v-for="field in fields" :key="`${field.kind}:${field.path}`" class="space-y-1">
109
- <label class="text-xs font-medium">{{ field.label }}</label>
114
+ <!-- A file field has no control to point at once a file is attached —
115
+ the drop zone is replaced by the file's own row — so there the
116
+ caption stays a caption and the controls name themselves. -->
117
+ <label v-if="field.kind === 'file'" class="text-xs font-medium">{{ field.label }}</label>
118
+ <label v-else :for="fieldId(field.path)" class="text-xs font-medium">{{ field.label }}</label>
110
119
 
111
120
  <template v-if="field.kind === 'file'">
112
121
  <div
@@ -121,6 +130,7 @@ function prettyBytes(n) {
121
130
  Drop a file here or click to choose
122
131
  <input
123
132
  type="file"
133
+ :aria-label="field.label"
124
134
  class="absolute inset-0 cursor-pointer opacity-0"
125
135
  @change="onPick(field.path, $event)"
126
136
  >
@@ -133,6 +143,7 @@ function prettyBytes(n) {
133
143
  type="button"
134
144
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:bg-muted/60 hover:text-rose-600"
135
145
  title="Remove file"
146
+ :aria-label="`Remove the file for ${field.label}`"
136
147
  @click="detach(field.path)"
137
148
  >
138
149
  <Trash2 :size="13" />
@@ -145,6 +156,7 @@ function prettyBytes(n) {
145
156
 
146
157
  <template v-else>
147
158
  <Input
159
+ :id="fieldId(field.path)"
148
160
  :model-value="textValues[field.path] ?? ''"
149
161
  class="h-8 text-sm"
150
162
  :placeholder="`\${{ trigger.payload.${field.path} }}`"
@@ -163,6 +175,7 @@ function prettyBytes(n) {
163
175
  <template v-else>
164
176
  <Textarea
165
177
  v-model="model"
178
+ aria-label="Trigger payload (JSON)"
166
179
  :rows="8"
167
180
  spellcheck="false"
168
181
  class="font-mono text-xs"
@@ -181,7 +181,8 @@ function onViewMouseDown(e) {
181
181
  v-else
182
182
  ref="textareaEl"
183
183
  v-model="draft"
184
- class="nodrag nowheel h-full w-full resize-none bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400"
184
+ aria-label="Note text"
185
+ class="nodrag nowheel h-full w-full resize-none bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-stone-500/40"
185
186
  placeholder="Write a note… (# heading, **bold**, *italic*, `code`, [link](https://…), - list)"
186
187
  spellcheck="false"
187
188
  @blur="commitEdit"
@@ -0,0 +1,18 @@
1
+ import type { WorkflowTemplate } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ template: WorkflowTemplate;
4
+ /** How much room the surface has. See the comment above; no default on purpose. */
5
+ density: 'full' | 'compact';
6
+ /** Given → the card is a link there. Omitted → a button that emits `select`. */
7
+ to?: string;
8
+ /** This card is the one being acted on: it says so and cannot be pressed again. */
9
+ busy?: boolean;
10
+ /** Another card on the surface is being acted on, so this one is out of reach. */
11
+ disabled?: boolean;
12
+ };
13
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ select: () => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onSelect?: (() => any) | undefined;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ export default _default;
@@ -0,0 +1,109 @@
1
+ <script setup>
2
+ import { computed, resolveComponent } from "vue";
3
+ import { RefreshCircle as Loader2, ClockCircle as Clock, Global as Webhook, Play, Bolt as Zap } from "#solar-icons";
4
+ import { templateCategoryLabel, templatePackageLabel } from "../../data/integrationTemplates";
5
+ import TemplateFlowPreview from "./TemplateFlowPreview.vue";
6
+ const props = defineProps({
7
+ template: { type: Object, required: true },
8
+ density: { type: String, required: true },
9
+ to: { type: String, required: false },
10
+ busy: { type: Boolean, required: false },
11
+ disabled: { type: Boolean, required: false }
12
+ });
13
+ const emit = defineEmits(["select"]);
14
+ const NuxtLink = resolveComponent("NuxtLink");
15
+ const full = computed(() => props.density === "full");
16
+ const banner = computed(
17
+ () => full.value ? props.template.images.find((i) => i.category === "banner")?.src ?? null : null
18
+ );
19
+ const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
20
+ const TRIGGER_LABEL = { schedule: "Scheduled", webhook: "Webhook", manual: "Run on demand", event: "Event-driven" };
21
+ function levelDot(level) {
22
+ return level === "beginner" ? "bg-emerald-500" : level === "intermediate" ? "bg-amber-500" : "bg-rose-500";
23
+ }
24
+ const packageLabel = computed(() => templatePackageLabel(props.template));
25
+ const categoryLabel = computed(() => templateCategoryLabel(props.template.category));
26
+ const CHIP = "inline-flex items-center whitespace-nowrap rounded-full uppercase font-medium";
27
+ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-px text-[11px]");
28
+ </script>
29
+
30
+ <template>
31
+ <component
32
+ :is="to ? NuxtLink : 'button'"
33
+ v-bind="to ? { to } : { type: 'button', disabled: disabled || busy }"
34
+ class="flex flex-col overflow-hidden rounded-lg border border-black/[0.06] bg-white text-left transition hover:border-black/20 hover:shadow-sm disabled:opacity-60"
35
+ @click="to ? void 0 : emit('select')"
36
+ >
37
+ <!-- The picture: the flow this template builds, drawn from its own
38
+ definition, or the publisher's banner where one ships. -->
39
+ <div
40
+ class="relative shrink-0 border-b border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)]"
41
+ :class="full ? 'h-36 [background-size:14px_14px]' : 'h-24 [background-size:12px_12px]'"
42
+ >
43
+ <img v-if="banner" :src="banner" alt="" class="h-full w-full object-cover">
44
+ <!-- `full` keeps its chips on the picture, so the drawing starts below
45
+ them: the graph is fitted to whichever axis binds, and a flow slightly
46
+ taller in proportion than this box reaches the top edge — where the
47
+ chip is. The 28px are the chip's own 20 plus its 8 of inset, so the
48
+ two can no longer meet (PO-243).
49
+
50
+ No horizontal padding, deliberately: all three surfaces used to pass
51
+ one (`px-2` in the gallery, `px-1` in the compact places) and none of
52
+ them insetted anything. The preview's `svg` is `absolute inset-0`, so
53
+ it is laid out against its parent's *padding box* and fills it whatever
54
+ the padding is — measured identical to the picture, 421.3px wide either
55
+ way. The graph's own inset is `TemplateFlowPreview`'s `padding` prop,
56
+ 36 logical units, which is where it belongs. -->
57
+ <TemplateFlowPreview
58
+ v-else-if="template.detailLoaded"
59
+ :blob="template.blob"
60
+ :height="full ? 116 : 96"
61
+ :class="full ? 'mt-7' : ''"
62
+ />
63
+ <!-- Named, because "still on its way" and "this template draws nothing"
64
+ are the same sight otherwise — and a spinner nobody can address is a
65
+ promise no test can hold to. -->
66
+ <div
67
+ v-else
68
+ role="status"
69
+ aria-label="Loading the flow"
70
+ class="flex h-full items-center justify-center"
71
+ >
72
+ <Loader2 :size="full ? 16 : 14" class="animate-spin text-muted-foreground/50" />
73
+ </div>
74
+
75
+ <div v-if="full" class="absolute left-2 top-2 flex items-center gap-1.5">
76
+ <span v-if="categoryLabel" :class="[CHIP, chipSize, 'bg-slate-100 text-slate-700']">{{ categoryLabel }}</span>
77
+ <span v-if="packageLabel" :class="[CHIP, chipSize, 'bg-violet-100 text-violet-700']">{{ packageLabel }}</span>
78
+ </div>
79
+
80
+ <Loader2 v-if="busy" :size="13" class="absolute right-1.5 top-1.5 animate-spin text-muted-foreground" />
81
+ </div>
82
+
83
+ <div class="flex min-h-0 flex-1 flex-col" :class="full ? 'p-4' : 'p-3'">
84
+ <div class="flex items-center gap-2">
85
+ <div class="min-w-0 flex-1 truncate text-sm font-semibold text-foreground">{{ template.name }}</div>
86
+ <!-- Compact has no room on the picture, so the category rides the name. -->
87
+ <span v-if="!full && categoryLabel" :class="[CHIP, chipSize, 'bg-slate-100 text-slate-700']">{{ categoryLabel }}</span>
88
+ </div>
89
+ <p class="mt-1 flex-1 text-sm text-muted-foreground line-clamp-2">{{ template.description }}</p>
90
+
91
+ <div v-if="full" class="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
92
+ <span
93
+ v-for="t in template.triggerTypes"
94
+ :key="t"
95
+ class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase"
96
+ >
97
+ <component :is="TRIGGER_ICON[t] ?? Zap" :size="10" />
98
+ {{ TRIGGER_LABEL[t] ?? t }}
99
+ </span>
100
+ <span class="flex-1" />
101
+ <span class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase">
102
+ <span class="size-1.5 rounded-full" :class="levelDot(template.level)" />
103
+ {{ template.level }}
104
+ </span>
105
+ <span v-if="template.publisher.kind !== 'revenexx'" class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase">{{ template.publisher.name }}</span>
106
+ </div>
107
+ </div>
108
+ </component>
109
+ </template>
@@ -0,0 +1,18 @@
1
+ import type { WorkflowTemplate } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ template: WorkflowTemplate;
4
+ /** How much room the surface has. See the comment above; no default on purpose. */
5
+ density: 'full' | 'compact';
6
+ /** Given → the card is a link there. Omitted → a button that emits `select`. */
7
+ to?: string;
8
+ /** This card is the one being acted on: it says so and cannot be pressed again. */
9
+ busy?: boolean;
10
+ /** Another card on the surface is being acted on, so this one is out of reach. */
11
+ disabled?: boolean;
12
+ };
13
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ select: () => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onSelect?: (() => any) | undefined;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ export default _default;
@@ -11,9 +11,9 @@ type __VLS_Props = {
11
11
  */
12
12
  showErrorBranches?: boolean;
13
13
  };
14
- declare var __VLS_12: {};
14
+ declare var __VLS_8: {};
15
15
  type __VLS_Slots = {} & {
16
- empty?: (props: typeof __VLS_12) => any;
16
+ empty?: (props: typeof __VLS_8) => any;
17
17
  };
18
18
  declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
19
19
  height: number;
@@ -3,7 +3,8 @@ import { computed } from "vue";
3
3
  import * as Lucide from "lucide-vue-next";
4
4
  import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
5
5
  import { resolveLocalized } from "../../utils/localize";
6
- import IntegrationsNodeLogo from "./NodeLogo.vue";
6
+ import { describeCron } from "../../utils/schedule";
7
+ import IntegrationsNodeMark from "./NodeMark.vue";
7
8
  const props = defineProps({
8
9
  blob: { type: [Object, null], required: true },
9
10
  height: { type: Number, required: false, default: 144 },
@@ -23,7 +24,7 @@ const boxes = computed(() => {
23
24
  triggers.forEach((t, i) => {
24
25
  const pos = blob.triggerPositions?.[t.id] ?? { x: 40, y: 40 + i * 160 };
25
26
  const template = TRIGGER_TEMPLATES.find((tt) => tt.type === t.type);
26
- const subtitle = t.type === "schedule" && t.config?.cron ? String(t.config.cron) : t.type === "webhook" && t.config?.path ? String(t.config.path) : resolveLocalized(template?.description ?? "");
27
+ const subtitle = t.type === "schedule" && t.config?.cron ? describeCron(String(t.config.cron)) ?? String(t.config.cron) : t.type === "webhook" && t.config?.path ? String(t.config.path) : resolveLocalized(template?.description ?? "");
27
28
  out.push({
28
29
  key: `t-${t.id}`,
29
30
  kind: "trigger",
@@ -59,9 +60,6 @@ const boxes = computed(() => {
59
60
  });
60
61
  return out;
61
62
  });
62
- function hasLogo(m) {
63
- return (m?.images ?? []).some((i) => i.category === "logo");
64
- }
65
63
  function prettify(slug) {
66
64
  const tail = slug.includes(":") ? slug.split(":").pop() : slug;
67
65
  return tail.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
@@ -100,8 +98,8 @@ const edgePaths = computed(() => {
100
98
  });
101
99
  const EDGE_STROKE = {
102
100
  default: "#94a3b8",
103
- trigger: "#10b981",
104
- error: "#f43f5e"
101
+ trigger: "#94a3b8",
102
+ error: "#cbd5e1"
105
103
  };
106
104
  const bounds = computed(() => {
107
105
  if (!boxes.value.length) return { x: 0, y: 0, w: 1, h: 1 };
@@ -182,10 +180,9 @@ const bounds = computed(() => {
182
180
  >
183
181
  <span
184
182
  class="flex size-8 shrink-0 items-center justify-center rounded-md"
185
- :class="hasLogo(b.manifest) ? 'bg-white ring-1 ring-black/[0.06]' : b.categoryColor"
183
+ :class="b.categoryColor"
186
184
  >
187
- <IntegrationsNodeLogo v-if="hasLogo(b.manifest)" :manifest="b.manifest" :size="18" />
188
- <component :is="b.icon" v-else :size="15" :stroke-width="2.25" />
185
+ <IntegrationsNodeMark :manifest="b.manifest" :size="18" />
189
186
  </span>
190
187
  <div class="min-w-0 flex-1">
191
188
  <div class="truncate text-sm font-semibold leading-tight text-foreground">{{ b.label }}</div>
@@ -11,9 +11,9 @@ type __VLS_Props = {
11
11
  */
12
12
  showErrorBranches?: boolean;
13
13
  };
14
- declare var __VLS_12: {};
14
+ declare var __VLS_8: {};
15
15
  type __VLS_Slots = {} & {
16
- empty?: (props: typeof __VLS_12) => any;
16
+ empty?: (props: typeof __VLS_8) => any;
17
17
  };
18
18
  declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
19
19
  height: number;
@@ -20,9 +20,10 @@ import {
20
20
  } from "@revenexx/studio";
21
21
  import { toast } from "vue-sonner";
22
22
  import { TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
23
- import TemplateFlowPreview from "./TemplateFlowPreview.vue";
23
+ import TemplateCard from "./TemplateCard.vue";
24
24
  import WorkflowIconPicker from "./WorkflowIconPicker.vue";
25
25
  import WorkflowTagsInput from "./WorkflowTagsInput.vue";
26
+ import { resolveLocalized } from "../../utils/localize";
26
27
  const open = defineModel("open", { type: Boolean, ...{ default: false } });
27
28
  const router = useRouter();
28
29
  const { create, workflows } = useIntegrationsWorkflows();
@@ -32,10 +33,6 @@ const modeTabs = [
32
33
  { key: "blank", label: "Start blank" },
33
34
  { key: "template", label: "Start from template" }
34
35
  ];
35
- function loc(v) {
36
- if (!v) return "";
37
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
38
- }
39
36
  function iconFor(name2) {
40
37
  if (!name2) return Lucide.Box;
41
38
  return Lucide[name2] || Lucide.Box;
@@ -165,26 +162,26 @@ async function useTemplate(slug) {
165
162
  </div>
166
163
 
167
164
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
168
- <Label>Tags</Label>
169
- <WorkflowTagsInput v-model="tags" :suggestions="tagSuggestions" />
165
+ <Label for="wf-tags">Tags</Label>
166
+ <WorkflowTagsInput v-model="tags" input-id="wf-tags" :suggestions="tagSuggestions" />
170
167
  </div>
171
168
 
172
169
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
173
- <Label>Icon</Label>
174
- <WorkflowIconPicker v-model="icon" />
170
+ <Label id="wf-icon-label">Icon</Label>
171
+ <WorkflowIconPicker v-model="icon" trigger-labelledby="wf-icon-label" />
175
172
  </div>
176
173
 
177
174
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
178
- <Label>Trigger</Label>
175
+ <Label for="wf-trigger">Trigger</Label>
179
176
  <Select v-model="triggerType">
180
- <SelectTrigger class="w-full bg-white">
177
+ <SelectTrigger id="wf-trigger" class="w-full bg-white">
181
178
  <SelectValue>
182
179
  <span class="flex min-w-0 items-center gap-2">
183
180
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
184
181
  <component :is="iconFor(selectedTrigger?.icon)" :size="13" />
185
182
  </span>
186
- <span class="shrink-0 text-sm font-medium">{{ loc(selectedTrigger?.name) }}</span>
187
- <span class="truncate text-sm text-muted-foreground">{{ loc(selectedTrigger?.description) }}</span>
183
+ <span class="shrink-0 text-sm font-medium">{{ resolveLocalized(selectedTrigger?.name) }}</span>
184
+ <span class="truncate text-sm text-muted-foreground">{{ resolveLocalized(selectedTrigger?.description) }}</span>
188
185
  </span>
189
186
  </SelectValue>
190
187
  </SelectTrigger>
@@ -195,8 +192,8 @@ async function useTemplate(slug) {
195
192
  <component :is="iconFor(t.icon)" :size="14" />
196
193
  </span>
197
194
  <span class="min-w-0">
198
- <span class="block text-sm font-medium">{{ loc(t.name) }}</span>
199
- <span class="block text-sm text-muted-foreground">{{ loc(t.description) }}</span>
195
+ <span class="block text-sm font-medium">{{ resolveLocalized(t.name) }}</span>
196
+ <span class="block text-sm text-muted-foreground">{{ resolveLocalized(t.description) }}</span>
200
197
  </span>
201
198
  </span>
202
199
  </SelectItem>
@@ -205,9 +202,9 @@ async function useTemplate(slug) {
205
202
  </div>
206
203
 
207
204
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
208
- <Label>Execution mode</Label>
205
+ <Label for="wf-exec-mode">Execution mode</Label>
209
206
  <Select v-model="executionMode">
210
- <SelectTrigger class="w-full bg-white">
207
+ <SelectTrigger id="wf-exec-mode" class="w-full bg-white">
211
208
  <SelectValue>
212
209
  <span class="flex min-w-0 items-center gap-2">
213
210
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
@@ -256,6 +253,7 @@ async function useTemplate(slug) {
256
253
  <input
257
254
  v-model="templateSearch"
258
255
  type="text"
256
+ aria-label="Search templates"
259
257
  placeholder="Search templates…"
260
258
  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"
261
259
  >
@@ -282,29 +280,15 @@ async function useTemplate(slug) {
282
280
  </p>
283
281
 
284
282
  <div v-else class="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
285
- <button
283
+ <TemplateCard
286
284
  v-for="tpl in filteredTemplates"
287
285
  :key="tpl.slug"
288
- type="button"
289
- class="flex flex-col overflow-hidden rounded-lg border border-black/[0.06] bg-white text-left transition hover:border-black/20 hover:shadow-sm disabled:opacity-60"
286
+ :template="tpl"
287
+ density="compact"
288
+ :busy="instantiatingSlug === tpl.slug"
290
289
  :disabled="!!instantiatingSlug"
291
- @click="useTemplate(tpl.slug)"
292
- >
293
- <div class="relative h-24 shrink-0 border-b border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:12px_12px]">
294
- <TemplateFlowPreview v-if="tpl.detailLoaded" :blob="tpl.blob" :height="96" class="px-1" />
295
- <div v-else class="flex h-full items-center justify-center">
296
- <Loader2 :size="14" class="animate-spin text-muted-foreground/40" />
297
- </div>
298
- <span class="absolute left-1.5 top-1.5 inline-flex items-center rounded-full bg-slate-100 px-1.5 py-px uppercase text-[11px] font-medium text-slate-700">
299
- {{ tpl.category.charAt(0).toUpperCase() + tpl.category.slice(1) }}
300
- </span>
301
- <Loader2 v-if="instantiatingSlug === tpl.slug" :size="13" class="absolute right-1.5 top-1.5 animate-spin text-muted-foreground" />
302
- </div>
303
- <div class="flex min-h-0 flex-1 flex-col p-3">
304
- <div class="truncate text-sm font-medium">{{ tpl.name }}</div>
305
- <p class="mt-0.5 line-clamp-2 text-sm text-muted-foreground">{{ tpl.description }}</p>
306
- </div>
307
- </button>
290
+ @select="useTemplate(tpl.slug)"
291
+ />
308
292
  </div>
309
293
 
310
294
  <div class="flex items-center justify-between pt-1">
@@ -4,10 +4,14 @@ import { type EdgeProps } from '@vue-flow/core';
4
4
  *
5
5
  * - hard right-angle routing (`getSmoothStepPath`, radius 0)
6
6
  * - arrow-head pointing into the target
7
- * - color encodes the *source port kind* — branch outputs (`true`/`false`,
8
- * switch cases) get a soft color, error outputs get rose, defaults stay
9
- * slate
10
- * - optional center label (used by the canvas to print branch names)
7
+ * - colour encodes IMPORTANCE, not meaning (PO-197): the main flow leads in
8
+ * slate, the error/exception path recedes in a paler slate. The semantic
9
+ * colour (green = normal/active output incl. branch & switch cases, rose =
10
+ * error) lives on the source PORT DOT — a small, localised legend — so the
11
+ * connective edges stay calm.
12
+ * - optional center label (used by the canvas to print branch names) — branch
13
+ * routes only, never the error route (promised: `specs/editor-output-paths.md`,
14
+ * reasoning: `docs/editor-path-colors.md`)
11
15
  * - n8n-style midpoint tools on hover/selection: [+] splices a node into
12
16
  * the connection (editor-provided action), [trash] deletes the edge
13
17
  *
@@ -19,6 +23,16 @@ interface Data {
19
23
  kind: EdgeKind;
20
24
  /** Optional human label shown mid-edge (e.g. "true", "cases:paid"). */
21
25
  label?: string;
26
+ /**
27
+ * Fan-out lane: this edge's rank among its source node's ACTUAL outgoing
28
+ * edges (ordered left-to-right by output handle) and how many there are.
29
+ * Siblings leaving a multi-output node (Switch, out/error) would otherwise
30
+ * bend on the same horizontal line and overlap — the renderer staggers the
31
+ * bend height by lane so the horizontal runs sit on distinct levels. A lone
32
+ * edge has `laneCount` 1 and gets no stagger.
33
+ */
34
+ laneIndex?: number;
35
+ laneCount?: number;
22
36
  /**
23
37
  * Run-replay overlay (WorkflowRunReplayPanel): the edge is on the path the
24
38
  * run actually took — drawn blue and thicker. `ahead` marks the part of the
@@ -41,6 +41,19 @@ const props = defineProps({
41
41
  });
42
42
  const flow = useVueFlow();
43
43
  const edgeActions = inject("workflow-edge-actions", null);
44
+ const centerY = computed(() => {
45
+ const n = props.data?.laneCount ?? 1;
46
+ const i = props.data?.laneIndex ?? 0;
47
+ if (n <= 1) return void 0;
48
+ const mid = (props.sourceY + props.targetY) / 2;
49
+ const STEP = 11;
50
+ const span = Math.abs(props.targetY - props.sourceY);
51
+ const dist = Math.abs(i - (n - 1) / 2);
52
+ const tie = i / Math.max(1, n - 1) * (STEP / 3);
53
+ const lift = Math.min(dist * STEP + tie, Math.max(0, span / 2 - span / 6));
54
+ const towardSource = Math.sign(props.sourceY - props.targetY) || -1;
55
+ return mid + towardSource * lift;
56
+ });
44
57
  const path = computed(() => getSmoothStepPath({
45
58
  sourceX: props.sourceX,
46
59
  sourceY: props.sourceY,
@@ -48,15 +61,18 @@ const path = computed(() => getSmoothStepPath({
48
61
  targetX: props.targetX,
49
62
  targetY: props.targetY,
50
63
  targetPosition: props.targetPosition,
51
- borderRadius: 0
64
+ borderRadius: 0,
65
+ centerY: centerY.value
52
66
  }));
67
+ const NEUTRAL = { stroke: "#94a3b8", label: "#475569", bg: "#f1f5f9" };
68
+ const MUTED = { stroke: "#cbd5e1", label: "#94a3b8", bg: "#f8fafc" };
53
69
  const palette = {
54
- default: { stroke: "#94a3b8", label: "#475569", bg: "#f1f5f9" },
55
- success: { stroke: "#10b981", label: "#047857", bg: "#ecfdf5" },
56
- error: { stroke: "#f43f5e", label: "#be123c", bg: "#fff1f2" },
57
- "branch-true": { stroke: "#10b981", label: "#047857", bg: "#ecfdf5" },
58
- "branch-false": { stroke: "#f59e0b", label: "#b45309", bg: "#fffbeb" },
59
- branch: { stroke: "#8b5cf6", label: "#6d28d9", bg: "#f5f3ff" }
70
+ default: NEUTRAL,
71
+ success: NEUTRAL,
72
+ error: MUTED,
73
+ "branch-true": NEUTRAL,
74
+ "branch-false": NEUTRAL,
75
+ branch: NEUTRAL
60
76
  };
61
77
  const color = computed(() => palette[props.data?.kind ?? "default"] ?? palette.default);
62
78
  const isSelected = computed(() => props.selected);
@@ -4,10 +4,14 @@ import { type EdgeProps } from '@vue-flow/core';
4
4
  *
5
5
  * - hard right-angle routing (`getSmoothStepPath`, radius 0)
6
6
  * - arrow-head pointing into the target
7
- * - color encodes the *source port kind* — branch outputs (`true`/`false`,
8
- * switch cases) get a soft color, error outputs get rose, defaults stay
9
- * slate
10
- * - optional center label (used by the canvas to print branch names)
7
+ * - colour encodes IMPORTANCE, not meaning (PO-197): the main flow leads in
8
+ * slate, the error/exception path recedes in a paler slate. The semantic
9
+ * colour (green = normal/active output incl. branch & switch cases, rose =
10
+ * error) lives on the source PORT DOT — a small, localised legend — so the
11
+ * connective edges stay calm.
12
+ * - optional center label (used by the canvas to print branch names) — branch
13
+ * routes only, never the error route (promised: `specs/editor-output-paths.md`,
14
+ * reasoning: `docs/editor-path-colors.md`)
11
15
  * - n8n-style midpoint tools on hover/selection: [+] splices a node into
12
16
  * the connection (editor-provided action), [trash] deletes the edge
13
17
  *
@@ -19,6 +23,16 @@ interface Data {
19
23
  kind: EdgeKind;
20
24
  /** Optional human label shown mid-edge (e.g. "true", "cases:paid"). */
21
25
  label?: string;
26
+ /**
27
+ * Fan-out lane: this edge's rank among its source node's ACTUAL outgoing
28
+ * edges (ordered left-to-right by output handle) and how many there are.
29
+ * Siblings leaving a multi-output node (Switch, out/error) would otherwise
30
+ * bend on the same horizontal line and overlap — the renderer staggers the
31
+ * bend height by lane so the horizontal runs sit on distinct levels. A lone
32
+ * edge has `laneCount` 1 and gets no stagger.
33
+ */
34
+ laneIndex?: number;
35
+ laneCount?: number;
22
36
  /**
23
37
  * Run-replay overlay (WorkflowRunReplayPanel): the edge is on the path the
24
38
  * run actually took — drawn blue and thicker. `ahead` marks the part of the
@@ -6,8 +6,29 @@ type __VLS_Props = {
6
6
  workflow: Workflow;
7
7
  /** Busy flag for the control-pane Play button (a run is being triggered). */
8
8
  runBusy?: boolean;
9
+ /** The run is queued behind a bundle build — busy, but worth saying why. */
10
+ runWaiting?: boolean;
11
+ /**
12
+ * One of the page's own lightboxes is open above the editor (settings,
13
+ * duplicate, run payload, the sync response, the deactivation prompt). The
14
+ * canvas stands its keyboard shortcuts down while one is up — see
15
+ * `canvasHasKeyboard` below. A new page-level dialog belongs in here too.
16
+ */
17
+ overlayOpen?: boolean;
9
18
  };
10
- declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ /**
20
+ * Show a node the user was sent to from outside the canvas — the validation
21
+ * banner's per-node line (PO-209). Centres the card and opens its settings,
22
+ * which is where the fields at fault are.
23
+ *
24
+ * A node inside a collapsed group has no card on the canvas: centring would pan
25
+ * to empty space, so it only opens the settings. The reverse can't happen — an
26
+ * id that is on the canvas is in the blob by construction.
27
+ */
28
+ declare function revealNode(id: string): void;
29
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {
30
+ revealNode: typeof revealNode;
31
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
32
  change: (blob: WorkflowBlob) => any;
12
33
  run: () => any;
13
34
  toggleActive: () => any;