perf(team): lazy load create team suggestions
This commit is contained in:
parent
f6b2bc4cec
commit
bc8d47aaa2
8 changed files with 106 additions and 33 deletions
|
|
@ -46,7 +46,6 @@ import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors'
|
||||||
import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence';
|
import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence';
|
||||||
import { useCreateTeamDraft } from '@renderer/hooks/useCreateTeamDraft';
|
import { useCreateTeamDraft } from '@renderer/hooks/useCreateTeamDraft';
|
||||||
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
|
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
|
||||||
import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer';
|
|
||||||
import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions';
|
import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions';
|
||||||
import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions';
|
import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions';
|
||||||
import { useTheme } from '@renderer/hooks/useTheme';
|
import { useTheme } from '@renderer/hooks/useTheme';
|
||||||
|
|
@ -473,6 +472,7 @@ export const CreateTeamDialog = ({
|
||||||
>({});
|
>({});
|
||||||
const [providerSettingsProviderId, setProviderSettingsProviderId] =
|
const [providerSettingsProviderId, setProviderSettingsProviderId] =
|
||||||
useState<TeamProviderId | null>(null);
|
useState<TeamProviderId | null>(null);
|
||||||
|
const [workflowMentionSuggestionsEnabled, setWorkflowMentionSuggestionsEnabled] = useState(false);
|
||||||
const prepareRequestSeqRef = useRef(0);
|
const prepareRequestSeqRef = useRef(0);
|
||||||
const prepareIdleHandlesRef = useRef(new Set<ScheduledIdleHandle>());
|
const prepareIdleHandlesRef = useRef(new Set<ScheduledIdleHandle>());
|
||||||
const prepareUnmountGenerationRef = useRef(0);
|
const prepareUnmountGenerationRef = useRef(0);
|
||||||
|
|
@ -561,6 +561,9 @@ export const CreateTeamDialog = ({
|
||||||
setSelectedFastModeRaw(value);
|
setSelectedFastModeRaw(value);
|
||||||
setStoredCreateTeamFastMode(value);
|
setStoredCreateTeamFastMode(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
const enableWorkflowMentionSuggestions = useCallback((): void => {
|
||||||
|
setWorkflowMentionSuggestionsEnabled(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const setWorktreeEnabled = (value: boolean): void => {
|
const setWorktreeEnabled = (value: boolean): void => {
|
||||||
setWorktreeEnabledRaw(value);
|
setWorktreeEnabledRaw(value);
|
||||||
|
|
@ -1235,6 +1238,7 @@ export const CreateTeamDialog = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
setWorkflowMentionSuggestionsEnabled(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1430,10 +1434,12 @@ export const CreateTeamDialog = ({
|
||||||
setSelectedProjectPath('');
|
setSelectedProjectPath('');
|
||||||
}, [open, cwdMode, projects, selectedProjectPath, setSelectedProjectPath]);
|
}, [open, cwdMode, projects, selectedProjectPath, setSelectedProjectPath]);
|
||||||
|
|
||||||
useFileListCacheWarmer(effectiveCwd || null);
|
const { suggestions: taskSuggestions } = useTaskSuggestions(null, {
|
||||||
|
enabled: workflowMentionSuggestionsEnabled,
|
||||||
const { suggestions: taskSuggestions } = useTaskSuggestions(null);
|
});
|
||||||
const { suggestions: teamMentionSuggestions } = useTeamSuggestions(null);
|
const { suggestions: teamMentionSuggestions } = useTeamSuggestions(null, {
|
||||||
|
enabled: workflowMentionSuggestionsEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
const description = descriptionDraft.value;
|
const description = descriptionDraft.value;
|
||||||
const prompt = promptDraft.value;
|
const prompt = promptDraft.value;
|
||||||
|
|
@ -2206,6 +2212,7 @@ export const CreateTeamDialog = ({
|
||||||
projectPath={effectiveCwd || null}
|
projectPath={effectiveCwd || null}
|
||||||
taskSuggestions={taskSuggestions}
|
taskSuggestions={taskSuggestions}
|
||||||
teamSuggestions={teamMentionSuggestions}
|
teamSuggestions={teamMentionSuggestions}
|
||||||
|
onWorkflowSuggestionsNeeded={enableWorkflowMentionSuggestions}
|
||||||
defaultProviderId={selectedProviderId}
|
defaultProviderId={selectedProviderId}
|
||||||
inheritedProviderId={selectedProviderId}
|
inheritedProviderId={selectedProviderId}
|
||||||
inheritedModel={selectedModel}
|
inheritedModel={selectedModel}
|
||||||
|
|
@ -2297,6 +2304,11 @@ export const CreateTeamDialog = ({
|
||||||
title="Optional launch settings"
|
title="Optional launch settings"
|
||||||
description="Prompt, safety, and CLI overrides live here when you need them."
|
description="Prompt, safety, and CLI overrides live here when you need them."
|
||||||
summary={launchOptionalSummary}
|
summary={launchOptionalSummary}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
enableWorkflowMentionSuggestions();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{selectedProviderId === 'anthropic' ? (
|
{selectedProviderId === 'anthropic' ? (
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ interface OptionalSettingsSectionProps {
|
||||||
summary?: string[];
|
summary?: string[];
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +62,7 @@ export const OptionalSettingsSection = ({
|
||||||
summary = [],
|
summary = [],
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
className,
|
className,
|
||||||
|
onOpenChange,
|
||||||
children,
|
children,
|
||||||
}: OptionalSettingsSectionProps): React.JSX.Element => {
|
}: OptionalSettingsSectionProps): React.JSX.Element => {
|
||||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
|
@ -106,6 +108,12 @@ export const OptionalSettingsSection = ({
|
||||||
? 'color-mix(in srgb, var(--color-text-muted) 64%, var(--color-text) 36%)'
|
? 'color-mix(in srgb, var(--color-text-muted) 64%, var(--color-text) 36%)'
|
||||||
: 'color-mix(in srgb, var(--color-text-muted) 54%, white 46%)';
|
: 'color-mix(in srgb, var(--color-text-muted) 54%, white 46%)';
|
||||||
|
|
||||||
|
const handleToggleOpen = (): void => {
|
||||||
|
const nextOpen = !isOpen;
|
||||||
|
setIsOpen(nextOpen);
|
||||||
|
onOpenChange?.(nextOpen);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -119,7 +127,7 @@ export const OptionalSettingsSection = ({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center gap-3 p-2.5 text-left transition-colors hover:bg-[var(--color-surface-raised)]"
|
className="flex w-full items-center gap-3 p-2.5 text-left transition-colors hover:bg-[var(--color-surface-raised)]"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={handleToggleOpen}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ interface MemberDraftRowProps {
|
||||||
mentionSuggestions?: MentionSuggestion[];
|
mentionSuggestions?: MentionSuggestion[];
|
||||||
taskSuggestions?: MentionSuggestion[];
|
taskSuggestions?: MentionSuggestion[];
|
||||||
teamSuggestions?: MentionSuggestion[];
|
teamSuggestions?: MentionSuggestion[];
|
||||||
|
onWorkflowSuggestionsNeeded?: () => void;
|
||||||
lockProviderModel?: boolean;
|
lockProviderModel?: boolean;
|
||||||
lockRole?: boolean;
|
lockRole?: boolean;
|
||||||
lockedRoleLabel?: string;
|
lockedRoleLabel?: string;
|
||||||
|
|
@ -144,6 +145,7 @@ export const MemberDraftRow = ({
|
||||||
mentionSuggestions = [],
|
mentionSuggestions = [],
|
||||||
taskSuggestions,
|
taskSuggestions,
|
||||||
teamSuggestions,
|
teamSuggestions,
|
||||||
|
onWorkflowSuggestionsNeeded,
|
||||||
lockProviderModel = false,
|
lockProviderModel = false,
|
||||||
lockRole = false,
|
lockRole = false,
|
||||||
lockedRoleLabel,
|
lockedRoleLabel,
|
||||||
|
|
@ -428,6 +430,15 @@ export const MemberDraftRow = ({
|
||||||
effectiveModel?.trim() ?? '',
|
effectiveModel?.trim() ?? '',
|
||||||
effectiveEffort
|
effectiveEffort
|
||||||
);
|
);
|
||||||
|
const toggleWorkflowExpanded = useCallback(() => {
|
||||||
|
setWorkflowExpanded((prev) => {
|
||||||
|
const next = !prev;
|
||||||
|
if (next) {
|
||||||
|
onWorkflowSuggestionsNeeded?.();
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [onWorkflowSuggestionsNeeded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -604,7 +615,7 @@ export const MemberDraftRow = ({
|
||||||
aria-label={workflowTooltipText}
|
aria-label={workflowTooltipText}
|
||||||
aria-expanded={workflowExpanded}
|
aria-expanded={workflowExpanded}
|
||||||
disabled={isRemoved}
|
disabled={isRemoved}
|
||||||
onClick={() => setWorkflowExpanded((prev) => !prev)}
|
onClick={toggleWorkflowExpanded}
|
||||||
>
|
>
|
||||||
<WorkflowIcon className="size-3.5" />
|
<WorkflowIcon className="size-3.5" />
|
||||||
{!workflowExpanded && workflowDraft.value.trim() ? (
|
{!workflowExpanded && workflowDraft.value.trim() ? (
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@ export interface MembersEditorSectionProps {
|
||||||
taskSuggestions?: MentionSuggestion[];
|
taskSuggestions?: MentionSuggestion[];
|
||||||
/** Team suggestions for @@team mentions in workflow */
|
/** Team suggestions for @@team mentions in workflow */
|
||||||
teamSuggestions?: MentionSuggestion[];
|
teamSuggestions?: MentionSuggestion[];
|
||||||
|
/** Called before workflow mention suggestions are needed. */
|
||||||
|
onWorkflowSuggestionsNeeded?: () => void;
|
||||||
/** Extra content rendered right below the "Members" label row */
|
/** Extra content rendered right below the "Members" label row */
|
||||||
headerExtra?: React.ReactNode;
|
headerExtra?: React.ReactNode;
|
||||||
/** When true, hides member rows and action buttons (label + headerExtra still visible) */
|
/** When true, hides member rows and action buttons (label + headerExtra still visible) */
|
||||||
|
|
@ -166,6 +168,7 @@ export const MembersEditorSection = ({
|
||||||
projectPath,
|
projectPath,
|
||||||
taskSuggestions,
|
taskSuggestions,
|
||||||
teamSuggestions,
|
teamSuggestions,
|
||||||
|
onWorkflowSuggestionsNeeded,
|
||||||
headerExtra,
|
headerExtra,
|
||||||
hideContent = false,
|
hideContent = false,
|
||||||
existingMembers,
|
existingMembers,
|
||||||
|
|
@ -552,6 +555,7 @@ export const MembersEditorSection = ({
|
||||||
mentionSuggestions={mentionSuggestions}
|
mentionSuggestions={mentionSuggestions}
|
||||||
taskSuggestions={taskSuggestions}
|
taskSuggestions={taskSuggestions}
|
||||||
teamSuggestions={teamSuggestions}
|
teamSuggestions={teamSuggestions}
|
||||||
|
onWorkflowSuggestionsNeeded={onWorkflowSuggestionsNeeded}
|
||||||
lockProviderModel={lockProviderModel}
|
lockProviderModel={lockProviderModel}
|
||||||
lockIdentity={lockExistingMemberIdentity && Boolean(member.originalName?.trim())}
|
lockIdentity={lockExistingMemberIdentity && Boolean(member.originalName?.trim())}
|
||||||
identityLockReason={identityLockReason}
|
identityLockReason={identityLockReason}
|
||||||
|
|
@ -604,6 +608,7 @@ export const MembersEditorSection = ({
|
||||||
mentionSuggestions={mentionSuggestions}
|
mentionSuggestions={mentionSuggestions}
|
||||||
taskSuggestions={taskSuggestions}
|
taskSuggestions={taskSuggestions}
|
||||||
teamSuggestions={teamSuggestions}
|
teamSuggestions={teamSuggestions}
|
||||||
|
onWorkflowSuggestionsNeeded={onWorkflowSuggestionsNeeded}
|
||||||
lockProviderModel
|
lockProviderModel
|
||||||
modelLockReason="Removed members are kept for soft delete history. Restore them to edit settings."
|
modelLockReason="Removed members are kept for soft delete history. Restore them to edit settings."
|
||||||
isRemoved
|
isRemoved
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ interface TeamRosterEditorSectionProps {
|
||||||
projectPath?: string | null;
|
projectPath?: string | null;
|
||||||
taskSuggestions?: MentionSuggestion[];
|
taskSuggestions?: MentionSuggestion[];
|
||||||
teamSuggestions?: MentionSuggestion[];
|
teamSuggestions?: MentionSuggestion[];
|
||||||
|
onWorkflowSuggestionsNeeded?: () => void;
|
||||||
hideMembersContent?: boolean;
|
hideMembersContent?: boolean;
|
||||||
existingMembers?: readonly { name: string; color?: string; removedAt?: number | string | null }[];
|
existingMembers?: readonly { name: string; color?: string; removedAt?: number | string | null }[];
|
||||||
defaultProviderId?: TeamProviderId;
|
defaultProviderId?: TeamProviderId;
|
||||||
|
|
@ -76,6 +77,7 @@ const TeamRosterEditorSectionImpl = ({
|
||||||
projectPath,
|
projectPath,
|
||||||
taskSuggestions,
|
taskSuggestions,
|
||||||
teamSuggestions,
|
teamSuggestions,
|
||||||
|
onWorkflowSuggestionsNeeded,
|
||||||
hideMembersContent = false,
|
hideMembersContent = false,
|
||||||
existingMembers,
|
existingMembers,
|
||||||
defaultProviderId = 'anthropic',
|
defaultProviderId = 'anthropic',
|
||||||
|
|
@ -153,6 +155,7 @@ const TeamRosterEditorSectionImpl = ({
|
||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
taskSuggestions={taskSuggestions}
|
taskSuggestions={taskSuggestions}
|
||||||
teamSuggestions={teamSuggestions}
|
teamSuggestions={teamSuggestions}
|
||||||
|
onWorkflowSuggestionsNeeded={onWorkflowSuggestionsNeeded}
|
||||||
hideContent={hideMembersContent}
|
hideContent={hideMembersContent}
|
||||||
existingMembers={existingMembers}
|
existingMembers={existingMembers}
|
||||||
defaultProviderId={defaultProviderId}
|
defaultProviderId={defaultProviderId}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* Folders are derived from file paths (no extra IPC call needed).
|
* Folders are derived from file paths (no extra IPC call needed).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getQuickOpenCache,
|
getQuickOpenCache,
|
||||||
|
|
@ -190,16 +190,6 @@ export function useFileSuggestions(
|
||||||
return onQuickOpenCacheInvalidated(() => setFetchTrigger((n) => n + 1));
|
return onQuickOpenCacheInvalidated(() => setFetchTrigger((n) => n + 1));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Lazy refetch: when dropdown opens and cache is stale, trigger a reload
|
|
||||||
const prevEnabledRef = useRef(enabled);
|
|
||||||
useEffect(() => {
|
|
||||||
if (enabled && !prevEnabledRef.current && projectPath && !getQuickOpenCache(projectPath)) {
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional trigger on state transition
|
|
||||||
setFetchTrigger((n) => n + 1);
|
|
||||||
}
|
|
||||||
prevEnabledRef.current = enabled;
|
|
||||||
}, [enabled, projectPath]);
|
|
||||||
|
|
||||||
// Load files from API when cache is empty.
|
// Load files from API when cache is empty.
|
||||||
// Uses project:listFiles (not editor:listFiles) — works without editor being open.
|
// Uses project:listFiles (not editor:listFiles) — works without editor being open.
|
||||||
const fetchFiles = useCallback(
|
const fetchFiles = useCallback(
|
||||||
|
|
@ -231,13 +221,14 @@ export function useFileSuggestions(
|
||||||
// - effect (projectPath change)
|
// - effect (projectPath change)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectPath) return;
|
if (!projectPath) return;
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
const cached = getQuickOpenCache(projectPath);
|
const cached = getQuickOpenCache(projectPath);
|
||||||
if (cached) return;
|
if (cached) return;
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- setLoading before async fetch is intentional
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- setLoading before async fetch is intentional
|
||||||
return fetchFiles(projectPath);
|
return fetchFiles(projectPath);
|
||||||
}, [projectPath, fetchTrigger, fetchFiles]);
|
}, [projectPath, enabled, fetchTrigger, fetchFiles]);
|
||||||
|
|
||||||
// Derive folders from file list (memoized)
|
// Derive folders from file list (memoized)
|
||||||
const allFolders = useMemo(
|
const allFolders = useMemo(
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,22 @@ import { getTaskDisplayId } from '@shared/utils/taskIdentity';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||||
import type { GlobalTask, TeamTaskWithKanban } from '@shared/types';
|
import type { GlobalTask, TeamSummary, TeamTaskWithKanban } from '@shared/types';
|
||||||
|
|
||||||
|
const EMPTY_GLOBAL_TASKS: GlobalTask[] = [];
|
||||||
|
const EMPTY_TEAM_TASKS: TeamTaskWithKanban[] = [];
|
||||||
|
const EMPTY_TEAM_MEMBERS: NonNullable<TeamSummary['members']> = [];
|
||||||
|
const EMPTY_TEAM_BY_NAME: Record<string, TeamSummary> = {};
|
||||||
|
const EMPTY_TASK_SUGGESTIONS: MentionSuggestion[] = [];
|
||||||
|
|
||||||
export interface UseTaskSuggestionsResult {
|
export interface UseTaskSuggestionsResult {
|
||||||
suggestions: MentionSuggestion[];
|
suggestions: MentionSuggestion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UseTaskSuggestionsOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface TaskWithTeamContext {
|
interface TaskWithTeamContext {
|
||||||
task: TeamTaskWithKanban | GlobalTask;
|
task: TeamTaskWithKanban | GlobalTask;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
|
|
@ -60,19 +70,29 @@ function isVisibleTask(task: TeamTaskWithKanban | GlobalTask): boolean {
|
||||||
return task.status !== 'deleted' && !task.deletedAt;
|
return task.status !== 'deleted' && !task.deletedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTaskSuggestions(currentTeamName: string | null): UseTaskSuggestionsResult {
|
export function useTaskSuggestions(
|
||||||
|
currentTeamName: string | null,
|
||||||
|
options: UseTaskSuggestionsOptions = {}
|
||||||
|
): UseTaskSuggestionsResult {
|
||||||
|
const enabled = options.enabled ?? true;
|
||||||
const { globalTasks, currentTeamData, currentTeamMembers, teamByName } = useStore(
|
const { globalTasks, currentTeamData, currentTeamMembers, teamByName } = useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
globalTasks: s.globalTasks,
|
globalTasks: enabled ? s.globalTasks : EMPTY_GLOBAL_TASKS,
|
||||||
currentTeamData: currentTeamName ? selectTeamDataForName(s, currentTeamName) : null,
|
currentTeamData:
|
||||||
currentTeamMembers: currentTeamName
|
enabled && currentTeamName ? selectTeamDataForName(s, currentTeamName) : null,
|
||||||
? selectResolvedMembersForTeamName(s, currentTeamName)
|
currentTeamMembers:
|
||||||
: [],
|
enabled && currentTeamName
|
||||||
teamByName: s.teamByName,
|
? selectResolvedMembersForTeamName(s, currentTeamName)
|
||||||
|
: EMPTY_TEAM_MEMBERS,
|
||||||
|
teamByName: enabled ? s.teamByName : EMPTY_TEAM_BY_NAME,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const suggestions = useMemo<MentionSuggestion[]>(() => {
|
const suggestions = useMemo<MentionSuggestion[]>(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return EMPTY_TASK_SUGGESTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
const tasks: TaskWithTeamContext[] = [];
|
const tasks: TaskWithTeamContext[] = [];
|
||||||
const seenTaskIds = new Set<string>();
|
const seenTaskIds = new Set<string>();
|
||||||
|
|
||||||
|
|
@ -80,7 +100,10 @@ export function useTaskSuggestions(currentTeamName: string | null): UseTaskSugge
|
||||||
const currentTeamSummary = teamByName[currentTeamName];
|
const currentTeamSummary = teamByName[currentTeamName];
|
||||||
const currentTeamDisplayName = currentTeamSummary?.displayName || currentTeamName;
|
const currentTeamDisplayName = currentTeamSummary?.displayName || currentTeamName;
|
||||||
const currentTeamTasks =
|
const currentTeamTasks =
|
||||||
currentTeamData?.tasks ?? globalTasks.filter((task) => task.teamName === currentTeamName);
|
currentTeamData?.tasks ??
|
||||||
|
(currentTeamName
|
||||||
|
? globalTasks.filter((task) => task.teamName === currentTeamName)
|
||||||
|
: EMPTY_TEAM_TASKS);
|
||||||
const currentTeamMemberColors =
|
const currentTeamMemberColors =
|
||||||
currentTeamMembers.length > 0 ? currentTeamMembers : (currentTeamSummary?.members ?? []);
|
currentTeamMembers.length > 0 ? currentTeamMembers : (currentTeamSummary?.members ?? []);
|
||||||
|
|
||||||
|
|
@ -125,7 +148,7 @@ export function useTaskSuggestions(currentTeamName: string | null): UseTaskSugge
|
||||||
});
|
});
|
||||||
|
|
||||||
return tasks.map(buildTaskSuggestion);
|
return tasks.map(buildTaskSuggestion);
|
||||||
}, [currentTeamData, currentTeamMembers, currentTeamName, globalTasks, teamByName]);
|
}, [currentTeamData, currentTeamMembers, currentTeamName, enabled, globalTasks, teamByName]);
|
||||||
|
|
||||||
return { suggestions };
|
return { suggestions };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,19 +15,31 @@ import { useStore } from '@renderer/store';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||||
|
import type { TeamSummary } from '@shared/types';
|
||||||
|
|
||||||
export interface UseTeamSuggestionsResult {
|
export interface UseTeamSuggestionsResult {
|
||||||
suggestions: MentionSuggestion[];
|
suggestions: MentionSuggestion[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UseTeamSuggestionsOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_TEAMS: TeamSummary[] = [];
|
||||||
|
const EMPTY_TEAM_SUGGESTIONS: MentionSuggestion[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns team MentionSuggestion[] sorted by online status (online first).
|
* Returns team MentionSuggestion[] sorted by online status (online first).
|
||||||
*
|
*
|
||||||
* @param currentTeamName - The current team name to exclude from suggestions
|
* @param currentTeamName - The current team name to exclude from suggestions
|
||||||
*/
|
*/
|
||||||
export function useTeamSuggestions(currentTeamName: string | null): UseTeamSuggestionsResult {
|
export function useTeamSuggestions(
|
||||||
const teams = useStore(useShallow((s) => s.teams));
|
currentTeamName: string | null,
|
||||||
|
options: UseTeamSuggestionsOptions = {}
|
||||||
|
): UseTeamSuggestionsResult {
|
||||||
|
const enabled = options.enabled ?? true;
|
||||||
|
const teams = useStore(useShallow((s) => (enabled ? s.teams : EMPTY_TEAMS)));
|
||||||
const [aliveTeams, setAliveTeams] = useState<Set<string>>(new Set());
|
const [aliveTeams, setAliveTeams] = useState<Set<string>>(new Set());
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
|
@ -45,11 +57,19 @@ export function useTeamSuggestions(currentTeamName: string | null): UseTeamSugge
|
||||||
|
|
||||||
// Fetch on mount and when teams list changes
|
// Fetch on mount and when teams list changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
void fetchAlive();
|
void fetchAlive();
|
||||||
}, [fetchAlive, teams]);
|
}, [enabled, fetchAlive, teams]);
|
||||||
|
|
||||||
// Build suggestion list sorted: online first, then offline
|
// Build suggestion list sorted: online first, then offline
|
||||||
const suggestions = useMemo<MentionSuggestion[]>(() => {
|
const suggestions = useMemo<MentionSuggestion[]>(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return EMPTY_TEAM_SUGGESTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
const nonDeleted = teams.filter((t) => !t.deletedAt && t.teamName !== currentTeamName);
|
const nonDeleted = teams.filter((t) => !t.deletedAt && t.teamName !== currentTeamName);
|
||||||
|
|
||||||
const result: MentionSuggestion[] = nonDeleted.map((t) => {
|
const result: MentionSuggestion[] = nonDeleted.map((t) => {
|
||||||
|
|
@ -72,7 +92,7 @@ export function useTeamSuggestions(currentTeamName: string | null): UseTeamSugge
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [teams, currentTeamName, aliveTeams]);
|
}, [enabled, teams, currentTeamName, aliveTeams]);
|
||||||
|
|
||||||
return { suggestions, loading };
|
return { suggestions, loading };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue