|
@@ -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<Props, State> {
|
|
|
static defaultProps = {
|
|
|
- multi: true,
|
|
|
lockedMessageSubject: t('page'),
|
|
|
};
|
|
|
|
|
@@ -50,6 +50,13 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
|
|
|
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<Props, State> {
|
|
|
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<Props, State> {
|
|
|
};
|
|
|
|
|
|
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 (
|
|
|
<Tooltip title={t('Issues Stream')} position="bottom">
|
|
|
<StyledLink
|
|
@@ -181,8 +188,8 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
|
|
|
value,
|
|
|
projects,
|
|
|
isGlobalSelectionReady,
|
|
|
+ disableMultipleProjectSelection,
|
|
|
nonMemberProjects,
|
|
|
- multi,
|
|
|
organization,
|
|
|
shouldForceProject,
|
|
|
forceProject,
|
|
@@ -190,6 +197,7 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
|
|
|
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<Props, State> {
|
|
|
menuFooter={({actions}) => (
|
|
|
<SelectorFooterControls
|
|
|
selected={selectedProjectIds}
|
|
|
- multi={multi}
|
|
|
+ disableMultipleProjectSelection={disableMultipleProjectSelection}
|
|
|
organization={organization}
|
|
|
hasChanges={this.state.hasChanges}
|
|
|
onApply={() => this.handleUpdate(actions)}
|
|
@@ -305,20 +313,28 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+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<number>;
|
|
|
- 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 (
|
|
|
<FooterContainer>
|
|
|
{message && <FooterMessage>{message}</FooterMessage>}
|
|
|
-
|
|
|
<FooterActions>
|
|
|
- {showAllProjects && (
|
|
|
- <Button onClick={onShowAllProjects} priority="default" size="xsmall">
|
|
|
- {t('View All Projects')}
|
|
|
- </Button>
|
|
|
- )}
|
|
|
- {showMyProjects && (
|
|
|
- <Button onClick={onShowMyProjects} priority="default" size="xsmall">
|
|
|
- {t('View My Projects')}
|
|
|
- </Button>
|
|
|
+ {!disableMultipleProjectSelection && (
|
|
|
+ <Feature
|
|
|
+ features={['organizations:global-views']}
|
|
|
+ organization={organization}
|
|
|
+ hookName="feature-disabled:project-selector-all-projects"
|
|
|
+ renderDisabled={false}
|
|
|
+ >
|
|
|
+ {({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 (
|
|
|
+ <Button priority="default" size="xsmall" onClick={onProjectClick}>
|
|
|
+ {buttonText}
|
|
|
+ </Button>
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ </Feature>
|
|
|
)}
|
|
|
+
|
|
|
{hasChanges && (
|
|
|
<SubmitButton onClick={onApply} size="xsmall" priority="primary">
|
|
|
{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)`
|