From 56a87198633c16cf02059a727ad1dc0e5cf9ffe2 Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 19 Sep 2026 14:11:32 +0000 Subject: [PATCH] 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 --- web/src/App.tsx | 399 +++++++----------- .../components/layout/WorkbenchView.test.ts | 13 + web/src/components/layout/WorkbenchView.tsx | 294 +++++++++++++ 3 files changed, 456 insertions(+), 250 deletions(-) create mode 100644 web/src/components/layout/WorkbenchView.test.ts create mode 100644 web/src/components/layout/WorkbenchView.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index 35d564aa..40ff42b3 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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 ( - <> - { - 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={ - - } - docContent={ - - } - /> - - {/* 用户在当前激活项目中的法定职责与权限矩阵弹窗 */} - setRightsModalOpen(false)} - currentUser={currentUser} - currentProjectTitle={sessions.find((s) => s.sessionId === activeSessionId)?.title || '当前项目'} - deliverables={visibleDeliverables} - activeDocId={activeDocId} - canAssignLead={canAssignLead} - onSelectDoc={handleSelectDoc} - onRoleChangeForDoc={handleRoleChangeForDoc} - /> - - {/* 首次登录强制改密阻断弹窗 */} - { - const updated = { ...session, mustChangePassword: false }; - setSession(updated); - authClient.saveSession(updated); - }} - onLogout={handleLogout} - /> - - {/* 用户个人设置与偏好中心 (全员可用) */} - 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} - /> - - {/* 单文档可见性覆盖弹窗 */} - setDocVisibilityModalOpen(false)} - doc={targetDocForVisibility} - roles={roles} - currentUserName={currentUser.name} - onSaveRule={handleSaveDocVisibilityRule} - /> - - {/* 发起医疗器械新项目立项弹窗 */} - { - if (createProjectSubmitting) return; - setCreateProjectModalOpen(false); - setCreateProjectError(null); - }} - onSubmit={handleCreateProjectConfirm} - submitError={createProjectError} - submitting={createProjectSubmitting} - submitPhase={createProjectSubmitPhase} - currentUser={currentUser} - availableUsers={allUsers} - /> - - {/* 项目成员与职责分配管理弹窗 */} - setProjectMembersModalOpen(false)} - project={currentProject} - currentUser={currentUser} - availableUsers={allUsers} - canManage={canManageProjectMembers} - membersError={projectMembersError} - onAddMember={handleAddProjectMember} - onUpdateMemberRole={handleUpdateProjectMemberRole} - onRemoveMember={handleRemoveProjectMember} - onRestoreMember={handleRestoreProjectMember} - /> - - 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', - }, - })); - }} - /> - + + } + docContent={ + + } + 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} + /> ); + } diff --git a/web/src/components/layout/WorkbenchView.test.ts b/web/src/components/layout/WorkbenchView.test.ts new file mode 100644 index 00000000..745325e9 --- /dev/null +++ b/web/src/components/layout/WorkbenchView.test.ts @@ -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(/ void; + setActiveDocId: (id: string) => void; + setActiveCategory: (category: DeliverableCategory) => void; + setCreateProjectModalOpen: (open: boolean) => void; + canCreateProject: boolean; + currentUser: UserProfile; + currentDocRole: UserRole; + setRightsModalOpen: Dispatch>; + setUserSettingsOpen: Dispatch>; + setView: (view: 'workbench' | 'admin') => void; + setProjectMembersModalOpen: Dispatch>; + currentWfState: WorkbenchWorkflowState; + workflowGovernanceEnabled: boolean; + setProjectSettingsOpen: Dispatch>; + setProjectWorkflowsState: Dispatch>>; + 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>; + handleLogout: () => void; + userSettingsOpen: boolean; + roles: RoleDefinition[]; + setAllUsers: Dispatch>; + setCurrentUser: Dispatch>; + docVisibilityModalOpen: boolean; + setDocVisibilityModalOpen: Dispatch>; + targetDocForVisibility: DeliverableItem | null; + handleSaveDocVisibilityRule: (docId: string, rule: DocVisibilityRule | undefined) => void; + createProjectModalOpen: boolean; + createProjectSubmitting: boolean; + setCreateProjectError: Dispatch>; + handleCreateProjectConfirm: (data: CreateProjectConfirmData) => void | Promise; + createProjectError: string | null; + createProjectSubmitPhase: CreateProjectSubmitPhase | null; + allUsers: UserProfile[]; + projectMembersModalOpen: boolean; + currentProject: ProjectInfo | undefined; + canManageProjectMembers: boolean; + projectMembersError: string | null; + handleAddProjectMember: (member: Omit) => void | Promise; + handleUpdateProjectMemberRole: (userId: number, role: ProjectMemberRole) => void | Promise; + handleRemoveProjectMember: (userId: number) => void | Promise; + handleRestoreProjectMember: (userId: number) => void | Promise; + 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 ( + <> + { + 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} + /> + setRightsModalOpen(false)} + currentUser={currentUser} + currentProjectTitle={sessions.find((s) => s.sessionId === activeSessionId)?.title || '当前项目'} + deliverables={visibleDeliverables} + activeDocId={activeDocId} + canAssignLead={canAssignLead} + onSelectDoc={handleSelectDoc} + onRoleChangeForDoc={handleRoleChangeForDoc} + /> + { + if (!session) return; + const updated = { ...session, mustChangePassword: false }; + setSession(updated); + authClient.saveSession(updated); + }} + onLogout={handleLogout} + /> + 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} + /> + setDocVisibilityModalOpen(false)} + doc={targetDocForVisibility} + roles={roles} + currentUserName={currentUser.name} + onSaveRule={handleSaveDocVisibilityRule} + /> + { + if (createProjectSubmitting) return; + setCreateProjectModalOpen(false); + setCreateProjectError(null); + }} + onSubmit={handleCreateProjectConfirm} + submitError={createProjectError} + submitting={createProjectSubmitting} + submitPhase={createProjectSubmitPhase} + currentUser={currentUser} + availableUsers={allUsers} + /> + setProjectMembersModalOpen(false)} + project={currentProject} + currentUser={currentUser} + availableUsers={allUsers} + canManage={canManageProjectMembers} + membersError={projectMembersError} + onAddMember={handleAddProjectMember} + onUpdateMemberRole={handleUpdateProjectMemberRole} + onRemoveMember={handleRemoveProjectMember} + onRestoreMember={handleRestoreProjectMember} + /> + 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', + }, + })); + }} + /> + + ); +}