From 9c052402520a4f5f0c5f7fc81e1cb40527073495 Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Fri, 8 Oct 2021 11:08:36 -0700 Subject: [PATCH 1/9] adds project selector hook --- static/app/components/acl/feature.tsx | 1 - .../globalSelectionHeader.tsx | 8 +- .../organizations/multipleProjectSelector.tsx | 90 +++++++++++-------- static/app/stores/hookStore.tsx | 1 + static/app/types/hooks.tsx | 12 +++ 5 files changed, 66 insertions(+), 46 deletions(-) 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 7ed46c71748bcb..c304e8dda00da3 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..071e688f8824ec 100644 --- a/static/app/components/organizations/multipleProjectSelector.tsx +++ b/static/app/components/organizations/multipleProjectSelector.tsx @@ -4,6 +4,7 @@ import {ClassNames} from '@emotion/react'; import styled from '@emotion/styled'; import Button from 'app/components/button'; +import HookOrDefault from 'app/components/hookOrDefault'; import Link from 'app/components/links/link'; import HeaderItem from 'app/components/organizations/headerItem'; import PlatformList from 'app/components/platformList'; @@ -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,26 @@ class MultipleProjectSelector extends React.PureComponent { } } +const MultiProjectOverride = HookOrDefault({ + hookName: 'project-selector-all-projects:customization', + defaultComponent: ({children, defaultButtonText, defaultOnClick}) => + children({buttonText: defaultButtonText, onClick: defaultOnClick}), +}); + 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 +340,41 @@ 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 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 projectText = canShowAllProjects + ? t('Select All Projects') + : t('Select My Projects'); + return ( {message && {message}} - {showAllProjects && ( - - )} - {showMyProjects && ( - + {!disableMultipleProjectSelection && ( + + {({buttonText, ...rest}) => ( + + )} + )} + {hasChanges && ( {t('Apply Filter')} diff --git a/static/app/stores/hookStore.tsx b/static/app/stores/hookStore.tsx index 3a78d3f6186d2d..a4155b912d2069 100644 --- a/static/app/stores/hookStore.tsx +++ b/static/app/stores/hookStore.tsx @@ -65,6 +65,7 @@ const validHookNames = new Set([ 'onboarding:extra-chrome', 'onboarding-wizard:skip-help', 'organization:header', + 'project-selector-all-projects:customization', 'routes', 'routes:admin', 'routes:api', diff --git a/static/app/types/hooks.tsx b/static/app/types/hooks.tsx index 0a7f1ac5e0dae9..922fa4f4b2691a 100644 --- a/static/app/types/hooks.tsx +++ b/static/app/types/hooks.tsx @@ -90,6 +90,7 @@ export type ComponentHooks = { export type CustomizationHooks = { 'integrations:feature-gates': IntegrationsFeatureGatesHook; 'member-invite-modal:customization': InviteModalCustomizationHook; + 'project-selector-all-projects:customization': MultiProjectSelectButtonHook; }; /** @@ -530,3 +531,14 @@ type InviteModalCustomizationHook = () => React.ComponentType<{ sendInvites: () => void; }) => React.ReactElement; }>; + +type MultiProjectSelectButtonHook = () => React.ComponentType<{ + defaultButtonText: string; + defaultOnClick: () => void; + canShowAllProjects: boolean; + children: (opts: { + buttonText: React.ReactNode; + onClick: () => void; + icon?: React.ReactNode; + }) => React.ReactElement; +}>; From 5fd49d317677df3924b64339e4942ccca6f0a504 Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Wed, 20 Oct 2021 12:13:16 -0700 Subject: [PATCH 2/9] update hook --- .../organizations/multipleProjectSelector.tsx | 10 ++++++++-- static/app/types/hooks.tsx | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/static/app/components/organizations/multipleProjectSelector.tsx b/static/app/components/organizations/multipleProjectSelector.tsx index 071e688f8824ec..8d5f9a46600eb4 100644 --- a/static/app/components/organizations/multipleProjectSelector.tsx +++ b/static/app/components/organizations/multipleProjectSelector.tsx @@ -315,8 +315,13 @@ class MultipleProjectSelector extends React.PureComponent { const MultiProjectOverride = HookOrDefault({ hookName: 'project-selector-all-projects:customization', - defaultComponent: ({children, defaultButtonText, defaultOnClick}) => - children({buttonText: defaultButtonText, onClick: defaultOnClick}), + defaultComponent: ({children, defaultButtonText, defaultOnClick, organization}) => { + // render nothing if feature unavailable + if (!organization.features.includes('global-views')) { + return null; + } + return children({buttonText: defaultButtonText, onClick: defaultOnClick}); + }, }); type ControlProps = { @@ -366,6 +371,7 @@ const SelectorFooterControls = ({ defaultButtonText={projectText} defaultOnClick={onProjectClick} canShowAllProjects={canShowAllProjects} + organization={organization} > {({buttonText, ...rest}) => ( - )} - + {({renderDisabledView}: FeatureRenderProps) => + renderDisabledView ? ( + renderDisabledView({ + onButtonClick: onProjectClick, + canShowAllProjects, + }) + ) : ( + + ) + } + )} {hasChanges && ( diff --git a/static/app/types/hooks.tsx b/static/app/types/hooks.tsx index 8db4ffa10d896f..aa85213ecc40fd 100644 --- a/static/app/types/hooks.tsx +++ b/static/app/types/hooks.tsx @@ -90,7 +90,6 @@ export type ComponentHooks = { export type CustomizationHooks = { 'integrations:feature-gates': IntegrationsFeatureGatesHook; 'member-invite-modal:customization': InviteModalCustomizationHook; - 'project-selector-all-projects:customization': MultiProjectSelectButtonHook; }; /** @@ -146,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; }; /** @@ -531,15 +531,3 @@ type InviteModalCustomizationHook = () => React.ComponentType<{ sendInvites: () => void; }) => React.ReactElement; }>; - -type MultiProjectSelectButtonHook = () => React.ComponentType<{ - organization: Organization; - defaultButtonText: string; - defaultOnClick: () => void; - canShowAllProjects: boolean; - children: (opts: { - buttonText: React.ReactNode; - onClick: () => void; - icon?: React.ReactNode; - }) => React.ReactElement; -}>; From 0d2b2719ef49d4cca452ce48a36c8d1daa0c2b69 Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Wed, 20 Oct 2021 17:18:38 -0700 Subject: [PATCH 5/9] rename --- .../components/organizations/multipleProjectSelector.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/app/components/organizations/multipleProjectSelector.tsx b/static/app/components/organizations/multipleProjectSelector.tsx index cc732f185d9162..e4e8462ab166d0 100644 --- a/static/app/components/organizations/multipleProjectSelector.tsx +++ b/static/app/components/organizations/multipleProjectSelector.tsx @@ -314,7 +314,7 @@ class MultipleProjectSelector extends React.PureComponent { } type FeatureRenderProps = { - renderDisabledView?: (p: { + renderShowAllButton?: (p: { canShowAllProjects: boolean; onButtonClick: () => void; }) => React.ReactNode; @@ -369,9 +369,9 @@ const SelectorFooterControls = ({ hookName="feature-disabled:project-selector-all-projects" renderDisabled={false} > - {({renderDisabledView}: FeatureRenderProps) => - renderDisabledView ? ( - renderDisabledView({ + {({renderShowAllButton}: FeatureRenderProps) => + renderShowAllButton ? ( + renderShowAllButton({ onButtonClick: onProjectClick, canShowAllProjects, }) From 5a03598451cf49dce8c50beba166118d4a8d86e1 Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Thu, 21 Oct 2021 15:02:22 -0700 Subject: [PATCH 6/9] fix tests --- .../organizations/globalSelectionHeader.spec.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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'); }); }); From cb1eb0d77f650fd149ce4eb5ab4f24aa433f5da5 Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Tue, 26 Oct 2021 09:57:53 -0700 Subject: [PATCH 7/9] fix bug when global views disabled --- .../organizations/multipleProjectSelector.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/static/app/components/organizations/multipleProjectSelector.tsx b/static/app/components/organizations/multipleProjectSelector.tsx index e4e8462ab166d0..4b080fd4027f6e 100644 --- a/static/app/components/organizations/multipleProjectSelector.tsx +++ b/static/app/components/organizations/multipleProjectSelector.tsx @@ -314,6 +314,7 @@ class MultipleProjectSelector extends React.PureComponent { } type FeatureRenderProps = { + hasFeature: boolean; renderShowAllButton?: (p: { canShowAllProjects: boolean; onButtonClick: () => void; @@ -369,18 +370,25 @@ const SelectorFooterControls = ({ hookName="feature-disabled:project-selector-all-projects" renderDisabled={false} > - {({renderShowAllButton}: FeatureRenderProps) => - renderShowAllButton ? ( - renderShowAllButton({ + {({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 ( - ) - } + ); + }} )} From 86e3eb21a94cebf2603cef4abf586856124ecdbf Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Tue, 26 Oct 2021 10:49:22 -0700 Subject: [PATCH 8/9] fix padding --- .../organizations/multipleProjectSelector.tsx | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/static/app/components/organizations/multipleProjectSelector.tsx b/static/app/components/organizations/multipleProjectSelector.tsx index 4b080fd4027f6e..5d09971bf695dc 100644 --- a/static/app/components/organizations/multipleProjectSelector.tsx +++ b/static/app/components/organizations/multipleProjectSelector.tsx @@ -359,60 +359,58 @@ const SelectorFooterControls = ({ : t('Select My Projects'); return ( - + {message && {message}} - - - {!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')} - - )} - - + {!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')} + + )} + ); }; export default withRouter(MultipleProjectSelector); -const FooterContainer = styled('div')` - padding: ${space(1)} 0; -`; 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; From 2c077ab2f96b579738ed3f9c8d30b892f1e664c8 Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Tue, 26 Oct 2021 12:48:56 -0700 Subject: [PATCH 9/9] fix css --- .../organizations/multipleProjectSelector.tsx | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/static/app/components/organizations/multipleProjectSelector.tsx b/static/app/components/organizations/multipleProjectSelector.tsx index 5d09971bf695dc..b3a84c46eb2e42 100644 --- a/static/app/components/organizations/multipleProjectSelector.tsx +++ b/static/app/components/organizations/multipleProjectSelector.tsx @@ -359,48 +359,55 @@ const SelectorFooterControls = ({ : t('Select My Projects'); return ( - + {message && {message}} - {!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')} - - )} - + + {!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')} + + )} + + ); }; export default withRouter(MultipleProjectSelector); +const FooterContainer = styled('div')` + display: flex; + justify-content: space-between; +`; + const FooterActions = styled('div')` padding: ${space(1)} 0; display: flex; @@ -418,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)`