fix(team): preserve task labels and change presence

This commit is contained in:
777genius 2026-05-25 15:43:07 +03:00
parent 086a8b3e29
commit 5513531053
4 changed files with 106 additions and 15 deletions

View file

@ -94,6 +94,7 @@ export const SidebarTaskItem = memo(function SidebarTaskItem({
getDisplaySubject,
}: SidebarTaskItemProps): React.JSX.Element {
const { t } = useAppTranslation('team');
const { t: tCommon } = useAppTranslation('common');
const openGlobalTaskDetail = useStore((s) => s.openGlobalTaskDetail);
const teamMembers = useStore(useShallow((s) => s.teamByName[task.teamName]?.members));
const unreadCount = useUnreadCommentCount(task.teamName, task.id, task.comments);
@ -137,10 +138,10 @@ export const SidebarTaskItem = memo(function SidebarTaskItem({
);
const updatedLabel = formatUpdatedLabel(
task,
t('tasks.date.updatedPrefix'),
t('tasks.date.updatedYesterday')
tCommon('tasks.date.updatedPrefix'),
tCommon('tasks.date.updatedYesterday')
);
const dateLabel = updatedLabel ?? formatTaskDate(task.createdAt, t('tasks.date.yesterday'));
const dateLabel = updatedLabel ?? formatTaskDate(task.createdAt, tCommon('tasks.date.yesterday'));
const ownerColorSet = useMemo(() => {
if (!teamMembers || !task.owner) return null;
@ -246,7 +247,7 @@ export const SidebarTaskItem = memo(function SidebarTaskItem({
<span
className={`ml-1.5 inline-block rounded-full px-1.5 py-0.5 align-middle text-[10px] font-medium leading-none ${REVIEW_STATE_DISPLAY.needsFix.bg} ${REVIEW_STATE_DISPLAY.needsFix.text}`}
>
{t('tasks.reviewState.needsFix')}
{tCommon('tasks.reviewState.needsFix')}
</span>
)}
</span>

View file

@ -522,10 +522,8 @@ export function __getTeamScopedTransientStateForTests(teamName: string): {
getResolvedMemberSelectorCacheSnapshotForTeam(teamName);
return {
hasResolvedMembersSelector:
resolvedMemberSelectorCacheSnapshot.hasResolvedMembersSelector,
resolvedMemberSelectorCount:
resolvedMemberSelectorCacheSnapshot.resolvedMemberSelectorCount,
hasResolvedMembersSelector: resolvedMemberSelectorCacheSnapshot.hasResolvedMembersSelector,
resolvedMemberSelectorCount: resolvedMemberSelectorCacheSnapshot.resolvedMemberSelectorCount,
hasMergedMessagesSelector: messageSelectorCache.hasMergedMessagesSelector,
memberMessagesSelectorCount: messageSelectorCache.memberMessagesSelectorCount,
hasPendingFreshTeamDataRefresh: pendingFreshTeamDataRefreshes.has(teamName),
@ -621,12 +619,7 @@ function maybeLogMemberSpawnUiEqualSuppressed(
teamName: string,
runId: string | null | undefined
): void {
if (
!shouldLogMemberSpawnUiEqualSuppressed(
teamName,
MEMBER_SPAWN_UI_EQUAL_WARN_THROTTLE_MS
)
) {
if (!shouldLogMemberSpawnUiEqualSuppressed(teamName, MEMBER_SPAWN_UI_EQUAL_WARN_THROTTLE_MS)) {
return;
}
logger.debug(
@ -2050,6 +2043,13 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
let changed = false;
const nextTasks = teamData.tasks.map((task) => {
const nextPresence = presenceByTaskId[task.id] ?? 'unknown';
if (
nextPresence === 'unknown' &&
task.changePresence &&
task.changePresence !== 'unknown'
) {
return task;
}
if (task.changePresence === nextPresence) {
return task;
}

View file

@ -1,5 +1,6 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { GlobalTask } from '../../../../src/shared/types';
@ -29,6 +30,28 @@ vi.mock('../../../../src/renderer/hooks/useTheme', () => ({
}),
}));
vi.mock('@features/localization/renderer', () => ({
useAppTranslation: (namespace: string) => {
const catalogs: Record<string, Record<string, string>> = {
common: {
'tasks.date.updatedPrefix': 'upd',
'tasks.date.updatedYesterday': 'upd yesterday',
'tasks.date.yesterday': 'Yesterday',
'tasks.reviewState.needsFix': 'Needs Fixes',
},
team: {
'tasks.teamPrefix': 'Team:',
'tasks.unassigned': 'Unassigned',
},
};
return {
resolvedLanguage: 'en',
t: (key: string) => catalogs[namespace]?.[key] ?? key,
};
},
}));
vi.mock('../../../../src/renderer/components/ui/tooltip', () => ({
Tooltip: ({ children }: React.PropsWithChildren) =>
React.createElement(React.Fragment, null, children),
@ -203,4 +226,38 @@ describe('SidebarTaskItem unread styling', () => {
await Promise.resolve();
});
});
it('renders translated updated and review labels instead of i18n keys', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const updatedAt = new Date();
const createdAt = new Date(updatedAt.getTime() - 5 * 60_000);
await act(async () => {
root.render(
React.createElement(SidebarTaskItem, {
task: makeTask({
createdAt: createdAt.toISOString(),
reviewState: 'needsFix',
updatedAt: updatedAt.toISOString(),
}),
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('upd');
expect(host.textContent).toContain('Needs Fixes');
expect(host.textContent).not.toContain('tasks.date.updatedPrefix');
expect(host.textContent).not.toContain('tasks.reviewState.needsFix');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
});

View file

@ -6,8 +6,8 @@ import {
__resetTeamSliceModuleStateForTests,
createTeamSlice,
getActiveTeamPendingReplyWaits,
hasActiveTeamPendingReplyWait,
getCurrentProvisioningProgressForTeam,
hasActiveTeamPendingReplyWait,
loadPersistedMessagesPanelMode,
savePersistedMessagesPanelMode,
selectMemberMessagesForTeamMember,
@ -24,6 +24,7 @@ import {
const hoisted = vi.hoisted(() => ({
list: vi.fn(),
getData: vi.fn(),
getTaskChangePresence: vi.fn(),
getMessagesPage: vi.fn(),
getMemberActivityMeta: vi.fn(),
createTeam: vi.fn(),
@ -61,6 +62,7 @@ vi.mock('@renderer/api', () => ({
teams: {
list: hoisted.list,
getData: hoisted.getData,
getTaskChangePresence: hoisted.getTaskChangePresence,
getMessagesPage: hoisted.getMessagesPage,
getMemberActivityMeta: hoisted.getMemberActivityMeta,
createTeam: hoisted.createTeam,
@ -302,6 +304,7 @@ describe('teamSlice actions', () => {
__resetTeamRefreshFanoutDiagnosticsForTests();
hoisted.list.mockResolvedValue([]);
hoisted.getData.mockResolvedValue(createTeamSnapshot());
hoisted.getTaskChangePresence.mockResolvedValue({});
hoisted.getMessagesPage.mockResolvedValue({
messages: [],
nextCursor: null,
@ -5147,6 +5150,36 @@ describe('teamSlice actions', () => {
expect(store.getState().selectedTeamData?.tasks[0]?.changePresence).toBe('has_changes');
});
it('does not clear known task changePresence when presence refresh returns unknown', async () => {
const store = createSliceStore();
store.setState({
selectedTeamName: 'my-team',
selectedTeamData: createTeamSnapshot({
tasks: [
{
id: 'task-1',
subject: 'Known changes',
status: 'in_progress',
owner: 'alice',
createdAt: '2026-03-01T10:00:00.000Z',
updatedAt: '2026-03-01T10:00:00.000Z',
workIntervals: [{ startedAt: '2026-03-01T10:05:00.000Z' }],
historyEvents: [],
comments: [],
attachments: [],
changePresence: 'has_changes',
},
],
}),
});
hoisted.getTaskChangePresence.mockResolvedValue({ 'task-1': 'unknown' });
await store.getState().refreshTeamChangePresence('my-team');
expect(store.getState().selectedTeamData?.tasks[0]?.changePresence).toBe('has_changes');
});
});
describe('provisioning run scoping', () => {