@revenexx/studio-integrations 0.6.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +643 -0
- package/README.md +31 -0
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -3
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
- package/dist/runtime/components/Integrations/ExpressionInput.vue +21 -15
- package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
- package/dist/runtime/components/Integrations/Layout.vue +28 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
- package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +82 -34
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +4 -2
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +8 -6
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +14 -2
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
- package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
- package/dist/runtime/data/integrationNodeCatalog.js +22 -4
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- package/dist/runtime/i18n/messages/de.json +1224 -0
- package/dist/runtime/i18n/messages/en.json +1224 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +52 -34
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
- package/dist/runtime/pages/integrations/credentials.vue +73 -52
- package/dist/runtime/pages/integrations/index.vue +66 -73
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +49 -28
- package/dist/runtime/pages/integrations/secrets.vue +34 -34
- package/dist/runtime/pages/integrations/templates/[slug].vue +89 -72
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/types/integrations.d.ts +25 -1
- package/dist/runtime/utils/cockpitSources.d.ts +22 -3
- package/dist/runtime/utils/credentialState.d.ts +30 -2
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +52 -5
- package/dist/runtime/utils/localize.js +47 -9
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +0 -25
- package/dist/runtime/utils/schedule.js +37 -25
- package/dist/runtime/utils/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -0
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +2 -2
- package/dist/runtime/utils/validationFindings.js +4 -3
- package/dist/types.d.mts +2 -6
- package/package.json +3 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One shortcut into a surface the *host* provides — Import & Export, the SFTP
|
|
3
|
+
* server, the event stream, inbound webhooks. None of the four is a page of this
|
|
4
|
+
* module: they are the cockpit's, and this card is the studio's landing page
|
|
5
|
+
* pointing at them.
|
|
6
|
+
*
|
|
7
|
+
* So the card has two states, and which one it is in is not its own decision:
|
|
8
|
+
*
|
|
9
|
+
* - **`available`** — the host has the surface. The card is a link and behaves
|
|
10
|
+
* exactly as it did before PO-223.
|
|
11
|
+
* - **not available** — the card stays on screen, looks the same, and is a button
|
|
12
|
+
* that explains where the surface lives instead of leading into a route the host
|
|
13
|
+
* does not serve. Before PO-223 it was a link into nothing: in the playground
|
|
14
|
+
* and in the standalone dev studio all four led to a 404.
|
|
15
|
+
*
|
|
16
|
+
* **Staying on screen is the point**, and it is the call the managed credential
|
|
17
|
+
* sources already made (`CredentialRevenexxSource.vue`, PO-222): a control that
|
|
18
|
+
* vanishes leaves nothing to read the explanation from, and the reader cannot tell
|
|
19
|
+
* a broken feature from one that belongs somewhere else.
|
|
20
|
+
*
|
|
21
|
+
* **No `aria-label`, on purpose** — like `TemplateCard`, the card takes its name
|
|
22
|
+
* from what it shows. The state is carried by the *role*: "Import & Export,
|
|
23
|
+
* button" instead of "link" is what says to a screen reader that this does not
|
|
24
|
+
* navigate, and it says it without announcing something no sighted reader can see
|
|
25
|
+
* (`specs/accessibility.md` AC-1 names the on-screen text as the source of a name).
|
|
26
|
+
*
|
|
27
|
+
* Who decides `available`, and why it is the router rather than a guess about the
|
|
28
|
+
* host, is `docs/host-surfaces.md`.
|
|
29
|
+
*
|
|
30
|
+
* The two branches draw the same body twice rather than sharing it: `as-child`
|
|
31
|
+
* wants a real element as its child, and the alternatives — a wrapper element
|
|
32
|
+
* around the card, or a second component for eight lines — cost either the grid
|
|
33
|
+
* layout or a file. **Keep the two bodies identical.**
|
|
34
|
+
*/
|
|
35
|
+
import type { Component } from 'vue';
|
|
36
|
+
type __VLS_Props = {
|
|
37
|
+
/** What the surface is called, in the reader's language. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** One line on what it is for. Shown in both states. */
|
|
40
|
+
hint: string;
|
|
41
|
+
icon: Component;
|
|
42
|
+
/** Where the surface lives in a host that has it. */
|
|
43
|
+
to: string;
|
|
44
|
+
/** Whether this host has it. See the comment above; no default on purpose. */
|
|
45
|
+
available: boolean;
|
|
46
|
+
/** What the popover says when it does not. */
|
|
47
|
+
note: string;
|
|
48
|
+
};
|
|
49
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
50
|
+
export default _default;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, resolveComponent } from "vue";
|
|
3
|
-
import { RefreshCircle as Loader2
|
|
3
|
+
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
|
+
import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
|
|
4
5
|
import { templateCategoryLabel, templatePackageLabel } from "../../data/integrationTemplates";
|
|
5
6
|
import TemplateFlowPreview from "./TemplateFlowPreview.vue";
|
|
6
7
|
const props = defineProps({
|
|
@@ -11,13 +12,22 @@ const props = defineProps({
|
|
|
11
12
|
disabled: { type: Boolean, required: false }
|
|
12
13
|
});
|
|
13
14
|
const emit = defineEmits(["select"]);
|
|
15
|
+
const t = useIntegrationsT();
|
|
14
16
|
const NuxtLink = resolveComponent("NuxtLink");
|
|
15
17
|
const full = computed(() => props.density === "full");
|
|
16
18
|
const banner = computed(
|
|
17
19
|
() => full.value ? props.template.images.find((i) => i.category === "banner")?.src ?? null : null
|
|
18
20
|
);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
+
const TRIGGER_LABEL_KEY = {
|
|
22
|
+
schedule: "integration.templates.card.trigger.schedule",
|
|
23
|
+
webhook: "integration.templates.trigger.webhook",
|
|
24
|
+
manual: "integration.templates.trigger.manual",
|
|
25
|
+
event: "integration.templates.card.trigger.event"
|
|
26
|
+
};
|
|
27
|
+
function triggerTypeLabel(type) {
|
|
28
|
+
const key = TRIGGER_LABEL_KEY[type];
|
|
29
|
+
return key ? t(key) : type;
|
|
30
|
+
}
|
|
21
31
|
function levelDot(level) {
|
|
22
32
|
return level === "beginner" ? "bg-emerald-500" : level === "intermediate" ? "bg-amber-500" : "bg-rose-500";
|
|
23
33
|
}
|
|
@@ -66,7 +76,7 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
|
|
|
66
76
|
<div
|
|
67
77
|
v-else
|
|
68
78
|
role="status"
|
|
69
|
-
aria-label="
|
|
79
|
+
:aria-label="t('integration.templates.card.state.loadingFlow')"
|
|
70
80
|
class="flex h-full items-center justify-center"
|
|
71
81
|
>
|
|
72
82
|
<Loader2 :size="full ? 16 : 14" class="animate-spin text-muted-foreground/50" />
|
|
@@ -90,12 +100,12 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
|
|
|
90
100
|
|
|
91
101
|
<div v-if="full" class="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
|
|
92
102
|
<span
|
|
93
|
-
v-for="
|
|
94
|
-
:key="
|
|
103
|
+
v-for="triggerType in template.triggerTypes"
|
|
104
|
+
:key="triggerType"
|
|
95
105
|
class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase"
|
|
96
106
|
>
|
|
97
|
-
<component :is="
|
|
98
|
-
{{
|
|
107
|
+
<component :is="glyphForTrigger(triggerType)" :size="10" />
|
|
108
|
+
{{ triggerTypeLabel(triggerType) }}
|
|
99
109
|
</span>
|
|
100
110
|
<span class="flex-1" />
|
|
101
111
|
<span class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, onMounted } from "vue";
|
|
3
3
|
import * as Lucide from "lucide-vue-next";
|
|
4
|
+
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
4
5
|
import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
5
6
|
import { resolveLocalized } from "../../utils/localize";
|
|
6
7
|
import { describeCron } from "../../utils/schedule";
|
|
@@ -11,6 +12,11 @@ const props = defineProps({
|
|
|
11
12
|
padding: { type: Number, required: false, default: 36 },
|
|
12
13
|
showErrorBranches: { type: Boolean, required: false, default: false }
|
|
13
14
|
});
|
|
15
|
+
const t = useIntegrationsT();
|
|
16
|
+
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
void loadNodeCatalog();
|
|
19
|
+
});
|
|
14
20
|
const STOP_SLUG = "revenexx:stop-and-error";
|
|
15
21
|
const NODE_W = 220;
|
|
16
22
|
const NODE_H = 58;
|
|
@@ -21,22 +27,26 @@ const boxes = computed(() => {
|
|
|
21
27
|
if (!blob) return [];
|
|
22
28
|
const out = [];
|
|
23
29
|
const triggers = blob.triggers ?? [];
|
|
24
|
-
triggers.forEach((
|
|
25
|
-
const pos = blob.triggerPositions?.[
|
|
26
|
-
const template = TRIGGER_TEMPLATES.find((tt) => tt.type ===
|
|
27
|
-
const subtitle =
|
|
30
|
+
triggers.forEach((t2, i) => {
|
|
31
|
+
const pos = blob.triggerPositions?.[t2.id] ?? { x: 40, y: 40 + i * 160 };
|
|
32
|
+
const template = TRIGGER_TEMPLATES.find((tt) => tt.type === t2.type);
|
|
33
|
+
const subtitle = t2.type === "schedule" && t2.config?.cron ? describeCron(String(t2.config.cron)) ?? String(t2.config.cron) : t2.type === "webhook" && t2.config?.path ? String(t2.config.path) : resolveLocalized(template?.description ?? "");
|
|
28
34
|
out.push({
|
|
29
|
-
key: `t-${
|
|
35
|
+
key: `t-${t2.id}`,
|
|
30
36
|
kind: "trigger",
|
|
31
37
|
x: pos.x,
|
|
32
38
|
y: pos.y,
|
|
33
39
|
w: TRIG_W,
|
|
34
40
|
h: TRIG_H,
|
|
35
|
-
label: typeof
|
|
41
|
+
label: typeof t2.name === "string" && t2.name || t2.type.charAt(0).toUpperCase() + t2.type.slice(1),
|
|
36
42
|
subtitle,
|
|
37
|
-
type:
|
|
43
|
+
type: t2.type,
|
|
38
44
|
manifest: null,
|
|
39
|
-
|
|
45
|
+
// The trigger's glyph from the one map, like every other trigger box. This
|
|
46
|
+
// resolved `TRIGGER_TEMPLATES`' icon name ('Zap', 'Play', …) out of Lucide
|
|
47
|
+
// instead — the same route the palette's tiles took — so the preview drew a
|
|
48
|
+
// Lucide bolt where the card it previews draws Solar's (PO-329).
|
|
49
|
+
icon: triggerGlyph(t2.type)
|
|
40
50
|
});
|
|
41
51
|
});
|
|
42
52
|
blob.nodes.forEach((n, i) => {
|
|
@@ -161,7 +171,7 @@ const bounds = computed(() => {
|
|
|
161
171
|
>
|
|
162
172
|
<div class="flex flex-1 items-center gap-2.5 px-3 py-2">
|
|
163
173
|
<span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-emerald-500 text-white shadow-sm">
|
|
164
|
-
<component :is="b.icon" :size="15"
|
|
174
|
+
<component :is="b.icon" :size="15" />
|
|
165
175
|
</span>
|
|
166
176
|
<div class="min-w-0 flex-1">
|
|
167
177
|
<div class="truncate text-sm font-semibold leading-tight text-foreground">{{ b.label }}</div>
|
|
@@ -169,7 +179,7 @@ const bounds = computed(() => {
|
|
|
169
179
|
</div>
|
|
170
180
|
</div>
|
|
171
181
|
<div class="rounded-b-md border-t border-emerald-500/15 bg-emerald-500/[0.04] px-3 py-1 text-mono font-semibold text-emerald-700/80">
|
|
172
|
-
|
|
182
|
+
{{ t("integration.templates.preview.triggerLabel") }} · {{ b.type }}
|
|
173
183
|
</div>
|
|
174
184
|
</div>
|
|
175
185
|
|
|
@@ -192,7 +202,7 @@ const bounds = computed(() => {
|
|
|
192
202
|
</foreignObject>
|
|
193
203
|
</svg>
|
|
194
204
|
<div v-else class="flex h-full items-center justify-center text-sm text-muted-foreground">
|
|
195
|
-
<slot name="empty">
|
|
205
|
+
<slot name="empty">{{ t("integration.templates.preview.state.loading") }}</slot>
|
|
196
206
|
</div>
|
|
197
207
|
</div>
|
|
198
208
|
</template>
|
|
@@ -17,23 +17,24 @@ const props = defineProps({
|
|
|
17
17
|
runBusy: { type: Boolean, required: false }
|
|
18
18
|
});
|
|
19
19
|
const emit = defineEmits(["action", "update:open"]);
|
|
20
|
+
const t = useIntegrationsT();
|
|
20
21
|
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
|
|
21
22
|
function formatShortcut(tokens) {
|
|
22
23
|
const parts = tokens.split("+");
|
|
23
24
|
const out = [];
|
|
24
25
|
for (const raw of parts) {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
else if (
|
|
28
|
-
else if (
|
|
29
|
-
else if (
|
|
30
|
-
else if (
|
|
31
|
-
else if (
|
|
32
|
-
else if (
|
|
33
|
-
else if (
|
|
34
|
-
else if (
|
|
35
|
-
else if (
|
|
36
|
-
else out.push(
|
|
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());
|
|
37
38
|
}
|
|
38
39
|
return isMac ? out.join("") : out.join("+");
|
|
39
40
|
}
|
|
@@ -42,19 +43,19 @@ const sections = computed(() => {
|
|
|
42
43
|
if (props.kind === "node") {
|
|
43
44
|
return [
|
|
44
45
|
[
|
|
45
|
-
{ id: "open", label: "
|
|
46
|
-
{ id: "rename", label: "
|
|
46
|
+
{ id: "open", label: t("integration.canvas.contextMenu.item.open"), shortcut: "enter" },
|
|
47
|
+
{ id: "rename", label: t("integration.canvas.contextMenu.item.rename"), shortcut: "f2" }
|
|
47
48
|
],
|
|
48
49
|
[
|
|
49
|
-
{ id: "copy", label: "
|
|
50
|
-
{ id: "duplicate", label: "
|
|
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 }
|
|
51
52
|
],
|
|
52
53
|
[
|
|
53
|
-
{ id: "run", label: "
|
|
54
|
-
{ id: "tidy-up", label: "
|
|
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" }
|
|
55
56
|
],
|
|
56
57
|
[
|
|
57
|
-
{ id: "delete", label: "
|
|
58
|
+
{ id: "delete", label: t("integration.common.action.delete"), shortcut: "backspace", danger: true }
|
|
58
59
|
]
|
|
59
60
|
];
|
|
60
61
|
}
|
|
@@ -62,37 +63,37 @@ const sections = computed(() => {
|
|
|
62
63
|
const n = props.selectionCount;
|
|
63
64
|
return [
|
|
64
65
|
[
|
|
65
|
-
{ id: "copy", label:
|
|
66
|
-
{ id: "duplicate", label:
|
|
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" }
|
|
67
68
|
],
|
|
68
69
|
[
|
|
69
|
-
{ id: "group", label:
|
|
70
|
-
{ id: "tidy-up", label: "
|
|
71
|
-
{ id: "select-all", label: "
|
|
72
|
-
{ id: "clear-selection", label: "
|
|
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" }
|
|
73
74
|
],
|
|
74
75
|
[
|
|
75
|
-
{ id: "delete", label:
|
|
76
|
+
{ id: "delete", label: t("integration.canvas.contextMenu.item.deleteNodes", n), shortcut: "backspace", danger: true }
|
|
76
77
|
]
|
|
77
78
|
];
|
|
78
79
|
}
|
|
79
80
|
return [
|
|
80
81
|
[
|
|
81
|
-
{ id: "add-node", label: "
|
|
82
|
-
{ id: "add-sticky", label: "
|
|
83
|
-
{ id: "paste", label: "
|
|
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" }
|
|
84
85
|
],
|
|
85
86
|
[
|
|
86
|
-
{ id: "tidy-up", label: "
|
|
87
|
-
{ id: "fit-view", label: "
|
|
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" }
|
|
88
89
|
],
|
|
89
90
|
[
|
|
90
|
-
{ id: "select-all", label: "
|
|
91
|
+
{ id: "select-all", label: t("integration.canvas.contextMenu.item.selectAll"), shortcut: "mod+a", disabled: allSelected.value || props.totalNodes === 0 }
|
|
91
92
|
]
|
|
92
93
|
];
|
|
93
94
|
});
|
|
94
95
|
const headerLabel = computed(
|
|
95
|
-
() => props.kind === "selection" ?
|
|
96
|
+
() => props.kind === "selection" ? t("integration.canvas.contextMenu.header.selectionCount", props.selectionCount) : null
|
|
96
97
|
);
|
|
97
98
|
function onSelect(item) {
|
|
98
99
|
if (item.disabled) return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
-
import { RefreshCircle as Loader2, MagicStick as Sparkles } from "#solar-icons";
|
|
3
|
+
import { RefreshCircle as Loader2, Magnifier as Search, MagicStick as Sparkles } from "#solar-icons";
|
|
4
|
+
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
4
5
|
import * as Lucide from "lucide-vue-next";
|
|
5
6
|
import {
|
|
6
7
|
Badge,
|
|
@@ -25,14 +26,15 @@ import WorkflowIconPicker from "./WorkflowIconPicker.vue";
|
|
|
25
26
|
import WorkflowTagsInput from "./WorkflowTagsInput.vue";
|
|
26
27
|
import { resolveLocalized } from "../../utils/localize";
|
|
27
28
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
29
|
+
const t = useIntegrationsT();
|
|
28
30
|
const router = useRouter();
|
|
29
31
|
const { create, workflows } = useIntegrationsWorkflows();
|
|
30
32
|
const { templates, loading: templatesLoading, error: templatesError, load: loadTemplates, ensureDetails, instantiate } = useIntegrationsTemplates();
|
|
31
33
|
const mode = ref("blank");
|
|
32
|
-
const modeTabs = [
|
|
33
|
-
{ key: "blank", label: "
|
|
34
|
-
{ key: "template", label: "
|
|
35
|
-
];
|
|
34
|
+
const modeTabs = computed(() => [
|
|
35
|
+
{ key: "blank", label: t("integration.workflows.create.tab.blank") },
|
|
36
|
+
{ key: "template", label: t("integration.workflows.create.tab.template") }
|
|
37
|
+
]);
|
|
36
38
|
function iconFor(name2) {
|
|
37
39
|
if (!name2) return Lucide.Box;
|
|
38
40
|
return Lucide[name2] || Lucide.Box;
|
|
@@ -45,18 +47,18 @@ const tags = ref([]);
|
|
|
45
47
|
const icon = ref(null);
|
|
46
48
|
const tagSuggestions = computed(() => {
|
|
47
49
|
const all = /* @__PURE__ */ new Set();
|
|
48
|
-
for (const wf of workflows.value) for (const
|
|
50
|
+
for (const wf of workflows.value) for (const t2 of wf.blob.tags ?? []) all.add(t2);
|
|
49
51
|
return [...all].sort();
|
|
50
52
|
});
|
|
51
53
|
const submitting = ref(false);
|
|
52
54
|
const error = ref(null);
|
|
53
|
-
const executionModes = [
|
|
54
|
-
{ key: "async_only", label: "
|
|
55
|
-
{ key: "sync_only", label: "
|
|
56
|
-
{ key: "caller_decides", label: "
|
|
57
|
-
];
|
|
58
|
-
const selectedTrigger = computed(() => TRIGGER_TEMPLATES.find((
|
|
59
|
-
const selectedMode = computed(() => executionModes.find((m) => m.key === executionMode.value) ?? executionModes[0]);
|
|
55
|
+
const executionModes = computed(() => [
|
|
56
|
+
{ key: "async_only", label: t("integration.workflows.create.executionMode.asyncOnly.label"), hint: t("integration.workflows.create.executionMode.asyncOnly.hint"), icon: "Inbox" },
|
|
57
|
+
{ key: "sync_only", label: t("integration.workflows.create.executionMode.syncOnly.label"), hint: t("integration.workflows.create.executionMode.syncOnly.hint"), icon: "Timer" },
|
|
58
|
+
{ key: "caller_decides", label: t("integration.workflows.create.executionMode.callerDecides.label"), hint: t("integration.workflows.create.executionMode.callerDecides.hint"), icon: "SlidersHorizontal" }
|
|
59
|
+
]);
|
|
60
|
+
const selectedTrigger = computed(() => TRIGGER_TEMPLATES.find((tt) => tt.type === triggerType.value) ?? null);
|
|
61
|
+
const selectedMode = computed(() => executionModes.value.find((m) => m.key === executionMode.value) ?? executionModes.value[0]);
|
|
60
62
|
watch(open, (v) => {
|
|
61
63
|
if (!v) return;
|
|
62
64
|
mode.value = "blank";
|
|
@@ -72,13 +74,13 @@ watch(open, (v) => {
|
|
|
72
74
|
});
|
|
73
75
|
async function submit() {
|
|
74
76
|
if (!name.value.trim()) {
|
|
75
|
-
error.value = "
|
|
77
|
+
error.value = t("integration.workflows.create.error.nameRequired");
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
submitting.value = true;
|
|
79
81
|
error.value = null;
|
|
80
82
|
try {
|
|
81
|
-
const tpl = TRIGGER_TEMPLATES.find((
|
|
83
|
+
const tpl = TRIGGER_TEMPLATES.find((tt) => tt.type === triggerType.value);
|
|
82
84
|
const handle = globalThis.crypto?.randomUUID?.() ?? `trg-${Date.now()}`;
|
|
83
85
|
const blob = {
|
|
84
86
|
nodeManifestVersion: "v0-draft",
|
|
@@ -100,7 +102,7 @@ async function submit() {
|
|
|
100
102
|
open.value = false;
|
|
101
103
|
await router.push(`/integrations/workflows/${created.id}`);
|
|
102
104
|
} catch (e) {
|
|
103
|
-
error.value = e.message || "
|
|
105
|
+
error.value = e.message || t("integration.workflows.create.error.generic");
|
|
104
106
|
} finally {
|
|
105
107
|
submitting.value = false;
|
|
106
108
|
}
|
|
@@ -108,12 +110,12 @@ async function submit() {
|
|
|
108
110
|
const instantiatingSlug = ref(null);
|
|
109
111
|
const templateSearch = ref("");
|
|
110
112
|
watch(() => templates.value.length, (n) => {
|
|
111
|
-
if (n) void ensureDetails(templates.value.map((
|
|
113
|
+
if (n) void ensureDetails(templates.value.map((tpl) => tpl.slug));
|
|
112
114
|
}, { immediate: true });
|
|
113
115
|
const filteredTemplates = computed(() => {
|
|
114
116
|
const q = templateSearch.value.trim().toLowerCase();
|
|
115
117
|
if (!q) return templates.value;
|
|
116
|
-
return templates.value.filter((
|
|
118
|
+
return templates.value.filter((tpl) => `${tpl.name} ${tpl.description} ${tpl.category}`.toLowerCase().includes(q));
|
|
117
119
|
});
|
|
118
120
|
async function useTemplate(slug) {
|
|
119
121
|
if (instantiatingSlug.value) return;
|
|
@@ -124,7 +126,7 @@ async function useTemplate(slug) {
|
|
|
124
126
|
open.value = false;
|
|
125
127
|
await router.push(`/integrations/workflows/${id}`);
|
|
126
128
|
} catch (e) {
|
|
127
|
-
toast.error(e?.message ?? "
|
|
129
|
+
toast.error(e?.message ?? t("integration.workflows.create.toast.templateError"));
|
|
128
130
|
} finally {
|
|
129
131
|
instantiatingSlug.value = null;
|
|
130
132
|
}
|
|
@@ -135,7 +137,7 @@ async function useTemplate(slug) {
|
|
|
135
137
|
<Dialog v-model:open="open">
|
|
136
138
|
<DialogContent class="sm:max-w-4xl">
|
|
137
139
|
<DialogHeader>
|
|
138
|
-
<DialogTitle>
|
|
140
|
+
<DialogTitle>{{ t("integration.workflows.create.dialog.title") }}</DialogTitle>
|
|
139
141
|
</DialogHeader>
|
|
140
142
|
|
|
141
143
|
<!-- Switcher: the same TabBar the studio surfaces use. -->
|
|
@@ -146,39 +148,39 @@ async function useTemplate(slug) {
|
|
|
146
148
|
<form v-if="mode === 'blank'" class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1" @submit.prevent="submit">
|
|
147
149
|
<!-- Two-column rows: label rail left, field right. -->
|
|
148
150
|
<div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
|
|
149
|
-
<Label for="wf-name">
|
|
150
|
-
<Input id="wf-name" v-model="name" placeholder="
|
|
151
|
+
<Label for="wf-name">{{ t("integration.workflows.create.field.name.label") }}</Label>
|
|
152
|
+
<Input id="wf-name" v-model="name" :placeholder="t('integration.workflows.create.field.name.placeholder')" class="bg-white" />
|
|
151
153
|
</div>
|
|
152
154
|
|
|
153
155
|
<div class="grid grid-cols-[130px_1fr] items-start gap-x-4">
|
|
154
|
-
<Label for="wf-desc" class="pt-2.5">
|
|
156
|
+
<Label for="wf-desc" class="pt-2.5">{{ t("integration.workflows.create.field.description.label") }}</Label>
|
|
155
157
|
<Textarea
|
|
156
158
|
id="wf-desc"
|
|
157
159
|
v-model="description"
|
|
158
160
|
rows="2"
|
|
159
|
-
placeholder="
|
|
161
|
+
:placeholder="t('integration.workflows.create.field.description.placeholder')"
|
|
160
162
|
class="bg-white"
|
|
161
163
|
/>
|
|
162
164
|
</div>
|
|
163
165
|
|
|
164
166
|
<div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
|
|
165
|
-
<Label for="wf-tags">
|
|
167
|
+
<Label for="wf-tags">{{ t("integration.workflows.create.field.tags.label") }}</Label>
|
|
166
168
|
<WorkflowTagsInput v-model="tags" input-id="wf-tags" :suggestions="tagSuggestions" />
|
|
167
169
|
</div>
|
|
168
170
|
|
|
169
171
|
<div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
|
|
170
|
-
<Label id="wf-icon-label">
|
|
172
|
+
<Label id="wf-icon-label">{{ t("integration.workflows.create.field.icon.label") }}</Label>
|
|
171
173
|
<WorkflowIconPicker v-model="icon" trigger-labelledby="wf-icon-label" />
|
|
172
174
|
</div>
|
|
173
175
|
|
|
174
176
|
<div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
|
|
175
|
-
<Label for="wf-trigger">
|
|
177
|
+
<Label for="wf-trigger">{{ t("integration.workflows.create.field.trigger.label") }}</Label>
|
|
176
178
|
<Select v-model="triggerType">
|
|
177
179
|
<SelectTrigger id="wf-trigger" class="w-full bg-white">
|
|
178
180
|
<SelectValue>
|
|
179
181
|
<span class="flex min-w-0 items-center gap-2">
|
|
180
182
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
181
|
-
<component :is="
|
|
183
|
+
<component :is="triggerGlyph(selectedTrigger?.type)" :size="13" />
|
|
182
184
|
</span>
|
|
183
185
|
<span class="shrink-0 text-sm font-medium">{{ resolveLocalized(selectedTrigger?.name) }}</span>
|
|
184
186
|
<span class="truncate text-sm text-muted-foreground">{{ resolveLocalized(selectedTrigger?.description) }}</span>
|
|
@@ -189,7 +191,7 @@ async function useTemplate(slug) {
|
|
|
189
191
|
<SelectItem v-for="t in TRIGGER_TEMPLATES" :key="t.id" :value="t.type">
|
|
190
192
|
<span class="flex items-center gap-2.5 py-0.5">
|
|
191
193
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
192
|
-
<component :is="
|
|
194
|
+
<component :is="triggerGlyph(t.type)" :size="14" />
|
|
193
195
|
</span>
|
|
194
196
|
<span class="min-w-0">
|
|
195
197
|
<span class="block text-sm font-medium">{{ resolveLocalized(t.name) }}</span>
|
|
@@ -202,7 +204,7 @@ async function useTemplate(slug) {
|
|
|
202
204
|
</div>
|
|
203
205
|
|
|
204
206
|
<div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
|
|
205
|
-
<Label for="wf-exec-mode">
|
|
207
|
+
<Label for="wf-exec-mode">{{ t("integration.workflows.create.field.executionMode.label") }}</Label>
|
|
206
208
|
<Select v-model="executionMode">
|
|
207
209
|
<SelectTrigger id="wf-exec-mode" class="w-full bg-white">
|
|
208
210
|
<SelectValue>
|
|
@@ -238,10 +240,10 @@ async function useTemplate(slug) {
|
|
|
238
240
|
<div class="flex gap-2 pt-1">
|
|
239
241
|
<Button type="submit" :disabled="submitting" class="h-8 text-sm">
|
|
240
242
|
<Loader2 v-if="submitting" :size="14" class="animate-spin" />
|
|
241
|
-
{{ submitting ? "
|
|
243
|
+
{{ submitting ? t("integration.workflows.create.action.submitting") : t("integration.workflows.create.action.submit") }}
|
|
242
244
|
</Button>
|
|
243
245
|
<Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
|
|
244
|
-
|
|
246
|
+
{{ t("integration.common.action.cancel") }}
|
|
245
247
|
</Button>
|
|
246
248
|
</div>
|
|
247
249
|
</form>
|
|
@@ -249,12 +251,12 @@ async function useTemplate(slug) {
|
|
|
249
251
|
<!-- ── From template ─────────────────────────────────────────────────── -->
|
|
250
252
|
<div v-else class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1">
|
|
251
253
|
<div class="relative">
|
|
252
|
-
<
|
|
254
|
+
<Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
253
255
|
<input
|
|
254
256
|
v-model="templateSearch"
|
|
255
257
|
type="text"
|
|
256
|
-
aria-label="
|
|
257
|
-
placeholder="
|
|
258
|
+
:aria-label="t('integration.workflows.create.template.search.aria')"
|
|
259
|
+
:placeholder="t('integration.workflows.create.template.search.placeholder')"
|
|
258
260
|
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"
|
|
259
261
|
>
|
|
260
262
|
</div>
|
|
@@ -273,10 +275,10 @@ async function useTemplate(slug) {
|
|
|
273
275
|
{{ templatesError }}
|
|
274
276
|
</div>
|
|
275
277
|
<p v-else-if="!templates.length" class="p-6 text-center text-sm text-muted-foreground">
|
|
276
|
-
|
|
278
|
+
{{ t("integration.workflows.create.template.empty") }}
|
|
277
279
|
</p>
|
|
278
280
|
<p v-else-if="!filteredTemplates.length" class="p-6 text-center text-sm text-muted-foreground">
|
|
279
|
-
|
|
281
|
+
{{ t("integration.workflows.create.template.noMatch", { query: templateSearch }) }}
|
|
280
282
|
</p>
|
|
281
283
|
|
|
282
284
|
<div v-else class="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
|
|
@@ -294,10 +296,10 @@ async function useTemplate(slug) {
|
|
|
294
296
|
<div class="flex items-center justify-between pt-1">
|
|
295
297
|
<NuxtLink to="/integrations/templates" class="inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary" @click="open = false">
|
|
296
298
|
<Sparkles :size="13" />
|
|
297
|
-
|
|
299
|
+
{{ t("integration.workflows.create.template.browseAll") }}
|
|
298
300
|
</NuxtLink>
|
|
299
301
|
<Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
|
|
300
|
-
|
|
302
|
+
{{ t("integration.common.action.cancel") }}
|
|
301
303
|
</Button>
|
|
302
304
|
</div>
|
|
303
305
|
</div>
|
|
@@ -8,13 +8,14 @@ const props = defineProps({
|
|
|
8
8
|
workflow: { type: [Object, null], required: true }
|
|
9
9
|
});
|
|
10
10
|
const emit = defineEmits(["duplicated"]);
|
|
11
|
+
const t = useIntegrationsT();
|
|
11
12
|
const { duplicate } = useIntegrationsWorkflows();
|
|
12
13
|
const name = ref("");
|
|
13
14
|
const busy = ref(false);
|
|
14
15
|
const error = ref(null);
|
|
15
16
|
watch(open, (o) => {
|
|
16
17
|
if (o) {
|
|
17
|
-
name.value = props.workflow ?
|
|
18
|
+
name.value = props.workflow ? t("integration.workflows.duplicate.field.name.default", { name: props.workflow.name }) : "";
|
|
18
19
|
error.value = null;
|
|
19
20
|
}
|
|
20
21
|
});
|
|
@@ -28,7 +29,7 @@ async function submit() {
|
|
|
28
29
|
open.value = false;
|
|
29
30
|
emit("duplicated", copy);
|
|
30
31
|
} catch (e) {
|
|
31
|
-
error.value = e?.message || "
|
|
32
|
+
error.value = e?.message || t("integration.workflows.duplicate.error.generic");
|
|
32
33
|
} finally {
|
|
33
34
|
busy.value = false;
|
|
34
35
|
}
|
|
@@ -39,23 +40,23 @@ async function submit() {
|
|
|
39
40
|
<Dialog v-model:open="open">
|
|
40
41
|
<DialogContent class="sm:max-w-md">
|
|
41
42
|
<DialogHeader>
|
|
42
|
-
<DialogTitle>
|
|
43
|
+
<DialogTitle>{{ t("integration.workflows.duplicate.dialog.title") }}</DialogTitle>
|
|
43
44
|
<DialogDescription class="text-sm">
|
|
44
|
-
|
|
45
|
+
{{ t("integration.workflows.duplicate.dialog.description", { name: props.workflow?.name ?? "" }) }}
|
|
45
46
|
</DialogDescription>
|
|
46
47
|
</DialogHeader>
|
|
47
48
|
|
|
48
49
|
<form class="space-y-2" @submit.prevent="submit">
|
|
49
|
-
<label class="text-sm font-medium" for="wf-duplicate-name">
|
|
50
|
+
<label class="text-sm font-medium" for="wf-duplicate-name">{{ t("integration.workflows.duplicate.field.name.label") }}</label>
|
|
50
51
|
<Input id="wf-duplicate-name" v-model="name" autofocus :disabled="busy" />
|
|
51
52
|
<p v-if="error" class="text-sm text-rose-600">{{ error }}</p>
|
|
52
53
|
</form>
|
|
53
54
|
|
|
54
55
|
<DialogFooter>
|
|
55
|
-
<Button variant="outline" size="sm" class="h-8 text-sm" :disabled="busy" @click="open = false">
|
|
56
|
+
<Button variant="outline" size="sm" class="h-8 text-sm" :disabled="busy" @click="open = false">{{ t("integration.common.action.cancel") }}</Button>
|
|
56
57
|
<Button size="sm" class="h-8 text-sm" :disabled="!canSubmit" @click="submit">
|
|
57
58
|
<Loader2 v-if="busy" :size="14" class="animate-spin" />
|
|
58
|
-
|
|
59
|
+
{{ t("integration.workflows.action.duplicate") }}
|
|
59
60
|
</Button>
|
|
60
61
|
</DialogFooter>
|
|
61
62
|
</DialogContent>
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
getSmoothStepPath,
|
|
7
7
|
useVueFlow
|
|
8
8
|
} from "@vue-flow/core";
|
|
9
|
-
import { Plus
|
|
9
|
+
import { Plus } from "lucide-vue-next";
|
|
10
|
+
import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
10
11
|
const props = defineProps({
|
|
11
12
|
id: { type: String, required: true },
|
|
12
13
|
sourceNode: { type: Object, required: true },
|
|
@@ -39,6 +40,7 @@ const props = defineProps({
|
|
|
39
40
|
targetX: { type: Number, required: true },
|
|
40
41
|
targetY: { type: Number, required: true }
|
|
41
42
|
});
|
|
43
|
+
const t = useIntegrationsT();
|
|
42
44
|
const flow = useVueFlow();
|
|
43
45
|
const edgeActions = inject("workflow-edge-actions", null);
|
|
44
46
|
const centerY = computed(() => {
|
|
@@ -145,7 +147,7 @@ function spliceNode() {
|
|
|
145
147
|
<button
|
|
146
148
|
type="button"
|
|
147
149
|
class="flex size-5 items-center justify-center rounded text-muted-foreground transition hover:bg-muted/60 hover:text-foreground"
|
|
148
|
-
title="
|
|
150
|
+
:title="t('integration.canvas.edge.action.insertNode')"
|
|
149
151
|
@click.stop="spliceNode"
|
|
150
152
|
>
|
|
151
153
|
<Plus :size="12" />
|
|
@@ -154,10 +156,10 @@ function spliceNode() {
|
|
|
154
156
|
<button
|
|
155
157
|
type="button"
|
|
156
158
|
class="flex size-5 items-center justify-center rounded text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
|
|
157
|
-
title="
|
|
159
|
+
:title="t('integration.canvas.edge.action.delete')"
|
|
158
160
|
@click.stop="removeEdge"
|
|
159
161
|
>
|
|
160
|
-
<
|
|
162
|
+
<TrashBin :size="12" />
|
|
161
163
|
</button>
|
|
162
164
|
</div>
|
|
163
165
|
</EdgeLabelRenderer>
|