@revenexx/studio-integrations 0.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 (106) hide show
  1. package/README.md +51 -0
  2. package/dist/module.d.mts +6 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +45 -0
  5. package/dist/runtime/components/Integrations/Layout.d.vue.ts +12 -0
  6. package/dist/runtime/components/Integrations/Layout.vue +20 -0
  7. package/dist/runtime/components/Integrations/Layout.vue.d.ts +12 -0
  8. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +16 -0
  9. package/dist/runtime/components/Integrations/NodeInspector.vue +450 -0
  10. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +16 -0
  11. package/dist/runtime/components/Integrations/NodePalette.d.vue.ts +2 -0
  12. package/dist/runtime/components/Integrations/NodePalette.vue +140 -0
  13. package/dist/runtime/components/Integrations/NodePalette.vue.d.ts +2 -0
  14. package/dist/runtime/components/Integrations/RunDetailDialog.d.vue.ts +11 -0
  15. package/dist/runtime/components/Integrations/RunDetailDialog.vue +169 -0
  16. package/dist/runtime/components/Integrations/RunDetailDialog.vue.d.ts +11 -0
  17. package/dist/runtime/components/Integrations/Sidebar.d.vue.ts +2 -0
  18. package/dist/runtime/components/Integrations/Sidebar.vue +32 -0
  19. package/dist/runtime/components/Integrations/Sidebar.vue.d.ts +2 -0
  20. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +23 -0
  21. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +86 -0
  22. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +23 -0
  23. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +14 -0
  24. package/dist/runtime/components/Integrations/WorkflowEditor.vue +419 -0
  25. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +14 -0
  26. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +11 -0
  27. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +200 -0
  28. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +11 -0
  29. package/dist/runtime/components/Integrations/WorkflowTriggerView.d.vue.ts +8 -0
  30. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +82 -0
  31. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue.d.ts +8 -0
  32. package/dist/runtime/components/Integrations/WorkflowsTable.d.vue.ts +6 -0
  33. package/dist/runtime/components/Integrations/WorkflowsTable.vue +268 -0
  34. package/dist/runtime/components/Integrations/WorkflowsTable.vue.d.ts +6 -0
  35. package/dist/runtime/components/Integrations/workflowLayout.d.ts +17 -0
  36. package/dist/runtime/components/Integrations/workflowLayout.js +29 -0
  37. package/dist/runtime/composables/integrationsApi.d.ts +13 -0
  38. package/dist/runtime/composables/integrationsApi.js +55 -0
  39. package/dist/runtime/composables/useCredentialTypes.d.ts +19 -0
  40. package/dist/runtime/composables/useCredentialTypes.js +47 -0
  41. package/dist/runtime/composables/useIntegrationsAudit.d.ts +26 -0
  42. package/dist/runtime/composables/useIntegrationsAudit.js +39 -0
  43. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +43 -0
  44. package/dist/runtime/composables/useIntegrationsCredentials.js +98 -0
  45. package/dist/runtime/composables/useIntegrationsFocus.d.ts +16 -0
  46. package/dist/runtime/composables/useIntegrationsFocus.js +17 -0
  47. package/dist/runtime/composables/useIntegrationsNodes.d.ts +6 -0
  48. package/dist/runtime/composables/useIntegrationsNodes.js +32 -0
  49. package/dist/runtime/composables/useIntegrationsRuns.d.ts +31 -0
  50. package/dist/runtime/composables/useIntegrationsRuns.js +109 -0
  51. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +14 -0
  52. package/dist/runtime/composables/useIntegrationsSchedules.js +32 -0
  53. package/dist/runtime/composables/useIntegrationsSecrets.d.ts +27 -0
  54. package/dist/runtime/composables/useIntegrationsSecrets.js +76 -0
  55. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +19 -0
  56. package/dist/runtime/composables/useIntegrationsTemplates.js +141 -0
  57. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +39 -0
  58. package/dist/runtime/composables/useIntegrationsTriggers.js +39 -0
  59. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +28 -0
  60. package/dist/runtime/composables/useIntegrationsWorkflows.js +152 -0
  61. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  62. package/dist/runtime/data/integrationNodeCatalog.js +67 -0
  63. package/dist/runtime/data/integrationRuns.d.ts +7 -0
  64. package/dist/runtime/data/integrationRuns.js +109 -0
  65. package/dist/runtime/data/integrationTemplates.d.ts +11 -0
  66. package/dist/runtime/data/integrationTemplates.js +9 -0
  67. package/dist/runtime/data/integrationWorkflowSamples.d.ts +18 -0
  68. package/dist/runtime/data/integrationWorkflowSamples.js +318 -0
  69. package/dist/runtime/pages/integrations/audit.d.vue.ts +2 -0
  70. package/dist/runtime/pages/integrations/audit.vue +156 -0
  71. package/dist/runtime/pages/integrations/audit.vue.d.ts +2 -0
  72. package/dist/runtime/pages/integrations/credentials/oauth-complete.d.vue.ts +2 -0
  73. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +28 -0
  74. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue.d.ts +2 -0
  75. package/dist/runtime/pages/integrations/credentials.d.vue.ts +2 -0
  76. package/dist/runtime/pages/integrations/credentials.vue +403 -0
  77. package/dist/runtime/pages/integrations/credentials.vue.d.ts +2 -0
  78. package/dist/runtime/pages/integrations/index.d.vue.ts +2 -0
  79. package/dist/runtime/pages/integrations/index.vue +226 -0
  80. package/dist/runtime/pages/integrations/index.vue.d.ts +2 -0
  81. package/dist/runtime/pages/integrations/runs.d.vue.ts +2 -0
  82. package/dist/runtime/pages/integrations/runs.vue +100 -0
  83. package/dist/runtime/pages/integrations/runs.vue.d.ts +2 -0
  84. package/dist/runtime/pages/integrations/schedules.d.vue.ts +2 -0
  85. package/dist/runtime/pages/integrations/schedules.vue +111 -0
  86. package/dist/runtime/pages/integrations/schedules.vue.d.ts +2 -0
  87. package/dist/runtime/pages/integrations/secrets.d.vue.ts +2 -0
  88. package/dist/runtime/pages/integrations/secrets.vue +248 -0
  89. package/dist/runtime/pages/integrations/secrets.vue.d.ts +2 -0
  90. package/dist/runtime/pages/integrations/workflows/[id].d.vue.ts +2 -0
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +326 -0
  92. package/dist/runtime/pages/integrations/workflows/[id].vue.d.ts +2 -0
  93. package/dist/runtime/pages/integrations/workflows/index.d.vue.ts +2 -0
  94. package/dist/runtime/pages/integrations/workflows/index.vue +31 -0
  95. package/dist/runtime/pages/integrations/workflows/index.vue.d.ts +2 -0
  96. package/dist/runtime/pages/integrations/workflows/new.d.vue.ts +2 -0
  97. package/dist/runtime/pages/integrations/workflows/new.vue +153 -0
  98. package/dist/runtime/pages/integrations/workflows/new.vue.d.ts +2 -0
  99. package/dist/runtime/types/integrations.d.ts +427 -0
  100. package/dist/runtime/types/integrations.js +0 -0
  101. package/dist/runtime/utils/localize.d.ts +9 -0
  102. package/dist/runtime/utils/localize.js +21 -0
  103. package/dist/runtime/utils/statusStyle.d.ts +15 -0
  104. package/dist/runtime/utils/statusStyle.js +78 -0
  105. package/dist/types.d.mts +9 -0
  106. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @revenexx/studio-integrations
