feedbackListHeader.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import {Fragment, ReactNode} from 'react';
  2. import styled from '@emotion/styled';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import {ModalRenderProps, openModal} from 'sentry/actionCreators/modal';
  9. import Button from 'sentry/components/actions/button';
  10. import ButtonBar from 'sentry/components/buttonBar';
  11. import Checkbox from 'sentry/components/checkbox';
  12. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  13. import ErrorBoundary from 'sentry/components/errorBoundary';
  14. import decodeMailbox from 'sentry/components/feedback/decodeMailbox';
  15. import MailboxPicker from 'sentry/components/feedback/list/mailboxPicker';
  16. import type useListItemCheckboxState from 'sentry/components/feedback/list/useListItemCheckboxState';
  17. import useMutateFeedback from 'sentry/components/feedback/useMutateFeedback';
  18. import PanelItem from 'sentry/components/panels/panelItem';
  19. import {Flex} from 'sentry/components/profiling/flex';
  20. import {IconEllipsis} from 'sentry/icons/iconEllipsis';
  21. import {t, tct} from 'sentry/locale';
  22. import {space} from 'sentry/styles/space';
  23. import {GroupStatus} from 'sentry/types';
  24. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. import useUrlParams from 'sentry/utils/useUrlParams';
  27. function openConfirmModal({
  28. onConfirm,
  29. body,
  30. footerConfirm,
  31. }: {
  32. body: ReactNode;
  33. footerConfirm: ReactNode;
  34. onConfirm: () => void | Promise<void>;
  35. }) {
  36. openModal(({Body, Footer, closeModal}: ModalRenderProps) => (
  37. <Fragment>
  38. <Body>{body}</Body>
  39. <Footer>
  40. <Flex gap={space(1)}>
  41. <ButtonBar gap={1}>
  42. <Button onClick={closeModal}>{t('Cancel')}</Button>
  43. <Button
  44. priority="primary"
  45. onClick={() => {
  46. closeModal();
  47. onConfirm();
  48. }}
  49. >
  50. {footerConfirm}
  51. </Button>
  52. </ButtonBar>
  53. </Flex>
  54. </Footer>
  55. </Fragment>
  56. ));
  57. }
  58. interface Props
  59. extends Pick<
  60. ReturnType<typeof useListItemCheckboxState>,
  61. | 'countSelected'
  62. | 'deselectAll'
  63. | 'isAllSelected'
  64. | 'isAnySelected'
  65. | 'selectAll'
  66. | 'selectedIds'
  67. > {}
  68. const statusToText: Record<string, string> = {
  69. resolved: 'Resolve',
  70. unresolved: 'Unresolve',
  71. };
  72. export default function FeedbackListHeader({
  73. countSelected,
  74. deselectAll,
  75. isAllSelected,
  76. isAnySelected,
  77. selectAll,
  78. selectedIds,
  79. }: Props) {
  80. const {mailbox} = useLocationQuery({
  81. fields: {
  82. mailbox: decodeMailbox,
  83. },
  84. });
  85. const {setParamValue: setMailbox} = useUrlParams('mailbox');
  86. return (
  87. <HeaderPanelItem>
  88. <Checkbox
  89. checked={isAllSelected}
  90. onChange={() => {
  91. if (isAllSelected === true) {
  92. deselectAll();
  93. } else {
  94. selectAll();
  95. }
  96. }}
  97. />
  98. {isAnySelected ? (
  99. <HasSelection
  100. mailbox={mailbox}
  101. countSelected={countSelected}
  102. selectedIds={selectedIds}
  103. deselectAll={deselectAll}
  104. />
  105. ) : (
  106. <MailboxPicker value={mailbox} onChange={setMailbox} />
  107. )}
  108. </HeaderPanelItem>
  109. );
  110. }
  111. interface HasSelectionProps
  112. extends Pick<
  113. ReturnType<typeof useListItemCheckboxState>,
  114. 'countSelected' | 'selectedIds' | 'deselectAll'
  115. > {
  116. mailbox: ReturnType<typeof decodeMailbox>;
  117. }
  118. function HasSelection({
  119. mailbox,
  120. countSelected,
  121. selectedIds,
  122. deselectAll,
  123. }: HasSelectionProps) {
  124. const organization = useOrganization();
  125. const {markAsRead, resolve} = useMutateFeedback({
  126. feedbackIds: selectedIds,
  127. organization,
  128. });
  129. const mutationOptionsResolve = {
  130. onError: () => {
  131. addErrorMessage(t('An error occurred while updating the feedbacks.'));
  132. },
  133. onSuccess: () => {
  134. addSuccessMessage(t('Updated feedbacks'));
  135. deselectAll();
  136. },
  137. };
  138. const mutationOptionsRead = {
  139. onError: () => {
  140. addErrorMessage(t('An error occurred while updating the feedbacks.'));
  141. },
  142. onSuccess: () => {
  143. addSuccessMessage(t('Updated feedbacks'));
  144. },
  145. };
  146. return (
  147. <Flex gap={space(1)} align="center" justify="space-between" style={{flexGrow: 1}}>
  148. <span>
  149. <strong>
  150. {tct('[countSelected] Selected', {
  151. countSelected,
  152. })}
  153. </strong>
  154. </span>
  155. <Flex gap={space(1)} justify="flex-end">
  156. <ErrorBoundary mini>
  157. <Button
  158. onClick={() => {
  159. const newStatus =
  160. mailbox === 'resolved' ? GroupStatus.UNRESOLVED : GroupStatus.RESOLVED;
  161. openConfirmModal({
  162. onConfirm: () => {
  163. addLoadingMessage(t('Updating feedbacks...'));
  164. resolve(newStatus, mutationOptionsResolve);
  165. },
  166. body: tct('Are you sure you want to [status] these feedbacks?', {
  167. status: statusToText[newStatus].toLowerCase(),
  168. }),
  169. footerConfirm: statusToText[newStatus],
  170. });
  171. }}
  172. >
  173. {mailbox === 'resolved' ? t('Unresolve') : t('Resolve')}
  174. </Button>
  175. </ErrorBoundary>
  176. <ErrorBoundary mini>
  177. <DropdownMenu
  178. position="bottom-end"
  179. triggerProps={{
  180. 'aria-label': t('Read Menu'),
  181. icon: <IconEllipsis size="xs" />,
  182. showChevron: false,
  183. size: 'xs',
  184. }}
  185. items={[
  186. {
  187. key: 'mark read',
  188. label: t('Mark Read'),
  189. onAction: () => {
  190. openConfirmModal({
  191. onConfirm: () => {
  192. addLoadingMessage(t('Updating feedbacks...'));
  193. markAsRead(true, mutationOptionsRead);
  194. },
  195. body: t('Are you sure you want to mark these feedbacks as read?'),
  196. footerConfirm: 'Mark read',
  197. });
  198. },
  199. },
  200. {
  201. key: 'mark unread',
  202. label: t('Mark Unread'),
  203. onAction: () => {
  204. openConfirmModal({
  205. onConfirm: () => {
  206. addLoadingMessage(t('Updating feedbacks...'));
  207. markAsRead(false, mutationOptionsRead);
  208. },
  209. body: t('Are you sure you want to mark these feedbacks as unread?'),
  210. footerConfirm: 'Mark unread',
  211. });
  212. },
  213. },
  214. ]}
  215. />
  216. </ErrorBoundary>
  217. </Flex>
  218. </Flex>
  219. );
  220. }
  221. const HeaderPanelItem = styled(PanelItem)`
  222. display: flex;
  223. padding: ${space(1)} ${space(2)} ${space(1)} ${space(2)};
  224. gap: ${space(1)};
  225. align-items: center;
  226. `;