From cf9eb2f6a2bc594e3007eff1494817a42a6f2d35 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 6 Feb 2018 16:11:41 +0100 Subject: [PATCH] rewrite groups app with react (#3017) --- .../sonar-web/src/main/js/api/user_groups.ts | 22 ++- server/sonar-web/src/main/js/app/types.ts | 8 + .../coding-rules/components/RuleDetails.tsx | 2 +- .../components/RuleDetailsCustomRules.tsx | 2 +- .../components/RuleDetailsProfiles.tsx | 2 +- .../coding-rules/components/RuleListItem.tsx | 2 +- .../main/js/apps/groups/components/App.tsx | 182 ++++++++++++++++++ .../js/apps/groups/components/EditGroup.tsx | 84 ++++++++ .../js/apps/groups/components/EditMembers.tsx | 126 ++++++++++++ .../main/js/apps/groups/components/Form.tsx | 117 +++++++++++ .../groups/components/GroupsAppContainer.js | 39 ---- .../main/js/apps/groups/components/Header.tsx | 89 +++++++++ .../main/js/apps/groups/components/List.tsx | 76 ++++++++ .../js/apps/groups/components/ListItem.tsx | 103 ++++++++++ .../components/__tests__/EditGroup-test.tsx | 58 ++++++ .../__tests__/EditMembers-test.tsx} | 42 ++-- .../groups/components/__tests__/Form-test.tsx | 49 +++++ .../__tests__/Header-test.tsx} | 29 ++- .../__tests__/List-test.tsx} | 62 +++--- .../components/__tests__/ListItem-test.tsx | 54 ++++++ .../__snapshots__/EditGroup-test.tsx.snap | 32 +++ .../__snapshots__/EditMembers-test.tsx.snap | 64 ++++++ .../__snapshots__/Form-test.tsx.snap | 86 +++++++++ .../__snapshots__/Header-test.tsx.snap | 75 ++++++++ .../__snapshots__/List-test.tsx.snap | 106 ++++++++++ .../__snapshots__/ListItem-test.tsx.snap | 118 ++++++++++++ .../src/main/js/apps/groups/create-view.js | 48 ----- .../src/main/js/apps/groups/delete-view.js | 57 ------ .../src/main/js/apps/groups/group.js | 62 ------ .../src/main/js/apps/groups/groups.js | 65 ------- .../sonar-web/src/main/js/apps/groups/init.js | 63 ------ .../src/main/js/apps/groups/layout.js | 32 --- .../src/main/js/apps/groups/list-item-view.js | 83 -------- .../src/main/js/apps/groups/list-view.js | 53 ----- .../src/main/js/apps/groups/routes.ts | 6 +- .../src/main/js/apps/groups/search-view.js | 86 --------- .../apps/groups/templates/groups-delete.hbs | 13 -- .../js/apps/groups/templates/groups-form.hbs | 24 --- .../apps/groups/templates/groups-header.hbs | 8 - .../apps/groups/templates/groups-layout.hbs | 6 - .../groups/templates/groups-list-footer.hbs | 6 - .../groups/templates/groups-list-item.hbs | 41 ---- .../js/apps/groups/templates/groups-list.hbs | 19 -- .../apps/groups/templates/groups-search.hbs | 15 -- .../js/apps/groups/templates/groups-users.hbs | 10 - .../src/main/js/apps/groups/update-view.js | 47 ----- .../src/main/js/apps/groups/users-view.js | 68 ------- .../components/OrganizationGroups.js | 45 ----- .../src/main/js/apps/organizations/routes.ts | 4 +- .../controls}/ConfirmButton.tsx | 36 +++- .../js/components/controls/SimpleModal.tsx | 11 ++ 51 files changed, 1550 insertions(+), 987 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/groups/components/App.tsx create mode 100644 server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx create mode 100644 server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx create mode 100644 server/sonar-web/src/main/js/apps/groups/components/Form.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js create mode 100644 server/sonar-web/src/main/js/apps/groups/components/Header.tsx create mode 100644 server/sonar-web/src/main/js/apps/groups/components/List.tsx create mode 100644 server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx rename server/sonar-web/src/main/js/apps/groups/{list-footer-view.js => components/__tests__/EditMembers-test.tsx} (56%) create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.tsx rename server/sonar-web/src/main/js/apps/groups/{form-view.js => components/__tests__/Header-test.tsx} (58%) rename server/sonar-web/src/main/js/apps/groups/{header-view.js => components/__tests__/List-test.tsx} (50%) create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Header-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/groups/create-view.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/delete-view.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/group.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/groups.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/init.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/layout.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/list-item-view.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/list-view.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/search-view.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs delete mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs delete mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs delete mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs delete mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs delete mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs delete mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-list.hbs delete mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs delete mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs delete mode 100644 server/sonar-web/src/main/js/apps/groups/update-view.js delete mode 100644 server/sonar-web/src/main/js/apps/groups/users-view.js delete mode 100644 server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroups.js rename server/sonar-web/src/main/js/{apps/coding-rules/components => components/controls}/ConfirmButton.tsx (76%) diff --git a/server/sonar-web/src/main/js/api/user_groups.ts b/server/sonar-web/src/main/js/api/user_groups.ts index 7fbddde3133..2e1b85c5cdf 100644 --- a/server/sonar-web/src/main/js/api/user_groups.ts +++ b/server/sonar-web/src/main/js/api/user_groups.ts @@ -17,7 +17,9 @@ * 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, post } from '../helpers/request'; +import { getJSON, post, postJSON } from '../helpers/request'; +import { Paging, Group } from '../app/types'; +import throwGlobalError from '../app/utils/throwGlobalError'; export function searchUsersGroups(data: { f?: string; @@ -25,7 +27,7 @@ export function searchUsersGroups(data: { p?: number; ps?: number; q?: string; -}) { +}): Promise<{ groups: Group[]; paging: Paging }> { return getJSON('/api/user_groups/search', data); } @@ -46,3 +48,19 @@ export function removeUserFromGroup(data: { }) { return post('/api/user_groups/remove_user', data); } + +export function createGroup(data: { + description?: string; + organization: string | undefined; + name: string; +}): Promise { + return postJSON('/api/user_groups/create', data).then(r => r.group, throwGlobalError); +} + +export function updateGroup(data: { description?: string; id: number; name?: string }) { + return post('/api/user_groups/update', data).catch(throwGlobalError); +} + +export function deleteGroup(data: { name: string; organization: string | undefined }) { + return post('/api/user_groups/delete', data).catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index ef1f97b8cf1..f0c37fbe7a3 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -254,3 +254,11 @@ export enum RuleInheritance { Inherited = 'INHERITED', Overridden = 'OVERRIDES' } + +export interface Group { + default?: boolean; + description?: string; + id: number; + membersCount: number; + name: string; +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx index d9c9007287f..16a758b4256 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import ConfirmButton from './ConfirmButton'; import CustomRuleButton from './CustomRuleButton'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import RuleDetailsCustomRules from './RuleDetailsCustomRules'; @@ -31,6 +30,7 @@ import { Query, Activation } from '../query'; import { Profile } from '../../../api/quality-profiles'; import { getRuleDetails, deleteRule, updateRule } from '../../../api/rules'; import { RuleActivation, RuleDetails as IRuleDetails } from '../../../app/types'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; import { translate, translateWithParameters } from '../../../helpers/l10n'; interface Props { diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx index 9d7cd528167..2804fbde9e9 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx @@ -20,11 +20,11 @@ import * as React from 'react'; import { Link } from 'react-router'; import { sortBy } from 'lodash'; -import ConfirmButton from './ConfirmButton'; import CustomRuleButton from './CustomRuleButton'; import { searchRules, deleteRule } from '../../../api/rules'; import { Rule, RuleDetails } from '../../../app/types'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; import SeverityHelper from '../../../components/shared/SeverityHelper'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getRuleUrl } from '../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx index f8dce629e1d..16286383865 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx @@ -21,10 +21,10 @@ import * as React from 'react'; import { filter } from 'lodash'; import { Link } from 'react-router'; import ActivationButton from './ActivationButton'; -import ConfirmButton from './ConfirmButton'; import RuleInheritanceIcon from './RuleInheritanceIcon'; import { Profile, deactivateRule, activateRule } from '../../../api/quality-profiles'; import { RuleActivation, RuleDetails, RuleInheritance } from '../../../app/types'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getQualityProfileUrl } from '../../../helpers/urls'; import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx index 2c97ea8e946..e713523ac8f 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx @@ -22,10 +22,10 @@ import * as classNames from 'classnames'; import { Link } from 'react-router'; import { Activation, Query } from '../query'; import ActivationButton from './ActivationButton'; -import ConfirmButton from './ConfirmButton'; import SimilarRulesFilter from './SimilarRulesFilter'; import { Profile, deactivateRule } from '../../../api/quality-profiles'; import { Rule, RuleInheritance } from '../../../app/types'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; import Tooltip from '../../../components/controls/Tooltip'; import SeverityIcon from '../../../components/shared/SeverityIcon'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; diff --git a/server/sonar-web/src/main/js/apps/groups/components/App.tsx b/server/sonar-web/src/main/js/apps/groups/components/App.tsx new file mode 100644 index 00000000000..5aebe166132 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/App.tsx @@ -0,0 +1,182 @@ +/* + * 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 List from './List'; +import { searchUsersGroups, deleteGroup, updateGroup, createGroup } from '../../../api/user_groups'; +import { Group, Paging } from '../../../app/types'; +import ListFooter from '../../../components/controls/ListFooter'; +import SearchBox from '../../../components/controls/SearchBox'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + organization?: { key: string }; +} + +interface State { + groups?: Group[]; + loading: boolean; + paging?: Paging; + query: string; +} + +export default class App extends React.PureComponent { + mounted: boolean; + state: State = { loading: true, query: '' }; + + componentDidMount() { + this.mounted = true; + this.fetchGroups(); + } + + componentWillUnmount() { + this.mounted = false; + } + + get organization() { + return this.props.organization && this.props.organization.key; + } + + makeFetchGroupsRequest = (data?: { p?: number; q?: string }) => { + this.setState({ loading: true }); + return searchUsersGroups({ + organization: this.organization, + q: this.state.query, + ...data + }); + }; + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + + fetchGroups = (data?: { p?: number; q?: string }) => { + this.makeFetchGroupsRequest(data).then(({ groups, paging }) => { + if (this.mounted) { + this.setState({ groups, loading: false, paging }); + } + }, this.stopLoading); + }; + + fetchMoreGroups = () => { + const { paging } = this.state; + if (paging && paging.total > paging.pageIndex * paging.pageSize) { + this.makeFetchGroupsRequest({ p: paging.pageIndex + 1 }).then(({ groups, paging }) => { + if (this.mounted) { + this.setState(({ groups: existingGroups = [] }) => ({ + groups: [...existingGroups, ...groups], + loading: false, + paging + })); + } + }, this.stopLoading); + } + }; + + search = (query: string) => { + this.fetchGroups({ q: query }); + this.setState({ query }); + }; + + refresh = () => { + this.fetchGroups({ q: this.state.query }); + }; + + handleCreate = (data: { description: string; name: string }) => { + return createGroup({ ...data, organization: this.organization }).then(group => { + if (this.mounted) { + this.setState(({ groups = [] }: State) => ({ + groups: [...groups, group] + })); + } + }); + }; + + handleDelete = (name: string) => { + return deleteGroup({ name, organization: this.organization }).then(() => { + if (this.mounted) { + this.setState(({ groups = [] }: State) => ({ + groups: groups.filter(group => group.name !== name) + })); + } + }); + }; + + handleEdit = (data: { description?: string; id: number; name?: string }) => { + return updateGroup(data).then(() => { + if (this.mounted) { + this.setState(({ groups = [] }: State) => ({ + groups: groups.map(group => (group.id === data.id ? { ...group, ...data } : group)) + })); + } + }); + }; + + render() { + const { groups, loading, paging, query } = this.state; + + const showAnyone = + this.props.organization === undefined && 'anyone'.includes(query.toLowerCase()); + + return ( + <> + +
+
+ + + + {groups !== undefined && ( + + )} + + {groups !== undefined && + paging !== undefined && ( + + )} +
+ + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx new file mode 100644 index 00000000000..8cf6bf7f8d2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx @@ -0,0 +1,84 @@ +/* + * 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 Form from './Form'; +import { Group } from '../../../app/types'; +import { translate } from '../../../helpers/l10n'; +import { omitNil } from '../../../helpers/request'; + +interface Props { + children: (props: { onClick: () => void }) => React.ReactNode; + group: Group; + onEdit: (data: { description?: string; id: number; name?: string }) => Promise; +} + +interface State { + modal: boolean; +} + +export default class EditGroup extends React.PureComponent { + mounted: boolean; + state: State = { modal: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleClick = () => { + this.setState({ modal: true }); + }; + + handleClose = () => { + if (this.mounted) { + this.setState({ modal: false }); + } + }; + + handleSubmit = ({ name, description }: { name: string; description: string }) => { + const { group } = this.props; + return this.props.onEdit({ + description, + id: group.id, + // pass `name` only if it has changed, otherwise the WS fails + ...omitNil({ name: name !== group.name ? name : undefined }) + }); + }; + + render() { + return ( + <> + {this.props.children({ onClick: this.handleClick })} + {this.state.modal && ( +
+ )} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx new file mode 100644 index 00000000000..6ab1d85fd0a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx @@ -0,0 +1,126 @@ +/* + * 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 escapeHtml from 'escape-html'; +import { Group } from '../../../app/types'; +import Modal from '../../../components/controls/Modal'; +import BulletListIcon from '../../../components/icons-components/BulletListIcon'; +import SelectList from '../../../components/SelectList'; +import { ButtonIcon } from '../../../components/ui/buttons'; +import { translate } from '../../../helpers/l10n'; +import { getBaseUrl } from '../../../helpers/urls'; + +interface Props { + group: Group; + onEdit: () => void; + organization: string | undefined; +} + +interface State { + modal: boolean; +} + +export default class EditMembers extends React.PureComponent { + container?: HTMLElement | null; + mounted: boolean; + state: State = { modal: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleMembersClick = () => { + this.setState({ modal: true }, () => { + // defer rendering of the SelectList to make sure we have `ref` assigned + setTimeout(this.renderSelectList, 0); + }); + }; + + handleModalClose = () => { + if (this.mounted) { + this.setState({ modal: false }); + this.props.onEdit(); + } + }; + + handleCloseClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.handleModalClose(); + }; + + renderSelectList = () => { + if (this.container) { + const extra = { name: this.props.group.name, organization: this.props.organization }; + + /* eslint-disable no-new */ + new SelectList({ + el: this.container, + width: '100%', + readOnly: false, + focusSearch: false, + dangerouslyUnescapedHtmlFormat: (item: { login: string; name: string }) => + `${escapeHtml(item.name)}
${escapeHtml(item.login)}`, + queryParam: 'q', + searchUrl: getBaseUrl() + '/api/user_groups/users?ps=100&id=' + this.props.group.id, + selectUrl: getBaseUrl() + '/api/user_groups/add_user', + deselectUrl: getBaseUrl() + '/api/user_groups/remove_user', + extra, + selectParameter: 'login', + selectParameterValue: 'login', + parse: (r: any) => r.users + }); + /* eslint-enable no-new */ + } + }; + + render() { + const modalHeader = translate('users.update'); + + return ( + <> + + + + {this.state.modal && ( + +
+

{modalHeader}

+
+ +
+
(this.container = node)} /> +
+ +
+ +
+ + )} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/Form.tsx b/server/sonar-web/src/main/js/apps/groups/components/Form.tsx new file mode 100644 index 00000000000..23452cef400 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/Form.tsx @@ -0,0 +1,117 @@ +/* + * 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 { Group } from '../../../app/types'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import SimpleModal from '../../../components/controls/SimpleModal'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + confirmButtonText: string; + group?: Group; + header: string; + onClose: () => void; + onSubmit: (data: { description: string; name: string }) => Promise; +} + +interface State { + description: string; + name: string; +} + +export default class Form extends React.PureComponent { + constructor(props: Props) { + super(props); + this.state = { + description: (props.group && props.group.description) || '', + name: (props.group && props.group.name) || '' + }; + } + + handleSubmit = () => { + return this.props + .onSubmit({ description: this.state.description, name: this.state.name }) + .then(this.props.onClose); + }; + + handleDescriptionChange = (event: React.SyntheticEvent) => { + this.setState({ description: event.currentTarget.value }); + }; + + handleNameChange = (event: React.SyntheticEvent) => { + this.setState({ name: event.currentTarget.value }); + }; + + render() { + return ( + + {({ onCloseClick, onFormSubmit, submitting }) => ( + +
+

{this.props.header}

+
+ +
+
+ + +
+
+ + - -
-
- - diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs deleted file mode 100644 index 95ba488355d..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs deleted file mode 100644 index 92fd355cc47..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs +++ /dev/null @@ -1,6 +0,0 @@ -
-
- -
- -
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs deleted file mode 100644 index 16d1bdec7f5..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs deleted file mode 100644 index 6d2600c9e41..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs +++ /dev/null @@ -1,41 +0,0 @@ -
- {{#unless default}} - - {{/unless}} -
- -
- {{name}} - {{#if default}} - ({{t 'default'}}) - {{/if}} -
- -
-
- {{t 'members'}} -
-
- {{membersCount}} - {{#unless default}} - - {{/unless}} -
-
- -
- {{description}} -
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-list.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-list.hbs deleted file mode 100644 index ac23f3c1f65..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/templates/groups-list.hbs +++ /dev/null @@ -1,19 +0,0 @@ -
- {{#isNull organization}} -
-
- {{t 'groups.anyone'}} -
- -
- -
- -
- {{t 'user_groups.anyone.description'}} -
-
- {{/isNull}} - -
    -
    diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs deleted file mode 100644 index ecf034da48e..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs +++ /dev/null @@ -1,15 +0,0 @@ -
    - -
    diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs deleted file mode 100644 index 7f9db589a3e..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/server/sonar-web/src/main/js/apps/groups/update-view.js b/server/sonar-web/src/main/js/apps/groups/update-view.js deleted file mode 100644 index 76023ba0190..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/update-view.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 FormView from './form-view'; - -export default FormView.extend({ - sendRequest() { - const that = this; - this.model.set({ - name: this.$('#create-group-name').val(), - description: this.$('#create-group-description').val() - }); - this.disableForm(); - return this.model - .save(null, { - organization: this.collection.organization, - statusCode: { - // do not show global error - 400: null - } - }) - .done(() => { - that.collection.refresh(); - that.destroy(); - }) - .fail(jqXHR => { - that.enableForm(); - that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); - }); - } -}); diff --git a/server/sonar-web/src/main/js/apps/groups/users-view.js b/server/sonar-web/src/main/js/apps/groups/users-view.js deleted file mode 100644 index 980974bb11c..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/users-view.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 escapeHtml from 'escape-html'; -import Modal from '../../components/common/modals'; -import SelectList from '../../components/SelectList'; -import Template from './templates/groups-users.hbs'; - -export default Modal.extend({ - template: Template, - - initialize(options) { - this.organization = options.organization; - }, - - onRender() { - Modal.prototype.onRender.apply(this, arguments); - - const extra = { - name: this.model.get('name') - }; - if (this.organization) { - extra.organization = this.organization.key; - } - - new SelectList({ - el: this.$('#groups-users'), - width: '100%', - readOnly: false, - focusSearch: false, - dangerouslyUnescapedHtmlFormat(item) { - return `${escapeHtml(item.name)}
    ${escapeHtml(item.login)}`; - }, - queryParam: 'q', - searchUrl: window.baseUrl + '/api/user_groups/users?ps=100&id=' + this.model.id, - selectUrl: window.baseUrl + '/api/user_groups/add_user', - deselectUrl: window.baseUrl + '/api/user_groups/remove_user', - extra, - selectParameter: 'login', - selectParameterValue: 'login', - parse(r) { - this.more = false; - return r.users; - } - }); - }, - - onDestroy() { - this.model.collection.refresh(); - Modal.prototype.onDestroy.apply(this, arguments); - } -}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroups.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroups.js deleted file mode 100644 index 7005366a0b4..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroups.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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. - */ -// @flow -import React from 'react'; -import Helmet from 'react-helmet'; -import init from '../../groups/init'; -import { translate } from '../../../helpers/l10n'; -/*:: import type { Organization } from '../../../store/organizations/duck'; */ - -export default class OrganizationGroups extends React.PureComponent { - /*:: props: { - organization: Organization - }; -*/ - - componentDidMount() { - init(this.refs.container, this.props.organization); - } - - render() { - return ( -
    - -
    -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/organizations/routes.ts b/server/sonar-web/src/main/js/apps/organizations/routes.ts index e6d2aa793ad..6546097f981 100644 --- a/server/sonar-web/src/main/js/apps/organizations/routes.ts +++ b/server/sonar-web/src/main/js/apps/organizations/routes.ts @@ -25,7 +25,6 @@ import OrganizationContainer from './components/OrganizationContainer'; import OrganizationProjects from './components/OrganizationProjects'; import OrganizationAdminContainer from './components/OrganizationAdminContainer'; import OrganizationEdit from './components/OrganizationEdit'; -import OrganizationGroups from './components/OrganizationGroups'; import OrganizationMembersContainer from './components/OrganizationMembersContainer'; import OrganizationDelete from './components/OrganizationDelete'; import PermissionTemplateApp from '../permission-templates/components/AppContainer'; @@ -34,6 +33,7 @@ import codingRulesRoutes from '../coding-rules/routes'; import qualityGatesRoutes from '../quality-gates/routes'; import qualityProfilesRoutes from '../quality-profiles/routes'; import Issues from '../issues/components/AppContainer'; +import GroupsApp from '../groups/components/App'; const routes = [ { @@ -85,7 +85,7 @@ const routes = [ childRoutes: [ { path: 'delete', component: OrganizationDelete }, { path: 'edit', component: OrganizationEdit }, - { path: 'groups', component: OrganizationGroups }, + { path: 'groups', component: GroupsApp }, { path: 'permissions', component: GlobalPermissionsApp }, { path: 'permission_templates', component: PermissionTemplateApp }, { path: 'projects_management', component: ProjectManagementApp } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx similarity index 76% rename from server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx rename to server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx index 775fe1a560f..777c576eee6 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx +++ b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx @@ -18,12 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import SimpleModal from '../../../components/controls/SimpleModal'; -import { translate } from '../../../helpers/l10n'; +import SimpleModal from './SimpleModal'; +import DeferredSpinner from '../common/DeferredSpinner'; +import { translate } from '../../helpers/l10n'; interface Props { children: ( - props: { onClick: (event: React.SyntheticEvent) => void } + props: { onClick: (event?: React.SyntheticEvent) => void } ) => React.ReactNode; confirmButtonText: string; confirmData?: string; @@ -37,26 +38,41 @@ interface State { modal: boolean; } -// TODO move this component to components/ and use everywhere! export default class ConfirmButton extends React.PureComponent { + mounted: boolean; state: State = { modal: false }; - handleButtonClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.currentTarget.blur(); + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleButtonClick = (event?: React.SyntheticEvent) => { + if (event) { + event.preventDefault(); + event.currentTarget.blur(); + } this.setState({ modal: true }); }; handleSubmit = () => { const result = this.props.onConfirm(this.props.confirmData); if (result) { - result.then(this.handleCloseModal, () => {}); + return result.then(this.handleCloseModal, () => {}); } else { this.handleCloseModal(); + return undefined; } }; - handleCloseModal = () => this.setState({ modal: false }); + handleCloseModal = () => { + if (this.mounted) { + this.setState({ modal: false }); + } + }; render() { const { confirmButtonText, isDestructive, modalBody, modalHeader } = this.props; @@ -78,7 +94,7 @@ export default class ConfirmButton extends React.PureComponent {
    {modalBody}