groupActivity.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import {Fragment, useCallback, useEffect, useMemo} from 'react';
  2. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  3. import type {
  4. TContext,
  5. TData,
  6. TError,
  7. TVariables,
  8. } from 'sentry/components/feedback/useMutateActivity';
  9. import useMutateActivity from 'sentry/components/feedback/useMutateActivity';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import LoadingError from 'sentry/components/loadingError';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import ReprocessedBox from 'sentry/components/reprocessedBox';
  14. import {t} from 'sentry/locale';
  15. import GroupStore from 'sentry/stores/groupStore';
  16. import type {NoteType} from 'sentry/types/alerts';
  17. import type {
  18. Group,
  19. GroupActivity as GroupActivityType,
  20. GroupActivityNote,
  21. GroupActivityReprocess,
  22. } from 'sentry/types/group';
  23. import type {User} from 'sentry/types/user';
  24. import type {MutateOptions} from 'sentry/utils/queryClient';
  25. import {useLocation} from 'sentry/utils/useLocation';
  26. import {useNavigate} from 'sentry/utils/useNavigate';
  27. import useOrganization from 'sentry/utils/useOrganization';
  28. import {useParams} from 'sentry/utils/useParams';
  29. import ActivitySection from 'sentry/views/issueDetails/activitySection';
  30. import {useGroup} from 'sentry/views/issueDetails/useGroup';
  31. import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute';
  32. import {
  33. getGroupMostRecentActivity,
  34. getGroupReprocessingStatus,
  35. ReprocessingStatus,
  36. useHasStreamlinedUI,
  37. } from 'sentry/views/issueDetails/utils';
  38. export type MutateActivityOptions = MutateOptions<TData, TError, TVariables, TContext>;
  39. interface GroupActivityProps {
  40. group: Group;
  41. }
  42. function GroupActivity({group}: GroupActivityProps) {
  43. const organization = useOrganization();
  44. const {activity: activities, count, id: groupId} = group;
  45. const groupCount = Number(count);
  46. const mostRecentActivity = getGroupMostRecentActivity(activities);
  47. const reprocessingStatus = getGroupReprocessingStatus(group, mostRecentActivity);
  48. const mutators = useMutateActivity({
  49. organization,
  50. group,
  51. });
  52. const deleteOptions: MutateActivityOptions = useMemo(() => {
  53. return {
  54. onError: () => {
  55. addErrorMessage(t('Failed to delete comment'));
  56. },
  57. onSuccess: () => {
  58. addSuccessMessage(t('Comment removed'));
  59. },
  60. };
  61. }, []);
  62. const createOptions: MutateActivityOptions = useMemo(() => {
  63. return {
  64. onError: () => {
  65. addErrorMessage(t('Unable to post comment'));
  66. },
  67. onSuccess: data => {
  68. GroupStore.addActivity(group.id, data);
  69. addSuccessMessage(t('Comment posted'));
  70. },
  71. };
  72. }, [group.id]);
  73. const updateOptions: MutateActivityOptions = useMemo(() => {
  74. return {
  75. onError: () => {
  76. addErrorMessage(t('Unable to update comment'));
  77. },
  78. onSuccess: data => {
  79. const d = data as GroupActivityNote;
  80. GroupStore.updateActivity(group.id, data.id, {text: d.data.text});
  81. addSuccessMessage(t('Comment updated'));
  82. },
  83. };
  84. }, [group.id]);
  85. const handleDelete = useCallback(
  86. (item: GroupActivityType) => {
  87. const restore = group.activity.find(activity => activity.id === item.id);
  88. const index = GroupStore.removeActivity(group.id, item.id);
  89. if (index === -1 || restore === undefined) {
  90. addErrorMessage(t('Failed to delete comment'));
  91. return;
  92. }
  93. mutators.handleDelete(
  94. item.id,
  95. group.activity.filter(a => a.id !== item.id),
  96. deleteOptions
  97. );
  98. },
  99. [deleteOptions, group.activity, mutators, group.id]
  100. );
  101. const handleCreate = useCallback(
  102. (n: NoteType, _me: User) => {
  103. mutators.handleCreate(n, group.activity, createOptions);
  104. },
  105. [createOptions, group.activity, mutators]
  106. );
  107. const handleUpdate = useCallback(
  108. (item: GroupActivityType, n: NoteType) => {
  109. mutators.handleUpdate(n, item.id, group.activity, updateOptions);
  110. },
  111. [updateOptions, group.activity, mutators]
  112. );
  113. return (
  114. <Fragment>
  115. {(reprocessingStatus === ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT ||
  116. reprocessingStatus === ReprocessingStatus.REPROCESSED_AND_HAS_EVENT) && (
  117. <ReprocessedBox
  118. reprocessActivity={mostRecentActivity as GroupActivityReprocess}
  119. groupCount={groupCount}
  120. orgSlug={organization.slug}
  121. groupId={groupId}
  122. />
  123. )}
  124. <Layout.Body>
  125. <Layout.Main>
  126. <ActivitySection
  127. group={group}
  128. onDelete={handleDelete}
  129. onCreate={handleCreate}
  130. onUpdate={handleUpdate}
  131. placeholderText={t(
  132. 'Add details or updates to this event. \nTag users with @, or teams with #'
  133. )}
  134. />
  135. </Layout.Main>
  136. </Layout.Body>
  137. </Fragment>
  138. );
  139. }
  140. function GroupActivityRoute() {
  141. const hasStreamlinedUI = useHasStreamlinedUI();
  142. const navigate = useNavigate();
  143. const {baseUrl} = useGroupDetailsRoute();
  144. const location = useLocation();
  145. const params = useParams<{groupId: string}>();
  146. const {
  147. data: group,
  148. isPending: isGroupPending,
  149. isError: isGroupError,
  150. refetch: refetchGroup,
  151. } = useGroup({groupId: params.groupId});
  152. // TODO(streamlined-ui): Activity will become a router redirect to the event details page
  153. useEffect(() => {
  154. if (hasStreamlinedUI) {
  155. navigate({
  156. ...location,
  157. pathname: baseUrl,
  158. });
  159. }
  160. }, [hasStreamlinedUI, navigate, baseUrl, location]);
  161. if (isGroupPending) {
  162. return <LoadingIndicator />;
  163. }
  164. if (isGroupError) {
  165. return <LoadingError onRetry={refetchGroup} />;
  166. }
  167. return <GroupActivity group={group} />;
  168. }
  169. export default GroupActivityRoute;