左侧栏 ProjectNav:项目完整列表(名 + 短 id + 我的角色, 选中高亮,点击切换)+ 创建项目(名/GitHub 仓库/repo_url) + 选中项目成员(邀请/改角色/新增移除);工作台删除 sec-projects 一节与任务列表里的项目下拉框,侧栏为唯一 项目选择器,自动选第一个项目逻辑不变。 薄模块补齐(均为已核实后端路由):成员移除 DELETE、 自动化详情(GET 单条 + 产出)与编辑 PATCH、分诊条目点进 任务/自动化详情、Workers 搜索与在线筛选、管理区设置配额 POST。主线新功能(Todos、Browser、taskMeta 等)原样保留, 未做任何回退。console 标记单测本地 53 项通过。
50 lines
2.6 KiB
TypeScript
50 lines
2.6 KiB
TypeScript
import { useState } from "react";
|
||
import { useConsole } from "../context";
|
||
import { Card, Bar, DataListRaw } from "../components/ui";
|
||
import { ROLES } from "../api/types";
|
||
|
||
// Right-panel context for the Projects view. Previously embedded in the
|
||
// projects card; moved here so the centre stays the project list/create
|
||
// and the right stays the people for the selected project.
|
||
export default function MembersPanel() {
|
||
const ctx = useConsole();
|
||
const [inviteUser, setInviteUser] = useState("");
|
||
const [inviteRole, setInviteRole] = useState("contributor");
|
||
const proj = ctx.projects.find((p) => p.id === ctx.projectId);
|
||
return (
|
||
<Card title="项目成员">
|
||
{proj ? <div className="muted">{proj.name} | {proj.id} | 仓库 {proj.repo_url || "未绑定"} | 我的角色 {proj.my_role || "未知"}</div> : <div className="muted">先选项目</div>}
|
||
<Bar>
|
||
<input type="text" placeholder="成员 user_id 或用户名" value={inviteUser} onChange={(e) => setInviteUser(e.target.value)} />
|
||
<select aria-label="邀请角色" value={inviteRole} onChange={(e) => setInviteRole(e.target.value)}>
|
||
<option value="viewer">viewer(只读)</option>
|
||
<option value="contributor">contributor(可建任务)</option>
|
||
<option value="maintainer">maintainer(可批合并)</option>
|
||
<option value="owner">owner(可管项目)</option>
|
||
</select>
|
||
<button onClick={async () => { await ctx.inviteMember(inviteUser, inviteRole); setInviteUser(""); }}>邀请成员</button>
|
||
<button onClick={ctx.refreshMembers}>刷新成员</button>
|
||
</Bar>
|
||
{ctx.memberError ? <div className="muted">{ctx.memberError}</div>
|
||
: ctx.members.length === 0 ? <div className="muted">先选项目</div>
|
||
: (
|
||
<DataListRaw>
|
||
{ctx.members.map((m) => {
|
||
const label = (m.username || "") + (m.username ? "(" + String(m.user_id).slice(0, 8) + ")" : String(m.user_id).slice(0, 8));
|
||
return (
|
||
<li key={m.user_id}>
|
||
<span className="row-main">{label}</span>
|
||
<select aria-label="成员角色" value={m.role || "viewer"} onChange={(e) => void ctx.changeMemberRole(m.user_id, e.target.value)}>
|
||
{ROLES.map((r) => <option key={r} value={r}>{r}</option>)}
|
||
</select>
|
||
<button onClick={() => void ctx.removeMember(m.user_id)}>移除</button>
|
||
</li>
|
||
);
|
||
})}
|
||
</DataListRaw>
|
||
)}
|
||
<span className="muted">{ctx.memberMsg}</span>
|
||
</Card>
|
||
);
|
||
}
|