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)`