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.
Files changed (42) hide show
  1. package/dist/client/187.d5545b7cc8b90bfc.js +10 -0
  2. package/dist/client/components/RunReviewButton.d.ts +1 -1
  3. package/dist/client/context/GitManagerContext.d.ts +2 -0
  4. package/dist/client/index.js +1 -1
  5. package/dist/externalVersion.js +6 -4
  6. package/dist/locale/en-US.json +10 -1
  7. package/dist/locale/vi-VN.json +2 -0
  8. package/dist/server/actions/git-actions.js +15 -12
  9. package/dist/server/actions/gitlab-api.js +14 -13
  10. package/dist/server/actions/review.d.ts +5 -2
  11. package/dist/server/actions/review.js +184 -37
  12. package/dist/server/ai-tools.js +2 -0
  13. package/dist/server/collections/gitCodeReviews.js +1 -0
  14. package/dist/server/collections/gitRepositories.js +12 -0
  15. package/dist/server/migrations/20260508000000-add-auto-review-flow-id.d.ts +6 -0
  16. package/dist/server/migrations/20260508000000-add-auto-review-flow-id.js +57 -0
  17. package/dist/server/plugin.d.ts +4 -0
  18. package/dist/server/plugin.js +43 -6
  19. package/dist/server/poller.js +3 -1
  20. package/package.json +1 -1
  21. package/src/client/components/CommitHistory.tsx +21 -3
  22. package/src/client/components/FileExplorer.tsx +29 -24
  23. package/src/client/components/GitOperations.tsx +32 -16
  24. package/src/client/components/PollingStatus.tsx +27 -1
  25. package/src/client/components/RepositoryConfig.tsx +76 -3
  26. package/src/client/components/ReviewFlows.tsx +11 -1
  27. package/src/client/components/ReviewHistory.tsx +14 -1
  28. package/src/client/components/RunReviewButton.tsx +375 -278
  29. package/src/client/context/GitManagerContext.tsx +2 -0
  30. package/src/client/index.tsx +31 -31
  31. package/src/locale/en-US.json +10 -1
  32. package/src/locale/vi-VN.json +2 -0
  33. package/src/server/actions/git-actions.ts +15 -12
  34. package/src/server/actions/gitlab-api.ts +8 -4
  35. package/src/server/actions/review.ts +226 -41
  36. package/src/server/ai-tools.ts +1 -0
  37. package/src/server/collections/gitCodeReviews.ts +1 -0
  38. package/src/server/collections/gitRepositories.ts +12 -0
  39. package/src/server/migrations/20260508000000-add-auto-review-flow-id.ts +29 -0
  40. package/src/server/plugin.ts +205 -164
  41. package/src/server/poller.ts +11 -2
  42. package/dist/client/187.eec7be93247463d7.js +0 -10
@@ -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
  });
@@ -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.9",
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
- <Text type="secondary">{commits.length} commits</Text>
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={commits}
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
- const { data } = await api.request({
79
- url: 'gitManager:fileTree',
80
- params: { repositoryId: selectedRepo.id, ref, treePath },
81
- });
82
- const responseData = data?.data || data || [];
83
- const list = Array.isArray(responseData) ? responseData : (Array.isArray(responseData?.data) ? responseData.data : []);
84
-
85
- return list.map((item: any) => ({
86
- key: item.path,
87
- title: (
88
- <Text style={{ fontSize: 13 }}>
89
- {item.name}
90
- {item.type === 'blob' && item.size > 0 && (
91
- <Text type="secondary" style={{ fontSize: 11, marginLeft: 8 }}>
92
- {item.size > 1024 ? `${(item.size / 1024).toFixed(1)}KB` : `${item.size}B`}
93
- </Text>
94
- )}
95
- </Text>
96
- ),
97
- icon: getFileIcon(item.name, item.type),
98
- isLeaf: item.type === 'blob',
99
- filePath: item.path,
100
- fileType: item.type,
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
- if (action === 'status') {
54
- const responseData = data?.data?.data || data?.data;
55
- setStatusData(responseData);
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={() => execAction('status')}
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 api.request({ url: 'gitManager:pollerStatus' });
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: { autoReview: checked },
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={4} placeholder={t('Extra guidance appended to every review prompt')} />
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' || review.postStatus === 'approved');
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 && (