plugin-git-manager 1.1.7 → 1.1.10

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 (38) hide show
  1. package/dist/client/187.08dd0bf4d0f68036.js +10 -0
  2. package/dist/client/components/LLMModelSelect.d.ts +10 -0
  3. package/dist/client/components/RunReviewButton.d.ts +1 -1
  4. package/dist/client/context/GitManagerContext.d.ts +2 -0
  5. package/dist/client/index.js +1 -1
  6. package/dist/externalVersion.js +6 -4
  7. package/dist/locale/en-US.json +8 -1
  8. package/dist/server/actions/gitlab-api.js +14 -13
  9. package/dist/server/actions/review.js +15 -9
  10. package/dist/server/ai-tools.js +29 -3
  11. package/dist/server/collections/gitCodeReviews.d.ts +1 -1
  12. package/dist/server/collections/gitRepositories.d.ts +1 -1
  13. package/dist/server/collections/gitRepositories.js +12 -0
  14. package/dist/server/collections/gitReviewFlows.d.ts +1 -1
  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 +38 -6
  19. package/dist/server/poller.js +3 -1
  20. package/package.json +1 -1
  21. package/src/client/components/CommitHistory.tsx +18 -3
  22. package/src/client/components/GitOperations.tsx +29 -16
  23. package/src/client/components/LLMModelSelect.tsx +63 -0
  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 +17 -2
  27. package/src/client/components/RunReviewButton.tsx +375 -278
  28. package/src/client/context/GitManagerContext.tsx +2 -0
  29. package/src/client/index.tsx +31 -31
  30. package/src/locale/en-US.json +8 -1
  31. package/src/server/actions/gitlab-api.ts +8 -4
  32. package/src/server/actions/review.ts +9 -3
  33. package/src/server/ai-tools.ts +31 -3
  34. package/src/server/collections/gitRepositories.ts +12 -0
  35. package/src/server/migrations/20260508000000-add-auto-review-flow-id.ts +29 -0
  36. package/src/server/plugin.ts +216 -180
  37. package/src/server/poller.ts +11 -2
  38. package/dist/client/322.c934c7c0e25a75b0.js +0 -10
@@ -49,8 +49,16 @@ function registerGitReviewAiTools(app) {
49
49
  (_c = (_b = app.log) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, "plugin-git-manager: AIManager.toolsManager not available; skipping AI tool registration");
50
50
  return;
51
51
  }
