diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index eed6311661..229f79f4f1 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1539,7 +1539,10 @@ "watchers": "Watchers", "digestHint": "Updates appear in your digest.", "memberUnavailable": "Member unavailable", - "saveError": "Could not save this change. Try again." + "saveError": "Could not save this change. Try again.", + "statusLabel": "Status", + "showEarlierUpdates": "Show {{count}} earlier updates", + "hideEarlierUpdates": "Hide earlier updates" }, "form": { "title": "Title", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 3d8253f3ef..169f7d83e7 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1531,7 +1531,10 @@ "watchers": "关注者", "digestHint": "更新会显示在你的摘要中。", "memberUnavailable": "成员不可用", - "saveError": "未能保存更改,请重试。" + "saveError": "未能保存更改,请重试。", + "statusLabel": "状态", + "showEarlierUpdates": "显示{{count}}条更早的更新", + "hideEarlierUpdates": "隐藏更早的更新" }, "form": { "title": "标题", diff --git a/frontend/src/v2/__tests__/V2PodBoard.test.tsx b/frontend/src/v2/__tests__/V2PodBoard.test.tsx index 9cff9ea468..48d51b3442 100644 --- a/frontend/src/v2/__tests__/V2PodBoard.test.tsx +++ b/frontend/src/v2/__tests__/V2PodBoard.test.tsx @@ -660,3 +660,24 @@ test('a delayed Follow response cannot replace another follower from the current expect(await screen.findByRole('button', { name: /Following/ })).toHaveAttribute('aria-pressed', 'true'); expect(document.querySelectorAll('.v2-board__watcher')).toHaveLength(2); }); + +test('detail status is a segment that moves the task, and older updates fold behind a toggle', async () => { + const updates = ['one', 'two', 'three', 'four', 'five'].map((text) => ({ text, author: 'Kai', authorId: 'kai-id' })); + const task = { taskId: 'TASK-5', title: 'Segmented', status: 'pending', watchers: [], updates }; + wireAxios([task]); + axios.patch.mockResolvedValue({ data: { task: { ...task, status: 'claimed' } } }); + renderBoard(); + fireEvent.click(await screen.findByTitle('TASK-5')); + const dialog = screen.getByRole('dialog', { name: /Segmented/ }); + const segment = within(dialog).getByRole('radiogroup', { name: 'Status' }); + expect(within(segment).getByRole('radio', { name: 'Pending' })).toHaveAttribute('aria-checked', 'true'); + fireEvent.click(within(segment).getByRole('radio', { name: /in progress/i })); + await waitFor(() => expect(axios.patch).toHaveBeenCalledWith('/api/v1/tasks/pod-1/TASK-5', { status: 'claimed' }, expect.any(Object))); + // Newest three show; the two oldest fold behind the toggle. + expect(within(dialog).queryByText('one')).toBeNull(); + expect(within(dialog).queryByText('two')).toBeNull(); + expect(within(dialog).getByText('five')).toBeInTheDocument(); + fireEvent.click(within(dialog).getByRole('button', { name: 'Show 2 earlier updates' })); + expect(within(dialog).getByText('one')).toBeInTheDocument(); + expect(within(dialog).getByRole('button', { name: 'Hide earlier updates' })).toHaveAttribute('aria-expanded', 'true'); +}); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 0326b1dc6e..86a5e6722b 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5092,7 +5092,14 @@ test('TASK-443 phone ownership sheet keeps its inset and scroll containment', () expect(modal).toContain('overflow: auto'); expect(modal).toContain('display: flex'); expect(modal).toContain('flex-direction: column'); - expect(read('../components/V2PodBoard.tsx')).toContain('className="v2-modal v2-board__detail"'); + expect(read('../components/V2PodBoard.tsx')).toContain('className="v2-modal v2-board__detail v2-board__drawer"'); +}); + +test('TASK-699 task detail is a 400px right drawer on desktop and a sheet on phones', () => { + const css = read('../v2.css'); + expect(ruleBody(css, '.v2-board .v2-board__drawer')).toContain('width: 400px'); + expect(ruleBody(css, '.v2-board .v2-board__overlay--drawer')).toContain('justify-content: flex-end'); + expect(read('../components/V2PodBoard.tsx')).toContain('className="v2-modal v2-board__detail v2-board__drawer"'); }); diff --git a/frontend/src/v2/components/V2PodBoard.tsx b/frontend/src/v2/components/V2PodBoard.tsx index 477e7a54fa..28474df9da 100644 --- a/frontend/src/v2/components/V2PodBoard.tsx +++ b/frontend/src/v2/components/V2PodBoard.tsx @@ -81,7 +81,6 @@ interface ColumnMeta { key: CanonicalStatus; labelKey: string; emptyKey: string; - pillClass: string; // Alias statuses written before the PATCH vocabulary gate (#921) render in // their canonical column instead of disappearing. statuses: string[]; @@ -94,7 +93,6 @@ const COLUMNS: ColumnMeta[] = [ key: 'pending', labelKey: 'board.col.pending', emptyKey: 'board.emptyCol.pending', - pillClass: 'v2-inspector__pill v2-inspector__pill--progress', statuses: ['pending', 'todo', 'open'], moves: [ { to: 'claimed', labelKey: 'board.move.start' }, @@ -105,7 +103,6 @@ const COLUMNS: ColumnMeta[] = [ key: 'claimed', labelKey: 'board.col.inProgress', emptyKey: 'board.emptyCol.inProgress', - pillClass: 'v2-inspector__pill v2-inspector__pill--progress', statuses: ['claimed', 'in_progress', 'in-progress'], moves: [ { to: 'done', labelKey: 'board.move.finish' }, @@ -116,7 +113,6 @@ const COLUMNS: ColumnMeta[] = [ key: 'blocked', labelKey: 'board.col.blocked', emptyKey: 'board.emptyCol.blocked', - pillClass: 'v2-inspector__pill v2-inspector__pill--blocked', statuses: ['blocked'], moves: [ { to: 'claimed', labelKey: 'board.move.resume' }, @@ -127,7 +123,6 @@ const COLUMNS: ColumnMeta[] = [ key: 'done', labelKey: 'board.col.done', emptyKey: 'board.emptyCol.done', - pillClass: 'v2-inspector__pill v2-inspector__pill--complete', statuses: ['done', 'completed', 'complete'], moves: [ { to: 'pending', labelKey: 'board.move.reopen' }, @@ -154,6 +149,7 @@ const V2PodBoard: React.FC = () => { const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(false); const [selected, setSelected] = useState(null); + const [updatesExpanded, setUpdatesExpanded] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [newTitle, setNewTitle] = useState(''); const [newAssignee, setNewAssignee] = useState(''); @@ -186,6 +182,7 @@ const V2PodBoard: React.FC = () => { taskMutationRef.current += 1; setDetailSaving(false); setDetailError(null); + setUpdatesExpanded(false); setSelected(task); }, []); @@ -635,11 +632,6 @@ const V2PodBoard: React.FC = () => { return byColumn; }, [tasks]); - const pillFor = (status: string): { className: string; label: string } => { - const col = COLUMNS.find((c) => c.statuses.includes(status)) || COLUMNS[0]; - return { className: col.pillClass, label: t(col.labelKey) }; - }; - return (
@@ -779,9 +771,9 @@ const V2PodBoard: React.FC = () => { )} {selected && ( -
openTask(null)}> +
openTask(null)}>
{
- {(() => { const pill = pillFor(selected.status); return {pill.label}; })()} +
+ {COLUMNS.map((column) => { + const current = column.statuses.includes(selected.status); + return ( + + ); + })} +
{selected.prUrl && ( {t('board.detail.pr')} @@ -832,32 +841,32 @@ const V2PodBoard: React.FC = () => { {detailError &&

{detailError}

} {selected.notes &&

{selected.notes}

} -
- {(COLUMNS.find((c) => c.statuses.includes(selected.status))?.moves || []).map((move) => ( - - ))} -
{(selected.updates?.length || 0) > 0 && ( <>
{t('board.detail.updates')}
    - {(selected.updates || []).slice().reverse().map((update, index) => ( - // Updates have no id; index-in-reversed-list is stable - // for a render of an immutable snapshot. - // eslint-disable-next-line react/no-array-index-key -
  • - {update.author} - {update.text} -
  • - ))} + {(() => { + // Newest first. The three newest stay visible; older ones fold behind a toggle. + const ordered = (selected.updates || []).slice().reverse(); + const earlier = Math.max(0, ordered.length - 3); + return (updatesExpanded ? ordered : ordered.slice(0, ordered.length - earlier)).map((update, index) => ( + // Updates have no id; index-in-reversed-list is stable + // for a render of an immutable snapshot. + // eslint-disable-next-line react/no-array-index-key +
  • + {update.author} + {update.text} +
  • + )); + })()}
+ {(selected.updates?.length || 0) > 3 && ( + + )} )}
diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 3014026a6d..34e446f9cc 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -9044,12 +9044,6 @@ body.modern-ui.v2-canvas { white-space: pre-wrap; } -.v2-board__detail-moves { - display: flex; - gap: 8px; - margin-bottom: 14px; -} - .v2-board__detail-updates { list-style: none; margin: 0; @@ -13765,6 +13759,15 @@ body.modern-ui.v2-canvas { .v2-board__detail .v2-modal__body { padding: 16px 24px 24px; display: grid; gap: 20px; } .v2-root .v2-board__detail button.v2-modal__close { width: 32px; height: 32px; border: 1px solid var(--v2-border); border-radius: 4px; flex-shrink: 0; } .v2-board__grab { display: none; } +/* D215 task board: details open in a 400px right drawer, not a centred modal. */ +.v2-board .v2-board__overlay--drawer { align-items: stretch; justify-content: flex-end; padding-top: 0; } +.v2-board .v2-board__drawer { width: 400px; max-width: 100%; height: 100%; max-height: none; border-radius: 14px 0 0 14px; box-shadow: var(--v2-shadow-lg); } +.v2-board__status-segment { display: inline-flex; max-width: 100%; border: 1px solid var(--v2-border); border-radius: 6px; overflow: hidden; } +.v2-root button.v2-board__status-option { min-height: 32px; padding: 0 12px; border: 0; border-radius: 0; background: var(--v2-bg); color: var(--v2-text-secondary); font-size: 13px; font-weight: 600; } +.v2-root button.v2-board__status-option + button.v2-board__status-option { border-left: 1px solid var(--v2-border-soft); } +.v2-root button.v2-board__status-option[aria-checked="true"] { background: var(--v2-surface); color: var(--v2-text-primary); } +.v2-root button.v2-board__status-option:disabled { cursor: default; } +.v2-root button.v2-board__updates-toggle { justify-self: start; min-height: 32px; padding: 0; border: 0; background: none; color: var(--v2-text-secondary); font-size: 13px; font-weight: 600; text-decoration: underline; } .v2-board__ownership { margin: 0; display: grid; grid-template-columns: 112px minmax(0, 1fr); border: 1px solid var(--v2-border-soft); border-radius: 4px; } .v2-board__ownership dt, .v2-board__ownership dd { margin: 0; padding: 10px 12px; min-height: 44px; display: flex; align-items: center; border-top: 1px solid var(--v2-border-soft); } .v2-board__ownership dt:first-of-type, .v2-board__ownership dd:first-of-type { border-top: 0; } @@ -13789,6 +13792,9 @@ body.modern-ui.v2-canvas { .v2-board__col { flex: 0 0 78%; min-width: 250px; } .v2-root button.v2-board__card-move { min-height: 44px; padding: 0 14px; font-size: 13px; } .v2-board .v2-board__detail { position: absolute; inset: 44px 0 0; width: 100%; max-height: none; border-radius: 6px 6px 0 0; } + .v2-board .v2-board__drawer { width: 100%; height: auto; border-radius: 6px 6px 0 0; } + .v2-root button.v2-board__status-option { min-height: 44px; padding: 0 14px; } + .v2-root button.v2-board__updates-toggle { min-height: 44px; } .v2-board__grab { display: block; width: 36px; height: 4px; border-radius: 2px; background: var(--v2-border-soft); margin: 8px auto 0; flex-shrink: 0; } .v2-board__detail .v2-modal__head { padding: 12px 16px 14px; } .v2-board__detail .v2-modal__body { padding: 14px 16px 24px; gap: 18px; }