actions.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. // eslint-disable-next-line no-restricted-imports
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import {pinSearch, unpinSearch} from 'sentry/actionCreators/savedSearches';
  6. import Button from 'sentry/components/button';
  7. import {MenuItemProps} from 'sentry/components/dropdownMenuItem';
  8. import CreateSavedSearchModal from 'sentry/components/modals/createSavedSearchModal';
  9. import {IconAdd, IconPin} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {SavedSearch, SavedSearchType} from 'sentry/types';
  12. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  13. import {useLocation} from 'sentry/utils/useLocation';
  14. import type {ActionBarItem, ActionProps} from './index';
  15. import {removeSpace} from './utils';
  16. type PinSearchActionOpts = {
  17. location: ReturnType<typeof useLocation>;
  18. /**
  19. * The current issue sort
  20. */
  21. sort: string;
  22. /**
  23. * The currently pinned search
  24. */
  25. pinnedSearch?: SavedSearch;
  26. };
  27. /**
  28. * The Pin Search action toggles the current as a pinned search
  29. */
  30. export function makePinSearchAction({
  31. pinnedSearch,
  32. sort,
  33. location,
  34. }: PinSearchActionOpts): ActionBarItem {
  35. const makeAction = ({api, organization, query, savedSearchType}: ActionProps) => {
  36. const onTogglePinnedSearch = async () => {
  37. if (savedSearchType === undefined) {
  38. return;
  39. }
  40. const {cursor: _cursor, page: _page, ...currentQuery} = location.query;
  41. trackAdvancedAnalyticsEvent('search.pin', {
  42. organization,
  43. action: pinnedSearch ? 'unpin' : 'pin',
  44. search_type: savedSearchType === SavedSearchType.ISSUE ? 'issues' : 'events',
  45. query: pinnedSearch?.query ?? query,
  46. });
  47. if (pinnedSearch) {
  48. unpinSearch(api, organization.slug, savedSearchType, pinnedSearch).then(() => {
  49. browserHistory.push({
  50. ...location,
  51. pathname: `/organizations/${organization.slug}/issues/`,
  52. query: {
  53. referrer: 'search-bar',
  54. ...currentQuery,
  55. query: pinnedSearch.query,
  56. sort: pinnedSearch.sort,
  57. },
  58. });
  59. });
  60. return;
  61. }
  62. const resp = await pinSearch(
  63. api,
  64. organization.slug,
  65. savedSearchType,
  66. removeSpace(query),
  67. sort
  68. );
  69. if (!resp || !resp.id) {
  70. return;
  71. }
  72. browserHistory.push({
  73. ...location,
  74. pathname: `/organizations/${organization.slug}/issues/searches/${resp.id}/`,
  75. query: {referrer: 'search-bar', ...currentQuery},
  76. });
  77. };
  78. const pinSearchMenuItem: MenuItemProps = {
  79. onAction: onTogglePinnedSearch,
  80. label: pinnedSearch ? t('Unpin Search') : t('Pin Search'),
  81. key: 'pinSearch',
  82. };
  83. const PinSearchActionButton = () => {
  84. const pinTooltip = pinnedSearch ? t('Unpin this search') : t('Pin this search');
  85. return (
  86. <ActionButton
  87. title={pinTooltip}
  88. disabled={!query}
  89. aria-label={pinTooltip}
  90. onClick={e => {
  91. e.preventDefault();
  92. e.stopPropagation();
  93. onTogglePinnedSearch();
  94. }}
  95. isActive={!!pinnedSearch}
  96. data-test-id="pin-icon"
  97. icon={<IconPin isSolid={!!pinnedSearch} size="xs" />}
  98. />
  99. );
  100. };
  101. return {Button: PinSearchActionButton, menuItem: pinSearchMenuItem};
  102. };
  103. return {key: 'pinSearch', makeAction};
  104. }
  105. type SaveSearchActionOpts = {
  106. disabled: boolean;
  107. /**
  108. * The current issue sort
  109. */
  110. sort: string;
  111. };
  112. /**
  113. * The Save Search action triggers the create saved search modal from the
  114. * current query.
  115. */
  116. export function makeSaveSearchAction({
  117. sort,
  118. disabled,
  119. }: SaveSearchActionOpts): ActionBarItem {
  120. const makeAction = ({query, organization}: ActionProps) => {
  121. const onSaveSearch = () =>
  122. openModal(deps => (
  123. <CreateSavedSearchModal {...deps} {...{organization, query, sort}} />
  124. ));
  125. const title = disabled
  126. ? t('You do not have permission to create a saved search')
  127. : t('Add to organization saved searches');
  128. const menuItem: MenuItemProps = {
  129. disabled,
  130. onAction: onSaveSearch,
  131. label: t('Create Saved Search'),
  132. key: 'saveSearch',
  133. details: disabled ? title : undefined,
  134. };
  135. const SaveSearchActionButton = () => (
  136. <ActionButton
  137. onClick={onSaveSearch}
  138. disabled={disabled}
  139. icon={<IconAdd size="xs" />}
  140. title={title}
  141. aria-label={title}
  142. data-test-id="save-current-search"
  143. />
  144. );
  145. return {Button: SaveSearchActionButton, menuItem};
  146. };
  147. return {key: 'saveSearch', makeAction};
  148. }
  149. export const ActionButton = styled(Button)<{isActive?: boolean}>`
  150. color: ${p => (p.isActive ? p.theme.blue300 : p.theme.gray300)};
  151. width: 18px;
  152. height: 18px;
  153. padding: 2px;
  154. min-height: auto;
  155. &,
  156. &:hover,
  157. &:focus {
  158. background: transparent;
  159. }
  160. &:hover {
  161. color: ${p => p.theme.gray400};
  162. }
  163. `;
  164. ActionButton.defaultProps = {
  165. type: 'button',
  166. borderless: true,
  167. size: 'zero',
  168. };