group.tsx 8.4 KB

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