Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion static/app/components/acl/feature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ class Feature extends React.Component<Props> {
customDisabledRender = hooks[0];
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidenwang I did some stuff to this file then reverted it. Too lazy to revert this whitespace change lol.

const renderProps = {
organization,
project,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,6 @@ class GlobalSelectionHeader extends React.Component<Props, State> {
searchQuery: '',
};

hasMultipleProjectSelection = () =>
new Set(this.props.organization.features).has('global-views');

// Returns an options object for `update*` actions
getUpdateOptions = () => ({
save: true,
Expand Down Expand Up @@ -346,10 +343,7 @@ class GlobalSelectionHeader extends React.Component<Props, State> {
value={this.state.projects || this.props.selection.projects}
onChange={this.handleChangeProjects}
onUpdate={this.handleUpdateProjects}
multi={
!disableMultipleProjectSelection &&
this.hasMultipleProjectSelection()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we need to render the new hook project-selector-all-projects:customization even if the feature is unavailable, we need to push the logic down into the child component

}
disableMultipleProjectSelection={disableMultipleProjectSelection}
{...(loadingProjects ? paginatedProjectSelectorCallbacks : {})}
showIssueStreamLink={showIssueStreamLink}
showProjectSettingsLink={showProjectSettingsLink}
Expand Down
118 changes: 77 additions & 41 deletions static/app/components/organizations/multipleProjectSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -42,14 +43,20 @@ type State = {

class MultipleProjectSelector extends React.PureComponent<Props, State> {
static defaultProps = {
multi: true,
lockedMessageSubject: t('page'),
};

state: 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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -181,15 +188,16 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
value,
projects,
isGlobalSelectionReady,
disableMultipleProjectSelection,
nonMemberProjects,
multi,
organization,
shouldForceProject,
forceProject,
showProjectSettingsLink,
footerMessage,
} = this.props;
const selectedProjectIds = new Set(value);
const multi = this.multi;

const allProjects = [...projects, ...nonMemberProjects];
const selected = allProjects.filter(project =>
Expand Down Expand Up @@ -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)}
Expand Down Expand Up @@ -305,62 +313,84 @@ 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,
onShowMyProjects,
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')}
Expand All @@ -374,22 +404,28 @@ 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;
`;

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)`
Expand Down
1 change: 1 addition & 0 deletions static/app/types/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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 () {
Expand All @@ -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 () {
Expand All @@ -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');
});
});

Expand Down