refactor(web): move the workbench shell out of App

Session state stays in App. The shell, rights dialogs, and project modals now render from WorkbenchView, so App no longer mounts AppShell itself.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-19 14:11:32 +00:00
co-authored by Cursor
parent 8a9599fbb0
commit 56a8719863
3 changed files with 456 additions and 250 deletions
+149 -250
View File
@@ -1,5 +1,5 @@
import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
import { AppShell } from './components/layout/AppShell';
import { WorkbenchView } from './components/layout/WorkbenchView';
import { DocumentPane } from './components/doc/DocumentPane';
import { ChatColumn } from './components/chat/ChatColumn';
import { fetchFreeformDoc } from './api/freeformDoc';
@@ -34,16 +34,9 @@ import {
import { DocumentLineage } from './types/projectWorkflow';
import { StaleDocumentNotice } from './components/workflow/StaleDocumentNotice';
import { StageAdvanceBanner } from './components/workflow/StageAdvanceBanner';
import { UserProjectRightsModal } from './components/user/UserProjectRightsModal';
import { UserSettingsModal } from './components/user/UserSettingsModal';
import { OnDemand } from './components/lazy/OnDemand';
import { DocVisibilityModal } from './components/doc/DocVisibilityModal';
import { CreateProjectModal } from './components/project/CreateProjectModal';
import { ProjectMembersModal } from './components/project/ProjectMembersModal';
import { ProjectSettingsModal } from './components/project/ProjectSettingsModal';
import { fetchProjectSettings } from './api/projectSettings';
import { LoginView } from './components/auth/LoginView';
import { FirstLoginPasswordModal } from './components/auth/FirstLoginPasswordModal';
import { authClient } from './api/auth';
import { LoginResult, SystemAuthConfig } from './types/auth';
import { markdownToBlockAST } from './engine/markdownToBlock';
@@ -1496,247 +1489,153 @@ export default function App() {
}
return (
<>
<AppShell
sessions={visibleSessions}
activeSessionId={activeSessionId}
onSelectSession={(sessId) => {
if (window.location.hash) {
history.replaceState(null, '', window.location.pathname + window.location.search);
}
setActiveSessionId(sessId);
const targetSess = sessions.find((s) => s.sessionId === sessId);
if (targetSess && targetSess.projectId !== currentProjectId) {
const targetProjDocs = deliverables.filter((d) => d.projectId === targetSess.projectId);
if (targetProjDocs.length > 0) {
setActiveDocId(targetProjDocs[0].docId);
setActiveCategory(targetProjDocs[0].category);
}
}
}}
onNewSession={() => setCreateProjectModalOpen(true)}
canCreateProject={canCreateProject}
currentUser={currentUser}
currentRole={currentDocRole}
currentUserName={currentUser.name}
onChangeRole={(_, __) => {}}
onOpenRightsModal={() => setRightsModalOpen(true)}
onOpenUserSettings={() => setUserSettingsOpen(true)}
onOpenAdminSettings={() => setView('admin')}
onOpenProjectMembers={() => setProjectMembersModalOpen(true)}
activeWorkflowKey={currentWfState.activeKey}
workflowVersion={currentWfState.version}
lifecycleStatus={currentWfState.lifecycleStatus}
isCreator={currentWfState.creatorId === currentUser.id}
governanceEnabled={workflowGovernanceEnabled}
onOpenProjectSettings={() => setProjectSettingsOpen(true)}
onWorkflowChanged={(newKey, newVer) => {
setProjectWorkflowsState((prev) => ({
...prev,
[currentProjectId]: {
...currentWfState,
activeKey: newKey,
version: newVer,
lifecycleStatus: 'active',
},
}));
}}
workflowBanner={workflowBanner}
chatContent={
<ChatColumn
activeSessionId={activeSessionId}
categoryStats={categoryStats}
chatWindow={chatWindow}
isCalendarOpen={isCalendarOpen}
setIsCalendarOpen={setIsCalendarOpen}
isSearchOpen={isSearchOpen}
setIsSearchOpen={setIsSearchOpen}
isDigestOpen={isDigestOpen}
setIsDigestOpen={setIsDigestOpen}
isExportOpen={isExportOpen}
setIsExportOpen={setIsExportOpen}
unreadTotal={unreadTotal}
dateStats={dateStats}
unreadMarks={unreadMarks}
unreadMessageIds={unreadMessageIds}
isAllReadCompleted={isAllReadCompleted}
consumeNextUnread={consumeNextUnread}
handleSelectDoc={handleSelectDoc}
handleCardAction={handleCardAction}
handleErrorAction={handleErrorAction}
handleQuoteMessage={handleQuoteMessage}
handleSaveMessageToPrivate={handleSaveMessageToPrivate}
handleExportReplyDocx={handleExportReplyDocx}
pendingProposal={pendingProposal}
proposalError={proposalError}
handleAcceptProposal={handleAcceptProposal}
handleRejectProposal={handleRejectProposal}
handleSendMessage={handleSendMessage}
isThinking={isThinking}
isStreamingActive={isStreamingActive}
handleStopStreaming={handleStopStreaming}
activeQuotes={activeQuotes}
handleRemoveQuote={handleRemoveQuote}
isCurrentProjectExited={isCurrentProjectExited}
/>
}
docContent={
<DocumentPane
isCurrentProjectExited={isCurrentProjectExited}
activeDocId={activeDocId}
deliverables={deliverables}
visibleDeliverables={visibleDeliverables}
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
handleSelectDoc={handleSelectDoc}
deckDrawerOpen={deckDrawerOpen}
setDeckDrawerOpen={setDeckDrawerOpen}
handleExportDocx={handleExportDocx}
handleExportXlsx={handleExportXlsx}
handleExportMarkdown={handleExportMarkdown}
docRevOverlayMode={docRevOverlayMode}
setDocRevOverlayMode={setDocRevOverlayMode}
setDocRevOverlay={setDocRevOverlay}
docRevRefreshKey={docRevRefreshKey}
docRevConflict={docRevConflict}
docRevOverlay={docRevOverlay}
currentUser={currentUser}
handleShareToProject={handleShareToProject}
handleRevokeShare={handleRevokeShare}
handleDeleteDoc={handleDeleteDoc}
setTargetDocForVisibility={setTargetDocForVisibility}
setDocVisibilityModalOpen={setDocVisibilityModalOpen}
docLoadErrors={docLoadErrors}
docLoadErrorInfo={docLoadErrorInfo}
retryControlledDocLoad={retryControlledDocLoad}
docAST={docAST}
currentDocRole={currentDocRole}
openBlockEditor={openBlockEditor}
handleInsertBlock={handleInsertBlock}
handleMoveBlock={handleMoveBlock}
handleDeleteBlock={handleDeleteBlock}
setCommentingBlock={setCommentingBlock}
handleSendMessage={handleSendMessage}
highlightedBlockId={highlightedBlockId}
canOverrideVisibility={canOverrideVisibility}
handleClaim={handleClaim}
handleSubmitReview={handleSubmitReview}
handlePassReview={handlePassReview}
handleRejectReview={handleRejectReview}
handleApprove={handleApprove}
handleRejectApprove={handleRejectApprove}
editorSession={editorSession}
handleSaveBlockEdit={handleSaveBlockEdit}
closeBlockEditor={closeBlockEditor}
commentingBlock={commentingBlock}
handleAddComment={handleAddComment}
/>
}
/>
{/* 用户在当前激活项目中的法定职责与权限矩阵弹窗 */}
<UserProjectRightsModal
isOpen={rightsModalOpen}
onClose={() => setRightsModalOpen(false)}
currentUser={currentUser}
currentProjectTitle={sessions.find((s) => s.sessionId === activeSessionId)?.title || '当前项目'}
deliverables={visibleDeliverables}
activeDocId={activeDocId}
canAssignLead={canAssignLead}
onSelectDoc={handleSelectDoc}
onRoleChangeForDoc={handleRoleChangeForDoc}
/>
{/* 首次登录强制改密阻断弹窗 */}
<FirstLoginPasswordModal
isOpen={!!session?.mustChangePassword}
user={currentUser}
onPasswordChanged={() => {
const updated = { ...session, mustChangePassword: false };
setSession(updated);
authClient.saveSession(updated);
}}
onLogout={handleLogout}
/>
{/* 用户个人设置与偏好中心 (全员可用) */}
<UserSettingsModal
isOpen={userSettingsOpen}
onClose={() => setUserSettingsOpen(false)}
currentUser={currentUser}
roles={roles}
onUpdateProfile={(updatedProfile) => {
setAllUsers((prev) =>
prev.map((u) => (u.id === currentUser.id ? { ...u, ...updatedProfile } : u))
);
setCurrentUser((prev) => ({ ...prev, ...updatedProfile }));
}}
onLogout={handleLogout}
/>
{/* 单文档可见性覆盖弹窗 */}
<DocVisibilityModal
isOpen={docVisibilityModalOpen}
onClose={() => setDocVisibilityModalOpen(false)}
doc={targetDocForVisibility}
roles={roles}
currentUserName={currentUser.name}
onSaveRule={handleSaveDocVisibilityRule}
/>
{/* 发起医疗器械新项目立项弹窗 */}
<CreateProjectModal
isOpen={createProjectModalOpen}
onClose={() => {
if (createProjectSubmitting) return;
setCreateProjectModalOpen(false);
setCreateProjectError(null);
}}
onSubmit={handleCreateProjectConfirm}
submitError={createProjectError}
submitting={createProjectSubmitting}
submitPhase={createProjectSubmitPhase}
currentUser={currentUser}
availableUsers={allUsers}
/>
{/* 项目成员与职责分配管理弹窗 */}
<ProjectMembersModal
isOpen={projectMembersModalOpen}
onClose={() => setProjectMembersModalOpen(false)}
project={currentProject}
currentUser={currentUser}
availableUsers={allUsers}
canManage={canManageProjectMembers}
membersError={projectMembersError}
onAddMember={handleAddProjectMember}
onUpdateMemberRole={handleUpdateProjectMemberRole}
onRemoveMember={handleRemoveProjectMember}
onRestoreMember={handleRestoreProjectMember}
/>
<ProjectSettingsModal
open={projectSettingsOpen}
projectId={currentProjectId}
projectName={currentActiveSession?.title || '当前项目'}
members={(currentProject?.members || [])
.filter((m) => m.status === 'active')
.map((m) => ({
userId: m.userId,
name: allUsers.find((u) => u.id === m.userId)?.name || `用户 ${m.userId}`,
}))}
onClose={() => setProjectSettingsOpen(false)}
onActivated={(activeKey) => {
setProjectWorkflowsState((prev) => ({
...prev,
[currentProjectId]: {
...currentWfState,
activeKey,
lifecycleStatus: 'active',
},
}));
}}
/>
</>
<WorkbenchView
visibleSessions={visibleSessions}
activeSessionId={activeSessionId}
sessions={sessions}
deliverables={deliverables}
currentProjectId={currentProjectId}
setActiveSessionId={setActiveSessionId}
setActiveDocId={setActiveDocId}
setActiveCategory={setActiveCategory}
setCreateProjectModalOpen={setCreateProjectModalOpen}
canCreateProject={canCreateProject}
currentUser={currentUser}
currentDocRole={currentDocRole}
setRightsModalOpen={setRightsModalOpen}
setUserSettingsOpen={setUserSettingsOpen}
setView={setView}
setProjectMembersModalOpen={setProjectMembersModalOpen}
currentWfState={currentWfState}
workflowGovernanceEnabled={workflowGovernanceEnabled}
setProjectSettingsOpen={setProjectSettingsOpen}
setProjectWorkflowsState={setProjectWorkflowsState}
workflowBanner={workflowBanner}
chatContent={
<ChatColumn
activeSessionId={activeSessionId}
categoryStats={categoryStats}
chatWindow={chatWindow}
isCalendarOpen={isCalendarOpen}
setIsCalendarOpen={setIsCalendarOpen}
isSearchOpen={isSearchOpen}
setIsSearchOpen={setIsSearchOpen}
isDigestOpen={isDigestOpen}
setIsDigestOpen={setIsDigestOpen}
isExportOpen={isExportOpen}
setIsExportOpen={setIsExportOpen}
unreadTotal={unreadTotal}
dateStats={dateStats}
unreadMarks={unreadMarks}
unreadMessageIds={unreadMessageIds}
isAllReadCompleted={isAllReadCompleted}
consumeNextUnread={consumeNextUnread}
handleSelectDoc={handleSelectDoc}
handleCardAction={handleCardAction}
handleErrorAction={handleErrorAction}
handleQuoteMessage={handleQuoteMessage}
handleSaveMessageToPrivate={handleSaveMessageToPrivate}
handleExportReplyDocx={handleExportReplyDocx}
pendingProposal={pendingProposal}
proposalError={proposalError}
handleAcceptProposal={handleAcceptProposal}
handleRejectProposal={handleRejectProposal}
handleSendMessage={handleSendMessage}
isThinking={isThinking}
isStreamingActive={isStreamingActive}
handleStopStreaming={handleStopStreaming}
activeQuotes={activeQuotes}
handleRemoveQuote={handleRemoveQuote}
isCurrentProjectExited={isCurrentProjectExited}
/>
}
docContent={
<DocumentPane
isCurrentProjectExited={isCurrentProjectExited}
activeDocId={activeDocId}
deliverables={deliverables}
visibleDeliverables={visibleDeliverables}
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
handleSelectDoc={handleSelectDoc}
deckDrawerOpen={deckDrawerOpen}
setDeckDrawerOpen={setDeckDrawerOpen}
handleExportDocx={handleExportDocx}
handleExportXlsx={handleExportXlsx}
handleExportMarkdown={handleExportMarkdown}
docRevOverlayMode={docRevOverlayMode}
setDocRevOverlayMode={setDocRevOverlayMode}
setDocRevOverlay={setDocRevOverlay}
docRevRefreshKey={docRevRefreshKey}
docRevConflict={docRevConflict}
docRevOverlay={docRevOverlay}
currentUser={currentUser}
handleShareToProject={handleShareToProject}
handleRevokeShare={handleRevokeShare}
handleDeleteDoc={handleDeleteDoc}
setTargetDocForVisibility={setTargetDocForVisibility}
setDocVisibilityModalOpen={setDocVisibilityModalOpen}
docLoadErrors={docLoadErrors}
docLoadErrorInfo={docLoadErrorInfo}
retryControlledDocLoad={retryControlledDocLoad}
docAST={docAST}
currentDocRole={currentDocRole}
openBlockEditor={openBlockEditor}
handleInsertBlock={handleInsertBlock}
handleMoveBlock={handleMoveBlock}
handleDeleteBlock={handleDeleteBlock}
setCommentingBlock={setCommentingBlock}
handleSendMessage={handleSendMessage}
highlightedBlockId={highlightedBlockId}
canOverrideVisibility={canOverrideVisibility}
handleClaim={handleClaim}
handleSubmitReview={handleSubmitReview}
handlePassReview={handlePassReview}
handleRejectReview={handleRejectReview}
handleApprove={handleApprove}
handleRejectApprove={handleRejectApprove}
editorSession={editorSession}
handleSaveBlockEdit={handleSaveBlockEdit}
closeBlockEditor={closeBlockEditor}
commentingBlock={commentingBlock}
handleAddComment={handleAddComment}
/>
}
rightsModalOpen={rightsModalOpen}
visibleDeliverables={visibleDeliverables}
activeDocId={activeDocId}
canAssignLead={canAssignLead}
handleSelectDoc={handleSelectDoc}
handleRoleChangeForDoc={handleRoleChangeForDoc}
session={session}
setSession={setSession}
handleLogout={handleLogout}
userSettingsOpen={userSettingsOpen}
roles={roles}
setAllUsers={setAllUsers}
setCurrentUser={setCurrentUser}
docVisibilityModalOpen={docVisibilityModalOpen}
setDocVisibilityModalOpen={setDocVisibilityModalOpen}
targetDocForVisibility={targetDocForVisibility}
handleSaveDocVisibilityRule={handleSaveDocVisibilityRule}
createProjectModalOpen={createProjectModalOpen}
createProjectSubmitting={createProjectSubmitting}
setCreateProjectError={setCreateProjectError}
handleCreateProjectConfirm={handleCreateProjectConfirm}
createProjectError={createProjectError}
createProjectSubmitPhase={createProjectSubmitPhase}
allUsers={allUsers}
projectMembersModalOpen={projectMembersModalOpen}
currentProject={currentProject}
canManageProjectMembers={canManageProjectMembers}
projectMembersError={projectMembersError}
handleAddProjectMember={handleAddProjectMember}
handleUpdateProjectMemberRole={handleUpdateProjectMemberRole}
handleRemoveProjectMember={handleRemoveProjectMember}
handleRestoreProjectMember={handleRestoreProjectMember}
projectSettingsOpen={projectSettingsOpen}
currentActiveSession={currentActiveSession}
/>
);
}
@@ -0,0 +1,13 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
test('workbench shell lives outside App', () => {
const app = readFileSync(new URL('../../App.tsx', import.meta.url), 'utf8');
const view = readFileSync(new URL('./WorkbenchView.tsx', import.meta.url), 'utf8');
assert.equal((app.match(/<WorkbenchView\b/g) || []).length, 1);
assert.equal(app.includes('<AppShell'), false);
assert.equal(view.includes('<AppShell'), true);
assert.equal(view.includes('<ProjectSettingsModal'), true);
assert.equal(view.includes('<FirstLoginPasswordModal'), true);
});
+294
View File
@@ -0,0 +1,294 @@
import type { Dispatch, ReactNode, SetStateAction } from 'react';
import { AppShell } from './AppShell';
import { UserProjectRightsModal } from '../user/UserProjectRightsModal';
import { UserSettingsModal } from '../user/UserSettingsModal';
import { DocVisibilityModal } from '../doc/DocVisibilityModal';
import { CreateProjectModal, type CreateProjectConfirmData } from '../project/CreateProjectModal';
import { ProjectMembersModal } from '../project/ProjectMembersModal';
import { ProjectSettingsModal } from '../project/ProjectSettingsModal';
import { FirstLoginPasswordModal } from '../auth/FirstLoginPasswordModal';
import { authClient } from '../../api/auth';
import type { LoginResult } from '../../types/auth';
import type { DocPermissionRole, UserProfile, UserRole } from '../../types/docgov';
import type { DocVisibilityRule, RoleDefinition } from '../../types/rbac';
import type { CreateProjectSubmitPhase } from '../../lib/createProjectSubmitGuard';
import type {
DeliverableCategory,
DeliverableItem,
ProjectInfo,
ProjectMember,
ProjectMemberRole,
ProjectSession,
} from '../../types/workbench';
export type WorkbenchWorkflowState = {
activeKey: string;
version: number;
creatorId: number;
lifecycleStatus: string;
};
export interface WorkbenchViewProps {
visibleSessions: ProjectSession[];
activeSessionId: string;
sessions: ProjectSession[];
deliverables: DeliverableItem[];
currentProjectId: number;
setActiveSessionId: (id: string) => void;
setActiveDocId: (id: string) => void;
setActiveCategory: (category: DeliverableCategory) => void;
setCreateProjectModalOpen: (open: boolean) => void;
canCreateProject: boolean;
currentUser: UserProfile;
currentDocRole: UserRole;
setRightsModalOpen: Dispatch<SetStateAction<boolean>>;
setUserSettingsOpen: Dispatch<SetStateAction<boolean>>;
setView: (view: 'workbench' | 'admin') => void;
setProjectMembersModalOpen: Dispatch<SetStateAction<boolean>>;
currentWfState: WorkbenchWorkflowState;
workflowGovernanceEnabled: boolean;
setProjectSettingsOpen: Dispatch<SetStateAction<boolean>>;
setProjectWorkflowsState: Dispatch<SetStateAction<Record<number, WorkbenchWorkflowState>>>;
workflowBanner: ReactNode;
chatContent: ReactNode;
docContent: ReactNode;
rightsModalOpen: boolean;
visibleDeliverables: DeliverableItem[];
activeDocId: string;
canAssignLead: boolean;
handleSelectDoc: (id: string) => void;
handleRoleChangeForDoc: (docId: string, role: DocPermissionRole) => void;
session: LoginResult | null;
setSession: Dispatch<SetStateAction<LoginResult | null>>;
handleLogout: () => void;
userSettingsOpen: boolean;
roles: RoleDefinition[];
setAllUsers: Dispatch<SetStateAction<UserProfile[]>>;
setCurrentUser: Dispatch<SetStateAction<UserProfile>>;
docVisibilityModalOpen: boolean;
setDocVisibilityModalOpen: Dispatch<SetStateAction<boolean>>;
targetDocForVisibility: DeliverableItem | null;
handleSaveDocVisibilityRule: (docId: string, rule: DocVisibilityRule | undefined) => void;
createProjectModalOpen: boolean;
createProjectSubmitting: boolean;
setCreateProjectError: Dispatch<SetStateAction<string | null>>;
handleCreateProjectConfirm: (data: CreateProjectConfirmData) => void | Promise<void>;
createProjectError: string | null;
createProjectSubmitPhase: CreateProjectSubmitPhase | null;
allUsers: UserProfile[];
projectMembersModalOpen: boolean;
currentProject: ProjectInfo | undefined;
canManageProjectMembers: boolean;
projectMembersError: string | null;
handleAddProjectMember: (member: Omit<ProjectMember, 'joinedAt' | 'status'>) => void | Promise<void>;
handleUpdateProjectMemberRole: (userId: number, role: ProjectMemberRole) => void | Promise<void>;
handleRemoveProjectMember: (userId: number) => void | Promise<void>;
handleRestoreProjectMember: (userId: number) => void | Promise<void>;
projectSettingsOpen: boolean;
currentActiveSession: ProjectSession | undefined;
}
export function WorkbenchView({
visibleSessions,
activeSessionId,
sessions,
deliverables,
currentProjectId,
setActiveSessionId,
setActiveDocId,
setActiveCategory,
setCreateProjectModalOpen,
canCreateProject,
currentUser,
currentDocRole,
setRightsModalOpen,
setUserSettingsOpen,
setView,
setProjectMembersModalOpen,
currentWfState,
workflowGovernanceEnabled,
setProjectSettingsOpen,
setProjectWorkflowsState,
workflowBanner,
chatContent,
docContent,
rightsModalOpen,
visibleDeliverables,
activeDocId,
canAssignLead,
handleSelectDoc,
handleRoleChangeForDoc,
session,
setSession,
handleLogout,
userSettingsOpen,
roles,
setAllUsers,
setCurrentUser,
docVisibilityModalOpen,
setDocVisibilityModalOpen,
targetDocForVisibility,
handleSaveDocVisibilityRule,
createProjectModalOpen,
createProjectSubmitting,
setCreateProjectError,
handleCreateProjectConfirm,
createProjectError,
createProjectSubmitPhase,
allUsers,
projectMembersModalOpen,
currentProject,
canManageProjectMembers,
projectMembersError,
handleAddProjectMember,
handleUpdateProjectMemberRole,
handleRemoveProjectMember,
handleRestoreProjectMember,
projectSettingsOpen,
currentActiveSession,
}: WorkbenchViewProps) {
return (
<>
<AppShell
sessions={visibleSessions}
activeSessionId={activeSessionId}
onSelectSession={(sessId) => {
if (window.location.hash) {
history.replaceState(null, '', window.location.pathname + window.location.search);
}
setActiveSessionId(sessId);
const targetSess = sessions.find((s) => s.sessionId === sessId);
if (targetSess && targetSess.projectId !== currentProjectId) {
const targetProjDocs = deliverables.filter((d) => d.projectId === targetSess.projectId);
if (targetProjDocs.length > 0) {
setActiveDocId(targetProjDocs[0].docId);
setActiveCategory(targetProjDocs[0].category);
}
}
}}
onNewSession={() => setCreateProjectModalOpen(true)}
canCreateProject={canCreateProject}
currentUser={currentUser}
currentRole={currentDocRole}
currentUserName={currentUser.name}
onChangeRole={(_, __) => {}}
onOpenRightsModal={() => setRightsModalOpen(true)}
onOpenUserSettings={() => setUserSettingsOpen(true)}
onOpenAdminSettings={() => setView('admin')}
onOpenProjectMembers={() => setProjectMembersModalOpen(true)}
activeWorkflowKey={currentWfState.activeKey}
workflowVersion={currentWfState.version}
lifecycleStatus={currentWfState.lifecycleStatus}
isCreator={currentWfState.creatorId === currentUser.id}
governanceEnabled={workflowGovernanceEnabled}
onOpenProjectSettings={() => setProjectSettingsOpen(true)}
onWorkflowChanged={(newKey, newVer) => {
setProjectWorkflowsState((prev) => ({
...prev,
[currentProjectId]: {
...currentWfState,
activeKey: newKey,
version: newVer,
lifecycleStatus: 'active',
},
}));
}}
workflowBanner={workflowBanner}
chatContent={chatContent}
docContent={docContent}
/>
<UserProjectRightsModal
isOpen={rightsModalOpen}
onClose={() => setRightsModalOpen(false)}
currentUser={currentUser}
currentProjectTitle={sessions.find((s) => s.sessionId === activeSessionId)?.title || '当前项目'}
deliverables={visibleDeliverables}
activeDocId={activeDocId}
canAssignLead={canAssignLead}
onSelectDoc={handleSelectDoc}
onRoleChangeForDoc={handleRoleChangeForDoc}
/>
<FirstLoginPasswordModal
isOpen={!!session?.mustChangePassword}
user={currentUser}
onPasswordChanged={() => {
if (!session) return;
const updated = { ...session, mustChangePassword: false };
setSession(updated);
authClient.saveSession(updated);
}}
onLogout={handleLogout}
/>
<UserSettingsModal
isOpen={userSettingsOpen}
onClose={() => setUserSettingsOpen(false)}
currentUser={currentUser}
roles={roles}
onUpdateProfile={(updatedProfile) => {
setAllUsers((prev) =>
prev.map((u) => (u.id === currentUser.id ? { ...u, ...updatedProfile } : u))
);
setCurrentUser((prev) => ({ ...prev, ...updatedProfile }));
}}
onLogout={handleLogout}
/>
<DocVisibilityModal
isOpen={docVisibilityModalOpen}
onClose={() => setDocVisibilityModalOpen(false)}
doc={targetDocForVisibility}
roles={roles}
currentUserName={currentUser.name}
onSaveRule={handleSaveDocVisibilityRule}
/>
<CreateProjectModal
isOpen={createProjectModalOpen}
onClose={() => {
if (createProjectSubmitting) return;
setCreateProjectModalOpen(false);
setCreateProjectError(null);
}}
onSubmit={handleCreateProjectConfirm}
submitError={createProjectError}
submitting={createProjectSubmitting}
submitPhase={createProjectSubmitPhase}
currentUser={currentUser}
availableUsers={allUsers}
/>
<ProjectMembersModal
isOpen={projectMembersModalOpen}
onClose={() => setProjectMembersModalOpen(false)}
project={currentProject}
currentUser={currentUser}
availableUsers={allUsers}
canManage={canManageProjectMembers}
membersError={projectMembersError}
onAddMember={handleAddProjectMember}
onUpdateMemberRole={handleUpdateProjectMemberRole}
onRemoveMember={handleRemoveProjectMember}
onRestoreMember={handleRestoreProjectMember}
/>
<ProjectSettingsModal
open={projectSettingsOpen}
projectId={currentProjectId}
projectName={currentActiveSession?.title || '当前项目'}
members={(currentProject?.members || [])
.filter((m) => m.status === 'active')
.map((m) => ({
userId: m.userId,
name: allUsers.find((u) => u.id === m.userId)?.name || `用户 ${m.userId}`,
}))}
onClose={() => setProjectSettingsOpen(false)}
onActivated={(activeKey) => {
setProjectWorkflowsState((prev) => ({
...prev,
[currentProjectId]: {
...currentWfState,
activeKey,
lifecycleStatus: 'active',
},
}));
}}
/>
</>
);
}