@revenexx/studio-integrations 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +638 -0
  2. package/README.md +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
  7. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
  8. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  9. package/dist/runtime/components/Integrations/ExpressionInput.vue +45 -19
  10. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  11. package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
  12. package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
  13. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
  14. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  16. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +88 -1
  18. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
  19. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +7 -3
  20. package/dist/runtime/components/Integrations/NodeInspector.vue +125 -54
  21. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +7 -3
  22. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  23. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  24. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  25. package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
  26. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  27. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  28. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  29. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  30. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  31. package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
  32. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  33. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  34. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  35. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  36. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  38. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  39. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  40. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  41. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  42. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  43. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  44. package/dist/runtime/components/Integrations/WorkflowEditor.vue +78 -30
  45. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  46. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  47. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  48. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  49. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  50. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  51. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  52. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  53. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  54. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  55. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  56. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  57. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  58. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  59. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  60. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  61. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  62. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  63. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  64. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  65. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  66. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  67. package/dist/runtime/composables/useIntegrationsTemplates.js +7 -5
  68. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  69. package/dist/runtime/composables/useIntegrationsWorkflows.js +10 -7
  70. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  71. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  72. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  73. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  74. package/dist/runtime/data/expressionEditor.js +11 -5
  75. package/dist/runtime/data/handleId.d.ts +61 -1
  76. package/dist/runtime/data/handleId.js +8 -1
  77. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  78. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  79. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  80. package/dist/runtime/data/nodeVersionMove.js +65 -0
  81. package/dist/runtime/data/payloadForm.js +3 -2
  82. package/dist/runtime/i18n/messages/de.json +79 -12
  83. package/dist/runtime/i18n/messages/en.json +79 -12
  84. package/dist/runtime/pages/integrations/audit.vue +7 -6
  85. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  86. package/dist/runtime/pages/integrations/index.vue +55 -8
  87. package/dist/runtime/pages/integrations/runs.vue +116 -7
  88. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  90. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +169 -24
  92. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  93. package/dist/runtime/types/integrations.d.ts +49 -1
  94. package/dist/runtime/utils/apiError.d.ts +21 -1
  95. package/dist/runtime/utils/apiError.js +1 -1
  96. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  97. package/dist/runtime/utils/buildSettle.js +29 -0
  98. package/dist/runtime/utils/clipboard.d.ts +33 -0
  99. package/dist/runtime/utils/clipboard.js +11 -0
  100. package/dist/runtime/utils/deepClone.d.ts +41 -0
  101. package/dist/runtime/utils/deepClone.js +3 -0
  102. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  103. package/dist/runtime/utils/formatDuration.js +13 -0
  104. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  105. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  106. package/dist/runtime/utils/notify.d.ts +1 -1
  107. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  108. package/dist/runtime/utils/runEnvelope.js +12 -0
  109. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  110. package/dist/runtime/utils/runFeedQuery.js +66 -0
  111. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  112. package/dist/runtime/utils/runOutputs.js +10 -0
  113. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  114. package/dist/runtime/utils/settingCondition.js +84 -0
  115. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  116. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  117. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  118. package/dist/runtime/utils/validationFindings.js +18 -3
  119. package/package.json +1 -1
@@ -1,19 +1,36 @@
1
1
  <script setup>
2
2
  import { computed, onMounted, onUnmounted, ref } from "vue";
3
- import { Badge } from "@revenexx/studio";
3
+ import {
4
+ Badge,
5
+ DropdownMenuItem,
6
+ DropdownMenuSeparator,
7
+ DropdownMenuSub,
8
+ DropdownMenuSubContent,
9
+ DropdownMenuSubTrigger
10
+ } from "@revenexx/studio";
11
+ import { DEFAULT_RUN_DIRECTION, DEFAULT_RUN_SORT } from "../../utils/runFeedQuery";
4
12
  import { RUN_STATUS_COLUMN_WIDTH, RUN_STATUS_WORDS, normalisedRunStatus, runStatusClass, runStatusLabel } from "../../utils/statusStyle";
13
+ import { formatDuration } from "../../utils/formatDuration";
5
14
  import { TRIGGER_TYPE_WORDS, triggerTypeLabel } from "../../data/integrationNodeCatalog";
