plugin-git-manager 1.1.9 → 1.1.12
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/dist/client/187.d5545b7cc8b90bfc.js +10 -0
- package/dist/client/components/RunReviewButton.d.ts +1 -1
- package/dist/client/context/GitManagerContext.d.ts +2 -0
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +6 -4
- package/dist/locale/en-US.json +10 -1
- package/dist/locale/vi-VN.json +2 -0
- package/dist/server/actions/git-actions.js +15 -12
- package/dist/server/actions/gitlab-api.js +14 -13
- package/dist/server/actions/review.d.ts +5 -2
- package/dist/server/actions/review.js +184 -37
- package/dist/server/ai-tools.js +2 -0
- package/dist/server/collections/gitCodeReviews.js +1 -0
- package/dist/server/collections/gitRepositories.js +12 -0
- package/dist/server/migrations/20260508000000-add-auto-review-flow-id.d.ts +6 -0
- package/dist/server/migrations/20260508000000-add-auto-review-flow-id.js +57 -0
- package/dist/server/plugin.d.ts +4 -0
- package/dist/server/plugin.js +43 -6
- package/dist/server/poller.js +3 -1
- package/package.json +1 -1
- package/src/client/components/CommitHistory.tsx +21 -3
- package/src/client/components/FileExplorer.tsx +29 -24
- package/src/client/components/GitOperations.tsx +32 -16
- package/src/client/components/PollingStatus.tsx +27 -1
- package/src/client/components/RepositoryConfig.tsx +76 -3
- package/src/client/components/ReviewFlows.tsx +11 -1
- package/src/client/components/ReviewHistory.tsx +14 -1
- package/src/client/components/RunReviewButton.tsx +375 -278
- package/src/client/context/GitManagerContext.tsx +2 -0
- package/src/client/index.tsx +31 -31
- package/src/locale/en-US.json +10 -1
- package/src/locale/vi-VN.json +2 -0
- package/src/server/actions/git-actions.ts +15 -12
- package/src/server/actions/gitlab-api.ts +8 -4
- package/src/server/actions/review.ts +226 -41
- package/src/server/ai-tools.ts +1 -0
- package/src/server/collections/gitCodeReviews.ts +1 -0
- package/src/server/collections/gitRepositories.ts +12 -0
- package/src/server/migrations/20260508000000-add-auto-review-flow-id.ts +29 -0
- package/src/server/plugin.ts +205 -164
- package/src/server/poller.ts +11 -2
- package/dist/client/187.eec7be93247463d7.js +0 -10
|
@@ -1,278 +1,375 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
import { Button, Dropdown, Modal, Form, Select, Input, message, Space, Tooltip, Tag, Alert } from 'antd';
|
|
3
|
-
import { RobotOutlined, MessageOutlined, ThunderboltOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
4
|
-
import { useAPIClient } from '@nocobase/client';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Button, Dropdown, Modal, Form, Select, Input, message, Space, Tooltip, Tag, Alert } from 'antd';
|
|
3
|
+
import { RobotOutlined, MessageOutlined, ThunderboltOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
4
|
+
import { useAPIClient } from '@nocobase/client';
|
|
5
|
+
import * as aiClient from '@nocobase/plugin-ai/client';
|
|
6
|
+
import { useT } from '../locale';
|
|
7
|
+
|
|
8
|
+
interface ReviewFlow {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
triggerMode: string;
|
|
13
|
+
aiEmployeeUsername?: string;
|
|
14
|
+
postMode: string;
|
|
15
|
+
repositoryId?: number;
|
|
16
|
+
llmService?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
instructions?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Target =
|
|
22
|
+
| { type: 'mr'; repositoryId: number; mrIid: number; title?: string }
|
|
23
|
+
| { type: 'commit'; repositoryId: number; commitSha: string; title?: string }
|
|
24
|
+
| { type: 'branch'; repositoryId: number; branch: string; title?: string };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Button that lets the user kick off a code review for a given target.
|
|
28
|
+
* Two paths:
|
|
29
|
+
* - "Run review" → POST gitManager:triggerReview (server-side, async)
|
|
30
|
+
* - "Open chat" → call (aiClient as any).useChatBoxActions?.() || {}.triggerTask if plugin-ai is loaded,
|
|
31
|
+
* so the user can chat with the AI employee using the same context.
|
|
32
|
+
*/
|
|
33
|
+
export const RunReviewButton: React.FC<{
|
|
34
|
+
target: Target;
|
|
35
|
+
size?: 'small' | 'middle' | 'large';
|
|
36
|
+
type?: 'default' | 'primary' | 'link';
|
|
37
|
+
onTriggered?: (reviewId: number) => void;
|
|
38
|
+
}> = ({ target, size = 'small', type = 'default', onTriggered }) => {
|
|
39
|
+
const t = useT();
|
|
40
|
+
const api = useAPIClient();
|
|
41
|
+
const aiConfigRepository = (aiClient as any).useAIConfigRepository?.() || {};
|
|
42
|
+
const { triggerTask } = (aiClient as any).useChatBoxActions?.() || {};
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
const [flows, setFlows] = useState<ReviewFlow[]>([]);
|
|
45
|
+
const [submitting, setSubmitting] = useState(false);
|
|
46
|
+
const [asking, setAsking] = useState(false);
|
|
47
|
+
const [existingReview, setExistingReview] = useState<any | null>(null);
|
|
48
|
+
const [form] = Form.useForm();
|
|
49
|
+
|
|
50
|
+
const loadExistingReview = useCallback(() => {
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
const filter: any = {
|
|
53
|
+
repositoryId: target.repositoryId,
|
|
54
|
+
targetType: target.type,
|
|
55
|
+
};
|
|
56
|
+
if (target.type === 'mr') filter.mrIid = target.mrIid;
|
|
57
|
+
if (target.type === 'commit') filter.commitSha = target.commitSha;
|
|
58
|
+
if (target.type === 'branch') filter.branch = target.branch;
|
|
59
|
+
|
|
60
|
+
api
|
|
61
|
+
.request({
|
|
62
|
+
url: 'gitCodeReviews:list',
|
|
63
|
+
params: {
|
|
64
|
+
pageSize: 1,
|
|
65
|
+
sort: ['-id'],
|
|
66
|
+
filter,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
.then((res) => {
|
|
70
|
+
if (cancelled) return;
|
|
71
|
+
const list = res?.data?.data || [];
|
|
72
|
+
setExistingReview(list[0] || null);
|
|
73
|
+
})
|
|
74
|
+
.catch(() => undefined);
|
|
75
|
+
return () => {
|
|
76
|
+
cancelled = true;
|
|
77
|
+
};
|
|
78
|
+
}, [api, target]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
return loadExistingReview();
|
|
82
|
+
}, [loadExistingReview]);
|
|
83
|
+
|
|
84
|
+
const loadFlows = useCallback(async () => {
|
|
85
|
+
const { data } = await api.request({
|
|
86
|
+
url: 'gitReviewFlows:list',
|
|
87
|
+
params: {
|
|
88
|
+
pageSize: 100,
|
|
89
|
+
filter: {
|
|
90
|
+
enabled: true,
|
|
91
|
+
$or: [{ repositoryId: target.repositoryId }, { repositoryId: null }],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const list: ReviewFlow[] = data?.data || [];
|
|
96
|
+
setFlows(list);
|
|
97
|
+
return list;
|
|
98
|
+
}, [api, target.repositoryId]);
|
|
99
|
+
|
|
100
|
+
const pickFlow = useCallback((list: ReviewFlow[]) => {
|
|
101
|
+
const repoFlow = list.find((f) => f.repositoryId === target.repositoryId);
|
|
102
|
+
return repoFlow ?? list[0] ?? null;
|
|
103
|
+
}, [target.repositoryId]);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!open) return;
|
|
107
|
+
let cancelled = false;
|
|
108
|
+
loadFlows()
|
|
109
|
+
.then((list) => {
|
|
110
|
+
if (cancelled) return;
|
|
111
|
+
const flow = pickFlow(list);
|
|
112
|
+
if (flow?.id) form.setFieldValue('flowId', flow.id);
|
|
113
|
+
})
|
|
114
|
+
.catch(() => undefined);
|
|
115
|
+
return () => {
|
|
116
|
+
cancelled = true;
|
|
117
|
+
};
|
|
118
|
+
}, [open, loadFlows, pickFlow, form]);
|
|
119
|
+
|
|
120
|
+
const handleRun = async () => {
|
|
121
|
+
try {
|
|
122
|
+
const values = await form.validateFields();
|
|
123
|
+
setSubmitting(true);
|
|
124
|
+
const params: any = {
|
|
125
|
+
repositoryId: target.repositoryId,
|
|
126
|
+
targetType: target.type,
|
|
127
|
+
flowId: values.flowId,
|
|
128
|
+
extraInstructions: values.extraInstructions || undefined,
|
|
129
|
+
};
|
|
130
|
+
if (target.type === 'mr') params.mrIid = target.mrIid;
|
|
131
|
+
if (target.type === 'commit') params.commitSha = target.commitSha;
|
|
132
|
+
if (target.type === 'branch') params.branch = target.branch;
|
|
133
|
+
const res = await api.request({
|
|
134
|
+
url: 'gitManager:triggerReview',
|
|
135
|
+
method: 'post',
|
|
136
|
+
data: params,
|
|
137
|
+
});
|
|
138
|
+
const reviewId = res?.data?.data?.reviewId;
|
|
139
|
+
message.success(t('Review started'));
|
|
140
|
+
setOpen(false);
|
|
141
|
+
form.resetFields();
|
|
142
|
+
loadExistingReview();
|
|
143
|
+
onTriggered?.(reviewId);
|
|
144
|
+
} catch (err: any) {
|
|
145
|
+
if (err?.errorFields) return; // validation error
|
|
146
|
+
message.error(err?.response?.data?.errors?.[0]?.message || err?.message || t('Failed to trigger review'));
|
|
147
|
+
} finally {
|
|
148
|
+
setSubmitting(false);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const buildWorkContext = () => {
|
|
153
|
+
const title = target.title || buildContextHint();
|
|
154
|
+
if (target.type === 'mr') {
|
|
155
|
+
return {
|
|
156
|
+
type: 'git-merge-request',
|
|
157
|
+
uid: `${target.repositoryId}:${target.mrIid}`,
|
|
158
|
+
title,
|
|
159
|
+
content: {
|
|
160
|
+
repositoryId: target.repositoryId,
|
|
161
|
+
mrIid: target.mrIid,
|
|
162
|
+
title,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (target.type === 'commit') {
|
|
167
|
+
return {
|
|
168
|
+
type: 'git-commit',
|
|
169
|
+
uid: `${target.repositoryId}:${target.commitSha}`,
|
|
170
|
+
title,
|
|
171
|
+
content: {
|
|
172
|
+
repositoryId: target.repositoryId,
|
|
173
|
+
commitSha: target.commitSha,
|
|
174
|
+
title,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
type: 'git-repository',
|
|
180
|
+
uid: String(target.repositoryId),
|
|
181
|
+
title,
|
|
182
|
+
content: {
|
|
183
|
+
repositoryId: target.repositoryId,
|
|
184
|
+
branch: target.branch,
|
|
185
|
+
title,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const buildChatPrompt = () => {
|
|
191
|
+
if (target.type === 'mr') {
|
|
192
|
+
return `Please review merge request !${target.mrIid} (${target.title || 'untitled'}). Use the attached Git merge request context and the available git tools when you need more detail.`;
|
|
193
|
+
}
|
|
194
|
+
if (target.type === 'commit') {
|
|
195
|
+
return `Please review commit ${target.commitSha} (${target.title || 'untitled'}). Use the attached Git commit context and the available git tools when you need more detail.`;
|
|
196
|
+
}
|
|
197
|
+
return `Please help me inspect branch ${target.branch}. Use the attached Git repository context and the available git tools when you need more detail.`;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const handleOpenChat = async () => {
|
|
201
|
+
if (!triggerTask || !aiConfigRepository?.getAIEmployees) {
|
|
202
|
+
message.warning(t('AI plugin is not available'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
setAsking(true);
|
|
206
|
+
try {
|
|
207
|
+
const list = flows.length ? flows : await loadFlows();
|
|
208
|
+
const flow = pickFlow(list);
|
|
209
|
+
if (!flow?.aiEmployeeUsername) {
|
|
210
|
+
message.warning(t('No matching flow available'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const employees: any[] = aiConfigRepository.aiEmployees?.length
|
|
214
|
+
? aiConfigRepository.aiEmployees
|
|
215
|
+
: await aiConfigRepository.getAIEmployees();
|
|
216
|
+
const aiEmployee = employees.find((item) => item.username === flow.aiEmployeeUsername);
|
|
217
|
+
if (!aiEmployee) {
|
|
218
|
+
message.warning(t('AI employee not found'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
await triggerTask({
|
|
222
|
+
aiEmployee,
|
|
223
|
+
tasks: [
|
|
224
|
+
{
|
|
225
|
+
title: `${flow.name}: ${buildContextHint()}`,
|
|
226
|
+
message: {
|
|
227
|
+
user: buildChatPrompt(),
|
|
228
|
+
system: flow.instructions || undefined,
|
|
229
|
+
workContext: [buildWorkContext()],
|
|
230
|
+
},
|
|
231
|
+
model: flow.llmService && flow.model ? { llmService: flow.llmService, model: flow.model } : null,
|
|
232
|
+
autoSend: false,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
} catch (err: any) {
|
|
237
|
+
message.error(err?.message || t('Failed to open AI chat'));
|
|
238
|
+
} finally {
|
|
239
|
+
setAsking(false);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const buildContextHint = () => {
|
|
244
|
+
if (target.type === 'mr') return `MR !${target.mrIid}`;
|
|
245
|
+
if (target.type === 'commit') return `Commit ${String(target.commitSha).slice(0, 7)}`;
|
|
246
|
+
return `Branch ${target.branch}`;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Derive re-run state for MR targets
|
|
250
|
+
const hasNewCommits =
|
|
251
|
+
existingReview &&
|
|
252
|
+
existingReview.headSha &&
|
|
253
|
+
existingReview.latestSha &&
|
|
254
|
+
existingReview.headSha !== existingReview.latestSha;
|
|
255
|
+
const isReReview = !!existingReview && existingReview.status !== 'pending';
|
|
256
|
+
const buttonLabel = isReReview
|
|
257
|
+
? hasNewCommits
|
|
258
|
+
? t('Re-review (new commits)')
|
|
259
|
+
: t('Re-run review')
|
|
260
|
+
: t('Code Review');
|
|
261
|
+
const ButtonIcon = isReReview ? ReloadOutlined : RobotOutlined;
|
|
262
|
+
|
|
263
|
+
const items = useMemo(() => [
|
|
264
|
+
{
|
|
265
|
+
key: 'run',
|
|
266
|
+
icon: <ThunderboltOutlined />,
|
|
267
|
+
label: isReReview ? t('Re-run automated review') : t('Run automated review'),
|
|
268
|
+
onClick: () => setOpen(true),
|
|
269
|
+
},
|
|
270
|
+
...(existingReview ? [{
|
|
271
|
+
key: 'view',
|
|
272
|
+
icon: <RobotOutlined />,
|
|
273
|
+
label: t(`Status: ${existingReview.status}`) + ' - ' + t('View in Review History tab'),
|
|
274
|
+
onClick: () => {
|
|
275
|
+
message.info(t('Please switch to the "Review History" tab to see the details.'));
|
|
276
|
+
},
|
|
277
|
+
}] : []),
|
|
278
|
+
{
|
|
279
|
+
key: 'chat',
|
|
280
|
+
icon: <MessageOutlined />,
|
|
281
|
+
label: t('Ask AI Employee'),
|
|
282
|
+
onClick: handleOpenChat,
|
|
283
|
+
disabled: asking,
|
|
284
|
+
},
|
|
285
|
+
], [isReReview, existingReview, asking, handleOpenChat]);
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<>
|
|
289
|
+
<Dropdown menu={{ items }} placement="bottomRight" trigger={['click']}>
|
|
290
|
+
<Button
|
|
291
|
+
size={size}
|
|
292
|
+
type={hasNewCommits ? 'primary' : type}
|
|
293
|
+
icon={<ButtonIcon />}
|
|
294
|
+
danger={!!hasNewCommits}
|
|
295
|
+
loading={asking}
|
|
296
|
+
>
|
|
297
|
+
{buttonLabel}
|
|
298
|
+
</Button>
|
|
299
|
+
</Dropdown>
|
|
300
|
+
<Modal
|
|
301
|
+
title={
|
|
302
|
+
<Space>
|
|
303
|
+
<RobotOutlined />
|
|
304
|
+
<span>{isReReview ? t('Re-run code review') : t('Run code review')}</span>
|
|
305
|
+
<span style={{ color: '#999', fontSize: 12, fontWeight: 400 }}>{buildContextHint()}</span>
|
|
306
|
+
</Space>
|
|
307
|
+
}
|
|
308
|
+
open={open}
|
|
309
|
+
onCancel={() => setOpen(false)}
|
|
310
|
+
onOk={handleRun}
|
|
311
|
+
okText={isReReview ? t('Re-run') : t('Start Review')}
|
|
312
|
+
confirmLoading={submitting}
|
|
313
|
+
destroyOnClose
|
|
314
|
+
>
|
|
315
|
+
{isReReview && (
|
|
316
|
+
<Alert
|
|
317
|
+
type={hasNewCommits ? 'warning' : 'info'}
|
|
318
|
+
showIcon
|
|
319
|
+
style={{ marginBottom: 12 }}
|
|
320
|
+
message={
|
|
321
|
+
hasNewCommits
|
|
322
|
+
? t('New commits detected since last review. Re-running will overwrite the existing review.')
|
|
323
|
+
: t('A review already exists for this target. Re-running will overwrite it.')
|
|
324
|
+
}
|
|
325
|
+
description={
|
|
326
|
+
<Space size={8} wrap style={{ fontSize: 12 }}>
|
|
327
|
+
{existingReview?.headSha && (
|
|
328
|
+
<span>
|
|
329
|
+
{t('Reviewed at')}:
|
|
330
|
+
<Tag style={{ fontFamily: 'monospace' }}>{String(existingReview.headSha).slice(0, 7)}</Tag>
|
|
331
|
+
</span>
|
|
332
|
+
)}
|
|
333
|
+
{hasNewCommits && existingReview?.latestSha && (
|
|
334
|
+
<span>
|
|
335
|
+
{t('Latest')}:
|
|
336
|
+
<Tag color="orange" style={{ fontFamily: 'monospace' }}>
|
|
337
|
+
{String(existingReview.latestSha).slice(0, 7)}
|
|
338
|
+
</Tag>
|
|
339
|
+
</span>
|
|
340
|
+
)}
|
|
341
|
+
</Space>
|
|
342
|
+
}
|
|
343
|
+
/>
|
|
344
|
+
)}
|
|
345
|
+
<Form form={form} layout="vertical">
|
|
346
|
+
<Form.Item
|
|
347
|
+
name="flowId"
|
|
348
|
+
label={t('Review Flow')}
|
|
349
|
+
rules={[{ required: true, message: t('Please select a review flow') }]}
|
|
350
|
+
extra={
|
|
351
|
+
flows.length === 0 ? (
|
|
352
|
+
<Tooltip title={t('Create a review flow in the Review Flows tab first')}>
|
|
353
|
+
<span style={{ color: '#faad14' }}>{t('No enabled flow found for this repository')}</span>
|
|
354
|
+
</Tooltip>
|
|
355
|
+
) : null
|
|
356
|
+
}
|
|
357
|
+
>
|
|
358
|
+
<Select
|
|
359
|
+
options={flows.map((f) => ({
|
|
360
|
+
value: f.id,
|
|
361
|
+
label: `${f.name}${f.aiEmployeeUsername ? ` — @${f.aiEmployeeUsername}` : ''}${
|
|
362
|
+
f.repositoryId == null ? ` (${t('global')})` : ''
|
|
363
|
+
}`,
|
|
364
|
+
}))}
|
|
365
|
+
placeholder={t('Select a review flow')}
|
|
366
|
+
/>
|
|
367
|
+
</Form.Item>
|
|
368
|
+
<Form.Item name="extraInstructions" label={t('Extra instructions (optional)')}>
|
|
369
|
+
<Input.TextArea rows={4} placeholder={t('e.g. Focus on security issues in authentication')} />
|
|
370
|
+
</Form.Item>
|
|
371
|
+
</Form>
|
|
372
|
+
</Modal>
|
|
373
|
+
</>
|
|
374
|
+
);
|
|
375
|
+
};
|