Browse Source

feat(issues): Move issue actions to top of page (#39523)

Scott Cooper 2 years ago
parent
commit
34976102e0

+ 7 - 2
docs-ui/stories/assets/icons/data.tsx

@@ -515,9 +515,14 @@ export const icons: IconData[] = [
     keywords: ['feature', 'promotion', 'fresh', 'new'],
   },
   {
-    id: 'bell',
+    id: 'subscribed',
     groups: ['action'],
-    keywords: ['alert', 'notification', 'ring'],
+    keywords: ['alert', 'notification', 'subscribe', 'bell', 'ring'],
+  },
+  {
+    id: 'unsubscribed',
+    groups: ['action'],
+    keywords: ['alert', 'notification', 'subscribe', 'bell', 'ring'],
   },
   {
     id: 'siren',

+ 71 - 40
static/app/components/actions/ignore.tsx

@@ -7,6 +7,7 @@ import {openConfirmModal} from 'sentry/components/confirm';
 import CustomIgnoreCountModal from 'sentry/components/customIgnoreCountModal';
 import CustomIgnoreDurationModal from 'sentry/components/customIgnoreDurationModal';
 import DropdownMenuControl from 'sentry/components/dropdownMenuControl';
+import type {MenuItemProps} from 'sentry/components/dropdownMenuItem';
 import Tooltip from 'sentry/components/tooltip';
 import {IconChevron, IconMute} from 'sentry/icons';
 import {t, tn} from 'sentry/locale';
@@ -39,25 +40,18 @@ const IGNORE_WINDOWS: SelectValue<number>[] = [
   {value: ONE_HOUR * 24 * 7, label: t('per week')},
 ];
 
-type Props = {
-  onUpdate: (params: GroupStatusResolution) => void;
-  confirmLabel?: string;
-  confirmMessage?: (
-    statusDetails: ResolutionStatusDetails | undefined
-  ) => React.ReactNode;
-  disabled?: boolean;
-  isIgnored?: boolean;
-  shouldConfirm?: boolean;
-};
-
-const IgnoreActions = ({
-  onUpdate,
-  disabled,
-  shouldConfirm,
+/**
+ * Create the dropdown submenus
+ */
+export function getIgnoreActions({
+  confirmLabel,
   confirmMessage,
-  confirmLabel = t('Ignore'),
-  isIgnored = false,
-}: Props) => {
+  shouldConfirm,
+  onUpdate,
+}: Pick<
+  IgnoreActionProps,
+  'shouldConfirm' | 'confirmMessage' | 'confirmLabel' | 'onUpdate'
+>) {
   const onIgnore = (
     statusDetails: ResolutionStatusDetails | undefined = {},
     {bypassConfirm} = {bypassConfirm: false}
@@ -78,22 +72,6 @@ const IgnoreActions = ({
     onIgnore(statusDetails, {bypassConfirm: true});
   };
 
-  if (isIgnored) {
-    return (
-      <Tooltip title={t('Change status to unresolved')}>
-        <Button
-          priority="primary"
-          size="xs"
-          onClick={() =>
-            onUpdate({status: ResolutionStatus.UNRESOLVED, statusDetails: {}})
-          }
-          aria-label={t('Unignore')}
-          icon={<IconMute size="xs" />}
-        />
-      </Tooltip>
-    );
-  }
-
   const openCustomIgnoreDuration = () =>
     openModal(deps => (
       <CustomIgnoreDurationModal
@@ -128,7 +106,8 @@ const IgnoreActions = ({
       />
     ));
 
-  const dropdownItems = [
+  // Move submenu placement when ignore used in top right menu
+  const dropdownItems: MenuItemProps[] = [
     {
       key: 'for',
       label: t('For\u2026'),
@@ -219,16 +198,68 @@ const IgnoreActions = ({
       ],
     },
   ];
+  return {dropdownItems, onIgnore};
+}
+
+type IgnoreActionProps = {
+  onUpdate: (params: GroupStatusResolution) => void;
+  className?: string;
+  confirmLabel?: string;
+  confirmMessage?: (
+    statusDetails: ResolutionStatusDetails | undefined
+  ) => React.ReactNode;
+  disableTooltip?: boolean;
+  disabled?: boolean;
+  hideIcon?: boolean;
+  isIgnored?: boolean;
+  shouldConfirm?: boolean;
+  size?: 'xs' | 'sm';
+};
+
+const IgnoreActions = ({
+  onUpdate,
+  disabled,
+  shouldConfirm,
+  confirmMessage,
+  className,
+  hideIcon,
+  disableTooltip,
+  size = 'xs',
+  confirmLabel = t('Ignore'),
+  isIgnored = false,
+}: IgnoreActionProps) => {
+  if (isIgnored) {
+    return (
+      <Tooltip title={t('Change status to unresolved')}>
+        <Button
+          priority="primary"
+          size="xs"
+          onClick={() =>
+            onUpdate({status: ResolutionStatus.UNRESOLVED, statusDetails: {}})
+          }
+          aria-label={t('Unignore')}
+          icon={<IconMute size="xs" />}
+        />
+      </Tooltip>
+    );
+  }
+
+  const {dropdownItems, onIgnore} = getIgnoreActions({
+    confirmLabel,
+    onUpdate,
+    shouldConfirm,
+    confirmMessage,
+  });
 
   return (
-    <ButtonBar merged>
+    <ButtonBar className={className} merged>
       <IgnoreButton
-        size="xs"
-        tooltipProps={{delay: 300, disabled}}
+        size={size}
+        tooltipProps={{delay: 300, disabled: disabled || disableTooltip}}
         title={t(
           'Silences alerts for this issue and removes it from the issue stream by default.'
         )}
-        icon={<IconMute size="xs" />}
+        icon={hideIcon ? null : <IconMute size={size} />}
         onClick={() => onIgnore()}
         disabled={disabled}
       >
@@ -240,7 +271,7 @@ const IgnoreActions = ({
           <DropdownTrigger
             {...triggerProps}
             aria-label={t('Ignore options')}
-            size="xs"
+            size={size}
             icon={<IconChevron direction="down" size="xs" />}
             disabled={disabled}
           />

+ 22 - 7
static/app/components/actions/resolve.tsx

@@ -8,6 +8,7 @@ import {openConfirmModal} from 'sentry/components/confirm';
 import CustomCommitsResolutionModal from 'sentry/components/customCommitsResolutionModal';
 import CustomResolutionModal from 'sentry/components/customResolutionModal';
 import DropdownMenuControl from 'sentry/components/dropdownMenuControl';
+import type {MenuItemProps} from 'sentry/components/dropdownMenuItem';
 import Tooltip from 'sentry/components/tooltip';
 import {IconCheckmark, IconChevron} from 'sentry/icons';
 import {t} from 'sentry/locale';
@@ -35,11 +36,15 @@ type Props = {
   organization: Organization;
   confirmMessage?: React.ReactNode;
   disableDropdown?: boolean;
+  disableTooltip?: boolean;
   disabled?: boolean;
+  hideIcon?: boolean;
   latestRelease?: Release;
+  priority?: 'primary';
   projectFetchError?: boolean;
   projectSlug?: string;
   shouldConfirm?: boolean;
+  size?: 'xs' | 'sm';
 } & Partial<typeof defaultProps>;
 
 class ResolveActions extends Component<Props> {
@@ -133,6 +138,8 @@ class ResolveActions extends Component<Props> {
       disabled,
       confirmLabel,
       disableDropdown,
+      size = 'xs',
+      priority,
     } = this.props;
 
     if (isResolved) {
@@ -152,7 +159,7 @@ class ResolveActions extends Component<Props> {
       });
     };
 
-    const items = [
+    const items: MenuItemProps[] = [
       {
         key: 'next-release',
         label: t('The next release'),
@@ -187,13 +194,14 @@ class ResolveActions extends Component<Props> {
 
     return (
       <DropdownMenuControl
-        size="sm"
         items={items}
         trigger={triggerProps => (
           <DropdownTrigger
             {...triggerProps}
+            type="button"
+            size={size}
+            priority={priority}
             aria-label={t('More resolve options')}
-            size="xs"
             icon={<IconChevron direction="down" size="xs" />}
             disabled={isDisabled}
           />
@@ -248,6 +256,10 @@ class ResolveActions extends Component<Props> {
       disabled,
       confirmLabel,
       projectFetchError,
+      disableTooltip,
+      priority,
+      size = 'xs',
+      hideIcon = false,
     } = this.props;
 
     if (isResolved) {
@@ -266,12 +278,14 @@ class ResolveActions extends Component<Props> {
       <Tooltip disabled={!projectFetchError} title={t('Error fetching project')}>
         <ButtonBar merged>
           <ResolveButton
-            size="xs"
+            type="button"
+            priority={priority}
+            size={size}
             title={t(
               'Resolves the issue. The issue will get unresolved if it happens again.'
             )}
-            tooltipProps={{delay: 300, disabled}}
-            icon={<IconCheckmark size="xs" />}
+            tooltipProps={{delay: 300, disabled: disabled || disableTooltip}}
+            icon={hideIcon ? null : <IconCheckmark size={size} />}
             onClick={onResolve}
             disabled={disabled}
           >
@@ -286,9 +300,10 @@ class ResolveActions extends Component<Props> {
 
 export default withOrganization(ResolveActions);
 
-const ResolveButton = styled(Button)`
+const ResolveButton = styled(Button)<{priority?: 'primary'}>`
   box-shadow: none;
   border-radius: ${p => p.theme.borderRadiusLeft};
+  ${p => (p.priority === 'primary' ? `border-right-color: ${p.theme.background};` : '')}
 `;
 
 const DropdownTrigger = styled(Button)`

+ 4 - 0
static/app/components/dropdownMenuItem.tsx

@@ -26,6 +26,10 @@ export type MenuItemProps = MenuListItemProps & {
    * `isSubmenu` is true, then they will be rendered together in a sub-menu.
    */
   children?: MenuItemProps[];
+  /**
+   * Plass a class name to the menu item.
+   */
+  className?: string;
   /**
    * Hide item from the dropdown menu. Note: this will also remove the item
    * from the selection manager.

+ 4 - 0
static/app/components/environmentPageFilter.tsx

@@ -4,6 +4,7 @@ import styled from '@emotion/styled';
 
 import {updateEnvironments} from 'sentry/actionCreators/pageFilters';
 import Badge from 'sentry/components/badge';
+import type {ButtonProps} from 'sentry/components/button';
 import EnvironmentSelector from 'sentry/components/organizations/environmentSelector';
 import PageFilterDropdownButton from 'sentry/components/organizations/pageFilters/pageFilterDropdownButton';
 import PageFilterPinIndicator from 'sentry/components/organizations/pageFilters/pageFilterPinIndicator';
@@ -30,6 +31,7 @@ type Props = {
    * Reset these URL params when we fire actions (custom routing only)
    */
   resetParamsOnChange?: string[];
+  size?: ButtonProps['size'];
 };
 
 function EnvironmentPageFilter({
@@ -38,6 +40,7 @@ function EnvironmentPageFilter({
   alignDropdown,
   disabled,
   maxTitleLength = 20,
+  size = 'md',
 }: Props) {
   const {projects, initiallyLoaded: projectsLoaded} = useProjects();
   const organization = useOrganization();
@@ -68,6 +71,7 @@ function EnvironmentPageFilter({
         highlighted={desyncedFilters.has('environments')}
         data-test-id="page-filter-environment-selector"
         disabled={disabled}
+        size={size}
       >
         <DropdownTitle>
           <PageFilterPinIndicator filter="environments">

+ 5 - 3
static/app/components/group/sidebar.tsx

@@ -209,9 +209,11 @@ class BaseGroupSidebar extends Component<Props, State> {
 
     return (
       <Container>
-        <PageFiltersContainer>
-          <EnvironmentPageFilter alignDropdown="right" />
-        </PageFiltersContainer>
+        {!organization.features.includes('issue-actions-v2') && (
+          <PageFiltersContainer>
+            <EnvironmentPageFilter alignDropdown="right" />
+          </PageFiltersContainer>
+        )}
 
         <Feature organization={organization} features={['issue-details-owners']}>
           <OwnedBy group={group} project={project} organization={organization} />

+ 3 - 2
static/app/components/organizations/pageFilters/pageFilterDropdownButton.tsx

@@ -10,9 +10,8 @@ type Props = {
   highlighted?: boolean;
 };
 
-export default styled(DropdownButton)<Props>`
+const PageFilterDropdownButton = styled(DropdownButton)<Props>`
   width: 100%;
-  height: 40px;
   text-overflow: ellipsis;
   ${p =>
     p.highlighted &&
@@ -26,3 +25,5 @@ export default styled(DropdownButton)<Props>`
     }
   `}
 `;
+
+export default PageFilterDropdownButton;

+ 0 - 16
static/app/icons/iconBell.tsx

@@ -1,16 +0,0 @@
-import {forwardRef} from 'react';
-
-import {SvgIcon, SVGIconProps} from './svgIcon';
-
-const IconBell = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
-  return (
-    <SvgIcon {...props} ref={ref}>
-      <path d="M7.00012 15.9999C6.30168 15.9972 5.63275 15.7179 5.13982 15.2231C4.64688 14.7283 4.37011 14.0583 4.37012 13.3599C4.37012 13.161 4.44913 12.9702 4.58979 12.8295C4.73044 12.6889 4.9212 12.6099 5.12012 12.6099C5.31903 12.6099 5.5098 12.6889 5.65045 12.8295C5.7911 12.9702 5.87012 13.161 5.87012 13.3599C5.87012 13.6596 5.98917 13.947 6.20109 14.1589C6.413 14.3708 6.70042 14.4899 7.00012 14.4899C7.29981 14.4899 7.58723 14.3708 7.79915 14.1589C8.01106 13.947 8.13012 13.6596 8.13012 13.3599C8.13012 13.161 8.20913 12.9702 8.34979 12.8295C8.49044 12.6889 8.68121 12.6099 8.88012 12.6099C9.07903 12.6099 9.26979 12.6889 9.41045 12.8295C9.5511 12.9702 9.63012 13.161 9.63012 13.3599C9.63012 14.0583 9.35335 14.7283 8.86042 15.2231C8.36748 15.7179 7.69855 15.9972 7.00012 15.9999Z" />
-      <path d="M13 14.07H1.00001C0.895161 14.0708 0.791282 14.0499 0.694908 14.0086C0.598533 13.9673 0.511748 13.9065 0.44001 13.83C0.370242 13.7536 0.317284 13.6633 0.284556 13.5652C0.251829 13.467 0.24006 13.363 0.25001 13.26L0.82001 6.71V6.65C0.757871 4.94869 1.37402 3.29235 2.53296 2.04528C3.6919 0.798201 5.29871 0.0625085 7.00001 0C8.69767 0.0574355 10.3034 0.785325 11.4655 2.02425C12.6275 3.26317 13.2513 4.91214 13.2 6.61C13.2047 6.62973 13.2047 6.65027 13.2 6.67L13.77 13.22C13.78 13.323 13.7682 13.427 13.7355 13.5252C13.7027 13.6233 13.6498 13.7136 13.58 13.79C13.5097 13.8763 13.4214 13.9461 13.3211 13.9945C13.2209 14.0429 13.1113 14.0686 13 14.07ZM1.80001 12.57H12.2L11.7 6.84V6.65C11.7515 5.34986 11.2859 4.08226 10.4051 3.12459C9.52424 2.16692 8.29991 1.59716 7.00001 1.54C5.70011 1.59716 4.47578 2.16692 3.59495 3.12459C2.71412 4.08226 2.24851 5.34986 2.30001 6.65V6.78L1.80001 12.57Z" />
-    </SvgIcon>
-  );
-});
-
-IconBell.displayName = 'IconBell';
-
-export {IconBell};

+ 15 - 0
static/app/icons/iconSubscribed.tsx

@@ -0,0 +1,15 @@
+import {forwardRef} from 'react';
+
+import {SvgIcon, SVGIconProps} from './svgIcon';
+
+const IconSubscribed = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
+  return (
+    <SvgIcon {...props} ref={ref}>
+      <path d="M10.533 14.07h3.466a.76.76 0 0 0 .58-.28.74.74 0 0 0 .19-.57l-.57-6.55a.13.13 0 0 0 0-.06A6.42 6.42 0 0 0 8 0a6.42 6.42 0 0 0-6.18 6.65v.06l-.57 6.55a.74.74 0 0 0 .19.57.76.76 0 0 0 .56.24h3.468A2.64 2.64 0 0 0 8 16a2.64 2.64 0 0 0 2.533-1.93Zm-1.654 0H7.121a1.13 1.13 0 0 0 1.758 0Zm4.32-1.5H2.8l.5-5.79v-.13A4.92 4.92 0 0 1 8 1.54a4.92 4.92 0 0 1 4.7 5.11v.19l.5 5.73Z" />
+    </SvgIcon>
+  );
+});
+
+IconSubscribed.displayName = 'IconSubscribe';
+
+export {IconSubscribed};

+ 16 - 0
static/app/icons/iconUnsubscribed.tsx

@@ -0,0 +1,16 @@
+import {forwardRef} from 'react';
+
+import {SvgIcon, SVGIconProps} from './svgIcon';
+
+const IconUnsubscribed = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
+  return (
+    <SvgIcon {...props} ref={ref}>
+      <path d="M10.533 14.07h3.466a.76.76 0 0 0 .58-.28.74.74 0 0 0 .19-.57l-.57-6.55a.13.13 0 0 0 0-.06A6.42 6.42 0 0 0 8 0a6.42 6.42 0 0 0-6.18 6.65v.06l-.57 6.55a.74.74 0 0 0 .19.57.76.76 0 0 0 .56.24h3.468A2.64 2.64 0 0 0 8 16a2.64 2.64 0 0 0 2.533-1.93Zm-1.654 0H7.121a1.13 1.13 0 0 0 1.758 0Zm4.32-1.5H2.8l.5-5.79v-.13A4.92 4.92 0 0 1 8 1.54a4.92 4.92 0 0 1 4.7 5.11v.19l.5 5.73Z" />
+      <path d="M.801 16.5a.798.798 0 0 1-.563-.234.786.786 0 0 1 0-1.127L14.635.733a.798.798 0 0 1 1.127 0 .787.787 0 0 1 0 1.127L1.365 16.266A.84.84 0 0 1 .8 16.5Z" />
+    </SvgIcon>
+  );
+});
+
+IconUnsubscribed.displayName = 'IconUnsubscribed';
+
+export {IconUnsubscribed};

Some files were not shown because too many files changed in this diff