diff --git a/static/app/components/acl/feature.tsx b/static/app/components/acl/feature.tsx index d96eedab7e9020..fd22e8c343c222 100644 --- a/static/app/components/acl/feature.tsx +++ b/static/app/components/acl/feature.tsx @@ -177,7 +177,6 @@ class Feature extends React.Component { customDisabledRender = hooks[0]; } } - const renderProps = { organization, project, diff --git a/static/app/components/organizations/globalSelectionHeader/globalSelectionHeader.tsx b/static/app/components/organizations/globalSelectionHeader/globalSelectionHeader.tsx index fdb68556ef5f9a..5d7ba77a5493c5 100644 --- a/static/app/components/organizations/globalSelectionHeader/globalSelectionHeader.tsx +++ b/static/app/components/organizations/globalSelectionHeader/globalSelectionHeader.tsx @@ -171,9 +171,6 @@ class GlobalSelectionHeader extends React.Component { searchQuery: '', }; - hasMultipleProjectSelection = () => - new Set(this.props.organization.features).has('global-views'); - // Returns an options object for `update*` actions getUpdateOptions = () => ({ save: true, @@ -346,10 +343,7 @@ class GlobalSelectionHeader extends React.Component { value={this.state.projects || this.props.selection.projects} onChange={this.handleChangeProjects} onUpdate={this.handleUpdateProjects} - multi={ - !disableMultipleProjectSelection && - this.hasMultipleProjectSelection() - } + disableMultipleProjectSelection={disableMultipleProjectSelection} {...(loadingProjects ? paginatedProjectSelectorCallbacks : {})} showIssueStreamLink={showIssueStreamLink} showProjectSettingsLink={showProjectSettingsLink} diff --git a/static/app/components/organizations/multipleProjectSelector.tsx b/static/app/components/organizations/multipleProjectSelector.tsx index a1499e646c1f51..b3a84c46eb2e42 100644 --- a/static/app/components/organizations/multipleProjectSelector.tsx +++ b/static/app/components/organizations/multipleProjectSelector.tsx @@ -3,6 +3,7 @@ import {withRouter, WithRouterProps} from 'react-router'; import {ClassNames} from '@emotion/react'; import styled from '@emotion/styled'; +import Feature from 'app/components/acl/feature'; import Button from 'app/components/button'; import Link from 'app/components/links/link'; import HeaderItem from 'app/components/organizations/headerItem'; @@ -27,7 +28,7 @@ type Props = WithRouterProps & { onChange: (selected: number[]) => unknown; onUpdate: () => unknown; isGlobalSelectionReady?: boolean; - multi?: boolean; + disableMultipleProjectSelection?: boolean; shouldForceProject?: boolean; forceProject?: MinimalProject | null; showIssueStreamLink?: boolean; @@ -42,7 +43,6 @@ type State = { class MultipleProjectSelector extends React.PureComponent { static defaultProps = { - multi: true, lockedMessageSubject: t('page'), }; @@ -50,6 +50,13 @@ class MultipleProjectSelector extends React.PureComponent { hasChanges: false, }; + get multi() { + const {organization, disableMultipleProjectSelection} = this.props; + return ( + !disableMultipleProjectSelection && organization.features.includes('global-views') + ); + } + // Reset "hasChanges" state and call `onUpdate` callback doUpdate = () => { this.setState({hasChanges: false}, this.props.onUpdate); @@ -93,12 +100,12 @@ class MultipleProjectSelector extends React.PureComponent { return; } - const {value, multi} = this.props; + const {value} = this.props; analytics('projectselector.update', { count: value.length, path: getRouteStringFromRoutes(this.props.router.routes), org_id: parseInt(this.props.organization.id, 10), - multi, + multi: this.multi, }); this.doUpdate(); @@ -139,9 +146,9 @@ class MultipleProjectSelector extends React.PureComponent { }; renderProjectName() { - const {forceProject, location, multi, organization, showIssueStreamLink} = this.props; + const {forceProject, location, organization, showIssueStreamLink} = this.props; - if (showIssueStreamLink && forceProject && multi) { + if (showIssueStreamLink && forceProject && this.multi) { return ( { value, projects, isGlobalSelectionReady, + disableMultipleProjectSelection, nonMemberProjects, - multi, organization, shouldForceProject, forceProject, @@ -190,6 +197,7 @@ class MultipleProjectSelector extends React.PureComponent { footerMessage, } = this.props; const selectedProjectIds = new Set(value); + const multi = this.multi; const allProjects = [...projects, ...nonMemberProjects]; const selected = allProjects.filter(project => @@ -246,7 +254,7 @@ class MultipleProjectSelector extends React.PureComponent { menuFooter={({actions}) => ( this.handleUpdate(actions)} @@ -305,20 +313,28 @@ class MultipleProjectSelector extends React.PureComponent { } } +type FeatureRenderProps = { + hasFeature: boolean; + renderShowAllButton?: (p: { + canShowAllProjects: boolean; + onButtonClick: () => void; + }) => React.ReactNode; +}; + type ControlProps = { organization: Organization; - onApply: (e: React.MouseEvent) => void; - onShowAllProjects: (e: React.MouseEvent) => void; - onShowMyProjects: (e: React.MouseEvent) => void; + onApply: () => void; + onShowAllProjects: () => void; + onShowMyProjects: () => void; selected?: Set; - multi?: boolean; + disableMultipleProjectSelection?: boolean; hasChanges?: boolean; message?: React.ReactNode; }; const SelectorFooterControls = ({ selected, - multi, + disableMultipleProjectSelection, hasChanges, onApply, onShowAllProjects, @@ -326,41 +342,55 @@ const SelectorFooterControls = ({ organization, message, }: ControlProps) => { - let showMyProjects = false; - let showAllProjects = false; - if (multi) { - showMyProjects = true; - - const hasGlobalRole = - organization.role === 'owner' || organization.role === 'manager'; - const hasOpenMembership = organization.features.includes('open-membership'); - const allSelected = selected && selected.has(ALL_ACCESS_PROJECTS); - if ((hasGlobalRole || hasOpenMembership) && !allSelected) { - showAllProjects = true; - showMyProjects = false; - } - } - // Nothing to show. - if (!(showAllProjects || showMyProjects || hasChanges || message)) { + if (disableMultipleProjectSelection && !hasChanges && !message) { return null; } + // see if we should show "All Projects" or "My Projects" if disableMultipleProjectSelection isn't true + const hasGlobalRole = organization.role === 'owner' || organization.role === 'manager'; + const hasOpenMembership = organization.features.includes('open-membership'); + const allSelected = selected && selected.has(ALL_ACCESS_PROJECTS); + + const canShowAllProjects = (hasGlobalRole || hasOpenMembership) && !allSelected; + const onProjectClick = canShowAllProjects ? onShowAllProjects : onShowMyProjects; + const buttonText = canShowAllProjects + ? t('Select All Projects') + : t('Select My Projects'); + return ( {message && {message}} - - {showAllProjects && ( - - )} - {showMyProjects && ( - + {!disableMultipleProjectSelection && ( + + {({renderShowAllButton, hasFeature}: FeatureRenderProps) => { + // if our hook is adding renderShowAllButton, render that + if (renderShowAllButton) { + return renderShowAllButton({ + onButtonClick: onProjectClick, + canShowAllProjects, + }); + } + // if no hook, render null if feature is disabled + if (!hasFeature) { + return null; + } + // otherwise render the buton + return ( + + ); + }} + )} + {hasChanges && ( {t('Apply Filter')} @@ -374,14 +404,20 @@ const SelectorFooterControls = ({ export default withRouter(MultipleProjectSelector); const FooterContainer = styled('div')` - padding: ${space(1)} 0; + display: flex; + justify-content: space-between; `; + const FooterActions = styled('div')` + padding: ${space(1)} 0; display: flex; justify-content: flex-end; & > * { margin-left: ${space(0.5)}; } + &:empty { + display: none; + } `; const SubmitButton = styled(Button)` animation: 0.1s ${growIn} ease-in; @@ -389,7 +425,7 @@ const SubmitButton = styled(Button)` const FooterMessage = styled('div')` font-size: ${p => p.theme.fontSizeSmall}; - padding: 0 ${space(0.5)}; + padding: ${space(1)} ${space(0.5)}; `; const StyledProjectSelector = styled(ProjectSelector)` diff --git a/static/app/types/hooks.tsx b/static/app/types/hooks.tsx index 0a7f1ac5e0dae9..aa85213ecc40fd 100644 --- a/static/app/types/hooks.tsx +++ b/static/app/types/hooks.tsx @@ -145,6 +145,7 @@ export type FeatureDisabledHooks = { 'feature-disabled:sso-saml2': FeatureDisabledHook; 'feature-disabled:trace-view-link': FeatureDisabledHook; 'feature-disabled:alert-wizard-performance': FeatureDisabledHook; + 'feature-disabled:project-selector-all-projects': FeatureDisabledHook; }; /** diff --git a/tests/js/spec/components/organizations/globalSelectionHeader.spec.jsx b/tests/js/spec/components/organizations/globalSelectionHeader.spec.jsx index fdc934e10b8afa..d817c98fdc62f9 100644 --- a/tests/js/spec/components/organizations/globalSelectionHeader.spec.jsx +++ b/tests/js/spec/components/organizations/globalSelectionHeader.spec.jsx @@ -1031,7 +1031,7 @@ describe('GlobalSelectionHeader', function () { // My projects in the footer expect( projectSelector.find('SelectorFooterControls Button').first().text() - ).toEqual('View My Projects'); + ).toEqual('Select My Projects'); }); it('shows "All Projects" button based on features', async function () { @@ -1056,7 +1056,7 @@ describe('GlobalSelectionHeader', function () { // All projects in the footer expect( projectSelector.find('SelectorFooterControls Button').first().text() - ).toEqual('View All Projects'); + ).toEqual('Select All Projects'); }); it('shows "All Projects" button based on role', async function () { @@ -1081,7 +1081,7 @@ describe('GlobalSelectionHeader', function () { // All projects in the footer expect( projectSelector.find('SelectorFooterControls Button').first().text() - ).toEqual('View All Projects'); + ).toEqual('Select All Projects'); }); it('shows "My Projects" when "all projects" is selected', async function () { @@ -1106,7 +1106,7 @@ describe('GlobalSelectionHeader', function () { // My projects in the footer expect( projectSelector.find('SelectorFooterControls Button').first().text() - ).toEqual('View My Projects'); + ).toEqual('Select My Projects'); }); });