52
- const enforceAcl = (ctx, resource, action) => {
52
+ const enforceAcl = (ctx, resource, action, params) => {
53
53
  var _a2, _b2, _c2, _d2;
54
+ if (ctx.isBackgroundReview) {
55
+ if (params.repositoryId && params.repositoryId !== ctx.reviewTargetRepositoryId) {
56
+ const err = new Error("Permission denied: AI cannot access other repositories");
57
+ err.status = 403;
58
+ throw err;
59
+ }
60
+ return;
61
+ }
54
62
  const user = (_a2 = ctx == null ? void 0 : ctx.state) == null ? void 0 : _a2.currentUser;
55
63
  if (!(user == null ? void 0 : user.id)) {
56
64
  const err = new Error("AI tool requires an authenticated user context");
@@ -72,13 +80,31 @@ function registerGitReviewAiTools(app) {
72
80
  throw err;
73
81
  }
74
82
  };
83
+ const sanitizeForDb = (obj) => {
84
+ if (typeof obj === "string") {
85
+ return obj.replace(/\u0000/g, "");
86
+ }
87
+ if (Array.isArray(obj)) {
88
+ return obj.map(sanitizeForDb);
89
+ }
90
+ if (obj && typeof obj === "object") {
91
+ const sanitized = {};
92
+ for (const [key, value] of Object.entries(obj)) {
93
+ sanitized[key] = sanitizeForDb(value);
94
+ }
95
+ return sanitized;
96
+ }
97
+ return obj;
98
+ };
75
99
  const runResourceAction = async (ctx, handler, params, gate) => {
76
- enforceAcl(ctx, gate.resource, gate.action);
100
+ var _a2;
101
+ enforceAcl(ctx, gate.resource, gate.action, params);
77
102
  const synthCtx = {
78
103
  ...ctx,
79
104
  app: ctx.app,
80
105
  db: ctx.db,
81
106
  action: { params },
107
+ request: { ...ctx.request || {}, body: ((_a2 = ctx.request) == null ? void 0 : _a2.body) || {} },
82
108
  throw: (status, message) => {
83
109
  const err = new Error(message);
84
110
  err.status = status;
@@ -86,7 +112,7 @@ function registerGitReviewAiTools(app) {
86
112
  }
87
113
  };
88
114
  await handler(synthCtx, async () => void 0);
89
- return synthCtx.body;
115
+ return sanitizeForDb(synthCtx.body);
90
116
  };
91
117
  aiManager.toolsManager.registerTools([
92
118
  {
@@ -1,2 +1,2 @@
1
- declare const _default: import("@nocobase/database").CollectionOptions;
1
+ declare const _default: any;
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: import("@nocobase/database").CollectionOptions;
1
+ declare const _default: any;
2
2
  export default _default;
@@ -87,6 +87,18 @@ var gitRepositories_default = (0, import_database.defineCollection)({
87
87
  interface: "checkbox",
88
88
  uiSchema: { title: "Auto Review", type: "boolean", "x-component": "Checkbox" }
89
89
  },
90
+ {
91
+ type: "belongsTo",
92
+ name: "autoReviewFlow",
93
+ target: "gitReviewFlows",
94
+ foreignKey: "autoReviewFlowId",
95
+ interface: "m2o",
96
+ uiSchema: {
97
+ title: "Primary Auto Review Flow",
98
+ "x-component": "AssociationField",
99
+ "x-component-props": { fieldNames: { label: "name", value: "id" } }
100
+ }
101
+ },
90
102
  {
91
103
  type: "date",
92
104
  name: "lastPolledAt",
@@ -1,2 +1,2 @@
1
- declare const _default: import("@nocobase/database").CollectionOptions;
1
+ declare const _default: any;
2
2
  export default _default;
@@ -0,0 +1,6 @@
1
+ import { Migration } from '@nocobase/server';
2
+ export default class AddAutoReviewFlowIdMigration extends Migration {
3
+ on: string;
4
+ up(): Promise<void>;
5
+ down(): Promise<void>;
6
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var add_auto_review_flow_id_exports = {};
28
+ __export(add_auto_review_flow_id_exports, {
29
+ default: () => AddAutoReviewFlowIdMigration
30
+ });
31
+ module.exports = __toCommonJS(add_auto_review_flow_id_exports);
32
+ var import_server = require("@nocobase/server");
33
+ var import_sequelize = require("sequelize");
34
+ class AddAutoReviewFlowIdMigration extends import_server.Migration {
35
+ on = "afterLoad";
36
+ async up() {
37
+ var _a;
38
+ const queryInterface = this.db.sequelize.getQueryInterface();
39
+ const tablePrefix = ((_a = this.db.options) == null ? void 0 : _a.tablePrefix) || "";
40
+ const tableName = `${tablePrefix}gitRepositories`;
41
+ const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
42
+ if (!tableInfo || tableInfo.autoReviewFlowId) return;
43
+ await queryInterface.addColumn(tableName, "autoReviewFlowId", {
44
+ type: import_sequelize.DataTypes.INTEGER,
45
+ allowNull: true
46
+ });
47
+ }
48
+ async down() {
49
+ var _a;
50
+ const queryInterface = this.db.sequelize.getQueryInterface();
51
+ const tablePrefix = ((_a = this.db.options) == null ? void 0 : _a.tablePrefix) || "";
52
+ const tableName = `${tablePrefix}gitRepositories`;
53
+ const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
54
+ if (!(tableInfo == null ? void 0 : tableInfo.autoReviewFlowId)) return;
55
+ await queryInterface.removeColumn(tableName, "autoReviewFlowId");
56
+ }
57
+ }
@@ -1,8 +1,12 @@
1
1
  import { Plugin } from '@nocobase/server';
2
2
  export declare class PluginGitManagerServer extends Plugin {
3
+ app: any;
4
+ db: any;
5
+ beforeLoad(): Promise<void>;
3
6
  load(): Promise<void>;
4
7
  install(): Promise<void>;
5
8
  beforeDisable(): Promise<void>;
6
9
  beforeUnload(): Promise<void>;
7
10
  }
11
+ export declare function ensureAutoReviewFlowSchema(app: any): Promise<void>;
8
12
  export default PluginGitManagerServer;
@@ -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: {
@@ -97,7 +106,13 @@ class PluginGitManagerServer extends import_server.Plugin {
97
106
  return next();
98
107
  });
99
108
  (0, import_ai_tools.registerGitReviewAiTools)(this.app);
100
- this.app.on("afterStart", () => {
109
+ this.app.on("afterStart", async () => {
110
+ await ensureAutoReviewFlowSchema(this.app).catch(
111
+ (err) => {
112
+ var _a, _b;
113
+ return (_b = (_a = this.app.log) == null ? void 0 : _a.error) == null ? void 0 : _b.call(_a, "plugin-git-manager: ensure schema error", err);
114
+ }
115
+ );
101
116
  (0, import_review.recoverStuckReviews)(this.app).catch(
102
117
  (err) => {
103
118
  var _a, _b;
@@ -188,6 +203,8 @@ class PluginGitManagerServer extends import_server.Plugin {
188
203
  });
189
204
  }
190
205
  async install() {
206
+ var _a;
207
+ await ((_a = this.app.db.getCollection("gitRepositories")) == null ? void 0 : _a.sync());
191
208
  }
192
209
  async beforeDisable() {
193
210
  (0, import_poller.stopPoller)();
@@ -196,8 +213,23 @@ class PluginGitManagerServer extends import_server.Plugin {
196
213
  (0, import_poller.stopPoller)();
197
214
  }
198
215
  }
216
+ async function ensureAutoReviewFlowSchema(app) {
217
+ var _a, _b, _c;
218
+ const sequelize = (_a = app.db) == null ? void 0 : _a.sequelize;
219
+ const queryInterface = (_b = sequelize == null ? void 0 : sequelize.getQueryInterface) == null ? void 0 : _b.call(sequelize);
220
+ if (!queryInterface) return;
221
+ const tablePrefix = ((_c = app.db.options) == null ? void 0 : _c.tablePrefix) || "";
222
+ const tableName = `${tablePrefix}gitRepositories`;
223
+ const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
224
+ if (!tableInfo || tableInfo.autoReviewFlowId) return;
225
+ await queryInterface.addColumn(tableName, "autoReviewFlowId", {
226
+ type: import_sequelize.DataTypes.INTEGER,
227
+ allowNull: true
228
+ });
229
+ }
199
230
  var plugin_default = PluginGitManagerServer;
200
231
  // Annotate the CommonJS export names for ESM import in node:
201
232
  0 && (module.exports = {
202
- PluginGitManagerServer
233
+ PluginGitManagerServer,
234
+ ensureAutoReviewFlowSchema
203
235
  });
@@ -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.7",
6
+ "version": "1.1.10",
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;
@@ -61,6 +62,12 @@ export const CommitHistory: React.FC = () => {
61
62
  }
62
63
  }, [selectedRepo]);
63
64
 
65
+ const filteredCommits = commits.filter((commit) => {
66
+ const keyword = searchKeyword.trim().toLowerCase();
67
+ if (!keyword) return true;
68
+ return String(commit.message || commit.subject || commit.body || '').toLowerCase().includes(keyword);
69
+ });
70
+
64
71
  const openCommitDetail = async (commit: any) => {
65
72
  setSelectedCommit(commit);
66
73
  setDetailLoading(true);
@@ -174,14 +181,22 @@ export const CommitHistory: React.FC = () => {
174
181
  options={branchList.map((b) => ({ label: b, value: b }))}
175
182
  disabled
176
183
  />
177
- <Text type="secondary">{commits.length} commits</Text>
184
+ <Input.Search
185
+ allowClear
186
+ size="small"
187
+ style={{ width: 280 }}
188
+ placeholder={t('Search commit title or message')}
189
+ value={searchKeyword}
190
+ onChange={(event) => setSearchKeyword(event.target.value)}
191
+ />
192
+ <Text type="secondary">{filteredCommits.length} commits</Text>
178
193
  </div>
179
194
 
180
195
  {loading ? (
181
196
  <Spin />
182
197
  ) : (
183
198
  <Table
184
- dataSource={commits}
199
+ dataSource={filteredCommits}
185
200
  columns={columns}
186
201
  rowKey="hash"
187
202
  size="small"
@@ -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,29 @@ 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
+ } finally {
54
+ setActionLoading(null);
55
+ }
56
+ }, [api, selectedRepo]);
57
+
58
+ useEffect(() => {
59
+ loadStatus();
60
+ }, [loadStatus]);
61
+
40
62
  const execAction = useCallback(
41
63
  async (action: string) => {
42
64
  if (!selectedRepo) return;
@@ -50,10 +72,9 @@ export const GitOperations: React.FC = () => {
50
72
  });
51
73
  setLastResult({ action, success: true, message: t('{{action}} completed successfully', { action }) });
52
74
  message.success(t('{{action}} completed', { action }));
53
- if (action === 'status') {
54
- const responseData = data?.data?.data || data?.data;
55
- setStatusData(responseData);
56
- }
75
+ const responseData = data?.data?.data || data?.data;
76
+ if (action === 'status') setStatusData(responseData);
77
+ if (action === 'fetch' || action === 'pull') await loadStatus();
57
78
  await refreshRepos();
58
79
  } catch (err: any) {
59
80
  const msg = err?.response?.data?.errors?.[0]?.message || t('{{action}} failed', { action });
@@ -63,7 +84,7 @@ export const GitOperations: React.FC = () => {
63
84
  setActionLoading(null);
64
85
  }
65
86
  },
66
- [api, selectedRepo, refreshRepos],
87
+ [api, selectedRepo, refreshRepos, loadStatus],
67
88
  );
68
89
 
69
90
  if (!selectedRepo) {
@@ -104,19 +125,11 @@ export const GitOperations: React.FC = () => {
104
125
  >
105
126
  {t('Pull')}
106
127
  </Button>
107
- <Button
108
- size="large"
109
- icon={<CloudUploadOutlined />}
110
- loading={actionLoading === 'push'}
111
- onClick={() => execAction('push')}
112
- >
113
- {t('Push')}
114
- </Button>
115
128
  <Button
116
129
  size="large"
117
130
  icon={<BranchesOutlined />}
118
131
  loading={actionLoading === 'status'}
119
- onClick={() => execAction('status')}
132
+ onClick={loadStatus}
120
133
  >
121
134
  {t('Status')}
122
135
  </Button>
@@ -0,0 +1,63 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Select } from 'antd';
3
+ import { useAPIClient } from '@nocobase/client';
4
+
5
+ export const LLMModelSelect: React.FC<{
6
+ llmService?: string;
7
+ value?: string;
8
+ onChange?: (value: string) => void;
9
+ allowClear?: boolean;
10
+ size?: 'small' | 'middle' | 'large';
11
+ style?: React.CSSProperties;
12
+ placeholder?: string;
13
+ }> = ({ llmService, value, onChange, allowClear = true, size, style, placeholder }) => {
14
+ const api = useAPIClient();
15
+ const [models, setModels] = useState<{ id: string }[]>([]);
16
+ const [loading, setLoading] = useState(false);
17
+
18
+ useEffect(() => {
19
+ let cancelled = false;
20
+ if (!llmService) {
21
+ setModels([]);
22
+ return;
23
+ }
24
+
25
+ setLoading(true);
26
+ api
27
+ .request({ url: 'ai:listModels', params: { llmService } })
28
+ .then((res) => {
29
+ if (cancelled) return;
30
+ const list = res?.data?.data || [];
31
+ setModels(Array.isArray(list) ? list : []);
32
+ })
33
+ .catch(() => {
34
+ if (!cancelled) setModels([]);
35
+ })
36
+ .finally(() => {
37
+ if (!cancelled) setLoading(false);
38
+ });
39
+
40
+ return () => {
41
+ cancelled = true;
42
+ };
43
+ }, [api, llmService]);
44
+
45
+ return (
46
+ <Select
47
+ value={value}
48
+ onChange={onChange}
49
+ allowClear={allowClear}
50
+ loading={loading}
51
+ size={size}
52
+ style={style}
53
+ placeholder={placeholder || 'Select LLM Model'}
54
+ options={models.map((m) => ({
55
+ value: m.id,
56
+ label: m.id,
57
+ }))}
58
+ showSearch
59
+ optionFilterProp="label"
60
+ disabled={!llmService}
61
+ />
62
+ );
63
+ };
@@ -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>