diff options
12 files changed, 335 insertions, 150 deletions
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx index c62d51dfd5e..5cc79d2d179 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx @@ -18,33 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import { Link } from 'react-router'; import ProductNewsMenuItem from './ProductNewsMenuItem'; import { SuggestionLink } from './SuggestionsProvider'; -import { CurrentUser, isLoggedIn, hasGlobalPermission } from '../../types'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/urls'; import { isSonarCloud } from '../../../helpers/system'; import { DropdownOverlay } from '../../../components/controls/Dropdown'; interface Props { - currentUser: CurrentUser; onClose: () => void; suggestions: Array<SuggestionLink>; } export default class EmbedDocsPopup extends React.PureComponent<Props> { - static contextTypes = { - openProjectOnboarding: PropTypes.func - }; - - onAnalyzeProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); - event.currentTarget.blur(); - this.context.openProjectOnboarding(); - }; - renderTitle(text: string) { return <li className="menu-header">{text}</li>; } @@ -119,17 +106,8 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { } renderSonarQubeLinks() { - const { currentUser } = this.props; return ( <React.Fragment> - {isLoggedIn(currentUser) && - hasGlobalPermission(currentUser, 'provisioning') && ( - <li> - <a data-test="analyze-new-project" href="#" onClick={this.onAnalyzeProjectClick}> - {translate('embed_docs.analyze_new_project')} - </a> - </li> - )} <li className="divider" /> <li> <a href="https://community.sonarsource.com/" rel="noopener noreferrer" target="_blank"> diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx index ff39f16ad71..8e2486b5025 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx @@ -19,7 +19,6 @@ */ import * as React from 'react'; import { SuggestionLink } from './SuggestionsProvider'; -import { CurrentUser } from '../../types'; import Toggler from '../../../components/controls/Toggler'; import HelpIcon from '../../../components/icons-components/HelpIcon'; import { lazyLoad } from '../../../components/lazyLoad'; @@ -28,9 +27,7 @@ import { translate } from '../../../helpers/l10n'; const EmbedDocsPopup = lazyLoad(() => import('./EmbedDocsPopup')); interface Props { - currentUser: CurrentUser; suggestions: Array<SuggestionLink>; - tooltip: boolean; } interface State { helpOpen: boolean; @@ -85,11 +82,7 @@ export default class EmbedDocsPopupHelper extends React.PureComponent<Props, Sta onRequestClose={this.closeHelp} open={this.state.helpOpen} overlay={ - <EmbedDocsPopup - currentUser={this.props.currentUser} - onClose={this.closeHelp} - suggestions={this.props.suggestions} - /> + <EmbedDocsPopup onClose={this.closeHelp} suggestions={this.props.suggestions} /> }> <a className="navbar-help" href="#" onClick={this.handleClick} title={translate('help')}> <HelpIcon /> diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx index 24f5fbec4d6..e21c57fb020 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx @@ -28,55 +28,19 @@ const suggestions = [{ link: '#', text: 'foo' }, { link: '#', text: 'bar' }]; it('should display suggestion links', () => { const context = {}; - const wrapper = shallow( - <EmbedDocsPopup - currentUser={{ isLoggedIn: true }} - onClose={jest.fn()} - suggestions={suggestions} - />, - { - context - } - ); + const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} suggestions={suggestions} />, { + context + }); wrapper.update(); expect(wrapper).toMatchSnapshot(); }); -it('should display analyze new project link when user has permission', () => { - const wrapper = shallow( - <EmbedDocsPopup - currentUser={{ isLoggedIn: true, permissions: { global: ['provisioning'] } }} - onClose={jest.fn()} - suggestions={suggestions} - /> - ); - expect(wrapper.find('[data-test="analyze-new-project"]').exists()).toBe(true); -}); - -it('should not display analyze new project link when user does not have permission', () => { - const wrapper = shallow( - <EmbedDocsPopup - currentUser={{ isLoggedIn: true }} - onClose={jest.fn()} - suggestions={suggestions} - /> - ); - expect(wrapper.find('[data-test="analyze-new-project"]').exists()).toBe(false); -}); - it('should display correct links for SonarCloud', () => { (isSonarCloud as jest.Mock<any>).mockReturnValueOnce(true); const context = {}; - const wrapper = shallow( - <EmbedDocsPopup - currentUser={{ isLoggedIn: true }} - onClose={jest.fn()} - suggestions={suggestions} - />, - { - context - } - ); + const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} suggestions={suggestions} />, { + context + }); wrapper.update(); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index 069472c96e5..fbdad095b35 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -38,6 +38,7 @@ import HelpTooltip from '../../../../components/controls/HelpTooltip'; import Toggler from '../../../../components/controls/Toggler'; import DropdownIcon from '../../../../components/icons-components/DropdownIcon'; import { isSonarCloud } from '../../../../helpers/system'; +import { getPortfolioAdminUrl } from '../../../../helpers/urls'; interface Props { branchLikes: BranchLike[]; @@ -128,15 +129,13 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State }; renderOverlay = () => { - const adminLink = { - pathname: '/project/admin/extension/governance/console', - query: { id: this.props.component.breadcrumbs[0].key, qualifier: 'APP' } - }; return ( <> <p>{translate('application.branches.help')}</p> <hr className="spacer-top spacer-bottom" /> - <Link className="spacer-left link-no-underline" to={adminLink}> + <Link + className="spacer-left link-no-underline" + to={getPortfolioAdminUrl(this.props.component.breadcrumbs[0].key, 'APP')}> {translate('application.branches.link')} </Link> </> diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index 396050e4731..2c1aa0f6813 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -61,16 +61,15 @@ export class GlobalNav extends React.PureComponent<Props> { <ul className="global-navbar-menu global-navbar-menu-right"> {isSonarCloud() && <GlobalNavExplore location={this.props.location} />} - <EmbedDocsPopupHelper - currentUser={this.props.currentUser} - suggestions={this.props.suggestions} - tooltip={!isSonarCloud()} - /> + <EmbedDocsPopupHelper suggestions={this.props.suggestions} /> <Search appState={this.props.appState} currentUser={this.props.currentUser} /> - {isLoggedIn(this.props.currentUser) && - isSonarCloud() && ( - <GlobalNavPlus openProjectOnboarding={this.context.openProjectOnboarding} /> - )} + {isLoggedIn(this.props.currentUser) && ( + <GlobalNavPlus + appState={this.props.appState} + currentUser={this.props.currentUser} + openProjectOnboarding={this.context.openProjectOnboarding} + /> + )} <GlobalNavUserContainer {...this.props} /> </ul> </NavBar> diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx index 57f21824533..e702ca72b3d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx @@ -18,47 +18,166 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; -import PlusIcon from '../../../../components/icons-components/PlusIcon'; +import { Link, withRouter, WithRouterProps } from 'react-router'; +import CreateFormShim from '../../../../apps/portfolio/components/CreateFormShim'; import Dropdown from '../../../../components/controls/Dropdown'; +import PlusIcon from '../../../../components/icons-components/PlusIcon'; +import { AppState, hasGlobalPermission, CurrentUser } from '../../../types'; +import { getPortfolioAdminUrl } from '../../../../helpers/urls'; +import { getExtensionStart } from '../../extensions/utils'; +import { isSonarCloud } from '../../../../helpers/system'; import { translate } from '../../../../helpers/l10n'; interface Props { + appState: Pick<AppState, 'qualifiers'>; + currentUser: CurrentUser; openProjectOnboarding: () => void; } -export default class GlobalNavPlus extends React.PureComponent<Props> { - handleNewProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { +interface State { + createPortfolio: boolean; + governanceReady: boolean; +} + +export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps, State> { + mounted = false; + state: State = { createPortfolio: false, governanceReady: false }; + + componentDidMount() { + this.mounted = true; + if (this.props.appState.qualifiers.includes('VW')) { + getExtensionStart('governance/console').then( + () => { + if (this.mounted) { + this.setState({ governanceReady: true }); + } + }, + () => {} + ); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + handleNewProjectClick = (event: React.MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); this.props.openProjectOnboarding(); }; - render() { + openCreatePortfolioForm = () => { + this.setState({ createPortfolio: true }); + }; + + closeCreatePortfolioForm = () => { + this.setState({ createPortfolio: false }); + }; + + handleNewPortfolioClick = (event: React.MouseEvent<HTMLAnchorElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.openCreatePortfolioForm(); + }; + + handleCreatePortfolio = ({ key, qualifier }: { key: string; qualifier: string }) => { + this.closeCreatePortfolioForm(); + this.props.router.push(getPortfolioAdminUrl(key, qualifier)); + }; + + renderCreateProject() { + const { currentUser } = this.props; + if (!hasGlobalPermission(currentUser, 'provisioning')) { + return null; + } return ( - <Dropdown - overlay={ - <ul className="menu"> - <li> - <a className="js-new-project" href="#" onClick={this.handleNewProjectClick}> - {translate('provisioning.create_new_project')} - </a> - </li> - <li className="divider" /> - <li> - <Link className="js-new-organization" to="/create-organization"> - {translate('my_account.create_new_organization')} - </Link> - </li> - </ul> - } - tagName="li"> - <a - className="navbar-plus" - href="#" - title={translate('my_account.create_new_project_or_organization')}> - <PlusIcon /> + <li> + <a className="js-new-project" href="#" onClick={this.handleNewProjectClick}> + {translate('provisioning.create_new_project')} </a> - </Dropdown> + </li> + ); + } + + renderCreateOrganization() { + if (!isSonarCloud()) { + return null; + } + + return ( + <li> + <Link className="js-new-organization" to="/create-organization"> + {translate('my_account.create_new_organization')} + </Link> + </li> + ); + } + + renderCreatePortfolio(showGovernanceEntry: boolean, defaultQualifier?: string) { + const governanceInstalled = this.props.appState.qualifiers.includes('VW'); + if (!governanceInstalled || !showGovernanceEntry) { + return null; + } + + return ( + <li> + <a className="js-new-portfolio" href="#" onClick={this.handleNewPortfolioClick}> + {defaultQualifier + ? translate('my_account.create_new', defaultQualifier) + : translate('my_account.create_new_portfolio_application')} + </a> + </li> + ); + } + + render() { + const { currentUser } = this.props; + const canCreateApplication = hasGlobalPermission(currentUser, 'applicationcreator'); + const canCreatePortfolio = hasGlobalPermission(currentUser, 'portfoliocreator'); + + let defaultQualifier: string | undefined; + if (!canCreateApplication) { + defaultQualifier = 'VW'; + } else if (!canCreatePortfolio) { + defaultQualifier = 'APP'; + } + + return ( + <> + <Dropdown + overlay={ + <ul className="menu"> + {this.renderCreateProject()} + {this.renderCreateOrganization()} + {this.renderCreatePortfolio( + canCreateApplication || canCreatePortfolio, + defaultQualifier + )} + </ul> + } + tagName="li"> + <a + className="navbar-plus" + href="#" + title={ + isSonarCloud() + ? translate('my_account.create_new_project_or_organization') + : translate('my_account.create_new_project_portfolio_or_application') + }> + <PlusIcon /> + </a> + </Dropdown> + {this.state.governanceReady && + this.state.createPortfolio && ( + <CreateFormShim + onClose={this.closeCreatePortfolioForm} + onCreate={this.handleCreatePortfolio} + defaultQualifier={defaultQualifier} + /> + )} + </> ); } } + +export default withRouter(GlobalNavPlus); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx index 88aaf4f172d..9305f1ddcbb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx @@ -18,23 +18,84 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { shallow } from 'enzyme'; -import GlobalNavPlus from '../GlobalNavPlus'; -import { click } from '../../../../../helpers/testUtils'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { GlobalNavPlus } from '../GlobalNavPlus'; +import { isSonarCloud } from '../../../../../helpers/system'; +import { click, mockRouter } from '../../../../../helpers/testUtils'; + +jest.mock('../../../../../helpers/system', () => ({ + isSonarCloud: jest.fn() +})); + +beforeEach(() => { + (isSonarCloud as jest.Mock).mockReturnValue(false); +}); it('render', () => { - const wrapper = shallow(<GlobalNavPlus openProjectOnboarding={jest.fn()} />); - expect(wrapper.is('Dropdown')).toBe(true); + const wrapper = getWrapper(); expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('opens onboarding', () => { const openProjectOnboarding = jest.fn(); - const wrapper = shallow( - shallow(<GlobalNavPlus openProjectOnboarding={openProjectOnboarding} />) - .find('Dropdown') - .prop('overlay') - ); + const wrapper = getOverlayWrapper(getWrapper({ openProjectOnboarding })); click(wrapper.find('.js-new-project')); expect(openProjectOnboarding).toBeCalled(); }); + +it('should display create new project link when user has permission only', () => { + expect( + getOverlayWrapper(getWrapper({}, [])) + .find('.js-new-project') + .exists() + ).toBe(false); +}); + +it('should display create new organization on SonarCloud only', () => { + (isSonarCloud as jest.Mock).mockReturnValue(true); + expect(getOverlayWrapper(getWrapper())).toMatchSnapshot(); +}); + +it('should display create portfolio and application', () => { + checkOpenCreatePortfolio(['applicationcreator', 'portfoliocreator'], undefined); +}); + +it('should display create portfolio', () => { + checkOpenCreatePortfolio(['portfoliocreator'], 'VW'); +}); + +it('should display create application', () => { + checkOpenCreatePortfolio(['applicationcreator'], 'APP'); +}); + +function getWrapper(props = {}, globalPermissions?: string[]) { + return shallow( + <GlobalNavPlus + appState={{ qualifiers: [] }} + currentUser={{ + isLoggedIn: true, + permissions: { global: globalPermissions || ['provisioning'] } + }} + openProjectOnboarding={jest.fn()} + // @ts-ignore avoid passing everything from WithRouterProps + router={mockRouter()} + {...props} + /> + ); +} + +function getOverlayWrapper(wrapper: ShallowWrapper) { + return shallow(wrapper.find('Dropdown').prop('overlay')); +} + +function checkOpenCreatePortfolio(permissions: string[], defaultQualifier?: string) { + const wrapper = getWrapper({ appState: { qualifiers: ['VW'] } }, permissions); + wrapper.setState({ governanceReady: true }); + const overlayWrapper = getOverlayWrapper(wrapper); + expect(overlayWrapper.find('.js-new-portfolio')).toMatchSnapshot(); + + click(overlayWrapper.find('.js-new-portfolio')); + wrapper.update(); + expect(wrapper.find('CreateFormShim').exists()).toBe(true); + expect(wrapper.find('CreateFormShim').prop('defaultQualifier')).toBe(defaultQualifier); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap index 6598367a9cc..62c2757d57b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap @@ -39,13 +39,7 @@ exports[`should render for SonarCloud 1`] = ` } /> <EmbedDocsPopupHelper - currentUser={ - Object { - "isLoggedIn": false, - } - } suggestions={Array []} - tooltip={false} /> <withRouter(Search) appState={ @@ -119,13 +113,7 @@ exports[`should render for SonarQube 1`] = ` className="global-navbar-menu global-navbar-menu-right" > <EmbedDocsPopupHelper - currentUser={ - Object { - "isLoggedIn": false, - } - } suggestions={Array []} - tooltip={true} /> <withRouter(Search) appState={ diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap index a051a721d21..a54b7dfefcb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap @@ -15,19 +15,6 @@ exports[`render 1`] = ` provisioning.create_new_project </a> </li> - <li - className="divider" - /> - <li> - <Link - className="js-new-organization" - onlyActiveOnIndex={false} - style={Object {}} - to="/create-organization" - > - my_account.create_new_organization - </Link> - </li> </ul> } tagName="li" @@ -35,9 +22,65 @@ exports[`render 1`] = ` <a className="navbar-plus" href="#" - title="my_account.create_new_project_or_organization" + title="my_account.create_new_project_portfolio_or_application" > <PlusIcon /> </a> </Dropdown> `; + +exports[`should display create application 1`] = ` +<a + className="js-new-portfolio" + href="#" + onClick={[Function]} +> + my_account.create_new.APP +</a> +`; + +exports[`should display create new organization on SonarCloud only 1`] = ` +<ul + className="menu" +> + <li> + <a + className="js-new-project" + href="#" + onClick={[Function]} + > + provisioning.create_new_project + </a> + </li> + <li> + <Link + className="js-new-organization" + onlyActiveOnIndex={false} + style={Object {}} + to="/create-organization" + > + my_account.create_new_organization + </Link> + </li> +</ul> +`; + +exports[`should display create portfolio 1`] = ` +<a + className="js-new-portfolio" + href="#" + onClick={[Function]} +> + my_account.create_new.VW +</a> +`; + +exports[`should display create portfolio and application 1`] = ` +<a + className="js-new-portfolio" + href="#" + onClick={[Function]} +> + my_account.create_new_portfolio_application +</a> +`; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx new file mode 100644 index 00000000000..0bf661035b6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; + +interface Props { + defaultQualifier?: string; + onClose: () => void; + onCreate: (portfolio: { key: string; qualifier: string }) => void; +} + +export default class CreateFormShim extends React.Component<Props> { + render() { + const { CreateForm } = (window as any).SonarGovernance; + return <CreateForm {...this.props} />; + } +} diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 9d3959260bf..cc207fd5c2b 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -57,6 +57,10 @@ export function getPortfolioUrl(key: string): Location { return { pathname: '/portfolio', query: { id: key } }; } +export function getPortfolioAdminUrl(key: string, qualifier: string) { + return { pathname: '/project/admin/extension/governance/console', query: { id: key, qualifier } }; +} + export function getComponentBackgroundTaskUrl(componentKey: string, status?: string): Location { return { pathname: '/project/background_tasks', query: { id: componentKey, status } }; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index aaac8fa91d7..66508b5d588 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1492,8 +1492,12 @@ my_account.create_organization=Create Organization my_account.search_project=Search Project my_account.set_notifications_for=Set notifications for my_account.analyze_new_project=Analyze new project +my_account.create_new_portfolio_application=Create new portfolio / application +my_account.create_new.VW=Create new portfolio +my_account.create_new.APP=Create new application my_account.create_new_organization=Create new organization my_account.create_new_project_or_organization=Create new project or organization +my_account.create_new_project_portfolio_or_application=Create new project, portfolio or application #------------------------------------------------------------------------------ |