Files
sentinel/static/app/stores/selectedGroupStore.tsx
2024-02-15 17:01:50 +00:00

168 lines
3.8 KiB
TypeScript

import {createStore} from 'reflux';
import GroupStore from 'sentry/stores/groupStore';
import type {CommonStoreDefinition} from './types';
interface InternalDefinition {
/**
* The last item to have been selected
*/
lastSelected: string | null;
/**
* Mapping of item ID -> if it is selected. This is a map to
* make it easier to track if everything has been selected or not.
*/
records: Map<string, boolean>;
}
interface SelectedGroupStoreDefinition
extends CommonStoreDefinition<Record<string, boolean>>,
InternalDefinition {
add(ids: string[]): void;
allSelected(): boolean;
anySelected(): boolean;
deselectAll(): void;
getSelectedIds(): Set<string>;
init(): void;
isSelected(itemId: string): boolean;
multiSelected(): boolean;
onGroupChange(itemIds: Set<string>): void;
prune(): void;
reset(): void;
shiftToggleItems(itemId: string): void;
toggleSelect(itemId: string): void;
toggleSelectAll(): void;
}
const storeConfig: SelectedGroupStoreDefinition = {
records: new Map(),
lastSelected: null,
unsubscribeListeners: [],
init() {
// XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux
// listeners due to their leaky nature in tests.
this.reset();
},
reset() {
this.records = new Map();
this.lastSelected = null;
},
getState() {
return Object.fromEntries(this.records);
},
onGroupChange(itemIds) {
this.prune();
this.add([...itemIds]);
this.trigger();
},
add(ids) {
const allSelected = this.allSelected();
ids
.filter(id => !this.records.has(id))
.forEach(id => this.records.set(id, allSelected));
},
prune() {
const existingIds = new Set(GroupStore.getAllItemIds());
this.lastSelected = null;
// Remove everything that no longer exists
[...this.records.keys()]
.filter(id => !existingIds.has(id))
.forEach(id => this.records.delete(id));
},
allSelected() {
const itemIds = this.getSelectedIds();
return itemIds.size > 0 && itemIds.size === this.records.size;
},
numSelected() {
return this.getSelectedIds().size;
},
anySelected() {
return this.getSelectedIds().size > 0;
},
multiSelected() {
return this.getSelectedIds().size > 1;
},
getSelectedIds() {
return new Set([...this.records.keys()].filter(id => this.records.get(id)));
},
isSelected(itemId) {
return !!this.records.get(itemId);
},
deselectAll() {
this.records.forEach((_, id) => this.records.set(id, false));
this.trigger();
},
toggleSelect(itemId) {
if (!this.records.has(itemId)) {
return;
}
const newState = !this.records.get(itemId);
this.records.set(itemId, newState);
this.lastSelected = itemId;
this.trigger();
},
toggleSelectAll() {
const allSelected = !this.allSelected();
this.lastSelected = null;
this.records.forEach((_, id) => this.records.set(id, allSelected));
this.trigger();
},
shiftToggleItems(itemId) {
if (!this.records.has(itemId)) {
return;
}
if (!this.lastSelected) {
this.toggleSelect(itemId);
return;
}
const ids = GroupStore.getAllItemIds();
const lastIdx = ids.findIndex(id => id === this.lastSelected);
const currentIdx = ids.findIndex(id => id === itemId);
if (lastIdx === -1 || currentIdx === -1) {
return;
}
const newValue = !this.records.get(itemId);
const selected =
lastIdx < currentIdx
? ids.slice(lastIdx, currentIdx)
: ids.slice(currentIdx, lastIdx);
[...selected, this.lastSelected, itemId]
.filter(id => this.records.has(id))
.forEach(id => this.records.set(id, newValue));
this.lastSelected = itemId;
this.trigger();
},
};
const SelectedGroupStore = createStore(storeConfig);
export default SelectedGroupStore;