diff --git a/static/app/components/stream/supergroups/supergroupCheckbox.tsx b/static/app/components/stream/supergroups/supergroupCheckbox.tsx
deleted file mode 100644
index d0aba581ff380a..00000000000000
--- a/static/app/components/stream/supergroups/supergroupCheckbox.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import {useCallback} from 'react';
-import styled from '@emotion/styled';
-
-import {Checkbox} from '@sentry/scraps/checkbox';
-
-import {t} from 'sentry/locale';
-import {useOptionalIssueSelectionActions} from 'sentry/views/issueList/issueSelectionContext';
-
-interface SupergroupCheckboxProps {
- matchedGroupIds: string[];
- selectedCount: number;
-}
-
-export function SupergroupCheckbox({
- matchedGroupIds,
- selectedCount,
-}: SupergroupCheckboxProps) {
- const actions = useOptionalIssueSelectionActions();
-
- const checkedState =
- selectedCount === 0
- ? false
- : selectedCount === matchedGroupIds.length
- ? true
- : ('indeterminate' as const);
-
- const handleChange = useCallback(
- (evt: React.MouseEvent) => {
- evt.stopPropagation();
- const nextValue = checkedState !== true;
- actions?.setSelectionForIds(matchedGroupIds, nextValue);
- },
- [actions, matchedGroupIds, checkedState]
- );
-
- if (!actions) {
- return null;
- }
-
- return (
-
- {}}
- onClick={handleChange}
- />
-
- );
-}
-
-export const CheckboxLabel = styled('label')`
- margin: 0;
- display: flex;
- align-items: center;
- justify-content: center;
-`;
-
-const CheckboxWithBackground = styled(Checkbox)`
- background-color: ${p => p.theme.tokens.background.primary};
-`;
diff --git a/static/app/components/stream/supergroups/supergroupRow.tsx b/static/app/components/stream/supergroups/supergroupRow.tsx
index 399e4522a307dc..9ded1f077c0fbe 100644
--- a/static/app/components/stream/supergroups/supergroupRow.tsx
+++ b/static/app/components/stream/supergroups/supergroupRow.tsx
@@ -1,4 +1,4 @@
-import {useMemo, useState} from 'react';
+import {useState} from 'react';
import styled from '@emotion/styled';
import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
@@ -11,21 +11,15 @@ import {Count} from 'sentry/components/count';
import {useDrawer} from 'sentry/components/globalDrawer';
import {PanelItem} from 'sentry/components/panels/panelItem';
import {Placeholder} from 'sentry/components/placeholder';
-import {
- CheckboxLabel,
- SupergroupCheckbox,
-} from 'sentry/components/stream/supergroups/supergroupCheckbox';
import {TimeSince} from 'sentry/components/timeSince';
import {IconStack} from 'sentry/icons';
import {t} from 'sentry/locale';
import {COLUMN_BREAKPOINTS} from 'sentry/views/issueList/actions/utils';
-import {useOptionalIssueSelectionSummary} from 'sentry/views/issueList/issueSelectionContext';
import type {AggregatedSupergroupStats} from 'sentry/views/issueList/supergroups/aggregateSupergroupStats';
import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer';
import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
interface SupergroupRowProps {
- matchedGroupIds: string[];
supergroup: SupergroupDetail;
aggregatedStats?: AggregatedSupergroupStats | null;
memberList?: IndexedMembersByProject;
@@ -33,11 +27,9 @@ interface SupergroupRowProps {
export function SupergroupRow({
supergroup,
- matchedGroupIds,
aggregatedStats,
memberList,
}: SupergroupRowProps) {
- const matchedCount = matchedGroupIds.length;
const {openDrawer, isDrawerOpen} = useDrawer();
const [isActive, setIsActive] = useState(false);
const handleClick = () => {
@@ -58,20 +50,6 @@ export function SupergroupRow({
);
};
- const summary = useOptionalIssueSelectionSummary();
- const selectedCount = useMemo(() => {
- if (!summary) {
- return 0;
- }
- let count = 0;
- for (const id of matchedGroupIds) {
- if (summary.records.get(id)) {
- count++;
- }
- }
- return count;
- }, [summary, matchedGroupIds]);
-
const highlighted = isActive && isDrawerOpen;
return (
@@ -79,10 +57,6 @@ export function SupergroupRow({
-
@@ -92,21 +66,15 @@ export function SupergroupRow({
{supergroup.error_type}
-
- {supergroup.code_area}
-
- {supergroup.code_area && matchedCount > 0 ? : null}
- {matchedCount > 0 ? (
- 0 ? 'primary' : 'muted'}
- bold={selectedCount > 0}
- >
- {selectedCount > 0
- ? `${selectedCount} / ${supergroup.group_ids.length} ${t('issues selected')}`
- : `${matchedCount} / ${supergroup.group_ids.length} ${t('issues matched')}`}
+ {supergroup.code_area ? (
+
+ {supergroup.code_area}
) : null}
+ {supergroup.code_area ? : null}
+
+ {`${supergroup.group_ids.length} ${t('issues')}`}
+
@@ -192,12 +160,6 @@ const Wrapper = styled(PanelItem)<{highlighted: boolean}>`
min-height: 82px;
background: ${p =>
p.highlighted ? p.theme.tokens.background.secondary : 'transparent'};
-
- &:not(:hover):not(:has(input:checked)):not(:has(input:indeterminate)) {
- ${CheckboxLabel} {
- ${p => p.theme.visuallyHidden};
- }
- }
`;
const Summary = styled('div')`
diff --git a/static/app/views/issueList/groupListBody.tsx b/static/app/views/issueList/groupListBody.tsx
index cb7d48cadff080..a4bb65dbc2bf4b 100644
--- a/static/app/views/issueList/groupListBody.tsx
+++ b/static/app/views/issueList/groupListBody.tsx
@@ -237,7 +237,6 @@ function GroupList({
diff --git a/static/app/views/issueList/supergroups/supergroupDrawer.tsx b/static/app/views/issueList/supergroups/supergroupDrawer.tsx
index 767d8e71d12ec1..8e5abada896b48 100644
--- a/static/app/views/issueList/supergroups/supergroupDrawer.tsx
+++ b/static/app/views/issueList/supergroups/supergroupDrawer.tsx
@@ -1,12 +1,19 @@
-import {Fragment, useState} from 'react';
+import {Fragment, useCallback, useMemo, useState} from 'react';
import styled from '@emotion/styled';
import {Badge} from '@sentry/scraps/badge';
import {Button} from '@sentry/scraps/button';
+import {Checkbox} from '@sentry/scraps/checkbox';
import {inlineCodeStyles} from '@sentry/scraps/code';
import {Container, Flex, Stack} from '@sentry/scraps/layout';
import {Heading, Text} from '@sentry/scraps/text';
+import {bulkDelete, bulkUpdate, mergeGroups} from 'sentry/actionCreators/group';
+import {
+ addErrorMessage,
+ addLoadingMessage,
+ clearIndicators,
+} from 'sentry/actionCreators/indicator';
import type {IndexedMembersByProject} from 'sentry/actionCreators/members';
import {
CrumbContainer,
@@ -15,8 +22,9 @@ import {
} from 'sentry/components/events/eventDrawer';
import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components';
import type {GroupListColumn} from 'sentry/components/issues/groupList';
-import {GroupListHeader} from 'sentry/components/issues/groupListHeader';
+import {IssueStreamHeaderLabel} from 'sentry/components/IssueStreamHeaderLabel';
import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants';
+import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {Panel} from 'sentry/components/panels/panel';
import {PanelBody} from 'sentry/components/panels/panelBody';
import {
@@ -26,14 +34,25 @@ import {
} from 'sentry/components/stream/group';
import {IconChevron, IconFilter, IconFocus} from 'sentry/icons';
import {t} from 'sentry/locale';
+import {GroupStore} from 'sentry/stores/groupStore';
import type {Group} from 'sentry/types/group';
import {apiOptions} from 'sentry/utils/api/apiOptions';
+import {uniq} from 'sentry/utils/array/uniq';
import {MarkedText} from 'sentry/utils/marked/markedText';
-import {useQuery} from 'sentry/utils/queryClient';
+import {useQuery, useQueryClient} from 'sentry/utils/queryClient';
+import {useApi} from 'sentry/utils/useApi';
import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
+import {ActionSet} from 'sentry/views/issueList/actions/actionSet';
+import {COLUMN_BREAKPOINTS, ConfirmAction} from 'sentry/views/issueList/actions/utils';
+import {
+ IssueSelectionProvider,
+ useIssueSelectionActions,
+ useIssueSelectionSummary,
+} from 'sentry/views/issueList/issueSelectionContext';
import {SupergroupFeedback} from 'sentry/views/issueList/supergroups/supergroupFeedback';
import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
+import type {IssueUpdateData} from 'sentry/views/issueList/types';
const DRAWER_COLUMNS: GroupListColumn[] = [
'graph',
@@ -198,7 +217,10 @@ function SupergroupIssueList({
if (isPending) {
return (
-
+
+ {t('Issue')}
+
+
{pageGroupIds.map(id => (
@@ -224,6 +246,8 @@ function SupergroupIssueList({
return 0;
});
+ const visibleGroupIds = sortedGroups.map(g => g.id);
+
return (
{matchedIds.size > 0 && (
@@ -234,34 +258,36 @@ function SupergroupIssueList({
)}
-
-
-
- {sortedGroups.map(group => {
- const members = memberList?.[group.project?.slug]
- ? memberList[group.project.slug]
- : undefined;
- return (
-
- {matchedIds.has(group.id) && (
-
-
-
- )}
-
-
- );
- })}
-
-
+
+
+
+
+ {sortedGroups.map(group => {
+ const members = memberList?.[group.project?.slug]
+ ? memberList[group.project.slug]
+ : undefined;
+ return (
+
+ {matchedIds.has(group.id) && (
+
+
+
+ )}
+
+
+ );
+ })}
+
+
+
{totalPages > 1 && (
@@ -289,6 +315,229 @@ function SupergroupIssueList({
);
}
+function DrawerActionsBar({groupIds}: {groupIds: string[]}) {
+ const api = useApi();
+ const organization = useOrganization();
+ const queryClient = useQueryClient();
+ const {selection} = usePageFilters();
+ const {toggleSelectAllVisible, deselectAll} = useIssueSelectionActions();
+ const {pageSelected, anySelected, multiSelected, selectedIdsSet} =
+ useIssueSelectionSummary();
+
+ const selectedProjectSlug = useMemo(() => {
+ const projects = [...selectedIdsSet]
+ .map(id => GroupStore.get(id))
+ .filter((group): group is Group => !!group?.project)
+ .map(group => group.project.slug);
+ const uniqProjects = uniq(projects);
+ return uniqProjects.length === 1 ? uniqProjects[0] : undefined;
+ }, [selectedIdsSet]);
+
+ const handleUpdate = useCallback(
+ (data: IssueUpdateData) => {
+ const itemIds = [...selectedIdsSet];
+ if (itemIds.length) {
+ addLoadingMessage(t('Saving changes\u2026'));
+ }
+ bulkUpdate(
+ api,
+ {
+ orgId: organization.slug,
+ itemIds,
+ data,
+ project: selection.projects,
+ environment: selection.environments,
+ ...selection.datetime,
+ },
+ {
+ success: () => {
+ clearIndicators();
+ for (const itemId of itemIds) {
+ queryClient.invalidateQueries({
+ queryKey: [`/organizations/${organization.slug}/issues/${itemId}/`],
+ exact: false,
+ });
+ }
+ },
+ error: () => {
+ clearIndicators();
+ addErrorMessage(t('Unable to update issues'));
+ },
+ }
+ );
+ deselectAll();
+ },
+ [api, organization.slug, selectedIdsSet, selection, queryClient, deselectAll]
+ );
+
+ const handleDelete = useCallback(() => {
+ const itemIds = [...selectedIdsSet];
+ bulkDelete(
+ api,
+ {
+ orgId: organization.slug,
+ itemIds,
+ project: selection.projects,
+ environment: selection.environments,
+ ...selection.datetime,
+ },
+ {}
+ );
+ deselectAll();
+ }, [api, organization.slug, selectedIdsSet, selection, deselectAll]);
+
+ const handleMerge = useCallback(() => {
+ const itemIds = [...selectedIdsSet];
+ mergeGroups(
+ api,
+ {
+ orgId: organization.slug,
+ itemIds,
+ project: selection.projects,
+ environment: selection.environments,
+ ...selection.datetime,
+ },
+ {}
+ );
+ deselectAll();
+ }, [api, organization.slug, selectedIdsSet, selection, deselectAll]);
+
+ const onShouldConfirm = useCallback(
+ (action: ConfirmAction) => {
+ switch (action) {
+ case ConfirmAction.RESOLVE:
+ case ConfirmAction.UNRESOLVE:
+ case ConfirmAction.ARCHIVE:
+ case ConfirmAction.SET_PRIORITY:
+ case ConfirmAction.UNBOOKMARK:
+ return pageSelected && selectedIdsSet.size > 1;
+ case ConfirmAction.BOOKMARK:
+ return selectedIdsSet.size > 1;
+ case ConfirmAction.MERGE:
+ case ConfirmAction.DELETE:
+ default:
+ return true;
+ }
+ },
+ [pageSelected, selectedIdsSet.size]
+ );
+
+ return (
+
+
+ {anySelected ? (
+
+
+
+ ) : (
+
+ {t('Issue')}
+
+
+ )}
+
+ );
+}
+
+function DrawerColumnHeaders() {
+ return (
+
+ {DRAWER_COLUMNS.includes('lastSeen') && (
+
+ {t('Last Seen')}
+
+ )}
+ {DRAWER_COLUMNS.includes('firstSeen') && (
+
+ {t('Age')}
+
+ )}
+ {DRAWER_COLUMNS.includes('graph') && (
+
+ {t('Graph')}
+
+ )}
+ {DRAWER_COLUMNS.includes('event') && (
+
+ {t('Events')}
+
+ )}
+ {DRAWER_COLUMNS.includes('users') && (
+
+ {t('Users')}
+
+ )}
+ {DRAWER_COLUMNS.includes('assignee') && (
+
+ {t('Assignee')}
+
+ )}
+
+ );
+}
+
+const ActionsBarContainer = styled('div')`
+ display: flex;
+ gap: ${p => p.theme.space.md};
+ height: 36px;
+ padding: 0;
+ padding-left: ${p => p.theme.space.xl};
+ align-items: center;
+ background: ${p => p.theme.tokens.background.secondary};
+ border-radius: ${p => p.theme.radius.md} ${p => p.theme.radius.md} 0 0;
+ border-bottom: 1px solid ${p => p.theme.tokens.border.primary};
+`;
+
+const HeaderButtonsWrapper = styled('div')`
+ flex: 1;
+ display: flex;
+ gap: ${p => p.theme.space.xs};
+ white-space: nowrap;
+`;
+
+const IssueLabel = styled(IssueStreamHeaderLabel)`
+ flex: 1;
+`;
+
+const ColumnLabel = styled(IssueStreamHeaderLabel)`
+ width: 60px;
+`;
+
+const GraphColumnLabel = styled(IssueStreamHeaderLabel)`
+ width: 175px;
+`;
+
+const AssigneeColumnLabel = styled(IssueStreamHeaderLabel)`
+ width: 66px;
+`;
+
+const LoadingHeader = styled('div')`
+ display: flex;
+ min-height: 36px;
+ padding: ${p => p.theme.space.xs} 0;
+ padding-left: ${p => p.theme.space.xl};
+ align-items: center;
+ background: ${p => p.theme.tokens.background.secondary};
+ border-radius: ${p => p.theme.radius.md} ${p => p.theme.radius.md} 0 0;
+ border-bottom: 1px solid ${p => p.theme.tokens.border.primary};
+`;
+
const DrawerContentBody = styled(DrawerBody)`
padding: 0;
`;
@@ -300,21 +549,19 @@ const PanelContainer = styled(Panel)`
const IssueRow = styled('div')`
position: relative;
- > * {
- /* Leave room for checkbox + filter icon */
- padding-left: 12px;
+ /* Hide the unread indicator — the filter icon replaces it in this context */
+ [data-test-id='unread-issue-indicator'] {
+ display: none;
}
`;
-const MatchedIcon = styled('div')`
+const MatchedIndicator = styled('div')`
position: absolute;
- top: 23px;
- /* Positioned after where the checkbox will go */
- left: -2px;
- padding-left: 0;
- transform: translateY(-50%);
- z-index: 1;
+ top: 14px;
+ left: 18px;
+ z-index: 2;
color: ${p => p.theme.tokens.graphics.accent.vibrant};
+ pointer-events: none;
`;
const StyledMarkedText = styled(MarkedText)`