withSavedSearches.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import {Component, useMemo} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import SavedSearchesStore from 'sentry/stores/savedSearchesStore';
  4. import {SavedSearch} from 'sentry/types';
  5. import getDisplayName from 'sentry/utils/getDisplayName';
  6. import useOrganization from 'sentry/utils/useOrganization';
  7. import {useFetchSavedSearchesForOrg} from 'sentry/views/issueList/queries/useFetchSavedSearchesForOrg';
  8. import {useSelectedSavedSearch} from 'sentry/views/issueList/utils/useSelectedSavedSearch';
  9. type InjectedSavedSearchesProps = {
  10. savedSearch: SavedSearch | null;
  11. savedSearchLoading: boolean;
  12. savedSearches: SavedSearch[];
  13. } & RouteComponentProps<{searchId?: string}, {}>;
  14. type State = {
  15. isLoading: boolean;
  16. savedSearches: SavedSearch[];
  17. };
  18. /**
  19. * HOC to provide saved search data to class components.
  20. * When possible, use the hooks directly instead.
  21. */
  22. function withSavedSearchesV2<P extends InjectedSavedSearchesProps>(
  23. WrappedComponent: React.ComponentType<P>
  24. ) {
  25. return (
  26. props: Omit<P, keyof InjectedSavedSearchesProps> & Partial<InjectedSavedSearchesProps>
  27. ) => {
  28. const organization = useOrganization();
  29. const {data: savedSearches, isLoading} = useFetchSavedSearchesForOrg({
  30. orgSlug: organization.slug,
  31. });
  32. const selectedSavedSearch = useSelectedSavedSearch();
  33. return (
  34. <WrappedComponent
  35. {...(props as P)}
  36. savedSearches={props.savedSearches ?? savedSearches}
  37. savedSearchLoading={props.savedSearchLoading ?? isLoading}
  38. savedSearch={props.savedSearch ?? selectedSavedSearch}
  39. />
  40. );
  41. };
  42. }
  43. /**
  44. * Wrap a component with saved issue search data from the store.
  45. */
  46. function withSavedSearchesV1<P extends InjectedSavedSearchesProps>(
  47. WrappedComponent: React.ComponentType<P>
  48. ) {
  49. class WithSavedSearches extends Component<
  50. Omit<P, keyof InjectedSavedSearchesProps> & Partial<InjectedSavedSearchesProps>,
  51. State
  52. > {
  53. static displayName = `withSavedSearches(${getDisplayName(WrappedComponent)})`;
  54. state = SavedSearchesStore.get();
  55. componentWillUnmount() {
  56. this.unsubscribe();
  57. }
  58. unsubscribe = SavedSearchesStore.listen(
  59. (searchesState: State) => this.onUpdate(searchesState),
  60. undefined
  61. );
  62. onUpdate(newState: State) {
  63. this.setState(newState);
  64. }
  65. render() {
  66. const {
  67. params,
  68. location,
  69. savedSearchLoading,
  70. savedSearch: savedSearchProp,
  71. savedSearches: savedSearchesProp,
  72. } = this.props as P;
  73. const {searchId} = params;
  74. const {savedSearches, isLoading} = this.state as State;
  75. let savedSearch: SavedSearch | null = null;
  76. // Switch to the current saved search or pinned result if available
  77. if (!isLoading && savedSearches) {
  78. if (searchId) {
  79. const match = savedSearches.find(search => search.id === searchId);
  80. savedSearch = match ? match : null;
  81. }
  82. // If there's no direct saved search being requested (via URL route)
  83. // *AND* there's no query in URL, then check if there is pinned search
  84. //
  85. // Note: Don't use pinned searches when there is an empty query (query === empty string)
  86. if (!savedSearch && typeof location.query.query === 'undefined') {
  87. const pin = savedSearches.find(search => search.isPinned);
  88. savedSearch = pin ? pin : null;
  89. }
  90. }
  91. return (
  92. <WrappedComponent
  93. {...(this.props as P)}
  94. savedSearches={savedSearchesProp ?? savedSearches}
  95. savedSearchLoading={savedSearchLoading ?? isLoading}
  96. savedSearch={savedSearchProp ?? savedSearch}
  97. />
  98. );
  99. }
  100. }
  101. return WithSavedSearches;
  102. }
  103. /**
  104. * Temporary wrapper that provides saved searches data from the store or react-query,
  105. * depending on the issue-list-saved-searches-v2 feature flag.
  106. */
  107. function withSavedSearches<P extends InjectedSavedSearchesProps>(
  108. WrappedComponent: React.ComponentType<P>
  109. ) {
  110. return (
  111. props: Omit<P, keyof InjectedSavedSearchesProps> & Partial<InjectedSavedSearchesProps>
  112. ) => {
  113. const organization = useOrganization();
  114. const WithSavedSearchesComponent = useMemo(() => {
  115. return organization.features.includes('issue-list-saved-searches-v2')
  116. ? withSavedSearchesV2(WrappedComponent)
  117. : withSavedSearchesV1(WrappedComponent);
  118. }, [organization]);
  119. return <WithSavedSearchesComponent {...props} />;
  120. };
  121. }
  122. export default withSavedSearches;