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
package/dist/server/plugin.js
CHANGED
|
@@ -37,11 +37,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
37
37
|
var plugin_exports = {};
|
|
38
38
|
__export(plugin_exports, {
|
|
39
39
|
PluginGitManagerServer: () => PluginGitManagerServer,
|
|
40
|
-
default: () => plugin_default
|
|
40
|
+
default: () => plugin_default,
|
|
41
|
+
ensureAutoReviewFlowSchema: () => ensureAutoReviewFlowSchema
|
|
41
42
|
});
|
|
42
43
|
module.exports = __toCommonJS(plugin_exports);
|
|
43
44
|
var import_server = require("@nocobase/server");
|
|
44
45
|
var import_path = require("path");
|
|
46
|
+
var import_sequelize = require("sequelize");
|
|
45
47
|
var gitActions = __toESM(require("./actions/git-actions"));
|
|
46
48
|
var gitlabApi = __toESM(require("./actions/gitlab-api"));
|
|
47
49
|
var reviewActions = __toESM(require("./actions/review"));
|
|
@@ -50,15 +52,22 @@ var import_review = require("./actions/review");
|
|
|
50
52
|
var import_ai_tools = require("./ai-tools");
|
|
51
53
|
var import_poller = require("./poller");
|
|
52
54
|
class PluginGitManagerServer extends import_server.Plugin {
|
|
55
|
+
async beforeLoad() {
|
|
56
|
+
await this.app.db.import({
|
|
57
|
+
directory: (0, import_path.resolve)(__dirname, "collections")
|
|
58
|
+
});
|
|
59
|
+
this.app.db.addMigrations({
|
|
60
|
+
namespace: this.name,
|
|
61
|
+
directory: (0, import_path.resolve)(__dirname, "migrations"),
|
|
62
|
+
context: { plugin: this }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
53
65
|
async load() {
|
|
54
66
|
const dayjsLib = require("dayjs");
|
|
55
67
|
const utcPlugin = require("dayjs/plugin/utc");
|
|
56
68
|
const timezonePlugin = require("dayjs/plugin/timezone");
|
|
57
69
|
dayjsLib.extend(utcPlugin);
|
|
58
70
|
dayjsLib.extend(timezonePlugin);
|
|
59
|
-
await this.db.import({
|
|
60
|
-
directory: (0, import_path.resolve)(__dirname, "collections")
|
|
61
|
-
});
|
|
62
71
|
this.app.resourceManager.define({
|
|
63
72
|
name: "gitManager",
|
|
64
73
|
actions: {
|
|
@@ -96,8 +105,15 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
96
105
|
}
|
|
97
106
|
return next();
|
|
98
107
|
});
|
|
108
|
+
(0, import_review.registerReviewQueue)(this.app);
|
|
99
109
|
(0, import_ai_tools.registerGitReviewAiTools)(this.app);
|
|
100
|
-
this.app.on("afterStart", () => {
|
|
110
|
+
this.app.on("afterStart", async () => {
|
|
111
|
+
await ensureAutoReviewFlowSchema(this.app).catch(
|
|
112
|
+
(err) => {
|
|
113
|
+
var _a, _b;
|
|
114
|
+
return (_b = (_a = this.app.log) == null ? void 0 : _a.error) == null ? void 0 : _b.call(_a, "plugin-git-manager: ensure schema error", err);
|
|
115
|
+
}
|
|
116
|
+
);
|
|
101
117
|
(0, import_review.recoverStuckReviews)(this.app).catch(
|
|
102
118
|
(err) => {
|
|
103
119
|
var _a, _b;
|
|
@@ -107,9 +123,11 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
107
123
|
(0, import_poller.startPoller)(this.app);
|
|
108
124
|
});
|
|
109
125
|
this.app.on("beforeStop", () => {
|
|
126
|
+
(0, import_review.unregisterReviewQueue)(this.app);
|
|
110
127
|
(0, import_poller.stopPoller)();
|
|
111
128
|
});
|
|
112
129
|
this.app.on("beforeDestroy", () => {
|
|
130
|
+
(0, import_review.unregisterReviewQueue)(this.app);
|
|
113
131
|
(0, import_poller.stopPoller)();
|
|
114
132
|
});
|
|
115
133
|
this.app.acl.registerSnippet({
|
|
@@ -188,16 +206,35 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
188
206
|
});
|
|
189
207
|
}
|
|
190
208
|
async install() {
|
|
209
|
+
var _a;
|
|
210
|
+
await ((_a = this.app.db.getCollection("gitRepositories")) == null ? void 0 : _a.sync());
|
|
191
211
|
}
|
|
192
212
|
async beforeDisable() {
|
|
213
|
+
(0, import_review.unregisterReviewQueue)(this.app);
|
|
193
214
|
(0, import_poller.stopPoller)();
|
|
194
215
|
}
|
|
195
216
|
async beforeUnload() {
|
|
217
|
+
(0, import_review.unregisterReviewQueue)(this.app);
|
|
196
218
|
(0, import_poller.stopPoller)();
|
|
197
219
|
}
|
|
198
220
|
}
|
|
221
|
+
async function ensureAutoReviewFlowSchema(app) {
|
|
222
|
+
var _a, _b, _c;
|
|
223
|
+
const sequelize = (_a = app.db) == null ? void 0 : _a.sequelize;
|
|
224
|
+
const queryInterface = (_b = sequelize == null ? void 0 : sequelize.getQueryInterface) == null ? void 0 : _b.call(sequelize);
|
|
225
|
+
if (!queryInterface) return;
|
|
226
|
+
const tablePrefix = ((_c = app.db.options) == null ? void 0 : _c.tablePrefix) || "";
|
|
227
|
+
const tableName = `${tablePrefix}gitRepositories`;
|
|
228
|
+
const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
|
|
229
|
+
if (!tableInfo || tableInfo.autoReviewFlowId) return;
|
|
230
|
+
await queryInterface.addColumn(tableName, "autoReviewFlowId", {
|
|
231
|
+
type: import_sequelize.DataTypes.INTEGER,
|
|
232
|
+
allowNull: true
|
|
233
|
+
});
|
|
234
|
+
}
|
|
199
235
|
var plugin_default = PluginGitManagerServer;
|
|
200
236
|
// Annotate the CommonJS export names for ESM import in node:
|
|
201
237
|
0 && (module.exports = {
|
|
202
|
-
PluginGitManagerServer
|
|
238
|
+
PluginGitManagerServer,
|
|
239
|
+
ensureAutoReviewFlowSchema
|
|
203
240
|
});
|
package/dist/server/poller.js
CHANGED
|
@@ -202,13 +202,15 @@ async function pollOneRepo(app, repo) {
|
|
|
202
202
|
sort: ["-repositoryId"]
|
|
203
203
|
});
|
|
204
204
|
if (!(flows == null ? void 0 : flows.length)) return { scanned: 0, triggered: 0 };
|
|
205
|
+
const primaryFlowId = repo.get("autoReviewFlowId");
|
|
206
|
+
const primaryFlow = primaryFlowId ? flows.find((flow) => Number(flow.get("id")) === Number(primaryFlowId)) : null;
|
|
205
207
|
const pat = repo.get("pat");
|
|
206
208
|
if (!pat) return { scanned: 0, triggered: 0 };
|
|
207
209
|
const lastPolledAt = repo.get("lastPolledAt");
|
|
208
210
|
const mrs = await listMergeRequests(repo, lastPolledAt);
|
|
209
211
|
let triggered = 0;
|
|
210
212
|
for (const mr of mrs) {
|
|
211
|
-
const flow = flows.find((f) => (0, import_review.branchMatches)(f, mr.source_branch));
|
|
213
|
+
const flow = primaryFlow && (0, import_review.branchMatches)(primaryFlow, mr.source_branch) ? primaryFlow : flows.find((f) => (0, import_review.branchMatches)(f, mr.source_branch));
|
|
212
214
|
if (!flow) continue;
|
|
213
215
|
const existing = await reviewsRepo.findOne({
|
|
214
216
|
filter: {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "Git Manager",
|
|
4
4
|
"displayName.zh-CN": "Git 管理器",
|
|
5
5
|
"description": "Manage Git repositories with PAT authentication - pull, push, fetch, diff, file browsing",
|
|
6
|
-
"version": "1.1.
|
|
6
|
+
"version": "1.1.12",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"main": "dist/server/index.js",
|
|
9
9
|
"files": [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useState, useCallback, memo } from 'react';
|
|
2
|
-
import { Table, Empty, Spin, Typography, Tag, Drawer, List, Button, Space, Select, theme } from 'antd';
|
|
2
|
+
import { Table, Empty, Spin, Typography, Tag, Drawer, List, Button, Space, Select, Input, theme } from 'antd';
|
|
3
3
|
import {
|
|
4
4
|
BranchesOutlined,
|
|
5
5
|
UserOutlined,
|
|
@@ -37,6 +37,7 @@ export const CommitHistory: React.FC = () => {
|
|
|
37
37
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
38
38
|
const [diffContent, setDiffContent] = useState<string | null>(null);
|
|
39
39
|
const [diffFile, setDiffFile] = useState<string | null>(null);
|
|
40
|
+
const [searchKeyword, setSearchKeyword] = useState('');
|
|
40
41
|
|
|
41
42
|
const loadHistory = useCallback(async () => {
|
|
42
43
|
if (!selectedRepo) return;
|
|
@@ -48,6 +49,9 @@ export const CommitHistory: React.FC = () => {
|
|
|
48
49
|
});
|
|
49
50
|
const responseData = data?.data?.data || data?.data;
|
|
50
51
|
setCommits(responseData?.all || []);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn('Failed to load commit history:', error);
|
|
54
|
+
setCommits([]);
|
|
51
55
|
} finally {
|
|
52
56
|
setLoading(false);
|
|
53
57
|
}
|
|
@@ -61,6 +65,12 @@ export const CommitHistory: React.FC = () => {
|
|
|
61
65
|
}
|
|
62
66
|
}, [selectedRepo]);
|
|
63
67
|
|
|
68
|
+
const filteredCommits = commits.filter((commit) => {
|
|
69
|
+
const keyword = searchKeyword.trim().toLowerCase();
|
|
70
|
+
if (!keyword) return true;
|
|
71
|
+
return String(commit.message || commit.subject || commit.body || '').toLowerCase().includes(keyword);
|
|
72
|
+
});
|
|
73
|
+
|
|
64
74
|
const openCommitDetail = async (commit: any) => {
|
|
65
75
|
setSelectedCommit(commit);
|
|
66
76
|
setDetailLoading(true);
|
|
@@ -174,14 +184,22 @@ export const CommitHistory: React.FC = () => {
|
|
|
174
184
|
options={branchList.map((b) => ({ label: b, value: b }))}
|
|
175
185
|
disabled
|
|
176
186
|
/>
|
|
177
|
-
<
|
|
187
|
+
<Input.Search
|
|
188
|
+
allowClear
|
|
189
|
+
size="small"
|
|
190
|
+
style={{ width: 280 }}
|
|
191
|
+
placeholder={t('Search commit title or message')}
|
|
192
|
+
value={searchKeyword}
|
|
193
|
+
onChange={(event) => setSearchKeyword(event.target.value)}
|
|
194
|
+
/>
|
|
195
|
+
<Text type="secondary">{filteredCommits.length} commits</Text>
|
|
178
196
|
</div>
|
|
179
197
|
|
|
180
198
|
{loading ? (
|
|
181
199
|
<Spin />
|
|
182
200
|
) : (
|
|
183
201
|
<Table
|
|
184
|
-
dataSource={
|
|
202
|
+
dataSource={filteredCommits}
|
|
185
203
|
columns={columns}
|
|
186
204
|
rowKey="hash"
|
|
187
205
|
size="small"
|
|
@@ -75,30 +75,35 @@ export const FileExplorer: React.FC = () => {
|
|
|
75
75
|
const loadTree = useCallback(
|
|
76
76
|
async (treePath = '', ref = currentRef) => {
|
|
77
77
|
if (!selectedRepo) return [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
78
|
+
try {
|
|
79
|
+
const { data } = await api.request({
|
|
80
|
+
url: 'gitManager:fileTree',
|
|
81
|
+
params: { repositoryId: selectedRepo.id, ref, treePath },
|
|
82
|
+
});
|
|
83
|
+
const responseData = data?.data || data || [];
|
|
84
|
+
const list = Array.isArray(responseData) ? responseData : (Array.isArray(responseData?.data) ? responseData.data : []);
|
|
85
|
+
|
|
86
|
+
return list.map((item: any) => ({
|
|
87
|
+
key: item.path,
|
|
88
|
+
title: (
|
|
89
|
+
<Text style={{ fontSize: 13 }}>
|
|
90
|
+
{item.name}
|
|
91
|
+
{item.type === 'blob' && item.size > 0 && (
|
|
92
|
+
<Text type="secondary" style={{ fontSize: 11, marginLeft: 8 }}>
|
|
93
|
+
{item.size > 1024 ? `${(item.size / 1024).toFixed(1)}KB` : `${item.size}B`}
|
|
94
|
+
</Text>
|
|
95
|
+
)}
|
|
96
|
+
</Text>
|
|
97
|
+
),
|
|
98
|
+
icon: getFileIcon(item.name, item.type),
|
|
99
|
+
isLeaf: item.type === 'blob',
|
|
100
|
+
filePath: item.path,
|
|
101
|
+
fileType: item.type,
|
|
102
|
+
}));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.warn('Failed to load file tree:', error);
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
102
107
|
},
|
|
103
108
|
[api, selectedRepo, currentRef],
|
|
104
109
|
);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import React, { useState, useCallback } from 'react';
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
2
|
import { Button, Space, Card, Typography, Tag, Empty, message, Spin, Descriptions, List, Badge, theme } from 'antd';
|
|
3
3
|
import {
|
|
4
4
|
CloudDownloadOutlined,
|
|
5
|
-
CloudUploadOutlined,
|
|
6
5
|
SyncOutlined,
|
|
7
6
|
BranchesOutlined,
|
|
8
7
|
CheckCircleOutlined,
|
|
@@ -37,6 +36,32 @@ export const GitOperations: React.FC = () => {
|
|
|
37
36
|
const [statusData, setStatusData] = useState<any>(null);
|
|
38
37
|
const [lastResult, setLastResult] = useState<{ action: string; success: boolean; message: string } | null>(null);
|
|
39
38
|
|
|
39
|
+
const loadStatus = useCallback(async () => {
|
|
40
|
+
if (!selectedRepo || selectedRepo.status !== 'connected') {
|
|
41
|
+
setStatusData(null);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setActionLoading('status');
|
|
45
|
+
try {
|
|
46
|
+
const { data } = await api.request({
|
|
47
|
+
url: 'gitManager:status',
|
|
48
|
+
method: 'post',
|
|
49
|
+
params: { repositoryId: selectedRepo.id },
|
|
50
|
+
});
|
|
51
|
+
const responseData = data?.data?.data || data?.data;
|
|
52
|
+
setStatusData(responseData);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn('Failed to load git status:', error);
|
|
55
|
+
setStatusData(null);
|
|
56
|
+
} finally {
|
|
57
|
+
setActionLoading(null);
|
|
58
|
+
}
|
|
59
|
+
}, [api, selectedRepo]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
loadStatus();
|
|
63
|
+
}, [loadStatus]);
|
|
64
|
+
|
|
40
65
|
const execAction = useCallback(
|
|
41
66
|
async (action: string) => {
|
|
42
67
|
if (!selectedRepo) return;
|
|
@@ -50,10 +75,9 @@ export const GitOperations: React.FC = () => {
|
|
|
50
75
|
});
|
|
51
76
|
setLastResult({ action, success: true, message: t('{{action}} completed successfully', { action }) });
|
|
52
77
|
message.success(t('{{action}} completed', { action }));
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
78
|
+
const responseData = data?.data?.data || data?.data;
|
|
79
|
+
if (action === 'status') setStatusData(responseData);
|
|
80
|
+
if (action === 'fetch' || action === 'pull') await loadStatus();
|
|
57
81
|
await refreshRepos();
|
|
58
82
|
} catch (err: any) {
|
|
59
83
|
const msg = err?.response?.data?.errors?.[0]?.message || t('{{action}} failed', { action });
|
|
@@ -63,7 +87,7 @@ export const GitOperations: React.FC = () => {
|
|
|
63
87
|
setActionLoading(null);
|
|
64
88
|
}
|
|
65
89
|
},
|
|
66
|
-
[api, selectedRepo, refreshRepos],
|
|
90
|
+
[api, selectedRepo, refreshRepos, loadStatus],
|
|
67
91
|
);
|
|
68
92
|
|
|
69
93
|
if (!selectedRepo) {
|
|
@@ -104,19 +128,11 @@ export const GitOperations: React.FC = () => {
|
|
|
104
128
|
>
|
|
105
129
|
{t('Pull')}
|
|
106
130
|
</Button>
|
|
107
|
-
<Button
|
|
108
|
-
size="large"
|
|
109
|
-
icon={<CloudUploadOutlined />}
|
|
110
|
-
loading={actionLoading === 'push'}
|
|
111
|
-
onClick={() => execAction('push')}
|
|
112
|
-
>
|
|
113
|
-
{t('Push')}
|
|
114
|
-
</Button>
|
|
115
131
|
<Button
|
|
116
132
|
size="large"
|
|
117
133
|
icon={<BranchesOutlined />}
|
|
118
134
|
loading={actionLoading === 'status'}
|
|
119
|
-
onClick={
|
|
135
|
+
onClick={loadStatus}
|
|
120
136
|
>
|
|
121
137
|
{t('Status')}
|
|
122
138
|
</Button>
|
|
@@ -24,12 +24,26 @@ export const PollingStatus: React.FC = () => {
|
|
|
24
24
|
const [status, setStatus] = useState<PollerStatus | null>(null);
|
|
25
25
|
const [loading, setLoading] = useState(false);
|
|
26
26
|
const [pollingId, setPollingId] = useState<number | 'all' | null>(null);
|
|
27
|
+
const [flows, setFlows] = useState<any[]>([]);
|
|
27
28
|
|
|
28
29
|
const reload = useCallback(async () => {
|
|
29
30
|
setLoading(true);
|
|
30
31
|
try {
|
|
31
|
-
const { data } = await
|
|
32
|
+
const [{ data }, flowsRes] = await Promise.all([
|
|
33
|
+
api.request({ url: 'gitManager:pollerStatus' }),
|
|
34
|
+
api.request({
|
|
35
|
+
url: 'gitReviewFlows:list',
|
|
36
|
+
params: {
|
|
37
|
+
pageSize: 100,
|
|
38
|
+
filter: {
|
|
39
|
+
enabled: true,
|
|
40
|
+
triggerMode: { $in: ['onMergeRequestCreated', 'both'] },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
]);
|
|
32
45
|
setStatus(data?.data || null);
|
|
46
|
+
setFlows(flowsRes?.data?.data || []);
|
|
33
47
|
await refreshRepos();
|
|
34
48
|
} finally {
|
|
35
49
|
setLoading(false);
|
|
@@ -162,6 +176,18 @@ export const PollingStatus: React.FC = () => {
|
|
|
162
176
|
pagination={false}
|
|
163
177
|
columns={[
|
|
164
178
|
{ title: t('Repository Name'), dataIndex: 'name' },
|
|
179
|
+
{
|
|
180
|
+
title: t('Primary Auto Flow'),
|
|
181
|
+
dataIndex: 'autoReviewFlowId',
|
|
182
|
+
width: 220,
|
|
183
|
+
render: (flowId: number | null, record: any) => {
|
|
184
|
+
const flow = flows.find((item) => Number(item.id) === Number(flowId));
|
|
185
|
+
const fallback = flows.find((item) => item.repositoryId === record.id || item.repositoryId == null);
|
|
186
|
+
if (flow) return <Tag color="blue">{flow.name}</Tag>;
|
|
187
|
+
if (fallback) return <Tag color="default">{t('Fallback')}: {fallback.name}</Tag>;
|
|
188
|
+
return <Tag color="red">{t('No matching flow available')}</Tag>;
|
|
189
|
+
},
|
|
190
|
+
},
|
|
165
191
|
{
|
|
166
192
|
title: t('Last Polled At'),
|
|
167
193
|
dataIndex: 'lastPolledAt',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Table, Button, Modal, Form, Input, Space, Tag, Popconfirm, Switch, Tooltip, message } from 'antd';
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { Table, Button, Modal, Form, Input, Space, Tag, Popconfirm, Switch, Tooltip, Select, message } from 'antd';
|
|
3
3
|
import { PlusOutlined, DeleteOutlined, LinkOutlined, RobotOutlined } from '@ant-design/icons';
|
|
4
4
|
import { useAPIClient } from '@nocobase/client';
|
|
5
5
|
import { useGitManager } from '../context/GitManagerContext';
|
|
@@ -13,6 +13,33 @@ export const RepositoryConfig: React.FC = () => {
|
|
|
13
13
|
const [editingRepo, setEditingRepo] = useState<any>(null);
|
|
14
14
|
const [form] = Form.useForm();
|
|
15
15
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
16
|
+
const [reviewFlows, setReviewFlows] = useState<any[]>([]);
|
|
17
|
+
|
|
18
|
+
const loadReviewFlows = useCallback(async () => {
|
|
19
|
+
const { data } = await api.request({
|
|
20
|
+
url: 'gitReviewFlows:list',
|
|
21
|
+
params: {
|
|
22
|
+
pageSize: 100,
|
|
23
|
+
filter: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
triggerMode: { $in: ['onMergeRequestCreated', 'both'] },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
setReviewFlows(data?.data || []);
|
|
30
|
+
}, [api]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
loadReviewFlows().catch(() => undefined);
|
|
34
|
+
}, [loadReviewFlows]);
|
|
35
|
+
|
|
36
|
+
const getAutoFlowOptions = (repoId: number) =>
|
|
37
|
+
reviewFlows
|
|
38
|
+
.filter((flow) => flow.repositoryId === repoId || flow.repositoryId == null)
|
|
39
|
+
.map((flow) => ({
|
|
40
|
+
value: flow.id,
|
|
41
|
+
label: `${flow.name}${flow.repositoryId == null ? ` (${t('global')})` : ''}`,
|
|
42
|
+
}));
|
|
16
43
|
|
|
17
44
|
const handleSave = async () => {
|
|
18
45
|
const values = await form.validateFields();
|
|
@@ -100,11 +127,15 @@ export const RepositoryConfig: React.FC = () => {
|
|
|
100
127
|
checked={!!v}
|
|
101
128
|
onChange={async (checked) => {
|
|
102
129
|
try {
|
|
130
|
+
const options = getAutoFlowOptions(record.id);
|
|
103
131
|
await api.request({
|
|
104
132
|
url: 'gitRepositories:update',
|
|
105
133
|
method: 'post',
|
|
106
134
|
params: { filterByTk: record.id },
|
|
107
|
-
data: {
|
|
135
|
+
data: {
|
|
136
|
+
autoReview: checked,
|
|
137
|
+
autoReviewFlowId: checked && !record.autoReviewFlowId ? options[0]?.value ?? null : record.autoReviewFlowId,
|
|
138
|
+
},
|
|
108
139
|
});
|
|
109
140
|
await refreshRepos();
|
|
110
141
|
} catch (err: any) {
|
|
@@ -114,6 +145,40 @@ export const RepositoryConfig: React.FC = () => {
|
|
|
114
145
|
/>
|
|
115
146
|
),
|
|
116
147
|
},
|
|
148
|
+
{
|
|
149
|
+
title: t('Primary Auto Flow'),
|
|
150
|
+
dataIndex: 'autoReviewFlowId',
|
|
151
|
+
key: 'autoReviewFlowId',
|
|
152
|
+
width: 220,
|
|
153
|
+
render: (value: number | null, record: any) => {
|
|
154
|
+
const options = getAutoFlowOptions(record.id);
|
|
155
|
+
return (
|
|
156
|
+
<Select
|
|
157
|
+
allowClear
|
|
158
|
+
size="small"
|
|
159
|
+
style={{ width: '100%' }}
|
|
160
|
+
disabled={!record.autoReview}
|
|
161
|
+
placeholder={t('Select a flow')}
|
|
162
|
+
value={value || undefined}
|
|
163
|
+
options={options}
|
|
164
|
+
notFoundContent={t('No matching flow available')}
|
|
165
|
+
onChange={async (flowId) => {
|
|
166
|
+
try {
|
|
167
|
+
await api.request({
|
|
168
|
+
url: 'gitRepositories:update',
|
|
169
|
+
method: 'post',
|
|
170
|
+
params: { filterByTk: record.id },
|
|
171
|
+
data: { autoReviewFlowId: flowId || null },
|
|
172
|
+
});
|
|
173
|
+
await refreshRepos();
|
|
174
|
+
} catch (err: any) {
|
|
175
|
+
message.error(err?.message || t('Failed to save'));
|
|
176
|
+
}
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
},
|
|
117
182
|
{
|
|
118
183
|
title: t('Status'),
|
|
119
184
|
dataIndex: 'status',
|
|
@@ -216,6 +281,14 @@ export const RepositoryConfig: React.FC = () => {
|
|
|
216
281
|
<Form.Item name="defaultBranch" label={t('Default Branch')}>
|
|
217
282
|
<Input placeholder="main" />
|
|
218
283
|
</Form.Item>
|
|
284
|
+
<Form.Item name="autoReviewFlowId" label={t('Primary Auto Review Flow')} extra={t('Only flows with automatic trigger modes are shown')}>
|
|
285
|
+
<Select
|
|
286
|
+
allowClear
|
|
287
|
+
placeholder={t('Select a flow')}
|
|
288
|
+
options={getAutoFlowOptions(editingRepo?.id)}
|
|
289
|
+
notFoundContent={t('No matching flow available')}
|
|
290
|
+
/>
|
|
291
|
+
</Form.Item>
|
|
219
292
|
</Form>
|
|
220
293
|
</Modal>
|
|
221
294
|
</div>
|
|
@@ -22,6 +22,15 @@ const POST_LABELS: Record<string, string> = {
|
|
|
22
22
|
disabled: 'Disabled',
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
const DEFAULT_INSTRUCTION = `Please review the code changes with a focus on:
|
|
26
|
+
1. Best Practices & Style:
|
|
27
|
+
- C#/VB.NET: Follow standard naming conventions (PascalCase for methods/properties, camelCase for fields). Ensure proper use of async/await (avoid .Result/.Wait()). Use LINQ efficiently.
|
|
28
|
+
- JS/TS: Use strict equality (===), prefer const/let. Ensure TypeScript types are explicit and avoid 'any'.
|
|
29
|
+
2. Performance: Watch out for N+1 queries, unnecessary loops, and memory leaks.
|
|
30
|
+
3. Security: Identify potential SQL injections, XSS vulnerabilities, and improper data validation.
|
|
31
|
+
4. Maintainability: Check for readability, SOLID principles, and DRY. Ensure meaningful naming.
|
|
32
|
+
5. Error Handling: Verify that exceptions are properly caught, handled, and logged.`;
|
|
33
|
+
|
|
25
34
|
export const ReviewFlows: React.FC = () => {
|
|
26
35
|
const t = useT();
|
|
27
36
|
const api = useAPIClient();
|
|
@@ -92,6 +101,7 @@ export const ReviewFlows: React.FC = () => {
|
|
|
92
101
|
enabled: true,
|
|
93
102
|
triggerMode: 'manual',
|
|
94
103
|
postMode: 'manual',
|
|
104
|
+
instructions: DEFAULT_INSTRUCTION,
|
|
95
105
|
});
|
|
96
106
|
setOpen(true);
|
|
97
107
|
};
|
|
@@ -259,7 +269,7 @@ export const ReviewFlows: React.FC = () => {
|
|
|
259
269
|
<Input placeholder="^(feature|hotfix)/.*$" />
|
|
260
270
|
</Form.Item>
|
|
261
271
|
<Form.Item name="instructions" label={t('Additional Instructions (optional)')}>
|
|
262
|
-
<Input.TextArea rows={
|
|
272
|
+
<Input.TextArea rows={8} placeholder={t('Extra guidance appended to every review prompt')} />
|
|
263
273
|
</Form.Item>
|
|
264
274
|
<Form.Item name="enabled" label={t('Enabled')} valuePropName="checked">
|
|
265
275
|
<Switch />
|
|
@@ -25,6 +25,7 @@ const POST_STATUS_COLOR: Record<string, string> = {
|
|
|
25
25
|
pending_approval: 'orange',
|
|
26
26
|
approved: 'cyan',
|
|
27
27
|
posted: 'green',
|
|
28
|
+
post_failed: 'red',
|
|
28
29
|
skipped: 'default',
|
|
29
30
|
rejected: 'red',
|
|
30
31
|
};
|
|
@@ -278,6 +279,7 @@ export const ReviewHistory: React.FC<{ initialFilter?: 'all' | 'pending_approval
|
|
|
278
279
|
{ value: 'pending_approval', label: t('pending_approval') },
|
|
279
280
|
{ value: 'approved', label: t('approved') },
|
|
280
281
|
{ value: 'posted', label: t('posted') },
|
|
282
|
+
{ value: 'post_failed', label: t('post_failed') },
|
|
281
283
|
{ value: 'skipped', label: t('skipped') },
|
|
282
284
|
{ value: 'rejected', label: t('rejected') },
|
|
283
285
|
]}
|
|
@@ -395,7 +397,9 @@ const ReviewDetailView: React.FC<{
|
|
|
395
397
|
const canApprove =
|
|
396
398
|
review.status === 'completed' &&
|
|
397
399
|
review.targetType === 'mr' &&
|
|
398
|
-
(review.postStatus === 'pending_approval' ||
|
|
400
|
+
(review.postStatus === 'pending_approval' ||
|
|
401
|
+
review.postStatus === 'approved' ||
|
|
402
|
+
review.postStatus === 'post_failed');
|
|
399
403
|
|
|
400
404
|
return (
|
|
401
405
|
<div>
|
|
@@ -417,6 +421,15 @@ const ReviewDetailView: React.FC<{
|
|
|
417
421
|
style={{ marginBottom: 16 }}
|
|
418
422
|
/>
|
|
419
423
|
)}
|
|
424
|
+
{review.status === 'completed' && review.postStatus === 'post_failed' && review.error && (
|
|
425
|
+
<Alert
|
|
426
|
+
type="error"
|
|
427
|
+
showIcon
|
|
428
|
+
message={t('Post Failed')}
|
|
429
|
+
description={<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{review.error}</pre>}
|
|
430
|
+
style={{ marginBottom: 16 }}
|
|
431
|
+
/>
|
|
432
|
+
)}
|
|
420
433
|
|
|
421
434
|
{/* Action bar */}
|
|
422
435
|
{canApprove && (
|