feat: add project action modals and APIs
This commit is contained in:
@@ -4,8 +4,16 @@ import '@arco-design/web-react/dist/css/arco.css'
|
||||
import '../App.css'
|
||||
import { login, setApiBaseUrl, type ApiSession } from '../api/client'
|
||||
import { mapWorkspace } from '../api/mappers'
|
||||
import { fetchProjectWorkspace, fetchProjects } from '../api/projects'
|
||||
import {
|
||||
createCronPlan,
|
||||
createProject,
|
||||
createTask,
|
||||
fetchProjectWorkspace,
|
||||
fetchProjects,
|
||||
uploadSource,
|
||||
} from '../api/projects'
|
||||
import { LoginPage } from '../pages/login'
|
||||
import { ProjectActionModals, type CronDraft, type ProjectActionModal, type ProjectDraft, type SourceDraft, type TaskDraft } from '../pages/projects/project-action-modals'
|
||||
import { ProjectPage } from '../pages/workspace-home'
|
||||
import type { ChannelKey, Project, ProjectWorkspace, Screen, Theme, WorkbenchView } from '../pages/projects/project-types'
|
||||
|
||||
@@ -20,21 +28,30 @@ function App() {
|
||||
const [activeTaskID, setActiveTaskID] = useState<string | null>(null)
|
||||
const [, setSelectedItem] = useState('Inbox 待处理(36)')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [actionLoading, setActionLoading] = useState(false)
|
||||
const [activeModal, setActiveModal] = useState<ProjectActionModal>(null)
|
||||
|
||||
const dark = theme === 'dark'
|
||||
const activeWorkspace = workspaces.find((workspace) => workspace.project.id === activeProjectID) ?? workspaces[0]
|
||||
|
||||
async function loadWorkspaces(nextSession: ApiSession, preferredProjectID = activeProjectID) {
|
||||
const backendProjects = await fetchProjects(nextSession)
|
||||
const backendWorkspaces = await Promise.all(
|
||||
backendProjects.map((project, index) => fetchProjectWorkspace(nextSession, project.ID ?? project.id ?? '').then((workspace) => mapWorkspace(workspace, index))),
|
||||
)
|
||||
setWorkspaces(backendWorkspaces)
|
||||
const nextProjectID = backendWorkspaces.find((workspace) => workspace.project.id === preferredProjectID)?.project.id ?? backendWorkspaces[0]?.project.id ?? ''
|
||||
setActiveProjectID(nextProjectID)
|
||||
return backendWorkspaces
|
||||
}
|
||||
|
||||
async function handleLogin(input: { server: string; email: string; password: string }) {
|
||||
setLoading(true)
|
||||
try {
|
||||
setApiBaseUrl(input.server)
|
||||
const nextSession = await login(input.email, input.password)
|
||||
const backendProjects = await fetchProjects(nextSession)
|
||||
const backendWorkspaces = await Promise.all(
|
||||
backendProjects.map((project, index) => fetchProjectWorkspace(nextSession, project.ID ?? project.id ?? '').then((workspace) => mapWorkspace(workspace, index))),
|
||||
)
|
||||
const backendWorkspaces = await loadWorkspaces(nextSession, '')
|
||||
setSession(nextSession)
|
||||
setWorkspaces(backendWorkspaces)
|
||||
setActiveProjectID(backendWorkspaces[0]?.project.id ?? '')
|
||||
setActiveChannel('overview')
|
||||
setActiveView('workspace')
|
||||
@@ -46,6 +63,110 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAfterAction(nextProjectID = activeProjectID) {
|
||||
if (!session) return
|
||||
await loadWorkspaces(session, nextProjectID)
|
||||
}
|
||||
|
||||
async function runAction(action: () => Promise<void>, success: string) {
|
||||
setActionLoading(true)
|
||||
try {
|
||||
await action()
|
||||
setActiveModal(null)
|
||||
Message.success(success)
|
||||
} catch (error) {
|
||||
Message.error(error instanceof Error ? error.message : '操作失败')
|
||||
} finally {
|
||||
setActionLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function requireSession() {
|
||||
if (!session) throw new Error('未登录')
|
||||
return session
|
||||
}
|
||||
|
||||
function requireActiveProject() {
|
||||
if (!activeWorkspace?.project.id) throw new Error('未选择项目')
|
||||
return activeWorkspace.project.id
|
||||
}
|
||||
|
||||
function normalizeOptionalTime(value: string) {
|
||||
const trimmed = value.trim()
|
||||
return trimmed === '' ? undefined : trimmed
|
||||
}
|
||||
|
||||
function handleCreateProject(draft: ProjectDraft) {
|
||||
if (!draft.name.trim()) {
|
||||
Message.warning('请输入项目名称')
|
||||
return
|
||||
}
|
||||
void runAction(async () => {
|
||||
const created = await createProject(requireSession(), { name: draft.name.trim(), description: draft.description.trim() })
|
||||
const nextProjectID = String(created.ID ?? created.id ?? '')
|
||||
await refreshAfterAction(nextProjectID)
|
||||
if (nextProjectID) {
|
||||
setActiveProjectID(nextProjectID)
|
||||
setActiveView('project')
|
||||
setActiveChannel('overview')
|
||||
}
|
||||
}, '项目已创建')
|
||||
}
|
||||
|
||||
function handleCreateTask(draft: TaskDraft) {
|
||||
if (!draft.title.trim()) {
|
||||
Message.warning('请输入任务标题')
|
||||
return
|
||||
}
|
||||
void runAction(async () => {
|
||||
await createTask(requireSession(), requireActiveProject(), {
|
||||
title: draft.title.trim(),
|
||||
description: draft.description.trim(),
|
||||
status: 'open',
|
||||
dueAt: normalizeOptionalTime(draft.dueAt),
|
||||
})
|
||||
await refreshAfterAction()
|
||||
setActiveChannel('tasks')
|
||||
}, '任务已创建')
|
||||
}
|
||||
|
||||
function handleUploadSource(draft: SourceDraft) {
|
||||
if (!draft.file) {
|
||||
Message.warning('请选择文件')
|
||||
return
|
||||
}
|
||||
const file = draft.file
|
||||
void runAction(async () => {
|
||||
await uploadSource(requireSession(), requireActiveProject(), {
|
||||
title: draft.title.trim(),
|
||||
file,
|
||||
})
|
||||
await refreshAfterAction()
|
||||
setActiveChannel('notes')
|
||||
}, '文件已上传')
|
||||
}
|
||||
|
||||
function handleCreateCronPlan(draft: CronDraft) {
|
||||
if (!draft.title.trim()) {
|
||||
Message.warning('请输入计划名称')
|
||||
return
|
||||
}
|
||||
if (!draft.schedule.trim()) {
|
||||
Message.warning('请输入 Cron 表达式')
|
||||
return
|
||||
}
|
||||
void runAction(async () => {
|
||||
await createCronPlan(requireSession(), requireActiveProject(), {
|
||||
title: draft.title.trim(),
|
||||
schedule: draft.schedule.trim(),
|
||||
enabled: draft.enabled,
|
||||
nextRunAt: normalizeOptionalTime(draft.nextRunAt),
|
||||
})
|
||||
await refreshAfterAction()
|
||||
setActiveChannel('cron')
|
||||
}, '计划任务已创建')
|
||||
}
|
||||
|
||||
function openTask(project: Project, taskID: string) {
|
||||
setActiveProjectID(project.id)
|
||||
setActiveView('project')
|
||||
@@ -84,10 +205,23 @@ function App() {
|
||||
onOpenTask={openTask}
|
||||
onCloseTask={() => setActiveTaskID(null)}
|
||||
onToggleTheme={() => setTheme(dark ? 'light' : 'dark')}
|
||||
onCreateProject={() => setActiveModal('project')}
|
||||
onCreateTask={() => setActiveModal('task')}
|
||||
onUploadSource={() => setActiveModal('source')}
|
||||
onCreateCronPlan={() => setActiveModal('cron')}
|
||||
/>
|
||||
) : (
|
||||
<Spin loading />
|
||||
)}
|
||||
<ProjectActionModals
|
||||
activeModal={activeModal}
|
||||
loading={actionLoading}
|
||||
onClose={() => setActiveModal(null)}
|
||||
onCreateProject={handleCreateProject}
|
||||
onCreateTask={handleCreateTask}
|
||||
onUploadSource={handleUploadSource}
|
||||
onCreateCronPlan={handleCreateCronPlan}
|
||||
/>
|
||||
</main>
|
||||
</ConfigProvider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user