6
15
  const props = defineProps({
7
16
  runs: { type: Array, required: true },
8
17
  loading: { type: Boolean, required: false, default: false },
9
18
  emptyText: { type: String, required: false },
10
19
  pagination: { type: Boolean, required: false, default: true },
11
- showWorkflow: { type: Boolean, required: false, default: true }
20
+ showWorkflow: { type: Boolean, required: false, default: true },
21
+ serverMode: { type: Boolean, required: false, default: false },
22
+ total: { type: Number, required: false, default: 0 },
23
+ perPage: { type: Number, required: false, default: 25 },
24
+ status: { type: Array, required: false },
25
+ triggerType: { type: String, required: false, default: "" },
26
+ sort: { type: null, required: false, default: DEFAULT_RUN_SORT },
27
+ direction: { type: String, required: false, default: DEFAULT_RUN_DIRECTION }
12
28
  });
13
- const emit = defineEmits(["changed"]);
29
+ const emit = defineEmits(["changed", "narrow", "search", "page-size", "reorder"]);
30
+ const page = defineModel("page", { type: Number, ...{ default: 1 } });
14
31
  const t = useIntegrationsT();
15
32
  const router = useRouter();
16
- const emptyText = computed(() => props.emptyText ?? t("integration.runs.table.empty"));
33
+ const emptyText = computed(() => props.emptyText ?? (anyNarrowing.value || searchText.value ? t("integration.runs.table.empty.narrowed") : t("integration.runs.table.empty")));
17
34
  const now = ref(Date.now());
18
35
  let tick = null;
