group.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import * as Sentry from '@sentry/react';
  2. import type {RequestCallbacks, RequestOptions} from 'sentry/api';
  3. import {Client} from 'sentry/api';
  4. import GroupStore from 'sentry/stores/groupStore';
  5. import type {Actor} from 'sentry/types/core';
  6. import type {Group, Note, Tag as GroupTag, TagValue} from 'sentry/types/group';
  7. import type {Member} from 'sentry/types/organization';
  8. import type {User} from 'sentry/types/user';
  9. import {buildTeamId, buildUserId} from 'sentry/utils';
  10. import {uniqueId} from 'sentry/utils/guid';
  11. import type {ApiQueryKey, UseApiQueryOptions} from 'sentry/utils/queryClient';
  12. import {useApiQuery} from 'sentry/utils/queryClient';
  13. type AssignedBy = 'suggested_assignee' | 'assignee_selector';
  14. type AssignToUserParams = {
  15. assignedBy: AssignedBy;
  16. /**
  17. * Issue id
  18. */
  19. id: string;
  20. orgSlug: string;
  21. user: User | Actor;
  22. member?: Member;
  23. };
  24. export function assignToUser(params: AssignToUserParams) {
  25. const api = new Client();
  26. const endpoint = `/organizations/${params.orgSlug}/issues/${params.id}/`;
  27. const id = uniqueId();
  28. GroupStore.onAssignTo(id, params.id, {
  29. email: params.member?.email ?? '',
  30. });
  31. const request = api.requestPromise(endpoint, {
  32. method: 'PUT',
  33. // Sending an empty value to assignedTo is the same as "clear",
  34. // so if no member exists, that implies that we want to clear the
  35. // current assignee.
  36. data: {
  37. assignedTo: params.user ? buildUserId(params.user.id) : '',
  38. assignedBy: params.assignedBy,
  39. },
  40. });
  41. request
  42. .then(data => {
  43. GroupStore.onAssignToSuccess(id, params.id, data);
  44. })
  45. .catch(data => {
  46. GroupStore.onAssignToError(id, params.id, data);
  47. });
  48. return request;
  49. }
  50. export function clearAssignment(
  51. groupId: string,
  52. orgSlug: string,
  53. assignedBy: AssignedBy
  54. ) {
  55. const api = new Client();
  56. const endpoint = `/organizations/${orgSlug}/issues/${groupId}/`;
  57. const id = uniqueId();
  58. GroupStore.onAssignTo(id, groupId, {
  59. email: '',
  60. });
  61. const request = api.requestPromise(endpoint, {
  62. method: 'PUT',
  63. // Sending an empty value to assignedTo is the same as "clear"
  64. data: {
  65. assignedTo: '',
  66. assignedBy,
  67. },
  68. });
  69. request
  70. .then(data => {
  71. GroupStore.onAssignToSuccess(id, groupId, data);
  72. })
  73. .catch(data => {
  74. GroupStore.onAssignToError(id, groupId, data);
  75. });
  76. return request;
  77. }
  78. type AssignToActorParams = {
  79. actor: Pick<Actor, 'id' | 'type'>;
  80. assignedBy: AssignedBy;
  81. /**
  82. * Issue id
  83. */
  84. id: string;
  85. orgSlug: string;
  86. };
  87. export function assignToActor({id, actor, assignedBy, orgSlug}: AssignToActorParams) {
  88. const api = new Client();
  89. const endpoint = `/organizations/${orgSlug}/issues/${id}/`;
  90. const guid = uniqueId();
  91. let actorId = '';
  92. GroupStore.onAssignTo(guid, id, {email: ''});
  93. switch (actor.type) {
  94. case 'user':
  95. actorId = buildUserId(actor.id);
  96. break;
  97. case 'team':
  98. actorId = buildTeamId(actor.id);
  99. break;
  100. default:
  101. Sentry.withScope(scope => {
  102. scope.setExtra('actor', actor);
  103. Sentry.captureException('Unknown assignee type');
  104. });
  105. }
  106. return api
  107. .requestPromise(endpoint, {
  108. method: 'PUT',
  109. data: {assignedTo: actorId, assignedBy},
  110. })
  111. .then(data => {
  112. GroupStore.onAssignToSuccess(guid, id, data);
  113. })
  114. .catch(data => {
  115. GroupStore.onAssignToSuccess(guid, id, data);
  116. });
  117. }
  118. export function deleteNote(
  119. api: Client,
  120. orgSlug: string,
  121. group: Group,
  122. id: string,
  123. _oldText: string
  124. ) {
  125. const restore = group.activity.find(activity => activity.id === id);
  126. const index = GroupStore.removeActivity(group.id, id);
  127. if (index === -1 || restore === undefined) {
  128. // I dunno, the id wasn't found in the GroupStore
  129. return Promise.reject(new Error('Group was not found in store'));
  130. }
  131. const promise = api.requestPromise(
  132. `/organizations/${orgSlug}/issues/${group.id}/comments/${id}/`,
  133. {
  134. method: 'DELETE',
  135. }
  136. );
  137. promise.catch(() => GroupStore.addActivity(group.id, restore, index));
  138. return promise;
  139. }
  140. export function createNote(api: Client, orgSlug: string, group: Group, note: Note) {
  141. const promise = api.requestPromise(
  142. `/organizations/${orgSlug}/issues/${group.id}/comments/`,
  143. {
  144. method: 'POST',
  145. data: note,
  146. }
  147. );
  148. promise.then(data => GroupStore.addActivity(group.id, data));
  149. return promise;
  150. }
  151. export function updateNote(
  152. api: Client,
  153. orgSlug: string,
  154. group: Group,
  155. note: Note,
  156. id: string,
  157. oldText: string
  158. ) {
  159. GroupStore.updateActivity(group.id, id, {text: note.text});
  160. const promise = api.requestPromise(
  161. `/organizations/${orgSlug}/issues/${group.id}/comments/${id}/`,
  162. {
  163. method: 'PUT',
  164. data: note,
  165. }
  166. );
  167. promise.catch(() => GroupStore.updateActivity(group.id, id, {text: oldText}));
  168. return promise;
  169. }
  170. type ParamsType = {
  171. environment?: string | string[] | null;
  172. itemIds?: string[];
  173. project?: number[] | string[] | null;
  174. query?: string;
  175. };
  176. type UpdateParams = ParamsType & {
  177. orgId: string;
  178. projectId?: string;
  179. };
  180. type QueryArgs =
  181. | {
  182. query: string;
  183. environment?: string | Array<string>;
  184. project?: Array<number | string>;
  185. }
  186. | {
  187. id: Array<number> | Array<string>;
  188. environment?: string | Array<string>;
  189. project?: Array<number | string>;
  190. }
  191. | {
  192. environment?: string | Array<string>;
  193. project?: Array<number | string>;
  194. };
  195. /**
  196. * Converts input parameters to API-compatible query arguments
  197. */
  198. export function paramsToQueryArgs(params: ParamsType): QueryArgs {
  199. const p: QueryArgs = params.itemIds
  200. ? {id: params.itemIds} // items matching array of itemids
  201. : params.query
  202. ? {query: params.query} // items matching search query
  203. : {}; // all items
  204. // only include environment if it is not null/undefined
  205. if (params.query && params.environment !== null && params.environment !== undefined) {
  206. p.environment = params.environment;
  207. }
  208. // only include projects if it is not null/undefined/an empty array
  209. if (params.project?.length) {
  210. p.project = params.project;
  211. }
  212. // only include date filters if they are not null/undefined
  213. if (params.query) {
  214. ['start', 'end', 'period', 'utc'].forEach(prop => {
  215. if (params[prop] !== null && params[prop] !== undefined) {
  216. p[prop === 'period' ? 'statsPeriod' : prop] = params[prop];
  217. }
  218. });
  219. }
  220. return p;
  221. }
  222. function getUpdateUrl({projectId, orgId}: UpdateParams) {
  223. return projectId
  224. ? `/projects/${orgId}/${projectId}/issues/`
  225. : `/organizations/${orgId}/issues/`;
  226. }
  227. function chainUtil<Args extends any[]>(
  228. ...funcs: Array<((...args: Args) => any) | undefined>
  229. ) {
  230. const filteredFuncs = funcs.filter(
  231. (f): f is (...args: Args) => any => typeof f === 'function'
  232. );
  233. return (...args: Args): void => {
  234. filteredFuncs.forEach(func => {
  235. func.apply(funcs, args);
  236. });
  237. };
  238. }
  239. function wrapRequest(
  240. api: Client,
  241. path: string,
  242. options: RequestOptions,
  243. extraParams: RequestCallbacks = {}
  244. ) {
  245. options.success = chainUtil(options.success, extraParams.success);
  246. options.error = chainUtil(options.error, extraParams.error);
  247. options.complete = chainUtil(options.complete, extraParams.complete);
  248. return api.request(path, options);
  249. }
  250. type BulkDeleteParams = UpdateParams;
  251. export function bulkDelete(
  252. api: Client,
  253. params: BulkDeleteParams,
  254. options: RequestCallbacks
  255. ) {
  256. const {itemIds} = params;
  257. const path = getUpdateUrl(params);
  258. const query: QueryArgs = paramsToQueryArgs(params);
  259. const id = uniqueId();
  260. GroupStore.onDelete(id, itemIds);
  261. return wrapRequest(
  262. api,
  263. path,
  264. {
  265. query,
  266. method: 'DELETE',
  267. success: response => {
  268. GroupStore.onDeleteSuccess(id, itemIds, response);
  269. },
  270. error: error => {
  271. GroupStore.onDeleteError(id, itemIds, error);
  272. },
  273. },
  274. options
  275. );
  276. }
  277. type BulkUpdateParams = UpdateParams & {
  278. data?: any;
  279. failSilently?: boolean;
  280. };
  281. export function bulkUpdate(
  282. api: Client,
  283. params: BulkUpdateParams,
  284. options: RequestCallbacks
  285. ) {
  286. const {itemIds, failSilently, data} = params;
  287. const path = getUpdateUrl(params);
  288. const query: QueryArgs = paramsToQueryArgs(params);
  289. const id = uniqueId();
  290. GroupStore.onUpdate(id, itemIds, data);
  291. return wrapRequest(
  292. api,
  293. path,
  294. {
  295. query,
  296. method: 'PUT',
  297. data,
  298. success: response => {
  299. GroupStore.onUpdateSuccess(id, itemIds, response);
  300. },
  301. error: () => {
  302. GroupStore.onUpdateError(id, itemIds, !!failSilently);
  303. },
  304. },
  305. options
  306. );
  307. }
  308. type MergeGroupsParams = UpdateParams;
  309. export function mergeGroups(
  310. api: Client,
  311. params: MergeGroupsParams,
  312. options: RequestCallbacks
  313. ) {
  314. const {itemIds} = params;
  315. const path = getUpdateUrl(params);
  316. const query: QueryArgs = paramsToQueryArgs(params);
  317. const id = uniqueId();
  318. GroupStore.onMerge(id, itemIds);
  319. return wrapRequest(
  320. api,
  321. path,
  322. {
  323. query,
  324. method: 'PUT',
  325. data: {merge: 1},
  326. success: response => {
  327. GroupStore.onMergeSuccess(id, itemIds, response);
  328. },
  329. error: error => {
  330. GroupStore.onMergeError(id, itemIds, error);
  331. },
  332. },
  333. options
  334. );
  335. }
  336. type FetchIssueTagValuesParameters = {
  337. groupId: string;
  338. orgSlug: string;
  339. tagKey: string;
  340. cursor?: string;
  341. environment?: string[];
  342. sort?: string | string[];
  343. };
  344. export const makeFetchIssueTagValuesQueryKey = ({
  345. orgSlug,
  346. groupId,
  347. tagKey,
  348. environment,
  349. sort,
  350. cursor,
  351. }: FetchIssueTagValuesParameters): ApiQueryKey => [
  352. `/organizations/${orgSlug}/issues/${groupId}/tags/${tagKey}/values/`,
  353. {query: {environment, sort, cursor}},
  354. ];
  355. export function useFetchIssueTagValues(
  356. parameters: FetchIssueTagValuesParameters,
  357. options: Partial<UseApiQueryOptions<TagValue[]>> = {}
  358. ) {
  359. return useApiQuery<TagValue[]>(makeFetchIssueTagValuesQueryKey(parameters), {
  360. staleTime: 0,
  361. retry: false,
  362. ...options,
  363. });
  364. }
  365. type FetchIssueTagParameters = {
  366. groupId: string;
  367. orgSlug: string;
  368. tagKey: string;
  369. };
  370. export const makeFetchIssueTagQueryKey = ({
  371. orgSlug,
  372. groupId,
  373. tagKey,
  374. environment,
  375. sort,
  376. }: FetchIssueTagValuesParameters): ApiQueryKey => [
  377. `/organizations/${orgSlug}/issues/${groupId}/tags/${tagKey}/`,
  378. {query: {environment, sort}},
  379. ];
  380. export function useFetchIssueTag(
  381. parameters: FetchIssueTagParameters,
  382. options: Partial<UseApiQueryOptions<GroupTag>> = {}
  383. ) {
  384. return useApiQuery<GroupTag>(makeFetchIssueTagQueryKey(parameters), {
  385. staleTime: 0,
  386. retry: false,
  387. ...options,
  388. });
  389. }