@revenexx/studio-integrations 0.6.0 → 0.7.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 +227 -0
- package/README.md +31 -0
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -2
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +29 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
- 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 +16 -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.vue +15 -14
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
- package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
- package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
- 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 +37 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
- 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.js +6 -4
- 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 +3 -1
- 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/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.js +4 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- package/dist/runtime/i18n/messages/de.json +1207 -0
- package/dist/runtime/i18n/messages/en.json +1207 -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 +70 -52
- package/dist/runtime/pages/integrations/index.vue +50 -56
- 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 +77 -68
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
- 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/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +18 -5
- package/dist/runtime/utils/localize.js +32 -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/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
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
|
3
3
|
import { Badge } from "@revenexx/studio";
|
|
4
|
-
import { statusClass } from "../../utils/statusStyle";
|
|
4
|
+
import { RUN_STATUS_WORDS, normalisedRunStatus, runStatusLabel, statusClass } from "../../utils/statusStyle";
|
|
5
|
+
import { TRIGGER_TYPE_WORDS, triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
5
6
|
const props = defineProps({
|
|
6
7
|
runs: { type: Array, required: true },
|
|
7
8
|
loading: { type: Boolean, required: false, default: false },
|
|
8
|
-
emptyText: { type: String, required: false
|
|
9
|
+
emptyText: { type: String, required: false },
|
|
9
10
|
pagination: { type: Boolean, required: false, default: true },
|
|
10
11
|
showWorkflow: { type: Boolean, required: false, default: true }
|
|
11
12
|
});
|
|
12
13
|
const emit = defineEmits(["changed"]);
|
|
14
|
+
const t = useIntegrationsT();
|
|
13
15
|
const router = useRouter();
|
|
16
|
+
const emptyText = computed(() => props.emptyText ?? t("integration.runs.table.empty"));
|
|
14
17
|
const now = ref(Date.now());
|
|
15
18
|
let tick = null;
|
|
16
19
|
onMounted(() => {
|
|
@@ -31,10 +34,10 @@ function formatRelative(iso) {
|
|
|
31
34
|
if (!iso) return "\u2014";
|
|
32
35
|
const ms = Date.now() - new Date(iso).getTime();
|
|
33
36
|
if (Number.isNaN(ms)) return "\u2014";
|
|
34
|
-
if (ms < 6e4) return
|
|
35
|
-
if (ms < 36e5) return
|
|
36
|
-
if (ms < 864e5) return
|
|
37
|
-
return
|
|
37
|
+
if (ms < 6e4) return t("integration.common.time.secondsAgo", Math.max(1, Math.floor(ms / 1e3)));
|
|
38
|
+
if (ms < 36e5) return t("integration.common.time.minutesAgo", Math.floor(ms / 6e4));
|
|
39
|
+
if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
|
|
40
|
+
return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
|
|
38
41
|
}
|
|
39
42
|
function formatDuration(ms) {
|
|
40
43
|
if (ms == null) return "\u2014";
|
|
@@ -49,16 +52,35 @@ function liveDuration(run) {
|
|
|
49
52
|
const end = run.finishedAt ? Date.parse(run.finishedAt) : now.value;
|
|
50
53
|
return end - start;
|
|
51
54
|
}
|
|
55
|
+
const filterTabs = computed(() => props.pagination ? [
|
|
56
|
+
{
|
|
57
|
+
key: "status",
|
|
58
|
+
label: t("integration.runs.table.column.status"),
|
|
59
|
+
getter: (r) => normalisedRunStatus(r.status),
|
|
60
|
+
options: RUN_STATUS_WORDS.map((v) => ({ value: v, label: runStatusLabel(v) }))
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "trigger",
|
|
64
|
+
label: t("integration.runs.table.column.trigger"),
|
|
65
|
+
getter: (r) => r.triggerType ?? "",
|
|
66
|
+
options: TRIGGER_TYPE_WORDS.map((v) => ({ value: v, label: triggerTypeLabel(v) }))
|
|
67
|
+
}
|
|
68
|
+
] : void 0);
|
|
52
69
|
const columns = computed(() => [
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
// Sorted and searched by the word on screen, not by the raw field: a column
|
|
71
|
+
// with no `searchGetter` is searched through its `sortGetter`, so a German
|
|
72
|
+
// reader typing the status they can see has to find the row that shows it.
|
|
73
|
+
{ key: "status", label: t("integration.runs.table.column.status"), width: "100px", sortable: true, sortGetter: (r) => runStatusLabel(r.status) },
|
|
74
|
+
...props.showWorkflow ? [{ key: "workflow", label: t("integration.runs.table.column.workflow"), sortable: true, sortGetter: (r) => r.workflowName.toLowerCase(), searchGetter: (r) => r.workflowName }] : [],
|
|
55
75
|
// Fixed floor wide enough for a full UUID, flexible above it — header and
|
|
56
76
|
// body rows are separate grids, so a content-sized track (max-content)
|
|
57
77
|
// would misalign them. The flex share also lets the table fill the width.
|
|
58
|
-
{ key: "id", label: "
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{ key: "
|
|
78
|
+
{ key: "id", label: t("integration.runs.table.column.id"), width: "minmax(280px,1fr)", searchGetter: (r) => `${r.temporalRunId ?? ""} ${r.id}` },
|
|
79
|
+
// Same rule as the status column above: the word on screen is what sorts and
|
|
80
|
+
// what a search has to match.
|
|
81
|
+
{ key: "trigger", label: t("integration.runs.table.column.trigger"), width: "120px", sortable: true, sortGetter: (r) => triggerTypeLabel(r.triggerType) },
|
|
82
|
+
{ key: "started", label: t("integration.runs.table.column.started"), width: "120px", sortable: true, sortGetter: (r) => r.startedAt },
|
|
83
|
+
{ key: "duration", label: t("integration.runs.table.column.duration"), width: "100px", sortable: true, sortGetter: (r) => r.durationMs ?? -1 },
|
|
62
84
|
{ key: "actions", label: "", width: "52px" }
|
|
63
85
|
]);
|
|
64
86
|
</script>
|
|
@@ -69,8 +91,8 @@ const columns = computed(() => [
|
|
|
69
91
|
:columns="columns"
|
|
70
92
|
:row-key="(r) => r.id"
|
|
71
93
|
:default-sort="{ column: 'started', direction: 'desc' }"
|
|
72
|
-
:search="{ placeholder: '
|
|
73
|
-
:filter="
|
|
94
|
+
:search="{ placeholder: t('integration.runs.table.search.placeholder') }"
|
|
95
|
+
:filter-tabs="filterTabs"
|
|
74
96
|
:loading="loading"
|
|
75
97
|
:pagination="pagination"
|
|
76
98
|
:empty-text="emptyText"
|
|
@@ -78,7 +100,7 @@ const columns = computed(() => [
|
|
|
78
100
|
>
|
|
79
101
|
<template #cell-status="{ row: run }">
|
|
80
102
|
<Badge variant="outline" :class="['border text-xs font-medium uppercase', statusClass(run.status)]">
|
|
81
|
-
{{ run.status }}
|
|
103
|
+
{{ runStatusLabel(run.status) }}
|
|
82
104
|
</Badge>
|
|
83
105
|
</template>
|
|
84
106
|
|
|
@@ -93,7 +115,7 @@ const columns = computed(() => [
|
|
|
93
115
|
|
|
94
116
|
<template #cell-trigger="{ row: run }">
|
|
95
117
|
<span class="inline-flex items-center whitespace-nowrap rounded-md border border-black/[0.06] bg-muted/50 px-1.5 py-0.5 text-xs font-medium uppercase text-muted-foreground">
|
|
96
|
-
{{ run.triggerType }}
|
|
118
|
+
{{ triggerTypeLabel(run.triggerType) }}
|
|
97
119
|
</span>
|
|
98
120
|
</template>
|
|
99
121
|
|
|
@@ -14,7 +14,6 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {
|
|
|
14
14
|
onChanged?: (() => any) | undefined;
|
|
15
15
|
}>, {
|
|
16
16
|
loading: boolean;
|
|
17
|
-
emptyText: string;
|
|
18
17
|
pagination: boolean;
|
|
19
18
|
showWorkflow: boolean;
|
|
20
19
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -5,6 +5,7 @@ import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogHeader,
|
|
|
5
5
|
import { useIntegrationsCredentials } from "../../composables/useIntegrationsCredentials";
|
|
6
6
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
7
7
|
const emit = defineEmits(["created"]);
|
|
8
|
+
const t = useIntegrationsT();
|
|
8
9
|
const sftp = typeof useConsoleSftp === "function" ? useConsoleSftp() : {
|
|
9
10
|
accounts: ref([]),
|
|
10
11
|
loading: ref(false),
|
|
@@ -30,7 +31,7 @@ async function importAccount(account) {
|
|
|
30
31
|
try {
|
|
31
32
|
const creds = await sftp.revealPassword(tenant, account.id);
|
|
32
33
|
if (!creds?.password) {
|
|
33
|
-
error.value = "
|
|
34
|
+
error.value = t("integration.credentials.sftpImport.error.readPassword");
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
const created = await create({
|
|
@@ -46,7 +47,7 @@ async function importAccount(account) {
|
|
|
46
47
|
emit("created", created.id);
|
|
47
48
|
open.value = false;
|
|
48
49
|
} catch (err) {
|
|
49
|
-
error.value = err instanceof Error ? err.message : "
|
|
50
|
+
error.value = err instanceof Error ? err.message : t("integration.credentials.sftpImport.error.importFailed");
|
|
50
51
|
} finally {
|
|
51
52
|
busyId.value = null;
|
|
52
53
|
}
|
|
@@ -57,9 +58,9 @@ async function importAccount(account) {
|
|
|
57
58
|
<Dialog v-model:open="open">
|
|
58
59
|
<DialogContent class="sm:max-w-md">
|
|
59
60
|
<DialogHeader>
|
|
60
|
-
<DialogTitle>
|
|
61
|
+
<DialogTitle>{{ t("integration.credentials.sftpImport.title") }}</DialogTitle>
|
|
61
62
|
<DialogDescription class="text-sm">
|
|
62
|
-
|
|
63
|
+
{{ t("integration.credentials.sftpImport.description") }}
|
|
63
64
|
</DialogDescription>
|
|
64
65
|
</DialogHeader>
|
|
65
66
|
|
|
@@ -67,7 +68,7 @@ async function importAccount(account) {
|
|
|
67
68
|
<div v-for="n in 3" :key="`sk-${n}`" class="h-9 rounded-md bg-black/[0.07] animate-pulse" />
|
|
68
69
|
</div>
|
|
69
70
|
<p v-else-if="!activeAccounts.length" class="py-4 text-center text-sm text-muted-foreground">
|
|
70
|
-
|
|
71
|
+
{{ t("integration.credentials.sftpImport.empty") }}
|
|
71
72
|
</p>
|
|
72
73
|
<div v-else class="max-h-72 space-y-1 overflow-y-auto">
|
|
73
74
|
<button
|
|
@@ -82,7 +83,7 @@ async function importAccount(account) {
|
|
|
82
83
|
<span class="block truncate font-mono text-sm">{{ account.login_username }}</span>
|
|
83
84
|
<span class="block truncate text-xs text-muted-foreground">{{ account.home_directory ?? account.server ?? "" }}</span>
|
|
84
85
|
</span>
|
|
85
|
-
<Badge variant="outline" class="text-xs font-medium uppercase shrink-0">
|
|
86
|
+
<Badge variant="outline" class="text-xs font-medium uppercase shrink-0">{{ t("integration.credentials.sftpImport.badge.active") }}</Badge>
|
|
86
87
|
<Loader2 v-if="busyId === account.id" :size="14" class="shrink-0 animate-spin text-muted-foreground" />
|
|
87
88
|
</button>
|
|
88
89
|
</div>
|
|
@@ -90,7 +91,7 @@ async function importAccount(account) {
|
|
|
90
91
|
<p v-if="error" class="text-sm text-rose-600">{{ error }}</p>
|
|
91
92
|
|
|
92
93
|
<div class="flex justify-end">
|
|
93
|
-
<Button variant="outline" size="sm" class="h-8 text-sm" @click="open = false">
|
|
94
|
+
<Button variant="outline" size="sm" class="h-8 text-sm" @click="open = false">{{ t("integration.common.action.close") }}</Button>
|
|
94
95
|
</div>
|
|
95
96
|
</DialogContent>
|
|
96
97
|
</Dialog>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { Pulse as Activity, ClockCircle as Clock, Home, Key as KeyRound, Layers, ShieldCheck, ShieldKeyhole as Credentials, Programming as Workflow } from "#solar-icons";
|
|
3
3
|
import SidebarNav from "@revenexx/studio-shared/components/Shared/SidebarNav.vue";
|
|
4
|
+
const t = useIntegrationsT();
|
|
4
5
|
const { workflows } = useIntegrationsWorkflows();
|
|
5
6
|
const { runningCount } = useIntegrationsRuns();
|
|
6
7
|
const { activeCount: schedulesActive } = useIntegrationsSchedules();
|
|
@@ -10,25 +11,28 @@ const sections = computed(() => [
|
|
|
10
11
|
{
|
|
11
12
|
key: "pinned",
|
|
12
13
|
items: [
|
|
13
|
-
{ label: "
|
|
14
|
+
{ label: t("integration.nav.item.dashboard"), to: "/integrations", icon: Home, exact: true }
|
|
14
15
|
]
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
key: "integrations",
|
|
18
|
-
|
|
19
|
+
// A rail's section heading is a proper noun and stays English in every UI
|
|
20
|
+
// language. It is still a key, so the decision is written down where a
|
|
21
|
+
// translator will look for it rather than only in a review comment.
|
|
22
|
+
label: t("integration.nav.section.integrations"),
|
|
19
23
|
items: [
|
|
20
|
-
{ label: "
|
|
21
|
-
{ label: "
|
|
22
|
-
{ label: "
|
|
23
|
-
{ label: "
|
|
24
|
-
{ label: "
|
|
25
|
-
{ label: "
|
|
26
|
-
{ label: "
|
|
24
|
+
{ label: t("integration.nav.item.workflows"), to: "/integrations/workflows", icon: Workflow, badge: workflows.value.length || null },
|
|
25
|
+
{ label: t("integration.nav.item.templates"), to: "/integrations/templates", icon: Layers },
|
|
26
|
+
{ label: t("integration.nav.item.runs"), to: "/integrations/runs", icon: Activity, badge: runningCount.value || null },
|
|
27
|
+
{ label: t("integration.nav.item.schedules"), to: "/integrations/schedules", icon: Clock, badge: schedulesActive.value || null },
|
|
28
|
+
{ label: t("integration.nav.item.credentials"), to: "/integrations/credentials", icon: Credentials, badge: credentialsTotal.value || null },
|
|
29
|
+
{ label: t("integration.nav.item.secrets"), to: "/integrations/secrets", icon: KeyRound, badge: secretsTotal.value || null },
|
|
30
|
+
{ label: t("integration.nav.item.audit"), to: "/integrations/audit", icon: ShieldCheck }
|
|
27
31
|
]
|
|
28
32
|
}
|
|
29
33
|
]);
|
|
30
34
|
</script>
|
|
31
35
|
|
|
32
36
|
<template>
|
|
33
|
-
<SidebarNav title="
|
|
37
|
+
<SidebarNav :title="t('integration.nav.title')" :icon="Workflow" :sections="sections" minimal />
|
|
34
38
|
</template>
|
|
@@ -11,13 +11,23 @@ import type { WorkflowSticky } from '../../types/integrations.js';
|
|
|
11
11
|
* - NodeToolbar on hover/selection: 7-color pastel palette + delete.
|
|
12
12
|
* - Hand-rolled bottom-right resize handle (no @vue-flow/node-resizer dep);
|
|
13
13
|
* size persists once on pointer-up.
|
|
14
|
+
*
|
|
15
|
+
* **Absence of the callbacks is what makes a note read-only** — the run replay and
|
|
16
|
+
* the history preview hand it none, and then it draws no toolbar, no resize handle
|
|
17
|
+
* and cannot be opened for typing (behaviour specs/sticky-notes.md AC-9). They used
|
|
18
|
+
* to be handed no-ops instead, which left every one of those affordances on screen
|
|
19
|
+
* doing nothing: a reader could not tell "not allowed here" from "broken" (PO-284).
|
|
20
|
+
* `GroupFrameView` gates its own toolbar on the same absence.
|
|
14
21
|
*/
|
|
15
22
|
export interface StickyNodeData {
|
|
16
23
|
sticky: WorkflowSticky;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Persist a partial change into the blob (editor-side, syncs + pushes).
|
|
26
|
+
* Absent on a read-only canvas — see the note above.
|
|
27
|
+
*/
|
|
28
|
+
onUpdate?: (patch: Partial<Pick<WorkflowSticky, 'content' | 'size' | 'color'>>) => void;
|
|
29
|
+
/** Remove this sticky from the blob (editor-side). Absent when read-only. */
|
|
30
|
+
onRemove?: () => void;
|
|
21
31
|
}
|
|
22
32
|
type __VLS_Props = {
|
|
23
33
|
id: string;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { computed, nextTick, ref, watch } from "vue";
|
|
3
3
|
import { Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
|
-
import {
|
|
5
|
+
import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
6
6
|
const props = defineProps({
|
|
7
7
|
id: { type: String, required: true },
|
|
8
8
|
data: { type: Object, required: true },
|
|
@@ -10,20 +10,22 @@ const props = defineProps({
|
|
|
10
10
|
});
|
|
11
11
|
defineOptions({ inheritAttrs: false });
|
|
12
12
|
defineEmits(["updateNodeInternals"]);
|
|
13
|
+
const t = useIntegrationsT();
|
|
13
14
|
const flow = useVueFlow();
|
|
14
15
|
const STICKY_COLORS = [
|
|
15
|
-
{ bg: "#fef3c7", border: "#fde68a",
|
|
16
|
-
{ bg: "#ffedd5", border: "#fed7aa",
|
|
17
|
-
{ bg: "#ffe4e6", border: "#fecdd3",
|
|
18
|
-
{ bg: "#dcfce7", border: "#bbf7d0",
|
|
19
|
-
{ bg: "#dbeafe", border: "#bfdbfe",
|
|
20
|
-
{ bg: "#ede9fe", border: "#ddd6fe",
|
|
21
|
-
{ bg: "#f5f5f4", border: "#e7e5e4",
|
|
16
|
+
{ bg: "#fef3c7", border: "#fde68a", nameKey: "integration.canvas.color.amber" },
|
|
17
|
+
{ bg: "#ffedd5", border: "#fed7aa", nameKey: "integration.canvas.color.orange" },
|
|
18
|
+
{ bg: "#ffe4e6", border: "#fecdd3", nameKey: "integration.canvas.color.rose" },
|
|
19
|
+
{ bg: "#dcfce7", border: "#bbf7d0", nameKey: "integration.canvas.color.green" },
|
|
20
|
+
{ bg: "#dbeafe", border: "#bfdbfe", nameKey: "integration.canvas.color.blue" },
|
|
21
|
+
{ bg: "#ede9fe", border: "#ddd6fe", nameKey: "integration.canvas.color.violet" },
|
|
22
|
+
{ bg: "#f5f5f4", border: "#e7e5e4", nameKey: "integration.canvas.color.gray" }
|
|
22
23
|
];
|
|
23
24
|
const color = computed(() => STICKY_COLORS[props.data.sticky.color ?? 0] ?? STICKY_COLORS[0]);
|
|
25
|
+
const editable = computed(() => !!props.data.onUpdate && !!props.data.onRemove);
|
|
24
26
|
function pickColor(i) {
|
|
25
27
|
if ((props.data.sticky.color ?? 0) === i) return;
|
|
26
|
-
props.data.onUpdate({ color: i });
|
|
28
|
+
props.data.onUpdate?.({ color: i });
|
|
27
29
|
}
|
|
28
30
|
const DEFAULT_WIDTH = 240;
|
|
29
31
|
const DEFAULT_HEIGHT = 160;
|
|
@@ -54,13 +56,18 @@ function onResizeMove(e) {
|
|
|
54
56
|
function onResizeEnd() {
|
|
55
57
|
if (!resizing.value) return;
|
|
56
58
|
resizing.value = false;
|
|
57
|
-
props.data.onUpdate({ size: { width: width.value, height: height.value } });
|
|
59
|
+
props.data.onUpdate?.({ size: { width: width.value, height: height.value } });
|
|
58
60
|
}
|
|
59
61
|
const editing = ref(false);
|
|
60
62
|
const draft = ref("");
|
|
61
63
|
const textareaEl = ref(null);
|
|
64
|
+
function onDoubleClick(event) {
|
|
65
|
+
if (!editable.value) return;
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
startEdit();
|
|
68
|
+
}
|
|
62
69
|
function startEdit() {
|
|
63
|
-
if (editing.value) return;
|
|
70
|
+
if (!editable.value || editing.value) return;
|
|
64
71
|
draft.value = props.data.sticky.content;
|
|
65
72
|
editing.value = true;
|
|
66
73
|
nextTick(() => textareaEl.value?.focus());
|
|
@@ -69,7 +76,7 @@ function commitEdit() {
|
|
|
69
76
|
if (!editing.value) return;
|
|
70
77
|
editing.value = false;
|
|
71
78
|
if (draft.value !== props.data.sticky.content) {
|
|
72
|
-
props.data.onUpdate({ content: draft.value });
|
|
79
|
+
props.data.onUpdate?.({ content: draft.value });
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
function onTextareaKeydown(e) {
|
|
@@ -91,7 +98,7 @@ function onHoverLeave() {
|
|
|
91
98
|
hovered.value = false;
|
|
92
99
|
}, 250);
|
|
93
100
|
}
|
|
94
|
-
const toolbarVisible = computed(() => !editing.value && (hovered.value || !!props.selected));
|
|
101
|
+
const toolbarVisible = computed(() => editable.value && !editing.value && (hovered.value || !!props.selected));
|
|
95
102
|
function escapeHtml(s) {
|
|
96
103
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
97
104
|
}
|
|
@@ -133,17 +140,17 @@ function onViewMouseDown(e) {
|
|
|
133
140
|
class="size-5 rounded-full border transition hover:scale-110"
|
|
134
141
|
:class="(data.sticky.color ?? 0) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
|
|
135
142
|
:style="{ backgroundColor: c.bg, borderColor: c.border }"
|
|
136
|
-
:title="c.
|
|
143
|
+
:title="t(c.nameKey)"
|
|
137
144
|
@click="pickColor(i)"
|
|
138
145
|
/>
|
|
139
146
|
<div class="mx-0.5 h-4 w-px bg-black/10" />
|
|
140
147
|
<button
|
|
141
148
|
type="button"
|
|
142
149
|
class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
|
|
143
|
-
title="
|
|
144
|
-
@click="data.onRemove()"
|
|
150
|
+
:title="t('integration.canvas.sticky.action.delete')"
|
|
151
|
+
@click="data.onRemove?.()"
|
|
145
152
|
>
|
|
146
|
-
<
|
|
153
|
+
<TrashBin :size="13" />
|
|
147
154
|
</button>
|
|
148
155
|
</div>
|
|
149
156
|
</NodeToolbar>
|
|
@@ -163,7 +170,7 @@ function onViewMouseDown(e) {
|
|
|
163
170
|
}"
|
|
164
171
|
@mouseenter="onHoverEnter"
|
|
165
172
|
@mouseleave="onHoverLeave"
|
|
166
|
-
@dblclick
|
|
173
|
+
@dblclick="onDoubleClick"
|
|
167
174
|
>
|
|
168
175
|
<!-- View mode -->
|
|
169
176
|
<div
|
|
@@ -172,7 +179,11 @@ function onViewMouseDown(e) {
|
|
|
172
179
|
@mousedown="onViewMouseDown"
|
|
173
180
|
>
|
|
174
181
|
<div v-if="data.sticky.content" v-html="rendered" />
|
|
175
|
-
|
|
182
|
+
<!-- The hint is an instruction, not content, so it goes with the rest of the
|
|
183
|
+
affordances: on a read-only canvas it would name the one gesture
|
|
184
|
+
`startEdit` refuses, which is the false affordance AC-9 removes rather
|
|
185
|
+
than a smaller version of it. An empty note there simply reads empty. -->
|
|
186
|
+
<span v-else-if="editable" class="select-none text-stone-400">{{ t("integration.canvas.sticky.state.emptyHint") }}</span>
|
|
176
187
|
</div>
|
|
177
188
|
|
|
178
189
|
<!-- Edit mode — nodrag/nowheel so typing and scrolling never drag or zoom
|
|
@@ -181,9 +192,9 @@ function onViewMouseDown(e) {
|
|
|
181
192
|
v-else
|
|
182
193
|
ref="textareaEl"
|
|
183
194
|
v-model="draft"
|
|
184
|
-
aria-label="
|
|
195
|
+
:aria-label="t('integration.canvas.sticky.textarea.aria')"
|
|
185
196
|
class="nodrag nowheel h-full w-full resize-none bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-stone-500/40"
|
|
186
|
-
placeholder="
|
|
197
|
+
:placeholder="t('integration.canvas.sticky.textarea.placeholder')"
|
|
187
198
|
spellcheck="false"
|
|
188
199
|
@blur="commitEdit"
|
|
189
200
|
@keydown="onTextareaKeydown"
|
|
@@ -191,10 +202,13 @@ function onViewMouseDown(e) {
|
|
|
191
202
|
@dblclick.stop
|
|
192
203
|
/>
|
|
193
204
|
|
|
194
|
-
<!-- Bottom-right resize handle (hand-rolled; min 150x80)
|
|
205
|
+
<!-- Bottom-right resize handle (hand-rolled; min 150x80). Gone on a
|
|
206
|
+
read-only canvas: a handle that cannot resize is the same lie as a bin
|
|
207
|
+
that cannot delete. -->
|
|
195
208
|
<div
|
|
209
|
+
v-if="editable"
|
|
196
210
|
class="nodrag absolute bottom-0 right-0 flex size-4 cursor-se-resize items-end justify-end p-0.5 text-stone-400/80"
|
|
197
|
-
title="
|
|
211
|
+
:title="t('integration.canvas.sticky.action.resize')"
|
|
198
212
|
@pointerdown="onResizeStart"
|
|
199
213
|
@pointermove="onResizeMove"
|
|
200
214
|
@pointerup="onResizeEnd"
|
|
@@ -11,13 +11,23 @@ import type { WorkflowSticky } from '../../types/integrations.js';
|
|
|
11
11
|
* - NodeToolbar on hover/selection: 7-color pastel palette + delete.
|
|
12
12
|
* - Hand-rolled bottom-right resize handle (no @vue-flow/node-resizer dep);
|
|
13
13
|
* size persists once on pointer-up.
|
|
14
|
+
*
|
|
15
|
+
* **Absence of the callbacks is what makes a note read-only** — the run replay and
|
|
16
|
+
* the history preview hand it none, and then it draws no toolbar, no resize handle
|
|
17
|
+
* and cannot be opened for typing (behaviour specs/sticky-notes.md AC-9). They used
|
|
18
|
+
* to be handed no-ops instead, which left every one of those affordances on screen
|
|
19
|
+
* doing nothing: a reader could not tell "not allowed here" from "broken" (PO-284).
|
|
20
|
+
* `GroupFrameView` gates its own toolbar on the same absence.
|
|
14
21
|
*/
|
|
15
22
|
export interface StickyNodeData {
|
|
16
23
|
sticky: WorkflowSticky;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Persist a partial change into the blob (editor-side, syncs + pushes).
|
|
26
|
+
* Absent on a read-only canvas — see the note above.
|
|
27
|
+
*/
|
|
28
|
+
onUpdate?: (patch: Partial<Pick<WorkflowSticky, 'content' | 'size' | 'color'>>) => void;
|
|
29
|
+
/** Remove this sticky from the blob (editor-side). Absent when read-only. */
|
|
30
|
+
onRemove?: () => void;
|
|
21
31
|
}
|
|
22
32
|
type __VLS_Props = {
|
|
23
33
|
id: string;
|
|
@@ -11,13 +11,23 @@ const props = defineProps({
|
|
|
11
11
|
disabled: { type: Boolean, required: false }
|
|
12
12
|
});
|
|
13
13
|
const emit = defineEmits(["select"]);
|
|
14
|
+
const t = useIntegrationsT();
|
|
14
15
|
const NuxtLink = resolveComponent("NuxtLink");
|
|
15
16
|
const full = computed(() => props.density === "full");
|
|
16
17
|
const banner = computed(
|
|
17
18
|
() => full.value ? props.template.images.find((i) => i.category === "banner")?.src ?? null : null
|
|
18
19
|
);
|
|
19
20
|
const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
|
|
20
|
-
const
|
|
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="TRIGGER_ICON[
|
|
98
|
-
{{
|
|
107
|
+
<component :is="TRIGGER_ICON[triggerType] ?? Zap" :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">
|
|
@@ -11,6 +11,7 @@ const props = defineProps({
|
|
|
11
11
|
padding: { type: Number, required: false, default: 36 },
|
|
12
12
|
showErrorBranches: { type: Boolean, required: false, default: false }
|
|
13
13
|
});
|
|
14
|
+
const t = useIntegrationsT();
|
|
14
15
|
const STOP_SLUG = "revenexx:stop-and-error";
|
|
15
16
|
const NODE_W = 220;
|
|
16
17
|
const NODE_H = 58;
|
|
@@ -21,20 +22,20 @@ const boxes = computed(() => {
|
|
|
21
22
|
if (!blob) return [];
|
|
22
23
|
const out = [];
|
|
23
24
|
const triggers = blob.triggers ?? [];
|
|
24
|
-
triggers.forEach((
|
|
25
|
-
const pos = blob.triggerPositions?.[
|
|
26
|
-
const template = TRIGGER_TEMPLATES.find((tt) => tt.type ===
|
|
27
|
-
const subtitle =
|
|
25
|
+
triggers.forEach((t2, i) => {
|
|
26
|
+
const pos = blob.triggerPositions?.[t2.id] ?? { x: 40, y: 40 + i * 160 };
|
|
27
|
+
const template = TRIGGER_TEMPLATES.find((tt) => tt.type === t2.type);
|
|
28
|
+
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
29
|
out.push({
|
|
29
|
-
key: `t-${
|
|
30
|
+
key: `t-${t2.id}`,
|
|
30
31
|
kind: "trigger",
|
|
31
32
|
x: pos.x,
|
|
32
33
|
y: pos.y,
|
|
33
34
|
w: TRIG_W,
|
|
34
35
|
h: TRIG_H,
|
|
35
|
-
label: typeof
|
|
36
|
+
label: typeof t2.name === "string" && t2.name || t2.type.charAt(0).toUpperCase() + t2.type.slice(1),
|
|
36
37
|
subtitle,
|
|
37
|
-
type:
|
|
38
|
+
type: t2.type,
|
|
38
39
|
manifest: null,
|
|
39
40
|
icon: iconFor(template?.icon ?? "Zap")
|
|
40
41
|
});
|
|
@@ -169,7 +170,7 @@ const bounds = computed(() => {
|
|
|
169
170
|
</div>
|
|
170
171
|
</div>
|
|
171
172
|
<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
|
-
|
|
173
|
+
{{ t("integration.templates.preview.triggerLabel") }} · {{ b.type }}
|
|
173
174
|
</div>
|
|
174
175
|
</div>
|
|
175
176
|
|
|
@@ -192,7 +193,7 @@ const bounds = computed(() => {
|
|
|
192
193
|
</foreignObject>
|
|
193
194
|
</svg>
|
|
194
195
|
<div v-else class="flex h-full items-center justify-center text-sm text-muted-foreground">
|
|
195
|
-
<slot name="empty">
|
|
196
|
+
<slot name="empty">{{ t("integration.templates.preview.state.loading") }}</slot>
|
|
196
197
|
</div>
|
|
197
198
|
</div>
|
|
198
199
|
</template>
|