@revenexx/studio-integrations 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +581 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +15 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue +158 -49
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +15 -1
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/NodeInspector.vue +136 -81
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +200 -54
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
- package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
- package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
- package/dist/runtime/composables/useCredentialTypes.js +3 -9
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
- package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
- package/dist/runtime/composables/useIntegrationsTemplates.js +10 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
- package/dist/runtime/composables/useIntegrationsWorkflows.js +11 -7
- package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
- package/dist/runtime/data/canvasShortcuts.js +68 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
- package/dist/runtime/data/integrationNodeCatalog.js +8 -0
- package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
- package/dist/runtime/data/nodeVersionMove.js +65 -0
- package/dist/runtime/i18n/messages/de.json +83 -20
- package/dist/runtime/i18n/messages/en.json +83 -20
- package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
- package/dist/runtime/pages/integrations/workflows/[id].vue +158 -53
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
- package/dist/runtime/types/integrations.d.ts +44 -1
- package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
- package/dist/runtime/utils/apiEnvelope.js +20 -0
- package/dist/runtime/utils/buildSettle.d.ts +68 -0
- package/dist/runtime/utils/buildSettle.js +29 -0
- package/dist/runtime/utils/configDefaults.d.ts +18 -0
- package/dist/runtime/utils/configDefaults.js +7 -0
- package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
- package/dist/runtime/utils/errorPathAdvice.js +19 -0
- package/dist/runtime/utils/notify.d.ts +2 -2
- package/dist/runtime/utils/runEnvelope.d.ts +12 -0
- package/dist/runtime/utils/runEnvelope.js +2 -1
- package/dist/runtime/utils/selectOptions.d.ts +48 -0
- package/dist/runtime/utils/selectOptions.js +19 -0
- package/dist/runtime/utils/settingCondition.d.ts +48 -0
- package/dist/runtime/utils/settingCondition.js +84 -0
- package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
- package/dist/runtime/utils/shortcutLabel.js +48 -0
- package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
- package/dist/runtime/utils/stateNamespaceName.js +8 -0
- package/package.json +1 -1
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
RefreshCircle as Loader2
|
|
15
15
|
} from "#solar-icons";
|
|
16
16
|
import { Badge, Button } from "@revenexx/studio";
|
|
17
|
-
import { toast } from "vue-sonner";
|
|
18
17
|
import "@vue-flow/core/dist/style.css";
|
|
19
18
|
import "@vue-flow/core/dist/theme-default.css";
|
|
20
19
|
import WorkflowNodeView from "./WorkflowNodeView.vue";
|
|
@@ -27,7 +26,6 @@ import { findManifest } from "../../data/integrationNodeCatalog";
|
|
|
27
26
|
import { OPPOSITE_SIDE } from "./canvasSides";
|
|
28
27
|
import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
29
28
|
import { nodeLoopData } from "../../data/loopBody";
|
|
30
|
-
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
31
29
|
import { formatDuration } from "../../utils/formatDuration";
|
|
32
30
|
import { SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
33
31
|
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
@@ -37,7 +35,7 @@ const props = defineProps({
|
|
|
37
35
|
initialRunId: { type: [String, null], required: false },
|
|
38
36
|
single: { type: Boolean, required: false }
|
|
39
37
|
});
|
|
40
|
-
const {
|
|
38
|
+
const { getSteps } = useIntegrationsRuns();
|
|
41
39
|
const t = useIntegrationsT();
|
|
42
40
|
const TERMINAL = /* @__PURE__ */ new Set(["completed", "succeeded", "failed", "cancelled", "terminated"]);
|
|
43
41
|
const STEPS_POLL_MS = 4e3;
|
|
@@ -46,33 +44,7 @@ const { delivering: realtimeDelivering, runTick, lastRunEvent } = useIntegration
|
|
|
46
44
|
function stepsPollDelay() {
|
|
47
45
|
return realtimeDelivering.value ? SAFETY_POLL_MS : STEPS_POLL_MS;
|
|
48
46
|
}
|
|
49
|
-
const runs = ref([]);
|
|
50
|
-
const runsPage = ref(1);
|
|
51
|
-
const runsLastPage = ref(1);
|
|
52
|
-
const runsLoading = ref(false);
|
|
53
47
|
const selectedRunId = ref(null);
|
|
54
|
-
async function loadRuns(reset = false) {
|
|
55
|
-
if (runsLoading.value) return;
|
|
56
|
-
runsLoading.value = true;
|
|
57
|
-
try {
|
|
58
|
-
const res = await loadForWorkflow(props.workflow.id, props.workflow.name, {
|
|
59
|
-
page: reset ? 1 : runsPage.value,
|
|
60
|
-
reconcile: true
|
|
61
|
-
});
|
|
62
|
-
runs.value = reset ? res.runs : [...runs.value, ...res.runs];
|
|
63
|
-
runsPage.value = res.page;
|
|
64
|
-
runsLastPage.value = res.lastPage;
|
|
65
|
-
} catch (e) {
|
|
66
|
-
toast.error(errorReason(e, t("integration.replay.toast.loadRunsFailed")));
|
|
67
|
-
} finally {
|
|
68
|
-
runsLoading.value = false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
function loadMoreRuns() {
|
|
72
|
-
if (runsPage.value >= runsLastPage.value) return;
|
|
73
|
-
runsPage.value += 1;
|
|
74
|
-
void loadRuns();
|
|
75
|
-
}
|
|
76
48
|
const steps = ref([]);
|
|
77
49
|
const stepsSource = ref(null);
|
|
78
50
|
const runStatus = ref(null);
|
|
@@ -382,14 +354,6 @@ const edgeTypes = {
|
|
|
382
354
|
preview.onNodeClick((evt) => {
|
|
383
355
|
if (evt.node.type === "workflow") onNodeClick(evt.node.id);
|
|
384
356
|
});
|
|
385
|
-
function formatDateTime(iso) {
|
|
386
|
-
if (!iso) return "\u2014";
|
|
387
|
-
try {
|
|
388
|
-
return new Date(iso).toLocaleString();
|
|
389
|
-
} catch {
|
|
390
|
-
return String(iso);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
357
|
function formatTime(iso) {
|
|
394
358
|
if (!iso) return "\u2014";
|
|
395
359
|
try {
|
|
@@ -405,30 +369,16 @@ function pretty(v) {
|
|
|
405
369
|
return String(v);
|
|
406
370
|
}
|
|
407
371
|
}
|
|
408
|
-
function triggerIcon(type) {
|
|
409
|
-
return triggerGlyph(type);
|
|
410
|
-
}
|
|
411
372
|
const activeStepTitle = computed(() => {
|
|
412
373
|
const s = activeStep.value;
|
|
413
374
|
if (!s) return "";
|
|
414
375
|
return `${s.node_slug || s.node_id}${s.attempt > 0 ? ` (#${s.attempt + 1})` : ""}`;
|
|
415
376
|
});
|
|
416
|
-
onMounted(
|
|
417
|
-
if (props.
|
|
418
|
-
selectRun(props.initialRunId);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
await loadRuns(true);
|
|
422
|
-
const initial = props.initialRunId && runs.value.some((r) => r.id === props.initialRunId) ? props.initialRunId : runs.value[0]?.id;
|
|
423
|
-
if (initial) selectRun(initial);
|
|
377
|
+
onMounted(() => {
|
|
378
|
+
if (props.initialRunId) selectRun(props.initialRunId);
|
|
424
379
|
});
|
|
425
380
|
watch(() => props.initialRunId, (id) => {
|
|
426
|
-
if (
|
|
427
|
-
if (props.single) {
|
|
428
|
-
selectRun(id);
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (runs.value.some((r) => r.id === id)) selectRun(id);
|
|
381
|
+
if (id) selectRun(id);
|
|
432
382
|
});
|
|
433
383
|
</script>
|
|
434
384
|
|
|
@@ -589,58 +539,6 @@ watch(() => props.initialRunId, (id) => {
|
|
|
589
539
|
</p>
|
|
590
540
|
</aside>
|
|
591
541
|
|
|
592
|
-
<!-- Run list — hidden in single mode (the run detail replays ONE run). -->
|
|
593
|
-
<aside v-if="!single" class="flex w-80 shrink-0 flex-col border-l border-black/[0.06] bg-white">
|
|
594
|
-
<div class="border-b border-black/[0.06] px-3 py-2">
|
|
595
|
-
<div class="text-sm font-semibold">{{ t("integration.replay.runsList.heading") }}</div>
|
|
596
|
-
<p class="text-sm text-muted-foreground">
|
|
597
|
-
{{ t("integration.replay.runsList.description") }}
|
|
598
|
-
</p>
|
|
599
|
-
</div>
|
|
600
|
-
|
|
601
|
-
<div class="flex-1 space-y-1 overflow-y-auto p-2">
|
|
602
|
-
<button
|
|
603
|
-
v-for="r in runs"
|
|
604
|
-
:key="r.id"
|
|
605
|
-
type="button"
|
|
606
|
-
class="w-full rounded-lg border p-2.5 text-left transition"
|
|
607
|
-
:class="selectedRunId === r.id ? 'border-primary/40 bg-primary/[0.04]' : 'border-transparent hover:border-black/[0.08] hover:bg-muted/30'"
|
|
608
|
-
@click="selectRun(r.id)"
|
|
609
|
-
>
|
|
610
|
-
<div class="flex items-center justify-between gap-2">
|
|
611
|
-
<span class="flex min-w-0 items-center gap-1.5 text-sm font-medium">
|
|
612
|
-
<component :is="triggerIcon(r.triggerType)" :size="12" class="shrink-0 text-muted-foreground" />
|
|
613
|
-
<span class="truncate">#{{ r.id }}</span>
|
|
614
|
-
</span>
|
|
615
|
-
<Badge variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="runStatusClass(r.status)">
|
|
616
|
-
{{ runStatusLabel(r.status) }}
|
|
617
|
-
</Badge>
|
|
618
|
-
</div>
|
|
619
|
-
<div class="mt-0.5 flex items-center justify-between gap-2 text-sm text-muted-foreground">
|
|
620
|
-
<span class="tabular-nums">{{ formatDateTime(r.startedAt) }}</span>
|
|
621
|
-
<span class="tabular-nums">{{ formatDuration(r.durationMs) }}</span>
|
|
622
|
-
</div>
|
|
623
|
-
</button>
|
|
624
|
-
|
|
625
|
-
<p v-if="!runs.length && !runsLoading" class="px-2 py-6 text-center text-sm text-muted-foreground">
|
|
626
|
-
{{ t("integration.replay.runsList.empty") }}
|
|
627
|
-
</p>
|
|
628
|
-
|
|
629
|
-
<div v-if="runsLoading" class="flex justify-center py-3">
|
|
630
|
-
<Loader2 :size="15" class="animate-spin text-muted-foreground" />
|
|
631
|
-
</div>
|
|
632
|
-
<Button
|
|
633
|
-
v-else-if="runsPage < runsLastPage"
|
|
634
|
-
variant="ghost"
|
|
635
|
-
size="sm"
|
|
636
|
-
class="w-full text-sm"
|
|
637
|
-
@click="loadMoreRuns"
|
|
638
|
-
>
|
|
639
|
-
{{ t("integration.replay.runsList.loadMore") }}
|
|
640
|
-
</Button>
|
|
641
|
-
</div>
|
|
642
|
-
</aside>
|
|
643
|
-
|
|
644
542
|
</div>
|
|
645
543
|
</template>
|
|
646
544
|
|
|
@@ -3,9 +3,13 @@ import '@vue-flow/core/dist/theme-default.css';
|
|
|
3
3
|
import type { Workflow } from '../../types/integrations.js';
|
|
4
4
|
type __VLS_Props = {
|
|
5
5
|
workflow: Workflow;
|
|
6
|
-
/**
|
|
6
|
+
/** The run to replay (deep-link from the Runs table). */
|
|
7
7
|
initialRunId?: string | null;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Accepted and ignored since PO-350 — the panel has no other mode
|
|
10
|
+
* to switch out of. Kept so a host passing it keeps working; drop it at the
|
|
11
|
+
* next major.
|
|
12
|
+
*/
|
|
9
13
|
single?: boolean;
|
|
10
14
|
};
|
|
11
15
|
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>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WorkflowStateNamespace } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
state: WorkflowStateNamespace[];
|
|
5
|
+
};
|
|
6
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
"update:open": (args_0: boolean) => any;
|
|
8
|
+
"update:state": (next: WorkflowStateNamespace[]) => any;
|
|
9
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
11
|
+
"onUpdate:state"?: ((next: WorkflowStateNamespace[]) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { AddCircle as Plus, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
Input,
|
|
13
|
+
Label,
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue
|
|
19
|
+
} from "@revenexx/studio";
|
|
20
|
+
import { deepClone } from "../../utils/deepClone";
|
|
21
|
+
import { stateNamespaceErrorKey } from "../../utils/stateNamespaceName";
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
open: { type: Boolean, required: true },
|
|
24
|
+
state: { type: Array, required: true }
|
|
25
|
+
});
|
|
26
|
+
const emit = defineEmits(["update:open", "update:state"]);
|
|
27
|
+
const t = useIntegrationsT();
|
|
28
|
+
const ROLES = ["mapping", "cursor", "dedupe", "digest"];
|
|
29
|
+
const working = ref([]);
|
|
30
|
+
watch(() => props.open, (isOpen) => {
|
|
31
|
+
if (!isOpen) return;
|
|
32
|
+
working.value = deepClone(props.state ?? []);
|
|
33
|
+
newName.value = "";
|
|
34
|
+
newRole.value = "mapping";
|
|
35
|
+
newVisibility.value = "private";
|
|
36
|
+
});
|
|
37
|
+
const changed = computed(() => JSON.stringify(working.value) !== JSON.stringify(props.state ?? []));
|
|
38
|
+
function applyAndClose() {
|
|
39
|
+
if (changed.value) emit("update:state", working.value);
|
|
40
|
+
emit("update:open", false);
|
|
41
|
+
}
|
|
42
|
+
function setVisibility(index, visibility) {
|
|
43
|
+
const next = [...working.value];
|
|
44
|
+
const row = next[index];
|
|
45
|
+
if (!row) return;
|
|
46
|
+
next[index] = { ...row, visibility };
|
|
47
|
+
working.value = next;
|
|
48
|
+
}
|
|
49
|
+
function removeNamespace(index) {
|
|
50
|
+
working.value = working.value.filter((_, i) => i !== index);
|
|
51
|
+
}
|
|
52
|
+
const newName = ref("");
|
|
53
|
+
const newRole = ref("mapping");
|
|
54
|
+
const newVisibility = ref("private");
|
|
55
|
+
const newNameError = computed(() => {
|
|
56
|
+
const key = stateNamespaceErrorKey(newName.value, working.value.map((n) => n.namespace));
|
|
57
|
+
return key ? t(key) : null;
|
|
58
|
+
});
|
|
59
|
+
function addNamespace() {
|
|
60
|
+
const namespace = newName.value.trim();
|
|
61
|
+
if (!namespace || newNameError.value) return;
|
|
62
|
+
working.value = [...working.value, { namespace, role: newRole.value, visibility: newVisibility.value }];
|
|
63
|
+
newName.value = "";
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
69
|
+
<DialogContent class="max-w-[94vw] sm:max-w-3xl">
|
|
70
|
+
<DialogHeader>
|
|
71
|
+
<DialogTitle>{{ t("integration.workflows.state.dialog.title") }}</DialogTitle>
|
|
72
|
+
<DialogDescription class="text-sm">
|
|
73
|
+
{{ t("integration.workflows.state.dialog.description") }}
|
|
74
|
+
</DialogDescription>
|
|
75
|
+
</DialogHeader>
|
|
76
|
+
|
|
77
|
+
<div class="max-h-[62vh] space-y-3 overflow-y-auto pr-1">
|
|
78
|
+
<p v-if="!working.length" class="rounded-lg border border-dashed border-black/[0.12] px-4 py-6 text-center text-sm text-muted-foreground">
|
|
79
|
+
{{ t("integration.workflows.state.empty") }}
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
<div
|
|
83
|
+
v-for="(row, index) in working"
|
|
84
|
+
:key="row.namespace"
|
|
85
|
+
role="group"
|
|
86
|
+
:aria-label="t('integration.workflows.state.row.aria', { namespace: row.namespace })"
|
|
87
|
+
class="rounded-lg border border-black/[0.06] bg-white p-3"
|
|
88
|
+
>
|
|
89
|
+
<div class="flex items-center gap-2">
|
|
90
|
+
<code class="min-w-0 truncate font-mono text-sm font-semibold">{{ row.namespace }}</code>
|
|
91
|
+
<!-- The role is stated, not offered: entries written under one
|
|
92
|
+
meaning cannot be re-read under another (specs AC-3). -->
|
|
93
|
+
<span class="shrink-0 rounded-lg bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
|
94
|
+
{{ t(`integration.workflows.state.role.${row.role}.label`) }}
|
|
95
|
+
</span>
|
|
96
|
+
<div class="flex-1" />
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
|
|
100
|
+
:title="t('integration.workflows.state.action.remove')"
|
|
101
|
+
@click="removeNamespace(index)"
|
|
102
|
+
>
|
|
103
|
+
<TrashBin :size="13" />
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<p class="mt-1 text-xs text-muted-foreground">
|
|
108
|
+
{{ t(`integration.workflows.state.role.${row.role}.hint`) }}
|
|
109
|
+
{{ t("integration.workflows.state.role.fixed") }}
|
|
110
|
+
</p>
|
|
111
|
+
|
|
112
|
+
<div class="mt-2 grid gap-2 sm:grid-cols-[10rem_1fr]">
|
|
113
|
+
<div class="space-y-1">
|
|
114
|
+
<Label :for="`state-${row.namespace}-visibility`" class="text-mono text-muted-foreground">
|
|
115
|
+
{{ t("integration.workflows.state.field.visibility.label") }}
|
|
116
|
+
</Label>
|
|
117
|
+
<Select
|
|
118
|
+
:model-value="row.visibility ?? 'private'"
|
|
119
|
+
@update:model-value="(v) => setVisibility(index, String(v))"
|
|
120
|
+
>
|
|
121
|
+
<SelectTrigger
|
|
122
|
+
:id="`state-${row.namespace}-visibility`"
|
|
123
|
+
:aria-label="t('integration.workflows.state.field.visibility.label')"
|
|
124
|
+
class="h-8 bg-white text-sm"
|
|
125
|
+
>
|
|
126
|
+
<SelectValue />
|
|
127
|
+
</SelectTrigger>
|
|
128
|
+
<SelectContent>
|
|
129
|
+
<SelectItem value="private">
|
|
130
|
+
{{ t("integration.workflows.state.visibility.private.label") }}
|
|
131
|
+
</SelectItem>
|
|
132
|
+
<SelectItem value="shared">
|
|
133
|
+
{{ t("integration.workflows.state.visibility.shared.label") }}
|
|
134
|
+
</SelectItem>
|
|
135
|
+
</SelectContent>
|
|
136
|
+
</Select>
|
|
137
|
+
</div>
|
|
138
|
+
<!-- What the choice costs, beside the choice — a shared namespace is
|
|
139
|
+
written by every workflow in the tenant and outlives this one
|
|
140
|
+
(specs AC-4). -->
|
|
141
|
+
<p class="self-end pb-1 text-xs text-muted-foreground">
|
|
142
|
+
{{ t(`integration.workflows.state.visibility.${row.visibility ?? "private"}.hint`) }}
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<p class="mt-2 text-xs text-muted-foreground">
|
|
147
|
+
{{ t("integration.workflows.state.action.remove.hint") }}
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="rounded-lg border border-black/[0.06] bg-muted/30 p-3">
|
|
152
|
+
<Label for="state-new-name" class="text-mono text-muted-foreground">
|
|
153
|
+
{{ t("integration.workflows.state.field.newName.label") }}
|
|
154
|
+
</Label>
|
|
155
|
+
<div class="mt-1 grid gap-2 sm:grid-cols-[1fr_10rem_10rem_auto]">
|
|
156
|
+
<Input
|
|
157
|
+
id="state-new-name"
|
|
158
|
+
v-model="newName"
|
|
159
|
+
class="h-8 bg-white font-mono text-sm"
|
|
160
|
+
:placeholder="t('integration.workflows.state.field.newName.placeholder')"
|
|
161
|
+
@keydown.enter.prevent="addNamespace"
|
|
162
|
+
/>
|
|
163
|
+
<!-- The role IS chosen here, and only here — the positive half of
|
|
164
|
+
AC-3, without which "no role control on a row" proves nothing. -->
|
|
165
|
+
<Select v-model="newRole">
|
|
166
|
+
<SelectTrigger
|
|
167
|
+
id="state-new-role"
|
|
168
|
+
:aria-label="t('integration.workflows.state.field.newRole.label')"
|
|
169
|
+
class="h-8 bg-white text-sm"
|
|
170
|
+
>
|
|
171
|
+
<SelectValue />
|
|
172
|
+
</SelectTrigger>
|
|
173
|
+
<SelectContent>
|
|
174
|
+
<SelectItem v-for="role in ROLES" :key="role" :value="role">
|
|
175
|
+
{{ t(`integration.workflows.state.role.${role}.label`) }}
|
|
176
|
+
</SelectItem>
|
|
177
|
+
</SelectContent>
|
|
178
|
+
</Select>
|
|
179
|
+
<Select v-model="newVisibility">
|
|
180
|
+
<SelectTrigger
|
|
181
|
+
id="state-new-visibility"
|
|
182
|
+
:aria-label="t('integration.workflows.state.field.newVisibility.label')"
|
|
183
|
+
class="h-8 bg-white text-sm"
|
|
184
|
+
>
|
|
185
|
+
<SelectValue />
|
|
186
|
+
</SelectTrigger>
|
|
187
|
+
<SelectContent>
|
|
188
|
+
<SelectItem value="private">
|
|
189
|
+
{{ t("integration.workflows.state.visibility.private.label") }}
|
|
190
|
+
</SelectItem>
|
|
191
|
+
<SelectItem value="shared">
|
|
192
|
+
{{ t("integration.workflows.state.visibility.shared.label") }}
|
|
193
|
+
</SelectItem>
|
|
194
|
+
</SelectContent>
|
|
195
|
+
</Select>
|
|
196
|
+
<Button size="sm" class="h-8" :disabled="!newName.trim() || !!newNameError" @click="addNamespace">
|
|
197
|
+
<Plus :size="14" class="mr-1" />
|
|
198
|
+
{{ t("integration.workflows.state.action.add") }}
|
|
199
|
+
</Button>
|
|
200
|
+
</div>
|
|
201
|
+
<p v-if="newNameError" class="mt-1 text-xs text-rose-600">{{ newNameError }}</p>
|
|
202
|
+
<p class="mt-1 text-xs text-muted-foreground">{{ t(`integration.workflows.state.role.${newRole}.hint`) }}</p>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<DialogFooter>
|
|
207
|
+
<Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
|
|
208
|
+
{{ t("integration.common.action.cancel") }}
|
|
209
|
+
</Button>
|
|
210
|
+
<Button size="sm" class="h-8" @click="applyAndClose">
|
|
211
|
+
{{ t("integration.workflows.state.action.done") }}
|
|
212
|
+
</Button>
|
|
213
|
+
</DialogFooter>
|
|
214
|
+
</DialogContent>
|
|
215
|
+
</Dialog>
|
|
216
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WorkflowStateNamespace } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
state: WorkflowStateNamespace[];
|
|
5
|
+
};
|
|
6
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
"update:open": (args_0: boolean) => any;
|
|
8
|
+
"update:state": (next: WorkflowStateNamespace[]) => any;
|
|
9
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
11
|
+
"onUpdate:state"?: ((next: WorkflowStateNamespace[]) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
export default _default;
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
DropdownMenuItem,
|
|
15
15
|
DropdownMenuLabel,
|
|
16
16
|
DropdownMenuSeparator,
|
|
17
|
-
DropdownMenuTrigger
|
|
17
|
+
DropdownMenuTrigger,
|
|
18
|
+
Label,
|
|
19
|
+
Switch
|
|
18
20
|
} from "@revenexx/studio";
|
|
19
21
|
defineProps({
|
|
20
22
|
workflows: { type: Array, required: true },
|
|
@@ -247,16 +249,25 @@ const columns = computed(() => [
|
|
|
247
249
|
@row-click="(wf2) => router.push(`/integrations/workflows/${wf2.id}`)"
|
|
248
250
|
>
|
|
249
251
|
<template #toolbar-left>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
<!-- A switch rather than a button: the control is a state, not an action.
|
|
253
|
+
A button labelled "Group by tags" ungroups once grouping is on, and
|
|
254
|
+
said so neither to a reader nor to a screen reader — the state lived
|
|
255
|
+
in a colour class alone. The Label is what names the switch. -->
|
|
256
|
+
<div class="flex h-7 items-center gap-2">
|
|
257
|
+
<Switch
|
|
258
|
+
id="wf-group-by-tags"
|
|
259
|
+
:model-value="groupByTags"
|
|
260
|
+
@update:model-value="(on) => groupByTags = on"
|
|
261
|
+
/>
|
|
262
|
+
<Label
|
|
263
|
+
for="wf-group-by-tags"
|
|
264
|
+
class="flex cursor-pointer items-center gap-1.5 text-sm font-normal"
|
|
265
|
+
:class="groupByTags ? 'text-primary' : 'text-muted-foreground'"
|
|
266
|
+
>
|
|
267
|
+
<Layers :size="13" />
|
|
268
|
+
{{ t("integration.workflows.table.action.groupByTags") }}
|
|
269
|
+
</Label>
|
|
270
|
+
</div>
|
|
260
271
|
</template>
|
|
261
272
|
|
|
262
273
|
<!-- Name (icon + title + description) -->
|
|
@@ -32,6 +32,40 @@ export type ShortcutHandler = ((event: KeyboardEvent) => void) | {
|
|
|
32
32
|
run: (event: KeyboardEvent) => void;
|
|
33
33
|
};
|
|
34
34
|
export type ShortcutMap = Record<string, ShortcutHandler>;
|
|
35
|
+
/**
|
|
36
|
+
* A map key in its comparable form.
|
|
37
|
+
*
|
|
38
|
+
* Exported for the unit suite: the whole layout question below is decided here
|
|
39
|
+
* and in {@link shortcutCandidates}, and both are cheaper to prove against
|
|
40
|
+
* hand-built events than against a browser per keyboard layout.
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizeShortcut(shortcut: string): string;
|
|
43
|
+
/** Everything a match reads from a keydown — so the rule needs no DOM to prove. */
|
|
44
|
+
export interface ShortcutKeyEvent {
|
|
45
|
+
key: string;
|
|
46
|
+
code?: string;
|
|
47
|
+
shiftKey?: boolean;
|
|
48
|
+
altKey?: boolean;
|
|
49
|
+
metaKey?: boolean;
|
|
50
|
+
ctrlKey?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The two forms a keydown is looked up under.
|
|
54
|
+
*
|
|
55
|
+
* `byKey` is what the layout PRODUCED and `byCode` is which key was pressed. Both
|
|
56
|
+
* are needed and neither is enough: a Dvorak or AZERTY layout moves the letters,
|
|
57
|
+
* so a letter binding is only found by key — while a character a layout reaches
|
|
58
|
+
* through a modifier, or a dead key that produces no useful character at all
|
|
59
|
+
* (⌥⇧T is `ˇ`), is only found by code.
|
|
60
|
+
*
|
|
61
|
+
* Shift is dropped from `byKey` alone, where it is part of the character. `byCode`
|
|
62
|
+
* stays strict: there the physical key says nothing about what was produced, so
|
|
63
|
+
* Shift is the only thing separating `shift+s` from `s`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function shortcutCandidates(event: ShortcutKeyEvent, isMac?: boolean): {
|
|
66
|
+
byKey: string;
|
|
67
|
+
byCode: string;
|
|
68
|
+
};
|
|
35
69
|
export declare function useCanvasShortcuts(keymap: MaybeRefOrGetter<ShortcutMap>, options?: {
|
|
36
70
|
disabled?: MaybeRefOrGetter<boolean>;
|
|
37
71
|
}): void;
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import { computed, onBeforeUnmount, onMounted, toValue } from "vue";
|
|
2
|
-
|
|
2
|
+
import { IS_MAC, shortcutTokens } from "../utils/shortcutLabel.js";
|
|
3
3
|
function canonical(parts) {
|
|
4
4
|
return parts.map((p) => p.toLowerCase()).sort((a, b) => a.localeCompare(b)).join("+");
|
|
5
5
|
}
|
|
6
|
-
function normalizeShortcut(shortcut) {
|
|
7
|
-
|
|
8
|
-
return canonical(shortcut.split("+"));
|
|
6
|
+
export function normalizeShortcut(shortcut) {
|
|
7
|
+
return canonical(shortcutTokens(shortcut));
|
|
9
8
|
}
|
|
10
9
|
function codeToKey(code) {
|
|
11
10
|
if (code.startsWith("Key")) return code.slice(3).toLowerCase();
|
|
12
11
|
if (code.startsWith("Digit")) return code.slice(5);
|
|
13
12
|
return code.toLowerCase();
|
|
14
13
|
}
|
|
15
|
-
function
|
|
14
|
+
function shiftMadeTheCharacter(loweredKey) {
|
|
15
|
+
return loweredKey.length === 1 && !/^[\p{L}\p{N}]$/u.test(loweredKey);
|
|
16
|
+
}
|
|
17
|
+
export function shortcutCandidates(event, isMac = IS_MAC) {
|
|
16
18
|
const mods = [];
|
|
17
19
|
if (event.shiftKey) mods.push("shift");
|
|
18
20
|
if (event.altKey) mods.push("alt");
|
|
19
|
-
if (
|
|
21
|
+
if ((isMac ? event.metaKey : event.ctrlKey) ?? false) mods.push("mod");
|
|
20
22
|
const key = event.key === " " ? "space" : event.key.toLowerCase();
|
|
21
23
|
const code = codeToKey(event.code ?? "");
|
|
24
|
+
const keyMods = shiftMadeTheCharacter(key) ? mods.filter((m) => m !== "shift") : mods;
|
|
22
25
|
return {
|
|
23
|
-
byKey: canonical([...
|
|
26
|
+
byKey: canonical([...keyMods, key]),
|
|
24
27
|
byCode: canonical([...mods, code])
|
|
25
28
|
};
|
|
26
29
|
}
|
|
@@ -48,7 +51,7 @@ export function useCanvasShortcuts(keymap, options) {
|
|
|
48
51
|
if (toValue(options?.disabled)) return;
|
|
49
52
|
if (event.defaultPrevented) return;
|
|
50
53
|
if (isEditingContext(event)) return;
|
|
51
|
-
const { byKey, byCode } =
|
|
54
|
+
const { byKey, byCode } = shortcutCandidates(event);
|
|
52
55
|
const handler = normalized.value[byKey] ?? normalized.value[byCode];
|
|
53
56
|
if (!handler) return;
|
|
54
57
|
const run = typeof handler === "function" ? handler : handler.disabled() ? void 0 : handler.run;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
2
2
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
3
3
|
import { errorReason } from "../utils/apiError.js";
|
|
4
|
+
import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
4
5
|
import { useLoadGate } from "./useLoadGate.js";
|
|
5
6
|
export function useCredentialTypes() {
|
|
6
7
|
const types = useState("integrations.credentialTypes", () => []);
|
|
@@ -10,17 +11,11 @@ export function useCredentialTypes() {
|
|
|
10
11
|
const gate = useLoadGate("integrations.credentialTypes", { loading, loaded });
|
|
11
12
|
const api = useIntegrationsApi();
|
|
12
13
|
const t = useIntegrationsT();
|
|
13
|
-
function normalize(res) {
|
|
14
|
-
const obj = res;
|
|
15
|
-
if (Array.isArray(obj)) return obj;
|
|
16
|
-
if (Array.isArray(obj?.data)) return obj.data;
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
14
|
async function load(force = false) {
|
|
20
15
|
if (!await gate.claim(force)) return types.value;
|
|
21
16
|
error.value = null;
|
|
22
17
|
try {
|
|
23
|
-
types.value =
|
|
18
|
+
types.value = unwrapList(await api.get("/credential-types"));
|
|
24
19
|
loaded.value = true;
|
|
25
20
|
} catch (e) {
|
|
26
21
|
error.value = errorReason(e, t("integration.credentials.error.loadTypes"));
|
|
@@ -36,8 +31,7 @@ export function useCredentialTypes() {
|
|
|
36
31
|
const cached = bySlug(slug);
|
|
37
32
|
if (cached) return cached;
|
|
38
33
|
try {
|
|
39
|
-
|
|
40
|
-
return res?.data ?? res ?? null;
|
|
34
|
+
return unwrapRecord(await api.get(`/credential-types/${encodeURIComponent(slug)}`));
|
|
41
35
|
} catch {
|
|
42
36
|
return null;
|
|
43
37
|
}
|
|
@@ -2,6 +2,7 @@ import { computed } from "vue";
|
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
|
|
5
|
+
import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
5
6
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
7
|
export function useIntegrationsCredentials() {
|
|
7
8
|
const credentials = useState("integrations.credentials", () => []);
|
|
@@ -11,20 +12,11 @@ export function useIntegrationsCredentials() {
|
|
|
11
12
|
const gate = useLoadGate("integrations.credentials", { loading, loaded });
|
|
12
13
|
const api = useIntegrationsApi();
|
|
13
14
|
const t = useIntegrationsT();
|
|
14
|
-
function normalize(res) {
|
|
15
|
-
const obj = res;
|
|
16
|
-
if (Array.isArray(obj)) return obj;
|
|
17
|
-
if (Array.isArray(obj?.data)) return obj.data;
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
function unwrap(res) {
|
|
21
|
-
return res?.data ?? res;
|
|
22
|
-
}
|
|
23
15
|
async function load(force = false) {
|
|
24
16
|
if (!await gate.claim(force)) return credentials.value;
|
|
25
17
|
error.value = null;
|
|
26
18
|
try {
|
|
27
|
-
credentials.value =
|
|
19
|
+
credentials.value = unwrapList(await api.get("/credentials"));
|
|
28
20
|
loaded.value = true;
|
|
29
21
|
} catch (e) {
|
|
30
22
|
error.value = errorReason(e, t("integration.credentials.error.loadFailed"));
|
|
@@ -37,15 +29,15 @@ export function useIntegrationsCredentials() {
|
|
|
37
29
|
return credentials.value.filter((c) => c.credential_type_slug === slug);
|
|
38
30
|
}
|
|
39
31
|
async function get(id) {
|
|
40
|
-
return
|
|
32
|
+
return unwrapRecord(await api.get(`/credentials/${encodeURIComponent(id)}`));
|
|
41
33
|
}
|
|
42
34
|
async function create(input) {
|
|
43
|
-
const created =
|
|
35
|
+
const created = unwrapRecord(await api.post("/credentials", input));
|
|
44
36
|
await load(true);
|
|
45
37
|
return created;
|
|
46
38
|
}
|
|
47
39
|
async function update(id, input) {
|
|
48
|
-
const updated =
|
|
40
|
+
const updated = unwrapRecord(await api.patch(`/credentials/${encodeURIComponent(id)}`, input));
|
|
49
41
|
await load(true);
|
|
50
42
|
return updated;
|
|
51
43
|
}
|
|
@@ -71,7 +63,7 @@ export function useIntegrationsCredentials() {
|
|
|
71
63
|
}
|
|
72
64
|
async function oauthAuthorizeUrl(id) {
|
|
73
65
|
const res = await api.post(`/credentials/${encodeURIComponent(id)}/oauth/authorize-url`);
|
|
74
|
-
const authorizeUrl = res
|
|
66
|
+
const authorizeUrl = unwrapRecord(res).authorize_url;
|
|
75
67
|
if (!authorizeUrl) throw new IntegrationsApiError(0, res, t("integration.credentials.error.noAuthorizeUrl"));
|
|
76
68
|
return authorizeUrl;
|
|
77
69
|
}
|