123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- import isArray from 'lodash/isArray';
- import isUndefined from 'lodash/isUndefined';
- import {createStore, StoreDefinition} from 'reflux';
- import GroupActions from 'sentry/actions/groupActions';
- import {t} from 'sentry/locale';
- import IndicatorStore from 'sentry/stores/indicatorStore';
- import {
- Activity,
- BaseGroup,
- Group,
- GroupCollapseRelease,
- GroupRelease,
- GroupStats,
- } from 'sentry/types';
- import {makeSafeRefluxStore} from 'sentry/utils/makeSafeRefluxStore';
- function showAlert(msg, type) {
- IndicatorStore.addMessage(msg, type, {duration: 4000});
- }
- // TODO(ts) Type this any better.
- type Change = [string, string, any];
- class PendingChangeQueue {
- changes: Change[] = [];
- getForItem(itemId: string) {
- return this.changes.filter((change: Change) => change[1] === itemId);
- }
- push(changeId: string, itemId: string, data: any) {
- this.changes.push([changeId, itemId, data]);
- }
- remove(changeId: string, itemId?: string) {
- this.changes = this.changes.filter(
- change => change[0] !== changeId || change[1] !== itemId
- );
- }
- forEach(...args: any[]) {
- this.changes.forEach.apply(this.changes, args);
- }
- }
- type Item = BaseGroup | Group | GroupCollapseRelease;
- interface InternalDefinition {
- addActivity: (groupId: string, data: Activity, index?: number) => void;
- indexOfActivity: (groupId: string, id: string) => number;
- items: Item[];
- pendingChanges: PendingChangeQueue;
- removeActivity: (groupId: string, id: string) => number;
- statuses: Record<string, Record<string, boolean>>;
- updateActivity: (groupId: string, id: string, data: Partial<Activity>) => void;
- }
- interface GroupStoreDefinition extends StoreDefinition, InternalDefinition {
- add: (items: Item[]) => void;
- addStatus: (id: string, status: string) => void;
- clearStatus: (id: string, status: string) => void;
- get: (id: string) => Item | undefined;
- getAllItemIds: () => string[];
- getAllItems: () => Item[];
- hasStatus: (id: string, status: string) => boolean;
- init: () => void;
- loadInitialData: (items: Item[]) => void;
- onAssignTo: (changeId: string, itemId: string, data: any) => void;
- onAssignToError: (changeId: string, itemId: string, error: Error) => void;
- onAssignToSuccess: (changeId: string, itemId: string, response: any) => void;
- onDelete: (changeId: string, itemIds: string[]) => void;
- onDeleteError: (changeId: string, itemIds: string[], error: Error) => void;
- onDeleteSuccess: (changeId: string, itemIds: string[], response: any) => void;
- onDiscard: (changeId: string, itemId: string) => void;
- onDiscardError: (changeId: string, itemId: string, response: any) => void;
- onDiscardSuccess: (changeId: string, itemId: string, response: any) => void;
- onMerge: (changeId: string, itemIds: string[]) => void;
- onMergeError: (changeId: string, itemIds: string[], response: any) => void;
- onMergeSuccess: (changeId: string, itemIds: string[], response: any) => void;
- onPopulateReleases: (itemId: string, releaseData: GroupRelease) => void;
- onPopulateStats: (itemIds: string[], response: GroupStats[]) => void;
- onUpdate: (changeId: string, itemIds: string[], data: any) => void;
- onUpdateError: (
- changeId: string,
- itemIds: string[],
- error: Error,
- silent: boolean
- ) => void;
- onUpdateSuccess: (
- changeId: string,
- itemIds: string[],
- response: Partial<Group>
- ) => void;
- remove: (itemIds: string[]) => void;
- reset: () => void;
- }
- const storeConfig: GroupStoreDefinition = {
- listenables: [GroupActions],
- pendingChanges: new PendingChangeQueue(),
- items: [],
- statuses: {},
- init() {
- this.reset();
- },
- reset() {
- this.pendingChanges = new PendingChangeQueue();
- this.items = [];
- this.statuses = {};
- },
- // TODO(dcramer): this should actually come from an action of some sorts
- loadInitialData(items) {
- this.reset();
- const itemIds = new Set<string>();
- items.forEach(item => {
- itemIds.add(item.id);
- this.items.push(item);
- });
- this.trigger(itemIds);
- },
- add(items) {
- if (!isArray(items)) {
- items = [items];
- }
- const itemsById = {};
- const itemIds = new Set<string>();
- items.forEach(item => {
- itemsById[item.id] = item;
- itemIds.add(item.id);
- });
- // See if any existing items are updated by this new set of items
- this.items.forEach((item, idx) => {
- if (itemsById[item.id]) {
- this.items[idx] = {
- ...item,
- ...itemsById[item.id],
- };
- delete itemsById[item.id];
- }
- });
- // New items
- for (const itemId in itemsById) {
- this.items.push(itemsById[itemId]);
- }
- this.trigger(itemIds);
- },
- remove(itemIds) {
- this.items = this.items.filter(item => !itemIds.includes(item.id));
- this.trigger(new Set(itemIds));
- },
- addStatus(id, status) {
- if (isUndefined(this.statuses[id])) {
- this.statuses[id] = {};
- }
- this.statuses[id][status] = true;
- },
- clearStatus(id, status) {
- if (isUndefined(this.statuses[id])) {
- return;
- }
- this.statuses[id][status] = false;
- },
- hasStatus(id, status) {
- if (isUndefined(this.statuses[id])) {
- return false;
- }
- return this.statuses[id][status] || false;
- },
- indexOfActivity(group_id, id) {
- const group = this.get(group_id);
- if (!group) {
- return -1;
- }
- for (let i = 0; i < group.activity.length; i++) {
- if (group.activity[i].id === id) {
- return i;
- }
- }
- return -1;
- },
- addActivity(id, data, index = -1) {
- const group = this.get(id);
- if (!group) {
- return;
- }
- // insert into beginning by default
- if (index === -1) {
- group.activity.unshift(data);
- } else {
- group.activity.splice(index, 0, data);
- }
- if (data.type === 'note') {
- group.numComments++;
- }
- this.trigger(new Set([id]));
- },
- updateActivity(group_id, id, data) {
- const group = this.get(group_id);
- if (!group) {
- return;
- }
- const index = this.indexOfActivity(group_id, id);
- if (index === -1) {
- return;
- }
- // Here, we want to merge the new `data` being passed in
- // into the existing `data` object. This effectively
- // allows passing in an object of only changes.
- group.activity[index].data = Object.assign(group.activity[index].data, data);
- this.trigger(new Set([group.id]));
- },
- removeActivity(group_id, id) {
- const group = this.get(group_id);
- if (!group) {
- return -1;
- }
- const index = this.indexOfActivity(group.id, id);
- if (index === -1) {
- return -1;
- }
- const activity = group.activity.splice(index, 1);
- if (activity[0].type === 'note') {
- group.numComments--;
- }
- this.trigger(new Set([group.id]));
- return index;
- },
- get(id) {
- // TODO(ts) This needs to be constrained further. It was left as any
- // because the PendingChanges signatures and this were not aligned.
- const pendingForId: any[] = [];
- this.pendingChanges.forEach(change => {
- if (change.id === id) {
- pendingForId.push(change);
- }
- });
- for (let i = 0; i < this.items.length; i++) {
- if (this.items[i].id === id) {
- let rItem = this.items[i];
- if (pendingForId.length) {
- // copy the object so dirty state doesnt mutate original
- rItem = {...rItem};
- for (let c = 0; c < pendingForId.length; c++) {
- rItem = {
- ...rItem,
- ...pendingForId[c].params,
- };
- }
- }
- return rItem;
- }
- }
- return undefined;
- },
- getAllItemIds() {
- return this.items.map(item => item.id);
- },
- getAllItems() {
- // regroup pending changes by their itemID
- const pendingById = {};
- this.pendingChanges.forEach(change => {
- if (isUndefined(pendingById[change.id])) {
- pendingById[change.id] = [];
- }
- pendingById[change.id].push(change);
- });
- return this.items.map(item => {
- let rItem = item;
- if (!isUndefined(pendingById[item.id])) {
- // copy the object so dirty state doesnt mutate original
- rItem = {...rItem};
- pendingById[item.id].forEach(change => {
- rItem = {
- ...rItem,
- ...change.params,
- };
- });
- }
- return rItem;
- });
- },
- onAssignTo(_changeId, itemId, _data) {
- this.addStatus(itemId, 'assignTo');
- this.trigger(new Set([itemId]));
- },
- // TODO(dcramer): This is not really the best place for this
- onAssignToError(_changeId, itemId, _error) {
- this.clearStatus(itemId, 'assignTo');
- showAlert(t('Unable to change assignee. Please try again.'), 'error');
- },
- onAssignToSuccess(_changeId, itemId, response) {
- const item = this.get(itemId);
- if (!item) {
- return;
- }
- item.assignedTo = response.assignedTo;
- this.clearStatus(itemId, 'assignTo');
- this.trigger(new Set([itemId]));
- },
- onDelete(_changeId, itemIds) {
- itemIds = this._itemIdsOrAll(itemIds);
- itemIds.forEach(itemId => {
- this.addStatus(itemId, 'delete');
- });
- this.trigger(new Set(itemIds));
- },
- onDeleteError(_changeId, itemIds, _response) {
- showAlert(t('Unable to delete events. Please try again.'), 'error');
- if (!itemIds) {
- return;
- }
- itemIds.forEach(itemId => {
- this.clearStatus(itemId, 'delete');
- });
- this.trigger(new Set(itemIds));
- },
- onDeleteSuccess(_changeId, itemIds, _response) {
- itemIds = this._itemIdsOrAll(itemIds);
- if (itemIds.length > 1) {
- showAlert(t(`Deleted ${itemIds.length} Issues`), 'success');
- } else {
- const shortId = itemIds.map(item => GroupStore.get(item)?.shortId).join('');
- showAlert(t(`Deleted ${shortId}`), 'success');
- }
- const itemIdSet = new Set(itemIds);
- itemIds.forEach(itemId => {
- delete this.statuses[itemId];
- this.clearStatus(itemId, 'delete');
- });
- this.items = this.items.filter(item => !itemIdSet.has(item.id));
- this.trigger(new Set(itemIds));
- },
- onDiscard(_changeId, itemId) {
- this.addStatus(itemId, 'discard');
- this.trigger(new Set([itemId]));
- },
- onDiscardError(_changeId, itemId, _response) {
- this.clearStatus(itemId, 'discard');
- showAlert(t('Unable to discard event. Please try again.'), 'error');
- this.trigger(new Set([itemId]));
- },
- onDiscardSuccess(_changeId, itemId, _response) {
- delete this.statuses[itemId];
- this.clearStatus(itemId, 'discard');
- this.items = this.items.filter(item => item.id !== itemId);
- showAlert(t('Similar events will be filtered and discarded.'), 'success');
- this.trigger(new Set([itemId]));
- },
- onMerge(_changeId, itemIds) {
- itemIds = this._itemIdsOrAll(itemIds);
- itemIds.forEach(itemId => {
- this.addStatus(itemId, 'merge');
- });
- // XXX(billy): Not sure if this is a bug or not but do we need to publish all itemIds?
- // Seems like we only need to publish parent id
- this.trigger(new Set(itemIds));
- },
- onMergeError(_changeId, itemIds, _response) {
- itemIds = this._itemIdsOrAll(itemIds);
- itemIds.forEach(itemId => {
- this.clearStatus(itemId, 'merge');
- });
- showAlert(t('Unable to merge events. Please try again.'), 'error');
- this.trigger(new Set(itemIds));
- },
- onMergeSuccess(_changeId, mergedIds, response) {
- mergedIds = this._itemIdsOrAll(mergedIds); // everything on page
- mergedIds.forEach(itemId => {
- this.clearStatus(itemId, 'merge');
- });
- // Remove all but parent id (items were merged into this one)
- const mergedIdSet = new Set(mergedIds);
- // Looks like the `PUT /api/0/projects/:orgId/:projectId/issues/` endpoint
- // actually returns a 204, so there is no `response` body
- this.items = this.items.filter(
- item =>
- !mergedIdSet.has(item.id) ||
- (response && response.merge && item.id === response.merge.parent)
- );
- showAlert(t(`Merged ${mergedIds.length} Issues`), 'success');
- this.trigger(new Set(mergedIds));
- },
- /**
- * If itemIds is undefined, returns all ids in the store
- */
- _itemIdsOrAll(itemIds) {
- if (isUndefined(itemIds)) {
- itemIds = this.items.map(item => item.id);
- }
- return itemIds;
- },
- onUpdate(changeId, itemIds, data) {
- itemIds = this._itemIdsOrAll(itemIds);
- itemIds.forEach(itemId => {
- this.addStatus(itemId, 'update');
- this.pendingChanges.push(changeId, itemId, data);
- });
- this.trigger(new Set(itemIds));
- },
- onUpdateError(changeId, itemIds, _error, failSilently) {
- itemIds = this._itemIdsOrAll(itemIds);
- this.pendingChanges.remove(changeId);
- itemIds.forEach(itemId => {
- this.clearStatus(itemId, 'update');
- });
- if (!failSilently) {
- showAlert(t('Unable to update events. Please try again.'), 'error');
- }
- this.trigger(new Set(itemIds));
- },
- onUpdateSuccess(changeId, itemIds, response) {
- itemIds = this._itemIdsOrAll(itemIds);
- this.items.forEach((item, idx) => {
- if (itemIds.indexOf(item.id) !== -1) {
- this.items[idx] = {
- ...item,
- ...response,
- };
- this.clearStatus(item.id, 'update');
- }
- });
- this.pendingChanges.remove(changeId);
- this.trigger(new Set(itemIds));
- },
- onPopulateStats(itemIds: string[], response: GroupStats[]) {
- // Organize stats by id
- const groupStatsMap = response.reduce((map, stats) => {
- map[stats.id] = stats;
- return map;
- }, {});
- this.items.forEach((item, idx) => {
- if (itemIds.includes(item.id)) {
- this.items[idx] = {
- ...item,
- ...groupStatsMap[item.id],
- };
- }
- });
- this.trigger(new Set(this.items.map(item => item.id)));
- },
- onPopulateReleases(itemId: string, releaseData: GroupRelease) {
- this.items.forEach((item, idx) => {
- if (item.id === itemId) {
- this.items[idx] = {
- ...item,
- ...releaseData,
- };
- }
- });
- this.trigger(new Set([itemId]));
- },
- };
- const GroupStore = createStore(makeSafeRefluxStore(storeConfig));
- export default GroupStore;
|