diff --git a/src/app/(app)/mingo/context/query-keys.ts b/src/app/(app)/mingo/context/query-keys.ts new file mode 100644 index 00000000..5b6070f6 --- /dev/null +++ b/src/app/(app)/mingo/context/query-keys.ts @@ -0,0 +1,13 @@ +/** + * Canonical TanStack Query key factories for the Mingo context items. + * + * Centralizing these avoids ad-hoc inline query key arrays drifting apart + * across files (e.g. rest-items.tsx and use-mingo-dialog.ts) and failing to + * invalidate the correct cache entries. + */ + +export const mingoContextKeys = { + tickets: (query: string) => ['mingo-context', 'tickets', query] as const, + policies: (query: string) => ['mingo-context', 'policies', query] as const, + queries: (query: string) => ['mingo-context', 'queries', query] as const, +}; diff --git a/src/app/(app)/mingo/context/rest-items.tsx b/src/app/(app)/mingo/context/rest-items.tsx index 6abd597e..76cc8e76 100644 --- a/src/app/(app)/mingo/context/rest-items.tsx +++ b/src/app/(app)/mingo/context/rest-items.tsx @@ -18,6 +18,7 @@ import { apiClient } from '@/lib/api-client'; import { fleetApiClient } from '@/lib/fleet-api-client'; import { CONTEXT_ENTITY_KIND } from './context-types'; import { type ContextItemsProps, MINGO_CONTEXT_PAGE_SIZE, useClientPaging } from './items-shared'; +import { mingoContextKeys } from './query-keys'; // ───────────────────────────── Ticket ─────────────────────────────────────── @@ -68,7 +69,7 @@ async function fetchTicketsPage( export function TicketItems({ query, selectedKeys, onToggle, atLimit }: ContextItemsProps) { const q = useSuspenseInfiniteQuery({ - queryKey: ['mingo-context', 'tickets', query], + queryKey: mingoContextKeys.tickets(query), queryFn: ({ pageParam }) => fetchTicketsPage(query, pageParam), initialPageParam: null as string | null, getNextPageParam: last => last.nextCursor, @@ -104,7 +105,7 @@ async function fetchPolicies(query: string): Promise { export function PolicyItems({ query, selectedKeys, onToggle, atLimit }: ContextItemsProps) { const { data } = useSuspenseQuery({ - queryKey: ['mingo-context', 'policies', query], + queryKey: mingoContextKeys.policies(query), queryFn: () => fetchPolicies(query), staleTime: 30 * 1000, }); @@ -138,7 +139,7 @@ async function fetchQueriesPage(query: string, page: number): Promise<{ items: C export function QueryItems({ query, selectedKeys, onToggle, atLimit }: ContextItemsProps) { const q = useSuspenseInfiniteQuery({ - queryKey: ['mingo-context', 'queries', query], + queryKey: mingoContextKeys.queries(query), queryFn: ({ pageParam }) => fetchQueriesPage(query, pageParam), initialPageParam: 0, getNextPageParam: (last, all) => (last.hasMore ? all.length : undefined), diff --git a/src/app/(app)/monitoring/hooks/use-labels.ts b/src/app/(app)/monitoring/hooks/use-labels.ts index 1ba88b94..5321f0cf 100644 --- a/src/app/(app)/monitoring/hooks/use-labels.ts +++ b/src/app/(app)/monitoring/hooks/use-labels.ts @@ -10,8 +10,10 @@ const EMPTY_LABELS: FleetLabel[] = []; // ============ Query Keys ============ +const LABELS_BASE_KEY = 'labels' as const; + export const labelsQueryKeys = { - all: ['labels'] as const, + all: [LABELS_BASE_KEY] as const, list: () => [...labelsQueryKeys.all, 'list'] as const, }; diff --git a/src/app/(app)/tickets/hooks/use-ticket-options.ts b/src/app/(app)/tickets/hooks/use-ticket-options.ts index 599869ce..5ef255a5 100644 --- a/src/app/(app)/tickets/hooks/use-ticket-options.ts +++ b/src/app/(app)/tickets/hooks/use-ticket-options.ts @@ -30,6 +30,16 @@ export interface AvatarOption extends AutocompleteOption { const EMPTY_AUTOCOMPLETE_OPTIONS: AutocompleteOption[] = []; const EMPTY_AVATAR_OPTIONS: AvatarOption[] = []; +// Named query-key builders for this module's cache entries. Centralizing these +// here (rather than inlining string arrays at each useQuery call) gives any +// future invalidateQueries call site a single source of truth to reference. +export const ticketOptionsQueryKeys = { + organizations: (search: string) => ['ticket-options', 'organizations', search] as const, + assignees: () => ['ticket-options', 'assignees'] as const, + tickets: (search: string, organizationId: string | null, statusIds: string[] | null) => + ['ticket-options', 'tickets', search, organizationId, statusIds] as const, +}; + /** An image reference as both the GraphQL and REST endpoints below return it. */ interface OptionImage { imageUrl?: string | null; @@ -53,6 +63,12 @@ interface UserOption { // --- Organizations (reuse existing query via /api/graphql) --- +// NOTE: This uses apiClient.post against a REST-shaped /api/graphql endpoint +// rather than react-relay (useLazyLoadQuery/useFragment). Per the org's Relay +// migration mandate, new GraphQL data fetching should go through react-relay; +// this is flagged as technical debt against that mandate rather than migrated +// here, since doing so would require restructuring this hook and its callers +// beyond the scope of this fix. async function fetchCustomerOptions(search: string): Promise { const response = await apiClient.post<{ data?: { organizations?: { edges?: { node: OrganizationOptionNode }[] } }; @@ -72,7 +88,7 @@ async function fetchCustomerOptions(search: string): Promise { export function useOrganizationOptions(search = '', enabled = true) { const query = useQuery({ - queryKey: ['ticket-options', 'organizations', search], + queryKey: ticketOptionsQueryKeys.organizations(search), queryFn: () => fetchCustomerOptions(search), enabled, }); @@ -151,7 +167,7 @@ async function fetchAssigneeOptions(): Promise { export function useAssigneeOptions(enabled = true) { const query = useQuery({ - queryKey: ['ticket-options', 'assignees'], + queryKey: ticketOptionsQueryKeys.assignees(), queryFn: fetchAssigneeOptions, enabled, }); @@ -263,7 +279,7 @@ export function useTicketSearchOptions(search = '', organizationId?: string, ena ); const query = useQuery({ - queryKey: ['ticket-options', 'tickets', search, organizationId ?? null, nonArchivedStatusIds ?? null], + queryKey: ticketOptionsQueryKeys.tickets(search, organizationId ?? null, nonArchivedStatusIds ?? null), queryFn: () => fetchTicketSearchOptions(search, organizationId, nonArchivedStatusIds), enabled: enabled && !statusesQuery.isLoading, }); diff --git a/src/app/(app)/tickets/utils/optimistic-board.ts b/src/app/(app)/tickets/utils/optimistic-board.ts index e99c7298..47b2af68 100644 --- a/src/app/(app)/tickets/utils/optimistic-board.ts +++ b/src/app/(app)/tickets/utils/optimistic-board.ts @@ -16,8 +16,15 @@ export interface OptimisticMoveSnapshot { detail?: { key: QueryKey; data: Dialog | null | undefined }; } +const boardColumnsKeyPrefix = dialogsQueryKeys.boardColumns(); + function isBoardQueryKey(key: QueryKey): boolean { - return Array.isArray(key) && key.length >= 4 && key[0] === 'dialogs' && key[1] === 'boardColumn'; + return ( + Array.isArray(key) && + key.length >= 4 && + key[0] === boardColumnsKeyPrefix[0] && + key[1] === boardColumnsKeyPrefix[1] + ); } /**