Before this commit one piece of state (`selectedAgentId`) did three
jobs: roster highlight, navigation destination, and Edit-modal open
flag. The three jobs had incompatible lifecycles — highlight should
persist across modal close, navigation should land on a screen
without opening a modal, and the modal itself only wants to open on
explicit intent. The conflation was the root cause of the "Edit: CEO
auto-opens on first Team view" bug (commit 41132e2 patched the most
visible symptom by not pre-selecting the lead agent, but the coupling
remained).
Split into two slice fields:
- `selectedAgentId` — which agent is currently highlighted in the
roster / OrgChart. Survives modal close. Set by: row click, card
click, navigation from Inbox / Hiring (all of which pass through
`openAgentEditor` which sets both so the highlight follows the last
interaction).
- `editingAgentId` — which agent's Edit modal is open, or
NEW_ENTITY_ID for the "Request new hire" modal. Only set by
explicit user action; cleared on modal close. Never set by
navigation or onboarding — the pre-select pathway cannot
accidentally open a modal again.
Plumbing:
- `selection.ts`: new `editingAgentId` + `setEditingAgentId`, plus
clear on `clearCompanyScopedSelections`.
- `useCompanyData.ts`: derive `editingAgent` record from the id.
- `useCompanyController.ts`: forward `editingAgent` on the section
context.
- `OrganizationSections.tsx`: `agents` section modals read
`editingAgentId` / `editingAgent`. "Request new hire" button uses
`setEditingAgentId(NEW_ENTITY_ID)`. Single-click handlers go
through `openAgentEditor(id)` which updates both states.
Also improved Edit modal title (was "Edit: CEO", a machined English
prefix on a Chinese UI with zero context about the role):
- zh: "编辑 {name}({role})"
- en: "Edit {name} ({role})"
Renders as "编辑 CEO(首席执行官)" in the screenshot example.
Covers J9 / S2.
146 lines
6.0 KiB
TypeScript
146 lines
6.0 KiB
TypeScript
import { useMemo } from "react";
|
|
import type { CompanyRecord, ProfileSnapshot, SectionId } from "@shared/types";
|
|
import { useAppStore } from "../state/store";
|
|
|
|
function filterByCompany<T extends { companyId: string }>(
|
|
items: T[] | undefined,
|
|
currentCompany: CompanyRecord | null,
|
|
): T[] {
|
|
if (!items) return [];
|
|
if (!currentCompany) return items;
|
|
return items.filter((item) => item.companyId === currentCompany.id);
|
|
}
|
|
|
|
export function useCompanyData(
|
|
snapshot: ProfileSnapshot | null,
|
|
currentCompany: CompanyRecord | null,
|
|
) {
|
|
const selectedRunId = useAppStore((s) => s.selectedRunId);
|
|
const selectedTaskId = useAppStore((s) => s.selectedTaskId);
|
|
const selectedApprovalId = useAppStore((s) => s.selectedApprovalId);
|
|
const selectedAgentId = useAppStore((s) => s.selectedAgentId);
|
|
const editingAgentId = useAppStore((s) => s.editingAgentId);
|
|
const selectedGoalId = useAppStore((s) => s.selectedGoalId);
|
|
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
|
const selectedWorkspaceId = useAppStore((s) => s.selectedWorkspaceId);
|
|
|
|
const tasks = useMemo(() => filterByCompany(snapshot?.tasks, currentCompany), [snapshot, currentCompany]);
|
|
const approvals = useMemo(() => filterByCompany(snapshot?.approvals, currentCompany), [snapshot, currentCompany]);
|
|
const pendingApprovals = useMemo(() => approvals.filter((approval) => approval.state === "pending"), [approvals]);
|
|
const runs = useMemo(() => filterByCompany(snapshot?.runs, currentCompany), [snapshot, currentCompany]);
|
|
const companyAgents = useMemo(() => filterByCompany(snapshot?.agents, currentCompany), [snapshot, currentCompany]);
|
|
const companyGoals = useMemo(() => filterByCompany(snapshot?.goals, currentCompany), [snapshot, currentCompany]);
|
|
const companyProjects = useMemo(() => filterByCompany(snapshot?.projects, currentCompany), [snapshot, currentCompany]);
|
|
const companyWorkspaces = useMemo(() => filterByCompany(snapshot?.workspaces, currentCompany), [snapshot, currentCompany]);
|
|
const companyCosts = useMemo(() => filterByCompany(snapshot?.costs, currentCompany), [snapshot, currentCompany]);
|
|
const filteredActivity = useMemo(() => filterByCompany(snapshot?.activity, currentCompany), [snapshot, currentCompany]);
|
|
const companySecrets = useMemo(() => filterByCompany(snapshot?.secrets, currentCompany), [snapshot, currentCompany]);
|
|
const companyDocuments = useMemo(() => filterByCompany(snapshot?.documents, currentCompany), [snapshot, currentCompany]);
|
|
const companyKnowledge = useMemo(() => filterByCompany(snapshot?.knowledgeBase, currentCompany), [snapshot, currentCompany]);
|
|
const companyMeetings = useMemo(() => filterByCompany(snapshot?.meetings, currentCompany), [snapshot, currentCompany]);
|
|
const companySprints = useMemo(() => filterByCompany(snapshot?.sprints, currentCompany), [snapshot, currentCompany]);
|
|
|
|
// HOOK-005 fix: memoize selection derivations so their identity is stable
|
|
// across renders when the backing collections and selected id have not
|
|
// changed.
|
|
const selectedTask = useMemo(
|
|
() => (selectedTaskId ? (tasks.find((task) => task.id === selectedTaskId) ?? null) : null),
|
|
[selectedTaskId, tasks],
|
|
);
|
|
const selectedApproval = useMemo(
|
|
() => (selectedApprovalId ? (approvals.find((approval) => approval.id === selectedApprovalId) ?? null) : null),
|
|
[selectedApprovalId, approvals],
|
|
);
|
|
const selectedRun = useMemo(
|
|
() => runs.find((run) => run.id === selectedRunId) ?? runs[0] ?? null,
|
|
[runs, selectedRunId],
|
|
);
|
|
const selectedAgent = useMemo(
|
|
() => companyAgents.find((a) => a.id === selectedAgentId) ?? null,
|
|
[companyAgents, selectedAgentId],
|
|
);
|
|
const editingAgent = useMemo(
|
|
() => companyAgents.find((a) => a.id === editingAgentId) ?? null,
|
|
[companyAgents, editingAgentId],
|
|
);
|
|
const selectedGoal = useMemo(
|
|
() => companyGoals.find((g) => g.id === selectedGoalId) ?? null,
|
|
[companyGoals, selectedGoalId],
|
|
);
|
|
const selectedProject = useMemo(
|
|
() => companyProjects.find((p) => p.id === selectedProjectId) ?? null,
|
|
[companyProjects, selectedProjectId],
|
|
);
|
|
const selectedWorkspace = useMemo(
|
|
() => companyWorkspaces.find((workspace) => workspace.id === selectedWorkspaceId) ?? companyWorkspaces[0] ?? null,
|
|
[companyWorkspaces, selectedWorkspaceId],
|
|
);
|
|
|
|
const navBadgesSource = useAppStore((s) => s.badges);
|
|
const navBadges = useMemo<Partial<Record<SectionId, { count: number; tone: "default" | "warn" | "danger" }>>>(() => navBadgesSource ? {
|
|
runs:
|
|
navBadgesSource.failedRuns > 0
|
|
? { count: navBadgesSource.failedRuns, tone: "danger" }
|
|
: navBadgesSource.activeRuns > 0
|
|
? { count: navBadgesSource.activeRuns, tone: "default" }
|
|
: undefined,
|
|
tasks: navBadgesSource.todoTasks > 0 ? { count: navBadgesSource.todoTasks, tone: "default" } : undefined,
|
|
approvals: navBadgesSource.pendingApprovals > 0 ? { count: navBadgesSource.pendingApprovals, tone: "warn" } : undefined,
|
|
communication: navBadgesSource.unreadMessages > 0 ? { count: navBadgesSource.unreadMessages, tone: "warn" } : undefined,
|
|
} : {}, [navBadgesSource]);
|
|
|
|
return useMemo(() => ({
|
|
tasks,
|
|
approvals,
|
|
pendingApprovals,
|
|
runs,
|
|
companyAgents,
|
|
companyGoals,
|
|
companyProjects,
|
|
companyWorkspaces,
|
|
companyCosts,
|
|
filteredActivity,
|
|
companySecrets,
|
|
companyDocuments,
|
|
companyKnowledge,
|
|
companyMeetings,
|
|
companySprints,
|
|
selectedTask,
|
|
selectedApproval,
|
|
selectedRun,
|
|
selectedAgent,
|
|
editingAgent,
|
|
selectedGoal,
|
|
selectedProject,
|
|
selectedWorkspace,
|
|
navBadges,
|
|
}), [
|
|
tasks,
|
|
approvals,
|
|
pendingApprovals,
|
|
runs,
|
|
companyAgents,
|
|
companyGoals,
|
|
companyProjects,
|
|
companyWorkspaces,
|
|
companyCosts,
|
|
filteredActivity,
|
|
companySecrets,
|
|
companyDocuments,
|
|
companyKnowledge,
|
|
companyMeetings,
|
|
companySprints,
|
|
selectedTask,
|
|
selectedApproval,
|
|
selectedRun,
|
|
selectedAgent,
|
|
editingAgent,
|
|
selectedGoal,
|
|
selectedProject,
|
|
selectedWorkspace,
|
|
navBadges,
|
|
]);
|
|
}
|
|
|
|
export type CompanyData = ReturnType<typeof useCompanyData>;
|