From 88d4756bbd2b43974a6837c33aa912a7110ceea8 Mon Sep 17 00:00:00 2001 From: axb Date: Wed, 19 Aug 2026 23:46:15 +0800 Subject: [PATCH 01/11] fix(wework): polish issue creation composer --- .../features/todo/CloudTodoWorkspace.test.tsx | 3 +- .../src/features/todo/IssueComposer.test.tsx | 3 + wework/src/features/todo/IssueComposer.tsx | 99 ++++++++++++++++--- wework/src/i18n/locales/en/common.json | 8 ++ wework/src/i18n/locales/zh-CN/common.json | 8 ++ 5 files changed, 105 insertions(+), 16 deletions(-) diff --git a/wework/src/features/todo/CloudTodoWorkspace.test.tsx b/wework/src/features/todo/CloudTodoWorkspace.test.tsx index b180470d67e..b7ff4f9e23f 100644 --- a/wework/src/features/todo/CloudTodoWorkspace.test.tsx +++ b/wework/src/features/todo/CloudTodoWorkspace.test.tsx @@ -2845,7 +2845,8 @@ describe('CloudTodoWorkspace', () => { expect(screen.getByTestId('workspace-issue-composer')).toBeVisible() expect(screen.getByTestId('workspace-issue-input')).toHaveValue('Issue with details') expect(screen.getByTestId('workspace-issue-composer')).toHaveClass('fixed') - expect(screen.queryByRole('heading', { name: '新建 Issue' })).not.toBeInTheDocument() + expect(screen.getByRole('heading', { name: '新建 Issue' })).toBeVisible() + expect(screen.getByTestId('workspace-issue-close')).toBeVisible() }) it('only offers direct creation in intake columns', async () => { diff --git a/wework/src/features/todo/IssueComposer.test.tsx b/wework/src/features/todo/IssueComposer.test.tsx index a4c20959d00..dc68c029ec9 100644 --- a/wework/src/features/todo/IssueComposer.test.tsx +++ b/wework/src/features/todo/IssueComposer.test.tsx @@ -96,6 +96,8 @@ describe('IssueComposer', () => { 'aria-selected', 'true' ) + expect(screen.getByTestId('workspace-issue-heading')).toHaveTextContent('要推进什么?') + expect(screen.getByText('描述目标、问题或交付,创建后会进入当前项目空间。')).toBeVisible() expect(screen.queryByText('描述要推进的事情,创建后自动进入所选看板。')).not.toBeInTheDocument() expect(screen.queryByTestId('project-work-button')).not.toBeInTheDocument() expect(screen.queryByTestId('model-selector-button')).not.toBeInTheDocument() @@ -133,6 +135,7 @@ describe('IssueComposer', () => { await userEvent.click(screen.getByTestId('workspace-create-task-tab')) expect(screen.getByTestId('workspace-create-task-tab')).toHaveAttribute('aria-selected', 'true') + expect(screen.getByTestId('workspace-issue-heading')).toHaveTextContent('要执行什么?') expect(screen.getByTestId('workspace-issue-input')).toHaveTextContent('修复创建流程') expect(screen.getByTestId('project-work-button')).toHaveTextContent('Wegent') await userEvent.click(screen.getByTestId('project-work-button')) diff --git a/wework/src/features/todo/IssueComposer.tsx b/wework/src/features/todo/IssueComposer.tsx index d2d482c57b2..a26017f09ee 100644 --- a/wework/src/features/todo/IssueComposer.tsx +++ b/wework/src/features/todo/IssueComposer.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useRef, useState } from 'react' -import { ArrowUp, Maximize2, Minimize2, Paperclip, X } from 'lucide-react' +import { ArrowUp, Bot, LayoutDashboard, Maximize2, Minimize2, Paperclip, X } from 'lucide-react' import type { CloudProject } from '@/api/deliveries' import { type ProjectChatControls, type ProjectWorkControls } from '@/components/chat/ChatInput' import { AttachmentBadges } from '@/components/chat/composer/AttachmentBadges' @@ -406,10 +406,10 @@ export function IssueComposer({ focusable[nextIndex]?.focus() }} className={cn( - 'w-full max-w-[760px]', + 'w-full max-w-[736px]', presentation === 'popup' && !fullScreen && - 'max-h-full overflow-y-auto rounded-2xl border border-border bg-background p-6 shadow-2xl', + 'max-h-full overflow-y-auto rounded-2xl border border-border bg-background p-6 shadow-xl', fullScreen && 'fixed bottom-4 left-4 right-4 top-[54px] z-modal flex w-auto max-h-none max-w-none flex-col overflow-hidden rounded-2xl border border-border bg-background p-0 shadow-xl' )} @@ -540,6 +540,54 @@ export function IssueComposer({ ) : ( <> + {presentation === 'page' ? ( +
+
+ ) : ( +
+
+ +
+

+ {t('todo.new_issue', '新建 Issue')} +

+

+ {selectedWorkItemProject?.name ?? + t('workbench.default_work_item_board', '我的任务')} +

+
+
+ +
+ )}
- {creationMode === 'issue' ? ( - - ) : null} +
+ + {creationMode === 'issue' + ? t('todo.issue_create_mode_hint', '仅创建,不会启动执行') + : t('todo.task_create_mode_hint', '创建后立即进入执行流程')} + + {creationMode === 'issue' ? ( + + ) : null} +
+
+
+ + + + {selectedWorkItemProject?.name ?? + t('workbench.default_work_item_board', '我的任务')} + + + + {creationMode === 'issue' + ? t('todo.issue_submit_shortcut', '⌘↵ 创建') + : t('todo.task_submit_shortcut', '⌘↵ 创建并执行')} +
{creationMode === 'task' && workbench?.selectProject && selectedLocalProject ? ( +
Date: Thu, 20 Aug 2026 00:00:58 +0800 Subject: [PATCH 02/11] fix(wework): enlarge mobile issue controls --- wework/src/features/todo/IssueComposer.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wework/src/features/todo/IssueComposer.tsx b/wework/src/features/todo/IssueComposer.tsx index a26017f09ee..93ade3e8b4e 100644 --- a/wework/src/features/todo/IssueComposer.tsx +++ b/wework/src/features/todo/IssueComposer.tsx @@ -428,7 +428,7 @@ export function IssueComposer({ type="button" data-testid="workspace-issue-collapse" onClick={() => setFullScreen(false)} - className="flex h-8 w-8 items-center justify-center rounded-lg text-text-secondary hover:bg-muted hover:text-text-primary" + className="flex h-11 w-11 items-center justify-center rounded-lg text-text-secondary hover:bg-muted hover:text-text-primary md:h-8 md:w-8" aria-label={t('todo.collapse_issue_editor', '收起编辑器')} > @@ -437,7 +437,7 @@ export function IssueComposer({ type="button" data-testid="workspace-issue-close" onClick={onCancel} - className="flex h-8 w-8 items-center justify-center rounded-lg text-text-secondary hover:bg-muted hover:text-text-primary" + className="flex h-11 w-11 items-center justify-center rounded-lg text-text-secondary hover:bg-muted hover:text-text-primary md:h-8 md:w-8" aria-label={t('common.close', '关闭')} > @@ -581,7 +581,7 @@ export function IssueComposer({ type="button" data-testid="workspace-issue-close" onClick={onCancel} - className="flex h-8 w-8 items-center justify-center rounded-lg text-text-secondary hover:bg-muted hover:text-text-primary" + className="flex h-11 w-11 items-center justify-center rounded-lg text-text-secondary hover:bg-muted hover:text-text-primary md:h-8 md:w-8" aria-label={t('common.close', '关闭')} > @@ -637,7 +637,7 @@ export function IssueComposer({ type="button" data-testid="workspace-issue-expand" onClick={() => setFullScreen(true)} - className="flex h-8 w-8 items-center justify-center rounded-lg text-text-secondary hover:bg-muted hover:text-text-primary" + className="flex h-11 w-11 items-center justify-center rounded-lg text-text-secondary hover:bg-muted hover:text-text-primary md:h-8 md:w-8" aria-label={t('todo.expand_issue_editor', '全屏编辑')} > From 310953ba84e91225177dcee6eac34a1eef11423a Mon Sep 17 00:00:00 2001 From: axb Date: Thu, 20 Aug 2026 00:29:25 +0800 Subject: [PATCH 03/11] fix(wework): simplify issue composer actions --- wework/src/features/todo/IssueComposer.tsx | 38 ++++++++-------------- wework/src/i18n/locales/en/common.json | 4 --- wework/src/i18n/locales/zh-CN/common.json | 4 --- 3 files changed, 13 insertions(+), 33 deletions(-) diff --git a/wework/src/features/todo/IssueComposer.tsx b/wework/src/features/todo/IssueComposer.tsx index 93ade3e8b4e..43d3b92710e 100644 --- a/wework/src/features/todo/IssueComposer.tsx +++ b/wework/src/features/todo/IssueComposer.tsx @@ -588,7 +588,7 @@ export function IssueComposer({
)} -
+
-
- - {creationMode === 'issue' - ? t('todo.issue_create_mode_hint', '仅创建,不会启动执行') - : t('todo.task_create_mode_hint', '创建后立即进入执行流程')} - - {creationMode === 'issue' ? ( - - ) : null} -
+ {creationMode === 'issue' ? ( + + ) : null}
-
+
@@ -653,11 +646,6 @@ export function IssueComposer({ t('workbench.default_work_item_board', '我的任务')} - - {creationMode === 'issue' - ? t('todo.issue_submit_shortcut', '⌘↵ 创建') - : t('todo.task_submit_shortcut', '⌘↵ 创建并执行')} -
{creationMode === 'task' && workbench?.selectProject && selectedLocalProject ? ( Date: Thu, 20 Aug 2026 00:39:22 +0800 Subject: [PATCH 04/11] fix(wework): place issue expand control in composer --- .../src/features/todo/IssueComposer.test.tsx | 6 + wework/src/features/todo/IssueComposer.tsx | 118 ++++++++++-------- 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/wework/src/features/todo/IssueComposer.test.tsx b/wework/src/features/todo/IssueComposer.test.tsx index dc68c029ec9..697df81c81d 100644 --- a/wework/src/features/todo/IssueComposer.test.tsx +++ b/wework/src/features/todo/IssueComposer.test.tsx @@ -99,6 +99,12 @@ describe('IssueComposer', () => { expect(screen.getByTestId('workspace-issue-heading')).toHaveTextContent('要推进什么?') expect(screen.getByText('描述目标、问题或交付,创建后会进入当前项目空间。')).toBeVisible() expect(screen.queryByText('描述要推进的事情,创建后自动进入所选看板。')).not.toBeInTheDocument() + const expandButton = screen.getByTestId('workspace-issue-expand') + expect(screen.getByTestId('workspace-issue-composer-input-shell')).toContainElement( + expandButton + ) + expect(expandButton).toHaveAccessibleName('全屏编辑') + expect(expandButton).toHaveClass('hover:bg-muted', 'hover:text-text-primary') expect(screen.queryByTestId('project-work-button')).not.toBeInTheDocument() expect(screen.queryByTestId('model-selector-button')).not.toBeInTheDocument() await userEvent.type(screen.getByTestId('workspace-issue-input'), '修复工作空间创建入口') diff --git a/wework/src/features/todo/IssueComposer.tsx b/wework/src/features/todo/IssueComposer.tsx index 43d3b92710e..2b4e776b072 100644 --- a/wework/src/features/todo/IssueComposer.tsx +++ b/wework/src/features/todo/IssueComposer.tsx @@ -5,6 +5,7 @@ import { type ProjectChatControls, type ProjectWorkControls } from '@/components import { AttachmentBadges } from '@/components/chat/composer/AttachmentBadges' import { BufferedChatInput } from '@/components/layout/BufferedChatInput' import { WorkbenchHarnessSelector } from '@/components/layout/WorkbenchHarnessSelector' +import { Tooltip } from '@/components/ui/tooltip' import { useTranslation } from '@/hooks/useTranslation' import { releaseAttachmentPreview } from '@/lib/attachments' import { cn } from '@/lib/utils' @@ -318,48 +319,68 @@ export function IssueComposer({ } : undefined const renderComposer = (resolvedProjectWork: ProjectWorkControls | undefined) => ( - 0} - showExecutionTools={creationMode === 'task'} - showWorkspaceMenu={false} - projectWorkBarMiddleContext={ - creationMode === 'task' && selectedWorkItemProject ? ( - - setBoardKey(`${project.project_store}:${String(project.id)}`) - } - /> - ) : undefined - } - projectWorkBarTrailingContext={ - creationMode === 'task' ? ( - undefined} - /> - ) : undefined - } - /> +
+ 0} + showExecutionTools={creationMode === 'task'} + showWorkspaceMenu={false} + projectWorkBarMiddleContext={ + creationMode === 'task' && selectedWorkItemProject ? ( + + setBoardKey(`${project.project_store}:${String(project.id)}`) + } + /> + ) : undefined + } + projectWorkBarTrailingContext={ + creationMode === 'task' ? ( + undefined} + /> + ) : undefined + } + /> + {creationMode === 'issue' ? ( + + + + ) : null} +
) return ( @@ -588,7 +609,7 @@ export function IssueComposer({
)} -
+
- {creationMode === 'issue' ? ( - - ) : null}
From 46375ccc42d2d1ce8acc8aed33e6918c616254c3 Mon Sep 17 00:00:00 2001 From: axb Date: Thu, 20 Aug 2026 01:19:33 +0800 Subject: [PATCH 05/11] fix(wework): redesign fullscreen issue creation --- .../src/features/todo/CloudTodoWorkspace.tsx | 32 +- .../src/features/todo/IssueComposer.test.tsx | 92 +++- wework/src/features/todo/IssueComposer.tsx | 494 +++++++++++++++--- wework/src/i18n/locales/en/common.json | 8 + wework/src/i18n/locales/zh-CN/common.json | 8 + 5 files changed, 548 insertions(+), 86 deletions(-) diff --git a/wework/src/features/todo/CloudTodoWorkspace.tsx b/wework/src/features/todo/CloudTodoWorkspace.tsx index 682874c8fc5..7ce7113c980 100644 --- a/wework/src/features/todo/CloudTodoWorkspace.tsx +++ b/wework/src/features/todo/CloudTodoWorkspace.tsx @@ -2296,6 +2296,11 @@ export function CloudTodoWorkspace({ files: File[] createTask: boolean localProjectId: number | null + continueCreating?: boolean + status?: CloudLoopItem['status'] + priority?: CloudLoopItem['priority'] + tags?: string[] + assigneeUserId?: number | null }) { const targetProject = projects.find( project => projectSpaceKey(projectSpaceRef(project)) === input.boardKey @@ -2310,7 +2315,9 @@ export function CloudTodoWorkspace({ let created = await targetApi.createLoopItem(targetProject.id, { title: input.title, description: input.description, - status: input.createTask ? 'pending' : 'inbox', + status: input.status ?? (input.createTask ? 'pending' : 'inbox'), + ...(input.priority ? { priority: input.priority } : {}), + ...(input.tags ? { tags: input.tags } : {}), parent_id: null, ...(input.createTask && isDefaultWorkItemProject(targetProject) && issueLocalProject ? { @@ -2334,6 +2341,20 @@ export function CloudTodoWorkspace({ }) } } + if (input.assigneeUserId) { + if (typeof targetApi.assignLoopItem === 'function') { + created = await targetApi.assignLoopItem(targetProject.id, created.id, { + version: created.version, + assigneeType: 'user', + assigneeId: String(input.assigneeUserId), + }) + } else { + created = await targetApi.updateLoopItem(created.id, { + version: created.version, + assignee_user_id: input.assigneeUserId, + }) + } + } const locatedItem: LocatedLoopItem = { ...created, project_store: targetProject.project_store, @@ -2357,8 +2378,12 @@ export function CloudTodoWorkspace({ setRootView('projects') setProjectView('board') setBoardParentId(null) - setIssueComposerOpen(false) - setSelectedItem(locatedItem) + if (input.continueCreating) { + setSelectedItem(null) + } else { + setIssueComposerOpen(false) + setSelectedItem(locatedItem) + } if (input.createTask) { setTaskComposerRequest({ workItemId: locatedItem.id, @@ -2661,6 +2686,7 @@ export function CloudTodoWorkspace({ } initialContent={issueComposerInitialContent} localProjects={localProjectOptions} + projectMembers={projectMembers} initialLocalProjectId={selectedLocalProject?.id ?? null} presentation={issueComposerPresentation} busy={issueComposerBusy} diff --git a/wework/src/features/todo/IssueComposer.test.tsx b/wework/src/features/todo/IssueComposer.test.tsx index 697df81c81d..65caa353fab 100644 --- a/wework/src/features/todo/IssueComposer.test.tsx +++ b/wework/src/features/todo/IssueComposer.test.tsx @@ -250,7 +250,7 @@ describe('IssueComposer', () => { }) }) - it('expands into an app-fullscreen editor that derives the title from content', async () => { + it('expands into an app-fullscreen form with separate issue fields', async () => { const onCreate = vi.fn() const file = new File(['image'], 'context.png', { type: 'image/png' }) render( @@ -279,11 +279,15 @@ describe('IssueComposer', () => { 'inset-0', 'top-4' ) - expect(screen.queryByTestId('workspace-issue-title')).not.toBeInTheDocument() + expect(screen.getByTestId('workspace-issue-title')).toHaveValue('自动生成的标题') + expect(screen.getByTestId('workspace-issue-project')).toHaveValue('backend:1') expect(screen.getByTestId('workspace-issue-description')).toHaveValue('自动生成的标题') + fireEvent.change(screen.getByTestId('workspace-issue-title'), { + target: { value: '默认生成的标题' }, + }) fireEvent.change(screen.getByTestId('workspace-issue-description'), { - target: { value: '默认生成的标题\n完整 Issue 内容' }, + target: { value: '完整 Issue 内容' }, }) fireEvent.change(screen.getByTestId('workspace-issue-file-input'), { target: { files: [file] }, @@ -291,10 +295,8 @@ describe('IssueComposer', () => { await userEvent.click(screen.getByTestId('workspace-issue-collapse')) await userEvent.click(screen.getByTestId('workspace-issue-expand')) - expect(screen.queryByTestId('workspace-issue-title')).not.toBeInTheDocument() - expect(screen.getByTestId('workspace-issue-description')).toHaveValue( - '默认生成的标题\n完整 Issue 内容' - ) + expect(screen.getByTestId('workspace-issue-title')).toHaveValue('默认生成的标题') + expect(screen.getByTestId('workspace-issue-description')).toHaveValue('完整 Issue 内容') expect(screen.getByRole('button', { name: 'context.png' })).toBeInTheDocument() expect( screen.getByTestId('workspace-issue-composer-panel').querySelector('header') @@ -323,8 +325,8 @@ describe('IssueComposer', () => { expect(onCreate).toHaveBeenCalledWith({ boardKey: 'backend:1', - title: '默认生成的标题 完整 Issue 内容', - description: '默认生成的标题\n完整 Issue 内容', + title: '默认生成的标题', + description: '完整 Issue 内容', files: [file], createTask: false, localProjectId: null, @@ -344,8 +346,11 @@ describe('IssueComposer', () => { ) await userEvent.click(screen.getByTestId('workspace-issue-expand')) + fireEvent.change(screen.getByTestId('workspace-issue-title'), { + target: { value: '未完成的 Issue' }, + }) fireEvent.change(screen.getByTestId('workspace-issue-description'), { - target: { value: '未完成的 Issue\n关闭后需要恢复的内容' }, + target: { value: '关闭后需要恢复的内容' }, }) fireEvent.change(screen.getByTestId('workspace-issue-file-input'), { target: { files: [file] }, @@ -371,13 +376,62 @@ describe('IssueComposer', () => { ) await userEvent.click(screen.getByTestId('workspace-issue-expand')) - expect(screen.queryByTestId('workspace-issue-title')).not.toBeInTheDocument() - expect(screen.getByTestId('workspace-issue-description')).toHaveValue( - '未完成的 Issue\n关闭后需要恢复的内容' - ) + expect(screen.getByTestId('workspace-issue-title')).toHaveValue('未完成的 Issue') + expect(screen.getByTestId('workspace-issue-description')).toHaveValue('关闭后需要恢复的内容') expect(screen.getByRole('button', { name: 'draft.png' })).toBeInTheDocument() }) + it('keeps the fullscreen form open and resets it for continuous issue creation', async () => { + const onCreate = vi.fn(async () => true) + render( + + ) + + await userEvent.click(screen.getByTestId('workspace-issue-expand')) + await userEvent.type(screen.getByTestId('workspace-issue-title'), '第一个 Issue') + await userEvent.type(screen.getByTestId('workspace-issue-description'), '第一条描述') + await userEvent.selectOptions(screen.getByTestId('workspace-issue-status'), 'in_progress') + await userEvent.selectOptions(screen.getByTestId('workspace-issue-priority'), 'high') + await userEvent.selectOptions(screen.getByTestId('workspace-issue-assignee'), '7') + await userEvent.type(screen.getByTestId('workspace-issue-tag-input'), '体验{Enter}') + await userEvent.click(screen.getByTestId('workspace-issue-continue-creating')) + await userEvent.click(screen.getByTestId('workspace-issue-fullscreen-submit')) + + expect(onCreate).toHaveBeenCalledWith({ + boardKey: 'local:default-work-items', + title: '第一个 Issue', + description: '第一条描述', + files: [], + createTask: false, + localProjectId: null, + continueCreating: true, + status: 'in_progress', + priority: 'high', + tags: ['体验'], + assigneeUserId: 7, + }) + expect(screen.getByTestId('workspace-issue-title')).toHaveValue('') + expect(screen.getByTestId('workspace-issue-description')).toHaveValue('') + expect(screen.getByTestId('workspace-issue-composer-panel')).toBeInTheDocument() + await waitFor(() => expect(screen.getByTestId('workspace-issue-title')).toHaveFocus()) + }) + it('clears the persisted draft after explicit discard or successful creation', async () => { const onCreate = vi.fn(async () => true) const firstRender = render( @@ -390,17 +444,23 @@ describe('IssueComposer', () => { ) await userEvent.click(screen.getByTestId('workspace-issue-expand')) - fireEvent.change(screen.getByTestId('workspace-issue-description'), { + fireEvent.change(screen.getByTestId('workspace-issue-title'), { target: { value: '可清理草稿' }, }) + fireEvent.change(screen.getByTestId('workspace-issue-description'), { + target: { value: '清理前的描述' }, + }) await waitFor(() => expect(localStorage.getItem('wework-issue-composer-draft:backend:1:issue')).not.toBeNull() ) await userEvent.click(screen.getByTestId('workspace-issue-clear-draft')) expect(localStorage.getItem('wework-issue-composer-draft:backend:1:issue')).toBeNull() + fireEvent.change(screen.getByTestId('workspace-issue-title'), { + target: { value: '创建后清理' }, + }) fireEvent.change(screen.getByTestId('workspace-issue-description'), { - target: { value: '创建后清理\n提交内容' }, + target: { value: '提交内容' }, }) await userEvent.click(screen.getByTestId('workspace-issue-fullscreen-submit')) diff --git a/wework/src/features/todo/IssueComposer.tsx b/wework/src/features/todo/IssueComposer.tsx index 2b4e776b072..af80f28b181 100644 --- a/wework/src/features/todo/IssueComposer.tsx +++ b/wework/src/features/todo/IssueComposer.tsx @@ -1,6 +1,19 @@ import { useContext, useEffect, useRef, useState } from 'react' -import { ArrowUp, Bot, LayoutDashboard, Maximize2, Minimize2, Paperclip, X } from 'lucide-react' -import type { CloudProject } from '@/api/deliveries' +import { + Bot, + ChevronDown, + Check, + CircleUserRound, + Flag, + Folder, + LayoutDashboard, + Maximize2, + Minimize2, + Paperclip, + Tag, + X, +} from 'lucide-react' +import type { CloudLoopItem, CloudProject, CloudProjectMember } from '@/api/deliveries' import { type ProjectChatControls, type ProjectWorkControls } from '@/components/chat/ChatInput' import { AttachmentBadges } from '@/components/chat/composer/AttachmentBadges' import { BufferedChatInput } from '@/components/layout/BufferedChatInput' @@ -21,6 +34,7 @@ interface IssueComposerProps { initialStartExecution?: boolean initialContent?: string localProjects?: ProjectWithTasks[] + projectMembers?: Record initialLocalProjectId?: number | null presentation?: 'page' | 'popup' busy?: boolean @@ -33,6 +47,11 @@ interface IssueComposerProps { files: File[] createTask: boolean localProjectId: number | null + continueCreating?: boolean + status?: CloudLoopItem['status'] + priority?: CloudLoopItem['priority'] + tags?: string[] + assigneeUserId?: number | null }) => Promise | boolean | void } @@ -43,9 +62,14 @@ interface StagedIssueAttachment { interface IssueComposerDraft { boardKey: string + title: string content: string creationMode: 'issue' | 'task' localProjectId: number | null + status: CloudLoopItem['status'] + priority: CloudLoopItem['priority'] + tags: string[] + assigneeUserId: number | null } const issueDraftAttachmentStore = new Map() @@ -74,9 +98,23 @@ function readIssueComposerDraft(key: string): IssueComposerDraft | null { } return { boardKey: parsed.boardKey, + title: typeof parsed.title === 'string' ? parsed.title : '', content: parsed.content, creationMode: parsed.creationMode, localProjectId: typeof parsed.localProjectId === 'number' ? parsed.localProjectId : null, + status: + typeof parsed.status === 'string' ? (parsed.status as CloudLoopItem['status']) : 'inbox', + priority: + parsed.priority === 'low' || + parsed.priority === 'medium' || + parsed.priority === 'high' || + parsed.priority === 'urgent' + ? parsed.priority + : 'none', + tags: Array.isArray(parsed.tags) + ? parsed.tags.filter((tag): tag is string => typeof tag === 'string') + : [], + assigneeUserId: typeof parsed.assigneeUserId === 'number' ? parsed.assigneeUserId : null, } } catch { return null @@ -107,6 +145,7 @@ export function IssueComposer({ initialStartExecution = false, initialContent = '', localProjects = [], + projectMembers = {}, initialLocalProjectId = null, presentation = 'page', busy = false, @@ -123,8 +162,16 @@ export function IssueComposer({ draft ? (issueDraftAttachmentStore.get(draftKey) ?? []) : [] ) const [boardKey, setBoardKey] = useState(draft?.boardKey ?? initialBoardKey) + const [title, setTitle] = useState(draft?.title ?? '') const [content, setContent] = useState(initialContent || draft?.content || '') + const contentRef = useRef(initialContent || draft?.content || '') const [fullScreen, setFullScreen] = useState(false) + const [continueCreating, setContinueCreating] = useState(false) + const [status, setStatus] = useState(draft?.status ?? 'inbox') + const [priority, setPriority] = useState(draft?.priority ?? 'none') + const [tags, setTags] = useState(draft?.tags ?? []) + const [tagDraft, setTagDraft] = useState('') + const [assigneeUserId, setAssigneeUserId] = useState(draft?.assigneeUserId ?? null) const [creationMode, setCreationMode] = useState<'issue' | 'task'>( draft?.creationMode ?? initialMode ) @@ -145,6 +192,14 @@ export function IssueComposer({ projects.find(project => `${project.project_store}:${String(project.id)}` === boardKey) ?? projects[0] ?? null + const selectedProjectMembers = projectMembers[boardKey] ?? [] + const selectedAssigneeName = + selectedProjectMembers.find(member => member.user_id === assigneeUserId)?.user_name ?? null + const statusOptions = selectedWorkItemProject?.board_config?.statuses ?? [ + { id: 'inbox', name: t('todo.status_inbox', '收集箱') }, + { id: 'pending', name: t('todo.status_pending', '待处理') }, + { id: 'in_progress', name: t('todo.status_in_progress', '进行中') }, + ] const projectRuntimeWork = workbench?.state?.runtimeWork ?? { projects: localProjects.map(project => ({ project: { @@ -171,6 +226,7 @@ export function IssueComposer({ const nextAttachmentId = useRef(-(restoredFiles.length + 1)) const panelRef = useRef(null) const fileInputRef = useRef(null) + const titleInputRef = useRef(null) useEffect(() => { stagedAttachmentsRef.current = stagedAttachments @@ -197,7 +253,11 @@ export function IssueComposer({ [] ) - const hasDraft = Boolean(content.trim()) || stagedAttachments.length > 0 + const hasDraft = + Boolean(title.trim() || content.trim() || tags.length || assigneeUserId) || + status !== 'inbox' || + priority !== 'none' || + stagedAttachments.length > 0 useEffect(() => { if (!hasDraft) { @@ -207,9 +267,14 @@ export function IssueComposer({ } const snapshot: IssueComposerDraft = { boardKey, + title, content, creationMode, localProjectId, + status, + priority, + tags, + assigneeUserId, } localStorage.setItem(draftKey, JSON.stringify(snapshot)) if (stagedAttachments.length > 0) { @@ -220,7 +285,20 @@ export function IssueComposer({ } else { issueDraftAttachmentStore.delete(draftKey) } - }, [boardKey, content, creationMode, draftKey, hasDraft, localProjectId, stagedAttachments]) + }, [ + boardKey, + content, + creationMode, + draftKey, + hasDraft, + localProjectId, + priority, + stagedAttachments, + status, + tags, + title, + assigneeUserId, + ]) useEffect(() => { if (presentation !== 'popup') return @@ -261,7 +339,14 @@ export function IssueComposer({ stagedAttachments.forEach(item => releaseAttachmentPreview(item.attachment)) localStorage.removeItem(draftKey) issueDraftAttachmentStore.delete(draftKey) + setTitle('') + contentRef.current = '' setContent('') + setStatus('inbox') + setPriority('none') + setTags([]) + setTagDraft('') + setAssigneeUserId(null) setStagedAttachments([]) } const projectChat: ProjectChatControls = { @@ -284,9 +369,22 @@ export function IssueComposer({ handleFileSelect: async files => stageFiles(files), removeAttachment: async attachmentId => removeAttachment(attachmentId), } - const submit = async (submittedContent?: string) => { - const description = (submittedContent ?? content).trim() - const submittedDraft = issueDraftFromText(description) + const createIssue = async ( + submittedContent: string, + submittedTitle?: string, + keepOpen = false + ) => { + const description = submittedContent.trim() + const pendingTag = tagDraft.trim().replace(/^#/, '') + const submittedTags = pendingTag && !tags.includes(pendingTag) ? [...tags, pendingTag] : tags + const derivedDraft = issueDraftFromText(description) + const submittedDraft = { + title: + creationMode === 'issue' + ? (submittedTitle ?? title).trim() || derivedDraft.title + : derivedDraft.title, + description, + } if (!boardKey || !submittedDraft.title || busy) return false const created = await onCreate({ boardKey, @@ -294,13 +392,57 @@ export function IssueComposer({ files: stagedAttachments.map(item => item.file), createTask: creationMode === 'task', localProjectId: creationMode === 'task' ? localProjectId : null, + ...(keepOpen ? { continueCreating: true } : {}), + ...(status !== 'inbox' ? { status } : {}), + ...(priority !== 'none' ? { priority } : {}), + ...(submittedTags.length ? { tags: submittedTags } : {}), + ...(assigneeUserId ? { assigneeUserId } : {}), }) if (created !== false) { localStorage.removeItem(draftKey) issueDraftAttachmentStore.delete(draftKey) + if (keepOpen) { + stagedAttachments.forEach(item => releaseAttachmentPreview(item.attachment)) + setTitle('') + contentRef.current = '' + setContent('') + setTags(submittedTags) + setTagDraft('') + setStagedAttachments([]) + window.requestAnimationFrame(() => titleInputRef.current?.focus()) + } } return created } + const submit = (submittedContent?: string) => + createIssue(submittedContent ?? contentRef.current, title) + const updateCompactContent = (value: string) => { + contentRef.current = value + setContent(value) + } + const updateDescription = (value: string) => { + contentRef.current = value + setContent(value) + } + const commitTagDraft = () => { + const nextTag = tagDraft.trim().replace(/^#/, '') + if (nextTag && !tags.includes(nextTag)) setTags(current => [...current, nextTag]) + setTagDraft('') + } + const openFullScreen = () => { + const compactInput = panelRef.current?.querySelector( + '[data-testid="workspace-issue-input"]' + ) + const renderedContent = + compactInput instanceof HTMLTextAreaElement + ? compactInput.value + : (compactInput?.innerText ?? compactInput?.textContent ?? '') + const latestContent = renderedContent || contentRef.current + contentRef.current = latestContent + setContent(latestContent) + setTitle(currentTitle => currentTitle.trim() || issueDraftFromText(latestContent).title) + setFullScreen(true) + } const fallbackProjectWork: ProjectWorkControls | undefined = localProjects.length > 0 ? { @@ -322,7 +464,7 @@ export function IssueComposer({
setFullScreen(true)} + onClick={openFullScreen} className="flex h-11 w-11 items-center justify-center rounded-lg text-text-muted transition-colors hover:bg-muted hover:text-text-primary focus-visible:bg-muted focus-visible:text-text-primary focus-visible:outline-none md:h-8 md:w-8" aria-label={t('todo.expand_issue_editor', '全屏编辑')} > @@ -408,6 +550,18 @@ export function IssueComposer({ presentation === 'popup' || fullScreen ? t('todo.new_issue', '新建 Issue') : undefined } onKeyDown={event => { + if ( + fullScreen && + event.key === 'Enter' && + (event.metaKey || event.ctrlKey) && + !busy && + boardKey && + title.trim() + ) { + event.preventDefault() + void createIssue(content, title, continueCreating) + return + } if ((presentation !== 'popup' && !fullScreen) || event.key !== 'Tab') return const focusable = Array.from( panelRef.current?.querySelectorAll( @@ -437,9 +591,14 @@ export function IssueComposer({ > {fullScreen ? ( <> -
- - {t('todo.new_issue', '新建 Issue')} +
+ + + + {selectedWorkItemProject?.name ?? + t('workbench.default_work_item_board', '我的任务')}{' '} + · {t('todo.new_issue', '新建 Issue')} +
-