19
36
  onMounted(() => {
@@ -39,12 +56,6 @@ function formatRelative(iso) {
39
56
  if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
40
57
  return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
41
58
  }
42
- function formatDuration(ms) {
43
- if (ms == null) return "\u2014";
44
- if (ms < 1e3) return `${ms}ms`;
45
- if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
46
- return `${(ms / 6e4).toFixed(1)}m`;
47
- }
48
59
  function liveDuration(run) {
49
60
  if (!run.startedAt) return null;
50
61
  const start = Date.parse(run.startedAt);
@@ -52,7 +63,7 @@ function liveDuration(run) {
52
63
  const end = run.finishedAt ? Date.parse(run.finishedAt) : now.value;
53
64
  return end - start;
54
65
  }
55
- const filterTabs = computed(() => props.pagination ? [
66
+ const filterTabs = computed(() => props.pagination && !props.serverMode ? [
56
67
  {
57
68
  key: "status",
58
69
  label: t("integration.runs.table.column.status"),
@@ -66,6 +77,54 @@ const filterTabs = computed(() => props.pagination ? [
66
77
  options: TRIGGER_TYPE_WORDS.map((v) => ({ value: v, label: triggerTypeLabel(v) }))
67
78
  }
68
79
  ] : void 0);
80
+ const chosenStatus = computed(() => props.status ?? []);
81
+ const anyNarrowing = computed(() => chosenStatus.value.length > 0 || props.triggerType !== "");
82
+ function setStatus(word) {
83
+ emit("narrow", { status: word ? [word] : [] });
84
+ }
85
+ function setTrigger(kind) {
86
+ emit("narrow", { triggerType: kind ?? "" });
87
+ }
88
+ const statusApplied = computed(() => chosenStatus.value[0] ? runStatusLabel(chosenStatus.value[0]) : "");
89
+ const triggerApplied = computed(() => props.triggerType ? triggerTypeLabel(props.triggerType) : "");
90
+ const searchText = ref("");
91
+ function onTableState(state) {
92
+ searchText.value = state.search;
93
+ emit("search", state.search);
94
+ if (state.pageSize !== props.perPage) emit("page-size", state.pageSize);
95
+ }
96
+ const tableSort = computed(() => ({
97
+ column: sortedColumn.value,
98
+ direction: sortedColumn.value ? props.direction : null
99
+ }));
100
+ function onTableSort(next) {
101
+ const sort = next.column ? SERVER_SORTS[next.column] : void 0;
102
+ if (!sort || !next.direction) {
103
+ emit("reorder", { sort: DEFAULT_RUN_SORT, direction: DEFAULT_RUN_DIRECTION });
104
+ return;
105
+ }
106
+ emit("reorder", { sort, direction: next.direction });
107
+ }
108
+ const SERVER_SORTS = {
109
+ started: "started_at",
110
+ duration: "duration"
111
+ };
112
+ function sortableColumn(key) {
113
+ return props.serverMode && key in SERVER_SORTS;
114
+ }
115
+ const sortedColumn = computed(() => {
116
+ const entry = Object.entries(SERVER_SORTS).find(([, value]) => value === props.sort);
117
+ return entry ? entry[0] : null;
118
+ });
119
+ const serverModels = computed(() => props.serverMode ? {
120
+ "sort": tableSort.value,
121
+ "onUpdate:sort": onTableSort,
122
+ // 1-based here, as the platform pages; the shared table counts from zero.
123
+ "page": page.value - 1,
124
+ "onUpdate:page": (p) => {
125
+ page.value = p + 1;
126
+ }
127
+ } : {});
69
128
  const columns = computed(() => [
70
129
  // Sorted and searched by the word on screen, not by the raw field: a column
71
130
  // with no `searchGetter` is searched through its `sortGetter`, so a German
@@ -74,17 +133,17 @@ const columns = computed(() => [
74
133
  // number guessed twice — see `RUN_STATUS_COLUMN_WIDTH` for how it was chosen and
75
134
  // why the track cannot be elastic. At the 100px it used to be, the German
76
135
  // "fehlgeschlagen" ran out of its cell and under the workflow name (PO-362).
77
- { key: "status", label: t("integration.runs.table.column.status"), width: RUN_STATUS_COLUMN_WIDTH, sortable: true, sortGetter: (r) => runStatusLabel(r.status) },
78
- ...props.showWorkflow ? [{ key: "workflow", label: t("integration.runs.table.column.workflow"), sortable: true, sortGetter: (r) => r.workflowName.toLowerCase(), searchGetter: (r) => r.workflowName }] : [],
136
+ { key: "status", label: t("integration.runs.table.column.status"), width: RUN_STATUS_COLUMN_WIDTH, sortable: sortableColumn("status"), sortGetter: (r) => runStatusLabel(r.status) },
137
+ ...props.showWorkflow ? [{ key: "workflow", label: t("integration.runs.table.column.workflow"), sortable: sortableColumn("workflow"), sortGetter: (r) => r.workflowName.toLowerCase(), searchGetter: (r) => r.workflowName }] : [],
79
138
  // Fixed floor wide enough for a full UUID, flexible above it — header and
80
139
  // body rows are separate grids, so a content-sized track (max-content)
81
140
  // would misalign them. The flex share also lets the table fill the width.
82
141
  { key: "id", label: t("integration.runs.table.column.id"), width: "minmax(280px,1fr)", searchGetter: (r) => `${r.temporalRunId ?? ""} ${r.id}` },
83
142
  // Same rule as the status column above: the word on screen is what sorts and
84
143
  // what a search has to match.
85
- { key: "trigger", label: t("integration.runs.table.column.trigger"), width: "120px", sortable: true, sortGetter: (r) => triggerTypeLabel(r.triggerType) },
86
- { key: "started", label: t("integration.runs.table.column.started"), width: "120px", sortable: true, sortGetter: (r) => r.startedAt },
87
- { key: "duration", label: t("integration.runs.table.column.duration"), width: "100px", sortable: true, sortGetter: (r) => r.durationMs ?? -1 },
144
+ { key: "trigger", label: t("integration.runs.table.column.trigger"), width: "120px", sortable: sortableColumn("trigger"), sortGetter: (r) => triggerTypeLabel(r.triggerType) },
145
+ { key: "started", label: t("integration.runs.table.column.started"), width: "120px", sortable: sortableColumn("started"), sortGetter: (r) => r.startedAt },
146
+ { key: "duration", label: t("integration.runs.table.column.duration"), width: "100px", sortable: sortableColumn("duration"), sortGetter: (r) => r.durationMs ?? -1 },
88
147
  { key: "actions", label: "", width: "52px" }
89
148
  ]);
90
149
  </script>
@@ -94,14 +153,66 @@ const columns = computed(() => [
94
153
  :data="runs"
95
154
  :columns="columns"
96
155
  :row-key="(r) => r.id"
97
- :default-sort="{ column: 'started', direction: 'desc' }"
98
156
  :search="{ placeholder: t('integration.runs.table.search.placeholder') }"
99
157
  :filter-tabs="filterTabs"
158
+ :filter="serverMode ? { width: 'w-72', active: anyNarrowing } : false"
159
+ :server-mode="serverMode"
160
+ :total="total"
100
161
  :loading="loading"
101
- :pagination="pagination"
162
+ :pagination="serverMode ? { pageSize: perPage } : pagination"
102
163
  :empty-text="emptyText"
164
+ v-bind="serverModels"
165
+ @state-change="onTableState"
103
166
  @row-click="(r) => openRun(r)"
104
167
  >
168
+ <!--
169
+ The narrowing, where a server-mode surface has to put it: the shared table
170
+ owns the popover, this page owns the selection. Its own in-memory control
171
+ (`filterTabs`) cannot be used here, because it compares the rows in hand and
172
+ those are one page.
173
+
174
+ Built to the same shape as that control on purpose — a dimension per
175
+ sub-menu, "All" first, one value at a time — so this list is narrowed by the
176
+ same gesture as the five that narrow what they hold, and one test helper
177
+ still addresses all six (`e2e/support/studio-table.ts`). What is NOT copied
178
+ is the count beside each option: the built-in counts the rows in hand, and
179
+ counting a page here would put a number next to "failed" that describes this
180
+ page rather than the history the option narrows.
181
+ -->
182
+ <template v-if="serverMode" #filter-menu>
183
+ <DropdownMenuSub>
184
+ <DropdownMenuSubTrigger class="gap-2">
185
+ <span class="flex-1">{{ t("integration.runs.table.column.status") }}</span>
186
+ <span v-if="statusApplied" class="text-xs text-primary">{{ statusApplied }}</span>
187
+ </DropdownMenuSubTrigger>
188
+ <DropdownMenuSubContent class="w-48">
189
+ <DropdownMenuItem @select="setStatus(null)">
190
+ {{ t("integration.runs.table.filter.all") }}
191
+ </DropdownMenuItem>
192
+ <DropdownMenuSeparator />
193
+ <DropdownMenuItem v-for="word in RUN_STATUS_WORDS" :key="word" @select="setStatus(word)">
194
+ {{ runStatusLabel(word) }}
195
+ </DropdownMenuItem>
196
+ </DropdownMenuSubContent>
197
+ </DropdownMenuSub>
198
+
199
+ <DropdownMenuSub>
200
+ <DropdownMenuSubTrigger class="gap-2">
201
+ <span class="flex-1">{{ t("integration.runs.table.column.trigger") }}</span>
202
+ <span v-if="triggerApplied" class="text-xs text-primary">{{ triggerApplied }}</span>
203
+ </DropdownMenuSubTrigger>
204
+ <DropdownMenuSubContent class="w-48">
205
+ <DropdownMenuItem @select="setTrigger(null)">
206
+ {{ t("integration.runs.table.filter.all") }}
207
+ </DropdownMenuItem>
208
+ <DropdownMenuSeparator />
209
+ <DropdownMenuItem v-for="kind in TRIGGER_TYPE_WORDS" :key="kind" @select="setTrigger(kind)">
210
+ {{ triggerTypeLabel(kind) }}
211
+ </DropdownMenuItem>
212
+ </DropdownMenuSubContent>
213
+ </DropdownMenuSub>
214
+ </template>
215
+
105
216
  <template #cell-status="{ row: run }">
106
217
  <!-- `max-w-full` is the net, not the fix: a word wider than the column above is
107
218
  then clipped inside its own cell instead of being drawn over the one beside
@@ -1,4 +1,5 @@
1
- import type { WorkflowRun } from '../../types/integrations.js';
1
+ import type { RunStatus, TriggerType, WorkflowRun } from '../../types/integrations.js';
2
+ import type { RunSort, RunSortDirection } from '../../utils/runFeedQuery.js';
2
3
  type __VLS_Props = {
3
4
  runs: WorkflowRun[];
4
5
  loading?: boolean;
@@ -7,14 +8,71 @@ type __VLS_Props = {
7
8
  pagination?: boolean;
8
9
  /** false inside a workflow — the context makes the column redundant. */
9
10
  showWorkflow?: boolean;
11
+ /**
12
+ * The owning list reads its page and its narrowing from the platform.
13
+ *
14
+ * The table then renders the rows it is handed and reports what the operator
15
+ * asked for, instead of narrowing and slicing an array. That is what lets the
16
+ * list reach past the newest hundred runs at all (PO-369).
17
+ */
18
+ serverMode?: boolean;
19
+ /** How many runs match the narrowing. The pager's "of 7"; server mode only. */
20
+ total?: number;
21
+ /**
22
+ * How many rows a page holds — the SAME number the page asks the platform for.
23
+ *
24
+ * The shared table works its footer's "showing 1–25 of 140" out from its own
25
+ * page size, so a table left on its default while the request asks for another
26
+ * number states a range that is simply wrong: it counted twenty where
27
+ * twenty-five had arrived.
28
+ */
29
+ perPage?: number;
30
+ /** The narrowing in force. Owned by the page, drawn here. */
31
+ status?: RunStatus[];
32
+ triggerType?: TriggerType | '';
33
+ /** The order the platform is reading the list in. Server mode only. */
34
+ sort?: RunSort;
35
+ direction?: RunSortDirection;
10
36
  };
11
- declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
37
+ type __VLS_PublicProps = __VLS_Props & {
38
+ /** 1-based, as the platform pages. The shared table counts from zero. */
39
+ 'page'?: number;
40
+ };
41
+ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
42
+ search: (text: string) => any;
12
43
  changed: () => any;
13
- }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
44
+ narrow: (patch: {
45
+ status?: RunStatus[];
46
+ triggerType?: TriggerType | "";
47
+ }) => any;
48
+ "page-size": (size: number) => any;
49
+ reorder: (order: {
50
+ sort: RunSort;
51
+ direction: RunSortDirection;
52
+ }) => any;
53
+ "update:page": (value: number) => any;
54
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
55
+ onSearch?: ((text: string) => any) | undefined;
14
56
  onChanged?: (() => any) | undefined;
57
+ onNarrow?: ((patch: {
58
+ status?: RunStatus[];
59
+ triggerType?: TriggerType | "";
60
+ }) => any) | undefined;
61
+ "onPage-size"?: ((size: number) => any) | undefined;
62
+ onReorder?: ((order: {
63
+ sort: RunSort;
64
+ direction: RunSortDirection;
65
+ }) => any) | undefined;
66
+ "onUpdate:page"?: ((value: number) => any) | undefined;
15
67
  }>, {
68
+ sort: RunSort;
16
69
  loading: boolean;
70
+ total: number;
71
+ direction: RunSortDirection;
72
+ triggerType: TriggerType | "";
73
+ perPage: number;
17
74
  pagination: boolean;
18
75
  showWorkflow: boolean;
76
+ serverMode: boolean;
19
77
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
78
  export default _default;
@@ -2,6 +2,7 @@
2
2
  import { computed, ref, watch } from "vue";
3
3
  import { RefreshCircle as Loader2 } from "#solar-icons";
4
4
  import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@revenexx/studio";
5
+ import LoadingAnnouncement from "./LoadingAnnouncement.vue";
5
6
  import { useIntegrationsCredentials } from "../../composables/useIntegrationsCredentials";
6
7
  const open = defineModel("open", { type: Boolean, ...{ default: false } });
7
8
  const emit = defineEmits(["created"]);
@@ -64,7 +65,15 @@ async function importAccount(account) {
64
65
  </DialogDescription>
65
66
  </DialogHeader>
66
67
 
67
- <div v-if="sftp.loading.value && !activeAccounts.length" class="space-y-2">
68
+ <!-- Mounts with the dialog, like the one in the create dialog and with the same
69
+ limit written down there (PO-251). -->
70
+ <LoadingAnnouncement
71
+ :label="t('integration.credentials.sftpImport.title')"
72
+ :loading="sftp.loading.value && !activeAccounts.length"
73
+ :text="t('integration.credentials.sftpImport.state.loading')"
74
+ />
75
+
76
+ <div v-if="sftp.loading.value && !activeAccounts.length" aria-hidden="true" class="space-y-2">
68
77
  <div v-for="n in 3" :key="`sk-${n}`" class="h-9 rounded-md bg-black/[0.07] animate-pulse" />
69
78
  </div>
70
79
  <p v-else-if="!activeAccounts.length" class="py-4 text-center text-sm text-muted-foreground">
@@ -0,0 +1,16 @@
1
+ import type { WorkflowStateRole, WorkflowStateVisibility } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ open: boolean;
4
+ /** Fixed by the `state-ref` field that opened this. */
5
+ role: WorkflowStateRole;
6
+ /** Names the workflow already declares, so a collision is caught while typing. */
7
+ taken: string[];
8
+ };
9
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:open": (args_0: boolean) => any;
11
+ declared: (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
14
+ onDeclared?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ export default _default;
@@ -0,0 +1,111 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import {
4
+ Button,
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ Input,
12
+ Label,
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue
18
+ } from "@revenexx/studio";
19
+ import { stateNamespaceErrorKey } from "../../utils/stateNamespaceName";
20
+ const props = defineProps({
21
+ open: { type: Boolean, required: true },
22
+ role: { type: String, required: true },
23
+ taken: { type: Array, required: true }
24
+ });
25
+ const emit = defineEmits(["update:open", "declared"]);
26
+ const t = useIntegrationsT();
27
+ const name = ref("");
28
+ const visibility = ref("private");
29
+ watch(() => props.open, (isOpen) => {
30
+ if (!isOpen) return;
31
+ name.value = "";
32
+ visibility.value = "private";
33
+ });
34
+ const error = computed(() => {
35
+ const key = stateNamespaceErrorKey(name.value, props.taken);
36
+ return key ? t(key) : null;
37
+ });
38
+ function declare() {
39
+ const value = name.value.trim();
40
+ if (!value || error.value) return;
41
+ emit("declared", value, props.role, visibility.value);
42
+ emit("update:open", false);
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
48
+ <DialogContent class="max-w-[94vw] sm:max-w-lg">
49
+ <DialogHeader>
50
+ <DialogTitle>{{ t("integration.nodeConfig.state.create.title") }}</DialogTitle>
51
+ <DialogDescription class="text-sm">
52
+ {{ t("integration.nodeConfig.state.create.description", { role: t(`integration.workflows.state.role.${role}.label`) }) }}
53
+ </DialogDescription>
54
+ </DialogHeader>
55
+
56
+ <div class="space-y-3">
57
+ <div class="space-y-1">
58
+ <Label for="state-create-name" class="text-mono text-muted-foreground">
59
+ {{ t("integration.workflows.state.field.newName.label") }}
60
+ </Label>
61
+ <Input
62
+ id="state-create-name"
63
+ v-model="name"
64
+ class="h-8 bg-white font-mono text-sm"
65
+ :placeholder="t('integration.workflows.state.field.newName.placeholder')"
66
+ @keydown.enter.prevent="declare"
67
+ />
68
+ <p v-if="error" class="text-xs text-rose-600">{{ error }}</p>
69
+ <p class="text-xs text-muted-foreground">{{ t(`integration.workflows.state.role.${role}.hint`) }}</p>
70
+ </div>
71
+
72
+ <div class="space-y-1">
73
+ <Label for="state-create-visibility" class="text-mono text-muted-foreground">
74
+ {{ t("integration.workflows.state.field.visibility.label") }}
75
+ </Label>
76
+ <Select v-model="visibility">
77
+ <SelectTrigger
78
+ id="state-create-visibility"
79
+ :aria-label="t('integration.workflows.state.field.visibility.label')"
80
+ class="h-8 bg-white text-sm"
81
+ >
82
+ <SelectValue />
83
+ </SelectTrigger>
84
+ <SelectContent>
85
+ <SelectItem value="private">
86
+ {{ t("integration.workflows.state.visibility.private.label") }}
87
+ </SelectItem>
88
+ <SelectItem value="shared">
89
+ {{ t("integration.workflows.state.visibility.shared.label") }}
90
+ </SelectItem>
91
+ </SelectContent>
92
+ </Select>
93
+ <!-- The same consequence the workflow-level dialog states, in the same
94
+ place relative to the control that chooses it. -->
95
+ <p class="text-xs text-muted-foreground">
96
+ {{ t(`integration.workflows.state.visibility.${visibility}.hint`) }}
97
+ </p>
98
+ </div>
99
+ </div>
100
+
101
+ <DialogFooter>
102
+ <Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
103
+ {{ t("integration.common.action.cancel") }}
104
+ </Button>
105
+ <Button size="sm" class="h-8" :disabled="!name.trim() || !!error" @click="declare">
106
+ {{ t("integration.workflows.state.action.add") }}
107
+ </Button>
108
+ </DialogFooter>
109
+ </DialogContent>
110
+ </Dialog>
111
+ </template>
@@ -0,0 +1,16 @@
1
+ import type { WorkflowStateRole, WorkflowStateVisibility } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ open: boolean;
4
+ /** Fixed by the `state-ref` field that opened this. */
5
+ role: WorkflowStateRole;
6
+ /** Names the workflow already declares, so a collision is caught while typing. */
7
+ taken: string[];
8
+ };
9
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:open": (args_0: boolean) => any;
11
+ declared: (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
14
+ onDeclared?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ export default _default;
@@ -171,7 +171,7 @@ function onViewMouseDown(e) {
171
171
  v-for="(c, i) in STICKY_COLORS"
172
172
  :key="i"
173
173
  type="button"
174
- class="size-5 rounded-full border transition hover:scale-110"
174
+ class="size-6 rounded-full border transition hover:scale-110"
175
175
  :class="(data.sticky.color ?? 0) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
176
176
  :style="{ backgroundColor: c.bg, borderColor: c.border }"
177
177
  :title="t(c.nameKey)"
@@ -21,10 +21,12 @@ import {
21
21
  } from "@revenexx/studio";
22
22
  import { toast } from "vue-sonner";
23
23
  import { TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
24
+ import LoadingAnnouncement from "./LoadingAnnouncement.vue";
24
25
  import TemplateCard from "./TemplateCard.vue";
25
26
  import WorkflowIconPicker from "./WorkflowIconPicker.vue";
26
27
  import WorkflowTagsInput from "./WorkflowTagsInput.vue";
27
28
  import { resolveLocalized } from "../../utils/localize";
29
+ import { errorReason } from "../../utils/apiError";
28
30
  const open = defineModel("open", { type: Boolean, ...{ default: false } });
29
31
  const t = useIntegrationsT();
30
32
  const router = useRouter();
@@ -102,7 +104,7 @@ async function submit() {
102
104
  open.value = false;
103
105
  await router.push(`/integrations/workflows/${created.id}`);
104
106
  } catch (e) {
105
- error.value = e.message || t("integration.workflows.create.error.generic");
107
+ error.value = errorReason(e, t("integration.workflows.create.error.generic"));
106
108
  } finally {
107
109
  submitting.value = false;
108
110
  }
@@ -126,7 +128,7 @@ async function useTemplate(slug) {
126
128
  open.value = false;
127
129
  await router.push(`/integrations/workflows/${id}`);
128
130
  } catch (e) {
129
- toast.error(e?.message ?? t("integration.workflows.create.toast.templateError"));
131
+ toast.error(errorReason(e, t("integration.workflows.create.toast.templateError")));
130
132
  } finally {
131
133
  instantiatingSlug.value = null;
132
134
  }
@@ -143,6 +145,19 @@ async function useTemplate(slug) {
143
145
  <!-- Switcher: the same TabBar the studio surfaces use. -->
144
146
  <TabBar v-model="mode" :tabs="modeTabs" class="-mt-1 shrink-0" />
145
147
 
148
+ <!-- Beside the switcher rather than inside the template half, and that is the
149
+ whole point: the dialog opens on the blank half, so a region placed in the
150
+ other branch would be inserted at the moment the reader switches to it —
151
+ with the catalogue already on its way and its own text already set, which is
152
+ a live region nothing was listening to. Out here it is in the dialog from
153
+ the moment it opens, and the text changes under a reader who is already
154
+ there (PO-251). -->
155
+ <LoadingAnnouncement
156
+ :label="t('integration.workflows.create.tab.template')"
157
+ :loading="mode === 'template' && templatesLoading && !templates.length"
158
+ :text="t('integration.workflows.create.template.state.loading')"
159
+ />
160
+
146
161
  <!-- ── Blank ─────────────────────────────────────────────────────────── -->
147
162
  <!-- -m-1/p-1 keeps the selection rings clear of the scroll clipping. -->
148
163
  <form v-if="mode === 'blank'" class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1" @submit.prevent="submit">
@@ -261,7 +276,7 @@ async function useTemplate(slug) {
261
276
  >
262
277
  </div>
263
278
 
264
- <div v-if="templatesLoading && !templates.length" class="grid gap-2 p-1 sm:grid-cols-2">
279
+ <div v-if="templatesLoading && !templates.length" aria-hidden="true" class="grid gap-2 p-1 sm:grid-cols-2">
265
280
  <div v-for="n in 6" :key="`sk-tpl-${n}`" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
266
281
  <div class="flex items-center gap-2">
267
282
  <div class="size-7 rounded-md bg-black/[0.07] animate-pulse" />
@@ -0,0 +1,18 @@
1
+ export type DeletableWorkflow = {
2
+ id: number;
3
+ name: string;
4
+ };
5
+ type __VLS_Props = {
6
+ workflow: DeletableWorkflow | null;
7
+ };
8
+ type __VLS_PublicProps = __VLS_Props & {
9
+ 'open'?: boolean;
10
+ };
11
+ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
+ "update:open": (value: boolean) => any;
13
+ deleted: (workflow: DeletableWorkflow) => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
16
+ onDeleted?: ((workflow: DeletableWorkflow) => any) | undefined;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ export default _default;
@@ -0,0 +1,70 @@
1
+ <script setup>
2
+ import { ref, watch } from "vue";
3
+ import { RefreshCircle as Loader2 } from "#solar-icons";
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogContent,
7
+ AlertDialogDescription,
8
+ AlertDialogFooter,
9
+ AlertDialogHeader,
10
+ AlertDialogTitle,
11
+ Button
12
+ } from "@revenexx/studio";
13
+ import { useIntegrationsWorkflows } from "../../composables/useIntegrationsWorkflows";
14
+ import { errorReason } from "../../utils/apiError";
15
+ const open = defineModel("open", { type: Boolean, ...{ default: false } });
16
+ const props = defineProps({
17
+ workflow: { type: [Object, null], required: true }
18
+ });
19
+ const emit = defineEmits(["deleted"]);
20
+ const t = useIntegrationsT();
21
+ const { remove } = useIntegrationsWorkflows();
22
+ const busy = ref(false);
23
+ const error = ref(null);
24
+ watch(open, (o) => {
25
+ if (o) error.value = null;
26
+ });
27
+ async function confirm() {
28
+ const wf = props.workflow;
29
+ if (!wf || busy.value) return;
30
+ busy.value = true;
31
+ error.value = null;
32
+ try {
33
+ await remove(wf.id);
34
+ open.value = false;
35
+ emit("deleted", wf);
36
+ } catch (e) {
37
+ error.value = errorReason(e, t("integration.workflows.delete.error.generic"));
38
+ } finally {
39
+ busy.value = false;
40
+ }
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <AlertDialog :open="open" @update:open="(v) => {
46
+ if (!v && !busy) open = false;
47
+ }">
48
+ <AlertDialogContent>
49
+ <AlertDialogHeader>
50
+ <AlertDialogTitle>{{ t("integration.workflows.delete.dialog.title") }}</AlertDialogTitle>
51
+ <AlertDialogDescription>
52
+ <span v-if="props.workflow">
53
+ <span class="font-medium text-foreground">{{ props.workflow.name }}</span>
54
+ {{ t("integration.workflows.delete.dialog.description") }}
55
+ </span>
56
+ </AlertDialogDescription>
57
+ </AlertDialogHeader>
58
+ <p v-if="error" class="text-sm text-rose-600">{{ error }}</p>
59
+ <AlertDialogFooter>
60
+ <Button variant="outline" :disabled="busy" @click="open = false">
61
+ {{ t("integration.common.action.cancel") }}
62
+ </Button>
63
+ <Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="busy" @click="confirm">
64
+ <Loader2 v-if="busy" :size="14" class="animate-spin" />
65
+ {{ busy ? t("integration.workflows.delete.action.deleting") : t("integration.common.action.delete") }}
66
+ </Button>
67
+ </AlertDialogFooter>
68
+ </AlertDialogContent>
69
+ </AlertDialog>
70
+ </template>
@@ -0,0 +1,18 @@
1
+ export type DeletableWorkflow = {
2
+ id: number;
3
+ name: string;
4
+ };
5
+ type __VLS_Props = {
6
+ workflow: DeletableWorkflow | null;
7
+ };
8
+ type __VLS_PublicProps = __VLS_Props & {
9
+ 'open'?: boolean;
10
+ };
11
+ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
+ "update:open": (value: boolean) => any;
13
+ deleted: (workflow: DeletableWorkflow) => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
16
+ onDeleted?: ((workflow: DeletableWorkflow) => any) | undefined;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ export default _default;
@@ -3,6 +3,7 @@ import { computed, ref, watch } from "vue";
3
3
  import { RefreshCircle as Loader2 } from "#solar-icons";
4
4
  import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input } from "@revenexx/studio";
5
5
  import { useIntegrationsWorkflows } from "../../composables/useIntegrationsWorkflows";
6
+ import { errorReason } from "../../utils/apiError";
6
7
  const open = defineModel("open", { type: Boolean, ...{ default: false } });
7
8
  const props = defineProps({
8
9
  workflow: { type: [Object, null], required: true }
@@ -29,7 +30,7 @@ async function submit() {
29
30
  open.value = false;
30
31
  emit("duplicated", copy);
31
32
  } catch (e) {
32
- error.value = e?.message || t("integration.workflows.duplicate.error.generic");
33
+ error.value = errorReason(e, t("integration.workflows.duplicate.error.generic"));
33
34
  } finally {
34
35
  busy.value = false;
35
36
  }