perf(renderer): lazy render task tooltip content
This commit is contained in:
parent
a7a732e226
commit
38ea035fb5
1 changed files with 75 additions and 58 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useAppTranslation } from '@features/localization/renderer';
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||||
|
|
@ -65,16 +65,36 @@ interface TaskTooltipProps {
|
||||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tooltip that shows task summary on hover over any #taskId link.
|
|
||||||
* Reads task data from the current team in the store.
|
|
||||||
*/
|
|
||||||
export const TaskTooltip = memo(function TaskTooltip({
|
export const TaskTooltip = memo(function TaskTooltip({
|
||||||
taskId,
|
taskId,
|
||||||
teamName,
|
teamName,
|
||||||
children,
|
children,
|
||||||
side = 'top',
|
side = 'top',
|
||||||
}: TaskTooltipProps): React.JSX.Element {
|
}: TaskTooltipProps): React.JSX.Element {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
||||||
|
setOpen(nextOpen);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
|
{open ? <TaskTooltipContent taskId={taskId} teamName={teamName} side={side} /> : null}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface TaskTooltipContentProps {
|
||||||
|
taskId: string;
|
||||||
|
teamName?: string;
|
||||||
|
side: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskTooltipContent = memo(function TaskTooltipContent({
|
||||||
|
taskId,
|
||||||
|
teamName,
|
||||||
|
side,
|
||||||
|
}: TaskTooltipContentProps): React.JSX.Element | null {
|
||||||
const { t } = useAppTranslation('team');
|
const { t } = useAppTranslation('team');
|
||||||
const { selectedTeamName, selectedTeamData, selectedTeamMembers, globalTasks, teamByName } =
|
const { selectedTeamName, selectedTeamData, selectedTeamMembers, globalTasks, teamByName } =
|
||||||
useStore(
|
useStore(
|
||||||
|
|
@ -127,7 +147,7 @@ export const TaskTooltip = memo(function TaskTooltip({
|
||||||
);
|
);
|
||||||
|
|
||||||
// If task not found, render children without tooltip
|
// If task not found, render children without tooltip
|
||||||
if (!task) return children;
|
if (!task) return null;
|
||||||
|
|
||||||
const column = getEffectiveColumn(task);
|
const column = getEffectiveColumn(task);
|
||||||
const statusColor = STATUS_COLORS[column] ?? STATUS_COLORS.pending;
|
const statusColor = STATUS_COLORS[column] ?? STATUS_COLORS.pending;
|
||||||
|
|
@ -142,63 +162,60 @@ export const TaskTooltip = memo(function TaskTooltip({
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<TooltipContent side={side} className="max-w-xs space-y-1.5 p-2.5">
|
||||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
{resolvedTeamName ? (
|
||||||
<TooltipContent side={side} className="max-w-xs space-y-1.5 p-2.5">
|
<div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||||
{resolvedTeamName ? (
|
{resolvedTeamDisplayName || resolvedTeamName}
|
||||||
<div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">
|
|
||||||
{resolvedTeamDisplayName || resolvedTeamName}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{/* Subject */}
|
|
||||||
<div className="text-xs font-medium text-[var(--color-text)]">
|
|
||||||
<span className="text-[var(--color-text-muted)]">{formatTaskDisplayLabel(task)}</span>{' '}
|
|
||||||
{task.subject}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
{/* Subject */}
|
||||||
|
<div className="text-xs font-medium text-[var(--color-text)]">
|
||||||
|
<span className="text-[var(--color-text-muted)]">{formatTaskDisplayLabel(task)}</span>{' '}
|
||||||
|
{task.subject}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Status badge */}
|
{/* Status badge */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium"
|
||||||
|
style={{ color: statusColor.text, backgroundColor: statusColor.bg }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{isTeamTaskNeedsFixActionable(task) ? (
|
||||||
<span
|
<span
|
||||||
className="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium"
|
className={`inline-block rounded px-1.5 py-0.5 text-[10px] font-medium ${REVIEW_STATE_DISPLAY.needsFix.bg} ${REVIEW_STATE_DISPLAY.needsFix.text}`}
|
||||||
style={{ color: statusColor.text, backgroundColor: statusColor.bg }}
|
|
||||||
>
|
>
|
||||||
{label}
|
{REVIEW_STATE_DISPLAY.needsFix.label}
|
||||||
</span>
|
</span>
|
||||||
{isTeamTaskNeedsFixActionable(task) ? (
|
|
||||||
<span
|
|
||||||
className={`inline-block rounded px-1.5 py-0.5 text-[10px] font-medium ${REVIEW_STATE_DISPLAY.needsFix.bg} ${REVIEW_STATE_DISPLAY.needsFix.text}`}
|
|
||||||
>
|
|
||||||
{REVIEW_STATE_DISPLAY.needsFix.label}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Owner */}
|
|
||||||
{task.owner && members.length > 0 ? (
|
|
||||||
<MemberBadge
|
|
||||||
name={task.owner}
|
|
||||||
color={colorMap.get(task.owner)}
|
|
||||||
teamName={resolvedTeamName}
|
|
||||||
/>
|
|
||||||
) : task.owner ? (
|
|
||||||
<span className="text-[10px] text-[var(--color-text-secondary)]">{task.owner}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-[10px] text-[var(--color-text-muted)]">
|
|
||||||
{t('tasks.unassigned')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description — full markdown with scroll */}
|
|
||||||
{task.description ? (
|
|
||||||
<div className="max-h-[200px] overflow-y-auto text-[10px]">
|
|
||||||
<MarkdownViewer
|
|
||||||
content={linkifyTaskIdsInMarkdown(task.description, task.descriptionTaskRefs)}
|
|
||||||
maxHeight="max-h-none"
|
|
||||||
bare
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
{/* Owner */}
|
||||||
|
{task.owner && members.length > 0 ? (
|
||||||
|
<MemberBadge
|
||||||
|
name={task.owner}
|
||||||
|
color={colorMap.get(task.owner)}
|
||||||
|
teamName={resolvedTeamName}
|
||||||
|
/>
|
||||||
|
) : task.owner ? (
|
||||||
|
<span className="text-[10px] text-[var(--color-text-secondary)]">{task.owner}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-[10px] text-[var(--color-text-muted)]">
|
||||||
|
{t('tasks.unassigned')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description — full markdown with scroll */}
|
||||||
|
{task.description ? (
|
||||||
|
<div className="max-h-[200px] overflow-y-auto text-[10px]">
|
||||||
|
<MarkdownViewer
|
||||||
|
content={linkifyTaskIdsInMarkdown(task.description, task.descriptionTaskRefs)}
|
||||||
|
maxHeight="max-h-none"
|
||||||
|
bare
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</TooltipContent>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue