import {Fragment, useEffect, useState} from 'react';
import styled from '@emotion/styled';
import {bulkDelete, bulkUpdate, mergeGroups} from 'sentry/actionCreators/group';
import {Alert} from 'sentry/components/alert';
import Checkbox from 'sentry/components/checkbox';
import {Sticky} from 'sentry/components/sticky';
import {tct, tn} from 'sentry/locale';
import GroupStore from 'sentry/stores/groupStore';
import ProjectsStore from 'sentry/stores/projectsStore';
import SelectedGroupStore from 'sentry/stores/selectedGroupStore';
import {useLegacyStore} from 'sentry/stores/useLegacyStore';
import {space} from 'sentry/styles/space';
import type {Group, PageFilters} from 'sentry/types';
import {trackAnalytics} from 'sentry/utils/analytics';
import {uniq} from 'sentry/utils/array/uniq';
import theme from 'sentry/utils/theme';
import useApi from 'sentry/utils/useApi';
import useMedia from 'sentry/utils/useMedia';
import useOrganization from 'sentry/utils/useOrganization';
import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState';
import {SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY} from 'sentry/views/issueList/utils';
import ActionSet from './actionSet';
import Headers from './headers';
import IssueListSortOptions from './sortOptions';
import {BULK_LIMIT, BULK_LIMIT_STR, ConfirmAction} from './utils';
type IssueListActionsProps = {
allResultsVisible: boolean;
displayReprocessingActions: boolean;
groupIds: string[];
onDelete: () => void;
onSelectStatsPeriod: (period: string) => void;
onSortChange: (sort: string) => void;
query: string;
queryCount: number;
selection: PageFilters;
sort: string;
statsPeriod: string;
onActionTaken?: (itemIds: string[]) => void;
onMarkReviewed?: (itemIds: string[]) => void;
};
function IssueListActions({
allResultsVisible,
displayReprocessingActions,
groupIds,
onActionTaken,
onDelete,
onMarkReviewed,
onSelectStatsPeriod,
onSortChange,
queryCount,
query,
selection,
sort,
statsPeriod,
}: IssueListActionsProps) {
const api = useApi();
const organization = useOrganization();
const {
pageSelected,
multiSelected,
anySelected,
allInQuerySelected,
selectedIdsSet,
selectedProjectSlug,
setAllInQuerySelected,
} = useSelectedGroupsState();
const [isSavedSearchesOpen] = useSyncedLocalStorageState(
SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY,
false
);
const disableActions = useMedia(
`(max-width: ${
isSavedSearchesOpen ? theme.breakpoints.large : theme.breakpoints.small
})`
);
const numIssues = selectedIdsSet.size;
function actionSelectedGroups(callback: (itemIds: string[] | undefined) => void) {
const selectedIds = allInQuerySelected
? undefined // undefined means "all"
: groupIds.filter(itemId => selectedIdsSet.has(itemId));
callback(selectedIds);
SelectedGroupStore.deselectAll();
}
// TODO: Remove issue.category:error filter when merging/deleting performance issues is supported
// This silently avoids performance issues for bulk actions
const queryExcludingPerformanceIssues = `${query ?? ''} issue.category:error`;
function handleDelete() {
actionSelectedGroups(itemIds => {
bulkDelete(
api,
{
orgId: organization.slug,
itemIds,
query: queryExcludingPerformanceIssues,
project: selection.projects,
environment: selection.environments,
...selection.datetime,
},
{
complete: () => {
onDelete();
},
}
);
});
}
function handleMerge() {
actionSelectedGroups(itemIds => {
mergeGroups(
api,
{
orgId: organization.slug,
itemIds,
query: queryExcludingPerformanceIssues,
project: selection.projects,
environment: selection.environments,
...selection.datetime,
},
{}
);
if (selection.projects[0]) {
const trackProject = ProjectsStore.getById(`${selection.projects[0]}`);
trackAnalytics('issues_stream.merged', {
organization,
project_id: trackProject?.id,
platform: trackProject?.platform,
items_merged: allInQuerySelected ? 'all_in_query' : itemIds?.length,
});
}
});
}
function handleUpdate(data?: any) {
if (data.status === 'ignored') {
const statusDetails = data.statusDetails.ignoreCount
? 'ignoreCount'
: data.statusDetails.ignoreDuration
? 'ignoreDuration'
: data.statusDetails.ignoreUserCount
? 'ignoreUserCount'
: undefined;
trackAnalytics('issues_stream.archived', {
action_status_details: statusDetails,
action_substatus: data.substatus,
organization,
});
}
actionSelectedGroups(itemIds => {
if (data?.inbox === false) {
onMarkReviewed?.(itemIds ?? []);
}
onActionTaken?.(itemIds ?? []);
// If `itemIds` is undefined then it means we expect to bulk update all items
// that match the query.
//
// We need to always respect the projects selected in the global selection header:
// * users with no global views requires a project to be specified
// * users with global views need to be explicit about what projects the query will run against
const projectConstraints = {project: selection.projects};
bulkUpdate(
api,
{
orgId: organization.slug,
itemIds,
data,
query,
environment: selection.environments,
...projectConstraints,
...selection.datetime,
},
{}
);
});
}
return (
{!disableActions && (
SelectedGroupStore.toggleSelectAll()}
checked={pageSelected || (anySelected ? 'indeterminate' : false)}
disabled={displayReprocessingActions}
/>
)}
{!displayReprocessingActions && (
{!disableActions && (
shouldConfirm(action, {pageSelected, selectedIdsSet})
}
onDelete={handleDelete}
onMerge={handleMerge}
onUpdate={handleUpdate}
/>
)}
)}
{!allResultsVisible && pageSelected && (
{allInQuerySelected ? (
queryCount >= BULK_LIMIT ? (
tct(
'Selected up to the first [count] issues that match this search query.',
{
count: BULK_LIMIT_STR,
}
)
) : (
tct('Selected all [count] issues that match this search query.', {
count: queryCount,
})
)
) : (
{tn(
'%s issue on this page selected.',
'%s issues on this page selected.',
numIssues
)}
setAllInQuerySelected(true)}
data-test-id="issue-list-select-all-notice-link"
>
{queryCount >= BULK_LIMIT
? tct(
'Select the first [count] issues that match this search query.',
{
count: BULK_LIMIT_STR,
}
)
: tct('Select all [count] issues that match this search query.', {
count: queryCount,
})}
)}
)}
);
}
function useSelectedGroupsState() {
const [allInQuerySelected, setAllInQuerySelected] = useState(false);
const selectedIds = useLegacyStore(SelectedGroupStore);
const selected = SelectedGroupStore.getSelectedIds();
const projects = [...selected]
.map(id => GroupStore.get(id))
.filter((group): group is Group => !!(group && group.project))
.map(group => group.project.slug);
const uniqProjects = uniq(projects);
// we only want selectedProjectSlug set if there is 1 project
// more or fewer should result in a null so that the action toolbar
// can behave correctly.
const selectedProjectSlug = uniqProjects.length === 1 ? uniqProjects[0] : undefined;
const pageSelected = SelectedGroupStore.allSelected();
const multiSelected = SelectedGroupStore.multiSelected();
const anySelected = SelectedGroupStore.anySelected();
const selectedIdsSet = SelectedGroupStore.getSelectedIds();
useEffect(() => {
setAllInQuerySelected(false);
}, [selectedIds]);
return {
pageSelected,
multiSelected,
anySelected,
allInQuerySelected,
selectedIdsSet,
selectedProjectSlug,
setAllInQuerySelected,
};
}
function shouldConfirm(
action: ConfirmAction,
{pageSelected, selectedIdsSet}: {pageSelected: boolean; selectedIdsSet: Set}
) {
switch (action) {
case ConfirmAction.RESOLVE:
case ConfirmAction.UNRESOLVE:
case ConfirmAction.ARCHIVE:
case ConfirmAction.UNBOOKMARK: {
return pageSelected && selectedIdsSet.size > 1;
}
case ConfirmAction.BOOKMARK:
return selectedIdsSet.size > 1;
case ConfirmAction.MERGE:
case ConfirmAction.DELETE:
default:
return true; // By default, should confirm ...
}
}
const StickyActions = styled(Sticky)`
z-index: ${p => p.theme.zIndex.issuesList.stickyHeader};
/* Remove border radius from the action bar when stuck. Without this there is
* a small gap where color can peek through. */
&[data-stuck] > div {
border-radius: 0;
}
`;
const ActionsBar = styled('div')`
display: flex;
min-height: 45px;
padding-top: ${space(1)};
padding-bottom: ${space(1)};
align-items: center;
background: ${p => p.theme.backgroundSecondary};
border: 1px solid ${p => p.theme.border};
border-top: none;
border-radius: ${p => p.theme.panelBorderRadius} ${p => p.theme.panelBorderRadius} 0 0;
margin: 0 -1px -1px;
`;
const ActionsCheckbox = styled('div')<{isReprocessingQuery: boolean}>`
display: flex;
align-items: center;
padding-left: ${space(2)};
margin-bottom: 1px;
${p => p.isReprocessingQuery && 'flex: 1'};
`;
const HeaderButtonsWrapper = styled('div')`
@media (min-width: ${p => p.theme.breakpoints.large}) {
width: 50%;
}
flex: 1;
margin: 0 ${space(1)};
display: grid;
gap: ${space(0.5)};
grid-auto-flow: column;
justify-content: flex-start;
white-space: nowrap;
`;
const SelectAllNotice = styled('div')`
display: flex;
flex-wrap: wrap;
justify-content: center;
a:not([role='button']) {
color: ${p => p.theme.linkColor};
border-bottom: none;
}
`;
const SelectAllLink = styled('a')`
margin-left: ${space(1)};
`;
export {IssueListActions};
export default IssueListActions;