@revenexx/studio-integrations 3.3.0 → 3.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @revenexx/studio-integrations
2
2
 
3
+ ## 3.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e755223: A workflow's own Runs tab can be narrowed by outcome and by what started a run, with the same Filter control as the Runs page. The platform narrows the workflow's whole history before it pages it, so a failure from three pages back is found, and the pager counts the matches. A narrowing that matches nothing says so, instead of saying the workflow has never run.
8
+
9
+ ### Patch Changes
10
+
11
+ - 57b4430: A workflow's own Runs tab says what started each run. Every row there read "Manual", whatever had fired the run, because the list sent no trigger and the studio filled the gap with that word. The tab now shows the trigger the platform reports, and a dash where it reports none — a deleted trigger, or a run older than the platform recording it — on the tenant-wide Runs page as well. A run that is still going keeps its trigger while the list re-reads it.
12
+
3
13
  ## 3.3.0
4
14
 
5
15
  ### Minor Changes
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
3
  "configKey": "studioIntegrations",
4
- "version": "3.3.0",
4
+ "version": "3.4.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
@@ -39,6 +39,14 @@ type __VLS_Props = {
39
39
  /** The order the platform is reading the list in. Server mode or orderable only. */
40
40
  sort?: RunSort;
41
41
  direction?: RunSortDirection;
42
+ /**
43
+ * The owning list asks the platform to narrow it even though the table is not
44
+ * in server mode — the workflow's own Runs tab, which narrows on the server but
45
+ * keeps its own pager (PO-319). The table then offers the same narrowing control
46
+ * as the tenant-wide list and reports a choice through `narrow`. Server mode
47
+ * implies it.
48
+ */
49
+ narrowable?: boolean;
42
50
  };
