@n8n/frontend-module-inbox 0.2.3 → 0.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.
- package/README.md +25 -2
- package/package.json +12 -12
- package/src/components/InboxList.test.ts +53 -1
- package/src/components/InboxList.vue +7 -12
- package/src/components/InboxListItem.vue +54 -22
- package/src/inbox.constants.ts +3 -1
- package/src/inbox.module.ts +6 -1
- package/src/inbox.routes.test.ts +26 -0
- package/src/inbox.routes.ts +15 -2
- package/src/inbox.store.ts +8 -2
- package/src/promotions/PromotionReviewDetail.test.ts +244 -0
- package/src/promotions/PromotionReviewDetail.vue +395 -0
- package/src/promotions/promotionReviews.api.ts +15 -0
- package/src/self-healing/SelfHealingResultActivity.vue +160 -0
- package/src/self-healing/SelfHealingResultContent.test.ts +458 -0
- package/src/self-healing/SelfHealingResultContent.vue +430 -0
- package/src/self-healing/SelfHealingResultDetail.test.ts +804 -0
- package/src/self-healing/SelfHealingResultDetail.vue +340 -8
- package/src/self-healing/SelfHealingResultMetadata.vue +282 -0
- package/src/self-healing/selfHealingResult.store.test.ts +86 -0
- package/src/self-healing/selfHealingResult.store.ts +153 -0
- package/src/self-healing/selfHealingResults.api.test.ts +24 -0
- package/src/self-healing/selfHealingResults.api.ts +38 -0
- package/src/self-healing/selfHealingResults.test.utils.ts +106 -0
- package/src/views/InboxView.test.ts +126 -4
- package/src/views/InboxView.vue +26 -10
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @n8n/frontend-module-inbox
|
|
2
2
|
|
|
3
|
-
The shared Inbox lists workflow reviews and saved Assistant results. Review details, comments, and decisions live in `src/reviews`. The editor supplies
|
|
3
|
+
The shared Inbox lists workflow reviews and saved Assistant results. Review details, comments, and decisions live in `src/reviews`. The editor supplies the workflow diff renderer and Assistant chat handoff through the SDK. Backend services keep their existing access rules and actions.
|
|
4
4
|
|
|
5
5
|
Open has two groups: Waiting for review and Authored by you. Assistant results go
|
|
6
6
|
in Waiting for review. Each group has its own cursor, Load more button, and retry.
|
|
@@ -9,7 +9,30 @@ Closed is a flat list with infinite scroll. Group collapse state persists per us
|
|
|
9
9
|
Review decisions update the loaded rows and counts in place. Approving a selected
|
|
10
10
|
review opens the Closed tab, as before.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Assistant details live in `src/self-healing`. They show saved reports, workflow
|
|
13
|
+
changes, review activity, and usage. Result actions use the existing self-healing
|
|
14
|
+
API. Actions are available only while a result is open.
|
|
15
|
+
Fix ready shows Discard and close beside a Review menu: Apply and publish,
|
|
16
|
+
Continue with Assistant, and Continue with Editor.
|
|
17
|
+
Needs attention shows Discard and close beside a Continue in… menu with Continue
|
|
18
|
+
with Assistant and Continue with Editor. It has no publish action.
|
|
19
|
+
When a result has a suggestion, both menus show a note that tells what
|
|
20
|
+
continuing does.
|
|
21
|
+
Could not fix closes automatically and shows no actions.
|
|
22
|
+
Continuation applies a suggestion to the workflow draft, or dismisses a result
|
|
23
|
+
without one. It does not publish. The first successful action closes the result.
|
|
24
|
+
The frontend then updates the Inbox and opens the destination.
|
|
25
|
+
Chat uses the normal frontend Assistant handoff to create a private thread in the
|
|
26
|
+
reviewer's personal project. A reviewer can lack read access to the result's
|
|
27
|
+
project. The frontend loads the personal project before it closes the result.
|
|
28
|
+
If the project cannot load, the result stays open. The handoff sends the full
|
|
29
|
+
report and workflow attachment to start an Assistant run. If changes were
|
|
30
|
+
applied, the message says they are already in the draft.
|
|
31
|
+
Navigation failure does not reopen the result or undo saved changes.
|
|
32
|
+
After a lost response, the frontend refreshes the result and shows an error.
|
|
33
|
+
The user can retry the action if the result is still open.
|
|
34
|
+
A refresh does not navigate or retry publication.
|
|
35
|
+
Publishing remains a separate action. Self-healing remains off by default.
|
|
13
36
|
|
|
14
37
|
Frontend feature module. Consumed from source by the editor-ui shell through
|
|
15
38
|
`src/app/modules.manifest.ts`; there is no build step and no `dist`.
|
package/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n8n/frontend-module-inbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@n8n/api-types": "1.
|
|
11
|
-
"@n8n/composables": "1.
|
|
12
|
-
"@n8n/design-system": "2.
|
|
13
|
-
"@n8n/frontend-module-sdk": "0.
|
|
14
|
-
"@n8n/i18n": "2.
|
|
15
|
-
"@n8n/rest-api-client": "2.
|
|
16
|
-
"@n8n/stores": "2.
|
|
17
|
-
"n8n-workflow": "2.
|
|
10
|
+
"@n8n/api-types": "1.46.0",
|
|
11
|
+
"@n8n/composables": "1.36.0",
|
|
12
|
+
"@n8n/design-system": "2.45.0",
|
|
13
|
+
"@n8n/frontend-module-sdk": "0.16.0",
|
|
14
|
+
"@n8n/i18n": "2.46.0",
|
|
15
|
+
"@n8n/rest-api-client": "2.46.0",
|
|
16
|
+
"@n8n/stores": "2.46.0",
|
|
17
|
+
"n8n-workflow": "2.46.0",
|
|
18
18
|
"pinia": "^2.2.4",
|
|
19
19
|
"reka-ui": "^2.5.0",
|
|
20
20
|
"vue": "^3.5.13",
|
|
21
21
|
"vue-router": "^4.5.0",
|
|
22
|
-
"@n8n/frontend-constants": "0.
|
|
22
|
+
"@n8n/frontend-constants": "0.12.0",
|
|
23
23
|
"@vueuse/core": "^14.3.0",
|
|
24
24
|
"lodash": "4.18.1",
|
|
25
25
|
"vue-i18n": "^11.4.4"
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
"stylelint": "^16.23.0",
|
|
41
41
|
"typescript": "6.0.2",
|
|
42
42
|
"unplugin-icons": "^23.0.1",
|
|
43
|
-
"vite": "^8.
|
|
43
|
+
"vite": "^8.1.6",
|
|
44
44
|
"vite-svg-loader": "5.1.0",
|
|
45
45
|
"vitest": "5.0.1",
|
|
46
46
|
"vue-tsc": "^2.2.8",
|
|
47
47
|
"@pinia/testing": "^0.1.6",
|
|
48
48
|
"@testing-library/user-event": "^14.6.1",
|
|
49
49
|
"@types/lodash": "4.17.17",
|
|
50
|
-
"@n8n/utils": "1.
|
|
50
|
+
"@n8n/utils": "1.53.0"
|
|
51
51
|
},
|
|
52
52
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
53
53
|
"homepage": "https://n8n.io",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
InboxPromotionReviewItem,
|
|
3
|
+
InboxSelfHealingItem,
|
|
4
|
+
InboxWorkflowReviewItem,
|
|
5
|
+
} from '@n8n/api-types';
|
|
2
6
|
import { createComponentRenderer } from '@n8n/frontend-test-utils';
|
|
3
7
|
import { useUsersStore } from '@n8n/stores/users.store';
|
|
4
8
|
import userEvent from '@testing-library/user-event';
|
|
@@ -247,6 +251,54 @@ it.each([
|
|
|
247
251
|
},
|
|
248
252
|
);
|
|
249
253
|
|
|
254
|
+
it('renders promotion review rows with the branch as fallback title and the GitLab problem', () => {
|
|
255
|
+
const promotion = (props: Partial<InboxPromotionReviewItem>): InboxPromotionReviewItem => ({
|
|
256
|
+
type: 'promotion_review',
|
|
257
|
+
id: 'promotion',
|
|
258
|
+
state: 'closed',
|
|
259
|
+
reviewState: 'merged',
|
|
260
|
+
reviewType: 'gitlab',
|
|
261
|
+
title: 'Promote the thing',
|
|
262
|
+
webUrl: null,
|
|
263
|
+
branchName: 'n8n-promotion/1',
|
|
264
|
+
environmentName: null,
|
|
265
|
+
requester: null,
|
|
266
|
+
approver: null,
|
|
267
|
+
remoteError: null,
|
|
268
|
+
headCommitSha: null,
|
|
269
|
+
runCommitSha: null,
|
|
270
|
+
hasConflicts: false,
|
|
271
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
272
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
273
|
+
...props,
|
|
274
|
+
});
|
|
275
|
+
const { getByRole } = renderComponent({
|
|
276
|
+
props: {
|
|
277
|
+
sections: [
|
|
278
|
+
section('waiting', {
|
|
279
|
+
items: [
|
|
280
|
+
promotion({}),
|
|
281
|
+
promotion({
|
|
282
|
+
id: 'unanswered',
|
|
283
|
+
state: 'open',
|
|
284
|
+
reviewState: 'open',
|
|
285
|
+
title: null,
|
|
286
|
+
branchName: 'n8n-promotion/2',
|
|
287
|
+
remoteError: 'unreachable',
|
|
288
|
+
}),
|
|
289
|
+
],
|
|
290
|
+
}),
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
const [merged, unanswered] = within(getByRole('list')).getAllByRole('button');
|
|
295
|
+
expect(within(merged).getByText('Promote the thing')).toBeInTheDocument();
|
|
296
|
+
expect(within(merged).getByText('Promotion')).toBeInTheDocument();
|
|
297
|
+
expect(within(merged).getByRole('img', { name: 'Merged' }).className).toContain('merged');
|
|
298
|
+
expect(within(unanswered).getByText('n8n-promotion/2')).toBeInTheDocument();
|
|
299
|
+
expect(within(unanswered).getByText('GitLab unreachable')).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
250
302
|
it('separates partial-source recovery from a failed page retry', () => {
|
|
251
303
|
const { getByTestId, emitted } = renderComponent({
|
|
252
304
|
props: { sections: [section('waiting', { partial: true, error: new Error('Page failed') })] },
|
|
@@ -154,20 +154,15 @@ function onListBackgroundClick() {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
.header {
|
|
157
|
-
position: relative;
|
|
158
157
|
display: flex;
|
|
159
|
-
align-items:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
align-items: flex-end;
|
|
159
|
+
margin: 0 var(--spacing--md) var(--spacing--sm) 0;
|
|
160
|
+
/* The divider follows the bottom of the tabs, so the tab badges can change their height. */
|
|
161
|
+
border-bottom: var(--border);
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
left: 0;
|
|
168
|
-
right: var(--spacing--md);
|
|
169
|
-
bottom: calc(-1 * var(--review-tab-bar--indicator-overhang) - var(--border-width));
|
|
170
|
-
border-bottom: var(--border-width) solid var(--border-color);
|
|
163
|
+
> :global(.n8n-tabs) {
|
|
164
|
+
/* The active-tab indicator covers the divider, as in the detail tab bars. */
|
|
165
|
+
margin-bottom: calc(-1 * var(--border-width));
|
|
171
166
|
}
|
|
172
167
|
}
|
|
173
168
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { InboxItem, InboxSelfHealingItem } from '@n8n/api-types';
|
|
2
|
+
import type { InboxItem, InboxSelfHealingItem, PromotionReviewType } from '@n8n/api-types';
|
|
3
3
|
import {
|
|
4
4
|
N8nAssistantAvatar,
|
|
5
5
|
N8nBadge,
|
|
6
6
|
N8nCard,
|
|
7
|
-
N8nIcon,
|
|
8
7
|
N8nText,
|
|
9
8
|
N8nTimeAgo,
|
|
9
|
+
type IconName,
|
|
10
10
|
} from '@n8n/design-system';
|
|
11
11
|
import { useI18n } from '@n8n/i18n';
|
|
12
12
|
import { useRootStore } from '@n8n/stores/useRootStore';
|
|
@@ -18,6 +18,20 @@ const emit = defineEmits<{ select: [] }>();
|
|
|
18
18
|
const i18n = useI18n();
|
|
19
19
|
const rootStore = useRootStore();
|
|
20
20
|
|
|
21
|
+
// A promotion review shows the icon of its Git host. A new host needs an icon here.
|
|
22
|
+
const reviewTypeIcon = { gitlab: 'gitlab' } satisfies Record<PromotionReviewType, IconName>;
|
|
23
|
+
|
|
24
|
+
function itemTitle(item: InboxItem) {
|
|
25
|
+
switch (item.type) {
|
|
26
|
+
case 'workflow_review':
|
|
27
|
+
return item.title;
|
|
28
|
+
case 'self_healing_result':
|
|
29
|
+
return item.summary;
|
|
30
|
+
case 'promotion_review':
|
|
31
|
+
return item.title ?? item.branchName ?? i18n.baseText('inbox.promotionReview.untitled');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
function outcomeLabel(item: InboxSelfHealingItem) {
|
|
22
36
|
switch (item.outcome) {
|
|
23
37
|
case 'fix_ready':
|
|
@@ -57,13 +71,20 @@ function outcomeLabel(item: InboxSelfHealingItem) {
|
|
|
57
71
|
$style.cardTitle,
|
|
58
72
|
{ [$style.assistantTitle]: item.type === 'self_healing_result' },
|
|
59
73
|
]"
|
|
60
|
-
>{{ item
|
|
74
|
+
>{{ itemTitle(item) }}</N8nText
|
|
61
75
|
>
|
|
62
76
|
<WorkflowReviewStatusDot
|
|
63
77
|
v-if="item.type === 'workflow_review'"
|
|
64
78
|
:state="item.state"
|
|
65
79
|
:decision="item.decision"
|
|
66
80
|
/>
|
|
81
|
+
<span
|
|
82
|
+
v-else-if="item.type === 'promotion_review'"
|
|
83
|
+
:class="[$style.statusDot, $style[item.reviewState]]"
|
|
84
|
+
role="img"
|
|
85
|
+
:aria-label="i18n.baseText(`inbox.promotionReview.state.${item.reviewState}`)"
|
|
86
|
+
data-test-id="inbox-promotion-status"
|
|
87
|
+
/>
|
|
67
88
|
<span
|
|
68
89
|
v-else
|
|
69
90
|
:class="[
|
|
@@ -80,12 +101,28 @@ function outcomeLabel(item: InboxSelfHealingItem) {
|
|
|
80
101
|
/>
|
|
81
102
|
</div>
|
|
82
103
|
<div :class="$style.meta">
|
|
83
|
-
<N8nBadge
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
<N8nBadge
|
|
105
|
+
v-if="item.type === 'promotion_review'"
|
|
106
|
+
variant="outline"
|
|
107
|
+
:leading-icon="reviewTypeIcon[item.reviewType]"
|
|
108
|
+
:class="$style.workflowBadge"
|
|
109
|
+
>{{ i18n.baseText('inbox.promotionReview.badge') }}</N8nBadge
|
|
110
|
+
>
|
|
111
|
+
<N8nBadge
|
|
112
|
+
v-else-if="item.workflowName"
|
|
113
|
+
variant="outline"
|
|
114
|
+
leading-icon="workflow"
|
|
115
|
+
:class="$style.workflowBadge"
|
|
116
|
+
:title="item.workflowName"
|
|
117
|
+
>{{ item.workflowName }}</N8nBadge
|
|
118
|
+
>
|
|
88
119
|
<div :class="$style.metaActions">
|
|
120
|
+
<N8nText
|
|
121
|
+
v-if="item.type === 'promotion_review' && item.remoteError"
|
|
122
|
+
size="xsmall"
|
|
123
|
+
color="warning"
|
|
124
|
+
>{{ i18n.baseText(`inbox.promotionReview.remoteError.${item.remoteError}`) }}</N8nText
|
|
125
|
+
>
|
|
89
126
|
<N8nText :class="$style.time" size="xsmall" color="text-light">
|
|
90
127
|
<N8nTimeAgo :date="item.updatedAt" :locale="rootStore.defaultLocale" />
|
|
91
128
|
</N8nText>
|
|
@@ -165,20 +202,6 @@ function outcomeLabel(item: InboxSelfHealingItem) {
|
|
|
165
202
|
max-width: 100%;
|
|
166
203
|
}
|
|
167
204
|
}
|
|
168
|
-
.workflowBadgeText {
|
|
169
|
-
display: inline-flex;
|
|
170
|
-
align-items: center;
|
|
171
|
-
gap: var(--spacing--3xs);
|
|
172
|
-
max-width: 100%;
|
|
173
|
-
min-width: 0;
|
|
174
|
-
line-height: calc(var(--font-size--sm) + var(--border-width));
|
|
175
|
-
> span {
|
|
176
|
-
overflow: hidden;
|
|
177
|
-
text-overflow: ellipsis;
|
|
178
|
-
white-space: nowrap;
|
|
179
|
-
min-width: 0;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
205
|
.metaActions {
|
|
183
206
|
display: flex;
|
|
184
207
|
align-items: center;
|
|
@@ -205,4 +228,13 @@ function outcomeLabel(item: InboxSelfHealingItem) {
|
|
|
205
228
|
.closed {
|
|
206
229
|
background-color: var(--color--neutral-500);
|
|
207
230
|
}
|
|
231
|
+
.open {
|
|
232
|
+
background-color: var(--color--blue-500);
|
|
233
|
+
}
|
|
234
|
+
.changes_requested {
|
|
235
|
+
background-color: var(--color--yellow-500);
|
|
236
|
+
}
|
|
237
|
+
.merged {
|
|
238
|
+
background-color: var(--color--green-500);
|
|
239
|
+
}
|
|
208
240
|
</style>
|
package/src/inbox.constants.ts
CHANGED
|
@@ -2,13 +2,15 @@ import type { InboxSourceType, InboxState, WorkflowReviewRequestDecision } from
|
|
|
2
2
|
|
|
3
3
|
export const INBOX_VIEW = 'Inbox';
|
|
4
4
|
export const INBOX_ASSISTANT_RESULT_VIEW = 'InboxAssistantResult';
|
|
5
|
+
export const INBOX_PROMOTION_REVIEW_VIEW = 'InboxPromotionReview';
|
|
5
6
|
export const INBOX_PAGE_LIMIT = 15;
|
|
6
7
|
|
|
7
8
|
export type InboxSectionKey = 'waiting' | 'authored' | 'closed';
|
|
8
9
|
|
|
9
10
|
export type InboxSelection =
|
|
10
11
|
| { type: 'workflow_review'; id: string }
|
|
11
|
-
| { type: 'self_healing_result'; id: string; projectId: string; workflowId: string }
|
|
12
|
+
| { type: 'self_healing_result'; id: string; projectId: string; workflowId: string }
|
|
13
|
+
| { type: 'promotion_review'; id: string };
|
|
12
14
|
|
|
13
15
|
export type InboxItemChange = {
|
|
14
16
|
type: InboxSourceType;
|
package/src/inbox.module.ts
CHANGED
|
@@ -16,7 +16,7 @@ const inboxView = {
|
|
|
16
16
|
export const InboxModule = defineFrontendModule({
|
|
17
17
|
id: 'inbox',
|
|
18
18
|
name: 'Inbox',
|
|
19
|
-
description: 'Workflow reviews and saved Assistant results.',
|
|
19
|
+
description: 'Workflow reviews, promotion reviews, and saved Assistant results.',
|
|
20
20
|
icon: 'inbox',
|
|
21
21
|
routes: [
|
|
22
22
|
// Keep legacy /reviews links working after the move to Inbox.
|
|
@@ -51,5 +51,10 @@ export const InboxModule = defineFrontendModule({
|
|
|
51
51
|
path: '/inbox/assistant-results/:resultId',
|
|
52
52
|
name: 'InboxAssistantResult',
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
...inboxView,
|
|
56
|
+
path: '/inbox/promotion-reviews/:reviewId',
|
|
57
|
+
name: 'InboxPromotionReview',
|
|
58
|
+
},
|
|
54
59
|
],
|
|
55
60
|
});
|
package/src/inbox.routes.test.ts
CHANGED
|
@@ -59,6 +59,7 @@ it('redirects the old review list without selecting an item', async () => {
|
|
|
59
59
|
|
|
60
60
|
it.each([
|
|
61
61
|
['/inbox/reviews/review-1', { type: 'workflow_review', id: 'review-1' }],
|
|
62
|
+
['/inbox/promotion-reviews/review-1', { type: 'promotion_review', id: 'review-1' }],
|
|
62
63
|
[
|
|
63
64
|
'/inbox/assistant-results/result-1?projectId=project&workflowId=workflow',
|
|
64
65
|
{
|
|
@@ -87,12 +88,37 @@ it.each([
|
|
|
87
88
|
['Inbox', true],
|
|
88
89
|
['WorkflowReviewRequestsView', true],
|
|
89
90
|
['InboxAssistantResult', true],
|
|
91
|
+
['InboxPromotionReview', true],
|
|
90
92
|
['Workflow', false],
|
|
91
93
|
[undefined, false],
|
|
92
94
|
])('recognizes the Inbox route %s: %s', (name, expected) => {
|
|
93
95
|
expect(isInboxRoute({ name })).toBe(expected);
|
|
94
96
|
});
|
|
95
97
|
|
|
98
|
+
it('links a promotion review row to its detail route', () => {
|
|
99
|
+
const router = createInboxRouter();
|
|
100
|
+
const location = inboxItemLocation({
|
|
101
|
+
type: 'promotion_review',
|
|
102
|
+
id: 'review-1',
|
|
103
|
+
state: 'open',
|
|
104
|
+
reviewState: 'open',
|
|
105
|
+
reviewType: 'gitlab',
|
|
106
|
+
title: null,
|
|
107
|
+
webUrl: null,
|
|
108
|
+
branchName: null,
|
|
109
|
+
environmentName: null,
|
|
110
|
+
requester: null,
|
|
111
|
+
approver: null,
|
|
112
|
+
remoteError: null,
|
|
113
|
+
headCommitSha: null,
|
|
114
|
+
runCommitSha: null,
|
|
115
|
+
hasConflicts: false,
|
|
116
|
+
createdAt: '',
|
|
117
|
+
updatedAt: '',
|
|
118
|
+
});
|
|
119
|
+
expect(router.resolve(location).path).toBe('/inbox/promotion-reviews/review-1');
|
|
120
|
+
});
|
|
121
|
+
|
|
96
122
|
it('keeps the tab only when selecting the same item', async () => {
|
|
97
123
|
const router = createInboxRouter();
|
|
98
124
|
await router.push(
|
package/src/inbox.routes.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { InboxItem } from '@n8n/api-types';
|
|
2
2
|
import type { LocationQuery, RouteLocationNormalizedLoaded, RouteLocationRaw } from 'vue-router';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
INBOX_ASSISTANT_RESULT_VIEW,
|
|
6
|
+
INBOX_PROMOTION_REVIEW_VIEW,
|
|
7
|
+
INBOX_VIEW,
|
|
8
|
+
type InboxSelection,
|
|
9
|
+
} from './inbox.constants';
|
|
5
10
|
import { WORKFLOW_REVIEW_REQUESTS_VIEW } from './reviews/constants';
|
|
6
11
|
|
|
7
12
|
type InboxRoute = Pick<RouteLocationNormalizedLoaded, 'name' | 'params' | 'query'>;
|
|
@@ -14,7 +19,8 @@ export function isInboxRoute(route: Pick<InboxRoute, 'name'>): boolean {
|
|
|
14
19
|
return (
|
|
15
20
|
route.name === INBOX_VIEW ||
|
|
16
21
|
route.name === WORKFLOW_REVIEW_REQUESTS_VIEW ||
|
|
17
|
-
route.name === INBOX_ASSISTANT_RESULT_VIEW
|
|
22
|
+
route.name === INBOX_ASSISTANT_RESULT_VIEW ||
|
|
23
|
+
route.name === INBOX_PROMOTION_REVIEW_VIEW
|
|
18
24
|
);
|
|
19
25
|
}
|
|
20
26
|
|
|
@@ -23,6 +29,10 @@ export function selectionFromRoute({ name, params, query }: InboxRoute): InboxSe
|
|
|
23
29
|
const id = stringParam(params.reviewId);
|
|
24
30
|
return id ? { type: 'workflow_review', id } : null;
|
|
25
31
|
}
|
|
32
|
+
if (name === INBOX_PROMOTION_REVIEW_VIEW) {
|
|
33
|
+
const id = stringParam(params.reviewId);
|
|
34
|
+
return id ? { type: 'promotion_review', id } : null;
|
|
35
|
+
}
|
|
26
36
|
if (name !== INBOX_ASSISTANT_RESULT_VIEW) return null;
|
|
27
37
|
const id = stringParam(params.resultId);
|
|
28
38
|
if (!id) return null;
|
|
@@ -45,5 +55,8 @@ export function inboxItemLocation(item: InboxItem, route?: InboxRoute): RouteLoc
|
|
|
45
55
|
next.workflowId = item.workflowId;
|
|
46
56
|
return { name: INBOX_ASSISTANT_RESULT_VIEW, params: { resultId: item.id }, query: next };
|
|
47
57
|
}
|
|
58
|
+
if (item.type === 'promotion_review') {
|
|
59
|
+
return { name: INBOX_PROMOTION_REVIEW_VIEW, params: { reviewId: item.id }, query: next };
|
|
60
|
+
}
|
|
48
61
|
return { name: WORKFLOW_REVIEW_REQUESTS_VIEW, params: { reviewId: item.id }, query: next };
|
|
49
62
|
}
|
package/src/inbox.store.ts
CHANGED
|
@@ -15,7 +15,11 @@ import { fetchInbox, fetchInboxSummary } from './inbox.api';
|
|
|
15
15
|
import { INBOX_PAGE_LIMIT, type InboxItemChange, type InboxSectionKey } from './inbox.constants';
|
|
16
16
|
import { toError } from './reviews/workflowReviews.utils';
|
|
17
17
|
|
|
18
|
-
const sourceTypes: InboxSourceType[] = [
|
|
18
|
+
const sourceTypes: InboxSourceType[] = [
|
|
19
|
+
'workflow_review',
|
|
20
|
+
'self_healing_result',
|
|
21
|
+
'promotion_review',
|
|
22
|
+
];
|
|
19
23
|
|
|
20
24
|
type ListState = {
|
|
21
25
|
items: InboxItem[];
|
|
@@ -30,7 +34,9 @@ type ListState = {
|
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
function isSourceType(value: unknown): value is InboxSourceType {
|
|
33
|
-
return
|
|
37
|
+
return (
|
|
38
|
+
value === 'workflow_review' || value === 'self_healing_result' || value === 'promotion_review'
|
|
39
|
+
);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function disabledTypesFromError(error: unknown): InboxSourceType[] {
|