@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
|
@@ -28,13 +28,13 @@ type __VLS_Props = {
|
|
|
28
28
|
refusedFields?: Record<string, string>;
|
|
29
29
|
};
|
|
30
30
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
31
|
-
close: () => any;
|
|
32
31
|
update: (next: WorkflowTrigger | WorkflowNode) => any;
|
|
32
|
+
close: () => any;
|
|
33
33
|
remove: () => any;
|
|
34
34
|
navigate: (id: string) => any;
|
|
35
35
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
36
|
-
onClose?: (() => any) | undefined;
|
|
37
36
|
onUpdate?: ((next: WorkflowTrigger | WorkflowNode) => any) | undefined;
|
|
37
|
+
onClose?: (() => any) | undefined;
|
|
38
38
|
onRemove?: (() => any) | undefined;
|
|
39
39
|
onNavigate?: ((id: string) => any) | undefined;
|
|
40
40
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -3,7 +3,8 @@ import type { PortEntry } from '../../data/nodeInputData.js';
|
|
|
3
3
|
* The ports a node hands downstream, with their declared fields and whatever
|
|
4
4
|
* sample value is known for each.
|
|
5
5
|
*
|
|
6
|
-
* Rendered from one place — the Output tab (specs/node-
|
|
6
|
+
* Rendered from one place — the Output tab (specs/node-configuration.md AC-4,
|
|
7
|
+
* and AC-19 for the sentences the publisher wrote about a port and its fields). It
|
|
7
8
|
* keeps its own file for readability rather than for reuse: the schema list is
|
|
8
9
|
* a self-contained tree with its own type glyphs and sample formatting, and
|
|
9
10
|
* inlining it would bury the tab's own markup.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { portDotClass } from "../../data/portTone";
|
|
3
|
-
|
|
3
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
4
|
+
const props = defineProps({
|
|
4
5
|
ports: { type: Array, required: true },
|
|
5
6
|
valueAt: { type: Function, required: true }
|
|
6
7
|
});
|
|
8
|
+
const t = useIntegrationsT();
|
|
7
9
|
const TYPE_GLYPHS = {
|
|
8
10
|
string: { char: "A", cls: "text-sky-600" },
|
|
9
11
|
number: { char: "#", cls: "text-amber-600" },
|
|
@@ -15,6 +17,15 @@ const TYPE_GLYPHS = {
|
|
|
15
17
|
function typeGlyph(dataType) {
|
|
16
18
|
return dataType ? TYPE_GLYPHS[dataType] ?? null : null;
|
|
17
19
|
}
|
|
20
|
+
function portText(p) {
|
|
21
|
+
return resolveLocalized(p.description);
|
|
22
|
+
}
|
|
23
|
+
function fieldText(f) {
|
|
24
|
+
return resolveLocalized(f.description);
|
|
25
|
+
}
|
|
26
|
+
function saysNoFields(p) {
|
|
27
|
+
return !portText(p) || props.valueAt(p.name) !== null;
|
|
28
|
+
}
|
|
18
29
|
</script>
|
|
19
30
|
|
|
20
31
|
<template>
|
|
@@ -24,19 +35,35 @@ function typeGlyph(dataType) {
|
|
|
24
35
|
<span class="size-1.5 rounded-full" :class="portDotClass(p.kind, p.name)" />
|
|
25
36
|
<span class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{{ p.name }}</span>
|
|
26
37
|
</div>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
<p v-if="portText(p)" class="text-[11px] leading-snug text-muted-foreground">
|
|
39
|
+
{{ portText(p) }}
|
|
40
|
+
</p>
|
|
41
|
+
<!-- Two columns, sized to the widest field rather than split down the
|
|
42
|
+
middle: `max-content` puts every sentence one gap after the longest
|
|
43
|
+
name, so short names like `count` do not leave the reader crossing the
|
|
44
|
+
dialog to find what they mean, and the names still line up with each
|
|
45
|
+
other. The sample value is capped for the same reason — left free it
|
|
46
|
+
would widen the column and push the sentences back out. Both cells are
|
|
47
|
+
always emitted, or a field with nothing to say would slide the next
|
|
48
|
+
row's name into the sentence column. -->
|
|
49
|
+
<div v-if="p.fields.length" class="grid grid-cols-[max-content_minmax(0,1fr)] items-baseline gap-x-6 gap-y-0.5">
|
|
50
|
+
<template v-for="f in p.fields" :key="f.name">
|
|
51
|
+
<div class="flex min-w-0 items-baseline gap-1 font-mono text-[11px]">
|
|
52
|
+
<span v-if="typeGlyph(f.dataType)" class="shrink-0 font-semibold" :class="typeGlyph(f.dataType).cls">{{ typeGlyph(f.dataType).char }}</span>
|
|
53
|
+
<span class="shrink-0 text-slate-700">{{ f.name }}</span>
|
|
54
|
+
<span v-if="valueAt(p.name, f.name) !== null" class="max-w-56 truncate text-slate-400">: {{ valueAt(p.name, f.name) }}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<p class="min-w-0 text-[11px] leading-snug text-muted-foreground">
|
|
57
|
+
{{ fieldText(f) }}
|
|
58
|
+
</p>
|
|
59
|
+
</template>
|
|
33
60
|
</div>
|
|
34
|
-
<p v-else class="text-[11px] leading-snug text-muted-foreground">
|
|
35
|
-
|
|
61
|
+
<p v-else-if="saysNoFields(p)" class="text-[11px] leading-snug text-muted-foreground">
|
|
62
|
+
{{ t("integration.nodeInspector.output.schema.noFields") }}<template v-if="valueAt(p.name) !== null">{{ t("integration.nodeInspector.output.schema.sampledSuffix") }} <span class="font-mono">{{ valueAt(p.name) }}</span></template>.
|
|
36
63
|
</p>
|
|
37
64
|
</div>
|
|
38
65
|
<p v-if="!ports.length" class="text-xs leading-snug text-muted-foreground">
|
|
39
|
-
|
|
66
|
+
{{ t("integration.nodeInspector.output.schema.noOutputs") }}
|
|
40
67
|
</p>
|
|
41
68
|
</div>
|
|
42
69
|
</template>
|
|
@@ -3,7 +3,8 @@ import type { PortEntry } from '../../data/nodeInputData.js';
|
|
|
3
3
|
* The ports a node hands downstream, with their declared fields and whatever
|
|
4
4
|
* sample value is known for each.
|
|
5
5
|
*
|
|
6
|
-
* Rendered from one place — the Output tab (specs/node-
|
|
6
|
+
* Rendered from one place — the Output tab (specs/node-configuration.md AC-4,
|
|
7
|
+
* and AC-19 for the sentences the publisher wrote about a port and its fields). It
|
|
7
8
|
* keeps its own file for readability rather than for reuse: the schema list is
|
|
8
9
|
* a self-contained tree with its own type glyphs and sample formatting, and
|
|
9
10
|
* inlining it would bury the tab's own markup.
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { AltArrowLeft as ArrowLeft, Bolt as Zap, Magnifier as Search } from "#solar-icons";
|
|
4
|
+
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
3
5
|
import * as Lucide from "lucide-vue-next";
|
|
4
6
|
import { INTEGRATION_NODE_CATALOG, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
5
|
-
import { nodeIcon } from "../../data/nodeIcons";
|
|
6
7
|
import { resolveLocalized } from "../../utils/localize";
|
|
7
8
|
const { load: loadNodes } = useIntegrationsNodes();
|
|
8
9
|
onMounted(() => {
|
|
9
10
|
void loadNodes();
|
|
10
11
|
});
|
|
12
|
+
const t = useIntegrationsT();
|
|
11
13
|
const emit = defineEmits(["insert"]);
|
|
12
14
|
const search = ref("");
|
|
13
15
|
const searchInput = ref(null);
|
|
14
16
|
defineExpose({ focusSearch: () => searchInput.value?.focus() });
|
|
15
|
-
const iconFor = nodeIcon;
|
|
16
17
|
const CORE_GROUP_BY_CATEGORY = {
|
|
17
18
|
control: "Flow",
|
|
18
19
|
transform: "Data",
|
|
@@ -75,11 +76,14 @@ function isIntegration(m) {
|
|
|
75
76
|
function makeGroup(label) {
|
|
76
77
|
return { label, children: /* @__PURE__ */ new Map(), leaves: [], count: 0 };
|
|
77
78
|
}
|
|
79
|
+
function isExecutable(m) {
|
|
80
|
+
return m.__hasBundle !== false;
|
|
81
|
+
}
|
|
78
82
|
const latestManifests = computed(() => {
|
|
79
83
|
const seen = /* @__PURE__ */ new Set();
|
|
80
84
|
const out = [];
|
|
81
85
|
for (const m of INTEGRATION_NODE_CATALOG) {
|
|
82
|
-
if (seen.has(m.slug)) continue;
|
|
86
|
+
if (seen.has(m.slug) || !isExecutable(m)) continue;
|
|
83
87
|
seen.add(m.slug);
|
|
84
88
|
out.push(m);
|
|
85
89
|
}
|
|
@@ -88,6 +92,7 @@ const latestManifests = computed(() => {
|
|
|
88
92
|
const versionsBySlug = computed(() => {
|
|
89
93
|
const map = /* @__PURE__ */ new Map();
|
|
90
94
|
for (const m of INTEGRATION_NODE_CATALOG) {
|
|
95
|
+
if (!isExecutable(m)) continue;
|
|
91
96
|
const list = map.get(m.slug);
|
|
92
97
|
if (list) list.push(m);
|
|
93
98
|
else map.set(m.slug, [m]);
|
|
@@ -160,8 +165,8 @@ const currentGroup = computed(() => {
|
|
|
160
165
|
return cursor ?? null;
|
|
161
166
|
});
|
|
162
167
|
const levelTitle = computed(() => {
|
|
163
|
-
if (triggersLevel.value) return "
|
|
164
|
-
return currentGroup.value?.label ?? "
|
|
168
|
+
if (triggersLevel.value) return t("integration.canvas.palette.group.triggers");
|
|
169
|
+
return currentGroup.value?.label ?? t("integration.canvas.palette.title.addNode");
|
|
165
170
|
});
|
|
166
171
|
function pushPath(path) {
|
|
167
172
|
direction.value = "push";
|
|
@@ -194,7 +199,7 @@ const searchHits = computed(() => {
|
|
|
194
199
|
const matchedTriggers = computed(() => {
|
|
195
200
|
const q = search.value.trim().toLowerCase();
|
|
196
201
|
if (!q || currentGroup.value) return [];
|
|
197
|
-
const matches = (
|
|
202
|
+
const matches = (tpl) => resolveLocalized(tpl.name).toLowerCase().includes(q) || tpl.type.includes(q) || resolveLocalized(tpl.description).toLowerCase().includes(q);
|
|
198
203
|
return TRIGGER_TEMPLATES.filter(matches);
|
|
199
204
|
});
|
|
200
205
|
const nodeDrag = useNodeDrag();
|
|
@@ -278,23 +283,23 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
278
283
|
<button
|
|
279
284
|
type="button"
|
|
280
285
|
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
281
|
-
title="
|
|
286
|
+
:title="t('integration.canvas.palette.action.back')"
|
|
282
287
|
@click="popLevel"
|
|
283
288
|
>
|
|
284
|
-
<
|
|
289
|
+
<ArrowLeft :size="14" />
|
|
285
290
|
</button>
|
|
286
291
|
<div class="min-w-0 flex-1 truncate text-mono text-muted-foreground">
|
|
287
292
|
{{ levelTitle }}
|
|
288
293
|
</div>
|
|
289
294
|
</div>
|
|
290
295
|
<div class="relative">
|
|
291
|
-
<
|
|
296
|
+
<Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
292
297
|
<input
|
|
293
298
|
ref="searchInput"
|
|
294
299
|
v-model="search"
|
|
295
300
|
type="text"
|
|
296
|
-
:aria-label="stack.length ?
|
|
297
|
-
:placeholder="stack.length ?
|
|
301
|
+
:aria-label="stack.length ? t('integration.canvas.palette.search.ariaGroup', { group: levelTitle }) : t('integration.canvas.palette.search.ariaAll')"
|
|
302
|
+
:placeholder="stack.length ? t('integration.canvas.palette.search.placeholderGroup', { group: levelTitle }) : t('integration.canvas.palette.search.placeholderAll')"
|
|
298
303
|
class="h-7 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"
|
|
299
304
|
@keydown.esc="search ? search = '' : stack.length && popLevel()"
|
|
300
305
|
>
|
|
@@ -321,28 +326,28 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
321
326
|
<!-- ── Search results (flat, path as subtitle) ─────────────────── -->
|
|
322
327
|
<template v-if="searching">
|
|
323
328
|
<div v-if="matchedTriggers.length" class="mb-1">
|
|
324
|
-
<div class="px-2 py-1 text-mono text-muted-foreground">
|
|
329
|
+
<div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.triggers") }}</div>
|
|
325
330
|
<div
|
|
326
|
-
v-for="
|
|
327
|
-
:key="
|
|
331
|
+
v-for="tpl in matchedTriggers"
|
|
332
|
+
:key="tpl.id"
|
|
328
333
|
class="group flex cursor-grab items-center gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
|
|
329
334
|
draggable="true"
|
|
330
|
-
@dragstart="onTriggerDragStart(
|
|
335
|
+
@dragstart="onTriggerDragStart(tpl, $event)"
|
|
331
336
|
@dragend="onDragEnd"
|
|
332
|
-
@click="insertTrigger(
|
|
337
|
+
@click="insertTrigger(tpl)"
|
|
333
338
|
>
|
|
334
339
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
335
|
-
<component :is="
|
|
340
|
+
<component :is="triggerGlyph(tpl.type)" :size="13" />
|
|
336
341
|
</span>
|
|
337
342
|
<div class="min-w-0">
|
|
338
|
-
<div class="truncate text-sm font-medium">{{ resolveLocalized(
|
|
339
|
-
<div class="text-xs leading-snug text-muted-foreground">
|
|
343
|
+
<div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
|
|
344
|
+
<div class="text-xs leading-snug text-muted-foreground">{{ t("integration.canvas.badge.trigger") }}</div>
|
|
340
345
|
</div>
|
|
341
346
|
</div>
|
|
342
347
|
</div>
|
|
343
348
|
|
|
344
349
|
<p v-if="!searchHits.length && !matchedTriggers.length" class="px-2 py-6 text-center text-sm text-muted-foreground">
|
|
345
|
-
|
|
350
|
+
{{ t("integration.canvas.palette.search.empty", { query: search }) }}
|
|
346
351
|
</p>
|
|
347
352
|
<div
|
|
348
353
|
v-for="hit in searchHits"
|
|
@@ -364,7 +369,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
364
369
|
v-if="versionsOf(hit.manifest.slug).length > 1"
|
|
365
370
|
:value="effVersion(hit.manifest)"
|
|
366
371
|
class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
|
|
367
|
-
:aria-label="
|
|
372
|
+
:aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(hit.manifest.name) })"
|
|
368
373
|
@click.stop
|
|
369
374
|
@mousedown.stop
|
|
370
375
|
@change.stop="setSelectedVersion(hit.manifest.slug, $event.target.value)"
|
|
@@ -386,21 +391,21 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
386
391
|
<!-- ── Triggers level ──────────────────────────────────────────── -->
|
|
387
392
|
<template v-else-if="triggersLevel">
|
|
388
393
|
<div
|
|
389
|
-
v-for="
|
|
390
|
-
:key="
|
|
394
|
+
v-for="tpl in TRIGGER_TEMPLATES"
|
|
395
|
+
:key="tpl.id"
|
|
391
396
|
class="group flex cursor-grab items-center gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
|
|
392
397
|
draggable="true"
|
|
393
|
-
:title="resolveLocalized(
|
|
394
|
-
@dragstart="onTriggerDragStart(
|
|
398
|
+
:title="resolveLocalized(tpl.name)"
|
|
399
|
+
@dragstart="onTriggerDragStart(tpl, $event)"
|
|
395
400
|
@dragend="onDragEnd"
|
|
396
|
-
@click="insertTrigger(
|
|
401
|
+
@click="insertTrigger(tpl)"
|
|
397
402
|
>
|
|
398
403
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
399
|
-
<component :is="
|
|
404
|
+
<component :is="triggerGlyph(tpl.type)" :size="13" />
|
|
400
405
|
</span>
|
|
401
406
|
<div class="min-w-0">
|
|
402
|
-
<div class="truncate text-sm font-medium">{{ resolveLocalized(
|
|
403
|
-
<div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(
|
|
407
|
+
<div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
|
|
408
|
+
<div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(tpl.description) }}</div>
|
|
404
409
|
</div>
|
|
405
410
|
</div>
|
|
406
411
|
</template>
|
|
@@ -409,16 +414,16 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
409
414
|
<template v-else-if="!currentGroup">
|
|
410
415
|
<!-- Triggers group on top -->
|
|
411
416
|
<div v-if="tree.core.children.size">
|
|
412
|
-
<div class="px-2 py-1 text-mono text-muted-foreground">
|
|
417
|
+
<div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.core") }}</div>
|
|
413
418
|
<button
|
|
414
419
|
type="button"
|
|
415
420
|
class="flex w-full items-center gap-2 rounded-md border border-transparent px-2 py-1.5 text-left transition hover:border-black/[0.08] hover:bg-muted/40"
|
|
416
421
|
@click="pushPath(['__triggers'])"
|
|
417
422
|
>
|
|
418
423
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-emerald-600/80">
|
|
419
|
-
<
|
|
424
|
+
<Zap :size="13" />
|
|
420
425
|
</span>
|
|
421
|
-
<span class="min-w-0 flex-1 truncate text-sm font-medium">
|
|
426
|
+
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ t("integration.canvas.palette.group.triggers") }}</span>
|
|
422
427
|
<span class="text-sm tabular-nums text-muted-foreground">{{ TRIGGER_TEMPLATES.length }}</span>
|
|
423
428
|
<Lucide.ChevronRight :size="13" class="text-muted-foreground" />
|
|
424
429
|
</button>
|
|
@@ -439,7 +444,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
439
444
|
</div>
|
|
440
445
|
|
|
441
446
|
<div v-if="tree.apps.children.size" class="mt-2">
|
|
442
|
-
<div class="px-2 py-1 text-mono text-muted-foreground">
|
|
447
|
+
<div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.integrations") }}</div>
|
|
443
448
|
<button
|
|
444
449
|
v-for="[label, group] in appRows"
|
|
445
450
|
:key="`app-${label}`"
|
|
@@ -499,7 +504,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
499
504
|
v-if="versionsOf(m.slug).length > 1"
|
|
500
505
|
:value="effVersion(m)"
|
|
501
506
|
class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
|
|
502
|
-
:aria-label="
|
|
507
|
+
:aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(m.name) })"
|
|
503
508
|
@click.stop
|
|
504
509
|
@mousedown.stop
|
|
505
510
|
@change.stop="setSelectedVersion(m.slug, $event.target.value)"
|
|
@@ -521,7 +526,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
521
526
|
</div>
|
|
522
527
|
|
|
523
528
|
<div class="border-t border-black/[0.06] px-3 py-2 text-sm text-muted-foreground">
|
|
524
|
-
|
|
529
|
+
{{ t("integration.canvas.palette.footer.hint") }}
|
|
525
530
|
</div>
|
|
526
531
|
</aside>
|
|
527
532
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
-
import { AddCircle as Plus, TrashBinMinimalistic as
|
|
3
|
+
import { AddCircle as Plus, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
4
4
|
import {
|
|
5
5
|
Button,
|
|
6
6
|
Input,
|
|
@@ -15,27 +15,28 @@ const props = defineProps({
|
|
|
15
15
|
modelValue: { type: null, required: true }
|
|
16
16
|
});
|
|
17
17
|
const emit = defineEmits(["update:modelValue"]);
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
{ value: "
|
|
21
|
-
{ value: "
|
|
22
|
-
{ value: "
|
|
23
|
-
{ value: "
|
|
24
|
-
{ value: "
|
|
25
|
-
{ value: "
|
|
26
|
-
{ value: "
|
|
27
|
-
{ value: "
|
|
28
|
-
{ value: "
|
|
29
|
-
{ value: "
|
|
30
|
-
{ value: "
|
|
31
|
-
{ value: "
|
|
32
|
-
{ value: "
|
|
33
|
-
{ value: "
|
|
34
|
-
{ value: "
|
|
35
|
-
{ value: "
|
|
36
|
-
|
|
18
|
+
const t = useIntegrationsT();
|
|
19
|
+
const METHODS = computed(() => [
|
|
20
|
+
{ value: "equal", label: t("integration.nodeConfig.queryBuilder.method.equal"), attribute: true, values: "list" },
|
|
21
|
+
{ value: "notEqual", label: t("integration.nodeConfig.queryBuilder.method.notEqual"), attribute: true, values: "list" },
|
|
22
|
+
{ value: "contains", label: t("integration.nodeConfig.queryBuilder.method.contains"), attribute: true, values: "list" },
|
|
23
|
+
{ value: "search", label: t("integration.nodeConfig.queryBuilder.method.search"), attribute: true, values: "single" },
|
|
24
|
+
{ value: "startsWith", label: t("integration.nodeConfig.queryBuilder.method.startsWith"), attribute: true, values: "single" },
|
|
25
|
+
{ value: "endsWith", label: t("integration.nodeConfig.queryBuilder.method.endsWith"), attribute: true, values: "single" },
|
|
26
|
+
{ value: "lessThan", label: t("integration.nodeConfig.queryBuilder.method.lessThan"), attribute: true, values: "single" },
|
|
27
|
+
{ value: "lessThanEqual", label: t("integration.nodeConfig.queryBuilder.method.lessThanEqual"), attribute: true, values: "single" },
|
|
28
|
+
{ value: "greaterThan", label: t("integration.nodeConfig.queryBuilder.method.greaterThan"), attribute: true, values: "single" },
|
|
29
|
+
{ value: "greaterThanEqual", label: t("integration.nodeConfig.queryBuilder.method.greaterThanEqual"), attribute: true, values: "single" },
|
|
30
|
+
{ value: "between", label: t("integration.nodeConfig.queryBuilder.method.between"), attribute: true, values: "list" },
|
|
31
|
+
{ value: "isNull", label: t("integration.nodeConfig.queryBuilder.method.isNull"), attribute: true, values: "none" },
|
|
32
|
+
{ value: "isNotNull", label: t("integration.nodeConfig.queryBuilder.method.isNotNull"), attribute: true, values: "none" },
|
|
33
|
+
{ value: "orderAsc", label: t("integration.nodeConfig.queryBuilder.method.orderAsc"), attribute: true, values: "none" },
|
|
34
|
+
{ value: "orderDesc", label: t("integration.nodeConfig.queryBuilder.method.orderDesc"), attribute: true, values: "none" },
|
|
35
|
+
{ value: "limit", label: t("integration.nodeConfig.queryBuilder.method.limit"), attribute: false, values: "single" },
|
|
36
|
+
{ value: "offset", label: t("integration.nodeConfig.queryBuilder.method.offset"), attribute: false, values: "single" }
|
|
37
|
+
]);
|
|
37
38
|
function methodMeta(method) {
|
|
38
|
-
return METHODS.find((m) => m.value === method) ?? null;
|
|
39
|
+
return METHODS.value.find((m) => m.value === method) ?? null;
|
|
39
40
|
}
|
|
40
41
|
function formatValue(v) {
|
|
41
42
|
return typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -57,16 +58,16 @@ function parseRow(str) {
|
|
|
57
58
|
return { kind: "raw", text: str };
|
|
58
59
|
}
|
|
59
60
|
function coerceValue(part) {
|
|
60
|
-
const
|
|
61
|
-
if (/^-?\d+(\.\d+)?$/.test(
|
|
62
|
-
if (
|
|
63
|
-
if (
|
|
64
|
-
return
|
|
61
|
+
const t2 = part.trim();
|
|
62
|
+
if (/^-?\d+(\.\d+)?$/.test(t2)) return Number(t2);
|
|
63
|
+
if (t2 === "true") return true;
|
|
64
|
+
if (t2 === "false") return false;
|
|
65
|
+
return t2;
|
|
65
66
|
}
|
|
66
67
|
function rowToString(row) {
|
|
67
68
|
if (row.kind === "raw") {
|
|
68
|
-
const
|
|
69
|
-
return
|
|
69
|
+
const t2 = row.text.trim();
|
|
70
|
+
return t2 === "" ? null : t2;
|
|
70
71
|
}
|
|
71
72
|
const meta = methodMeta(row.method);
|
|
72
73
|
if (!meta) return null;
|
|
@@ -131,13 +132,13 @@ function writeJson(text) {
|
|
|
131
132
|
try {
|
|
132
133
|
const parsed = JSON.parse(text);
|
|
133
134
|
if (!Array.isArray(parsed)) {
|
|
134
|
-
jsonError.value = "
|
|
135
|
+
jsonError.value = t("integration.nodeConfig.queryBuilder.json.notArray");
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
137
138
|
jsonError.value = null;
|
|
138
139
|
emit("update:modelValue", parsed.map(String));
|
|
139
140
|
} catch {
|
|
140
|
-
jsonError.value = "
|
|
141
|
+
jsonError.value = t("integration.nodeConfig.json.invalid");
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? methodMeta(r.method) : null));
|
|
@@ -153,7 +154,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
153
154
|
:class="mode === 'builder' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
|
154
155
|
@click="mode = 'builder'"
|
|
155
156
|
>
|
|
156
|
-
|
|
157
|
+
{{ t("integration.nodeConfig.queryBuilder.mode.builder") }}
|
|
157
158
|
</button>
|
|
158
159
|
<button
|
|
159
160
|
type="button"
|
|
@@ -161,7 +162,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
161
162
|
:class="mode === 'json' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
|
162
163
|
@click="mode = 'json'"
|
|
163
164
|
>
|
|
164
|
-
|
|
165
|
+
{{ t("integration.nodeConfig.object.toggle.json") }}
|
|
165
166
|
</button>
|
|
166
167
|
</div>
|
|
167
168
|
</div>
|
|
@@ -175,7 +176,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
175
176
|
>
|
|
176
177
|
<!-- A repeating row has no room for visible labels, so each
|
|
177
178
|
control names itself and says which row it is. -->
|
|
178
|
-
<SelectTrigger :aria-label="
|
|
179
|
+
<SelectTrigger :aria-label="t('integration.nodeConfig.queryBuilder.row.comparisonAria', { n: idx + 1 })" class="h-7 w-40 shrink-0 bg-white text-sm">
|
|
179
180
|
<SelectValue />
|
|
180
181
|
</SelectTrigger>
|
|
181
182
|
<SelectContent>
|
|
@@ -186,17 +187,17 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
186
187
|
</Select>
|
|
187
188
|
<Input
|
|
188
189
|
v-if="rowMeta[idx]?.attribute"
|
|
189
|
-
:aria-label="
|
|
190
|
+
:aria-label="t('integration.nodeConfig.queryBuilder.row.fieldAria', { n: idx + 1 })"
|
|
190
191
|
:model-value="row.attribute"
|
|
191
|
-
placeholder="
|
|
192
|
+
:placeholder="t('integration.nodeConfig.queryBuilder.row.fieldPlaceholder')"
|
|
192
193
|
class="h-7 w-36 shrink-0 bg-white font-mono text-sm"
|
|
193
194
|
@update:model-value="(v) => setRow(idx, { attribute: String(v) })"
|
|
194
195
|
/>
|
|
195
196
|
<Input
|
|
196
197
|
v-if="rowMeta[idx]?.values !== 'none'"
|
|
197
|
-
:aria-label="
|
|
198
|
+
:aria-label="t('integration.nodeConfig.queryBuilder.row.valueAria', { n: idx + 1 })"
|
|
198
199
|
:model-value="row.values"
|
|
199
|
-
:placeholder="rowMeta[idx]?.values === 'list' ? '
|
|
200
|
+
:placeholder="rowMeta[idx]?.values === 'list' ? t('integration.nodeConfig.queryBuilder.row.valueListPlaceholder') : t('integration.nodeConfig.queryBuilder.row.valuePlaceholder')"
|
|
200
201
|
class="h-7 min-w-0 flex-1 bg-white text-sm"
|
|
201
202
|
@update:model-value="(v) => setRow(idx, { values: String(v) })"
|
|
202
203
|
/>
|
|
@@ -204,19 +205,19 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
204
205
|
</template>
|
|
205
206
|
<Input
|
|
206
207
|
v-else
|
|
207
|
-
:aria-label="
|
|
208
|
+
:aria-label="t('integration.nodeConfig.queryBuilder.row.rawTextAria', { n: idx + 1 })"
|
|
208
209
|
:model-value="row.text"
|
|
209
210
|
class="h-7 min-w-0 flex-1 bg-white font-mono text-sm"
|
|
210
|
-
title="
|
|
211
|
+
:title="t('integration.nodeConfig.queryBuilder.row.rawTextTitle')"
|
|
211
212
|
@update:model-value="(v) => setRow(idx, { text: String(v) })"
|
|
212
213
|
/>
|
|
213
214
|
<button
|
|
214
215
|
type="button"
|
|
215
|
-
:aria-label="
|
|
216
|
+
:aria-label="t('integration.nodeConfig.queryBuilder.row.removeAria', { n: idx + 1 })"
|
|
216
217
|
class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
|
|
217
218
|
@click="removeRow(idx)"
|
|
218
219
|
>
|
|
219
|
-
<
|
|
220
|
+
<TrashBin :size="11" />
|
|
220
221
|
</button>
|
|
221
222
|
</div>
|
|
222
223
|
<Button
|
|
@@ -227,13 +228,13 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
227
228
|
@click="addRow"
|
|
228
229
|
>
|
|
229
230
|
<Plus :size="11" />
|
|
230
|
-
|
|
231
|
+
{{ t("integration.nodeConfig.queryBuilder.action.addFilter") }}
|
|
231
232
|
</Button>
|
|
232
233
|
</template>
|
|
233
234
|
|
|
234
235
|
<template v-else>
|
|
235
236
|
<Textarea
|
|
236
|
-
aria-label="
|
|
237
|
+
:aria-label="t('integration.nodeConfig.queryBuilder.jsonAria')"
|
|
237
238
|
:model-value="jsonDraft"
|
|
238
239
|
:rows="6"
|
|
239
240
|
spellcheck="false"
|
|
@@ -14,6 +14,7 @@ const props = defineProps({
|
|
|
14
14
|
run: { type: Object, required: true }
|
|
15
15
|
});
|
|
16
16
|
const emit = defineEmits(["changed"]);
|
|
17
|
+
const t = useIntegrationsT();
|
|
17
18
|
const router = useRouter();
|
|
18
19
|
const { cancelRun, terminateRun, retryRun, resumeRun } = useIntegrationsRuns();
|
|
19
20
|
function viewDetails() {
|
|
@@ -26,8 +27,31 @@ const isActive = computed(() => props.run.status === "running" || props.run.stat
|
|
|
26
27
|
const isTerminal = computed(() => ["succeeded", "failed", "cancelled"].includes(props.run.status));
|
|
27
28
|
const canResume = computed(() => props.run.status === "failed");
|
|
28
29
|
const hasAny = computed(() => isActive.value || isTerminal.value);
|
|
29
|
-
const LABEL = { cancel: "Cancel", terminate: "Terminate", retry: "Retry", resume: "Resume" };
|
|
30
30
|
const busy = ref(false);
|
|
31
|
+
function actionLabel(a) {
|
|
32
|
+
switch (a) {
|
|
33
|
+
case "cancel":
|
|
34
|
+
return t("integration.runs.menu.cancel");
|
|
35
|
+
case "terminate":
|
|
36
|
+
return t("integration.runs.menu.terminate");
|
|
37
|
+
case "retry":
|
|
38
|
+
return t("integration.runs.menu.retry");
|
|
39
|
+
case "resume":
|
|
40
|
+
return t("integration.runs.menu.resume");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function actionErrorFallback(a) {
|
|
44
|
+
switch (a) {
|
|
45
|
+
case "cancel":
|
|
46
|
+
return t("integration.runs.error.cancel");
|
|
47
|
+
case "terminate":
|
|
48
|
+
return t("integration.runs.error.terminate");
|
|
49
|
+
case "retry":
|
|
50
|
+
return t("integration.runs.error.retry");
|
|
51
|
+
case "resume":
|
|
52
|
+
return t("integration.runs.error.resume");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
31
55
|
async function runAction(a) {
|
|
32
56
|
if (busy.value) return;
|
|
33
57
|
busy.value = true;
|
|
@@ -49,18 +73,18 @@ async function runAction(a) {
|
|
|
49
73
|
}
|
|
50
74
|
const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
|
|
51
75
|
toast.success(
|
|
52
|
-
(a === "retry" || a === "resume") && newId && newId !== String(props.run.id) ?
|
|
76
|
+
(a === "retry" || a === "resume") && newId && newId !== String(props.run.id) ? t("integration.runs.toast.action.started", { id: newId }) : t("integration.runs.toast.action.requested", { action: actionLabel(a) })
|
|
53
77
|
);
|
|
54
78
|
emit("changed");
|
|
55
79
|
} catch (e) {
|
|
56
|
-
toast.error(e?.message ??
|
|
80
|
+
toast.error(e?.message ?? actionErrorFallback(a));
|
|
57
81
|
} finally {
|
|
58
82
|
busy.value = false;
|
|
59
83
|
}
|
|
60
84
|
}
|
|
61
85
|
function confirmTerminate() {
|
|
62
|
-
toast(
|
|
63
|
-
action: { label: "
|
|
86
|
+
toast(t("integration.runs.toast.terminate.confirm", { id: props.run.id }), {
|
|
87
|
+
action: { label: actionLabel("terminate"), onClick: () => void runAction("terminate") }
|
|
64
88
|
});
|
|
65
89
|
}
|
|
66
90
|
</script>
|
|
@@ -70,19 +94,19 @@ function confirmTerminate() {
|
|
|
70
94
|
<DropdownMenuTrigger as-child>
|
|
71
95
|
<!-- Icon-only, so it needs a name of its own — the run id is the only
|
|
72
96
|
thing that tells two rows' menus apart. -->
|
|
73
|
-
<Button variant="ghost" size="icon" class="size-7" :disabled="busy" :aria-label="
|
|
97
|
+
<Button variant="ghost" size="icon" class="size-7" :disabled="busy" :aria-label="t('integration.runs.menu.aria.actions', { id: run.id })" @click.stop>
|
|
74
98
|
<MoreHorizontal :size="16" />
|
|
75
99
|
</Button>
|
|
76
100
|
</DropdownMenuTrigger>
|
|
77
101
|
<DropdownMenuContent align="end" class="w-40" @click.stop>
|
|
78
|
-
<DropdownMenuItem @select="viewDetails">
|
|
79
|
-
<DropdownMenuItem @select="viewReplay">
|
|
102
|
+
<DropdownMenuItem @select="viewDetails">{{ t("integration.runs.menu.viewDetails") }}</DropdownMenuItem>
|
|
103
|
+
<DropdownMenuItem @select="viewReplay">{{ t("integration.runs.menu.replay") }}</DropdownMenuItem>
|
|
80
104
|
<DropdownMenuSeparator v-if="hasAny" />
|
|
81
|
-
<DropdownMenuItem v-if="isActive" @select="runAction('cancel')">
|
|
82
|
-
<DropdownMenuItem v-if="isActive" class="text-rose-600" @select="confirmTerminate">
|
|
105
|
+
<DropdownMenuItem v-if="isActive" @select="runAction('cancel')">{{ actionLabel("cancel") }}</DropdownMenuItem>
|
|
106
|
+
<DropdownMenuItem v-if="isActive" class="text-rose-600" @select="confirmTerminate">{{ actionLabel("terminate") }}</DropdownMenuItem>
|
|
83
107
|
<DropdownMenuSeparator v-if="isActive && (canResume || isTerminal)" />
|
|
84
|
-
<DropdownMenuItem v-if="canResume" @select="runAction('resume')">
|
|
85
|
-
<DropdownMenuItem v-if="isTerminal" @select="runAction('retry')">
|
|
108
|
+
<DropdownMenuItem v-if="canResume" @select="runAction('resume')">{{ actionLabel("resume") }}</DropdownMenuItem>
|
|
109
|
+
<DropdownMenuItem v-if="isTerminal" @select="runAction('retry')">{{ actionLabel("retry") }}</DropdownMenuItem>
|
|
86
110
|
</DropdownMenuContent>
|
|
87
111
|
</DropdownMenu>
|
|
88
112
|
</template>
|