diff options
Diffstat (limited to 'server')
35 files changed, 1363 insertions, 26 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 56658e361e8..ac465a81a0b 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -18,6 +18,7 @@ "d3-shape": "1.2.0", "date-fns": "1.29.0", "escape-html": "1.0.3", + "formik": "0.11.7", "handlebars": "2.0.0", "history": "3.3.0", "intl-relativeformat": "2.1.0", diff --git a/server/sonar-web/src/main/js/api/webhooks.ts b/server/sonar-web/src/main/js/api/webhooks.ts index 547702464b1..f23cb2f079b 100644 --- a/server/sonar-web/src/main/js/api/webhooks.ts +++ b/server/sonar-web/src/main/js/api/webhooks.ts @@ -17,13 +17,34 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getJSON } from '../helpers/request'; +import { getJSON, post, postJSON } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; import { Webhook } from '../app/types'; +export function createWebhook(data: { + name: string; + organization: string | undefined; + project?: string; + url: string; +}): Promise<{ webhook: Webhook }> { + return postJSON('/api/webhooks/create', data).catch(throwGlobalError); +} + +export function deleteWebhook(data: { webhook: string }): Promise<void | Response> { + return post('/api/webhooks/delete', data).catch(throwGlobalError); +} + export function searchWebhooks(data: { organization: string | undefined; project?: string; }): Promise<{ webhooks: Webhook[] }> { - return getJSON('/api/webhooks/search', data).catch(throwGlobalError); + return getJSON('/api/webhooks/list', data).catch(throwGlobalError); +} + +export function updateWebhook(data: { + webhook: string; + name: string; + url: string; +}): Promise<void | Response> { + return post('/api/webhooks/update', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/styles/components/modals.css b/server/sonar-web/src/main/js/app/styles/components/modals.css index 3bf2f396559..dbe8a64cb98 100644 --- a/server/sonar-web/src/main/js/app/styles/components/modals.css +++ b/server/sonar-web/src/main/js/app/styles/components/modals.css @@ -121,19 +121,24 @@ ul.modal-head-metadata li { height: auto; } -.modal-field { +.modal-field, +.modal-large-field, +.modal-validation-field { clear: both; display: block; padding: 5px 0 5px 130px; } .modal-large-field { - clear: both; - display: block; padding: 20px 40px; } -.modal-field label { +.modal-validation-field { + padding: 3px 0 3px 130px; +} + +.modal-field label, +.modal-validation-field label { position: relative; left: -140px; display: block; @@ -149,7 +154,8 @@ ul.modal-head-metadata li { text-overflow: ellipsis; } -.modal-field label.simple-label { +.modal-field label.simple-label, +.modal-validation-field label.simple-label { display: inline-block; vertical-align: middle; float: none; @@ -215,6 +221,29 @@ ul.modal-head-metadata li { width: 100%; } +.modal-validation-field input, +.modal-validation-field textarea, +.modal-validation-field .Select { + margin-right: 5px; + margin-bottom: 2px; + width: 250px; +} + +.modal-validation-field input:not(.has-error), +.modal-validation-field .Select:not(.has-error) { + margin-bottom: 18px; +} + +.modal-validation-field .has-error, +.modal-validation-field .has-error > .Select-control { + border-color: var(--red); +} + +.modal-validation-field .is-valid, +.modal-validation-field .is-valid > .Select-control { + border-color: var(--green); +} + .modal-field-description { padding-bottom: 4px; line-height: 1.4; @@ -224,6 +253,10 @@ ul.modal-head-metadata li { text-overflow: ellipsis; } +.modal-validation-field .modal-field-description { + margin-top: 2px; +} + .modal-foot { line-height: var(--controlHeight); padding: 10px; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx index e1d559ef307..d48a3690400 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx @@ -19,9 +19,10 @@ */ import * as React from 'react'; import { Helmet } from 'react-helmet'; +import PageActions from './PageActions'; import PageHeader from './PageHeader'; import WebhooksList from './WebhooksList'; -import { searchWebhooks } from '../../../api/webhooks'; +import { createWebhook, deleteWebhook, searchWebhooks, updateWebhook } from '../../../api/webhooks'; import { LightComponent, Organization, Webhook } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; @@ -48,12 +49,8 @@ export default class App extends React.PureComponent<Props, State> { this.mounted = false; } - fetchWebhooks = ({ organization, component } = this.props) => { - this.setState({ loading: true }); - searchWebhooks({ - organization: organization && organization.key, - project: component && component.key - }).then( + fetchWebhooks = () => { + return searchWebhooks(this.getScopeParams()).then( ({ webhooks }) => { if (this.mounted) { this.setState({ loading: false, webhooks }); @@ -67,15 +64,65 @@ export default class App extends React.PureComponent<Props, State> { ); }; + getScopeParams = ({ organization, component } = this.props) => { + return { organization: organization && organization.key, project: component && component.key }; + }; + + handleCreate = (data: { name: string; url: string }) => { + return createWebhook({ + ...data, + ...this.getScopeParams() + }).then(({ webhook }) => { + if (this.mounted) { + this.setState(({ webhooks }) => ({ webhooks: [...webhooks, webhook] })); + } + }); + }; + + handleDelete = (webhook: string) => { + return deleteWebhook({ webhook }).then(() => { + if (this.mounted) { + this.setState(({ webhooks }) => ({ + webhooks: webhooks.filter(item => item.key !== webhook) + })); + } + }); + }; + + handleUpdate = (data: { webhook: string; name: string; url: string }) => { + return updateWebhook(data).then(() => { + if (this.mounted) { + this.setState(({ webhooks }) => ({ + webhooks: webhooks.map( + webhook => (webhook.key === data.webhook ? { ...webhook, ...data } : webhook) + ) + })); + } + }); + }; + render() { const { loading, webhooks } = this.state; + return ( <div className="page page-limited"> <Helmet title={translate('webhooks.page')} /> - <PageHeader loading={loading} /> + + <PageHeader loading={loading}> + <PageActions + loading={loading} + onCreate={this.handleCreate} + webhooksCount={webhooks.length} + /> + </PageHeader> + {!loading && ( <div className="boxed-group boxed-group-inner"> - <WebhooksList webhooks={webhooks} /> + <WebhooksList + onDelete={this.handleDelete} + onUpdate={this.handleUpdate} + webhooks={webhooks} + /> </div> )} </div> diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx new file mode 100644 index 00000000000..91a12c2ba7c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx @@ -0,0 +1,131 @@ +/* + * 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 { FormikProps } from 'formik'; +import ValidationModal from '../../../components/controls/ValidationModal'; +import InputValidationField from '../../../components/controls/InputValidationField'; +import { Webhook } from '../../../app/types'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + onClose: () => void; + onDone: (data: { name: string; url: string }) => Promise<void>; + webhook?: Webhook; +} + +interface Values { + name: string; + url: string; +} + +export default class CreateWebhookForm extends React.PureComponent<Props> { + handleCancelClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onClose(); + }; + + handleValidate = (data: Values) => { + const { name, url } = data; + const errors: { name?: string; url?: string } = {}; + if (!name.trim()) { + errors.name = translate('webhooks.name.required'); + } + if (!url.trim()) { + errors.url = translate('webhooks.url.required'); + } else if (!url.startsWith('http://') && !url.startsWith('https://')) { + errors.url = translate('webhooks.url.bad_protocol'); + } else if (url.indexOf(':', 6) > 0 && url.indexOf('@') <= 0) { + errors.url = translate('webhooks.url.bad_auth'); + } + return errors; + }; + + render() { + const { webhook } = this.props; + const isUpdate = !!webhook; + const modalHeader = isUpdate ? translate('webhooks.update') : translate('webhooks.create'); + const confirmButtonText = isUpdate ? translate('update_verb') : translate('create'); + return ( + <ValidationModal + confirmButtonText={confirmButtonText} + header={modalHeader} + initialValues={{ + name: webhook ? webhook.name : '', + url: webhook ? webhook.url : '' + }} + isInitialValid={isUpdate} + onClose={this.props.onClose} + onSubmit={this.props.onDone} + validate={this.handleValidate}> + {({ + dirty, + errors, + handleBlur, + handleChange, + isSubmitting, + touched, + values + }: FormikProps<Values>) => ( + <> + <InputValidationField + autoFocus={true} + dirty={dirty} + disabled={isSubmitting} + error={errors.name} + id="webhook-name" + label={ + <label htmlFor="webhook-name"> + {translate('webhooks.name')} + <em className="mandatory">*</em> + </label> + } + name="name" + onBlur={handleBlur} + onChange={handleChange} + touched={touched.name} + type="text" + value={values.name} + /> + <InputValidationField + description={translate('webhooks.url.description')} + disabled={isSubmitting} + dirty={dirty} + error={errors.url} + id="webhook-url" + label={ + <label htmlFor="webhook-url"> + {translate('webhooks.url')} + <em className="mandatory">*</em> + </label> + } + name="url" + onBlur={handleBlur} + onChange={handleChange} + touched={touched.url} + type="text" + value={values.url} + /> + </> + )} + </ValidationModal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx new file mode 100644 index 00000000000..92df63c688b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx @@ -0,0 +1,89 @@ +/* + * 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 CreateWebhookForm from './CreateWebhookForm'; +import Tooltip from '../../../components/controls/Tooltip'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + loading: boolean; + onCreate: (data: { name: string; url: string }) => Promise<void>; + webhooksCount: number; +} + +interface State { + openCreate: boolean; +} + +const WEBHOOKS_LIMIT = 10; + +export default class PageActions extends React.PureComponent<Props, State> { + mounted: boolean = false; + state: State = { openCreate: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCreateClose = () => { + if (this.mounted) { + this.setState({ openCreate: false }); + } + }; + + handleCreateOpen = () => { + this.setState({ openCreate: true }); + }; + + renderCreate = () => { + if (this.props.webhooksCount >= WEBHOOKS_LIMIT) { + return ( + <Tooltip + overlay={translateWithParameters('webhooks.maximum_reached', WEBHOOKS_LIMIT)} + placement="left"> + <button className="js-webhook-create disabled">{translate('create')}</button> + </Tooltip> + ); + } + + return ( + <> + <button className="js-webhook-create" onClick={this.handleCreateOpen}> + {translate('create')} + </button> + {this.state.openCreate && ( + <CreateWebhookForm onClose={this.handleCreateClose} onDone={this.props.onCreate} /> + )} + </> + ); + }; + + render() { + if (this.props.loading) { + return null; + } + + return <div className="page-actions">{this.renderCreate()}</div>; + } +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx index 229b835e5e9..2a23425df06 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx @@ -22,15 +22,18 @@ import { FormattedMessage } from 'react-intl'; import { translate } from '../../../helpers/l10n'; interface Props { + children?: React.ReactNode; loading: boolean; } -export default function PageHeader({ loading }: Props) { +export default function PageHeader({ children, loading }: Props) { return ( <header className="page-header"> <h1 className="page-title">{translate('webhooks.page')}</h1> {loading && <i className="spinner" />} + {children} + <p className="page-description"> <FormattedMessage defaultMessage={translate('webhooks.description')} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx new file mode 100644 index 00000000000..db7c401f832 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx @@ -0,0 +1,105 @@ +/* + * 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 CreateWebhookForm from './CreateWebhookForm'; +import ActionsDropdown, { + ActionsDropdownItem, + ActionsDropdownDivider +} from '../../../components/controls/ActionsDropdown'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { Webhook } from '../../../app/types'; + +interface Props { + onDelete: (webhook: string) => Promise<void>; + onUpdate: (data: { webhook: string; name: string; url: string }) => Promise<void>; + webhook: Webhook; +} + +interface State { + updating: boolean; +} + +export default class WebhookActions extends React.PureComponent<Props, State> { + mounted: boolean = false; + state: State = { updating: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleDelete = () => { + return this.props.onDelete(this.props.webhook.key); + }; + + handleUpdate = (data: { name: string; url: string }) => { + return this.props.onUpdate({ ...data, webhook: this.props.webhook.key }); + }; + + handleUpdateClick = () => { + this.setState({ updating: true }); + }; + + handleUpdatingStop = () => { + this.setState({ updating: false }); + }; + + render() { + const { webhook } = this.props; + + return ( + <> + <ActionsDropdown className="big-spacer-left"> + <ActionsDropdownItem className="js-webhook-update" onClick={this.handleUpdateClick}> + {translate('update_verb')} + </ActionsDropdownItem> + <ActionsDropdownDivider /> + <ConfirmButton + confirmButtonText={translate('delete')} + isDestructive={true} + modalBody={translateWithParameters('webhooks.delete.confirm', webhook.name)} + modalHeader={translate('webhooks.delete')} + onConfirm={this.handleDelete}> + {({ onClick }) => ( + <ActionsDropdownItem + className="js-webhook-delete" + destructive={true} + onClick={onClick}> + {translate('delete')} + </ActionsDropdownItem> + )} + </ConfirmButton> + </ActionsDropdown> + + {this.state.updating && ( + <CreateWebhookForm + onClose={this.handleUpdatingStop} + onDone={this.handleUpdate} + webhook={webhook} + /> + )} + </> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx index f156900d581..b51ceb085af 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx @@ -18,17 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import WebhookActions from './WebhookActions'; import { Webhook } from '../../../app/types'; interface Props { + onDelete: (webhook: string) => Promise<void>; + onUpdate: (data: { webhook: string; name: string; url: string }) => Promise<void>; webhook: Webhook; } -export default function WebhookItem({ webhook }: Props) { +export default function WebhookItem({ onDelete, onUpdate, webhook }: Props) { return ( <tr> <td>{webhook.name}</td> <td>{webhook.url}</td> + <td className="thin nowrap text-right"> + <WebhookActions onDelete={onDelete} onUpdate={onUpdate} webhook={webhook} /> + </td> </tr> ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx index 600fad4e8d3..6bfaea6527b 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx @@ -23,6 +23,8 @@ import { Webhook } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; interface Props { + onDelete: (webhook: string) => Promise<void>; + onUpdate: (data: { webhook: string; name: string; url: string }) => Promise<void>; webhooks: Webhook[]; } @@ -32,6 +34,7 @@ export default class WebhooksList extends React.PureComponent<Props> { <tr> <th>{translate('name')}</th> <th>{translate('webhooks.url')}</th> + <th /> </tr> </thead> ); @@ -45,7 +48,14 @@ export default class WebhooksList extends React.PureComponent<Props> { <table className="data zebra"> {this.renderHeader()} <tbody> - {webhooks.map(webhook => <WebhookItem key={webhook.key} webhook={webhook} />)} + {webhooks.map(webhook => ( + <WebhookItem + key={webhook.key} + onDelete={this.props.onDelete} + onUpdate={this.props.onUpdate} + webhook={webhook} + /> + ))} </tbody> </table> ); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx new file mode 100644 index 00000000000..a5e20a63282 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx @@ -0,0 +1,38 @@ +/* + * 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 CreateWebhookForm from '../CreateWebhookForm'; + +const webhook = { key: '1', name: 'foo', url: 'http://foo.bar' }; + +it('should render correctly when creating a new webhook', () => { + expect(getWrapper()).toMatchSnapshot(); +}); + +it('should render correctly when updating a webhook', () => { + expect(getWrapper({ webhook })).toMatchSnapshot(); +}); + +function getWrapper(props = {}) { + return shallow( + <CreateWebhookForm onClose={jest.fn()} onDone={jest.fn(() => Promise.resolve())} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageActions-test.tsx new file mode 100644 index 00000000000..f50a9b062a4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageActions-test.tsx @@ -0,0 +1,51 @@ +/* + * 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 PageActions from '../PageActions'; +import { click } from '../../../../helpers/testUtils'; + +it('should render correctly', () => { + expect(getWrapper()).toMatchSnapshot(); +}); + +it('should not render', () => { + expect(getWrapper({ loading: true }).type()).toBeNull(); +}); + +it('should not allow to create a new webhook', () => { + expect(getWrapper({ webhooksCount: 10 })).toMatchSnapshot(); +}); + +it('should display the create form', () => { + const onCreate = jest.fn(); + const wrapper = getWrapper({ onCreate }); + click(wrapper.find('.js-webhook-create')); + expect(wrapper.find('CreateWebhookForm').exists()).toBeTruthy(); + wrapper.find('CreateWebhookForm').prop<Function>('onDone')({ + name: 'foo', + url: 'http://foo.bar' + }); + expect(onCreate).lastCalledWith({ name: 'foo', url: 'http://foo.bar' }); +}); + +function getWrapper(props = {}) { + return shallow(<PageActions onCreate={jest.fn()} loading={false} webhooksCount={5} {...props} />); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx index 148d1570cd4..055228c98a2 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx @@ -22,5 +22,11 @@ import { shallow } from 'enzyme'; import PageHeader from '../PageHeader'; it('should render correctly', () => { - expect(shallow(<PageHeader loading={true} />)).toMatchSnapshot(); + expect( + shallow( + <PageHeader loading={true}> + <div /> + </PageHeader> + ) + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx new file mode 100644 index 00000000000..51aa702e132 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx @@ -0,0 +1,66 @@ +/* + * 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 WebhookActions from '../WebhookActions'; +import { click } from '../../../../helpers/testUtils'; + +const webhook = { key: '1', name: 'foo', url: 'http://foo.bar' }; + +it('should render correctly', () => { + expect(getWrapper()).toMatchSnapshot(); +}); + +it('should display the update webhook form', () => { + const onUpdate = jest.fn(() => Promise.resolve()); + const wrapper = getWrapper({ onUpdate }); + click(wrapper.find('.js-webhook-update')); + expect(wrapper.find('CreateWebhookForm').exists()).toBeTruthy(); + wrapper.find('CreateWebhookForm').prop<Function>('onDone')({ + name: webhook.name, + url: webhook.url + }); + expect(onUpdate).lastCalledWith({ webhook: webhook.key, name: webhook.name, url: webhook.url }); +}); + +it('should display the delete webhook form', () => { + const onDelete = jest.fn(() => Promise.resolve()); + const wrapper = getWrapper({ onDelete }); + click( + wrapper + .find('ConfirmButton') + .dive() + .find('.js-webhook-delete') + ); + expect(wrapper.find('ConfirmButton').exists()).toBeTruthy(); + wrapper.find('ConfirmButton').prop<Function>('onConfirm')(); + expect(onDelete).lastCalledWith(webhook.key); +}); + +function getWrapper(props = {}) { + return shallow( + <WebhookActions + onDelete={jest.fn(() => Promise.resolve())} + onUpdate={jest.fn(() => Promise.resolve())} + webhook={webhook} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx index ab0a1a5d0e4..9322cf58e0a 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx @@ -24,5 +24,13 @@ import WebhookItem from '../WebhookItem'; const webhook = { key: '1', name: 'my webhook', url: 'http://webhook.target' }; it('should render correctly', () => { - expect(shallow(<WebhookItem webhook={webhook} />)).toMatchSnapshot(); + expect( + shallow( + <WebhookItem + onDelete={jest.fn(() => Promise.resolve())} + onUpdate={jest.fn(() => Promise.resolve())} + webhook={webhook} + /> + ) + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx index e0337cc8f5c..531eaeded8f 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx @@ -27,9 +27,20 @@ const webhooks = [ ]; it('should correctly render empty webhook list', () => { - expect(shallow(<WebhooksList webhooks={[]} />)).toMatchSnapshot(); + expect(getWrapper({ webhooks: [] })).toMatchSnapshot(); }); it('should correctly render the webhooks', () => { - expect(shallow(<WebhooksList webhooks={webhooks} />)).toMatchSnapshot(); + expect(getWrapper()).toMatchSnapshot(); }); + +function getWrapper(props = {}) { + return shallow( + <WebhooksList + onDelete={jest.fn(() => Promise.resolve())} + onUpdate={jest.fn(() => Promise.resolve())} + webhooks={webhooks} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap index 717e2e76bb0..3490f184a15 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap @@ -11,7 +11,13 @@ exports[`should be in loading status 1`] = ` /> <PageHeader loading={true} - /> + > + <PageActions + loading={true} + onCreate={[Function]} + webhooksCount={0} + /> + </PageHeader> </div> `; @@ -26,11 +32,19 @@ exports[`should fetch webhooks and display them 1`] = ` /> <PageHeader loading={false} - /> + > + <PageActions + loading={false} + onCreate={[Function]} + webhooksCount={0} + /> + </PageHeader> <div className="boxed-group boxed-group-inner" > <WebhooksList + onDelete={[Function]} + onUpdate={[Function]} webhooks={Array []} /> </div> diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap new file mode 100644 index 00000000000..8ad194ad454 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly when creating a new webhook 1`] = ` +<ValidationModal + confirmButtonText="create" + header="webhooks.create" + initialValues={ + Object { + "name": "", + "url": "", + } + } + isInitialValid={false} + onClose={[MockFunction]} + onSubmit={[MockFunction]} + validate={[Function]} +/> +`; + +exports[`should render correctly when updating a webhook 1`] = ` +<ValidationModal + confirmButtonText="update_verb" + header="webhooks.update" + initialValues={ + Object { + "name": "foo", + "url": "http://foo.bar", + } + } + isInitialValid={true} + onClose={[MockFunction]} + onSubmit={[MockFunction]} + validate={[Function]} +/> +`; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap new file mode 100644 index 00000000000..abbb9ca3fb9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not allow to create a new webhook 1`] = ` +<div + className="page-actions" +> + <Tooltip + overlay="webhooks.maximum_reached.10" + placement="left" + > + <button + className="js-webhook-create disabled" + > + create + </button> + </Tooltip> +</div> +`; + +exports[`should render correctly 1`] = ` +<div + className="page-actions" +> + <React.Fragment> + <button + className="js-webhook-create" + onClick={[Function]} + > + create + </button> + </React.Fragment> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap index e40da7ba2d3..e44b8019fca 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap @@ -12,6 +12,7 @@ exports[`should render correctly 1`] = ` <i className="spinner" /> + <div /> <p className="page-description" > diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap new file mode 100644 index 00000000000..a82a5f3ed35 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<React.Fragment> + <ActionsDropdown + className="big-spacer-left" + > + <ActionsDropdownItem + className="js-webhook-update" + onClick={[Function]} + > + update_verb + </ActionsDropdownItem> + <ActionsDropdownDivider /> + <ConfirmButton + confirmButtonText="delete" + isDestructive={true} + modalBody="webhooks.delete.confirm.foo" + modalHeader="webhooks.delete" + onConfirm={[Function]} + /> + </ActionsDropdown> +</React.Fragment> +`; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap index b6968c76bed..7f45d646e0a 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap @@ -8,5 +8,20 @@ exports[`should render correctly 1`] = ` <td> http://webhook.target </td> + <td + className="thin nowrap text-right" + > + <WebhookActions + onDelete={[MockFunction]} + onUpdate={[MockFunction]} + webhook={ + Object { + "key": "1", + "name": "my webhook", + "url": "http://webhook.target", + } + } + /> + </td> </tr> `; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap index 01bcb34685a..ddf82de6e99 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap @@ -18,11 +18,14 @@ exports[`should correctly render the webhooks 1`] = ` <th> webhooks.url </th> + <th /> </tr> </thead> <tbody> <WebhookItem key="1" + onDelete={[MockFunction]} + onUpdate={[MockFunction]} webhook={ Object { "key": "1", @@ -33,6 +36,8 @@ exports[`should correctly render the webhooks 1`] = ` /> <WebhookItem key="2" + onDelete={[MockFunction]} + onUpdate={[MockFunction]} webhook={ Object { "key": "2", diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx index ab766a9eed6..354e11add9c 100644 --- a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx +++ b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx @@ -110,7 +110,9 @@ export default class ConfirmButton extends React.PureComponent<Props, State> { disabled={submitting}> {confirmButtonText} </SubmitButton> - <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink> + <ResetButtonLink disabled={submitting} onClick={onCloseClick}> + {translate('cancel')} + </ResetButtonLink> </footer> </form> )} diff --git a/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx b/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx new file mode 100644 index 00000000000..aae2cde6025 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx @@ -0,0 +1,52 @@ +/* + * 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 * as classNames from 'classnames'; +import ModalValidationField from './ModalValidationField'; + +interface Props { + autoFocus?: boolean; + className?: string; + description?: string; + dirty: boolean; + disabled: boolean; + error: string | undefined; + id?: string; + label?: React.ReactNode; + name: string; + onBlur: (event: React.FocusEvent<any>) => void; + onChange: (event: React.ChangeEvent<any>) => void; + placeholder?: string; + touched: boolean; + type?: string; + value: string; +} + +export default function InputValidationField({ className, ...props }: Props) { + const { description, dirty, error, label, touched, ...inputProps } = props; + const modalValidationProps = { description, dirty, error, label, touched }; + return ( + <ModalValidationField {...modalValidationProps}> + {({ className: validationClassName }) => ( + <input className={classNames(className, validationClassName)} {...inputProps} /> + )} + </ModalValidationField> + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx b/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx new file mode 100644 index 00000000000..503b58952e3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx @@ -0,0 +1,49 @@ +/* + * 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 * as classNames from 'classnames'; +import AlertErrorIcon from '../icons-components/AlertErrorIcon'; +import AlertSuccessIcon from '../icons-components/AlertSuccessIcon'; + +interface Props { + children: (props: { className?: string }) => React.ReactNode; + description?: string; + dirty: boolean; + error: string | undefined; + label?: React.ReactNode; + touched: boolean; +} + +export default function ModalValidationField(props: Props) { + const { description, dirty, error } = props; + + const isValid = dirty && props.touched && error === undefined; + const showError = dirty && props.touched && error !== undefined; + return ( + <div className="modal-validation-field"> + {props.label} + {props.children({ className: classNames({ 'has-error': showError, 'is-valid': isValid }) })} + {showError && <AlertErrorIcon className="little-spacer-top" />} + {isValid && <AlertSuccessIcon className="little-spacer-top" />} + {showError && <p className="text-danger">{error}</p>} + {description && <div className="modal-field-description">{description}</div>} + </div> + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx b/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx new file mode 100644 index 00000000000..d52a0b31af8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx @@ -0,0 +1,108 @@ +/* + * 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 { withFormik, Form, FormikActions, FormikProps } from 'formik'; +import Modal from './Modal'; +import DeferredSpinner from '../common/DeferredSpinner'; +import { translate } from '../../helpers/l10n'; + +interface InnerFormProps<Values> { + children: (props: FormikProps<Values>) => React.ReactNode; + confirmButtonText: string; + header: string; + initialValues: Values; +} + +interface Props<Values> extends InnerFormProps<Values> { + isInitialValid?: boolean; + onClose: () => void; + validate: (data: Values) => void | object | Promise<object>; + onSubmit: (data: Values) => void | Promise<void>; +} + +export default class ValidationModal<Values> extends React.PureComponent<Props<Values>> { + handleCancelClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onClose(); + }; + + handleSubmit = (data: Values, { setSubmitting }: FormikActions<Values>) => { + const result = this.props.onSubmit(data); + if (result) { + result.then( + () => { + setSubmitting(false); + this.props.onClose(); + }, + () => { + setSubmitting(false); + } + ); + } else { + setSubmitting(false); + this.props.onClose(); + } + }; + + render() { + const { header } = this.props; + + const InnerForm = withFormik<InnerFormProps<Values>, Values>({ + handleSubmit: this.handleSubmit, + isInitialValid: this.props.isInitialValid, + mapPropsToValues: props => props.initialValues, + validate: this.props.validate + })(props => ( + <Form> + <div className="modal-head"> + <h2>{props.header}</h2> + </div> + + <div className="modal-body">{props.children(props)}</div> + + <footer className="modal-foot"> + <DeferredSpinner className="spacer-right" loading={props.isSubmitting} /> + <button disabled={props.isSubmitting || !props.isValid || !props.dirty} type="submit"> + {props.confirmButtonText} + </button> + <button + className="button-link" + disabled={props.isSubmitting} + onClick={this.handleCancelClick} + type="reset"> + {translate('cancel')} + </button> + </footer> + </Form> + )); + + return ( + <Modal contentLabel={header} onRequestClose={this.props.onClose}> + <InnerForm + confirmButtonText={this.props.confirmButtonText} + header={header} + initialValues={this.props.initialValues}> + {this.props.children} + </InnerForm> + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/InputValidationField-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/InputValidationField-test.tsx new file mode 100644 index 00000000000..e0864a1b8fc --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/InputValidationField-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 InputValidationField from '../InputValidationField'; + +it('should render correctly', () => { + expect(getWrapper()).toMatchSnapshot(); +}); + +function getWrapper(props = {}) { + return shallow( + <InputValidationField + description="Field description" + dirty={true} + disabled={false} + error="Bad formatting" + label="Foo field" + name="field" + onBlur={jest.fn()} + onChange={jest.fn()} + touched={true} + value="foo" + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ModalValidationField-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ModalValidationField-test.tsx new file mode 100644 index 00000000000..ae9a2f74118 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/ModalValidationField-test.tsx @@ -0,0 +1,48 @@ +/* + * 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 ModalValidationField from '../ModalValidationField'; + +it('should display the field without any error/validation', () => { + expect(getWrapper({ description: 'Describe Foo.', touched: false })).toMatchSnapshot(); + expect(getWrapper({ dirty: false })).toMatchSnapshot(); +}); + +it('should display the field as valid', () => { + expect(getWrapper({ error: undefined })).toMatchSnapshot(); +}); + +it('should display the field with an error', () => { + expect(getWrapper()).toMatchSnapshot(); +}); + +function getWrapper(props = {}) { + return shallow( + <ModalValidationField + dirty={true} + error="Is required" + label={<label>Foo</label>} + touched={true} + {...props}> + {({ className }) => <input className={className} type="text" />} + </ModalValidationField> + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ValidationModal-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ValidationModal-test.tsx new file mode 100644 index 00000000000..f3048542aa1 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/ValidationModal-test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { FormikProps } from 'formik'; +import ValidationModal from '../ValidationModal'; + +it('should render correctly', () => { + const { wrapper, inner } = getWrapper(); + expect(wrapper).toMatchSnapshot(); + expect(inner).toMatchSnapshot(); +}); + +interface Values { + field: string; +} + +function getWrapper(props = {}) { + const wrapper = shallow( + <ValidationModal + confirmButtonText="confirm" + header="title" + initialValues={{ field: 'foo' }} + isInitialValid={true} + onClose={jest.fn()} + validate={(values: Values) => ({ field: values.field.length < 2 && 'Too small' })} + onSubmit={jest.fn(() => Promise.resolve())} + {...props}> + {(props: FormikProps<Values>) => ( + <form onSubmit={props.handleSubmit}> + <input + onChange={props.handleChange} + onBlur={props.handleBlur} + name="field" + type="text" + value={props.values.field} + /> + </form> + )} + </ValidationModal> + ); + return { + wrapper, + inner: wrapper + .childAt(0) + .dive() + .dive() + .dive() + }; +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap new file mode 100644 index 00000000000..8afa4d9aa5a --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<ModalValidationField + description="Field description" + dirty={true} + error="Bad formatting" + label="Foo field" + touched={true} +/> +`; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap new file mode 100644 index 00000000000..dc901ce06f2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display the field as valid 1`] = ` +<div + className="modal-validation-field" +> + <label> + Foo + </label> + <input + className="is-valid" + type="text" + /> + <AlertSuccessIcon + className="little-spacer-top" + /> +</div> +`; + +exports[`should display the field with an error 1`] = ` +<div + className="modal-validation-field" +> + <label> + Foo + </label> + <input + className="has-error" + type="text" + /> + <AlertErrorIcon + className="little-spacer-top" + /> + <p + className="text-danger" + > + Is required + </p> +</div> +`; + +exports[`should display the field without any error/validation 1`] = ` +<div + className="modal-validation-field" +> + <label> + Foo + </label> + <input + className="" + type="text" + /> + <div + className="modal-field-description" + > + Describe Foo. + </div> +</div> +`; + +exports[`should display the field without any error/validation 2`] = ` +<div + className="modal-validation-field" +> + <label> + Foo + </label> + <input + className="" + type="text" + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap new file mode 100644 index 00000000000..898438ec774 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<Modal + contentLabel="title" + onRequestClose={[MockFunction]} +> + <C + confirmButtonText="confirm" + header="title" + initialValues={ + Object { + "field": "foo", + } + } + /> +</Modal> +`; + +exports[`should render correctly 2`] = ` +<Form> + <div + className="modal-head" + > + <h2> + title + </h2> + </div> + <div + className="modal-body" + > + <form + onSubmit={[Function]} + > + <input + name="field" + onBlur={[Function]} + onChange={[Function]} + type="text" + value="foo" + /> + </form> + </div> + <footer + className="modal-foot" + > + <DeferredSpinner + className="spacer-right" + loading={false} + timeout={100} + /> + <button + disabled={true} + type="submit" + > + confirm + </button> + <button + className="button-link" + disabled={false} + onClick={[Function]} + type="reset" + > + cancel + </button> + </footer> +</Form> +`; diff --git a/server/sonar-web/src/main/js/components/icons-components/AlertSuccessIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/AlertSuccessIcon.tsx new file mode 100644 index 00000000000..432461ad528 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/AlertSuccessIcon.tsx @@ -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 React from 'react'; +import * as theme from '../../app/theme'; +import { IconProps } from './types'; + +export default function AlertSuccessIcon({ className, fill = theme.green, size = 16 }: IconProps) { + return ( + <svg + className={className} + width={size} + height={size} + viewBox="0 0 16 16" + version="1.1" + xmlnsXlink="http://www.w3.org/1999/xlink" + xmlSpace="preserve"> + <path + style={{ fill }} + d="M12.607 6.554q0-0.25-0.161-0.411l-0.813-0.804q-0.17-0.17-0.402-0.17t-0.402 0.17l-3.643 3.634-2.018-2.018q-0.17-0.17-0.402-0.17t-0.402 0.17l-0.813 0.804q-0.161 0.161-0.161 0.411 0 0.241 0.161 0.402l3.232 3.232q0.17 0.17 0.402 0.17 0.241 0 0.411-0.17l4.848-4.848q0.161-0.161 0.161-0.402zM14.857 8q0 1.866-0.92 3.442t-2.496 2.496-3.442 0.92-3.442-0.92-2.496-2.496-0.92-3.442 0.92-3.442 2.496-2.496 3.442-0.92 3.442 0.92 2.496 2.496 0.92 3.442z" + /> + </svg> + ); +} diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 7be3c394b57..4f6610a2e05 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -3173,6 +3173,16 @@ form-data@~2.3.1: combined-stream "^1.0.5" mime-types "^2.1.12" +formik@0.11.7: + version "0.11.7" + resolved "https://registry.yarnpkg.com/formik/-/formik-0.11.7.tgz#7b2c66a5546c793dfb07b39b965aef69dcd39326" + dependencies: + lodash.clonedeep "^4.5.0" + lodash.isequal "4.5.0" + lodash.topath "4.5.2" + prop-types "^15.5.10" + warning "^3.0.0" + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -4807,6 +4817,10 @@ lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" @@ -4823,6 +4837,10 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isequal@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + lodash.keys@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" @@ -4839,6 +4857,10 @@ lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" +lodash.topath@4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009" + lodash.unescape@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" |