diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-08-22 13:33:15 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-08-22 20:21:22 +0200 |
commit | 4835b94eb4214cbd9be4b1e0c4c7209253c3f0fb (patch) | |
tree | 007eda8fdff522d37694f13e0e134a9fa69380c5 /server/sonar-web/src/main/js/apps/projectLinks | |
parent | af00018774baa18f3539560de991e747cba74848 (diff) | |
download | sonarqube-4835b94eb4214cbd9be4b1e0c4c7209253c3f0fb.tar.gz sonarqube-4835b94eb4214cbd9be4b1e0c4c7209253c3f0fb.zip |
drop project links from redux store (#632)
Diffstat (limited to 'server/sonar-web/src/main/js/apps/projectLinks')
17 files changed, 1202 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectLinks/App.tsx b/server/sonar-web/src/main/js/apps/projectLinks/App.tsx new file mode 100644 index 00000000000..5e9d9a2b94e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/App.tsx @@ -0,0 +1,104 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import Helmet from 'react-helmet'; +import Header from './Header'; +import Table from './Table'; +import { getProjectLinks, createLink, deleteLink } from '../../api/projectLinks'; +import { ProjectLink, Component } from '../../app/types'; +import { translate } from '../../helpers/l10n'; +import DeferredSpinner from '../../components/common/DeferredSpinner'; + +interface Props { + component: Pick<Component, 'key'>; +} + +interface State { + links?: ProjectLink[]; + loading: boolean; +} + +export default class App extends React.PureComponent<Props, State> { + mounted = false; + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + this.fetchLinks(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.component.key !== this.props.component.key) { + this.fetchLinks(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchLinks = () => { + this.setState({ loading: true }); + getProjectLinks(this.props.component.key).then( + links => { + if (this.mounted) { + this.setState({ links, loading: false }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleCreateLink = (name: string, url: string) => { + return createLink({ name, projectKey: this.props.component.key, url }).then(link => { + if (this.mounted) { + this.setState(({ links = [] }) => ({ + links: [...links, link] + })); + } + }); + }; + + handleDeleteLink = (linkId: string) => { + return deleteLink(linkId).then(() => { + if (this.mounted) { + this.setState(({ links = [] }) => ({ + links: links.filter(link => link.id !== linkId) + })); + } + }); + }; + + render() { + return ( + <div className="page page-limited"> + <Helmet title={translate('project_links.page')} /> + <Header onCreate={this.handleCreateLink} /> + <DeferredSpinner loading={this.state.loading}> + {this.state.links && <Table links={this.state.links} onDelete={this.handleDeleteLink} />} + </DeferredSpinner> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectLinks/CreationModal.tsx b/server/sonar-web/src/main/js/apps/projectLinks/CreationModal.tsx new file mode 100644 index 00000000000..81a1361725a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/CreationModal.tsx @@ -0,0 +1,111 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DeferredSpinner from '../../components/common/DeferredSpinner'; +import SimpleModal from '../../components/controls/SimpleModal'; +import { SubmitButton, ResetButtonLink } from '../../components/ui/buttons'; +import { translate } from '../../helpers/l10n'; + +interface Props { + onClose: () => void; + onSubmit: (name: string, url: string) => Promise<void>; +} + +interface State { + name: string; + url: string; +} + +export default class CreationModal extends React.PureComponent<Props, State> { + state: State = { name: '', url: '' }; + + handleSubmit = () => { + return this.props.onSubmit(this.state.name, this.state.url).then(this.props.onClose); + }; + + handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { + this.setState({ name: event.currentTarget.value }); + }; + + handleUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => { + this.setState({ url: event.currentTarget.value }); + }; + + render() { + const header = translate('project_links.create_new_project_link'); + + return ( + <SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}> + {({ onCloseClick, onFormSubmit, submitting }) => ( + <form onSubmit={onFormSubmit}> + <header className="modal-head"> + <h2>{header}</h2> + </header> + + <div className="modal-body"> + <div className="modal-field"> + <label htmlFor="create-link-name"> + {translate('project_links.name')} + <em className="mandatory">*</em> + </label> + <input + autoFocus={true} + id="create-link-name" + maxLength={128} + name="name" + onChange={this.handleNameChange} + required={true} + type="text" + value={this.state.name} + /> + </div> + + <div className="modal-field"> + <label htmlFor="create-link-url"> + {translate('project_links.url')} + <em className="mandatory">*</em> + </label> + <input + id="create-link-url" + maxLength={128} + name="url" + onChange={this.handleUrlChange} + required={true} + type="text" + value={this.state.url} + /> + </div> + </div> + + <footer className="modal-foot"> + <DeferredSpinner className="spacer-right" loading={submitting} /> + <SubmitButton disabled={submitting} id="create-link-confirm"> + {translate('create')} + </SubmitButton> + <ResetButtonLink disabled={submitting} onClick={onCloseClick}> + {translate('cancel')} + </ResetButtonLink> + </footer> + </form> + )} + </SimpleModal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectLinks/Header.tsx b/server/sonar-web/src/main/js/apps/projectLinks/Header.tsx new file mode 100644 index 00000000000..9cc71970886 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/Header.tsx @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import CreationModal from './CreationModal'; +import { Button } from '../../components/ui/buttons'; +import { translate } from '../../helpers/l10n'; + +interface Props { + onCreate: (name: string, url: string) => Promise<void>; +} + +interface State { + creationModal: boolean; +} + +export default class Header extends React.PureComponent<Props, State> { + mounted = false; + state: State = { creationModal: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCreateClick = () => { + this.setState({ creationModal: true }); + }; + + handleCreationModalClose = () => { + if (this.mounted) { + this.setState({ creationModal: false }); + } + }; + + render() { + return ( + <> + <header className="page-header"> + <h1 className="page-title">{translate('project_links.page')}</h1> + <div className="page-actions"> + <Button id="create-project-link" onClick={this.handleCreateClick}> + {translate('create')} + </Button> + </div> + <div className="page-description">{translate('project_links.page.description')}</div> + </header> + {this.state.creationModal && ( + <CreationModal onClose={this.handleCreationModalClose} onSubmit={this.props.onCreate} /> + )} + </> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectLinks/LinkRow.tsx b/server/sonar-web/src/main/js/apps/projectLinks/LinkRow.tsx new file mode 100644 index 00000000000..7d2630cba14 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/LinkRow.tsx @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { isProvided, getLinkName } from './utils'; +import { ProjectLink } from '../../app/types'; +import ConfirmButton from '../../components/controls/ConfirmButton'; +import ProjectLinkIcon from '../../components/icons-components/ProjectLinkIcon'; +import { Button } from '../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../helpers/l10n'; + +interface Props { + link: ProjectLink; + onDelete: (linkId: string) => Promise<void>; +} + +export default class LinkRow extends React.PureComponent<Props> { + renderNameForProvided = (link: ProjectLink) => { + return ( + <div className="display-inline-block text-top"> + <div> + <span className="js-name">{getLinkName(link)}</span> + </div> + <div className="note little-spacer-top"> + <span className="js-type">{`sonar.links.${link.type}`}</span> + </div> + </div> + ); + }; + + renderName = (link: ProjectLink) => { + return ( + <div> + <ProjectLinkIcon className="little-spacer-right" type={link.type} /> + {isProvided(link) ? ( + this.renderNameForProvided(link) + ) : ( + <div className="display-inline-block text-top"> + <span className="js-name">{link.name}</span> + </div> + )} + </div> + ); + }; + + renderDeleteButton = (link: ProjectLink) => { + if (isProvided(link)) { + return null; + } + + return ( + <ConfirmButton + confirmButtonText={translate('delete')} + confirmData={link.id} + isDestructive={true} + modalBody={translateWithParameters( + 'project_links.are_you_sure_to_delete_x_link', + link.name! + )} + modalHeader={translate('project_links.delete_project_link')} + onConfirm={this.props.onDelete}> + {({ onClick }) => ( + <Button className="button-red js-delete-button" onClick={onClick}> + {translate('delete')} + </Button> + )} + </ConfirmButton> + ); + }; + + render() { + const { link } = this.props; + + return ( + <tr data-name={link.name}> + <td className="nowrap">{this.renderName(link)}</td> + <td className="nowrap js-url"> + <a href={link.url} rel="nofollow" target="_blank"> + {link.url} + </a> + </td> + <td className="thin nowrap">{this.renderDeleteButton(link)}</td> + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectLinks/Table.tsx b/server/sonar-web/src/main/js/apps/projectLinks/Table.tsx new file mode 100644 index 00000000000..41a21af6d92 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/Table.tsx @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import LinkRow from './LinkRow'; +import { orderLinks } from './utils'; +import { ProjectLink } from '../../app/types'; +import { translate } from '../../helpers/l10n'; + +interface Props { + links: ProjectLink[]; + onDelete: (linkId: string) => Promise<void>; +} + +export default class Table extends React.PureComponent<Props> { + renderHeader() { + // keep empty cell for actions + return ( + <thead> + <tr> + <th className="nowrap">{translate('project_links.name')}</th> + <th className="nowrap width-100">{translate('project_links.url')}</th> + <th className="thin"> </th> + </tr> + </thead> + ); + } + + render() { + if (!this.props.links.length) { + return <div className="note">{translate('no_results')}</div>; + } + + const orderedLinks = orderLinks(this.props.links); + + const linkRows = orderedLinks.map(link => ( + <LinkRow key={link.id} link={link} onDelete={this.props.onDelete} /> + )); + + return ( + <div className="boxed-group boxed-group-inner"> + <table className="data zebra" id="project-links"> + {this.renderHeader()} + <tbody>{linkRows}</tbody> + </table> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/App-test.tsx new file mode 100644 index 00000000000..c6fcd752a5f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/App-test.tsx @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import App from '../App'; +import { waitAndUpdate } from '../../../helpers/testUtils'; +import { createLink, deleteLink, getProjectLinks } from '../../../api/projectLinks'; + +// import { getProjectLinks, createLink, deleteLink } from '../../api/projectLinks'; +jest.mock('../../../api/projectLinks', () => ({ + getProjectLinks: jest + .fn() + .mockResolvedValue([ + { id: '1', type: 'homepage', url: 'http://example.com' }, + { id: '2', name: 'foo', type: 'foo', url: 'http://example.com/foo' } + ]), + createLink: jest + .fn() + .mockResolvedValue({ id: '3', name: 'bar', type: 'bar', url: 'http://example.com/bar' }), + deleteLink: jest.fn().mockResolvedValue(undefined) +})); + +it('should fetch links and render', async () => { + const wrapper = shallow(<App component={{ key: 'comp' }} />); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + expect(getProjectLinks).toBeCalledWith('comp'); +}); + +it('should fetch links when component changes', async () => { + const wrapper = shallow(<App component={{ key: 'comp' }} />); + await waitAndUpdate(wrapper); + expect(getProjectLinks).lastCalledWith('comp'); + + wrapper.setProps({ component: { key: 'another' } }); + expect(getProjectLinks).lastCalledWith('another'); +}); + +it('should create link', async () => { + const wrapper = shallow(<App component={{ key: 'comp' }} />); + await waitAndUpdate(wrapper); + + wrapper.find('Header').prop<Function>('onCreate')('bar', 'http://example.com/bar'); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + expect(createLink).toBeCalledWith({ + name: 'bar', + projectKey: 'comp', + url: 'http://example.com/bar' + }); +}); + +it('should delete link', async () => { + const wrapper = shallow(<App component={{ key: 'comp' }} />); + await waitAndUpdate(wrapper); + + wrapper.find('Table').prop<Function>('onDelete')('foo'); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + expect(deleteLink).toBeCalledWith('foo'); +}); diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/CreationModal-test.tsx b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/CreationModal-test.tsx new file mode 100644 index 00000000000..99160f81695 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/CreationModal-test.tsx @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import CreationModal from '../CreationModal'; +import { change, submit } from '../../../helpers/testUtils'; + +it('should create link', () => { + const onClose = jest.fn(); + const onSubmit = jest.fn().mockResolvedValue(undefined); + const wrapper = shallow(<CreationModal onClose={onClose} onSubmit={onSubmit} />); + const form = wrapper.dive(); + + change(form.find('#create-link-name'), 'foo'); + change(form.find('#create-link-url'), 'http://example.com/foo'); + expect(form).toMatchSnapshot(); + + submit(wrapper); + expect(onSubmit).toBeCalledWith('foo', 'http://example.com/foo'); +}); diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/Header-test.tsx new file mode 100644 index 00000000000..7b798e15084 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/Header-test.tsx @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Header from '../Header'; +import { click } from '../../../helpers/testUtils'; + +it('should render', () => { + expect(shallow(<Header onCreate={jest.fn()} />)).toMatchSnapshot(); +}); + +it('should open creation modal', () => { + const onCreate = jest.fn(); + const wrapper = shallow(<Header onCreate={onCreate} />); + click(wrapper.find('Button')); + expect(wrapper.find('CreationModal').exists()).toBe(true); + + wrapper.find('CreationModal').prop<Function>('onSubmit')('foo', 'http://example.com/foo'); + expect(onCreate).toBeCalledWith('foo', 'http://example.com/foo'); + + wrapper.find('CreationModal').prop<Function>('onClose')(); + wrapper.update(); + expect(wrapper.find('CreationModal').exists()).toBe(false); +}); diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/LinkRow-test.tsx b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/LinkRow-test.tsx new file mode 100644 index 00000000000..19cff23dd62 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/LinkRow-test.tsx @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import LinkRow from '../LinkRow'; + +it('should render provided link', () => { + expect( + shallow( + <LinkRow + link={{ id: '12', type: 'homepage', url: 'http://example.com' }} + onDelete={jest.fn()} + /> + ) + ).toMatchSnapshot(); +}); + +it('should render custom link', () => { + expect( + shallow( + <LinkRow + link={{ id: '12', name: 'foo', type: 'foo', url: 'http://example.com' }} + onDelete={jest.fn()} + /> + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/Table-test.tsx b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/Table-test.tsx new file mode 100644 index 00000000000..aeddcd78524 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/Table-test.tsx @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Table from '../Table'; + +it('should render', () => { + const links = [ + { id: '1', type: 'homepage', url: 'http://example.com/homepage' }, + { id: '2', type: 'issue', url: 'http://example.com/issue' }, + { id: '3', name: 'foo', type: 'foo', url: 'http://example.com/foo' }, + { id: '4', name: 'bar', type: 'bar', url: 'http://example.com/bar' } + ]; + expect(shallow(<Table links={links} onDelete={jest.fn()} />)).toMatchSnapshot(); +}); + +it('should render empty', () => { + expect(shallow(<Table links={[]} onDelete={jest.fn()} />)).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/App-test.tsx.snap new file mode 100644 index 00000000000..b817f3fbcfd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/App-test.tsx.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should create link 1`] = ` +<div + className="page page-limited" +> + <HelmetWrapper + defer={true} + encodeSpecialCharacters={true} + title="project_links.page" + /> + <Header + onCreate={[Function]} + /> + <DeferredSpinner + loading={false} + timeout={100} + > + <Table + links={ + Array [ + Object { + "id": "1", + "type": "homepage", + "url": "http://example.com", + }, + Object { + "id": "2", + "name": "foo", + "type": "foo", + "url": "http://example.com/foo", + }, + Object { + "id": "3", + "name": "bar", + "type": "bar", + "url": "http://example.com/bar", + }, + ] + } + onDelete={[Function]} + /> + </DeferredSpinner> +</div> +`; + +exports[`should delete link 1`] = ` +<div + className="page page-limited" +> + <HelmetWrapper + defer={true} + encodeSpecialCharacters={true} + title="project_links.page" + /> + <Header + onCreate={[Function]} + /> + <DeferredSpinner + loading={false} + timeout={100} + > + <Table + links={ + Array [ + Object { + "id": "1", + "type": "homepage", + "url": "http://example.com", + }, + Object { + "id": "2", + "name": "foo", + "type": "foo", + "url": "http://example.com/foo", + }, + ] + } + onDelete={[Function]} + /> + </DeferredSpinner> +</div> +`; + +exports[`should fetch links and render 1`] = ` +<div + className="page page-limited" +> + <HelmetWrapper + defer={true} + encodeSpecialCharacters={true} + title="project_links.page" + /> + <Header + onCreate={[Function]} + /> + <DeferredSpinner + loading={false} + timeout={100} + > + <Table + links={ + Array [ + Object { + "id": "1", + "type": "homepage", + "url": "http://example.com", + }, + Object { + "id": "2", + "name": "foo", + "type": "foo", + "url": "http://example.com/foo", + }, + ] + } + onDelete={[Function]} + /> + </DeferredSpinner> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/CreationModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/CreationModal-test.tsx.snap new file mode 100644 index 00000000000..b56b44cef77 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/CreationModal-test.tsx.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should create link 1`] = ` +<Modal + contentLabel="project_links.create_new_project_link" + onRequestClose={[MockFunction]} +> + <form + onSubmit={[Function]} + > + <header + className="modal-head" + > + <h2> + project_links.create_new_project_link + </h2> + </header> + <div + className="modal-body" + > + <div + className="modal-field" + > + <label + htmlFor="create-link-name" + > + project_links.name + <em + className="mandatory" + > + * + </em> + </label> + <input + autoFocus={true} + id="create-link-name" + maxLength={128} + name="name" + onChange={[Function]} + required={true} + type="text" + value="" + /> + </div> + <div + className="modal-field" + > + <label + htmlFor="create-link-url" + > + project_links.url + <em + className="mandatory" + > + * + </em> + </label> + <input + id="create-link-url" + maxLength={128} + name="url" + onChange={[Function]} + required={true} + type="text" + value="" + /> + </div> + </div> + <footer + className="modal-foot" + > + <DeferredSpinner + className="spacer-right" + loading={false} + timeout={100} + /> + <SubmitButton + disabled={false} + id="create-link-confirm" + > + create + </SubmitButton> + <ResetButtonLink + disabled={false} + onClick={[Function]} + > + cancel + </ResetButtonLink> + </footer> + </form> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/Header-test.tsx.snap new file mode 100644 index 00000000000..073d2a845a1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/Header-test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<React.Fragment> + <header + className="page-header" + > + <h1 + className="page-title" + > + project_links.page + </h1> + <div + className="page-actions" + > + <Button + id="create-project-link" + onClick={[Function]} + > + create + </Button> + </div> + <div + className="page-description" + > + project_links.page.description + </div> + </header> +</React.Fragment> +`; diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/LinkRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/LinkRow-test.tsx.snap new file mode 100644 index 00000000000..cea1ad2384f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/LinkRow-test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render custom link 1`] = ` +<tr + data-name="foo" +> + <td + className="nowrap" + > + <div> + <ProjectLinkIcon + className="little-spacer-right" + type="foo" + /> + <div + className="display-inline-block text-top" + > + <span + className="js-name" + > + foo + </span> + </div> + </div> + </td> + <td + className="nowrap js-url" + > + <a + href="http://example.com" + rel="nofollow" + target="_blank" + > + http://example.com + </a> + </td> + <td + className="thin nowrap" + > + <ConfirmButton + confirmButtonText="delete" + confirmData="12" + isDestructive={true} + modalBody="project_links.are_you_sure_to_delete_x_link.foo" + modalHeader="project_links.delete_project_link" + onConfirm={[MockFunction]} + /> + </td> +</tr> +`; + +exports[`should render provided link 1`] = ` +<tr> + <td + className="nowrap" + > + <div> + <ProjectLinkIcon + className="little-spacer-right" + type="homepage" + /> + <div + className="display-inline-block text-top" + > + <div> + <span + className="js-name" + > + project_links.homepage + </span> + </div> + <div + className="note little-spacer-top" + > + <span + className="js-type" + > + sonar.links.homepage + </span> + </div> + </div> + </div> + </td> + <td + className="nowrap js-url" + > + <a + href="http://example.com" + rel="nofollow" + target="_blank" + > + http://example.com + </a> + </td> + <td + className="thin nowrap" + /> +</tr> +`; diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/Table-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/Table-test.tsx.snap new file mode 100644 index 00000000000..c043f6a5f56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/Table-test.tsx.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<div + className="boxed-group boxed-group-inner" +> + <table + className="data zebra" + id="project-links" + > + <thead> + <tr> + <th + className="nowrap" + > + project_links.name + </th> + <th + className="nowrap width-100" + > + project_links.url + </th> + <th + className="thin" + > + + </th> + </tr> + </thead> + <tbody> + <LinkRow + key="1" + link={ + Object { + "id": "1", + "type": "homepage", + "url": "http://example.com/homepage", + } + } + onDelete={[MockFunction]} + /> + <LinkRow + key="2" + link={ + Object { + "id": "2", + "type": "issue", + "url": "http://example.com/issue", + } + } + onDelete={[MockFunction]} + /> + <LinkRow + key="4" + link={ + Object { + "id": "4", + "name": "bar", + "type": "bar", + "url": "http://example.com/bar", + } + } + onDelete={[MockFunction]} + /> + <LinkRow + key="3" + link={ + Object { + "id": "3", + "name": "foo", + "type": "foo", + "url": "http://example.com/foo", + } + } + onDelete={[MockFunction]} + /> + </tbody> + </table> +</div> +`; + +exports[`should render empty 1`] = ` +<div + className="note" +> + no_results +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/projectLinks/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/utils-test.ts new file mode 100644 index 00000000000..882d0064c16 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/__tests__/utils-test.ts @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as utils from '../utils'; + +it('#isProvided', () => { + expect(utils.isProvided({ type: 'homepage' })).toBe(true); + expect(utils.isProvided({ type: 'custom' })).toBe(false); +}); + +it('#orderLinks', () => { + const homepage = { type: 'homepage' }; + const issues = { type: 'issue' }; + const foo = { name: 'foo', type: 'foo' }; + const bar = { name: 'bar', type: 'bar' }; + expect(utils.orderLinks([foo, homepage, issues, bar])).toEqual([homepage, issues, bar, foo]); + expect(utils.orderLinks([foo, bar])).toEqual([bar, foo]); + expect(utils.orderLinks([issues, homepage])).toEqual([homepage, issues]); +}); + +it('#getLinkName', () => { + expect(utils.getLinkName({ type: 'homepage' })).toBe('project_links.homepage'); + expect(utils.getLinkName({ name: 'foo', type: 'custom' })).toBe('foo'); +}); diff --git a/server/sonar-web/src/main/js/apps/projectLinks/utils.ts b/server/sonar-web/src/main/js/apps/projectLinks/utils.ts new file mode 100644 index 00000000000..2bb1bd70c25 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectLinks/utils.ts @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { partition, sortBy } from 'lodash'; +import { ProjectLink } from '../../app/types'; +import { translate } from '../../helpers/l10n'; + +const PROVIDED_TYPES = ['homepage', 'ci', 'issue', 'scm', 'scm_dev']; +type NameAndType = Pick<ProjectLink, 'name' | 'type'>; + +export function isProvided(link: Pick<ProjectLink, 'type'>) { + return PROVIDED_TYPES.includes(link.type); +} + +export function orderLinks<T extends NameAndType>(links: T[]) { + const [provided, unknown] = partition<T>(links, isProvided); + return [ + ...sortBy(provided, link => PROVIDED_TYPES.indexOf(link.type)), + ...sortBy(unknown, link => link.name!.toLowerCase()) + ]; +} + +export function getLinkName(link: NameAndType) { + return isProvided(link) ? translate('project_links', link.type) : link.name; +} |