Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1605,10 +1605,9 @@
"defaultPriority": "default"
},
"owner": {
"left": "{{holder}} · <minutes>{{minutes}}m left</minutes>",
"assigned": "{{assignee}} · not claimed",
"expired": "lease expired · {{holder}}",
"claimed": "{{holder}} · claimed"
"leaseShort": "{{minutes}}m",
"expiredShort": "expired",
"claimedShort": "claimed"
},
"filter": {
"label": "Filters",
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1597,10 +1597,9 @@
"defaultPriority": "默认"
},
"owner": {
"left": "{{holder}} · <minutes>剩余{{minutes}}分钟</minutes>",
"assigned": "{{assignee}} · 尚未认领",
"expired": "认领已到期 · {{holder}}",
"claimed": "{{holder}} · 已认领"
"leaseShort": "{{minutes}}分钟",
"expiredShort": "已到期",
"claimedShort": "已认领"
},
"filter": {
"label": "筛选",
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/v2/__tests__/V2PodBoard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,10 @@ describe('V2PodBoard', () => {
claimedBy: 'kai-id', claimExpiresAt: expiresAt,
updates: [{ text: 'Lease renewed', author: 'Kai', authorId: 'kai-id' }],
};
const assignedOnly = { taskId: 'TASK-271', title: 'Assigned only', status: 'pending', assignee: 'lily-shen', updates: [] };
jest.useFakeTimers({ now });
axios.get.mockImplementation((url) => {
if (url.startsWith('/api/v1/tasks/')) return Promise.resolve({ data: { tasks: [task, holderFromUpdate] } });
if (url.startsWith('/api/v1/tasks/')) return Promise.resolve({ data: { tasks: [task, holderFromUpdate, assignedOnly] } });
if (url.endsWith('/focus')) return Promise.resolve({ data: FOCUS });
if (url.startsWith('/api/pods/')) return Promise.resolve({ data: {
name: 'My Workspace', members: [{ _id: 'forge-id', username: 'forge', displayName: 'Forge' }],
Expand All @@ -173,15 +174,19 @@ describe('V2PodBoard', () => {
renderBoard();
await act(async () => { await Promise.resolve(); });
expect(new Intl.DateTimeFormat().resolvedOptions().timeZone).toBe('America/Los_Angeles');
expect(screen.getByTitle('TASK-269')).toHaveTextContent('Forge · 1m left');
expect(screen.getByTitle('TASK-270')).toHaveTextContent('Kai · 1m left');
// The card line is the timer and the claimant's avatar (named by its title); no holder text.
expect(screen.getByTitle('TASK-269')).toHaveTextContent('1m');
expect(screen.getByTitle('TASK-269')).not.toHaveTextContent('Forge');
expect(screen.getByTitle('TASK-270')).toHaveTextContent('1m');
expect(screen.getByTitle('Forge')).toBeInTheDocument();
// An assigned task nobody has claimed shows no owner line at all.
expect(screen.getByTitle('TASK-271')).not.toHaveTextContent(/lily-shen|not claimed/);

act(() => { jest.advanceTimersByTime(59_999); });
expect(screen.getByTitle('TASK-269')).toHaveTextContent('Forge · 1m left');
expect(screen.getByTitle('TASK-269')).toHaveTextContent('1m');
act(() => { jest.advanceTimersByTime(1); });
expect(screen.queryByText(/1m left/)).not.toBeInTheDocument();
expect(screen.getByText('lease expired · Forge')).toBeInTheDocument();
expect(screen.getByText('lease expired · Kai')).toBeInTheDocument();
expect(screen.getByTitle('TASK-269')).toHaveTextContent('expired');
expect(screen.getByTitle('TASK-270')).toHaveTextContent('expired');
expect(axios.get.mock.calls.filter(([url]) => url.startsWith('/api/v1/tasks/'))).toHaveLength(1);
} finally {
jest.useRealTimers();
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/v2/__tests__/v2-layout-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5139,9 +5139,8 @@ describe('task priority and lease signals (TASK-443)', () => {
expect(ruleBody(css, '.v2-board__card--blocked')).toContain('box-shadow: inset 3px 0 0 var(--v2-status-blocked-edge)');
expect(ruleBody(css, '.v2-board__col--blocked .v2-board__col-name')).toContain('color: var(--v2-status-blocked)');
});
test('live, unclaimed and expired ownership retain distinct dots', () => {
test('live and expired ownership retain distinct dots', () => {
expect(ruleBody(css, '.v2-board__owner-dot--live')).toContain('background: var(--v2-status-live)');
expect(ruleBody(css, '.v2-board__owner-dot--not-claimed')).toContain('border: 1px dashed var(--v2-status-pending)');
const expired = ruleBody(css, '.v2-board__owner-dot--expired');
expect(expired).toContain('border: 1px solid var(--v2-status-idle)');
expect(expired).toContain('background: none');
Expand Down
27 changes: 17 additions & 10 deletions frontend/src/v2/components/V2PodBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// behaviorally interchangeable while v1 winds down.
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import AddIcon from '@mui/icons-material/Add';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import SearchIcon from '@mui/icons-material/Search';
Expand Down Expand Up @@ -250,7 +250,7 @@ const V2PodBoard: React.FC = () => {
}, [api, podId, detailSaving, t]);

const claimHolderName = useCallback((task: BoardTask): string => {
const member = podMembers.find((item) => memberId(item) === task.claimedBy);
const member = podMembers.find((item) => memberId(item) === task.claimedBy || item.username === task.claimedBy);
if (member) return memberLabel(member);
// The claim route records the claimant's User id and resolved author on
// the update. Match identity rather than the wording of the update text.
Expand All @@ -267,17 +267,24 @@ const V2PodBoard: React.FC = () => {
const timeLabel = (expiry: number) => new Intl.DateTimeFormat(undefined, {
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
}).format(new Date(expiry));
// The frame's card line is the lease timer and the claimant's avatar, nothing else:
// an assigned-but-unclaimed task shows no owner text.
const ownerLine = (task: BoardTask) => {
if (['done', 'completed', 'complete'].includes(task.status)) return null;
const lease = leaseFor(task);
if (!lease.claimed && !task.assignee) return null;
const state = lease.expired ? 'expired' : lease.claimed ? 'live' : 'not-claimed';
const text = lease.expired ? t('board.owner.expired', { holder: claimHolderName(task) })
: lease.claimed ? (lease.knownExpiry
? <Trans i18nKey="board.owner.left" values={{ holder: claimHolderName(task), minutes: lease.minutes }} components={{ minutes: <span className="v2-board__owner-time" /> }} />
: t('board.owner.claimed', { holder: claimHolderName(task) }))
: t('board.owner.assigned', { assignee: task.assignee });
return <span className="v2-board__owner-line"><span aria-hidden="true" className={`v2-board__owner-dot v2-board__owner-dot--${state}`} />{text}</span>;
if (!lease.claimed) return null;
const holder = claimHolderName(task);
const member = podMembers.find((item) => memberId(item) === task.claimedBy || item.username === task.claimedBy);
const state = lease.expired ? 'expired' : 'live';
return (
<span className="v2-board__owner-line">
<span aria-hidden="true" className={`v2-board__owner-dot v2-board__owner-dot--${state}`} />
<span className="v2-board__owner-time">
{lease.expired ? t('board.owner.expiredShort') : lease.knownExpiry ? t('board.owner.leaseShort', { minutes: lease.minutes }) : t('board.owner.claimedShort')}
</span>
<V2Avatar name={holder} src={member?.profilePicture} kind={member?.isBot === undefined ? undefined : member.isBot ? 'agent' : 'human'} seed={member ? memberId(member) : holder} size="sm" className="v2-board__card-avatar" />
</span>
);
};
const memberAvatar = (member: PodMember, name: string) => (
<V2Avatar name={name} src={member.profilePicture} kind={member.isBot === undefined ? undefined : member.isBot ? 'agent' : 'human'} seed={memberId(member)} size="sm" className="v2-board__member-avatar" />
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/v2/v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -13822,7 +13822,6 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor
.v2-board__owner-line { display: flex; align-items: center; gap: 6px; font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-secondary); }
.v2-board__owner-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.v2-board__owner-dot--live { background: var(--v2-status-live); }
.v2-board__owner-dot--not-claimed { border: 1px dashed var(--v2-status-pending); }
.v2-board__owner-dot--expired { border: 1px solid var(--v2-status-idle); background: none; }
.v2-board__card--done { border-color: var(--v2-border-soft); }
.v2-board__card--done .v2-board__card-title { color: var(--v2-text-secondary); }
Expand Down Expand Up @@ -13860,6 +13859,7 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor
.v2-board__watcher-list { display: flex; flex-wrap: wrap; gap: 6px; }
.v2-board__watcher { display: inline-flex; align-items: center; gap: 4px; min-height: 28px; max-width: 100%; padding: 0 8px 0 4px; border: 1px solid var(--v2-border-soft); border-radius: 4px; font-size: 12px; overflow-wrap: anywhere; }
.v2-board .v2-board__member-avatar { width: 20px; height: 20px; min-width: 20px; border-radius: 4px; }
.v2-board .v2-board__card-avatar { width: 20px; height: 20px; min-width: 20px; border-radius: 6px; }
.v2-board__watcher-hint, .v2-board__field small { margin: 8px 0 0; font-size: 12px; line-height: 16px; color: var(--v2-text-muted); }
.v2-board__create-row { display: grid; grid-template-columns: minmax(0, 1fr) 140px; gap: 12px; }
.v2-board__field input, .v2-board__field select { min-height: 40px; border-radius: 4px; }
Expand Down Expand Up @@ -13983,8 +13983,10 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor
.v2-board__card-main { display: grid; gap: 4px; padding: 0; border: 0; background: none; text-align: left; cursor: pointer; min-width: 0; color: inherit; font: inherit; }
.v2-board__card-kicker > :not(.v2-board__owner-line):not(.v2-board__card-spacer) { flex: none; }
.v2-board__card-kicker { display: flex; align-items: center; gap: 6px; min-width: 0; font: 500 11px/16px var(--v2-font-mono); }
/* The frame fixes the id row at 16px; its 20px avatar overflows it instead of growing the card. */
.v2-board__card-kicker, .v2-board__card-kicker .v2-board__owner-line { height: 16px; }
.v2-board__card-spacer { flex: 1 1 auto; }
.v2-board__card-kicker .v2-board__owner-line { min-width: 0; flex: 0 1 auto; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); }
.v2-board__card-kicker .v2-board__owner-line { min-width: 0; flex: 0 1 auto; overflow: visible; white-space: nowrap; font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); }
.v2-board__card-title {
font: 500 13px/18px var(--v2-font); display: -webkit-box; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; overflow: hidden;
Expand Down
Loading