nx-kv 0.11.2 → 0.11.4
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/CHANGELOG.md +22 -1
- package/README.md +1 -1
- package/assets/groups.json +4 -0
- package/assets/kv-repo/SKILL.md +83 -0
- package/assets/nx-kv/SKILL.md +5 -0
- package/assets/nx-kv/references/repo-registry.md +7 -5
- package/assets/nx-kv/references/todo-commands.md +1 -1
- package/package.json +1 -1
- package/src/core/config.js +1 -1
- package/src/modules/localtask/view.jsx +6 -18
- package/src/modules/repos/boards.js +60 -9
- package/src/modules/repos/index.js +22 -5
- package/src/modules/repos/service.js +8 -14
- package/src/modules/repos/view.jsx +178 -85
- package/src/modules/{group/view.jsx → todo/group-popup.jsx} +171 -159
- package/src/modules/todo/group.js +194 -0
- package/src/modules/todo/index.js +7 -1
- package/src/modules/todo/view.jsx +14 -1
- package/src/runtime/registry.js +3 -2
- package/src/web/frontend/registry.js +2 -1
- package/src/web/frontend/style.css +111 -44
- package/src/web/public/assets/{CliHints-CrVZoRln.js → CliHints-DltGgA21.js} +1 -1
- package/src/web/public/assets/{index-gEMHU8gc.js → index-Cg8J6GZg.js} +2 -2
- package/src/web/public/assets/index-DozxuMsm.css +1 -0
- package/src/web/public/assets/view-CCiJUhF6.js +1 -0
- package/src/web/public/assets/view-CV6w4TG6.js +3 -0
- package/src/web/public/assets/view-bE7JPI0N.js +13 -0
- package/src/web/public/assets/{view-QRE2Bldq.js → view-k4YFJYVz.js} +1 -1
- package/src/web/public/index.html +2 -2
- package/src/modules/group/index.js +0 -199
- package/src/web/public/assets/index-D3Gh8xAs.css +0 -1
- package/src/web/public/assets/view-CQT74IeU.js +0 -3
- package/src/web/public/assets/view-DZZz-uRj.js +0 -2
- package/src/web/public/assets/view-KyrAiPse.js +0 -1
- package/src/web/public/assets/view-PJtYOdms.js +0 -10
|
@@ -16,61 +16,52 @@
|
|
|
16
16
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
17
17
|
import { api } from '../../web/frontend/api/client.js';
|
|
18
18
|
import { useStore } from '../../web/frontend/store.jsx';
|
|
19
|
-
import { useGuard, useDialog, useToast, Modal } from '../../web/frontend/components/ui.jsx';
|
|
19
|
+
import { useGuard, useDialog, useToast, Modal, Copyable } from '../../web/frontend/components/ui.jsx';
|
|
20
20
|
import { CliHints } from '../../web/frontend/components/CliHints.jsx';
|
|
21
21
|
|
|
22
|
-
const EMPTY_FORM = { path: '', name: '', desc: '',
|
|
22
|
+
const EMPTY_FORM = { path: '', name: '', desc: '', notes: '' };
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
24
|
+
// 路径照搬目录看板的「可扫标签」画法:祖先目录变暗、末段略重,点击复制全文。
|
|
25
|
+
// 纯灰一长串(旧画法)里,人眼要逐字符对比才能找到目录名。
|
|
26
|
+
function RepoPath({ path }) {
|
|
27
|
+
const s = String(path || '');
|
|
28
|
+
const cut = s.replace(/\\/g, '/').lastIndexOf('/');
|
|
29
|
+
if (cut < 0) return <Copyable text={s}>{s}</Copyable>;
|
|
30
|
+
return (
|
|
31
|
+
<Copyable text={s} title="点击复制完整路径">
|
|
32
|
+
<span className="anc">{s.slice(0, cut)}</span>
|
|
33
|
+
{s.slice(cut)}
|
|
34
|
+
</Copyable>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 仓库行:名称 + 描述 / 路径(点击复制)/ 操作。
|
|
39
|
+
// 中心清单里**不再显示所属看板**——行内的 chip + × + 下拉三件套又挤又乱,
|
|
40
|
+
// 归属的增删统一走「成员管理」弹窗(筛选到看板 → 导入仓库 / 摘除)。
|
|
41
|
+
// 看板视图下描述行显示**组内描述**(该仓库在这个组里的角色),没写则回退仓库描述。
|
|
42
|
+
function RepoRow({ r, filterBoard, acts }) {
|
|
43
|
+
const note = filterBoard
|
|
44
|
+
? ((r.boards || []).find((b) => b.id === filterBoard.id)?.note || '')
|
|
45
|
+
: null;
|
|
46
|
+
const desc = note !== null ? (note || r.desc || '') : (r.desc || '');
|
|
47
|
+
const descTitle = note !== null
|
|
48
|
+
? (note ? `组内描述:${note}` : `(组内描述未写,暂显示仓库描述)${r.desc || ''}`)
|
|
49
|
+
: (r.desc || '');
|
|
28
50
|
return (
|
|
29
51
|
<tr>
|
|
30
52
|
<td className="rp-name">
|
|
31
53
|
<strong>{r.name}</strong>
|
|
32
|
-
{
|
|
33
|
-
</td>
|
|
34
|
-
<td className="mono rp-path" title={r.path}>{r.path}</td>
|
|
35
|
-
<td className="rp-tags">
|
|
36
|
-
{(r.tags || []).map((t) => (
|
|
37
|
-
<span className="tag" key={t}>{t}</span>
|
|
38
|
-
))}
|
|
39
|
-
</td>
|
|
40
|
-
<td className="rp-boards">
|
|
41
|
-
{mine.map((b) => (
|
|
42
|
-
<span className="tag badge rp-board" key={b.id} title={`「${b.name}」里的一份副本 —— 点 × 只摘这份,仓库仍在清单里`}>
|
|
43
|
-
{b.name}
|
|
44
|
-
<button
|
|
45
|
-
className="rp-x"
|
|
46
|
-
aria-label={`从 ${b.name} 移出`}
|
|
47
|
-
title="只从这一个看板移出(仓库仍在中心清单里)"
|
|
48
|
-
onClick={() => acts.out(r, b.id)}
|
|
49
|
-
>
|
|
50
|
-
×
|
|
51
|
-
</button>
|
|
52
|
-
</span>
|
|
53
|
-
))}
|
|
54
|
-
{notIn.length ? (
|
|
55
|
-
<select
|
|
56
|
-
className="rp-add"
|
|
57
|
-
value=""
|
|
58
|
-
title="加入看板 = 复制一份归属,仓库本体不动"
|
|
59
|
-
onChange={(e) => {
|
|
60
|
-
if (e.target.value) acts.add(r, e.target.value);
|
|
61
|
-
}}
|
|
62
|
-
>
|
|
63
|
-
<option value="">+ 看板</option>
|
|
64
|
-
{notIn.map((b) => (
|
|
65
|
-
<option key={b.id} value={b.id}>{b.name}</option>
|
|
66
|
-
))}
|
|
67
|
-
</select>
|
|
68
|
-
) : null}
|
|
54
|
+
{desc ? <div className="muted rp-desc" title={descTitle}>{desc}</div> : null}
|
|
69
55
|
</td>
|
|
56
|
+
<td className="mono rp-path"><RepoPath path={r.path} /></td>
|
|
70
57
|
<td className="rp-acts">
|
|
71
58
|
<button className="btn small ghost" title="在文件管理器中打开" onClick={() => acts.open(r)}>打开</button>
|
|
72
59
|
<button className="btn small ghost" onClick={() => acts.edit(r)}>编辑</button>
|
|
73
|
-
|
|
60
|
+
{filterBoard ? (
|
|
61
|
+
<button className="btn small ghost" title={`写「${r.name}」在看板「${filterBoard.name}」里的组内描述`}
|
|
62
|
+
onClick={() => acts.note(r)}>描述</button>
|
|
63
|
+
) : null}
|
|
64
|
+
<button className="btn small ghost rp-del" onClick={() => acts.del(r)}>删除</button>
|
|
74
65
|
</td>
|
|
75
66
|
</tr>
|
|
76
67
|
);
|
|
@@ -87,6 +78,11 @@ export default function ReposView() {
|
|
|
87
78
|
const [scanOpen, setScanOpen] = useState(false);
|
|
88
79
|
const [scan, setScan] = useState({ root: '', depth: '3' });
|
|
89
80
|
const [scanResult, setScanResult] = useState(null);
|
|
81
|
+
// 成员管理弹窗:打开时 = 正在往这个看板里点选导入 / 摘除仓库
|
|
82
|
+
const [impBoardId, setImpBoardId] = useState(null);
|
|
83
|
+
// 组内描述编辑:{ id, repo, boardId, boardName, note } | null
|
|
84
|
+
const [noteFor, setNoteFor] = useState(null);
|
|
85
|
+
const [noteText, setNoteText] = useState('');
|
|
90
86
|
|
|
91
87
|
// 看板(只作筛选与标签)
|
|
92
88
|
const [boards, setBoards] = useState(null); // { boards: [...], unassigned: [...] }
|
|
@@ -111,6 +107,17 @@ export default function ReposView() {
|
|
|
111
107
|
}, []);
|
|
112
108
|
useEffect(() => { loadRepos(); loadBoards(); }, [loadRepos, loadBoards]);
|
|
113
109
|
|
|
110
|
+
// 组内描述保存:note 为空 = 清除
|
|
111
|
+
const saveNote = () => guard(async () => {
|
|
112
|
+
await api('/api/boards/note', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: { id: noteFor.id, board: noteFor.boardId, note: noteText },
|
|
115
|
+
});
|
|
116
|
+
setNoteFor(null);
|
|
117
|
+
await Promise.all([loadRepos(), loadBoards()]);
|
|
118
|
+
toast('已保存组内描述');
|
|
119
|
+
});
|
|
120
|
+
|
|
114
121
|
// 看板清单(用来做「+ 看板」下拉与筛选器)
|
|
115
122
|
const boardList = useMemo(() => (boards && boards.boards) || [], [boards]);
|
|
116
123
|
|
|
@@ -121,8 +128,13 @@ export default function ReposView() {
|
|
|
121
128
|
return repos.filter((r) => (r.boards || []).some((b) => b.id === filter));
|
|
122
129
|
}, [repos, filter]);
|
|
123
130
|
|
|
131
|
+
// 当前筛选到的看板(用来显示领域描述上下文条)与成员管理弹窗的目标看板
|
|
132
|
+
const filterBoard = useMemo(() => boardList.find((b) => b.id === filter) || null, [boardList, filter]);
|
|
133
|
+
const impBoard = useMemo(() => boardList.find((b) => b.id === impBoardId) || null, [boardList, impBoardId]);
|
|
134
|
+
|
|
124
135
|
const openAdd = () => {
|
|
125
|
-
|
|
136
|
+
setForm(EMPTY_FORM);
|
|
137
|
+
setEditing(null);
|
|
126
138
|
setFormOpen(true);
|
|
127
139
|
};
|
|
128
140
|
|
|
@@ -132,7 +144,6 @@ export default function ReposView() {
|
|
|
132
144
|
path: form.path.trim(),
|
|
133
145
|
name: form.name.trim() || undefined,
|
|
134
146
|
desc: form.desc.trim() || undefined,
|
|
135
|
-
tags: form.tags.trim() || undefined,
|
|
136
147
|
notes: form.notes,
|
|
137
148
|
};
|
|
138
149
|
if (editing) await api(`/api/repos/${encodeURIComponent(editing)}`, { method: 'PATCH', body });
|
|
@@ -189,12 +200,14 @@ export default function ReposView() {
|
|
|
189
200
|
// ---- 看板归属:**复制**,不是移动 ----
|
|
190
201
|
// 加一个看板 = 多一份归属,仓库本体不动、别的看板也不动;
|
|
191
202
|
// 移出某个看板 = 只摘那一份,仓库本体不动。
|
|
192
|
-
|
|
203
|
+
// 行内的加/摘入口已收进「成员管理」弹窗(toggleMember),表格式逐行操作太乱。
|
|
204
|
+
const toggleMember = (r, add) => guard(async () => {
|
|
193
205
|
await api('/api/boards/assign', {
|
|
194
206
|
method: 'POST',
|
|
195
|
-
body:
|
|
207
|
+
body: add ? { id: r.id, board: impBoardId } : { id: r.id, board: impBoardId, remove: true },
|
|
196
208
|
});
|
|
197
|
-
await
|
|
209
|
+
setRepos((await api('/api/repos')) || []);
|
|
210
|
+
await loadBoards();
|
|
198
211
|
});
|
|
199
212
|
|
|
200
213
|
// ---- 看板管理 ----
|
|
@@ -251,8 +264,17 @@ export default function ReposView() {
|
|
|
251
264
|
open: (r) => rowAct('open', r),
|
|
252
265
|
edit: (r) => rowAct('edit', r),
|
|
253
266
|
del: (r) => rowAct('del', r),
|
|
254
|
-
|
|
255
|
-
|
|
267
|
+
note: (r) => {
|
|
268
|
+
const mine = (r.boards || []).find((b) => b.id === filter);
|
|
269
|
+
setNoteFor({
|
|
270
|
+
id: r.id,
|
|
271
|
+
repo: r.name,
|
|
272
|
+
boardId: filter,
|
|
273
|
+
boardName: filterBoard?.name || '',
|
|
274
|
+
note: mine?.note || '',
|
|
275
|
+
});
|
|
276
|
+
setNoteText(mine?.note || '');
|
|
277
|
+
},
|
|
256
278
|
};
|
|
257
279
|
|
|
258
280
|
return (
|
|
@@ -261,23 +283,18 @@ export default function ReposView() {
|
|
|
261
283
|
<div className="title-block">
|
|
262
284
|
<h2>仓库</h2>
|
|
263
285
|
<div className="page-desc">
|
|
264
|
-
本机 git 仓库的<strong
|
|
265
|
-
|
|
266
|
-
|
|
286
|
+
本机 git 仓库的<strong>中心清单</strong>。看板 = <strong>领域分组</strong>
|
|
287
|
+
(同一个仓库可同时归入多个):看板的领域描述写这组仓库的侧重与配合关系,
|
|
288
|
+
成员仓库还能写<strong>组内描述</strong>——它在组里的角色、关键路径与搜索关键词
|
|
289
|
+
(筛选到看板后点行内「描述」)。加入是复制一份归属,仓库本体永远在清单里。
|
|
267
290
|
</div>
|
|
268
291
|
</div>
|
|
269
292
|
<div className="acts">
|
|
270
293
|
<button className="btn ghost" onClick={() => setScanOpen(true)}>扫描</button>
|
|
271
|
-
<button className="btn" onClick={openAdd}
|
|
294
|
+
<button className="btn" onClick={openAdd}>添加仓库</button>
|
|
272
295
|
</div>
|
|
273
296
|
</div>
|
|
274
297
|
|
|
275
|
-
{editing ? (
|
|
276
|
-
<div className="muted" style={{ margin: '0 0 10px' }}>
|
|
277
|
-
正在编辑「{repos.find((r) => r.id === editing)?.name || editing}」——点右上角「继续编辑」改完保存。
|
|
278
|
-
</div>
|
|
279
|
-
) : null}
|
|
280
|
-
|
|
281
298
|
{scanResult ? (
|
|
282
299
|
<div className="card">
|
|
283
300
|
<div className="colhead"><h3>扫描结果</h3>
|
|
@@ -313,12 +330,30 @@ export default function ReposView() {
|
|
|
313
330
|
</button>
|
|
314
331
|
</div>
|
|
315
332
|
|
|
333
|
+
{/* 看板上下文条:筛选到某个看板时,把它的领域描述亮出来 —— desc 写的是
|
|
334
|
+
这个领域的侧重、谁配合谁(谁是中心/终端)、关键路径与搜索关键词。 */}
|
|
335
|
+
{filterBoard ? (
|
|
336
|
+
<div className="rp-context" data-testid="rp-context">
|
|
337
|
+
<span className="rp-context-name">{filterBoard.name}</span>
|
|
338
|
+
{filterBoard.desc ? (
|
|
339
|
+
<span className="rp-context-desc" title={filterBoard.desc}>{filterBoard.desc}</span>
|
|
340
|
+
) : (
|
|
341
|
+
<span className="muted rp-context-desc">
|
|
342
|
+
还没有领域描述 —— 点「看板管理」补一句:侧重什么、谁配合谁、关键路径与搜索关键词在哪。
|
|
343
|
+
</span>
|
|
344
|
+
)}
|
|
345
|
+
<span className="acts">
|
|
346
|
+
<button className="btn small" onClick={() => setImpBoardId(filterBoard.id)}>导入仓库</button>
|
|
347
|
+
</span>
|
|
348
|
+
</div>
|
|
349
|
+
) : null}
|
|
350
|
+
|
|
316
351
|
{/* 看板管理(次要):建 / 改 / 删 / 导入 / 导出 */}
|
|
317
352
|
{boardsOpen ? (
|
|
318
353
|
<div className="card rp-boards-manage">
|
|
319
354
|
<div className="colhead">
|
|
320
355
|
<h3>看板</h3>
|
|
321
|
-
<span className="muted"
|
|
356
|
+
<span className="muted">看板 = 领域分组,描述写给人和 agent 读:侧重、配合关系、关键路径与关键词。删一个看板,仓库全都在清单里,只是少了一份归属。</span>
|
|
322
357
|
<span className="acts" style={{ marginLeft: 'auto' }}>
|
|
323
358
|
<button className="btn small ghost" onClick={() => setBoardForm({ id: null, name: '', desc: '' })}>新建看板</button>
|
|
324
359
|
<button className="btn small ghost" title="粘贴/选择看板 JSON,一个看板 = 一个主题" onClick={() => setImpOpen(true)}>导入看板</button>
|
|
@@ -329,15 +364,21 @@ export default function ReposView() {
|
|
|
329
364
|
{boardList.map((b) => (
|
|
330
365
|
<div className="rp-board-item" key={b.id}>
|
|
331
366
|
<span className="rp-board-name">{b.name}</span>
|
|
332
|
-
{b.desc ? <span className="muted rp-board-desc">{b.desc}</span> : null}
|
|
333
|
-
<span className="
|
|
367
|
+
{b.desc ? <span className="muted rp-board-desc" title={b.desc}>{b.desc}</span> : null}
|
|
368
|
+
<span className="rp-board-count">{b.repos.length} 个仓库</span>
|
|
334
369
|
<span className="acts">
|
|
335
|
-
<button className="btn small ghost" onClick={() =>
|
|
370
|
+
<button className="btn small ghost" title="点选加入/摘除这个看板的成员仓库" onClick={() => setImpBoardId(b.id)}>成员管理</button>
|
|
371
|
+
<button className="btn small ghost" onClick={() => setBoardForm({ id: b.id, name: b.name, desc: b.desc || '' })}>编辑</button>
|
|
336
372
|
<button className="btn small ghost" onClick={() => removeBoard(b)}>删除看板</button>
|
|
337
373
|
</span>
|
|
338
374
|
</div>
|
|
339
375
|
))}
|
|
340
376
|
{!boardList.length ? <div className="muted">还没有看板 —— 建一个当标签用。</div> : null}
|
|
377
|
+
{boardList.length && boards.unassigned?.length ? (
|
|
378
|
+
<div className="rp-boards-unassigned">
|
|
379
|
+
未分组 <b>{boards.unassigned.length}</b> 个 —— 不属于任何看板,只在「全部」里可见。
|
|
380
|
+
</div>
|
|
381
|
+
) : null}
|
|
341
382
|
</div>
|
|
342
383
|
</div>
|
|
343
384
|
) : null}
|
|
@@ -347,22 +388,20 @@ export default function ReposView() {
|
|
|
347
388
|
<table className="rp-table">
|
|
348
389
|
<thead>
|
|
349
390
|
<tr>
|
|
350
|
-
<th style={{ width: '
|
|
351
|
-
<th style={{ width: '
|
|
352
|
-
<th style={{ width: '
|
|
353
|
-
<th style={{ width: '22%' }}>所属看板</th>
|
|
354
|
-
<th style={{ width: '10%' }}>操作</th>
|
|
391
|
+
<th scope="col" style={{ width: '32%' }}>仓库</th>
|
|
392
|
+
<th scope="col" style={{ width: '42%' }}>路径</th>
|
|
393
|
+
<th scope="col" style={{ width: '26%' }}>操作</th>
|
|
355
394
|
</tr>
|
|
356
395
|
</thead>
|
|
357
396
|
<tbody>
|
|
358
397
|
{shown.map((r) => (
|
|
359
|
-
<RepoRow key={r.id} r={r}
|
|
398
|
+
<RepoRow key={r.id} r={r} filterBoard={filterBoard} acts={rowActs} />
|
|
360
399
|
))}
|
|
361
400
|
{!shown.length ? (
|
|
362
401
|
<tr>
|
|
363
|
-
<td colSpan="
|
|
402
|
+
<td colSpan="3" className="muted rp-empty">
|
|
364
403
|
{repos.length
|
|
365
|
-
? '
|
|
404
|
+
? '这个看板还没有成员 —— 点上方「导入仓库」,从中心清单里点选加入。'
|
|
366
405
|
: '还没有登记仓库 —— 点「扫描」自动发现,或「添加仓库」手工登记。'}
|
|
367
406
|
</td>
|
|
368
407
|
</tr>
|
|
@@ -389,10 +428,6 @@ export default function ReposView() {
|
|
|
389
428
|
<label htmlFor="repo-desc">一句话描述</label>
|
|
390
429
|
<input id="repo-desc" spellCheck="false" placeholder="这个仓库是干什么的" value={form.desc} onChange={set('desc')} />
|
|
391
430
|
</div>
|
|
392
|
-
<div className="form-field">
|
|
393
|
-
<label htmlFor="repo-tags">标签</label>
|
|
394
|
-
<input id="repo-tags" placeholder="逗号分隔,如 web,tool" value={form.tags} onChange={set('tags')} />
|
|
395
|
-
</div>
|
|
396
431
|
<div className="form-field">
|
|
397
432
|
<label htmlFor="repo-notes">备注</label>
|
|
398
433
|
<textarea id="repo-notes" placeholder="补充信息(可选)" value={form.notes} onChange={set('notes')} />
|
|
@@ -418,7 +453,7 @@ export default function ReposView() {
|
|
|
418
453
|
</div>
|
|
419
454
|
<div className="form-field">
|
|
420
455
|
<label htmlFor="scan-depth">深度</label>
|
|
421
|
-
<input id="scan-depth" value={scan.depth}
|
|
456
|
+
<input id="scan-depth" type="number" min="1" max="10" value={scan.depth}
|
|
422
457
|
onChange={(e) => setScan({ ...scan, depth: e.target.value })} />
|
|
423
458
|
</div>
|
|
424
459
|
<div className="form-acts">
|
|
@@ -430,12 +465,12 @@ export default function ReposView() {
|
|
|
430
465
|
</Modal>
|
|
431
466
|
) : null}
|
|
432
467
|
|
|
433
|
-
{/* 新建 /
|
|
468
|
+
{/* 新建 / 编辑看板弹窗 */}
|
|
434
469
|
{boardForm ? (
|
|
435
|
-
<Modal title={boardForm.id ? '
|
|
470
|
+
<Modal title={boardForm.id ? '编辑看板' : '新建看板'} onClose={() => setBoardForm(null)}>
|
|
436
471
|
<div className="form-grid">
|
|
437
472
|
<div className="form-field">
|
|
438
|
-
<label htmlFor="bd-name"
|
|
473
|
+
<label htmlFor="bd-name">看板名称 *</label>
|
|
439
474
|
<input
|
|
440
475
|
id="bd-name"
|
|
441
476
|
placeholder="如 前端 / AI 工具"
|
|
@@ -444,10 +479,12 @@ export default function ReposView() {
|
|
|
444
479
|
/>
|
|
445
480
|
</div>
|
|
446
481
|
<div className="form-field">
|
|
447
|
-
<label htmlFor="bd-desc"
|
|
448
|
-
<
|
|
482
|
+
<label htmlFor="bd-desc">领域描述</label>
|
|
483
|
+
<textarea
|
|
449
484
|
id="bd-desc"
|
|
450
|
-
|
|
485
|
+
rows={4}
|
|
486
|
+
spellCheck="false"
|
|
487
|
+
placeholder={'这组仓库在这个环境里的侧重、配合关系、关键路径与搜索关键词。\n如:go-ah 是 OIDC 认证中心(issuer 192.0.2.47:8082),组内其余仓库都是它的接入终端。\n如:fr 组内仓库共享 API 契约(lib/api/),搜 fr_ 前缀。'}
|
|
451
488
|
value={boardForm.desc}
|
|
452
489
|
onChange={(e) => setBoardForm((f) => ({ ...f, desc: e.target.value }))}
|
|
453
490
|
/>
|
|
@@ -467,7 +504,10 @@ export default function ReposView() {
|
|
|
467
504
|
<Modal title="导入看板 JSON" onClose={() => { setImpOpen(false); setImpText(''); }}>
|
|
468
505
|
<div className="form-grid">
|
|
469
506
|
<div className="form-field">
|
|
470
|
-
<label htmlFor="bd-imp"
|
|
507
|
+
<label htmlFor="bd-imp">看板 JSON</label>
|
|
508
|
+
<div className="form-hint">
|
|
509
|
+
结构 {'{ boards:[{ name, repos:[{ path|name }] }] }'} —— 按 path 或 name 匹配已登记仓库,匹配不上的会如实报告
|
|
510
|
+
</div>
|
|
471
511
|
<input
|
|
472
512
|
id="bd-imp-file"
|
|
473
513
|
type="file"
|
|
@@ -498,6 +538,59 @@ export default function ReposView() {
|
|
|
498
538
|
</Modal>
|
|
499
539
|
) : null}
|
|
500
540
|
|
|
541
|
+
{/* 组内描述编辑:同一个仓库在不同看板里的角色各不相同,描述挂在成员关系上。
|
|
542
|
+
agent 读看板时会把它带上(见 kv-repo skill),所以值得写关键路径与搜索词。 */}
|
|
543
|
+
{noteFor ? (
|
|
544
|
+
<Modal title={`组内描述 · ${noteFor.repo} @「${noteFor.boardName}」`} onClose={() => setNoteFor(null)}>
|
|
545
|
+
<div className="form-grid">
|
|
546
|
+
<div className="form-field">
|
|
547
|
+
<label htmlFor="rp-note">「{noteFor.repo}」在看板「{noteFor.boardName}」里的角色</label>
|
|
548
|
+
<div className="form-hint">
|
|
549
|
+
写它在这个组里的特殊点与配合关系(谁是中心 / 终端、API 关联在哪),
|
|
550
|
+
可多行、可含关键路径与搜索关键词。留空保存 = 清除。
|
|
551
|
+
</div>
|
|
552
|
+
<textarea
|
|
553
|
+
id="rp-note"
|
|
554
|
+
rows={5}
|
|
555
|
+
spellCheck="false"
|
|
556
|
+
placeholder={'如:OIDC 认证中心(issuer 192.0.2.47:8082),组内其余仓库都是它的接入终端;\nAPI 契约见 internal/middleware/oidc.go,搜 oidc_ 前缀。'}
|
|
557
|
+
value={noteText}
|
|
558
|
+
onChange={(e) => setNoteText(e.target.value)}
|
|
559
|
+
/>
|
|
560
|
+
</div>
|
|
561
|
+
<div className="form-acts">
|
|
562
|
+
<button className="btn ghost" onClick={() => setNoteFor(null)}>取消</button>
|
|
563
|
+
<button className="btn" onClick={saveNote}>保存</button>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</Modal>
|
|
567
|
+
) : null}
|
|
568
|
+
|
|
569
|
+
{/* 成员管理弹窗:一个看板的全部候选仓库,点选加入 / 摘除 —— 批量导入
|
|
570
|
+
不必回「全部」逐行找下拉。加/摘即存,弹窗保持打开,一次导一批。 */}
|
|
571
|
+
{impBoard ? (
|
|
572
|
+
<Modal title={`「${impBoard.name}」· 成员仓库`} onClose={() => setImpBoardId(null)}>
|
|
573
|
+
{impBoard.desc ? <div className="rp-imp-desc">{impBoard.desc}</div> : null}
|
|
574
|
+
<div className="rp-imp-list">
|
|
575
|
+
{repos.map((r) => {
|
|
576
|
+
const inBoard = (r.boards || []).some((b) => b.id === impBoard.id);
|
|
577
|
+
return (
|
|
578
|
+
<div className="rp-imp-item" key={r.id}>
|
|
579
|
+
<span className="rp-imp-name">{r.name}</span>
|
|
580
|
+
<span className="rp-imp-path" title={r.path}>{r.path}</span>
|
|
581
|
+
{inBoard ? (
|
|
582
|
+
<button className="btn small ghost" onClick={() => toggleMember(r, false)}>摘除</button>
|
|
583
|
+
) : (
|
|
584
|
+
<button className="btn small" onClick={() => toggleMember(r, true)}>加入</button>
|
|
585
|
+
)}
|
|
586
|
+
</div>
|
|
587
|
+
);
|
|
588
|
+
})}
|
|
589
|
+
{!repos.length ? <div className="muted rp-empty">还没有登记仓库 —— 先「扫描」或「添加仓库」。</div> : null}
|
|
590
|
+
</div>
|
|
591
|
+
</Modal>
|
|
592
|
+
) : null}
|
|
593
|
+
|
|
501
594
|
{dialogNode}
|
|
502
595
|
</>
|
|
503
596
|
);
|