diff --git a/static/app/components/feedback/list/feedbackList.tsx b/static/app/components/feedback/list/feedbackList.tsx index d7500de50dd00f..71223b82554198 100644 --- a/static/app/components/feedback/list/feedbackList.tsx +++ b/static/app/components/feedback/list/feedbackList.tsx @@ -7,19 +7,20 @@ import waitingForEventImg from 'sentry-images/spot/waiting-for-event.svg'; import {Stack} from '@sentry/scraps/layout'; import {Tooltip} from '@sentry/scraps/tooltip'; -import type {ApiResult} from 'sentry/api'; import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {FeedbackListHeader} from 'sentry/components/feedback/list/feedbackListHeader'; import {FeedbackListItem} from 'sentry/components/feedback/list/feedbackListItem'; +import {useInfiniteFeedbackListQueryOptions} from 'sentry/components/feedback/useFeedbackListQueryOptions'; import {useFeedbackQueryKeys} from 'sentry/components/feedback/useFeedbackQueryKeys'; import {InfiniteListItems} from 'sentry/components/infiniteList/infiniteListItems'; import {InfiniteListState} from 'sentry/components/infiniteList/infiniteListState'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {t} from 'sentry/locale'; +import type {ApiResponse} from 'sentry/utils/api/apiFetch'; import type {FeedbackIssueListItem} from 'sentry/utils/feedback/types'; import {useListItemCheckboxContext} from 'sentry/utils/list/useListItemCheckboxState'; -import {useInfiniteApiQuery} from 'sentry/utils/queryClient'; -import type {InfiniteApiQueryKey} from 'sentry/utils/queryClient'; +import {useInfiniteQuery} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; function NoFeedback() { return ( @@ -36,25 +37,33 @@ interface Props { } export function FeedbackList({onItemSelect}: Props) { - const {listQueryKey} = useFeedbackQueryKeys(); - const queryResult = useInfiniteApiQuery({ - queryKey: - listQueryKey ?? - ([{infinite: true, version: 'v1'}, ''] as unknown as InfiniteApiQueryKey), - enabled: Boolean(listQueryKey), + const {listHeadTime} = useFeedbackQueryKeys(); + const organization = useOrganization(); + const listQueryOptions = useInfiniteFeedbackListQueryOptions({ + listHeadTime, + organization, + }); + const queryResult = useInfiniteQuery({ + ...listQueryOptions, + enabled: Boolean(listQueryOptions.queryKey), }); - // Deduplicated issues. In case one page overlaps with another. + // Can't use `select()` in useInfiniteQuery() because `` + // has it's own stuff going on, and that's a larger refactor for another time. const issues = useMemo( - () => uniqBy(queryResult.data?.pages.flatMap(result => result[0]) ?? [], 'id'), + () => + uniqBy( + queryResult.data?.pages.flatMap(result => result.json ?? []), + 'id' + ), [queryResult.data?.pages] ); + const hits = queryResult.data?.pages[0]?.headers['X-Hits'] ?? issues.length; + const checkboxState = useListItemCheckboxContext({ - hits: Number( - queryResult.data?.pages[0]?.[2]?.getResponseHeader('X-Hits') ?? issues.length - ), + hits, knownIds: issues.map(issue => issue.id), - queryKey: listQueryKey, + queryKey: listQueryOptions.queryKey, }); return ( @@ -66,10 +75,10 @@ export function FeedbackList({onItemSelect}: Props) { backgroundUpdatingMessage={() => null} loadingMessage={() => } > - > + > deduplicateItems={pages => uniqBy( - pages.flatMap(page => page[0]), + pages.flatMap(page => page.json ?? []), 'id' ) } diff --git a/static/app/components/feedback/list/feedbackListHeader.tsx b/static/app/components/feedback/list/feedbackListHeader.tsx index e3d7dc462a6856..254102451b21c5 100644 --- a/static/app/components/feedback/list/feedbackListHeader.tsx +++ b/static/app/components/feedback/list/feedbackListHeader.tsx @@ -34,8 +34,8 @@ export function FeedbackListHeader({ }: Props) { const [mailbox, setMailbox] = useMailbox(); - const {listPrefetchQueryKey, resetListHeadTime} = useFeedbackQueryKeys(); - const hasNewItems = useFeedbackHasNewItems({listPrefetchQueryKey}); + const {listHeadTime, resetListHeadTime} = useFeedbackQueryKeys(); + const hasNewItems = useFeedbackHasNewItems({listHeadTime}); const {invalidateListCache} = useFeedbackCache(); return ( diff --git a/static/app/components/feedback/list/useMailboxCounts.tsx b/static/app/components/feedback/list/useMailboxCounts.tsx index 3d6c2800d9d61f..b6e070c3bbe0d4 100644 --- a/static/app/components/feedback/list/useMailboxCounts.tsx +++ b/static/app/components/feedback/list/useMailboxCounts.tsx @@ -5,10 +5,11 @@ import type {Organization} from 'sentry/types/organization'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {coaleseIssueStatsPeriodQuery} from 'sentry/utils/feedback/coaleseIssueStatsPeriodQuery'; import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient'; -import {decodeList, decodeScalar} from 'sentry/utils/queryString'; import type {RequestError} from 'sentry/utils/requestError/requestError'; -import {useLocationQuery} from 'sentry/utils/url/useLocationQuery'; -import {useLocation} from 'sentry/utils/useLocation'; +import { + useListQueryState, + useSearchQueryState, +} from 'sentry/utils/url/useSentryQueryState'; interface Props { organization: Organization; @@ -27,43 +28,39 @@ type HookReturnType = { export function useMailboxCounts({ organization, }: Props): UseApiQueryResult { - const location = useLocation(); - const locationQuery = decodeScalar(location.query.query, ''); const {listHeadTime} = useFeedbackQueryKeys(); - // We should fetch the counts while taking the query into account - const MAILBOX: Record = { - unresolved: 'issue.category:feedback is:unassigned is:unresolved ' + locationQuery, - resolved: 'issue.category:feedback is:unassigned is:resolved ' + locationQuery, - ignored: 'issue.category:feedback is:unassigned is:ignored ' + locationQuery, - }; + const listQueryState = useListQueryState(); + const searchQueryState = useSearchQueryState(); - const mailboxQuery = Object.values(MAILBOX); + const MAILBOX = useMemo( + () => ({ + unresolved: + 'issue.category:feedback is:unassigned is:unresolved ' + searchQueryState.query, + resolved: + 'issue.category:feedback is:unassigned is:resolved ' + searchQueryState.query, + ignored: + 'issue.category:feedback is:unassigned is:ignored ' + searchQueryState.query, + }), + [searchQueryState.query] + ); - const queryView = useLocationQuery({ - fields: { - end: decodeScalar, - environment: decodeList, - field: decodeList, - project: decodeList, - query: mailboxQuery, - queryReferrer: 'feedback_mailbox_count', - start: decodeScalar, - statsPeriod: decodeScalar, - utc: decodeScalar, - }, - }); + const queryViewWithStatsPeriod = useMemo(() => { + // We should fetch the counts while taking the query into account + const mailboxQuery = Object.values(MAILBOX); - const queryViewWithStatsPeriod = useMemo( - () => - coaleseIssueStatsPeriodQuery({ - defaultStatsPeriod: '0d', + return { + ...listQueryState, + ...searchQueryState, + queryReferrer: 'feedback_mailbox_count', + query: mailboxQuery, + ...coaleseIssueStatsPeriodQuery({ listHeadTime, prefetch: false, - queryView, + statsPeriod: listQueryState.statsPeriod, }), - [listHeadTime, queryView] - ); + }; + }, [listHeadTime, listQueryState, searchQueryState, MAILBOX]); const result = useApiQuery( [ diff --git a/static/app/components/feedback/list/useRefetchFeedbackList.tsx b/static/app/components/feedback/list/useRefetchFeedbackList.tsx index 5ae9af0df96ab8..3751048de7cedb 100644 --- a/static/app/components/feedback/list/useRefetchFeedbackList.tsx +++ b/static/app/components/feedback/list/useRefetchFeedbackList.tsx @@ -1,19 +1,26 @@ import {useCallback} from 'react'; import {useFeedbackCache} from 'sentry/components/feedback/useFeedbackCache'; +import {useInfiniteFeedbackListQueryOptions} from 'sentry/components/feedback/useFeedbackListQueryOptions'; import {useFeedbackQueryKeys} from 'sentry/components/feedback/useFeedbackQueryKeys'; import {useQueryClient} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; export function useRefetchFeedbackList() { const queryClient = useQueryClient(); - const {listQueryKey, resetListHeadTime} = useFeedbackQueryKeys(); + const organization = useOrganization(); + const {listHeadTime, resetListHeadTime} = useFeedbackQueryKeys(); + const listQueryOptions = useInfiniteFeedbackListQueryOptions({ + listHeadTime, + organization, + }); const {invalidateListCache} = useFeedbackCache(); const refetchFeedbackList = useCallback(() => { - queryClient.invalidateQueries({queryKey: listQueryKey}); + queryClient.invalidateQueries({queryKey: listQueryOptions.queryKey}); resetListHeadTime(); invalidateListCache(); - }, [queryClient, listQueryKey, resetListHeadTime, invalidateListCache]); + }, [queryClient, listQueryOptions.queryKey, resetListHeadTime, invalidateListCache]); return {refetchFeedbackList}; } diff --git a/static/app/components/feedback/useFeedbackCache.tsx b/static/app/components/feedback/useFeedbackCache.tsx index d01563fdc888fb..5f76225128db3c 100644 --- a/static/app/components/feedback/useFeedbackCache.tsx +++ b/static/app/components/feedback/useFeedbackCache.tsx @@ -1,17 +1,19 @@ import {useCallback} from 'react'; -import type {ApiResult} from 'sentry/api'; +import {useInfiniteFeedbackListQueryOptions} from 'sentry/components/feedback/useFeedbackListQueryOptions'; import {useFeedbackQueryKeys} from 'sentry/components/feedback/useFeedbackQueryKeys'; import {defined} from 'sentry/utils'; +import type {ApiResponse} from 'sentry/utils/api/apiFetch'; import type {FeedbackIssue, FeedbackIssueListItem} from 'sentry/utils/feedback/types'; -import type {ApiQueryKey, InfiniteData, QueryState} from 'sentry/utils/queryClient'; +import type {ApiQueryKey, InfiniteData} from 'sentry/utils/queryClient'; import {setApiQueryData, useQueryClient} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; type TFeedbackIds = 'all' | string[]; type ListCache = { pageParams: unknown[]; - pages: Array>; + pages: Array>; }; const issueApiEndpointRegexp = /^\/organizations\/\w+\/issues\/\d+\/$/; @@ -22,7 +24,13 @@ function isIssueEndpointUrl(query: any) { export function useFeedbackCache() { const queryClient = useQueryClient(); - const {getItemQueryKeys, listQueryKey} = useFeedbackQueryKeys(); + const {getItemQueryKeys, listHeadTime} = useFeedbackQueryKeys(); + const organization = useOrganization(); + const listQueryOptions = useInfiniteFeedbackListQueryOptions({ + listHeadTime, + organization, + }); + const listQueryKey = listQueryOptions.queryKey; const updateCachedQueryKey = useCallback( (queryKey: ApiQueryKey, payload: Partial) => { @@ -58,13 +66,12 @@ export function useFeedbackCache() { } const listData = queryClient.getQueryData(listQueryKey); if (listData) { - const pages = listData.pages.map(([data, statusText, resp]) => [ - data.map(item => + const pages = listData.pages.map(({json, headers}) => ({ + json: json.map(item => ids === 'all' || ids.includes(item.id) ? {...item, ...payload} : item ), - statusText, - resp, - ]); + headers, + })); queryClient.setQueryData(listQueryKey, {...listData, pages}); } }, @@ -108,11 +115,11 @@ export function useFeedbackCache() { queryClient.refetchQueries({ queryKey: listQueryKey, predicate: query => { - // Check if any of the pages contain the items we want to invalidate + const data = query.state.data as + | InfiniteData> + | undefined; return Boolean( - ( - query.state.data as QueryState> - ).data?.pages.some(items => items.some(item => ids.includes(item.id))) + data?.pages.some(page => page.json.some(item => ids.includes(item.id))) ); }, }); diff --git a/static/app/components/feedback/useFeedbackHasNewItems.tsx b/static/app/components/feedback/useFeedbackHasNewItems.tsx index bbd9373e94743f..14e458df8dff1f 100644 --- a/static/app/components/feedback/useFeedbackHasNewItems.tsx +++ b/static/app/components/feedback/useFeedbackHasNewItems.tsx @@ -1,25 +1,33 @@ import {useEffect, useState} from 'react'; +import {useQuery} from '@tanstack/react-query'; -import type {ApiQueryKey} from 'sentry/utils/queryClient'; -import {useApiQuery} from 'sentry/utils/queryClient'; +import {parseQueryKey} from 'sentry/utils/api/apiQueryKey'; +import {useOrganization} from 'sentry/utils/useOrganization'; + +import {usePrefetchFeedbackListQueryOptions} from './useFeedbackListQueryOptions'; interface Props { - listPrefetchQueryKey: ApiQueryKey | undefined; + listHeadTime: number; } const POLLING_INTERVAL_MS = 10_000; -export function useFeedbackHasNewItems({listPrefetchQueryKey}: Props) { +export function useFeedbackHasNewItems({listHeadTime}: Props) { + const organization = useOrganization(); + const listPrefetchQueryOptions = usePrefetchFeedbackListQueryOptions({ + listHeadTime, + organization, + }); + const [foundData, setFoundData] = useState(false); - const {data} = useApiQuery( - listPrefetchQueryKey ?? ([''] as unknown as ApiQueryKey), - { - refetchInterval: POLLING_INTERVAL_MS, - staleTime: 0, - enabled: Boolean(listPrefetchQueryKey) && !foundData, - } - ); + const {statsPeriod} = + parseQueryKey(listPrefetchQueryOptions.queryKey).options?.query ?? {}; + const {data} = useQuery({ + ...listPrefetchQueryOptions, + refetchInterval: POLLING_INTERVAL_MS, + enabled: statsPeriod && !foundData, + }); useEffect(() => { // Once we found something, no need to keep polling. @@ -29,7 +37,7 @@ export function useFeedbackHasNewItems({listPrefetchQueryKey}: Props) { useEffect(() => { // New key, start polling again setFoundData(false); - }, [listPrefetchQueryKey]); + }, [listHeadTime]); return Boolean(data?.length); } diff --git a/static/app/components/feedback/useFeedbackListQueryKey.tsx b/static/app/components/feedback/useFeedbackListQueryKey.tsx deleted file mode 100644 index 7e40ff7aaabb72..00000000000000 --- a/static/app/components/feedback/useFeedbackListQueryKey.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import {useMemo} from 'react'; - -import {useMailbox} from 'sentry/components/feedback/useMailbox'; -import type {Organization} from 'sentry/types/organization'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {coaleseIssueStatsPeriodQuery} from 'sentry/utils/feedback/coaleseIssueStatsPeriodQuery'; -import type {ApiQueryKey} from 'sentry/utils/queryClient'; -import {decodeList, decodeScalar} from 'sentry/utils/queryString'; -import {useLocationQuery} from 'sentry/utils/url/useLocationQuery'; - -interface Props { - listHeadTime: number; - organization: Organization; - prefetch: boolean; -} - -const PER_PAGE = 25; - -export function useFeedbackListQueryKey({ - listHeadTime, - organization, - prefetch, -}: Props): ApiQueryKey | undefined { - const [mailbox] = useMailbox(); - const queryView = useLocationQuery({ - fields: { - limit: PER_PAGE, - queryReferrer: 'feedback_list_page', - end: decodeScalar, - environment: decodeList, - field: decodeList, - project: decodeList, - query: decodeScalar, - start: decodeScalar, - statsPeriod: decodeScalar, - utc: decodeScalar, - }, - }); - - const fixedQueryView = useMemo( - () => - coaleseIssueStatsPeriodQuery({ - defaultStatsPeriod: '0d', - listHeadTime, - prefetch, - queryView, - }), - [listHeadTime, prefetch, queryView] - ); - - return useMemo(() => { - if (!fixedQueryView) { - return undefined; - } - return [ - getApiUrl('/organizations/$organizationIdOrSlug/issues/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - ...fixedQueryView, - expand: prefetch - ? [] - : [ - 'pluginActions', // Gives us plugin actions available - 'pluginIssues', // Gives us plugin issues available - 'integrationIssues', // Gives us integration issues available - 'sentryAppIssues', // Gives us Sentry app issues available - 'latestEventHasAttachments', // Gives us whether the feedback has screenshots - ], - shortIdLookup: 0, - query: `issue.category:feedback status:${mailbox} ${fixedQueryView.query}`, - }, - }, - ]; - }, [organization.slug, prefetch, fixedQueryView, mailbox]); -} diff --git a/static/app/components/feedback/useFeedbackListQueryOptions.tsx b/static/app/components/feedback/useFeedbackListQueryOptions.tsx new file mode 100644 index 00000000000000..1244a01ee0297b --- /dev/null +++ b/static/app/components/feedback/useFeedbackListQueryOptions.tsx @@ -0,0 +1,84 @@ +import {useMailbox} from 'sentry/components/feedback/useMailbox'; +import type {Organization} from 'sentry/types/organization'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; +import {coaleseIssueStatsPeriodQuery} from 'sentry/utils/feedback/coaleseIssueStatsPeriodQuery'; +import type {FeedbackIssueListItem} from 'sentry/utils/feedback/types'; +import { + useListQueryState, + useSearchQueryState, +} from 'sentry/utils/url/useSentryQueryState'; + +interface Props { + listHeadTime: number; + organization: Organization; +} + +const PER_PAGE = 25; + +export function usePrefetchFeedbackListQueryOptions({listHeadTime, organization}: Props) { + const [mailbox] = useMailbox(); + + const listQueryState = useListQueryState(); + const searchQueryState = useSearchQueryState(); + + const query = { + ...listQueryState, + ...searchQueryState, + queryReferrer: 'feedback_list_page' as const, + ...coaleseIssueStatsPeriodQuery({ + listHeadTime, + prefetch: true, + statsPeriod: listQueryState.statsPeriod, + }), + expand: [], + collapse: ['stats', 'unhandled'], + shortIdLookup: 0, + query: `issue.category:feedback status:${mailbox} ${searchQueryState.query}`, + }; + + return apiOptions.as()( + '/organizations/$organizationIdOrSlug/issues/', + { + path: {organizationIdOrSlug: organization.slug}, + staleTime: 0, + query, + } + ); +} + +export function useInfiniteFeedbackListQueryOptions({listHeadTime, organization}: Props) { + const [mailbox] = useMailbox(); + + const listQueryState = useListQueryState(); + const searchQueryState = useSearchQueryState(); + + const query = { + ...listQueryState, + ...searchQueryState, + limit: PER_PAGE, + queryReferrer: 'feedback_list_page' as const, + ...coaleseIssueStatsPeriodQuery({ + listHeadTime, + prefetch: false, + statsPeriod: listQueryState.statsPeriod, + }), + expand: [ + 'pluginActions', // Gives us plugin actions available + 'pluginIssues', // Gives us plugin issues available + 'integrationIssues', // Gives us integration issues available + 'sentryAppIssues', // Gives us Sentry app issues available + 'latestEventHasAttachments', // Gives us whether the feedback has screenshots + ], + shortIdLookup: 0, + query: `issue.category:feedback status:${mailbox} ${searchQueryState.query}`, + }; + + return apiOptions.asInfinite()( + '/organizations/$organizationIdOrSlug/issues/', + { + path: {organizationIdOrSlug: organization.slug}, + staleTime: 0, + query, + } + ); +} diff --git a/static/app/components/feedback/useFeedbackQueryKeys.tsx b/static/app/components/feedback/useFeedbackQueryKeys.tsx index 576abe6b808f04..f79fc2d0ede8b0 100644 --- a/static/app/components/feedback/useFeedbackQueryKeys.tsx +++ b/static/app/components/feedback/useFeedbackQueryKeys.tsx @@ -2,10 +2,8 @@ import type {ReactNode} from 'react'; import {createContext, useCallback, useContext, useRef, useState} from 'react'; import {getFeedbackItemQueryKey} from 'sentry/components/feedback/getFeedbackItemQueryKey'; -import {useFeedbackListQueryKey} from 'sentry/components/feedback/useFeedbackListQueryKey'; import type {Organization} from 'sentry/types/organization'; -import {parseQueryKey} from 'sentry/utils/api/apiQueryKey'; -import type {ApiQueryKey, InfiniteApiQueryKey} from 'sentry/utils/queryClient'; +import type {ApiQueryKey} from 'sentry/utils/queryClient'; interface Props { children: ReactNode; @@ -20,8 +18,6 @@ type ItemQueryKeys = { interface TContext { getItemQueryKeys: (id: string) => ItemQueryKeys; listHeadTime: number; - listPrefetchQueryKey: ApiQueryKey | undefined; - listQueryKey: InfiniteApiQueryKey | undefined; resetListHeadTime: () => void; } @@ -30,8 +26,6 @@ const EMPTY_ITEM_QUERY_KEYS = {issueQueryKey: undefined, eventQueryKey: undefine const DEFAULT_CONTEXT: TContext = { getItemQueryKeys: () => EMPTY_ITEM_QUERY_KEYS, listHeadTime: 0, - listPrefetchQueryKey: undefined, - listQueryKey: undefined, resetListHeadTime: () => undefined, }; @@ -61,25 +55,11 @@ export function FeedbackQueryKeys({children, organization}: Props) { [organization] ); - const listQueryKey = useFeedbackListQueryKey({ - listHeadTime, - organization, - prefetch: false, - }); - - const listPrefetchQueryKey = useFeedbackListQueryKey({ - listHeadTime, - organization, - prefetch: true, - }); - return ( @@ -88,14 +68,6 @@ export function FeedbackQueryKeys({children, organization}: Props) { ); } -function getInfiniteListQueryKey(listQueryKey: ApiQueryKey | undefined) { - if (!listQueryKey) { - return undefined; - } - const {url, options} = parseQueryKey(listQueryKey); - return [{infinite: true, version: 'v1'}, url, options] as InfiniteApiQueryKey; -} - export function useFeedbackQueryKeys() { return useContext(FeedbackQueryKeysProvider); } diff --git a/static/app/components/feedback/useMutateFeedback.tsx b/static/app/components/feedback/useMutateFeedback.tsx index e77878ed3948c0..7944c78151ff7e 100644 --- a/static/app/components/feedback/useMutateFeedback.tsx +++ b/static/app/components/feedback/useMutateFeedback.tsx @@ -1,6 +1,7 @@ import {useCallback} from 'react'; import {useFeedbackCache} from 'sentry/components/feedback/useFeedbackCache'; +import {useInfiniteFeedbackListQueryOptions} from 'sentry/components/feedback/useFeedbackListQueryOptions'; import {useFeedbackQueryKeys} from 'sentry/components/feedback/useFeedbackQueryKeys'; import type {Actor} from 'sentry/types/core'; import type {GroupStatus} from 'sentry/types/group'; @@ -26,7 +27,11 @@ interface Props { } export function useMutateFeedback({feedbackIds, organization, projectIds}: Props) { - const {listQueryKey} = useFeedbackQueryKeys(); + const {listHeadTime} = useFeedbackQueryKeys(); + const listQueryOptions = useInfiniteFeedbackListQueryOptions({ + listHeadTime, + organization, + }); const {updateCached, invalidateCached} = useFeedbackCache(); const {mutate} = useMutation({ @@ -43,8 +48,8 @@ export function useMutateFeedback({feedbackIds, organization, projectIds}: Props // as `GET /issues/` when query params are set. IE: it should expand inbox & owners // Then we could push new data into the cache instead of re-fetching it again - const listQueryKeyOptions = listQueryKey - ? (parseQueryKey(listQueryKey).options ?? {}) + const listQueryKeyOptions = listQueryOptions.queryKey + ? (parseQueryKey(listQueryOptions.queryKey).options ?? {}) : {}; const options = isSingleId ? {} diff --git a/static/app/utils/feedback/coaleseIssueStatsPeriodQuery.spec.tsx b/static/app/utils/feedback/coaleseIssueStatsPeriodQuery.spec.tsx index 6c934ef1c02730..23af8073a1969a 100644 --- a/static/app/utils/feedback/coaleseIssueStatsPeriodQuery.spec.tsx +++ b/static/app/utils/feedback/coaleseIssueStatsPeriodQuery.spec.tsx @@ -7,7 +7,7 @@ describe('coaleseIssueStatsPeriodQuery', () => { it('should convert a statsPeriod into start+end fields', () => { const result = coaleseIssueStatsPeriodQuery({ listHeadTime: Oct31.getTime(), - queryView: {statsPeriod: '14d'}, + statsPeriod: '14d', }); expect(result).toEqual({ start: '2024-10-17T00:00:00.000Z', // Oct 18, 14 days earlier @@ -18,7 +18,7 @@ describe('coaleseIssueStatsPeriodQuery', () => { it('should default to 0d when statsPeriod is missing', () => { const result = coaleseIssueStatsPeriodQuery({ listHeadTime: Oct31.getTime(), - queryView: {statsPeriod: ''}, + statsPeriod: '', }); expect(result).toEqual({}); }); @@ -26,7 +26,7 @@ describe('coaleseIssueStatsPeriodQuery', () => { it('should ignore statsPeriod and start+end fields that have 1 day between them when prefetch is true', () => { const result = coaleseIssueStatsPeriodQuery({ listHeadTime: Jan1st.getTime(), - queryView: {statsPeriod: '14d'}, + statsPeriod: '14d', prefetch: true, }); expect(result).toEqual({ @@ -39,7 +39,7 @@ describe('coaleseIssueStatsPeriodQuery', () => { it('should undefined when there is no statsPeriod and prefetch is true', () => { const result = coaleseIssueStatsPeriodQuery({ listHeadTime: Jan1st.getTime(), - queryView: {statsPeriod: ''}, + statsPeriod: '', prefetch: true, }); expect(result).toBeUndefined(); diff --git a/static/app/utils/feedback/coaleseIssueStatsPeriodQuery.tsx b/static/app/utils/feedback/coaleseIssueStatsPeriodQuery.tsx index 2fd5e578fbc4c6..acb00e7593d256 100644 --- a/static/app/utils/feedback/coaleseIssueStatsPeriodQuery.tsx +++ b/static/app/utils/feedback/coaleseIssueStatsPeriodQuery.tsx @@ -2,19 +2,17 @@ import {intervalToMilliseconds} from 'sentry/utils/duration/intervalToMillisecon const ONE_DAY_MS = intervalToMilliseconds('1d'); -interface Props { +interface Props { listHeadTime: number; - queryView: QueryView; - defaultStatsPeriod?: string; + statsPeriod: string | null; prefetch?: boolean; } -export function coaleseIssueStatsPeriodQuery({ - queryView, +export function coaleseIssueStatsPeriodQuery({ + statsPeriod = '0d', listHeadTime, - defaultStatsPeriod, prefetch = false, -}: Props) { +}: Props) { // We don't want to use `statsPeriod` directly, because that will mean the // start time of our infinite list will change, shifting the index/page // where items appear if we invalidate the cache and refetch specific pages. @@ -31,7 +29,6 @@ export function coaleseIssueStatsPeriodQuery; +} + +/** + * Provides typical query state for list pages. + * + * You can add your own extra or static state + */ +export function useListQueryState({staticState}: Props = {}) { + const {selection} = usePageFilters(); + const queryState = useMemo( + () => ({ + end: selection.datetime.end, + environment: selection.environments, + project: selection.projects, + start: selection.datetime.start, + statsPeriod: selection.datetime.period, + utc: selection.datetime.utc, + ...staticState, + }), + [selection, staticState] + ); + + return queryState; +} + +export function useSearchQueryState() { + const [queryParams] = useQueryStates({ + field: parseAsArrayOf(parseAsString).withDefault([]), + query: parseAsString.withDefault(''), + }); + return queryParams; +} diff --git a/static/app/views/alerts/create.tsx b/static/app/views/alerts/create.tsx index 206b4556c42008..12f2cbe59aed89 100644 --- a/static/app/views/alerts/create.tsx +++ b/static/app/views/alerts/create.tsx @@ -78,7 +78,7 @@ export default function Create() { !(aggregate && dataset && eventTypes) && !createFromDuplicate ) { - router.replace( + navigate( normalizeUrl({ ...location, pathname: makeAlertsPathname({ @@ -90,7 +90,8 @@ export default function Create() { ...DEFAULT_WIZARD_TEMPLATE, project: project.slug, }, - }) + }), + {replace: true} ); } }, [ @@ -99,7 +100,7 @@ export default function Create() { dataset, eventTypes, createFromDuplicate, - router, + navigate, location, organization.slug, project.slug, diff --git a/static/app/views/alerts/rules/metric/create.tsx b/static/app/views/alerts/rules/metric/create.tsx index 87d3077f0ac7dd..c0c5c80450ba5d 100644 --- a/static/app/views/alerts/rules/metric/create.tsx +++ b/static/app/views/alerts/rules/metric/create.tsx @@ -7,6 +7,7 @@ import {metric} from 'sentry/utils/analytics'; import type {EventView} from 'sentry/utils/discover/eventView'; import {decodeScalar} from 'sentry/utils/queryString'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; +import {useNavigate} from 'sentry/utils/useNavigate'; import {makeAlertsPathname} from 'sentry/views/alerts/pathnames'; import { createDefaultRule, @@ -37,8 +38,9 @@ type Props = { */ export function MetricRulesCreate(props: Props) { const theme = useTheme(); + const navigate = useNavigate(); function handleSubmitSuccess(data: any) { - const {organization, project, router} = props; + const {organization, project} = props; const alertRuleId = data ? (data.id as string | undefined) : undefined; metric.endSpan({name: 'saveAlertRule'}); @@ -56,7 +58,7 @@ export function MetricRulesCreate(props: Props) { }), query: {project: project.id}, }; - router.push(normalizeUrl(target)); + navigate(normalizeUrl(target)); } const { diff --git a/static/app/views/alerts/rules/metric/duplicate.tsx b/static/app/views/alerts/rules/metric/duplicate.tsx index f66f1d8aba52ca..1813bd4fc0c140 100644 --- a/static/app/views/alerts/rules/metric/duplicate.tsx +++ b/static/app/views/alerts/rules/metric/duplicate.tsx @@ -9,6 +9,7 @@ import type {Project} from 'sentry/types/project'; import type {EventView} from 'sentry/utils/discover/eventView'; import {uniqueId} from 'sentry/utils/guid'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; +import {useNavigate} from 'sentry/utils/useNavigate'; import {useOrganization} from 'sentry/utils/useOrganization'; import {makeAlertsPathname} from 'sentry/views/alerts/pathnames'; import { @@ -40,6 +41,7 @@ export function MetricRuleDuplicate({ ...otherProps }: MetricRuleDuplicateProps) { const theme = useTheme(); + const navigate = useNavigate(); const organization = useOrganization(); const duplicateRuleId: string = otherProps.location.query.duplicateRuleId; const { @@ -68,7 +70,7 @@ export function MetricRuleDuplicate({ }), query: {project: project.id}, }; - otherProps.router.push(normalizeUrl(target)); + navigate(normalizeUrl(target)); }; if (isPending) {