Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1860-project-team.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A Project's page now opens with its **Project team**: the Project's own Chief, the assistant that speaks for you there, and the role pool (Owner, Maintainers, Members) together with the rank you hold and the ranks you may switch to. Each of the two Bots opens that chat straight from the board, and a Project that has no Chief yet keeps the row and reads **Not set** instead of hiding it ([#1860](https://github.com/ThinkFlowLab/FlyBot/issues/1860)).
131 changes: 124 additions & 7 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,12 @@ var ZH = {
"On the board": "在看板上",
"No member is named yet.": "还没有指定成员。",
"Member management is not part of this slice; owners and the members their cards name are listed here.": "成员管理不在本次范围内;这里列出项目负责人和卡片上点名的成员。",
// The Project team block (#1860): who runs the Project, who speaks for the reader, the pool.
"Project team": "项目团队",
"My agent": "我的助手",
"Role pool": "角色池",
"Can switch to {roles}": "可切换为 {roles}",
"No one in this pool yet.": "角色池里还没有人。",
"No cards yet.": "还没有卡片。",
"Open Projects in Settings": "在设置中打开项目"
};
Expand Down Expand Up @@ -9167,6 +9173,13 @@ var KANBAN_CSS = `
}
`;

// src/client/role-labels.js
var ROLE_LABELS = { owner: "Owner", maintainer: "Maintainer", member: "Member" };
function roleText(role) {
if (role === null || role === void 0 || role === "") return "";
return t(ROLE_LABELS[role] ?? String(role));
}

// src/client/projects-nav.js
var import_react16 = require("react");
var import_react_dom3 = require("react-dom");
Expand All @@ -9188,11 +9201,6 @@ var PROJECTS_NAV_CSS = `
.bt-project-menu-item:not([aria-checked="true"])::before { content: " "; margin-right: 2px; }
.bt-project-menu-item[disabled] { opacity: .6; cursor: default; }
`;
var ROLE_LABELS = { owner: "Owner", maintainer: "Maintainer", member: "Member" };
function roleText(role) {
if (role === null || role === void 0 || role === "") return "";
return t(ROLE_LABELS[role] ?? String(role));
}
function projectHash(id) {
return `#project=${encodeURIComponent(String(id ?? ""))}`;
}
Expand Down Expand Up @@ -20199,6 +20207,20 @@ var PROJECT_BOARD_CSS = `
.bt-project-member .bt-project-member-kind{margin-left:auto;font-size:11px;opacity:.6}
.bt-project-status-line{font-size:12px;opacity:.75;min-height:16px}
.bt-project-note{font-size:12px;opacity:.65;margin:0}

/* #1860: the Project team — who runs the Project, who speaks for the reader, and the role pool.
One quiet block above the cards, and a Bot row is a button because a reader is looking a
person up. The pool row keeps the rank it plays and the ranks the host said it may switch to. */
.bt-project-team{margin:0 0 14px}
.bt-project-team h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7}
.bt-project-team-row{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px}
.bt-project-team-row[data-open-bot]{cursor:pointer}
.bt-project-team-row[data-open-bot]:hover{background:var(--bt-hover,rgba(127,127,127,.14))}
.bt-project-team-label{font-size:12px;opacity:.7;min-width:96px}
.bt-project-team-bot{font-size:12px;font-family:var(--bt-mono,ui-monospace,monospace)}
.bt-project-team-roles{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;opacity:.75}
.bt-project-team-role{font-family:var(--bt-mono,ui-monospace,monospace)}
.bt-project-team-myrole{margin-left:auto;font-size:11px;opacity:.65}
`;
var PROJECT_SOURCES = ["bot", "group", "workflow"];
var SOURCE_LABELS = { bot: "Bots", group: "Groups", workflow: "Workflows" };
Expand Down Expand Up @@ -20564,12 +20586,99 @@ function ProjectBoardRecord({ record }) {
meetings.pending.length === 0 ? null : pendingRows
);
}
function ProjectBoardPane({ project, rows, canManage, tasks, decisions, meetings, queues, metrics, onOpen, onSettings, onMembers }) {
var botIdOf = (entry) => entry !== null && typeof entry === "object" ? text(entry.botId) : "";
var idList = (value) => Array.isArray(value) ? value.map((id) => text(id)).filter((id) => id !== "") : [];
function projectTeamView(project) {
const stored = project?.roles !== null && typeof project?.roles === "object" ? project.roles : {};
const owner = [];
const addOwner = (id) => {
const clean = text(id);
if (clean !== "" && !owner.includes(clean)) owner.push(clean);
};
addOwner(project?.ownerUserId);
for (const id of idList(project?.ownerUserIds)) addOwner(id);
const maintainers = idList(stored.maintainers).filter((id) => !owner.includes(id));
const members = idList(stored.members).filter((id) => !owner.includes(id) && !maintainers.includes(id));
return {
chief: botIdOf(project?.chief),
agent: botIdOf(project?.assistant),
roles: { owner, maintainers, members },
empty: owner.length === 0 && maintainers.length === 0 && members.length === 0,
// Both come from the `projects` listing (#1828), which the overlay keeps on the record: the
// `project-read` record answers no rank, and the panel never re-derives the host's policy.
role: text(project?.role),
choices: idList(project?.choices)
};
}
var teamRow = (role, label, botId, onOpenBot) => (0, import_react47.createElement)(
"div",
{
key: role,
className: "bt-project-team-row",
"data-team-role": role,
// A row with no Bot is the fallback the slice specifies: still drawn, with nothing to open.
...botId === "" ? { "data-team-state": "not-set" } : {
"data-open-bot": botId,
role: "button",
tabIndex: 0,
"aria-label": t("Open {name}'s chat", { name: botId }),
onClick: () => onOpenBot?.(botId),
onKeyDown: (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onOpenBot?.(botId);
}
}
}
},
(0, import_react47.createElement)("span", { key: "label", className: "bt-project-team-label" }, label),
botId === "" ? (0, import_react47.createElement)("span", { key: "value", className: "bt-project-missing" }, t("Not set")) : (0, import_react47.createElement)("span", { key: "value", className: "bt-project-team-bot" }, `@${botId}`)
);
var POOL_RANKS = [["owner", "owner"], ["maintainer", "maintainers"], ["member", "members"]];
function ProjectTeam({ project, onOpenBot }) {
const view = projectTeamView(project);
const ranks = view.empty ? (0, import_react47.createElement)("span", { className: "bt-project-missing" }, t("No one in this pool yet.")) : (0, import_react47.createElement)(
"span",
{ className: "bt-project-team-roles" },
POOL_RANKS.map(([role, key]) => view.roles[key].length === 0 ? null : (0, import_react47.createElement)(
"span",
{ key: role, className: "bt-project-team-role", "data-pool-role": role },
`${roleText(role)}: ${view.roles[key].map((id) => `@${id}`).join(", ")}`
))
);
return (0, import_react47.createElement)(
"section",
{ className: "bt-project-team", "data-team": "chief,agent,pool" },
(0, import_react47.createElement)("h4", null, t("Project team")),
teamRow("chief", t("Project Chief"), view.chief, onOpenBot),
teamRow("agent", t("My agent"), view.agent, onOpenBot),
(0, import_react47.createElement)(
"div",
{ key: "pool", className: "bt-project-team-row", "data-team-role": "pool" },
(0, import_react47.createElement)("span", { key: "label", className: "bt-project-team-label" }, t("Role pool")),
ranks,
// The caller's own rank and what the host said they may switch to: the same answer the
// sidebar menu reads, printed where the pool is.
view.role === "" ? null : (0, import_react47.createElement)(
"span",
{ key: "mine", className: "bt-project-team-myrole", "data-team-my-role": view.role },
[
t("My role"),
": ",
roleText(view.role),
view.choices.length === 0 ? null : ` · ${t("Can switch to {roles}", { roles: view.choices.map(roleText).join(", ") })}`
]
)
)
);
}
function ProjectBoardPane({ project, rows, canManage, tasks, decisions, meetings, queues, metrics, onOpen, onSettings, onMembers, onOpenBot }) {
const view = projectBoardView({ project, rows, canManage, tasks, decisions, meetings, queues, metrics });
return (0, import_react47.createElement)(
"div",
{ className: "bt-project-board", "data-can-manage": view.canManage ? "true" : "false" },
(0, import_react47.createElement)("h3", null, view.title),
(0, import_react47.createElement)(ProjectTeam, { project, onOpenBot }),
// Header counts (#1768): four numbers read from the host, an unreadable one shown as unknown.
view.counts === null ? null : (0, import_react47.createElement)(
"div",
Expand Down Expand Up @@ -20766,7 +20875,9 @@ function ProjectBoardOverlay({ actions, call: request } = {}) {
]);
const tasks = Array.isArray(taskList) ? taskList : taskList?.tasks ?? [];
const queues = { waiting: board?.waiting, inflight: board?.inflight, blocked: board?.blocked, counts: board?.counts ?? null };
return { project: record?.project ?? project, rows: board?.tasks ?? [], canManage: record?.canManage === true, tasks, decisions: board?.decisions, meetings: board?.meetings, queues, metrics: board?.metrics ?? null };
const base = record?.project ?? project;
const withRole = { ...base, role: project.role ?? null, choices: Array.isArray(project.choices) ? project.choices : [] };
return { project: withRole, rows: board?.tasks ?? [], canManage: record?.canManage === true, tasks, decisions: board?.decisions, meetings: board?.meetings, queues, metrics: board?.metrics ?? null };
};
const refresh = async (first) => {
try {
Expand Down Expand Up @@ -20822,6 +20933,12 @@ function ProjectBoardOverlay({ actions, call: request } = {}) {
metrics: state.metrics,
onSettings: () => setPanel("settings"),
onMembers: () => setPanel("members"),
// #1860: a team row opens that Bot's chat. The board is a modal over the conversation,
// so it closes first — otherwise the chat opens behind it.
onOpenBot: (id) => {
close();
actions?.openSession?.(id);
},
// The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and
// lands on that Bot or group. Nothing here re-decides which board that is.
onOpen: (board) => {
Expand Down
6 changes: 6 additions & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,12 @@ const ZH = {
'On the board': '在看板上',
'No member is named yet.': '还没有指定成员。',
'Member management is not part of this slice; owners and the members their cards name are listed here.': '成员管理不在本次范围内;这里列出项目负责人和卡片上点名的成员。',
// The Project team block (#1860): who runs the Project, who speaks for the reader, the pool.
'Project team': '项目团队',
'My agent': '我的助手',
'Role pool': '角色池',
'Can switch to {roles}': '可切换为 {roles}',
'No one in this pool yet.': '角色池里还没有人。',
'No cards yet.': '还没有卡片。',
'Open Projects in Settings': '在设置中打开项目',
}
Expand Down
Loading
Loading