43
51
  type __VLS_PublicProps = __VLS_Props & {
44
52
  /** 1-based, as the platform pages. The shared table counts from zero. */
@@ -81,5 +89,6 @@ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {},
81
89
  showWorkflow: boolean;
82
90
  serverMode: boolean;
83
91
  orderable: boolean;
92
+ narrowable: boolean;
84
93
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
85
94
  export default _default;
@@ -27,13 +27,14 @@ const props = defineProps({
27
27
  triggerType: { type: String, required: false, default: "" },
28
28
  orderable: { type: Boolean, required: false, default: false },
29
29
  sort: { type: null, required: false, default: DEFAULT_RUN_SORT },
30
- direction: { type: String, required: false, default: DEFAULT_RUN_DIRECTION }
30
+ direction: { type: String, required: false, default: DEFAULT_RUN_DIRECTION },
31
+ narrowable: { type: Boolean, required: false, default: false }
31
32
  });
32
33
  const emit = defineEmits(["changed", "narrow", "search", "page-size", "reorder"]);
33
34
  const page = defineModel("page", { type: Number, ...{ default: 1 } });
34
35
  const t = useIntegrationsT();
35
36
  const router = useRouter();
36
- const emptyText = computed(() => props.emptyText ?? (anyNarrowing.value || searchText.value ? t("integration.runs.table.empty.narrowed") : t("integration.runs.table.empty")));
37
+ const emptyText = computed(() => anyNarrowing.value || searchText.value ? t("integration.runs.table.empty.narrowed") : props.emptyText ?? t("integration.runs.table.empty"));
37
38
  const now = useNow();
38
39
  function openRun(r) {
39
40
  void router.push({ path: `/integrations/workflows/${r.workflowId}`, query: { run: r.id } });
@@ -45,7 +46,8 @@ function liveDuration(run) {
45
46
  const end = run.finishedAt ? Date.parse(run.finishedAt) : now.value;
46
47
  return end - start;
47
48
  }
48
- const filterTabs = computed(() => props.pagination && !props.serverMode ? [
49
+ const narrowsOnServer = computed(() => props.serverMode || props.narrowable);
50
+ const filterTabs = computed(() => props.pagination && !narrowsOnServer.value ? [
49
51
  {
50
52
  key: "status",
51
53
  label: t("integration.runs.table.column.status"),
@@ -140,7 +142,7 @@ const columns = computed(() => [
140
142
  :row-key="(r) => r.id"
141
143
  :search="{ placeholder: t('integration.runs.table.search.placeholder') }"
142
144
  :filter-tabs="filterTabs"
143
- :filter="serverMode ? { width: 'w-72', active: anyNarrowing } : false"
145
+ :filter="narrowsOnServer ? { width: 'w-72', active: anyNarrowing } : false"
144
146
  :server-mode="serverMode"
145
147
  :total="total"
146
148
  :loading="loading"
@@ -160,14 +162,15 @@ const columns = computed(() => [
160
162
  apply them together; this one picks one value per dimension and applies it on
161
163
  the spot, because each pick is a request to the platform and staging them
162
164
  would only delay the same round trip. So the shared narrowing helper does not
163
- reach this page and `runs.spec.ts` carries its own — said in both places
164
- rather than left to be discovered (`e2e/support/studio-table.ts`).
165
+ reach either run list, which share one of their own in `e2e/support/runFilter.ts`
166
+ — said in both places rather than left to be discovered
167
+ (`e2e/support/studio-table.ts`).
165
168
 
166
169
  What is still NOT copied is the count beside each option: the built-in counts
167
170
  the rows in hand, and counting a page here would put a number next to
168
171
  "failed" that describes this page rather than the history the option narrows.
169
172
  -->
170
- <template v-if="serverMode" #filter-menu>
173
+ <template v-if="narrowsOnServer" #filter-menu>
171
174
  <!--
172
175
  Two selects rather than the sub-menus this slot used to hold. Since
173
176
  `studio-shared` 0.3.2 the filter trigger opens a popover instead of a
@@ -242,9 +245,12 @@ const columns = computed(() => [
242
245
  </template>
243
246
 
244
247
  <template #cell-trigger="{ row: run }">
245
- <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">
248
+ <span v-if="run.triggerType" 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">
246
249
  {{ triggerTypeLabel(run.triggerType) }}
247
250
  </span>
251
+ <!-- What started the run is not known: nothing is said, rather than a guess
252
+ (specs/run-row.md AC-14). -->
253
+ <span v-else class="text-xs text-muted-foreground">—</span>
248
254
  </template>
249
255
 
250
256
  <template #cell-started="{ row: run }">
@@ -39,6 +39,14 @@ type __VLS_Props = {
39
39
  /** The order the platform is reading the list in. Server mode or orderable only. */
40
40
  sort?: RunSort;
41
41
  direction?: RunSortDirection;
42
+ /**
43
+ * The owning list asks the platform to narrow it even though the table is not
44
+ * in server mode — the workflow's own Runs tab, which narrows on the server but
45
+ * keeps its own pager (PO-319). The table then offers the same narrowing control
46
+ * as the tenant-wide list and reports a choice through `narrow`. Server mode
47
+ * implies it.
48
+ */
49
+ narrowable?: boolean;
42
50
  };
43
51
  type __VLS_PublicProps = __VLS_Props & {
44
52
  /** 1-based, as the platform pages. The shared table counts from zero. */
@@ -81,5 +89,6 @@ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {},
81
89
  showWorkflow: boolean;
82
90
  serverMode: boolean;
83
91
  orderable: boolean;
92
+ narrowable: boolean;
84
93
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
85
94
  export default _default;
@@ -1,5 +1,5 @@
1
1
  import type { RunStatus, RunStepRow, WorkflowRun } from '../types/integrations.js';
2
- import type { RunFeedQuery, RunSort, RunSortDirection } from '../utils/runFeedQuery.js';
2
+ import type { RunFeedQuery, RunSort, RunSortDirection, WorkflowRunsNarrowing } from '../utils/runFeedQuery.js';
3
3
  export type { RunFeedQuery };
4
4
  interface RunSummary {
5
5
  total: number;
@@ -27,6 +27,8 @@ export declare function useIntegrationsRuns(): {
27
27
  perPage?: number;
28
28
  sort?: RunSort;
29
29
  direction?: RunSortDirection;
30
+ /** Narrowed by the platform, before it pages — so `total` counts what is left. */
31
+ narrowing?: WorkflowRunsNarrowing;
30
32
  }) => Promise<{
31
33
  runs: WorkflowRun[];
32
34
  total: number;
@@ -38,7 +40,7 @@ export declare function useIntegrationsRuns(): {
38
40
  status?: RunStatus;
39
41
  }) => WorkflowRun[];
40
42
  get: (id: string) => WorkflowRun | null;
41
- fetchRun: (workflowId: number | string, runId: number | string, workflowName: string) => Promise<WorkflowRun>;
43
+ fetchRun: (workflowId: number | string, runId: number | string, workflowName: string, listed?: WorkflowRun) => Promise<WorkflowRun>;
42
44
  latestPerWorkflow: any;
43
45
  latestLoaded: any;
44
46
  latestLoading: any;
@@ -4,6 +4,7 @@ import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { errorReason } from "../utils/apiError.js";
5
5
  import { bareRecord } from "../utils/apiEnvelope.js";
6
6
  import { RUN_OWN_FIELDS } from "../utils/runEnvelope.js";
7
+ import { keepListTrigger, mapRun } from "../utils/runRecord.js";
7
8
  import { useLoadGate } from "./useLoadGate.js";
8
9
  import {
9
10
  DEFAULT_RUN_DIRECTION,
@@ -42,39 +43,6 @@ export function useIntegrationsRuns() {
42
43
  const SYNC_LIMIT = 5;
43
44
  const api = useIntegrationsApi();
44
45
  const t = useIntegrationsT();
45
- function mapRun(r, workflowName = "") {
46
- const startedMs = r.started_at ? new Date(r.started_at).getTime() : null;
47
- const finishedMs = r.completed_at ? new Date(r.completed_at).getTime() : null;
48
- return {
49
- id: String(r.id),
50
- workflowId: r.workflow_id,
51
- workflowName: r.workflow_name ?? workflowName,
52
- status: mapRunStatus(r.status),
53
- // Kept beside the mapped word, because the mapping folds `terminated` into
54
- // `failed` and the platform resumes only the literal `failed` (PO-355).
55
- apiStatus: r.status,
56
- temporalRunId: r.temporal_run_id ?? null,
57
- triggerId: r.trigger_handle ?? "",
58
- // The platform reports what started the run. This was hardcoded to
59
- // `manual` because the payload carried nothing, which left the trigger
60
- // column and its filter with one populated value in production.
61
- // Null — the trigger was deleted, or the run predates trigger_handle, or the
62
- // row came from a per-workflow read, which sends no trigger at all — is read
63
- // as `manual` here although the platform said nothing (IN-9, api-contract
64
- // runs.feed F6 and runs.execution F2).
65
- triggerType: r.trigger_type ?? "manual",
66
- startedAt: r.started_at ?? "",
67
- finishedAt: r.completed_at ?? null,
68
- // The list carries the reason a run failed; dropping it here was why every
69
- // failure read the same wherever the list is shown. An empty string is not
70
- // a reason — it has to fall through to whatever the surface says instead,
71
- // or it renders as a blank line where the sentence belongs.
72
- errorMessage: r.error || void 0,
73
- // Derived from the timestamps — the list payload carries no duration.
74
- durationMs: startedMs != null && finishedMs != null && finishedMs >= startedMs ? finishedMs - startedMs : null,
75
- steps: []
76
- };
77
- }
78
46
  async function load(force = false) {
79
47
  if (!await gate.claim(force)) return runs.value;
80
48
  error.value = null;
@@ -103,7 +71,7 @@ export function useIntegrationsRuns() {
103
71
  const active = runs.value.filter((r) => r.status === "running" || r.status === "queued").sort((a, b) => Number(b.id) - Number(a.id)).slice(0, SYNC_LIMIT);
104
72
  await Promise.allSettled(active.map(async (r) => {
105
73
  try {
106
- const fresh = await fetchRun(r.workflowId, r.id, r.workflowName);
74
+ const fresh = await fetchRun(r.workflowId, r.id, r.workflowName, r);
107
75
  runs.value = runs.value.map((x) => x.id === fresh.id ? fresh : x);
108
76
  } catch {
109
77
  }
@@ -125,22 +93,26 @@ export function useIntegrationsRuns() {
125
93
  const page = opts.page ?? 1;
126
94
  const perPage = opts.perPage ?? 25;
127
95
  const res = await api.get(
128
- `/workflows/${workflowId}/runs?${workflowRunsQueryString(page, perPage, opts.sort, opts.direction)}`
96
+ `/workflows/${workflowId}/runs?${workflowRunsQueryString(page, perPage, opts.sort, opts.direction, opts.narrowing)}`
129
97
  );
130
98
  let list2 = (res?.data ?? []).map((r) => mapRun(r, workflowName));
131
99
  if (opts.reconcile) {
132
100
  const active = list2.filter((r) => r.status === "running" || r.status === "queued").sort((a, b) => Number(b.id) - Number(a.id)).slice(0, SYNC_LIMIT);
133
101
  await Promise.allSettled(active.map(async (r) => {
134
102
  try {
135
- const fresh = await fetchRun(workflowId, r.id, workflowName);
103
+ const fresh = await fetchRun(workflowId, r.id, workflowName, r);
136
104
  list2 = list2.map((x) => x.id === fresh.id ? fresh : x);
137
105
  } catch {
138
106
  }
139
107
  }));
140
108
  }
109
+ const wanted = opts.narrowing?.status ?? [];
110
+ const before = list2.length;
111
+ if (wanted.length) list2 = list2.filter((r) => wanted.includes(r.status));
112
+ const total2 = res?.meta?.total ?? before;
141
113
  return {
142
114
  runs: list2,
143
- total: res?.meta?.total ?? list2.length,
115
+ total: total2 - (before - list2.length),
144
116
  page: res?.meta?.current_page ?? page,
145
117
  lastPage: res?.meta?.last_page ?? 1
146
118
  };
@@ -202,9 +174,10 @@ export function useIntegrationsRuns() {
202
174
  const retryRun = (workflowId, runId) => runAction(workflowId, runId, "retry");
203
175
  const resumeRun = (workflowId, runId, reason) => runAction(workflowId, runId, "resume", reason);
204
176
  const getRun = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}`);
205
- async function fetchRun(workflowId, runId, workflowName) {
177
+ async function fetchRun(workflowId, runId, workflowName, listed) {
206
178
  const apiRun = bareRecord(await getRun(workflowId, runId), RUN_OWN_FIELDS);
207
- return mapRun(apiRun, workflowName);
179
+ const fresh = mapRun(apiRun, workflowName);
180
+ return listed ? keepListTrigger(fresh, listed) : fresh;
208
181
  }
209
182
  const getDetails = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/details`);
210
183
  const getSteps = (workflowId, runId, includePayloads = false) => api.get(
@@ -209,6 +209,8 @@ watch(id, async () => {
209
209
  if (!w) await leaveTo("/integrations/workflows");
210
210
  draft.value = null;
211
211
  warningsExpanded.value = false;
212
+ runsStatus.value = [];
213
+ runsTrigger.value = "";
212
214
  if (tab.value === "runs") void loadRuns(1);
213
215
  settleBuildStatus();
214
216
  }, { immediate: true });
@@ -530,6 +532,8 @@ const runsLastPage = ref(1);
530
532
  const runsWanted = ref(1);
531
533
  const runsSort = ref(DEFAULT_RUN_SORT);
532
534
  const runsDirection = ref(DEFAULT_RUN_DIRECTION);
535
+ const runsStatus = ref([]);
536
+ const runsTrigger = ref("");
533
537
  let runsSeq = 0;
534
538
  let runsLoadingSeq = 0;
535
539
  const RUNS_POLL_MS = 3e3;
@@ -634,7 +638,8 @@ async function loadRuns(page = runsWanted.value, opts = {}) {
634
638
  page,
635
639
  perPage: RUNS_PER_PAGE,
636
640
  sort: runsSort.value,
637
- direction: runsDirection.value
641
+ direction: runsDirection.value,
642
+ narrowing: { status: runsStatus.value, triggerType: runsTrigger.value }
638
643
  });
639
644
  if (seq !== runsSeq) return;
640
645
  wfRuns.value = res.runs;
@@ -655,6 +660,11 @@ function reorderRuns(order) {
655
660
  runsDirection.value = order.direction;
656
661
  void loadRuns(1);
657
662
  }
663
+ function narrowRuns(patch) {
664
+ if (patch.status !== void 0) runsStatus.value = patch.status;
665
+ if (patch.triggerType !== void 0) runsTrigger.value = patch.triggerType;
666
+ void loadRuns(1);
667
+ }
658
668
  function goRunsPage(p) {
659
669
  const next = Math.min(Math.max(1, p), runsLastPage.value);
660
670
  if (next !== runsWanted.value) void loadRuns(next);
@@ -1179,9 +1189,13 @@ async function confirmDeactivate() {
1179
1189
  orderable
1180
1190
  :sort="runsSort"
1181
1191
  :direction="runsDirection"
1192
+ narrowable
1193
+ :status="runsStatus"
1194
+ :trigger-type="runsTrigger"
1182
1195
  :empty-text="t('integration.editor.runs.empty')"
1183
1196
  @changed="() => loadRuns()"
1184
1197
  @reorder="reorderRuns"
1198
+ @narrow="narrowRuns"
1185
1199
  />
1186
1200
 
1187
1201
  <!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
@@ -822,7 +822,12 @@ export interface WorkflowRun {
822
822
  /** Temporal run id for this execution; null when the row predates Temporal. */
823
823
  temporalRunId?: string | null;
824
824
  triggerId: string;
825
- triggerType: TriggerType;
825
+ /**
826
+ * What started the run, as the platform reported it. Null where it reported
827
+ * nothing — the trigger was deleted, or the run predates it being recorded — and
828
+ * drawn as nothing rather than as a guess (PO-335).
829
+ */
830
+ triggerType: TriggerType | null;
826
831
  startedAt: string;
827
832
  finishedAt: string | null;
828
833
  durationMs: number | null;
@@ -122,13 +122,16 @@ export interface RunFeedQuery {
122
122
  * empty `status` as a value and answers 422.
123
123
  */
124
124
  export declare function runFeedQueryString(query: RunFeedQuery): string;
125
+ /** The narrowing a workflow's own list takes: the feed's, minus the text search. */
126
+ export type WorkflowRunsNarrowing = Pick<RunFeedQuery, 'status' | 'triggerType'>;
125
127
  /**
126
- * The query string for a workflow's own `GET /workflows/{id}/runs`: a page, and
127
- * the order it is read in. That endpoint narrows by nothing yet (PO-319), so this
128
- * is the paging and the order alone — the order sent by the same rule as the
129
- * tenant feed's, so the two lists can never ask for one order two ways.
128
+ * The query string for a workflow's own `GET /workflows/{id}/runs`: a page, the
129
+ * order it is read in, and the narrowing by outcome and trigger kind (PO-319) —
130
+ * each sent by the same rule as the tenant feed's, so the two lists can never ask
131
+ * for one thing two ways. No text search: that endpoint takes none, and the tab's
132
+ * search box searches the page in hand.
130
133
  */
131
- export declare function workflowRunsQueryString(page: number, perPage: number, sort?: RunSort, direction?: RunSortDirection): string;
134
+ export declare function workflowRunsQueryString(page: number, perPage: number, sort?: RunSort, direction?: RunSortDirection, narrowing?: WorkflowRunsNarrowing): string;
132
135
  /**
133
136
  * The narrowing alone, without the paging — what `GET /runs/summary` is read
134
137
  * with so the pager's total describes the same rows as the page beside it.
@@ -29,10 +29,11 @@ export function runFeedQueryString(query) {
29
29
  setOrder(params, query.sort, query.direction);
30
30
  return params.toString();
31
31
  }
32
- export function workflowRunsQueryString(page, perPage, sort = DEFAULT_RUN_SORT, direction = DEFAULT_RUN_DIRECTION) {
32
+ export function workflowRunsQueryString(page, perPage, sort = DEFAULT_RUN_SORT, direction = DEFAULT_RUN_DIRECTION, narrowing = { status: [], triggerType: "" }) {
33
33
  const params = new URLSearchParams();
34
34
  params.set("page", String(page));
35
35
  params.set("per_page", String(perPage));
36
+ for (const [key, value] of outcomeParams(narrowing)) params.set(key, value);
36
37
  setOrder(params, sort, direction);
37
38
  return params.toString();
38
39
  }
@@ -45,11 +46,15 @@ function setOrder(params, sort, direction) {
45
46
  export function runFeedNarrowingString(query) {
46
47
  return narrowingParams(query).toString();
47
48
  }
48
- function narrowingParams(query) {
49
+ function outcomeParams(narrowing) {
49
50
  const params = new URLSearchParams();
50
- const statuses = apiStatusesFor(query.status);
51
+ const statuses = apiStatusesFor(narrowing.status);
51
52
  if (statuses.length) params.set("status", statuses.join(","));
52
- if (query.triggerType) params.set("trigger_type", query.triggerType);
53
+ if (narrowing.triggerType) params.set("trigger_type", narrowing.triggerType);
54
+ return params;
55
+ }
56
+ function narrowingParams(query) {
57
+ const params = outcomeParams(query);
53
58
  const search = query.search.trim();
54
59
  if (search) params.set("search", search);
55
60
  return params;
@@ -0,0 +1,39 @@
1
+ import type { TriggerType, WorkflowRun } from '../types/integrations.js';
2
+ /**
3
+ * A run as either list sends it, and as the by-id read does. The three do not
4
+ * carry the same fields: see `mapRun` and `keepListTrigger` for what fills in.
5
+ */
6
+ export interface ApiRun {
7
+ id: number;
8
+ workflow_id: number;
9
+ status: string;
10
+ temporal_workflow_id?: string;
11
+ temporal_run_id?: string;
12
+ started_at?: string;
13
+ completed_at?: string;
14
+ /** Why the run failed, as the platform recorded it. Present in the list too. */
15
+ error?: string | null;
16
+ /**
17
+ * Present on the tenant feed, absent on the per-workflow subresource — which
18
+ * has no need for them, since the caller supplied the workflow and the tab
19
+ * shows one workflow's runs.
20
+ */
21
+ workflow_name?: string;
22
+ trigger_type?: TriggerType | null;
23
+ trigger_handle?: string | null;
24
+ }
25
+ /**
26
+ * `workflowName` is a FALLBACK, not the source. The tenant feed carries the
27
+ * name on the row; the per-workflow subresource does not, and its caller knows
28
+ * it. Passing it used to be the only way a row had a name at all.
29
+ */
30
+ export declare function mapRun(r: ApiRun, workflowName?: string): WorkflowRun;
31
+ /**
32
+ * A run re-read by id, keeping what started it from the row it replaces.
33
+ *
34
+ * The by-id read sends no trigger — only the two lists do — so swapping a
35
+ * reconciled run in whole blanked a schedule's run to no trigger at all, a row
36
+ * after the list had named it (api-contract runs.execution F2). What started a
37
+ * run does not change while it runs, so the list's word stays the right one.
38
+ */
39
+ export declare function keepListTrigger(fresh: WorkflowRun, listed: WorkflowRun): WorkflowRun;
@@ -0,0 +1,39 @@
1
+ import { mapRunStatus } from "./runFeedQuery.js";
2
+ export function mapRun(r, workflowName = "") {
3
+ const startedMs = r.started_at ? new Date(r.started_at).getTime() : null;
4
+ const finishedMs = r.completed_at ? new Date(r.completed_at).getTime() : null;
5
+ return {
6
+ id: String(r.id),
7
+ workflowId: r.workflow_id,
8
+ workflowName: r.workflow_name ?? workflowName,
9
+ status: mapRunStatus(r.status),
10
+ // Kept beside the mapped word, because the mapping folds `terminated` into
11
+ // `failed` and the platform resumes only the literal `failed` (PO-355).
12
+ apiStatus: r.status,
13
+ temporalRunId: r.temporal_run_id ?? null,
14
+ triggerId: r.trigger_handle ?? "",
15
+ // The platform reports what started the run, on both lists. Null — the
16
+ // trigger was deleted, or the run predates trigger_handle — stays null: it
17
+ // was read as `manual` once, and every row of a workflow's own Runs tab then
18
+ // claimed a start by hand, because that list sent no trigger at all (PO-335).
19
+ // A by-id read still sends none; see `keepListTrigger`.
20
+ triggerType: r.trigger_type ?? null,
21
+ startedAt: r.started_at ?? "",
22
+ finishedAt: r.completed_at ?? null,
23
+ // The list carries the reason a run failed; dropping it here was why every
24
+ // failure read the same wherever the list is shown. An empty string is not
25
+ // a reason — it has to fall through to whatever the surface says instead,
26
+ // or it renders as a blank line where the sentence belongs.
27
+ errorMessage: r.error || void 0,
28
+ // Derived from the timestamps — the list payload carries no duration.
29
+ durationMs: startedMs != null && finishedMs != null && finishedMs >= startedMs ? finishedMs - startedMs : null,
30
+ steps: []
31
+ };
32
+ }
33
+ export function keepListTrigger(fresh, listed) {
34
+ return {
35
+ ...fresh,
36
+ triggerId: fresh.triggerId || listed.triggerId,
37
+ triggerType: fresh.triggerType ?? listed.triggerType
38
+ };
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "type": "module",
5
5
  "description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
6
6
  "repository": {
@@ -63,7 +63,7 @@
63
63
  "eslint": "^10.11.0",
64
64
  "nuxt": "^4.2.2",
65
65
  "typescript": "5.9.3",
66
- "vitest": "^3.2.0",
66
+ "vitest": "^5.0.5",
67
67
  "vue": "^3.5.26",
68
68
  "vue-tsc": "^2.2.0"
69
69
  },