2
+
3
+ The revenexx **Integration Studio** (core: Dashboard, Workflows + node editor,
4
+ Runs, Schedules, Credentials, Secrets, Audit) as a distributable **Nuxt module**.
5
+ Registers its `/integrations/*` pages, components and composables. Data/auth flow
6
+ through the `usePlatformAuth` / `usePlatformTenant` seam (from
7
+ `@revenexx/studio-shared`).
8
+
9
+ ## Install
10
+
11
+ Public on npm — no registry config or token needed:
12
+
13
+ ```bash
14
+ pnpm add @revenexx/studio-integrations @revenexx/studio-shared @revenexx/studio
15
+ ```
16
+
17
+ ## Use
18
+
19
+ ```ts
20
+ // nuxt.config.ts
21
+ export default defineNuxtConfig({
22
+ extends: ['@revenexx/studio-shared'], // shell primitives + platform provider
23
+ modules: ['@solar-icons/nuxt', '@revenexx/studio-integrations'],
24
+ })
25
+ ```
26
+
27
+ Point it at your Integrations backend via `NUXT_PUBLIC_INTEGRATIONS_API` and
28
+ supply a token/tenant through the platform provider (override
29
+ `usePlatformAuth` / `usePlatformTenant`, or the standalone defaults read
30
+ `NUXT_PUBLIC_DEV_TOKEN` / `NUXT_PUBLIC_DEV_TENANT`).
31
+
32
+ ## ⚠️ Tailwind: you MUST scan this package
33
+
34
+ The module ships its runtime as source-ish `.vue`; Tailwind generates the
35
+ classes from **your** build. Scan both this module and `studio-shared`, or
36
+ component styling silently breaks:
37
+
38
+ ```css
39
+ @import "tailwindcss";
40
+ @import "@revenexx/studio/tokens";
41
+ @import "@revenexx/studio/fonts";
42
+ @source "../node_modules/@revenexx/studio-shared/components";
43
+ @source "../node_modules/@revenexx/studio-integrations/dist/runtime";
44
+ ```
45
+
46
+ Adjust the relative paths to reach `node_modules` from your CSS file.
47
+
48
+ ## Peers
49
+ `@revenexx/studio`, `@revenexx/studio-shared`, `nuxt` ^4, `vue` ^3.5,
50
+ `@vue-flow/*` + `@dagrejs/dagre` (workflow editor), `lucide-vue-next`; register
51
+ `@solar-icons/nuxt` in the host.
@@ -0,0 +1,6 @@
1
+ import * as nuxt_schema from 'nuxt/schema';
2
+ export * from '../dist/runtime/types/integrations.js';
3
+
4
+ declare const _default: nuxt_schema.NuxtModule<nuxt_schema.ModuleOptions, nuxt_schema.ModuleOptions, false>;
5
+
6
+ export { _default as default };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@revenexx/studio-integrations",
3
+ "configKey": "studioIntegrations",
4
+ "version": "0.1.0",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "unknown"
8
+ }
9
+ }
@@ -0,0 +1,45 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir, extendPages } from '@nuxt/kit';
4
+
5
+ function toSegment(name) {
6
+ if (name.startsWith("[...")) return `:${name.slice(4, -1)}(.*)*`;
7
+ if (name.startsWith("[")) return `:${name.slice(1, -1)}`;
8
+ return name;
9
+ }
10
+ function scanRoutes(dir, baseUrl, baseName) {
11
+ const routes = [];
12
+ for (const entry of readdirSync(dir)) {
13
+ const full = join(dir, entry);
14
+ if (statSync(full).isDirectory()) {
15
+ routes.push(...scanRoutes(full, `${baseUrl}/${toSegment(entry)}`, `${baseName}-${entry}`));
16
+ } else if (entry.endsWith(".vue")) {
17
+ const base = entry.slice(0, -4);
18
+ const path = base === "index" ? baseUrl : `${baseUrl}/${toSegment(base)}`;
19
+ const name = `${baseName}-${base}`.replace(/[^a-z0-9]+/gi, "-").replace(/-+$/g, "");
20
+ routes.push({ name, path: path || "/", file: full });
21
+ }
22
+ }
23
+ return routes;
24
+ }
25
+ const module$1 = defineNuxtModule({
26
+ meta: {
27
+ name: "@revenexx/studio-integrations",
28
+ configKey: "studioIntegrations"
29
+ },
30
+ setup(_options, _nuxt) {
31
+ const { resolve } = createResolver(import.meta.url);
32
+ const runtime = resolve("./runtime");
33
+ addComponentsDir({ path: join(runtime, "components"), pathPrefix: true });
34
+ addImportsDir(join(runtime, "composables"));
35
+ extendPages((pages) => {
36
+ const pagesDir = join(runtime, "pages", "integrations");
37
+ if (!existsSync(pagesDir)) return;
38
+ for (const route of scanRoutes(pagesDir, "/integrations", "integrations")) {
39
+ pages.push(route);
40
+ }
41
+ });
42
+ }
43
+ });
44
+
45
+ export { module$1 as default };
@@ -0,0 +1,12 @@
1
+ declare var __VLS_4: {};
2
+ type __VLS_Slots = {} & {
3
+ default?: (props: typeof __VLS_4) => any;
4
+ };
5
+ declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
+ export default _default;
8
+ type __VLS_WithSlots<T, S> = T & {
9
+ new (): {
10
+ $slots: S;
11
+ };
12
+ };
@@ -0,0 +1,20 @@
1
+ <script setup>
2
+ import Sidebar from "./Sidebar.vue";
3
+ const { focused } = useIntegrationsFocus();
4
+ </script>
5
+
6
+ <template>
7
+ <div class="flex h-[calc(100%-0.75rem)] mb-3 mr-3 ml-1 overflow-hidden rounded-lg border border-gray-400/10 outline-1 outline-white/30">
8
+ <div
9
+ class="shrink-0 overflow-hidden transition-[width] duration-200 ease-out border-r border-black/[0.06] bg-white"
10
+ :class="focused ? 'w-0 !border-r-0' : 'w-64'"
11
+ >
12
+ <div class="h-full w-64 overflow-y-auto">
13
+ <Sidebar />
14
+ </div>
15
+ </div>
16
+ <div class="flex-1 min-w-0 overflow-y-auto bg-white/40 backdrop-blur-sm">
17
+ <slot />
18
+ </div>
19
+ </div>
20
+ </template>
@@ -0,0 +1,12 @@
1
+ declare var __VLS_4: {};
2
+ type __VLS_Slots = {} & {
3
+ default?: (props: typeof __VLS_4) => any;
4
+ };
5
+ declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
+ export default _default;
8
+ type __VLS_WithSlots<T, S> = T & {
9
+ new (): {
10
+ $slots: S;
11
+ };
12
+ };
@@ -0,0 +1,16 @@
1
+ import type { WorkflowNode, WorkflowTrigger } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ /** Either a workflow node OR a trigger. Distinguished by presence of `nodeSlug`. */
4
+ selection: WorkflowNode | WorkflowTrigger | null;
5
+ secretKeys: string[];
6
+ };
7
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ update: (next: WorkflowTrigger | WorkflowNode) => any;
9
+ remove: () => any;
10
+ close: () => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onUpdate?: ((next: WorkflowTrigger | WorkflowNode) => any) | undefined;
13
+ onRemove?: (() => any) | undefined;
14
+ onClose?: (() => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ export default _default;
@@ -0,0 +1,450 @@
1
+ <script setup>
2
+ import { computed, onMounted } from "vue";
3
+ import { AddCircle as Plus, TrashBinMinimalistic as Trash2, CloseCircle as X } from "#solar-icons";
4
+ import {
5
+ Button,
6
+ Input,
7
+ Label,
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ Switch,
14
+ Textarea
15
+ } from "@revenexx/studio";
16
+ import { findManifest } from "../../data/integrationNodeCatalog";
17
+ const props = defineProps({
18
+ selection: { type: [Object, null], required: true },
19
+ secretKeys: { type: Array, required: true }
20
+ });
21
+ const emit = defineEmits(["update", "close", "remove"]);
22
+ const isNode = computed(() => !!props.selection && "nodeSlug" in props.selection);
23
+ const { credentials, load: loadCredentials } = useIntegrationsCredentials();
24
+ onMounted(() => {
25
+ void loadCredentials();
26
+ });
27
+ function credsOfType(slug) {
28
+ if (!slug) return credentials.value;
29
+ return credentials.value.filter((c) => c.credential_type_slug === slug);
30
+ }
31
+ function credNeedsReauth(id) {
32
+ return credentials.value.some((c) => c.id === id && c.status === "needs_reauth");
33
+ }
34
+ const exprHint = "${{ \u2026 }} ok";
35
+ const manifest = computed(() => {
36
+ if (!isNode.value || !props.selection) return null;
37
+ const wn = props.selection;
38
+ return findManifest(wn.nodeSlug, wn.nodeVersion);
39
+ });
40
+ function labelText(v) {
41
+ if (!v) return "";
42
+ return typeof v === "string" ? v : Object.values(v)[0] ?? "";
43
+ }
44
+ function readConfig(key) {
45
+ if (!isNode.value || !props.selection) return void 0;
46
+ return props.selection.config?.[key];
47
+ }
48
+ function writeConfig(key, value) {
49
+ if (!isNode.value || !props.selection) return;
50
+ const wn = props.selection;
51
+ emit("update", { ...wn, config: { ...wn.config, [key]: value } });
52
+ }
53
+ function writeName(name) {
54
+ if (!props.selection) return;
55
+ emit("update", { ...props.selection, name });
56
+ }
57
+ function readTriggerConfig(key) {
58
+ if (isNode.value || !props.selection) return void 0;
59
+ return props.selection.config[key];
60
+ }
61
+ function writeTriggerConfig(key, value) {
62
+ if (isNode.value || !props.selection) return;
63
+ const wt = props.selection;
64
+ emit("update", { ...wt, config: { ...wt.config, [key]: value } });
65
+ }
66
+ function readArray(key) {
67
+ const v = readConfig(key);
68
+ return Array.isArray(v) ? v : [];
69
+ }
70
+ function pushArrayItem(key, defaultItem) {
71
+ writeConfig(key, [...readArray(key), defaultItem]);
72
+ }
73
+ function removeArrayItem(key, index) {
74
+ const arr = readArray(key).slice();
75
+ arr.splice(index, 1);
76
+ writeConfig(key, arr);
77
+ }
78
+ function setArrayItemField(key, index, fieldKey, value) {
79
+ const arr = readArray(key).slice();
80
+ arr[index] = { ...arr[index] ?? {}, [fieldKey]: value };
81
+ writeConfig(key, arr);
82
+ }
83
+ </script>
84
+
85
+ <template>
86
+ <aside class="flex h-full w-80 shrink-0 flex-col border-l border-black/[0.06] bg-white">
87
+ <template v-if="selection">
88
+ <div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
89
+ <div class="min-w-0">
90
+ <div class="text-mono text-muted-foreground">
91
+ {{ isNode ? "Node" : "Trigger" }}
92
+ </div>
93
+ <div class="text-sm font-semibold truncate text-foreground">
94
+ {{ labelText(selection.name) || (isNode ? selection.nodeSlug : selection.type) }}
95
+ </div>
96
+ </div>
97
+ <button
98
+ type="button"
99
+ class="flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
100
+ @click="emit('close')"
101
+ >
102
+ <X :size="14" />
103
+ </button>
104
+ </div>
105
+
106
+ <div class="flex-1 overflow-y-auto p-3 space-y-4">
107
+ <!-- Display name -->
108
+ <div class="space-y-1.5">
109
+ <Label class="text-mono text-muted-foreground">Display name</Label>
110
+ <Input
111
+ :model-value="labelText(selection.name)"
112
+ class="h-8 text-sm bg-white"
113
+ placeholder="Override the manifest name"
114
+ @update:model-value="(v) => writeName(String(v))"
115
+ />
116
+ </div>
117
+
118
+ <!-- Trigger config (manual / schedule / webhook) -->
119
+ <template v-if="!isNode">
120
+ <div class="space-y-3">
121
+ <template v-if="selection.type === 'schedule'">
122
+ <div class="space-y-1.5">
123
+ <Label class="text-mono text-muted-foreground">Cron</Label>
124
+ <Input
125
+ :model-value="readTriggerConfig('cron') ?? ''"
126
+ placeholder="0 2 * * *"
127
+ class="h-8 text-sm bg-white font-mono"
128
+ @update:model-value="(v) => writeTriggerConfig('cron', v)"
129
+ />
130
+ </div>
131
+ <div class="space-y-1.5">
132
+ <Label class="text-mono text-muted-foreground">Timezone</Label>
133
+ <Input
134
+ :model-value="readTriggerConfig('timezone') ?? ''"
135
+ placeholder="Europe/Berlin"
136
+ class="h-8 text-sm bg-white"
137
+ @update:model-value="(v) => writeTriggerConfig('timezone', v)"
138
+ />
139
+ </div>
140
+ </template>
141
+ <template v-else-if="selection.type === 'webhook'">
142
+ <div class="space-y-1.5">
143
+ <Label class="text-mono text-muted-foreground">Path</Label>
144
+ <Input
145
+ :model-value="readTriggerConfig('path') ?? ''"
146
+ placeholder="/orders"
147
+ class="h-8 text-sm bg-white font-mono"
148
+ @update:model-value="(v) => writeTriggerConfig('path', v)"
149
+ />
150
+ </div>
151
+ <div class="space-y-1.5">
152
+ <Label class="text-mono text-muted-foreground">Method</Label>
153
+ <Select
154
+ :model-value="readTriggerConfig('method') ?? 'POST'"
155
+ @update:model-value="(v) => writeTriggerConfig('method', v)"
156
+ >
157
+ <SelectTrigger class="h-8 text-sm bg-white">
158
+ <SelectValue />
159
+ </SelectTrigger>
160
+ <SelectContent>
161
+ <SelectItem value="GET">GET</SelectItem>
162
+ <SelectItem value="POST">POST</SelectItem>
163
+ <SelectItem value="PUT">PUT</SelectItem>
164
+ <SelectItem value="PATCH">PATCH</SelectItem>
165
+ <SelectItem value="DELETE">DELETE</SelectItem>
166
+ </SelectContent>
167
+ </Select>
168
+ </div>
169
+ <div class="space-y-1.5">
170
+ <Label class="text-mono text-muted-foreground">Signing secret</Label>
171
+ <Select
172
+ :model-value="readTriggerConfig('secretRef') ?? ''"
173
+ @update:model-value="(v) => writeTriggerConfig('secretRef', v)"
174
+ >
175
+ <SelectTrigger class="h-8 text-sm bg-white">
176
+ <SelectValue placeholder="None" />
177
+ </SelectTrigger>
178
+ <SelectContent>
179
+ <SelectItem v-for="k in secretKeys" :key="k" :value="k">{{ k }}</SelectItem>
180
+ </SelectContent>
181
+ </Select>
182
+ </div>
183
+ </template>
184
+ <template v-else-if="selection.type === 'event'">
185
+ <div class="space-y-1.5">
186
+ <Label class="text-mono text-muted-foreground">Event</Label>
187
+ <Input
188
+ :model-value="readTriggerConfig('event_type') ?? ''"
189
+ placeholder="customer.created"
190
+ class="h-8 text-sm bg-white font-mono"
191
+ @update:model-value="(v) => writeTriggerConfig('event_type', v)"
192
+ />
193
+ <p class="text-sm text-muted-foreground">
194
+ Runs whenever this platform event fires (e.g. <code class="font-mono">customer.created</code>, <code class="font-mono">order.placed</code>). The event payload is available to the workflow as <code class="font-mono">trigger.payload</code>.
195
+ </p>
196
+ </div>
197
+ </template>
198
+ <template v-else>
199
+ <p class="text-sm text-muted-foreground">
200
+ Manual triggers have no configuration. The workflow runs when invoked from the editor or via the API.
201
+ </p>
202
+ </template>
203
+ </div>
204
+ </template>
205
+
206
+ <!-- Node config — generated from manifest -->
207
+ <template v-else>
208
+ <div v-if="manifest?.config?.length" class="space-y-3">
209
+ <div
210
+ v-for="field in manifest.config"
211
+ :key="field.key"
212
+ class="space-y-1.5"
213
+ >
214
+ <div class="flex items-center justify-between">
215
+ <Label class="text-mono text-muted-foreground">
216
+ {{ labelText(field.label) }}{{ field.required ? " *" : "" }}
217
+ </Label>
218
+ <span v-if="field.expressionAllowed" class="text-sm font-mono text-emerald-600/70">
219
+ {{ exprHint }}
220
+ </span>
221
+ </div>
222
+
223
+ <!-- string / expression -->
224
+ <Input
225
+ v-if="['string', 'expression'].includes(field.type)"
226
+ :model-value="readConfig(field.key) ?? ''"
227
+ :placeholder="labelText(field.placeholder)"
228
+ class="h-8 text-sm bg-white"
229
+ :class="field.type === 'expression' ? 'font-mono' : ''"
230
+ @update:model-value="(v) => writeConfig(field.key, v)"
231
+ />
232
+
233
+ <!-- number -->
234
+ <Input
235
+ v-else-if="field.type === 'number'"
236
+ type="number"
237
+ :model-value="readConfig(field.key) ?? ''"
238
+ class="h-8 text-sm bg-white"
239
+ @update:model-value="(v) => writeConfig(field.key, v === '' ? null : Number(v))"
240
+ />
241
+
242
+ <!-- boolean -->
243
+ <div v-else-if="field.type === 'boolean'" class="flex items-center gap-2 pt-1">
244
+ <Switch
245
+ :model-value="Boolean(readConfig(field.key))"
246
+ @update:model-value="(v) => writeConfig(field.key, v)"
247
+ />
248
+ <span class="text-sm text-muted-foreground">
249
+ {{ labelText(field.description) }}
250
+ </span>
251
+ </div>
252
+
253
+ <!-- select -->
254
+ <Select
255
+ v-else-if="field.type === 'select'"
256
+ :model-value="readConfig(field.key) ?? ''"
257
+ @update:model-value="(v) => writeConfig(field.key, v)"
258
+ >
259
+ <SelectTrigger class="h-8 text-sm bg-white">
260
+ <SelectValue :placeholder="labelText(field.placeholder) || 'Pick one'" />
261
+ </SelectTrigger>
262
+ <SelectContent>
263
+ <SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
264
+ {{ labelText(opt.label) }}
265
+ </SelectItem>
266
+ </SelectContent>
267
+ </Select>
268
+
269
+ <!-- multiselect — pill toggles for now -->
270
+ <div v-else-if="field.type === 'multiselect'" class="flex flex-wrap gap-1">
271
+ <button
272
+ v-for="opt in field.options ?? []"
273
+ :key="String(opt.value)"
274
+ type="button"
275
+ class="rounded-md border border-black/[0.08] px-1.5 py-0.5 text-sm transition"
276
+ :class="(readConfig(field.key) ?? []).includes(opt.value) ? 'bg-emerald-500/15 text-emerald-700 border-emerald-500/30' : 'bg-white text-muted-foreground hover:text-foreground'"
277
+ @click="() => {
278
+ const current = readConfig(field.key) ?? [];
279
+ const has = current.includes(opt.value);
280
+ writeConfig(field.key, has ? current.filter((v) => v !== opt.value) : [...current, opt.value]);
281
+ }"
282
+ >
283
+ {{ labelText(opt.label) }}
284
+ </button>
285
+ </div>
286
+
287
+ <!-- secret-ref -->
288
+ <Select
289
+ v-else-if="field.type === 'secret-ref'"
290
+ :model-value="readConfig(field.key) ?? ''"
291
+ @update:model-value="(v) => writeConfig(field.key, v)"
292
+ >
293
+ <SelectTrigger class="h-8 text-sm bg-white">
294
+ <SelectValue placeholder="Pick a secret" />
295
+ </SelectTrigger>
296
+ <SelectContent>
297
+ <SelectItem v-for="k in secretKeys" :key="k" :value="k">{{ k }}</SelectItem>
298
+ </SelectContent>
299
+ </Select>
300
+
301
+ <!-- credentials-ref — pick a credential of the required type -->
302
+ <template v-else-if="field.type === 'credentials-ref'">
303
+ <Select
304
+ :model-value="readConfig(field.key) ?? ''"
305
+ @update:model-value="(v) => writeConfig(field.key, v)"
306
+ >
307
+ <SelectTrigger class="h-8 text-sm bg-white">
308
+ <SelectValue placeholder="Pick a credential" />
309
+ </SelectTrigger>
310
+ <SelectContent>
311
+ <SelectItem v-for="c in credsOfType(field.credentialType)" :key="c.id" :value="c.id">
312
+ {{ c.name }}
313
+ </SelectItem>
314
+ </SelectContent>
315
+ </Select>
316
+ <p v-if="credNeedsReauth(readConfig(field.key))" class="text-sm text-amber-600">
317
+ This credential needs reconnect.
318
+ </p>
319
+ <NuxtLink to="/integrations/credentials" class="text-sm text-violet-600 hover:underline">
320
+ Manage credentials →
321
+ </NuxtLink>
322
+ </template>
323
+
324
+ <!-- array of objects (e.g. headers / mappings) -->
325
+ <div v-else-if="field.type === 'array' && field.items?.type === 'object'" class="space-y-2">
326
+ <div
327
+ v-for="(item, idx) in readArray(field.key)"
328
+ :key="idx"
329
+ class="rounded-md border border-black/[0.06] bg-muted/20 p-2 space-y-1.5"
330
+ >
331
+ <div class="flex items-center justify-between">
332
+ <span class="text-mono text-muted-foreground">
333
+ #{{ idx + 1 }}
334
+ </span>
335
+ <button
336
+ type="button"
337
+ class="flex size-5 items-center justify-center rounded text-muted-foreground hover:text-rose-600"
338
+ @click="removeArrayItem(field.key, idx)"
339
+ >
340
+ <Trash2 :size="11" />
341
+ </button>
342
+ </div>
343
+ <div
344
+ v-for="sub in field.items.properties ?? []"
345
+ :key="sub.key"
346
+ class="space-y-1"
347
+ >
348
+ <Label class="text-mono text-muted-foreground">
349
+ {{ labelText(sub.label) }}
350
+ </Label>
351
+ <Input
352
+ :model-value="item[sub.key] ?? ''"
353
+ :placeholder="labelText(sub.placeholder)"
354
+ class="h-7 text-sm bg-white"
355
+ :class="sub.type === 'expression' ? 'font-mono' : ''"
356
+ @update:model-value="(v) => setArrayItemField(field.key, idx, sub.key, v)"
357
+ />
358
+ </div>
359
+ </div>
360
+ <Button
361
+ type="button"
362
+ variant="outline"
363
+ size="sm"
364
+ class="h-7 text-sm w-full"
365
+ @click="pushArrayItem(field.key, Object.fromEntries((field.items?.properties ?? []).map((p) => [p.key, ''])))"
366
+ >
367
+ <Plus :size="11" />
368
+ Add row
369
+ </Button>
370
+ </div>
371
+
372
+ <!-- fallback: JSON textarea for complex types -->
373
+ <Textarea
374
+ v-else
375
+ :model-value="readConfig(field.key) === void 0 ? '' : JSON.stringify(readConfig(field.key), null, 2)"
376
+ rows="3"
377
+ class="text-sm font-mono bg-white"
378
+ @update:model-value="(v) => {
379
+ try {
380
+ writeConfig(field.key, v ? JSON.parse(String(v)) : void 0);
381
+ } catch {
382
+ }
383
+ }"
384
+ />
385
+
386
+ <p v-if="field.description" class="text-sm text-muted-foreground">
387
+ {{ labelText(field.description) }}
388
+ </p>
389
+ </div>
390
+ </div>
391
+ <div v-else class="text-sm text-muted-foreground italic">
392
+ This node has no configuration.
393
+ </div>
394
+
395
+ <!-- Retry section (always available on nodes) -->
396
+ <div class="space-y-1.5 border-t border-black/[0.06] pt-3 mt-3">
397
+ <Label class="text-mono text-muted-foreground">Retry policy</Label>
398
+ <div class="flex items-center gap-2">
399
+ <Switch
400
+ :model-value="!!selection.retry"
401
+ @update:model-value="(v) => emit('update', { ...selection, retry: v ? { maxAttempts: 3, backoffSeconds: 1, strategy: 'exponential' } : void 0 })"
402
+ />
403
+ <span class="text-sm text-muted-foreground">Retry on failure</span>
404
+ </div>
405
+ <template v-if="selection.retry">
406
+ <div class="grid grid-cols-2 gap-2 pt-1">
407
+ <Input
408
+ type="number"
409
+ :model-value="selection.retry?.maxAttempts ?? 3"
410
+ class="h-7 text-sm bg-white"
411
+ @update:model-value="(v) => emit('update', { ...selection, retry: { ...selection.retry, maxAttempts: Number(v) || 1 } })"
412
+ />
413
+ <Input
414
+ type="number"
415
+ :model-value="selection.retry?.backoffSeconds ?? 0"
416
+ class="h-7 text-sm bg-white"
417
+ @update:model-value="(v) => emit('update', { ...selection, retry: { ...selection.retry, backoffSeconds: Number(v) || 0 } })"
418
+ />
419
+ </div>
420
+ <Select
421
+ :model-value="selection.retry?.strategy ?? 'exponential'"
422
+ @update:model-value="(v) => emit('update', { ...selection, retry: { ...selection.retry, strategy: v } })"
423
+ >
424
+ <SelectTrigger class="h-7 text-sm bg-white">
425
+ <SelectValue />
426
+ </SelectTrigger>
427
+ <SelectContent>
428
+ <SelectItem value="linear">linear backoff</SelectItem>
429
+ <SelectItem value="exponential">exponential backoff</SelectItem>
430
+ </SelectContent>
431
+ </Select>
432
+ </template>
433
+ </div>
434
+ </template>
435
+ </div>
436
+
437
+ <div class="border-t border-black/[0.06] p-3">
438
+ <Button
439
+ variant="outline"
440
+ size="sm"
441
+ class="w-full h-7 text-sm text-rose-600 hover:text-rose-700"
442
+ @click="emit('remove')"
443
+ >
444
+ <Trash2 :size="12" />
445
+ Remove from workflow
446
+ </Button>
447
+ </div>
448
+ </template>
449
+ </aside>
450
+ </template>
@@ -0,0 +1,16 @@
1
+ import type { WorkflowNode, WorkflowTrigger } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ /** Either a workflow node OR a trigger. Distinguished by presence of `nodeSlug`. */
4
+ selection: WorkflowNode | WorkflowTrigger | null;
5
+ secretKeys: string[];
6
+ };
7
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ update: (next: WorkflowTrigger | WorkflowNode) => any;
9
+ remove: () => any;
10
+ close: () => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onUpdate?: ((next: WorkflowTrigger | WorkflowNode) => any) | undefined;
13
+ onRemove?: (() => any) | undefined;
14
+ onClose?: (() => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ export default _default;