diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-09-05 11:00:00 +0200 |
---|---|---|
committer | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-09-11 11:28:29 +0200 |
commit | 71fec25c4056c1dcfe75769c2041b1d56a89a2e5 (patch) | |
tree | e640a76709b242652d3cc274a9d0a98f720ae768 /server | |
parent | 0926670e79d919e0afa3f0a2e11f656bdcd05916 (diff) | |
download | sonarqube-71fec25c4056c1dcfe75769c2041b1d56a89a2e5.tar.gz sonarqube-71fec25c4056c1dcfe75769c2041b1d56a89a2e5.zip |
SONAR-9784 rewrite projects management page
Diffstat (limited to 'server')
52 files changed, 3863 insertions, 739 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 168f41d4a32..7e9ac772df9 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -32,8 +32,8 @@ export function getGhosts(data: RequestData): Promise<any> { return getJSON('/api/projects/ghosts', data); } -export function deleteComponents(data: { projects: string; organization?: string }): Promise<void> { - return post('/api/projects/bulk_delete', data); +export function deleteComponents(projects: string[], organization: string): Promise<void> { + return post('/api/projects/bulk_delete', { projects: projects.join(), organization }); } export function deleteProject(project: string): Promise<void> { diff --git a/server/sonar-web/src/main/js/api/permissions.ts b/server/sonar-web/src/main/js/api/permissions.ts index de6b169104f..5c927bc3692 100644 --- a/server/sonar-web/src/main/js/api/permissions.ts +++ b/server/sonar-web/src/main/js/api/permissions.ts @@ -85,10 +85,30 @@ export function revokePermissionFromGroup( return post('/api/permissions/remove_group', data); } -/** - * Get list of permission templates - */ -export function getPermissionTemplates(organization?: string) { +export interface PermissionTemplate { + id: string; + name: string; + description?: string; + projectKeyPattern?: string; + createdAt: string; + updatedAt?: string; + permissions: Array<{ + key: string; + usersCount: number; + groupsCount: number; + withProjectCreator?: boolean; + }>; +} + +interface GetPermissionTemplatesResponse { + permissionTemplates: PermissionTemplate[]; + defaultTemplates: Array<{ templateId: string; qualifier: string }>; + permissions: Array<{ key: string; name: string; description: string }>; +} + +export function getPermissionTemplates( + organization?: string +): Promise<GetPermissionTemplatesResponse> { const url = '/api/permissions/search_templates'; return organization ? getJSON(url, { organization }) : getJSON(url); } diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js index 21540b25a99..903de6c74ea 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js @@ -42,7 +42,7 @@ class SettingsNav extends React.PureComponent { } isProjectsActive() { - const urls = ['/projects_admin', '/background_tasks']; + const urls = ['/admin/projects_management', '/background_tasks']; return this.isSomethingActive(urls); } @@ -158,7 +158,7 @@ class SettingsNav extends React.PureComponent { <ul className="dropdown-menu"> {!this.props.customOrganizations && <li> - <IndexLink to="/projects_admin" activeClassName="active"> + <IndexLink to="/admin/projects_management" activeClassName="active"> Management </IndexLink> </li>} diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap index 7f285d4906c..c66d4b28859 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap @@ -154,7 +154,7 @@ exports[`should work with extensions 1`] = ` <li> <IndexLink activeClassName="active" - to="/projects_admin" + to="/admin/projects_management" > Management </IndexLink> diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts new file mode 100644 index 00000000000..d5ff4713845 --- /dev/null +++ b/server/sonar-web/src/main/js/app/types.ts @@ -0,0 +1,120 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ +export enum BranchType { + LONG = 'LONG', + SHORT = 'SHORT' +} + +export interface MainBranch { + isMain: true; + name: string; + status?: { + qualityGateStatus: string; + }; +} + +export interface LongLivingBranch { + isMain: false; + name: string; + status?: { + qualityGateStatus: string; + }; + type: BranchType.LONG; +} + +export interface ShortLivingBranch { + isMain: false; + isOrphan?: true; + mergeBranch: string; + name: string; + status?: { + bugs: number; + codeSmells: number; + vulnerabilities: number; + }; + type: BranchType.SHORT; +} + +export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch; + +export interface ComponentExtension { + key: string; + name: string; +} + +export interface Component { + analysisDate?: string; + breadcrumbs: Array<{ + key: string; + name: string; + qualifier: string; + }>; + configuration?: ComponentConfiguration; + extensions?: ComponentExtension[]; + isFavorite?: boolean; + key: string; + name: string; + organization: string; + path?: string; + qualifier: string; + refKey?: string; + version?: string; +} + +export interface ComponentConfiguration { + extensions?: ComponentExtension[]; + showBackgroundTasks?: boolean; + showLinks?: boolean; + showManualMeasures?: boolean; + showQualityGates?: boolean; + showQualityProfiles?: boolean; + showPermissions?: boolean; + showSettings?: boolean; + showUpdateKey?: boolean; +} + +export interface Metric { + custom?: boolean; + decimalScale?: number; + description?: string; + direction?: number; + domain?: string; + hidden?: boolean; + key: string; + name: string; + qualitative?: boolean; + type: string; +} + +export interface Organization { + adminPages?: Array<{ key: string; name: string }>; + avatar?: string; + canAdmin?: boolean; + canDelete?: boolean; + canProvisionProjects?: boolean; + canUpdateProjectsVisibilityToPrivate?: boolean; + description?: string; + isDefault?: boolean; + key: string; + name: string; + pages?: Array<{ key: string; name: string }>; + projectVisibility: string; + url?: string; +} diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index cda4a172b5e..910761f150c 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -56,7 +56,7 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes'; import projectActivityRoutes from '../../apps/projectActivity/routes'; import projectAdminRoutes from '../../apps/project-admin/routes'; import projectsRoutes from '../../apps/projects/routes'; -import projectsAdminRoutes from '../../apps/projects-admin/routes'; +import projectsManagementRoutes from '../../apps/projectsManagement/routes'; import qualityGatesRoutes from '../../apps/quality-gates/routes'; import qualityProfilesRoutes from '../../apps/quality-profiles/routes'; import sessionsRoutes from '../../apps/sessions/routes'; @@ -115,6 +115,7 @@ const startReactApp = () => { }} /> + <Redirect from="/projects_admin" to="/admin/projects_management" /> <Redirect from="/component/index" to="/component" /> <Redirect from="/component_issues" to="/project/issues" /> <Redirect from="/dashboard/index" to="/dashboard" /> @@ -203,7 +204,10 @@ const startReactApp = () => { <Route path="groups" childRoutes={groupsRoutes} /> <Route path="metrics" childRoutes={metricsRoutes} /> <Route path="permission_templates" childRoutes={permissionTemplatesRoutes} /> - <Route path="projects_admin" childRoutes={projectsAdminRoutes} /> + <Route + path="admin/projects_management" + childRoutes={projectsManagementRoutes} + /> <Route path="roles/global" childRoutes={globalPermissionsRoutes} /> <Route path="settings" childRoutes={settingsRoutes} /> <Route path="system" childRoutes={systemRoutes} /> diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js index 8222ab2df1c..a8ae5dd4b09 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import { connect } from 'react-redux'; -import AppContainer from '../../projects-admin/AppContainer'; +import AppContainer from '../../projectsManagement/AppContainer'; import { getOrganizationByKey } from '../../../store/rootReducer'; /*:: import type { Organization } from '../../../store/organizations/duck'; */ diff --git a/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js b/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js index 3dc4dc144c6..8c8352a27d3 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js +++ b/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js @@ -87,7 +87,7 @@ export default class Form extends React.PureComponent { <form onSubmit={this.handleSubmit}> <div className="modal-head"> <h2> - {translate('qualifiers.delete.TRK')} + {translate('qualifier.delete.TRK')} </h2> </div> <div className="modal-body"> diff --git a/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js b/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js deleted file mode 100644 index e17e8668963..00000000000 --- a/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 React from 'react'; -import { shallow } from 'enzyme'; -import Projects from '../projects'; -import Checkbox from '../../../components/controls/Checkbox'; - -it('should render list of projects with no selection', () => { - const projects = [ - { id: '1', key: 'a', name: 'A', qualifier: 'TRK' }, - { id: '2', key: 'b', name: 'B', qualifier: 'TRK' } - ]; - - const result = shallow( - <Projects - organization={{ key: 'foo' }} - projects={projects} - selection={[]} - refresh={jest.fn()} - /> - ); - expect(result.find('tr').length).toBe(2); - expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(0); -}); - -it('should render list of projects with one selected', () => { - const projects = [ - { id: '1', key: 'a', name: 'A', qualifier: 'TRK' }, - { id: '2', key: 'b', name: 'B', qualifier: 'TRK' } - ]; - const selection = ['a']; - - const result = shallow( - <Projects - organization={{ key: 'foo' }} - projects={projects} - selection={selection} - refresh={jest.fn()} - /> - ); - expect(result.find('tr').length).toBe(2); - expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(1); -}); diff --git a/server/sonar-web/src/main/js/apps/projects-admin/form-view.js b/server/sonar-web/src/main/js/apps/projects-admin/form-view.js deleted file mode 100644 index cb2ed3ec2da..00000000000 --- a/server/sonar-web/src/main/js/apps/projects-admin/form-view.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 ModalForm from '../../components/common/modal-form'; - -export default ModalForm.extend({ - onRender() { - ModalForm.prototype.onRender.apply(this, arguments); - this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); - }, - - onDestroy() { - ModalForm.prototype.onDestroy.apply(this, arguments); - this.$('[data-toggle="tooltip"]').tooltip('destroy'); - }, - - onFormSubmit() { - ModalForm.prototype.onFormSubmit.apply(this, arguments); - this.sendRequest(); - } -}); diff --git a/server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs b/server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs deleted file mode 100644 index b4933eb193c..00000000000 --- a/server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs +++ /dev/null @@ -1,68 +0,0 @@ -<form class="js-form" autocomplete="off"> - <div class="modal-head"> - <h2>Bulk Apply Permission Template</h2> - </div> - - <div class="modal-body"> - <div class="js-modal-messages"></div> - - {{#if done}} - <div class="alert alert-success"> - {{t 'projects_role.apply_template.success'}} - </div> - {{/if}} - - {{#unless done}} - {{#notNull permissionTemplates}} - <div class="modal-field"> - <label for="project-permissions-template"> - Template<em class="mandatory">*</em> - </label> - <select id="project-permissions-template"> - {{#each permissionTemplates}} - <option value="{{id}}">{{name}}</option> - {{/each}} - </select> - </div> - {{else}} - <i class="spinner"></i> - {{/notNull}} - - - <div class="modal-field"> - <label>Apply To</label> - <ul style="padding-top: 4px;"> - {{#if selectionTotal}} - <li> - <input value="selected" id="apply-to-selected" name="apply-to" - type="radio" checked> - <label - for="apply-to-selected" - style="float: none; left: 0; display: inline; padding: 0;"> - Only Selected ({{selectionTotal}}) - </label> - </li> - {{/if}} - <li> - <input value="all" id="apply-to-all" name="apply-to" type="radio" - {{#unless selectionTotal}}checked{{/unless}}> - <label - for="apply-to-all" - style="float: none; left: 0; display: inline; padding: 0;"> - All ({{total}}) - </label> - </li> - </ul> - </div> - {{/unless}} - </div> - - <div class="modal-foot"> - {{#unless done}} - {{#notNull permissionTemplates}} - <button class="js-apply">Apply</button> - {{/notNull}} - {{/unless}} - <a href="#" class="js-modal-close">Close</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs b/server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs deleted file mode 100644 index 2ab69b28f72..00000000000 --- a/server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<form id="delete-project-form"> - <div class="modal-head"> - <h2>Delete Projects</h2> - </div> - <div class="modal-body"> - <div class="js-modal-messages"></div> - Are you sure you want to delete selected projects? - </div> - <div class="modal-foot"> - <button id="delete-project-submit" class="button-red">Delete</button> - <a href="#" class="js-modal-close" id="delete-project-cancel">Cancel</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js b/server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js deleted file mode 100644 index 5dd9e8ab54c..00000000000 --- a/server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 ModalForm from '../../../components/common/modal-form'; -import { - applyTemplateToProject, - bulkApplyTemplate, - getPermissionTemplates -} from '../../../api/permissions'; -import Template from '../templates/BulkApplyTemplateTemplate.hbs'; - -export default ModalForm.extend({ - template: Template, - - initialize() { - this.loadPermissionTemplates(); - this.done = false; - }, - - loadPermissionTemplates() { - const request = this.options.organization - ? getPermissionTemplates(this.options.organization.key) - : getPermissionTemplates(); - return request.then(r => { - this.permissionTemplates = r.permissionTemplates; - this.render(); - }); - }, - - onRender() { - ModalForm.prototype.onRender.apply(this, arguments); - this.$('#project-permissions-template').select2({ - width: '250px', - minimumResultsForSearch: 20 - }); - }, - - bulkApplyToAll(permissionTemplate) { - const data = { templateId: permissionTemplate }; - - if (this.options.query) { - data.q = this.options.query; - } - - if (this.options.qualifier) { - data.qualifier = this.options.qualifier; - } - - if (this.options.organization) { - data.organization = this.options.organization.key; - } - - return bulkApplyTemplate(data); - }, - - bulkApplyToSelected(permissionTemplate) { - const { selection } = this.options; - let lastRequest = Promise.resolve(); - - selection.forEach(projectKey => { - const data = { templateId: permissionTemplate, projectKey }; - if (this.options.organization) { - data.organization = this.options.organization.key; - } - lastRequest = lastRequest.then(() => applyTemplateToProject(data)); - }); - - return lastRequest; - }, - - onFormSubmit() { - ModalForm.prototype.onFormSubmit.apply(this, arguments); - const permissionTemplate = this.$('#project-permissions-template').val(); - const applyTo = this.$('[name="apply-to"]:checked').val(); - this.disableForm(); - - const request = - applyTo === 'all' - ? this.bulkApplyToAll(permissionTemplate) - : this.bulkApplyToSelected(permissionTemplate); - - request - .then(() => { - this.trigger('done'); - this.done = true; - this.render(); - }) - .catch(e => { - e.response.json().then(r => { - this.showErrors(r.errors, r.warnings); - this.enableForm(); - }); - }); - }, - - serializeData() { - return { - permissionTemplates: this.permissionTemplates, - selection: this.options.selection, - selectionTotal: this.options.selection.length, - total: this.options.total, - done: this.done - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/projects-admin/main.js b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx index eea76fcf10d..5a860abb5f7 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/main.js +++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx @@ -17,48 +17,42 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import Helmet from 'react-helmet'; import { debounce, uniq, without } from 'lodash'; -import Header from './header'; -import Search from './search'; -import Projects from './projects'; +import Header from './Header'; +import Search from './Search'; +import Projects from './Projects'; import CreateProjectForm from './CreateProjectForm'; import ListFooter from '../../components/controls/ListFooter'; -import { PAGE_SIZE, TYPE } from './constants'; -import { getComponents, getProvisioned, getGhosts, deleteComponents } from '../../api/components'; +import { PAGE_SIZE, Type, Project } from './utils'; +import { getComponents, getProvisioned, getGhosts } from '../../api/components'; +import { Organization } from '../../app/types'; import { translate } from '../../helpers/l10n'; -/*:: import type { Organization } from '../../store/organizations/duck'; */ -/*:: -type Props = {| - hasProvisionPermission: boolean, - onVisibilityChange: string => void, - onRequestFail: Object => void, - organization: Organization -|}; -*/ +export interface Props { + hasProvisionPermission?: boolean; + onVisibilityChange: (visibility: string) => void; + organization: Organization; + topLevelQualifiers: string[]; +} -/*:: -type State = { - createProjectForm: boolean, - ready: boolean, - projects: Array<{ key: string }>, - total: number, - page: number, - query: string, - qualifiers: string, - type: string, - selection: Array<string> -}; -*/ +interface State { + createProjectForm: boolean; + page: number; + projects: Project[]; + qualifiers: string; + query: string; + ready: boolean; + selection: string[]; + total: number; + type: Type; +} -export default class Main extends React.PureComponent { - /*:: props: Props; */ - /*:: state: State; */ +export default class App extends React.PureComponent<Props, State> { + mounted: boolean; - constructor(props /*: Props */) { + constructor(props: Props) { super(props); this.state = { createProjectForm: false, @@ -68,86 +62,87 @@ export default class Main extends React.PureComponent { page: 1, query: '', qualifiers: 'TRK', - type: TYPE.ALL, + type: Type.All, selection: [] }; this.requestProjects = debounce(this.requestProjects, 250); } componentDidMount() { + this.mounted = true; this.requestProjects(); } - getFilters = () => { - const filters /*: { [string]: string | number } */ = { - organization: this.props.organization.key, - ps: PAGE_SIZE - }; - if (this.state.page !== 1) { - filters.p = this.state.page; - } - if (this.state.query) { - filters.q = this.state.query; - } - return filters; - }; + componentWillUnmount() { + this.mounted = false; + } + + getFilters = () => ({ + organization: this.props.organization.key, + p: this.state.page !== 1 ? this.state.page : undefined, + ps: PAGE_SIZE, + q: this.state.query ? this.state.query : undefined + }); requestProjects = () => { switch (this.state.type) { - case TYPE.ALL: + case Type.All: this.requestAllProjects(); break; - case TYPE.PROVISIONED: + case Type.Provisioned: this.requestProvisioned(); break; - case TYPE.GHOSTS: + case Type.Ghosts: this.requestGhosts(); break; - default: - - // should never happen } }; requestGhosts = () => { const data = this.getFilters(); getGhosts(data).then(r => { - let projects = r.projects.map(project => ({ - ...project, - id: project.uuid, - qualifier: 'TRK' - })); - if (this.state.page > 1) { - projects = [].concat(this.state.projects, projects); + if (this.mounted) { + let projects: Project[] = r.projects.map((project: any) => ({ + ...project, + id: project.uuid, + qualifier: 'TRK' + })); + if (this.state.page > 1) { + projects = [...this.state.projects, ...projects]; + } + this.setState({ ready: true, projects, selection: [], total: r.total }); } - this.setState({ ready: true, projects, total: r.total }); }); }; requestProvisioned = () => { const data = this.getFilters(); getProvisioned(data).then(r => { - let projects = r.projects.map(project => ({ - ...project, - id: project.uuid, - qualifier: 'TRK' - })); - if (this.state.page > 1) { - projects = [].concat(this.state.projects, projects); + if (this.mounted) { + let projects: Project[] = r.projects.map((project: any) => ({ + ...project, + id: project.uuid, + qualifier: 'TRK' + })); + if (this.state.page > 1) { + projects = [...this.state.projects, ...projects]; + } + this.setState({ ready: true, projects, selection: [], total: r.paging.total }); } - this.setState({ ready: true, projects, total: r.paging.total }); }); }; requestAllProjects = () => { const data = this.getFilters(); - data.qualifiers = this.state.qualifiers; + Object.assign(data, { qualifiers: this.state.qualifiers }); getComponents(data).then(r => { - let projects = r.components; - if (this.state.page > 1) { - projects = [].concat(this.state.projects, projects); + if (this.mounted) { + let projects: Project[] = r.components; + if (this.state.page > 1) { + projects = [...this.state.projects, ...projects]; + } + this.setState({ ready: true, projects, selection: [], total: r.paging.total }); } - this.setState({ ready: true, projects, total: r.paging.total }); }); }; @@ -155,53 +150,31 @@ export default class Main extends React.PureComponent { this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects); }; - onSearch = (query /*: string */) => { - this.setState( - { - ready: false, - page: 1, - query, - selection: [] - }, - this.requestProjects - ); + onSearch = (query: string) => { + this.setState({ ready: false, page: 1, query, selection: [] }, this.requestProjects); }; - onTypeChanged = (newType /*: string */) => { + onTypeChanged = (newType: Type) => { this.setState( - { - ready: false, - page: 1, - query: '', - type: newType, - qualifiers: 'TRK', - selection: [] - }, + { ready: false, page: 1, query: '', type: newType, qualifiers: 'TRK', selection: [] }, this.requestProjects ); }; - onQualifierChanged = (newQualifier /*: string */) => { + onQualifierChanged = (newQualifier: string) => { this.setState( - { - ready: false, - page: 1, - query: '', - type: TYPE.ALL, - qualifiers: newQualifier, - selection: [] - }, + { ready: false, page: 1, query: '', type: Type.All, qualifiers: newQualifier, selection: [] }, this.requestProjects ); }; - onProjectSelected = (project /*: { key: string } */) => { - const newSelection = uniq([].concat(this.state.selection, project.key)); + onProjectSelected = (project: string) => { + const newSelection = uniq([...this.state.selection, project]); this.setState({ selection: newSelection }); }; - onProjectDeselected = (project /*: { key: string } */) => { - const newSelection = without(this.state.selection, project.key); + onProjectDeselected = (project: string) => { + const newSelection = without(this.state.selection, project); this.setState({ selection: newSelection }); }; @@ -214,18 +187,6 @@ export default class Main extends React.PureComponent { this.setState({ selection: [] }); }; - deleteProjects = () => { - this.setState({ ready: false }); - const projects = this.state.selection.join(','); - const data = { - organization: this.props.organization.key, - projects - }; - deleteComponents(data).then(() => { - this.setState({ page: 1, selection: [] }, this.requestProjects); - }); - }; - openCreateProjectForm = () => { this.setState({ createProjectForm: true }); }; @@ -249,18 +210,17 @@ export default class Main extends React.PureComponent { <Search {...this.props} {...this.state} - onSearch={this.onSearch} - onTypeChanged={this.onTypeChanged} - onQualifierChanged={this.onQualifierChanged} onAllSelected={this.onAllSelected} onAllDeselected={this.onAllDeselected} - deleteProjects={this.deleteProjects} + onDeleteProjects={this.requestProjects} + onQualifierChanged={this.onQualifierChanged} + onSearch={this.onSearch} + onTypeChanged={this.onTypeChanged} /> <Projects ready={this.state.ready} projects={this.state.projects} - refresh={this.requestProjects} selection={this.state.selection} onProjectSelected={this.onProjectSelected} onProjectDeselected={this.onProjectDeselected} @@ -278,7 +238,6 @@ export default class Main extends React.PureComponent { <CreateProjectForm onClose={this.closeCreateProjectForm} onProjectCreated={this.requestProjects} - onRequestFail={this.props.onRequestFail} organization={this.props.organization} />} </div> diff --git a/server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx index 1531bb344dc..0b00f3eba76 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx @@ -17,16 +17,28 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +import * as React from 'react'; import { connect } from 'react-redux'; -import Main from './main'; +import App from './App'; +import { Organization } from '../../app/types'; import { onFail } from '../../store/rootActions'; import { getAppState, getOrganizationByKey } from '../../store/rootReducer'; import { receiveOrganizations } from '../../store/organizations/duck'; import { changeProjectVisibility } from '../../api/organizations'; import { fetchOrganization } from '../../apps/organizations/actions'; -class AppContainer extends React.PureComponent { +interface Props { + appState: { + defaultOrganization: string; + qualifiers: string[]; + }; + fetchOrganization: (organization: string) => void; + onVisibilityChange: (organization: Organization, visibility: string) => void; + onRequestFail: (error: any) => void; + organization?: Organization; +} + +class AppContainer extends React.PureComponent<Props> { componentDidMount() { // if there is no organization, that means we are in the global scope // let's fetch defails for the default organization in this case @@ -35,12 +47,10 @@ class AppContainer extends React.PureComponent { } } - componentWillUnmount() { - this.mounted = false; - } - - handleVisibilityChange = visibility => { - this.props.onVisibilityChange(this.props.organization, visibility); + handleVisibilityChange = (visibility: string) => { + if (this.props.organization) { + this.props.onVisibilityChange(this.props.organization, visibility); + } }; render() { @@ -53,24 +63,25 @@ class AppContainer extends React.PureComponent { const topLevelQualifiers = organization.isDefault ? this.props.appState.qualifiers : ['TRK']; return ( - <Main + <App hasProvisionPermission={organization.canProvisionProjects} - topLevelQualifiers={topLevelQualifiers} onVisibilityChange={this.handleVisibilityChange} - onRequestFail={this.props.onRequestFail} organization={organization} + topLevelQualifiers={topLevelQualifiers} /> ); } } -const mapStateToProps = (state, ownProps) => ({ +const mapStateToProps = (state: any, ownProps: Props) => ({ appState: getAppState(state), organization: ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization) }); -const onVisibilityChange = (organization, visibility) => dispatch => { +const onVisibilityChange = (organization: Organization, visibility: string) => ( + dispatch: Function +) => { const currentVisibility = organization.projectVisibility; dispatch(receiveOrganizations([{ ...organization, projectVisibility: visibility }])); changeProjectVisibility(organization.key, visibility).catch(error => { @@ -79,11 +90,10 @@ const onVisibilityChange = (organization, visibility) => dispatch => { }); }; -const mapDispatchToProps = dispatch => ({ - fetchOrganization: key => dispatch(fetchOrganization(key)), - onVisibilityChange: (organization, visibility) => - dispatch(onVisibilityChange(organization, visibility)), - onRequestFail: error => onFail(dispatch)(error) +const mapDispatchToProps = (dispatch: Function) => ({ + fetchOrganization: (key: string) => dispatch(fetchOrganization(key)), + onVisibilityChange: (organization: Organization, visibility: string) => + dispatch(onVisibilityChange(organization, visibility)) }); -export default connect(mapStateToProps, mapDispatchToProps)(AppContainer); +export default connect<any, any, any>(mapStateToProps, mapDispatchToProps)(AppContainer); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx new file mode 100644 index 00000000000..a5f967fd04a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx @@ -0,0 +1,217 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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'; +import Modal from 'react-modal'; +import * as Select from 'react-select'; +import { Type } from './utils'; +import { + getPermissionTemplates, + PermissionTemplate, + bulkApplyTemplate, + applyTemplateToProject +} from '../../api/permissions'; +import { translate, translateWithParameters } from '../../helpers/l10n'; + +export interface Props { + onClose: () => void; + organization: string; + qualifier: string; + query: string; + selection: string[]; + total: number; + type: Type; +} + +interface State { + done: boolean; + loading: boolean; + permissionTemplate?: string; + permissionTemplates?: PermissionTemplate[]; + submitting: boolean; +} + +export default class BulkApplyTemplateModal extends React.PureComponent<Props, State> { + mounted: boolean; + state: State = { done: false, loading: true, submitting: false }; + + componentDidMount() { + this.mounted = true; + this.loadPermissionTemplates(); + } + + componentWillUnmount() { + this.mounted = false; + } + + loadPermissionTemplates() { + this.setState({ loading: true }); + getPermissionTemplates(this.props.organization).then( + ({ permissionTemplates }) => { + if (this.mounted) { + this.setState({ + loading: false, + permissionTemplate: + permissionTemplates.length > 0 ? permissionTemplates[0].id : undefined, + permissionTemplates: permissionTemplates + }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + } + + bulkApplyToAll = (permissionTemplate: string) => { + const data = { + organization: this.props.organization, + q: this.props.query ? this.props.query : undefined, + qualifier: this.props.qualifier, + templateId: permissionTemplate + }; + return bulkApplyTemplate(data); + }; + + bulkApplyToSelected = (permissionTemplate: string) => { + const { selection } = this.props; + let lastRequest = Promise.resolve(); + + selection.forEach(projectKey => { + const data = { + organization: this.props.organization, + projectKey, + templateId: permissionTemplate + }; + lastRequest = lastRequest.then(() => applyTemplateToProject(data)); + }); + + return lastRequest; + }; + + handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleConfirmClick = () => { + const { permissionTemplate } = this.state; + if (permissionTemplate) { + this.setState({ submitting: true }); + const request = this.props.selection.length + ? this.bulkApplyToSelected(permissionTemplate) + : this.bulkApplyToAll(permissionTemplate); + request.then( + () => { + if (this.mounted) { + this.setState({ done: true, submitting: false }); + } + }, + () => { + if (this.mounted) { + this.setState({ submitting: false }); + } + } + ); + } + }; + + handlePermissionTemplateChange = ({ value }: { value: string }) => { + this.setState({ permissionTemplate: value }); + }; + + renderWarning = () => { + return this.props.selection.length + ? <div className="alert alert-info"> + {translateWithParameters( + 'permission_templates.bulk_apply_permission_template.apply_to_selected', + this.props.selection.length + )} + </div> + : <div className="alert alert-warning"> + {translateWithParameters( + 'permission_templates.bulk_apply_permission_template.apply_to_all', + this.props.total + )} + </div>; + }; + + renderSelect = () => + <div className="modal-field"> + <label> + {translate('template')} + <em className="mandatory">*</em> + </label> + <Select + clearable={false} + disabled={this.state.submitting} + onChange={this.handlePermissionTemplateChange} + options={this.state.permissionTemplates!.map(t => ({ label: t.name, value: t.id }))} + searchable={false} + value={this.state.permissionTemplate} + /> + </div>; + + render() { + const { done, loading, permissionTemplates, submitting } = this.state; + const header = translate('permission_templates.bulk_apply_permission_template'); + + return ( + <Modal + isOpen={true} + contentLabel={header} + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + <header className="modal-head"> + <h2> + {header} + </h2> + </header> + + <div className="modal-body"> + {done && + <div className="alert alert-success"> + {translate('projects_role.apply_template.success')} + </div>} + + {loading && <i className="spinner" />} + + {!loading && !done && permissionTemplates && this.renderWarning()} + {!loading && !done && permissionTemplates && this.renderSelect()} + </div> + + <footer className="modal-foot"> + {submitting && <i className="spinner spacer-right" />} + {!loading && + !done && + permissionTemplates && + <button disabled={submitting} onClick={this.handleConfirmClick}> + {translate('apply')} + </button>} + <a className="js-modal-close" href="#" onClick={this.handleCancelClick}> + {done ? translate('close') : translate('cancel')} + </a> + </footer> + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx index b04c4c3e8ae..9c04349c688 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx @@ -17,53 +17,45 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import Modal from 'react-modal'; -import classNames from 'classnames'; +import * as classNames from 'classnames'; +import { Organization } from '../../app/types'; import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox'; import { translate } from '../../helpers/l10n'; -/*:: import type { Organization } from '../../store/organizations/duck'; */ +import { Visibility } from './utils'; -/*:: -type Props = { - onClose: () => void, - onConfirm: string => void, - organization: Organization -}; -*/ - -/*:: -type State = { - visibility: string -}; -*/ +export interface Props { + onClose: () => void; + onConfirm: (visiblity: Visibility) => void; + organization: Organization; +} -export default class ChangeVisibilityForm extends React.PureComponent { - /*:: props: Props; */ - /*:: state: State; */ +interface State { + visibility: Visibility; +} - constructor(props /*: Props */) { +export default class ChangeVisibilityForm extends React.PureComponent<Props, State> { + constructor(props: Props) { super(props); - this.state = { visibility: props.organization.projectVisibility }; + this.state = { visibility: props.organization.projectVisibility as Visibility }; } - handleCancelClick = (event /*: Event */) => { + handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); this.props.onClose(); }; - handleConfirmClick = (event /*: Event */) => { + handleConfirmClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { event.preventDefault(); this.props.onConfirm(this.state.visibility); this.props.onClose(); }; - handleVisibilityClick = (visibility /*: string */) => ( - event /*: Event & { currentTarget: HTMLElement } */ - ) => { + handleVisibilityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); + const visibility = event.currentTarget.dataset.visibility as Visibility; this.setState({ visibility }); }; @@ -84,10 +76,10 @@ export default class ChangeVisibilityForm extends React.PureComponent { </header> <div className="modal-body"> - {['public', 'private'].map(visibility => + {[Visibility.Public, Visibility.Private].map(visibility => <div className="big-spacer-bottom" key={visibility}> <p> - {visibility === 'private' && !canUpdateProjectsVisibilityToPrivate + {visibility === Visibility.Private && !canUpdateProjectsVisibilityToPrivate ? <span className="text-muted cursor-not-allowed"> <i className={classNames('icon-radio', 'spacer-right', { @@ -98,8 +90,9 @@ export default class ChangeVisibilityForm extends React.PureComponent { </span> : <a className="link-base-color link-no-underline" + data-visibility={visibility} href="#" - onClick={this.handleVisibilityClick(visibility)}> + onClick={this.handleVisibilityClick}> <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': this.state.visibility === visibility @@ -122,10 +115,10 @@ export default class ChangeVisibilityForm extends React.PureComponent { </div> <footer className="modal-foot"> - <button onClick={this.handleConfirmClick}> + <button className="js-confirm" onClick={this.handleConfirmClick}> {translate('organization.change_visibility_form.submit')} </button> - <a href="#" onClick={this.handleCancelClick}> + <a className="js-modal-close" href="#" onClick={this.handleCancelClick}> {translate('cancel')} </a> </footer> diff --git a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx index fa8e2ea0f6d..22a81acf160 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js +++ b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx @@ -17,50 +17,44 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import Modal from 'react-modal'; import { Link } from 'react-router'; +import { Organization } from '../../app/types'; import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox'; import VisibilitySelector from '../../components/common/VisibilitySelector'; import { createProject } from '../../api/components'; import { translate } from '../../helpers/l10n'; import { getProjectUrl } from '../../helpers/urls'; -/*:: import type { Organization } from '../../store/organizations/duck'; */ - -/*:: -type Props = {| - onClose: () => void, - onProjectCreated: () => void, - onRequestFail: Object => void, - organization?: Organization -|}; -*/ - -/*:: -type State = { - branch: string, - createdProject?: Object, - key: string, - loading: boolean, - name: string, - visibility: string -}; -*/ - -export default class CreateProjectForm extends React.PureComponent { - /*:: mounted: boolean; */ - /*:: props: Props; */ - /*:: state: State; */ - - constructor(props /*: Props */) { + +interface Props { + onClose: () => void; + onProjectCreated: () => void; + organization: Organization; +} + +interface State { + branch: string; + createdProject?: { key: string; name: string }; + key: string; + loading: boolean; + name: string; + visibility: string; + // add index declaration to be able to do `this.setState({ [name]: value });` + [x: string]: any; +} + +export default class CreateProjectForm extends React.PureComponent<Props, State> { + mounted: boolean; + + constructor(props: Props) { super(props); this.state = { branch: '', key: '', loading: false, name: '', - visibility: props.organization ? props.organization.projectVisibility : 'public' + visibility: props.organization.projectVisibility }; } @@ -72,32 +66,30 @@ export default class CreateProjectForm extends React.PureComponent { this.mounted = false; } - handleCancelClick = (event /*: Event */) => { + handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); this.props.onClose(); }; - handleInputChange = (event /*: { currentTarget: HTMLInputElement } */) => { + handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => { const { name, value } = event.currentTarget; this.setState({ [name]: value }); }; - handleVisibilityChange = (visibility /*: string */) => { + handleVisibilityChange = (visibility: string) => { this.setState({ visibility }); }; - handleFormSubmit = (event /*: Event */) => { + handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { event.preventDefault(); - const data /*: { [string]: string } */ = { + const data = { name: this.state.name, branch: this.state.branch, + organization: this.props.organization && this.props.organization.key, project: this.state.key, visibility: this.state.visibility }; - if (this.props.organization) { - data.organization = this.props.organization.key; - } this.setState({ loading: true }); createProject(data).then( @@ -165,7 +157,7 @@ export default class CreateProjectForm extends React.PureComponent { <input autoFocus={true} id="create-project-name" - maxLength="2000" + maxLength={2000} name="name" onChange={this.handleInputChange} required={true} @@ -179,7 +171,7 @@ export default class CreateProjectForm extends React.PureComponent { </label> <input id="create-project-branch" - maxLength="200" + maxLength={200} name="branch" onChange={this.handleInputChange} type="text" @@ -193,7 +185,7 @@ export default class CreateProjectForm extends React.PureComponent { </label> <input id="create-project-key" - maxLength="400" + maxLength={400} name="key" onChange={this.handleInputChange} required={true} @@ -203,18 +195,15 @@ export default class CreateProjectForm extends React.PureComponent { </div> <div className="modal-field"> <label> - {' '}{translate('visibility')}{' '} + {translate('visibility')} </label> <VisibilitySelector - canTurnToPrivate={ - organization == null || organization.canUpdateProjectsVisibilityToPrivate - } + canTurnToPrivate={organization.canUpdateProjectsVisibilityToPrivate} className="little-spacer-top" onChange={this.handleVisibilityChange} visibility={this.state.visibility} /> - {organization != null && - !organization.canUpdateProjectsVisibilityToPrivate && + {!organization.canUpdateProjectsVisibilityToPrivate && <div className="spacer-top"> <UpgradeOrganizationBox organization={organization.key} /> </div>} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx new file mode 100644 index 00000000000..7b4a45fc61b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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'; +import Modal from 'react-modal'; +import { deleteComponents } from '../../api/components'; +import { translate } from '../../helpers/l10n'; + +export interface Props { + onClose: () => void; + onConfirm: () => void; + organization: string; + qualifier: string; + selection: string[]; +} + +interface State { + loading: boolean; +} + +export default class DeleteModal extends React.PureComponent<Props, State> { + mounted: boolean; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleConfirmClick = () => { + this.setState({ loading: true }); + deleteComponents(this.props.selection, this.props.organization).then( + () => { + if (this.mounted) { + this.props.onConfirm(); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + render() { + const header = translate('qualifiers.delete', this.props.qualifier); + + return ( + <Modal + isOpen={true} + contentLabel={header} + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + <header className="modal-head"> + <h2> + {header} + </h2> + </header> + + <div className="modal-body"> + {translate('qualifiers.delete_confirm', this.props.qualifier)} + </div> + + <footer className="modal-foot"> + {this.state.loading && <i className="spinner spacer-right" />} + <button + className="button-red" + disabled={this.state.loading} + onClick={this.handleConfirmClick}> + {translate('delete')} + </button> + <a className="js-modal-close" href="#" onClick={this.handleCancelClick}> + {translate('cancel')} + </a> + </footer> + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects-admin/header.js b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx index 9fdf7d45ba0..ef77181c740 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/header.js +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx @@ -17,37 +17,32 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import ChangeVisibilityForm from './ChangeVisibilityForm'; +import { Visibility } from './utils'; +import { Organization } from '../../app/types'; import { translate } from '../../helpers/l10n'; -/*:: import type { Organization } from '../../store/organizations/duck'; */ -/*:: -type Props = {| - hasProvisionPermission: boolean, - onProjectCreate: () => void, - onVisibilityChange: string => void, - organization: Organization -|}; -*/ +export interface Props { + hasProvisionPermission?: boolean; + onProjectCreate: () => void; + onVisibilityChange: (visibility: Visibility) => void; + organization: Organization; +} -/*:: -type State = { - visibilityForm: boolean -}; -*/ +interface State { + visibilityForm: boolean; +} -export default class Header extends React.PureComponent { - /*:: props: Props; */ - state /*: State */ = { visibilityForm: false }; +export default class Header extends React.PureComponent<Props, State> { + state: State = { visibilityForm: false }; - handleCreateProjectClick = (event /*: Event */) => { + handleCreateProjectClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { event.preventDefault(); this.props.onProjectCreate(); }; - handleChangeVisibilityClick = (event /*: Event */) => { + handleChangeVisibilityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); this.setState({ visibilityForm: true }); }; @@ -64,12 +59,13 @@ export default class Header extends React.PureComponent { <h1 className="page-title"> {translate('projects_management')} </h1> + <div className="page-actions"> <span className="big-spacer-right"> {translate('organization.default_visibility_of_new_projects')}{' '} <strong>{translate('visibility', organization.projectVisibility)}</strong> <a - className="spacer-left icon-edit" + className="js-change-visibility spacer-left icon-edit" href="#" onClick={this.handleChangeVisibilityClick} /> @@ -79,6 +75,7 @@ export default class Header extends React.PureComponent { {translate('qualifiers.create.TRK')} </button>} </div> + <p className="page-description"> {translate('projects_management.page.description')} </p> diff --git a/server/sonar-web/src/main/js/apps/projects-admin/projects.js b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx index 0d0f59342ad..60f951dd32c 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/projects.js +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx @@ -1,7 +1,7 @@ /* * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 @@ -17,60 +17,42 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; import { Link } from 'react-router'; -import { getComponentPermissionsUrl } from '../../helpers/urls'; -import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView'; +import { Project, Visibility } from './utils'; +import PrivateBadge from '../../components/common/PrivateBadge'; import Checkbox from '../../components/controls/Checkbox'; import QualifierIcon from '../../components/shared/QualifierIcon'; -import PrivateBadge from '../../components/common/PrivateBadge'; import { translate } from '../../helpers/l10n'; +import { getComponentPermissionsUrl } from '../../helpers/urls'; -export default class Projects extends React.PureComponent { - static propTypes = { - projects: PropTypes.array.isRequired, - selection: PropTypes.array.isRequired, - organization: PropTypes.object.isRequired - }; - - componentWillMount() { - this.renderProject = this.renderProject.bind(this); - } - - onProjectCheck(project, checked) { - if (checked) { - this.props.onProjectSelected(project); - } else { - this.props.onProjectDeselected(project); - } - } +interface Props { + onApplyTemplateClick: (project: Project) => void; + onProjectCheck: (project: Project, checked: boolean) => void; + project: Project; + selected: boolean; +} - onApplyTemplateClick(project, e) { - e.preventDefault(); - e.target.blur(); - new ApplyTemplateView({ - project, - organization: this.props.organization - }).render(); - } +export default class ProjectRow extends React.PureComponent<Props> { + handleProjectCheck = (checked: boolean) => { + this.props.onProjectCheck(this.props.project, checked); + }; - isProjectSelected(project) { - return this.props.selection.indexOf(project.key) !== -1; - } + handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onApplyTemplateClick(this.props.project); + }; - renderProject(project) { - const permissionsUrl = getComponentPermissionsUrl(project.key); + render() { + const { project, selected } = this.props; return ( - <tr key={project.key}> + <tr> <td className="thin"> - <Checkbox - checked={this.isProjectSelected(project)} - onCheck={this.onProjectCheck.bind(this, project)} - /> + <Checkbox checked={selected} onCheck={this.handleProjectCheck} /> </td> + <td className="nowrap"> <Link to={{ pathname: '/dashboard', query: { id: project.key } }} @@ -78,14 +60,17 @@ export default class Projects extends React.PureComponent { <QualifierIcon qualifier={project.qualifier} /> <span>{project.name}</span> </Link> </td> + <td className="nowrap"> <span className="note"> {project.key} </span> </td> + <td className="width-20"> - {project.visibility === 'private' && <PrivateBadge />} + {project.visibility === Visibility.Private && <PrivateBadge />} </td> + <td className="thin nowrap"> <div className="dropdown"> <button className="dropdown-toggle" data-toggle="dropdown"> @@ -93,12 +78,12 @@ export default class Projects extends React.PureComponent { </button> <ul className="dropdown-menu dropdown-menu-right"> <li> - <Link to={permissionsUrl}> + <Link to={getComponentPermissionsUrl(project.key)}> {translate('edit_permissions')} </Link> </li> <li> - <a href="#" onClick={this.onApplyTemplateClick.bind(this, project)}> + <a className="js-apply-template" href="#" onClick={this.handleApplyTemplateClick}> {translate('projects_role.apply_template')} </a> </li> @@ -108,16 +93,4 @@ export default class Projects extends React.PureComponent { </tr> ); } - - render() { - const className = classNames('data', 'zebra', { 'new-loading': !this.props.ready }); - - return ( - <table className={className} id="projects-management-page-projects"> - <tbody> - {this.props.projects.map(this.renderProject)} - </tbody> - </table> - ); - } } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx new file mode 100644 index 00000000000..ff6264dce90 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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'; +import * as classNames from 'classnames'; +import ProjectRow from './ProjectRow'; +import { Project } from './utils'; +import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView'; +import { Organization } from '../../app/types'; + +interface Props { + onProjectDeselected: (project: string) => void; + onProjectSelected: (project: string) => void; + organization: Organization; + projects: Project[]; + ready?: boolean; + selection: string[]; +} + +export default class Projects extends React.PureComponent<Props> { + onProjectCheck = (project: Project, checked: boolean) => { + if (checked) { + this.props.onProjectSelected(project.key); + } else { + this.props.onProjectDeselected(project.key); + } + }; + + onApplyTemplateClick = (project: Project) => { + new ApplyTemplateView({ project, organization: this.props.organization }).render(); + }; + + render() { + return ( + <table + className={classNames('data', 'zebra', { 'new-loading': !this.props.ready })} + id="projects-management-page-projects"> + <tbody> + {this.props.projects.map(project => + <ProjectRow + key={project.key} + onApplyTemplateClick={this.onApplyTemplateClick} + onProjectCheck={this.onProjectCheck} + project={project} + selected={this.props.selection.includes(project.key)} + /> + )} + </tbody> + </table> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects-admin/search.js b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx index ce42b2050e9..914883f8359 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/search.js +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx @@ -17,38 +17,60 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; import { sortBy } from 'lodash'; -import { TYPE, QUALIFIERS_ORDER } from './constants'; -import DeleteView from './delete-view'; -import BulkApplyTemplateView from './views/BulkApplyTemplateView'; +import BulkApplyTemplateModal from './BulkApplyTemplateModal'; +import DeleteModal from './DeleteModal'; +import { Type, QUALIFIERS_ORDER } from './utils'; +import { Project } from './utils'; +import { Organization } from '../../app/types'; import RadioToggle from '../../components/controls/RadioToggle'; import Checkbox from '../../components/controls/Checkbox'; import { translate } from '../../helpers/l10n'; -export default class Search extends React.PureComponent { - static propTypes = { - onSearch: PropTypes.func.isRequired - }; +export interface Props { + onAllDeselected: () => void; + onAllSelected: () => void; + onDeleteProjects: () => void; + onQualifierChanged: (qualifier: string) => void; + onSearch: (query: string) => void; + onTypeChanged: (type: Type) => void; + organization: Organization; + projects: Project[]; + qualifiers: string; + query: string; + ready: boolean; + selection: any[]; + topLevelQualifiers: string[]; + total: number; + type: Type; +} + +interface State { + bulkApplyTemplateModal: boolean; + deleteModal: boolean; +} - onSubmit = e => { - e.preventDefault(); +export default class Search extends React.PureComponent<Props, State> { + input: HTMLInputElement; + mounted: boolean; + state: State = { bulkApplyTemplateModal: false, deleteModal: false }; + + onSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { + event.preventDefault(); this.search(); }; - search = () => { - const q = this.refs.input.value; + search = (event?: React.SyntheticEvent<HTMLInputElement>) => { + const q = event ? event.currentTarget.value : this.input.value; this.props.onSearch(q); }; - getTypeOptions = () => { - return [ - { value: TYPE.ALL, label: 'All' }, - { value: TYPE.PROVISIONED, label: 'Provisioned' }, - { value: TYPE.GHOSTS, label: 'Ghosts' } - ]; - }; + getTypeOptions = () => [ + { value: Type.All, label: 'All' }, + { value: Type.Provisioned, label: 'Provisioned' }, + { value: Type.Ghosts, label: 'Ghosts' } + ]; getQualifierOptions = () => { const options = this.props.topLevelQualifiers.map(q => { @@ -57,7 +79,7 @@ export default class Search extends React.PureComponent { return sortBy(options, option => QUALIFIERS_ORDER.indexOf(option.value)); }; - onCheck = checked => { + onCheck = (checked: boolean) => { if (checked) { this.props.onAllSelected(); } else { @@ -65,20 +87,29 @@ export default class Search extends React.PureComponent { } }; - deleteProjects = () => { - new DeleteView({ - deleteProjects: this.props.deleteProjects - }).render(); + handleDeleteClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ deleteModal: true }); + }; + + closeDeleteModal = () => { + this.setState({ deleteModal: false }); }; - bulkApplyTemplate = () => { - new BulkApplyTemplateView({ - total: this.props.total, - selection: this.props.selection, - query: this.props.query, - qualifier: this.props.qualifier, - organization: this.props.organization - }).render(); + handleDeleteConfirm = () => { + this.closeDeleteModal(); + this.props.onDeleteProjects(); + }; + + handleBulkApplyTemplateClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ bulkApplyTemplateModal: true }); + }; + + closeBulkApplyTemplateModal = () => { + this.setState({ bulkApplyTemplateModal: false }); }; renderCheckbox = () => { @@ -93,7 +124,7 @@ export default class Search extends React.PureComponent { }; renderGhostsDescription = () => { - if (this.props.type !== TYPE.GHOSTS || !this.props.ready) { + if (this.props.type !== Type.Ghosts || !this.props.ready) { return null; } return ( @@ -120,8 +151,6 @@ export default class Search extends React.PureComponent { ); }; - renderSpinner = () => <i className="spinner" />; - render() { const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0; return ( @@ -130,7 +159,7 @@ export default class Search extends React.PureComponent { <tbody> <tr> <td className="thin text-middle"> - {this.props.ready ? this.renderCheckbox() : this.renderSpinner()} + {this.props.ready ? this.renderCheckbox() : <i className="spinner" />} </td> {this.renderQualifierFilter()} <td className="thin nowrap text-middle"> @@ -149,7 +178,7 @@ export default class Search extends React.PureComponent { <input onChange={this.search} value={this.props.query} - ref="input" + ref={node => (this.input = node!)} className="search-box-input input-medium" type="search" placeholder="Search" @@ -159,12 +188,12 @@ export default class Search extends React.PureComponent { <td className="thin nowrap text-middle"> <button className="spacer-right js-bulk-apply-permission-template" - onClick={this.bulkApplyTemplate}> + onClick={this.handleBulkApplyTemplateClick}> {translate('permission_templates.bulk_apply_permission_template')} </button> <button - onClick={this.deleteProjects} - className="button-red" + onClick={this.handleDeleteClick} + className="js-delete button-red" disabled={!isSomethingSelected}> {translate('delete')} </button> @@ -173,6 +202,26 @@ export default class Search extends React.PureComponent { </tbody> </table> {this.renderGhostsDescription()} + + {this.state.bulkApplyTemplateModal && + <BulkApplyTemplateModal + onClose={this.closeBulkApplyTemplateModal} + organization={this.props.organization.key} + qualifier={this.props.qualifiers} + query={this.props.query} + selection={this.props.selection} + total={this.props.total} + type={this.props.type} + />} + + {this.state.deleteModal && + <DeleteModal + onClose={this.closeDeleteModal} + onConfirm={this.handleDeleteConfirm} + organization={this.props.organization.key} + qualifier={this.props.qualifiers} + selection={this.props.selection} + />} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx new file mode 100644 index 00000000000..1c9569d5700 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx @@ -0,0 +1,148 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ +jest.mock('lodash', () => { + const lodash = require.requireActual('lodash'); + lodash.debounce = (fn: Function) => (...args: any[]) => fn(args); + return lodash; +}); + +jest.mock('../../../api/components', () => ({ + getComponents: jest.fn(), + getProvisioned: jest.fn(() => Promise.resolve({ paging: { total: 0 }, projects: [] })), + getGhosts: jest.fn(() => Promise.resolve({ projects: [], total: 0 })) +})); + +import * as React from 'react'; +import { mount } from 'enzyme'; +import App, { Props } from '../App'; +import { Type } from '../utils'; + +const getComponents = require('../../../api/components').getComponents as jest.Mock<any>; +const getProvisioned = require('../../../api/components').getProvisioned as jest.Mock<any>; +const getGhosts = require('../../../api/components').getGhosts as jest.Mock<any>; + +const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; + +const defaultSearchParameters = { + organization: 'org', + p: undefined, + ps: 50, + q: undefined +}; + +beforeEach(() => { + getComponents + .mockImplementation(() => Promise.resolve({ paging: { total: 0 }, components: [] })) + .mockClear(); + getProvisioned.mockClear(); + getGhosts.mockClear(); +}); + +it('fetches all projects on mount', () => { + mountRender(); + expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'TRK' }); +}); + +it('changes type', () => { + const wrapper = mountRender(); + wrapper.find('Search').prop<Function>('onTypeChanged')(Type.Provisioned); + expect(getProvisioned).lastCalledWith(defaultSearchParameters); + wrapper.find('Search').prop<Function>('onTypeChanged')(Type.Ghosts); + expect(getGhosts).lastCalledWith(defaultSearchParameters); +}); + +it('changes qualifier and resets type', () => { + const wrapper = mountRender(); + wrapper.setState({ type: Type.Provisioned }); + wrapper.find('Search').prop<Function>('onQualifierChanged')('VW'); + expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'VW' }); +}); + +it('searches', () => { + const wrapper = mountRender(); + wrapper.find('Search').prop<Function>('onSearch')('foo'); + expect(getComponents).lastCalledWith({ ...defaultSearchParameters, q: 'foo', qualifiers: 'TRK' }); +}); + +it('loads more', async () => { + const wrapper = mountRender(); + wrapper.find('ListFooter').prop<Function>('loadMore')(); + expect(getComponents).lastCalledWith({ ...defaultSearchParameters, p: 2, qualifiers: 'TRK' }); +}); + +it('selects and deselects projects', async () => { + getComponents.mockImplementation(() => + Promise.resolve({ paging: { total: 2 }, components: [{ key: 'foo' }, { key: 'bar' }] }) + ); + const wrapper = mountRender(); + await new Promise(setImmediate); + + wrapper.find('Projects').prop<Function>('onProjectSelected')('foo'); + expect(wrapper.state('selection')).toEqual(['foo']); + + wrapper.find('Projects').prop<Function>('onProjectSelected')('bar'); + expect(wrapper.state('selection')).toEqual(['foo', 'bar']); + + // should not select already selected project + wrapper.find('Projects').prop<Function>('onProjectSelected')('bar'); + expect(wrapper.state('selection')).toEqual(['foo', 'bar']); + + wrapper.find('Projects').prop<Function>('onProjectDeselected')('foo'); + expect(wrapper.state('selection')).toEqual(['bar']); + + wrapper.find('Search').prop<Function>('onAllDeselected')(); + expect(wrapper.state('selection')).toEqual([]); + + wrapper.find('Search').prop<Function>('onAllSelected')(); + expect(wrapper.state('selection')).toEqual(['foo', 'bar']); +}); + +it('creates project', () => { + const wrapper = mountRender(); + expect(wrapper.find('CreateProjectForm').exists()).toBeFalsy(); + + wrapper.find('Header').prop<Function>('onProjectCreate')(); + expect(wrapper.find('CreateProjectForm').exists()).toBeTruthy(); + + wrapper.find('CreateProjectForm').prop<Function>('onProjectCreated')(); + expect(getComponents.mock.calls).toHaveLength(2); + + wrapper.find('CreateProjectForm').prop<Function>('onClose')(); + expect(wrapper.find('CreateProjectForm').exists()).toBeFalsy(); +}); + +it('changes default project visibility', () => { + const onVisibilityChange = jest.fn(); + const wrapper = mountRender({ onVisibilityChange }); + wrapper.find('Header').prop<Function>('onVisibilityChange')('private'); + expect(onVisibilityChange).toBeCalledWith('private'); +}); + +function mountRender(props?: { [P in keyof Props]?: Props[P] }) { + return mount( + <App + hasProvisionPermission={true} + onVisibilityChange={jest.fn()} + organization={organization} + topLevelQualifiers={['TRK', 'VW', 'APP']} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx new file mode 100644 index 00000000000..1c8399e6eda --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx @@ -0,0 +1,128 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ +jest.mock('../../../api/permissions', () => ({ + applyTemplateToProject: jest.fn(), + bulkApplyTemplate: jest.fn(), + getPermissionTemplates: jest.fn() +})); + +import * as React from 'react'; +import { mount, shallow } from 'enzyme'; +import BulkApplyTemplateModal, { Props } from '../BulkApplyTemplateModal'; +import { Type } from '../utils'; +import { click } from '../../../helpers/testUtils'; + +const applyTemplateToProject = require('../../../api/permissions') + .applyTemplateToProject as jest.Mock<any>; +const bulkApplyTemplate = require('../../../api/permissions').bulkApplyTemplate as jest.Mock<any>; +const getPermissionTemplates = require('../../../api/permissions') + .getPermissionTemplates as jest.Mock<any>; + +beforeEach(() => { + applyTemplateToProject.mockImplementation(() => Promise.resolve()).mockClear(); + bulkApplyTemplate.mockImplementation(() => Promise.resolve()).mockClear(); + getPermissionTemplates + .mockImplementation(() => Promise.resolve({ permissionTemplates: [] })) + .mockClear(); +}); + +it('fetches permission templates on mount', () => { + mount(render()); + expect(getPermissionTemplates).toBeCalledWith('org'); +}); + +it('bulk applies template to all results', async () => { + const wrapper = shallow(render()); + (wrapper.instance() as BulkApplyTemplateModal).mounted = true; + expect(wrapper).toMatchSnapshot(); + + wrapper.setState({ + loading: false, + permissionTemplate: 'foo', + permissionTemplates: [{ id: 'foo', name: 'Foo' }, { id: 'bar', name: 'Bar' }] + }); + expect(wrapper).toMatchSnapshot(); + + click(wrapper.find('button')); + expect(bulkApplyTemplate).toBeCalledWith({ + organization: 'org', + q: 'bla', + qualifier: 'TRK', + templateId: 'foo' + }); + expect(wrapper).toMatchSnapshot(); + + await new Promise(setImmediate); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); + +it('bulk applies template to selected results', async () => { + const wrapper = shallow(render({ selection: ['proj1', 'proj2'] })); + (wrapper.instance() as BulkApplyTemplateModal).mounted = true; + expect(wrapper).toMatchSnapshot(); + + wrapper.setState({ + loading: false, + permissionTemplate: 'foo', + permissionTemplates: [{ id: 'foo', name: 'Foo' }, { id: 'bar', name: 'Bar' }] + }); + expect(wrapper).toMatchSnapshot(); + + click(wrapper.find('button')); + expect(wrapper).toMatchSnapshot(); + await new Promise(setImmediate); + expect(applyTemplateToProject.mock.calls).toHaveLength(2); + expect(applyTemplateToProject).toBeCalledWith({ + organization: 'org', + projectKey: 'proj1', + templateId: 'foo' + }); + expect(applyTemplateToProject).toBeCalledWith({ + organization: 'org', + projectKey: 'proj2', + templateId: 'foo' + }); + + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); + +it('closes', () => { + const onClose = jest.fn(); + const wrapper = shallow(render({ onClose })); + click(wrapper.find('.js-modal-close')); + expect(onClose).toBeCalled(); +}); + +function render(props?: { [P in keyof Props]?: Props[P] }) { + return ( + <BulkApplyTemplateModal + onClose={jest.fn()} + organization="org" + qualifier="TRK" + query="bla" + selection={[]} + total={17} + type={Type.All} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx new file mode 100644 index 00000000000..61dce6dbfe4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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'; +import { shallow } from 'enzyme'; +import ChangeVisibilityForm, { Props } from '../ChangeVisibilityForm'; +import { click } from '../../../helpers/testUtils'; + +const organization = { + canUpdateProjectsVisibilityToPrivate: true, + key: 'org', + name: 'org', + projectVisibility: 'public' +}; + +it('renders disabled', () => { + expect( + shallowRender({ + organization: { ...organization, canUpdateProjectsVisibilityToPrivate: false } + }) + ).toMatchSnapshot(); +}); + +it('closes', () => { + const onClose = jest.fn(); + const wrapper = shallowRender({ onClose }); + click(wrapper.find('.js-modal-close')); + expect(onClose).toBeCalled(); +}); + +it('changes visibility', () => { + const onConfirm = jest.fn(); + const wrapper = shallowRender({ onConfirm }); + expect(wrapper).toMatchSnapshot(); + + click(wrapper.find('a[data-visibility="private"]'), { + currentTarget: { + blur() {}, + dataset: { visibility: 'private' } + } + }); + expect(wrapper).toMatchSnapshot(); + + click(wrapper.find('.js-confirm')); + expect(onConfirm).toBeCalledWith('private'); +}); + +function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { + return shallow( + <ChangeVisibilityForm + onClose={jest.fn()} + onConfirm={jest.fn()} + organization={organization} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx new file mode 100644 index 00000000000..0212d094e8e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx @@ -0,0 +1,72 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ +jest.mock('../../../api/components', () => ({ + createProject: jest.fn(({ name }: { name: string }) => + Promise.resolve({ project: { key: name, name } }) + ) +})); + +import * as React from 'react'; +import { shallow } from 'enzyme'; +import CreateProjectForm from '../CreateProjectForm'; +import { change, submit } from '../../../helpers/testUtils'; + +const createProject = require('../../../api/components').createProject as jest.Mock<any>; + +const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; + +it('creates project', async () => { + const wrapper = shallow( + <CreateProjectForm + onClose={jest.fn()} + onProjectCreated={jest.fn()} + organization={organization} + /> + ); + (wrapper.instance() as CreateProjectForm).mounted = true; + expect(wrapper).toMatchSnapshot(); + + change(wrapper.find('input[name="name"]'), 'name', { + currentTarget: { name: 'name', value: 'name' } + }); + change(wrapper.find('input[name="branch"]'), 'branch', { + currentTarget: { name: 'branch', value: 'branch' } + }); + change(wrapper.find('input[name="key"]'), 'key', { + currentTarget: { name: 'key', value: 'key' } + }); + wrapper.find('VisibilitySelector').prop<Function>('onChange')('private'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + submit(wrapper.find('form')); + expect(createProject).toBeCalledWith({ + branch: 'branch', + name: 'name', + organization: 'org', + project: 'key', + visibility: 'private' + }); + expect(wrapper).toMatchSnapshot(); + + await new Promise(resolve => setImmediate(resolve)); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx new file mode 100644 index 00000000000..d24f30e9cdf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ +jest.mock('../../../api/components', () => ({ + deleteComponents: jest.fn(() => Promise.resolve()) +})); + +import * as React from 'react'; +import { shallow } from 'enzyme'; +import DeleteModal, { Props } from '../DeleteModal'; +import { click } from '../../../helpers/testUtils'; + +const deleteComponents = require('../../../api/components').deleteComponents as jest.Mock<any>; + +it('deletes projects', async () => { + const onConfirm = jest.fn(); + const wrapper = shallowRender({ onConfirm }); + (wrapper.instance() as DeleteModal).mounted = true; + expect(wrapper).toMatchSnapshot(); + + click(wrapper.find('button')); + expect(wrapper).toMatchSnapshot(); + expect(deleteComponents).toBeCalledWith(['foo', 'bar'], 'org'); + + await new Promise(setImmediate); + expect(onConfirm).toBeCalled(); +}); + +it('closes', () => { + const onClose = jest.fn(); + const wrapper = shallowRender({ onClose }); + click(wrapper.find('.js-modal-close')); + expect(onClose).toBeCalled(); +}); + +function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { + return shallow( + <DeleteModal + onClose={jest.fn()} + onConfirm={jest.fn()} + organization="org" + qualifier="TRK" + selection={['foo', 'bar']} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx new file mode 100644 index 00000000000..726c6b186cb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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'; +import { shallow } from 'enzyme'; +import Header, { Props } from '../Header'; +import { Visibility } from '../utils'; +import { click } from '../../../helpers/testUtils'; + +const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; + +it('renders', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('creates project', () => { + const onProjectCreate = jest.fn(); + const wrapper = shallowRender({ onProjectCreate }); + click(wrapper.find('#create-project')); + expect(onProjectCreate).toBeCalledWith(); +}); + +it('changes default visibility', () => { + const onVisibilityChange = jest.fn(); + const wrapper = shallowRender({ onVisibilityChange }); + + click(wrapper.find('.js-change-visibility')); + + const modalWrapper = wrapper.find('ChangeVisibilityForm'); + expect(modalWrapper).toMatchSnapshot(); + modalWrapper.prop<Function>('onConfirm')(Visibility.Private); + expect(onVisibilityChange).toBeCalledWith(Visibility.Private); + + modalWrapper.prop<Function>('onClose')(); + expect(wrapper.find('ChangeVisibilityForm').exists()).toBeFalsy(); +}); + +function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { + return shallow( + <Header + hasProvisionPermission={true} + onProjectCreate={jest.fn()} + onVisibilityChange={jest.fn()} + organization={organization} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx new file mode 100644 index 00000000000..7b7bb09d5ae --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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'; +import { shallow } from 'enzyme'; +import ProjectRow from '../ProjectRow'; +import { Visibility } from '../utils'; +import { click } from '../../../helpers/testUtils'; + +const project = { + key: 'project', + name: 'Project', + qualifier: 'TRK', + visibility: Visibility.Private +}; + +it('renders', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('checks project', () => { + const onProjectCheck = jest.fn(); + const wrapper = shallowRender({ onProjectCheck }); + wrapper.find('Checkbox').prop<Function>('onCheck')(false); + expect(onProjectCheck).toBeCalledWith(project, false); +}); + +it('applies permission template', () => { + const onApplyTemplateClick = jest.fn(); + const wrapper = shallowRender({ onApplyTemplateClick }); + click(wrapper.find('.js-apply-template')); + expect(onApplyTemplateClick).toBeCalledWith(project); +}); + +function shallowRender(props?: any) { + return shallow( + <ProjectRow + onApplyTemplateClick={jest.fn()} + onProjectCheck={jest.fn()} + project={project} + selected={true} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx new file mode 100644 index 00000000000..b1f165cde7a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +jest.mock('../../permissions/project/views/ApplyTemplateView'); + +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Projects from '../Projects'; +import { Visibility } from '../utils'; +import ApplyTemplateView from '../../permissions/project/views/ApplyTemplateView'; + +const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; +const projects = [ + { key: 'a', name: 'A', qualifier: 'TRK', visibility: Visibility.Public }, + { key: 'b', name: 'B', qualifier: 'TRK', visibility: Visibility.Public } +]; +const selection = ['a']; + +it('renders list of projects', () => { + expect(shallowRender({ projects, selection })).toMatchSnapshot(); +}); + +it('selects and deselects project', () => { + const onProjectDeselected = jest.fn(); + const onProjectSelected = jest.fn(); + const wrapper = shallowRender({ onProjectDeselected, onProjectSelected, projects }); + + wrapper.find('ProjectRow').first().prop<Function>('onProjectCheck')(projects[0], true); + expect(onProjectSelected).toBeCalledWith('a'); + + wrapper.find('ProjectRow').first().prop<Function>('onProjectCheck')(projects[0], false); + expect(onProjectDeselected).toBeCalledWith('a'); +}); + +it('opens modal to apply permission template', () => { + const wrapper = shallowRender({ projects }); + wrapper.find('ProjectRow').first().prop<Function>('onApplyTemplateClick')(projects[0]); + expect(ApplyTemplateView).toBeCalledWith({ organization, project: projects[0] }); +}); + +function shallowRender(props?: any) { + return shallow( + <Projects + onProjectDeselected={jest.fn()} + onProjectSelected={jest.fn()} + organization={organization} + selection={[]} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx new file mode 100644 index 00000000000..950b78e1624 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx @@ -0,0 +1,107 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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'; +import { shallow } from 'enzyme'; +import Search, { Props } from '../Search'; +import { Type } from '../utils'; +import { change, click } from '../../../helpers/testUtils'; + +const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; + +it('renders', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('render qualifiers filter', () => { + expect(shallowRender({ topLevelQualifiers: ['TRK', 'VW', 'APP'] })).toMatchSnapshot(); +}); + +it('updates qualifier', () => { + const onQualifierChanged = jest.fn(); + const wrapper = shallowRender({ onQualifierChanged, topLevelQualifiers: ['TRK', 'VW', 'APP'] }); + wrapper.find('RadioToggle[name="projects-qualifier"]').prop<Function>('onCheck')('VW'); + expect(onQualifierChanged).toBeCalledWith('VW'); +}); + +it('updates type', () => { + const onTypeChanged = jest.fn(); + const wrapper = shallowRender({ onTypeChanged }); + wrapper.find('RadioToggle[name="projects-type"]').prop<Function>('onCheck')(Type.Provisioned); + expect(onTypeChanged).toBeCalledWith(Type.Provisioned); +}); + +it('searches', () => { + const onSearch = jest.fn(); + const wrapper = shallowRender({ onSearch }); + change(wrapper.find('input[type="search"]'), 'foo'); + expect(onSearch).toBeCalledWith('foo'); +}); + +it('checks all or none projects', () => { + const onAllDeselected = jest.fn(); + const onAllSelected = jest.fn(); + const wrapper = shallowRender({ onAllDeselected, onAllSelected }); + + wrapper.find('Checkbox').prop<Function>('onCheck')(true); + expect(onAllSelected).toBeCalled(); + + wrapper.find('Checkbox').prop<Function>('onCheck')(false); + expect(onAllDeselected).toBeCalled(); +}); + +it('deletes projects', () => { + const onDeleteProjects = jest.fn(); + const wrapper = shallowRender({ onDeleteProjects, selection: ['foo', 'bar'] }); + click(wrapper.find('.js-delete')); + expect(wrapper.find('DeleteModal')).toMatchSnapshot(); + wrapper.find('DeleteModal').prop<Function>('onConfirm')(); + expect(onDeleteProjects).toBeCalled(); +}); + +it('bulk applies permission template', () => { + const wrapper = shallowRender({}); + click(wrapper.find('.js-bulk-apply-permission-template')); + expect(wrapper.find('BulkApplyTemplateModal')).toMatchSnapshot(); + wrapper.find('BulkApplyTemplateModal').prop<Function>('onClose')(); + expect(wrapper.find('BulkApplyTemplateModal').exists()).toBeFalsy(); +}); + +function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { + return shallow( + <Search + onAllDeselected={jest.fn()} + onAllSelected={jest.fn()} + onDeleteProjects={jest.fn()} + onQualifierChanged={jest.fn()} + onSearch={jest.fn()} + onTypeChanged={jest.fn()} + organization={organization} + projects={[]} + qualifiers="TRK" + query="" + ready={true} + selection={[]} + topLevelQualifiers={['TRK']} + total={0} + type={Type.All} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap new file mode 100644 index 00000000000..bc3e39d7f64 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap @@ -0,0 +1,643 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`bulk applies template to all results 1`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="permission_templates.bulk_apply_permission_template" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + permission_templates.bulk_apply_permission_template + </h2> + </header> + <div + className="modal-body" + > + <i + className="spinner" + /> + </div> + <footer + className="modal-foot" + > + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; + +exports[`bulk applies template to all results 2`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="permission_templates.bulk_apply_permission_template" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + permission_templates.bulk_apply_permission_template + </h2> + </header> + <div + className="modal-body" + > + <div + className="alert alert-warning" + > + permission_templates.bulk_apply_permission_template.apply_to_all.17 + </div> + <div + className="modal-field" + > + <label> + template + <em + className="mandatory" + > + * + </em> + </label> + <Select + addLabelText="Add \\"{label}\\"?" + arrowRenderer={[Function]} + autosize={true} + backspaceRemoves={true} + backspaceToRemoveMessage="Press backspace to remove {label}" + clearAllText="Clear all" + clearRenderer={[Function]} + clearValueText="Clear value" + clearable={false} + deleteRemoves={true} + delimiter="," + disabled={false} + escapeClearsValue={true} + filterOptions={[Function]} + ignoreAccents={true} + ignoreCase={true} + inputProps={Object {}} + isLoading={false} + joinValues={false} + labelKey="label" + matchPos="any" + matchProp="any" + menuBuffer={0} + menuRenderer={[Function]} + multi={false} + noResultsText="No results found" + onBlurResetsInput={true} + onChange={[Function]} + onCloseResetsInput={true} + optionComponent={[Function]} + options={ + Array [ + Object { + "label": "Foo", + "value": "foo", + }, + Object { + "label": "Bar", + "value": "bar", + }, + ] + } + pageSize={5} + placeholder="Select..." + required={false} + scrollMenuIntoView={true} + searchable={false} + simpleValue={false} + tabSelectsValue={true} + value="foo" + valueComponent={[Function]} + valueKey="value" + /> + </div> + </div> + <footer + className="modal-foot" + > + <button + disabled={false} + onClick={[Function]} + > + apply + </button> + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; + +exports[`bulk applies template to all results 3`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="permission_templates.bulk_apply_permission_template" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + permission_templates.bulk_apply_permission_template + </h2> + </header> + <div + className="modal-body" + > + <div + className="alert alert-warning" + > + permission_templates.bulk_apply_permission_template.apply_to_all.17 + </div> + <div + className="modal-field" + > + <label> + template + <em + className="mandatory" + > + * + </em> + </label> + <Select + addLabelText="Add \\"{label}\\"?" + arrowRenderer={[Function]} + autosize={true} + backspaceRemoves={true} + backspaceToRemoveMessage="Press backspace to remove {label}" + clearAllText="Clear all" + clearRenderer={[Function]} + clearValueText="Clear value" + clearable={false} + deleteRemoves={true} + delimiter="," + disabled={true} + escapeClearsValue={true} + filterOptions={[Function]} + ignoreAccents={true} + ignoreCase={true} + inputProps={Object {}} + isLoading={false} + joinValues={false} + labelKey="label" + matchPos="any" + matchProp="any" + menuBuffer={0} + menuRenderer={[Function]} + multi={false} + noResultsText="No results found" + onBlurResetsInput={true} + onChange={[Function]} + onCloseResetsInput={true} + optionComponent={[Function]} + options={ + Array [ + Object { + "label": "Foo", + "value": "foo", + }, + Object { + "label": "Bar", + "value": "bar", + }, + ] + } + pageSize={5} + placeholder="Select..." + required={false} + scrollMenuIntoView={true} + searchable={false} + simpleValue={false} + tabSelectsValue={true} + value="foo" + valueComponent={[Function]} + valueKey="value" + /> + </div> + </div> + <footer + className="modal-foot" + > + <i + className="spinner spacer-right" + /> + <button + disabled={true} + onClick={[Function]} + > + apply + </button> + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; + +exports[`bulk applies template to all results 4`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="permission_templates.bulk_apply_permission_template" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + permission_templates.bulk_apply_permission_template + </h2> + </header> + <div + className="modal-body" + > + <div + className="alert alert-success" + > + projects_role.apply_template.success + </div> + </div> + <footer + className="modal-foot" + > + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + close + </a> + </footer> +</Modal> +`; + +exports[`bulk applies template to selected results 1`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="permission_templates.bulk_apply_permission_template" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + permission_templates.bulk_apply_permission_template + </h2> + </header> + <div + className="modal-body" + > + <i + className="spinner" + /> + </div> + <footer + className="modal-foot" + > + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; + +exports[`bulk applies template to selected results 2`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="permission_templates.bulk_apply_permission_template" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + permission_templates.bulk_apply_permission_template + </h2> + </header> + <div + className="modal-body" + > + <div + className="alert alert-info" + > + permission_templates.bulk_apply_permission_template.apply_to_selected.2 + </div> + <div + className="modal-field" + > + <label> + template + <em + className="mandatory" + > + * + </em> + </label> + <Select + addLabelText="Add \\"{label}\\"?" + arrowRenderer={[Function]} + autosize={true} + backspaceRemoves={true} + backspaceToRemoveMessage="Press backspace to remove {label}" + clearAllText="Clear all" + clearRenderer={[Function]} + clearValueText="Clear value" + clearable={false} + deleteRemoves={true} + delimiter="," + disabled={false} + escapeClearsValue={true} + filterOptions={[Function]} + ignoreAccents={true} + ignoreCase={true} + inputProps={Object {}} + isLoading={false} + joinValues={false} + labelKey="label" + matchPos="any" + matchProp="any" + menuBuffer={0} + menuRenderer={[Function]} + multi={false} + noResultsText="No results found" + onBlurResetsInput={true} + onChange={[Function]} + onCloseResetsInput={true} + optionComponent={[Function]} + options={ + Array [ + Object { + "label": "Foo", + "value": "foo", + }, + Object { + "label": "Bar", + "value": "bar", + }, + ] + } + pageSize={5} + placeholder="Select..." + required={false} + scrollMenuIntoView={true} + searchable={false} + simpleValue={false} + tabSelectsValue={true} + value="foo" + valueComponent={[Function]} + valueKey="value" + /> + </div> + </div> + <footer + className="modal-foot" + > + <button + disabled={false} + onClick={[Function]} + > + apply + </button> + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; + +exports[`bulk applies template to selected results 3`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="permission_templates.bulk_apply_permission_template" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + permission_templates.bulk_apply_permission_template + </h2> + </header> + <div + className="modal-body" + > + <div + className="alert alert-info" + > + permission_templates.bulk_apply_permission_template.apply_to_selected.2 + </div> + <div + className="modal-field" + > + <label> + template + <em + className="mandatory" + > + * + </em> + </label> + <Select + addLabelText="Add \\"{label}\\"?" + arrowRenderer={[Function]} + autosize={true} + backspaceRemoves={true} + backspaceToRemoveMessage="Press backspace to remove {label}" + clearAllText="Clear all" + clearRenderer={[Function]} + clearValueText="Clear value" + clearable={false} + deleteRemoves={true} + delimiter="," + disabled={true} + escapeClearsValue={true} + filterOptions={[Function]} + ignoreAccents={true} + ignoreCase={true} + inputProps={Object {}} + isLoading={false} + joinValues={false} + labelKey="label" + matchPos="any" + matchProp="any" + menuBuffer={0} + menuRenderer={[Function]} + multi={false} + noResultsText="No results found" + onBlurResetsInput={true} + onChange={[Function]} + onCloseResetsInput={true} + optionComponent={[Function]} + options={ + Array [ + Object { + "label": "Foo", + "value": "foo", + }, + Object { + "label": "Bar", + "value": "bar", + }, + ] + } + pageSize={5} + placeholder="Select..." + required={false} + scrollMenuIntoView={true} + searchable={false} + simpleValue={false} + tabSelectsValue={true} + value="foo" + valueComponent={[Function]} + valueKey="value" + /> + </div> + </div> + <footer + className="modal-foot" + > + <i + className="spinner spacer-right" + /> + <button + disabled={true} + onClick={[Function]} + > + apply + </button> + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; + +exports[`bulk applies template to selected results 4`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="permission_templates.bulk_apply_permission_template" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + permission_templates.bulk_apply_permission_template + </h2> + </header> + <div + className="modal-body" + > + <div + className="alert alert-success" + > + projects_role.apply_template.success + </div> + </div> + <footer + className="modal-foot" + > + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + close + </a> + </footer> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap new file mode 100644 index 00000000000..d46e88f161f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap @@ -0,0 +1,308 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`changes visibility 1`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="modal form" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + organization.change_visibility_form.header + </h2> + </header> + <div + className="modal-body" + > + <div + className="big-spacer-bottom" + > + <p> + <a + className="link-base-color link-no-underline" + data-visibility="public" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right is-checked" + /> + visibility.public + </a> + </p> + <p + className="text-muted spacer-top" + style={ + Object { + "paddingLeft": 22, + } + } + > + visibility.public.description.short + </p> + </div> + <div + className="big-spacer-bottom" + > + <p> + <a + className="link-base-color link-no-underline" + data-visibility="private" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right" + /> + visibility.private + </a> + </p> + <p + className="text-muted spacer-top" + style={ + Object { + "paddingLeft": 22, + } + } + > + visibility.private.description.short + </p> + </div> + <div + className="alert alert-warning" + > + organization.change_visibility_form.warning + </div> + </div> + <footer + className="modal-foot" + > + <button + className="js-confirm" + onClick={[Function]} + > + organization.change_visibility_form.submit + </button> + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; + +exports[`changes visibility 2`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="modal form" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + organization.change_visibility_form.header + </h2> + </header> + <div + className="modal-body" + > + <div + className="big-spacer-bottom" + > + <p> + <a + className="link-base-color link-no-underline" + data-visibility="public" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right" + /> + visibility.public + </a> + </p> + <p + className="text-muted spacer-top" + style={ + Object { + "paddingLeft": 22, + } + } + > + visibility.public.description.short + </p> + </div> + <div + className="big-spacer-bottom" + > + <p> + <a + className="link-base-color link-no-underline" + data-visibility="private" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right is-checked" + /> + visibility.private + </a> + </p> + <p + className="text-muted spacer-top" + style={ + Object { + "paddingLeft": 22, + } + } + > + visibility.private.description.short + </p> + </div> + <div + className="alert alert-warning" + > + organization.change_visibility_form.warning + </div> + </div> + <footer + className="modal-foot" + > + <button + className="js-confirm" + onClick={[Function]} + > + organization.change_visibility_form.submit + </button> + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; + +exports[`renders disabled 1`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="modal form" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + organization.change_visibility_form.header + </h2> + </header> + <div + className="modal-body" + > + <div + className="big-spacer-bottom" + > + <p> + <a + className="link-base-color link-no-underline" + data-visibility="public" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right is-checked" + /> + visibility.public + </a> + </p> + <p + className="text-muted spacer-top" + style={ + Object { + "paddingLeft": 22, + } + } + > + visibility.public.description.short + </p> + </div> + <div + className="big-spacer-bottom" + > + <p> + <span + className="text-muted cursor-not-allowed" + > + <i + className="icon-radio spacer-right" + /> + visibility.private + </span> + </p> + <p + className="text-muted spacer-top" + style={ + Object { + "paddingLeft": 22, + } + } + > + visibility.private.description.short + </p> + </div> + <UpgradeOrganizationBox + organization="org" + /> + </div> + <footer + className="modal-foot" + > + <button + className="js-confirm" + onClick={[Function]} + > + organization.change_visibility_form.submit + </button> + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap new file mode 100644 index 00000000000..fad19337758 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap @@ -0,0 +1,468 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`creates project 1`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="modal form" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <form + id="create-project-form" + onSubmit={[Function]} + > + <header + className="modal-head" + > + <h2> + qualifiers.create.TRK + </h2> + </header> + <div + className="modal-body" + > + <div + className="modal-field" + > + <label + htmlFor="create-project-name" + > + name + <em + className="mandatory" + > + * + </em> + </label> + <input + autoFocus={true} + id="create-project-name" + maxLength={2000} + name="name" + onChange={[Function]} + required={true} + type="text" + value="" + /> + </div> + <div + className="modal-field" + > + <label + htmlFor="create-project-branch" + > + branch + </label> + <input + id="create-project-branch" + maxLength={200} + name="branch" + onChange={[Function]} + type="text" + value="" + /> + </div> + <div + className="modal-field" + > + <label + htmlFor="create-project-key" + > + key + <em + className="mandatory" + > + * + </em> + </label> + <input + id="create-project-key" + maxLength={400} + name="key" + onChange={[Function]} + required={true} + type="text" + value="" + /> + </div> + <div + className="modal-field" + > + <label> + visibility + </label> + <VisibilitySelector + className="little-spacer-top" + onChange={[Function]} + visibility="public" + /> + <div + className="spacer-top" + > + <UpgradeOrganizationBox + organization="org" + /> + </div> + </div> + </div> + <footer + className="modal-foot" + > + <button + disabled={false} + id="create-project-submit" + type="submit" + > + create + </button> + <a + href="#" + id="create-project-cancel" + onClick={[Function]} + > + cancel + </a> + </footer> + </form> +</Modal> +`; + +exports[`creates project 2`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="modal form" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <form + id="create-project-form" + onSubmit={[Function]} + > + <header + className="modal-head" + > + <h2> + qualifiers.create.TRK + </h2> + </header> + <div + className="modal-body" + > + <div + className="modal-field" + > + <label + htmlFor="create-project-name" + > + name + <em + className="mandatory" + > + * + </em> + </label> + <input + autoFocus={true} + id="create-project-name" + maxLength={2000} + name="name" + onChange={[Function]} + required={true} + type="text" + value="name" + /> + </div> + <div + className="modal-field" + > + <label + htmlFor="create-project-branch" + > + branch + </label> + <input + id="create-project-branch" + maxLength={200} + name="branch" + onChange={[Function]} + type="text" + value="branch" + /> + </div> + <div + className="modal-field" + > + <label + htmlFor="create-project-key" + > + key + <em + className="mandatory" + > + * + </em> + </label> + <input + id="create-project-key" + maxLength={400} + name="key" + onChange={[Function]} + required={true} + type="text" + value="key" + /> + </div> + <div + className="modal-field" + > + <label> + visibility + </label> + <VisibilitySelector + className="little-spacer-top" + onChange={[Function]} + visibility="private" + /> + <div + className="spacer-top" + > + <UpgradeOrganizationBox + organization="org" + /> + </div> + </div> + </div> + <footer + className="modal-foot" + > + <button + disabled={false} + id="create-project-submit" + type="submit" + > + create + </button> + <a + href="#" + id="create-project-cancel" + onClick={[Function]} + > + cancel + </a> + </footer> + </form> +</Modal> +`; + +exports[`creates project 3`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="modal form" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <form + id="create-project-form" + onSubmit={[Function]} + > + <header + className="modal-head" + > + <h2> + qualifiers.create.TRK + </h2> + </header> + <div + className="modal-body" + > + <div + className="modal-field" + > + <label + htmlFor="create-project-name" + > + name + <em + className="mandatory" + > + * + </em> + </label> + <input + autoFocus={true} + id="create-project-name" + maxLength={2000} + name="name" + onChange={[Function]} + required={true} + type="text" + value="name" + /> + </div> + <div + className="modal-field" + > + <label + htmlFor="create-project-branch" + > + branch + </label> + <input + id="create-project-branch" + maxLength={200} + name="branch" + onChange={[Function]} + type="text" + value="branch" + /> + </div> + <div + className="modal-field" + > + <label + htmlFor="create-project-key" + > + key + <em + className="mandatory" + > + * + </em> + </label> + <input + id="create-project-key" + maxLength={400} + name="key" + onChange={[Function]} + required={true} + type="text" + value="key" + /> + </div> + <div + className="modal-field" + > + <label> + visibility + </label> + <VisibilitySelector + className="little-spacer-top" + onChange={[Function]} + visibility="private" + /> + <div + className="spacer-top" + > + <UpgradeOrganizationBox + organization="org" + /> + </div> + </div> + </div> + <footer + className="modal-foot" + > + <i + className="spinner spacer-right" + /> + <button + disabled={true} + id="create-project-submit" + type="submit" + > + create + </button> + <a + href="#" + id="create-project-cancel" + onClick={[Function]} + > + cancel + </a> + </footer> + </form> +</Modal> +`; + +exports[`creates project 4`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="modal form" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <div> + <header + className="modal-head" + > + <h2> + qualifiers.create.TRK + </h2> + </header> + <div + className="modal-body" + > + <div + className="alert alert-success" + > + Project + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "id": "name", + }, + } + } + > + name + </Link> + + has been successfully created. + </div> + </div> + <footer + className="modal-foot" + > + <a + href="#" + id="create-project-close" + onClick={[Function]} + > + close + </a> + </footer> + </div> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap new file mode 100644 index 00000000000..e57788fbc92 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`deletes projects 1`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="qualifiers.delete.TRK" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + qualifiers.delete.TRK + </h2> + </header> + <div + className="modal-body" + > + qualifiers.delete_confirm.TRK + </div> + <footer + className="modal-foot" + > + <button + className="button-red" + disabled={false} + onClick={[Function]} + > + delete + </button> + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; + +exports[`deletes projects 2`] = ` +<Modal + ariaHideApp={true} + bodyOpenClassName="ReactModal__Body--open" + className="modal" + closeTimeoutMS={0} + contentLabel="qualifiers.delete.TRK" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true} +> + <header + className="modal-head" + > + <h2> + qualifiers.delete.TRK + </h2> + </header> + <div + className="modal-body" + > + qualifiers.delete_confirm.TRK + </div> + <footer + className="modal-foot" + > + <i + className="spinner spacer-right" + /> + <button + className="button-red" + disabled={true} + onClick={[Function]} + > + delete + </button> + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + cancel + </a> + </footer> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap new file mode 100644 index 00000000000..9fb16cfbed5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`changes default visibility 1`] = ` +<ChangeVisibilityForm + onClose={[Function]} + onConfirm={[Function]} + organization={ + Object { + "key": "org", + "name": "org", + "projectVisibility": "public", + } + } +/> +`; + +exports[`renders 1`] = ` +<header + className="page-header" +> + <h1 + className="page-title" + > + projects_management + </h1> + <div + className="page-actions" + > + <span + className="big-spacer-right" + > + organization.default_visibility_of_new_projects + + <strong> + visibility.public + </strong> + <a + className="js-change-visibility spacer-left icon-edit" + href="#" + onClick={[Function]} + /> + </span> + <button + id="create-project" + onClick={[Function]} + > + qualifiers.create.TRK + </button> + </div> + <p + className="page-description" + > + projects_management.page.description + </p> +</header> +`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap new file mode 100644 index 00000000000..b306b2fe020 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +<tr> + <td + className="thin" + > + <Checkbox + checked={true} + onCheck={[Function]} + thirdState={false} + /> + </td> + <td + className="nowrap" + > + <Link + className="link-with-icon" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "id": "project", + }, + } + } + > + <QualifierIcon + qualifier="TRK" + /> + + <span> + Project + </span> + </Link> + </td> + <td + className="nowrap" + > + <span + className="note" + > + project + </span> + </td> + <td + className="width-20" + > + <PrivateBadge /> + </td> + <td + className="thin nowrap" + > + <div + className="dropdown" + > + <button + className="dropdown-toggle" + data-toggle="dropdown" + > + actions + + <i + className="icon-dropdown" + /> + </button> + <ul + className="dropdown-menu dropdown-menu-right" + > + <li> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project_roles", + "query": Object { + "id": "project", + }, + } + } + > + edit_permissions + </Link> + </li> + <li> + <a + className="js-apply-template" + href="#" + onClick={[Function]} + > + projects_role.apply_template + </a> + </li> + </ul> + </div> + </td> +</tr> +`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap new file mode 100644 index 00000000000..14bb03d1ec6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders list of projects 1`] = ` +<table + className="data zebra new-loading" + id="projects-management-page-projects" +> + <tbody> + <ProjectRow + onApplyTemplateClick={[Function]} + onProjectCheck={[Function]} + project={ + Object { + "key": "a", + "name": "A", + "qualifier": "TRK", + "visibility": "public", + } + } + selected={true} + /> + <ProjectRow + onApplyTemplateClick={[Function]} + onProjectCheck={[Function]} + project={ + Object { + "key": "b", + "name": "B", + "qualifier": "TRK", + "visibility": "public", + } + } + selected={false} + /> + </tbody> +</table> +`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap new file mode 100644 index 00000000000..84838c7bc2e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`bulk applies permission template 1`] = ` +<BulkApplyTemplateModal + onClose={[Function]} + organization="org" + qualifier="TRK" + query="" + selection={Array []} + total={0} + type="ALL" +/> +`; + +exports[`deletes projects 1`] = ` +<DeleteModal + onClose={[Function]} + onConfirm={[Function]} + organization="org" + qualifier="TRK" + selection={ + Array [ + "foo", + "bar", + ] + } +/> +`; + +exports[`render qualifiers filter 1`] = ` +<div + className="panel panel-vertical bordered-bottom spacer-bottom" +> + <table + className="data" + > + <tbody> + <tr> + <td + className="thin text-middle" + > + <Checkbox + checked={false} + onCheck={[Function]} + thirdState={false} + /> + </td> + <td + className="thin nowrap text-middle" + > + <RadioToggle + disabled={false} + name="projects-qualifier" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "qualifiers.TRK", + "value": "TRK", + }, + Object { + "label": "qualifiers.VW", + "value": "VW", + }, + Object { + "label": "qualifiers.APP", + "value": "APP", + }, + ] + } + value="TRK" + /> + </td> + <td + className="thin nowrap text-middle" + > + <RadioToggle + disabled={false} + name="projects-type" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "All", + "value": "ALL", + }, + Object { + "label": "Provisioned", + "value": "PROVISIONED", + }, + Object { + "label": "Ghosts", + "value": "GHOSTS", + }, + ] + } + value="ALL" + /> + </td> + <td + className="text-middle" + > + <form + className="search-box" + onSubmit={[Function]} + > + <button + className="search-box-submit button-clean" + > + <i + className="icon-search" + /> + </button> + <input + className="search-box-input input-medium" + onChange={[Function]} + placeholder="Search" + type="search" + value="" + /> + </form> + </td> + <td + className="thin nowrap text-middle" + > + <button + className="spacer-right js-bulk-apply-permission-template" + onClick={[Function]} + > + permission_templates.bulk_apply_permission_template + </button> + <button + className="js-delete button-red" + disabled={true} + onClick={[Function]} + > + delete + </button> + </td> + </tr> + </tbody> + </table> +</div> +`; + +exports[`renders 1`] = ` +<div + className="panel panel-vertical bordered-bottom spacer-bottom" +> + <table + className="data" + > + <tbody> + <tr> + <td + className="thin text-middle" + > + <Checkbox + checked={false} + onCheck={[Function]} + thirdState={false} + /> + </td> + <td + className="thin nowrap text-middle" + > + <RadioToggle + disabled={false} + name="projects-type" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "All", + "value": "ALL", + }, + Object { + "label": "Provisioned", + "value": "PROVISIONED", + }, + Object { + "label": "Ghosts", + "value": "GHOSTS", + }, + ] + } + value="ALL" + /> + </td> + <td + className="text-middle" + > + <form + className="search-box" + onSubmit={[Function]} + > + <button + className="search-box-submit button-clean" + > + <i + className="icon-search" + /> + </button> + <input + className="search-box-input input-medium" + onChange={[Function]} + placeholder="Search" + type="search" + value="" + /> + </form> + </td> + <td + className="thin nowrap text-middle" + > + <button + className="spacer-right js-bulk-apply-permission-template" + onClick={[Function]} + > + permission_templates.bulk_apply_permission_template + </button> + <button + className="js-delete button-red" + disabled={true} + onClick={[Function]} + > + delete + </button> + </td> + </tr> + </tbody> + </table> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/projects-admin/routes.ts b/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts index 447c6ae73dd..447c6ae73dd 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/routes.ts +++ b/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts diff --git a/server/sonar-web/src/main/js/apps/projects-admin/constants.js b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts index 057d08d9109..4e3f01888b3 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/constants.js +++ b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts @@ -21,8 +21,20 @@ export const PAGE_SIZE = 50; export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV']; -export const TYPE = { - ALL: 'ALL', - PROVISIONED: 'PROVISIONED', - GHOSTS: 'GHOSTS' -}; +export enum Type { + All = 'ALL', + Provisioned = 'PROVISIONED', + Ghosts = 'GHOSTS' +} + +export interface Project { + key: string; + name: string; + qualifier: string; + visibility: Visibility; +} + +export enum Visibility { + Public = 'public', + Private = 'private' +} diff --git a/server/sonar-web/src/main/js/components/common/PrivateBadge.js b/server/sonar-web/src/main/js/components/common/PrivateBadge.tsx index 0336cf7609e..d360d2105c6 100644 --- a/server/sonar-web/src/main/js/components/common/PrivateBadge.js +++ b/server/sonar-web/src/main/js/components/common/PrivateBadge.tsx @@ -17,20 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import Tooltip from '../controls/Tooltip'; import { translate } from '../../helpers/l10n'; -/*:: -type Props = { - className?: string, - tooltipPlacement?: string -}; -*/ +interface Props { + className?: string; + tooltipPlacement?: string; +} -export default function PrivateBadge({ className, tooltipPlacement = 'bottom' } /*: Props */) { +export default function PrivateBadge({ className, tooltipPlacement = 'bottom' }: Props) { return ( <Tooltip overlay={translate('visibility.private.description')} placement={tooltipPlacement}> <div className={classNames('outline-badge', className)}> diff --git a/server/sonar-web/src/main/js/components/common/VisibilitySelector.js b/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx index 40233097c9c..4e3642d03b0 100644 --- a/server/sonar-web/src/main/js/components/common/VisibilitySelector.js +++ b/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx @@ -17,30 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import { translate } from '../../helpers/l10n'; -/*:: -type Props = {| - canTurnToPrivate: boolean, - className?: string, - onChange: string => void, - visibility: string -|}; -*/ - -export default class VisibilitySelector extends React.PureComponent { - /*:: props: Props; */ +interface Props { + canTurnToPrivate?: boolean; + className?: string; + onChange: (x: string) => void; + visibility: string; +} - handlePublicClick = (event /*: Event & { currentTarget: HTMLElement } */) => { +export default class VisibilitySelector extends React.PureComponent<Props> { + handlePublicClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); this.props.onChange('public'); }; - handlePrivateClick = (event /*: Event & { currentTarget: HTMLElement } */) => { + handlePrivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); this.props.onChange('private'); diff --git a/server/sonar-web/src/main/js/apps/projects-admin/delete-view.js b/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx index 99bc391cbc5..02bdfcab213 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/delete-view.js +++ b/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx @@ -1,7 +1,7 @@ /* * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 @@ -17,15 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import ModalForm from '../../components/common/modal-form'; -import Template from './templates/projects-delete.hbs'; +import * as React from 'react'; +import { shallow } from 'enzyme'; +import PrivateBadge from '../PrivateBadge'; -export default ModalForm.extend({ - template: Template, - - onFormSubmit() { - ModalForm.prototype.onFormSubmit.apply(this, arguments); - this.options.deleteProjects(); - this.destroy(); - } +it('renders', () => { + expect(shallow(<PrivateBadge />)).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx new file mode 100644 index 00000000000..477df5763ab --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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'; +import { shallow } from 'enzyme'; +import VisibilitySelector from '../VisibilitySelector'; +import { click } from '../../../helpers/testUtils'; + +it('changes visibility', () => { + const onChange = jest.fn(); + const wrapper = shallow( + <VisibilitySelector canTurnToPrivate={true} onChange={onChange} visibility="public" /> + ); + expect(wrapper).toMatchSnapshot(); + + click(wrapper.find('#visibility-private')); + expect(onChange).toBeCalledWith('private'); + + wrapper.setProps({ visibility: 'private' }); + expect(wrapper).toMatchSnapshot(); + + click(wrapper.find('#visibility-public')); + expect(onChange).toBeCalledWith('public'); +}); + +it('renders disabled', () => { + expect( + shallow( + <VisibilitySelector canTurnToPrivate={false} onChange={jest.fn()} visibility="public" /> + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap new file mode 100644 index 00000000000..d8f24f25b26 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +<Tooltip + overlay="visibility.private.description" + placement="bottom" +> + <div + className="outline-badge" + > + visibility.private + </div> +</Tooltip> +`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/VisibilitySelector-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/VisibilitySelector-test.tsx.snap new file mode 100644 index 00000000000..745a71b9451 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/VisibilitySelector-test.tsx.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`changes visibility 1`] = ` +<div> + <a + className="link-base-color link-no-underline" + href="#" + id="visibility-public" + onClick={[Function]} + > + <i + className="icon-radio is-checked" + /> + <span + className="spacer-left" + > + visibility.public + </span> + </a> + <a + className="link-base-color link-no-underline huge-spacer-left" + href="#" + id="visibility-private" + onClick={[Function]} + > + <i + className="icon-radio" + /> + <span + className="spacer-left" + > + visibility.private + </span> + </a> +</div> +`; + +exports[`changes visibility 2`] = ` +<div> + <a + className="link-base-color link-no-underline" + href="#" + id="visibility-public" + onClick={[Function]} + > + <i + className="icon-radio" + /> + <span + className="spacer-left" + > + visibility.public + </span> + </a> + <a + className="link-base-color link-no-underline huge-spacer-left" + href="#" + id="visibility-private" + onClick={[Function]} + > + <i + className="icon-radio is-checked" + /> + <span + className="spacer-left" + > + visibility.private + </span> + </a> +</div> +`; + +exports[`renders disabled 1`] = ` +<div> + <a + className="link-base-color link-no-underline" + href="#" + id="visibility-public" + onClick={[Function]} + > + <i + className="icon-radio is-checked" + /> + <span + className="spacer-left" + > + visibility.public + </span> + </a> + <span + className="huge-spacer-left text-muted cursor-not-allowed" + id="visibility-private" + > + <i + className="icon-radio" + /> + <span + className="spacer-left" + > + visibility.private + </span> + </span> +</div> +`; diff --git a/server/sonar-web/src/main/js/components/controls/RadioToggle.js b/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx index 2041eba1422..fd27b3752dc 100644 --- a/server/sonar-web/src/main/js/components/controls/RadioToggle.js +++ b/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx @@ -17,38 +17,27 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; -export default class RadioToggle extends React.PureComponent { - static propTypes = { - value: PropTypes.string, - options: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string.isRequired, - label: PropTypes.string.isRequired - }) - ).isRequired, - name: PropTypes.string.isRequired, - onCheck: PropTypes.func.isRequired - }; +interface Props { + name: string; + onCheck: (value: string) => void; + options: Array<{ label: string; value: string }>; + value?: string; +} +export default class RadioToggle extends React.PureComponent<Props> { static defaultProps = { disabled: false, value: null }; - componentWillMount() { - this.renderOption = this.renderOption.bind(this); - this.handleChange = this.handleChange.bind(this); - } - - handleChange(e) { + handleChange = (e: React.SyntheticEvent<HTMLInputElement>) => { const newValue = e.currentTarget.value; this.props.onCheck(newValue); - } + }; - renderOption(option) { + renderOption = (option: { label: string; value: string }) => { const checked = option.value === this.props.value; const htmlId = this.props.name + '__' + option.value; return ( @@ -67,7 +56,7 @@ export default class RadioToggle extends React.PureComponent { </label> </li> ); - } + }; render() { return ( diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.js b/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx index b23e070c41c..f1a231b564c 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.js +++ b/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx @@ -17,25 +17,23 @@ * 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'; import { shallow } from 'enzyme'; -import React from 'react'; import RadioToggle from '../RadioToggle'; import { change } from '../../../helpers/testUtils'; -function getSample(props) { - const options = [{ value: 'one', label: 'first' }, { value: 'two', label: 'second' }]; - return <RadioToggle options={options} name="sample" onCheck={() => true} {...props} />; -} - -it('should render', () => { - const radioToggle = shallow(getSample()); - expect(radioToggle.find('input[type="radio"]').length).toBe(2); - expect(radioToggle.find('label').length).toBe(2); +it('renders', () => { + expect(shallow(getSample())).toMatchSnapshot(); }); -it('should call onCheck', () => { +it('calls onCheck', () => { const onCheck = jest.fn(); - const radioToggle = shallow(getSample({ onCheck })); - change(radioToggle.find('input[value="two"]'), 'two'); + const wrapper = shallow(getSample({ onCheck })); + change(wrapper.find('input[value="two"]'), 'two'); expect(onCheck).toBeCalledWith('two'); }); + +function getSample(props?: any) { + const options = [{ value: 'one', label: 'first' }, { value: 'two', label: 'second' }]; + return <RadioToggle options={options} name="sample" onCheck={() => true} {...props} />; +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap new file mode 100644 index 00000000000..df9579888bd --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +<ul + className="radio-toggle" +> + <li> + <input + checked={false} + id="sample__one" + name="sample" + onChange={[Function]} + type="radio" + value="one" + /> + <label + htmlFor="sample__one" + > + first + </label> + </li> + <li> + <input + checked={false} + id="sample__two" + name="sample" + onChange={[Function]} + type="radio" + value="two" + /> + <label + htmlFor="sample__two" + > + second + </label> + </li> +</ul> +`; diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index 02e2ce1c37c..6675a6791b1 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -42,10 +42,11 @@ export function submit(element: ShallowWrapper): void { }); } -export function change(element: ShallowWrapper, value: string): void { +export function change(element: ShallowWrapper, value: string, event = {}): void { element.simulate('change', { target: { value }, - currentTarget: { value } + currentTarget: { value }, + ...event }); } |