perf(renderer): lazy mount closed menu controls
This commit is contained in:
parent
f06a50f859
commit
808e5f73e4
4 changed files with 194 additions and 184 deletions
|
|
@ -68,12 +68,12 @@ export const MoreMenu = ({
|
||||||
openTeamsTab,
|
openTeamsTab,
|
||||||
} = useStore(
|
} = useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
openCommandPalette: () => s.openCommandPalette(),
|
openCommandPalette: s.openCommandPalette,
|
||||||
openExtensionsTab: () => s.openExtensionsTab(),
|
openExtensionsTab: s.openExtensionsTab,
|
||||||
openSessionReport: (tabId: string) => s.openSessionReport(tabId),
|
openSessionReport: s.openSessionReport,
|
||||||
openSchedulesTab: () => s.openSchedulesTab(),
|
openSchedulesTab: s.openSchedulesTab,
|
||||||
openSettingsTab: () => s.openSettingsTab(),
|
openSettingsTab: s.openSettingsTab,
|
||||||
openTeamsTab: () => s.openTeamsTab(),
|
openTeamsTab: s.openTeamsTab,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useAppTranslation } from '@features/localization/renderer';
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
|
|
@ -46,6 +46,7 @@ export const KanbanFilterPopover = ({
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
}: KanbanFilterPopoverProps): React.JSX.Element => {
|
}: KanbanFilterPopoverProps): React.JSX.Element => {
|
||||||
const { t } = useAppTranslation('team');
|
const { t } = useAppTranslation('team');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const activeCount = useMemo(() => {
|
const activeCount = useMemo(() => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
if (filter.sessionId !== null) count += 1;
|
if (filter.sessionId !== null) count += 1;
|
||||||
|
|
@ -83,7 +84,7 @@ export const KanbanFilterPopover = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|
@ -104,111 +105,113 @@ export const KanbanFilterPopover = ({
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">{t('kanban.filter.title')}</TooltipContent>
|
<TooltipContent side="bottom">{t('kanban.filter.title')}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent align="end" className="w-72 p-0">
|
{open ? (
|
||||||
{/* Session section */}
|
<PopoverContent align="end" className="w-72 p-0">
|
||||||
<div className="border-b border-[var(--color-border)] p-3">
|
{/* Session section */}
|
||||||
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
<div className="border-b border-[var(--color-border)] p-3">
|
||||||
{t('kanban.filter.session')}
|
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
||||||
</p>
|
{t('kanban.filter.session')}
|
||||||
<div className="max-h-40 space-y-0.5 overflow-y-auto">
|
</p>
|
||||||
<button
|
<div className="max-h-40 space-y-0.5 overflow-y-auto">
|
||||||
type="button"
|
<button
|
||||||
className={`w-full rounded-md px-2 py-1.5 text-left text-xs transition-colors ${
|
type="button"
|
||||||
filter.sessionId === null
|
className={`w-full rounded-md px-2 py-1.5 text-left text-xs transition-colors ${
|
||||||
? 'bg-blue-500/15 text-blue-300'
|
filter.sessionId === null
|
||||||
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
|
? 'bg-blue-500/15 text-blue-300'
|
||||||
}`}
|
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
|
||||||
onClick={() => handleSessionSelect(null)}
|
}`}
|
||||||
>
|
onClick={() => handleSessionSelect(null)}
|
||||||
{t('kanban.filter.allSessions')}
|
>
|
||||||
</button>
|
{t('kanban.filter.allSessions')}
|
||||||
{sessions.map((session) => {
|
</button>
|
||||||
const isLead = session.id === leadSessionId;
|
{sessions.map((session) => {
|
||||||
const isSelected = filter.sessionId === session.id;
|
const isLead = session.id === leadSessionId;
|
||||||
const label = formatSessionLabel(session.firstMessage) || session.id.slice(0, 8);
|
const isSelected = filter.sessionId === session.id;
|
||||||
return (
|
const label = formatSessionLabel(session.firstMessage) || session.id.slice(0, 8);
|
||||||
<button
|
return (
|
||||||
key={session.id}
|
<button
|
||||||
type="button"
|
key={session.id}
|
||||||
className={`flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs transition-colors ${
|
type="button"
|
||||||
isSelected
|
className={`flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs transition-colors ${
|
||||||
? 'bg-blue-500/15 text-blue-300'
|
isSelected
|
||||||
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
|
? 'bg-blue-500/15 text-blue-300'
|
||||||
}`}
|
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
|
||||||
onClick={() => handleSessionSelect(isSelected ? null : session.id)}
|
}`}
|
||||||
|
onClick={() => handleSessionSelect(isSelected ? null : session.id)}
|
||||||
|
>
|
||||||
|
{isLead && <Crown size={11} className="shrink-0 text-blue-400" />}
|
||||||
|
<span className="truncate">{label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Teammate section */}
|
||||||
|
<div className="border-b border-[var(--color-border)] p-3">
|
||||||
|
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
||||||
|
{t('kanban.filter.teammate')}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{members.map((member) => (
|
||||||
|
<label
|
||||||
|
key={member.name}
|
||||||
|
className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]"
|
||||||
>
|
>
|
||||||
{isLead && <Crown size={11} className="shrink-0 text-blue-400" />}
|
<Checkbox
|
||||||
<span className="truncate">{label}</span>
|
checked={filter.selectedOwners.has(member.name)}
|
||||||
</button>
|
onCheckedChange={() => handleOwnerToggle(member.name)}
|
||||||
);
|
/>
|
||||||
})}
|
{displayMemberName(member.name)}
|
||||||
</div>
|
</label>
|
||||||
</div>
|
))}
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- Radix Checkbox renders a button, not a native input */}
|
||||||
{/* Teammate section */}
|
<label className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs italic text-[var(--color-text-muted)] hover:bg-[var(--color-surface-raised)]">
|
||||||
<div className="border-b border-[var(--color-border)] p-3">
|
|
||||||
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
|
||||||
{t('kanban.filter.teammate')}
|
|
||||||
</p>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
{members.map((member) => (
|
|
||||||
<label
|
|
||||||
key={member.name}
|
|
||||||
className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]"
|
|
||||||
>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={filter.selectedOwners.has(member.name)}
|
checked={filter.selectedOwners.has(UNASSIGNED_OWNER)}
|
||||||
onCheckedChange={() => handleOwnerToggle(member.name)}
|
onCheckedChange={() => handleOwnerToggle(UNASSIGNED_OWNER)}
|
||||||
/>
|
/>
|
||||||
{displayMemberName(member.name)}
|
{t('kanban.filter.unassigned')}
|
||||||
</label>
|
</label>
|
||||||
))}
|
</div>
|
||||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- Radix Checkbox renders a button, not a native input */}
|
|
||||||
<label className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs italic text-[var(--color-text-muted)] hover:bg-[var(--color-surface-raised)]">
|
|
||||||
<Checkbox
|
|
||||||
checked={filter.selectedOwners.has(UNASSIGNED_OWNER)}
|
|
||||||
onCheckedChange={() => handleOwnerToggle(UNASSIGNED_OWNER)}
|
|
||||||
/>
|
|
||||||
{t('kanban.filter.unassigned')}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Column section */}
|
{/* Column section */}
|
||||||
<div className="border-b border-[var(--color-border)] p-3">
|
<div className="border-b border-[var(--color-border)] p-3">
|
||||||
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
||||||
{t('kanban.filter.column')}
|
{t('kanban.filter.column')}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{KANBAN_COLUMNS.map((col) => (
|
{KANBAN_COLUMNS.map((col) => (
|
||||||
<label
|
<label
|
||||||
key={col.id}
|
key={col.id}
|
||||||
className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs hover:bg-[var(--color-surface-raised)]"
|
className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs hover:bg-[var(--color-surface-raised)]"
|
||||||
style={{ color: col.color }}
|
style={{ color: col.color }}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={filter.columns.has(col.id)}
|
checked={filter.columns.has(col.id)}
|
||||||
onCheckedChange={() => handleColumnToggle(col.id)}
|
onCheckedChange={() => handleColumnToggle(col.id)}
|
||||||
/>
|
/>
|
||||||
{t(col.labelKey)}
|
{t(col.labelKey)}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex justify-end p-2">
|
<div className="flex justify-end p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 px-2 text-[11px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
className="h-6 px-2 text-[11px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
||||||
disabled={activeCount === 0}
|
disabled={activeCount === 0}
|
||||||
onClick={handleClearAll}
|
onClick={handleClearAll}
|
||||||
>
|
>
|
||||||
{t('kanban.filter.clearAll')}
|
{t('kanban.filter.clearAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
) : null}
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useAppTranslation } from '@features/localization/renderer';
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
|
||||||
|
|
@ -53,10 +55,11 @@ export const KanbanSortPopover = ({
|
||||||
onSortChange,
|
onSortChange,
|
||||||
}: KanbanSortPopoverProps): React.JSX.Element => {
|
}: KanbanSortPopoverProps): React.JSX.Element => {
|
||||||
const { t } = useAppTranslation('team');
|
const { t } = useAppTranslation('team');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const isNonDefault = sort.field !== 'updatedAt';
|
const isNonDefault = sort.field !== 'updatedAt';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|
@ -77,66 +80,68 @@ export const KanbanSortPopover = ({
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">{t('kanban.sort.title')}</TooltipContent>
|
<TooltipContent side="bottom">{t('kanban.sort.title')}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent align="end" className="w-56 p-0">
|
{open ? (
|
||||||
<div className="p-3">
|
<PopoverContent align="end" className="w-56 p-0">
|
||||||
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
<div className="p-3">
|
||||||
{t('kanban.sort.sortBy')}
|
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
||||||
</p>
|
{t('kanban.sort.sortBy')}
|
||||||
<div className="space-y-0.5">
|
</p>
|
||||||
{SORT_OPTIONS.map((option) => {
|
<div className="space-y-0.5">
|
||||||
const isSelected = sort.field === option.field;
|
{SORT_OPTIONS.map((option) => {
|
||||||
return (
|
const isSelected = sort.field === option.field;
|
||||||
<button
|
return (
|
||||||
key={option.field}
|
<button
|
||||||
type="button"
|
key={option.field}
|
||||||
className={cn(
|
type="button"
|
||||||
'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-xs transition-colors',
|
|
||||||
isSelected
|
|
||||||
? 'bg-blue-500/15 text-blue-300'
|
|
||||||
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
|
|
||||||
)}
|
|
||||||
onClick={() => onSortChange({ field: option.field })}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0',
|
'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-xs transition-colors',
|
||||||
isSelected ? 'text-blue-400' : 'text-[var(--color-text-muted)]'
|
isSelected
|
||||||
|
? 'bg-blue-500/15 text-blue-300'
|
||||||
|
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
|
||||||
)}
|
)}
|
||||||
|
onClick={() => onSortChange({ field: option.field })}
|
||||||
>
|
>
|
||||||
{option.icon}
|
<span
|
||||||
</span>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="font-medium">{t(option.labelKey)}</div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[10px]',
|
'shrink-0',
|
||||||
isSelected ? 'text-blue-300/70' : 'text-[var(--color-text-muted)]'
|
isSelected ? 'text-blue-400' : 'text-[var(--color-text-muted)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t(option.descriptionKey)}
|
{option.icon}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="font-medium">{t(option.labelKey)}</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-[10px]',
|
||||||
|
isSelected ? 'text-blue-300/70' : 'text-[var(--color-text-muted)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t(option.descriptionKey)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{isSelected && (
|
||||||
{isSelected && (
|
<ArrowDownUp size={12} className="ml-auto shrink-0 text-blue-400" />
|
||||||
<ArrowDownUp size={12} className="ml-auto shrink-0 text-blue-400" />
|
)}
|
||||||
)}
|
</button>
|
||||||
</button>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{isNonDefault && (
|
||||||
{isNonDefault && (
|
<div className="flex justify-end border-t border-[var(--color-border)] p-2">
|
||||||
<div className="flex justify-end border-t border-[var(--color-border)] p-2">
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
className="h-6 px-2 text-[11px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
||||||
className="h-6 px-2 text-[11px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
onClick={() => onSortChange({ field: 'updatedAt' })}
|
||||||
onClick={() => onSortChange({ field: 'updatedAt' })}
|
>
|
||||||
>
|
{t('kanban.sort.reset')}
|
||||||
{t('kanban.sort.reset')}
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</PopoverContent>
|
||||||
</PopoverContent>
|
) : null}
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -206,32 +206,34 @@ const CancelTaskButton = ({
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top">{t('kanban.taskCard.cancel')}</TooltipContent>
|
<TooltipContent side="top">{t('kanban.taskCard.cancel')}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent
|
{open ? (
|
||||||
className="w-56 p-3"
|
<PopoverContent
|
||||||
side="top"
|
className="w-56 p-3"
|
||||||
align="start"
|
side="top"
|
||||||
onClick={(e) => e.stopPropagation()}
|
align="start"
|
||||||
>
|
onClick={(e) => e.stopPropagation()}
|
||||||
<p className="mb-3 text-xs text-[var(--color-text-secondary)]">
|
>
|
||||||
{t('kanban.taskCard.moveBackToTodoConfirm')}
|
<p className="mb-3 text-xs text-[var(--color-text-secondary)]">
|
||||||
</p>
|
{t('kanban.taskCard.moveBackToTodoConfirm')}
|
||||||
<div className="flex gap-2">
|
</p>
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
variant="destructive"
|
<Button
|
||||||
size="sm"
|
variant="destructive"
|
||||||
className="flex-1"
|
size="sm"
|
||||||
onClick={() => {
|
className="flex-1"
|
||||||
setOpen(false);
|
onClick={() => {
|
||||||
onConfirm(taskId);
|
setOpen(false);
|
||||||
}}
|
onConfirm(taskId);
|
||||||
>
|
}}
|
||||||
{t('kanban.taskCard.confirm')}
|
>
|
||||||
</Button>
|
{t('kanban.taskCard.confirm')}
|
||||||
<Button variant="outline" size="sm" className="flex-1" onClick={() => setOpen(false)}>
|
</Button>
|
||||||
{t('kanban.taskCard.keep')}
|
<Button variant="outline" size="sm" className="flex-1" onClick={() => setOpen(false)}>
|
||||||
</Button>
|
{t('kanban.taskCard.keep')}
|
||||||
</div>
|
</Button>
|
||||||
</PopoverContent>
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
) : null}
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue