aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-02-06 16:11:41 +0100
committerGitHub <noreply@github.com>2018-02-06 16:11:41 +0100
commitcf9eb2f6a2bc594e3007eff1494817a42a6f2d35 (patch)
tree177cb35f5bc07f3811462a95c4137fc243c7ba48 /server
parent3c42d5d2e6b362d389c0058e069897bf26ef65f7 (diff)
downloadsonarqube-cf9eb2f6a2bc594e3007eff1494817a42a6f2d35.tar.gz
sonarqube-cf9eb2f6a2bc594e3007eff1494817a42a6f2d35.zip
rewrite groups app with react (#3017)
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/user_groups.ts22
-rw-r--r--server/sonar-web/src/main/js/app/types.ts8
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/App.tsx182
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx84
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx126
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/Form.tsx117
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js39
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/Header.tsx89
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/List.tsx76
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx103
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx58
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx (renamed from server/sonar-web/src/main/js/apps/groups/list-footer-view.js)42
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx (renamed from server/sonar-web/src/main/js/apps/groups/form-view.js)29
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx (renamed from server/sonar-web/src/main/js/apps/groups/header-view.js)62
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap32
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap64
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap86
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Header-test.tsx.snap75
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap106
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap118
-rw-r--r--server/sonar-web/src/main/js/apps/groups/create-view.js48
-rw-r--r--server/sonar-web/src/main/js/apps/groups/delete-view.js57
-rw-r--r--server/sonar-web/src/main/js/apps/groups/group.js62
-rw-r--r--server/sonar-web/src/main/js/apps/groups/groups.js65
-rw-r--r--server/sonar-web/src/main/js/apps/groups/init.js63
-rw-r--r--server/sonar-web/src/main/js/apps/groups/layout.js32
-rw-r--r--server/sonar-web/src/main/js/apps/groups/list-item-view.js83
-rw-r--r--server/sonar-web/src/main/js/apps/groups/list-view.js53
-rw-r--r--server/sonar-web/src/main/js/apps/groups/routes.ts6
-rw-r--r--server/sonar-web/src/main/js/apps/groups/search-view.js86
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs24
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs8
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs6
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs6
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs41
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-list.hbs19
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs15
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs10
-rw-r--r--server/sonar-web/src/main/js/apps/groups/update-view.js47
-rw-r--r--server/sonar-web/src/main/js/apps/groups/users-view.js68
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroups.js45
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/routes.ts4
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx (renamed from server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx)36
-rw-r--r--server/sonar-web/src/main/js/components/controls/SimpleModal.tsx11
51 files changed, 1550 insertions, 987 deletions
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<Group> {
+ 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<Props, State> {
+ 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 (
+ <>
+ <Helmet title={translate('user_groups.page')} />
+ <div className="page page-limited" id="groups-page">
+ <Header loading={loading} onCreate={this.handleCreate} />
+
+ <SearchBox
+ className="big-spacer-bottom"
+ id="groups-search"
+ minLength={2}
+ onChange={this.search}
+ placeholder={translate('search.search_by_name')}
+ value={query}
+ />
+
+ {groups !== undefined && (
+ <List
+ groups={groups}
+ onDelete={this.handleDelete}
+ onEdit={this.handleEdit}
+ onEditMembers={this.refresh}
+ organization={this.organization}
+ showAnyone={showAnyone}
+ />
+ )}
+
+ {groups !== undefined &&
+ paging !== undefined && (
+ <div id="groups-list-footer">
+ <ListFooter
+ count={showAnyone ? groups.length + 1 : groups.length}
+ loadMore={this.fetchMoreGroups}
+ ready={!loading}
+ total={showAnyone ? paging.total + 1 : paging.total}
+ />
+ </div>
+ )}
+ </div>
+ </>
+ );
+ }
+}
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<void>;
+}
+
+interface State {
+ modal: boolean;
+}
+
+export default class EditGroup extends React.PureComponent<Props, State> {
+ 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 && (
+ <Form
+ confirmButtonText={translate('update_verb')}
+ group={this.props.group}
+ header={translate('groups.update_group')}
+ onClose={this.handleClose}
+ onSubmit={this.handleSubmit}
+ />
+ )}
+ </>
+ );
+ }
+}
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<Props, State> {
+ 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<HTMLElement>) => {
+ 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)}<br><span class="note">${escapeHtml(item.login)}</span>`,
+ 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 (
+ <>
+ <ButtonIcon className="button-small" onClick={this.handleMembersClick}>
+ <BulletListIcon />
+ </ButtonIcon>
+ {this.state.modal && (
+ <Modal contentLabel={modalHeader} onRequestClose={this.handleModalClose}>
+ <header className="modal-head">
+ <h2>{modalHeader}</h2>
+ </header>
+
+ <div className="modal-body">
+ <div id="groups-users" ref={node => (this.container = node)} />
+ </div>
+
+ <footer className="modal-foot">
+ <button className="button-link" onClick={this.handleCloseClick} type="reset">
+ {translate('Done')}
+ </button>
+ </footer>
+ </Modal>
+ )}
+ </>
+ );
+ }
+}
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<void>;
+}
+
+interface State {
+ description: string;
+ name: string;
+}
+
+export default class Form extends React.PureComponent<Props, State> {
+ 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<HTMLTextAreaElement>) => {
+ this.setState({ description: event.currentTarget.value });
+ };
+
+ handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+ this.setState({ name: event.currentTarget.value });
+ };
+
+ render() {
+ return (
+ <SimpleModal
+ header={this.props.header}
+ onClose={this.props.onClose}
+ onSubmit={this.handleSubmit}>
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <form onSubmit={onFormSubmit}>
+ <header className="modal-head">
+ <h2>{this.props.header}</h2>
+ </header>
+
+ <div className="modal-body">
+ <div className="modal-field">
+ <label htmlFor="create-group-name">
+ {translate('name')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ autoFocus={true}
+ id="create-group-name"
+ maxLength={255}
+ name="name"
+ onChange={this.handleNameChange}
+ required={true}
+ size={50}
+ type="text"
+ value={this.state.name}
+ />
+ </div>
+ <div className="modal-field">
+ <label htmlFor="create-group-description">{translate('description')}</label>
+ <textarea
+ id="create-group-description"
+ name="description"
+ onChange={this.handleDescriptionChange}
+ value={this.state.description}
+ />
+ </div>
+ </div>
+
+ <footer className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={submitting} />
+ <button disabled={submitting} type="submit">
+ {this.props.confirmButtonText}
+ </button>
+ <button className="button-link" onClick={onCloseClick} type="reset">
+ {translate('cancel')}
+ </button>
+ </footer>
+ </form>
+ )}
+ </SimpleModal>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js b/server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js
deleted file mode 100644
index 30411e9f270..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js
+++ /dev/null
@@ -1,39 +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 React from 'react';
-import Helmet from 'react-helmet';
-import init from '../init';
-import { translate } from '../../../helpers/l10n';
-import '../../../components/controls/SearchBox.css';
-
-export default class GroupsAppContainer extends React.PureComponent {
- componentDidMount() {
- init(this.refs.container);
- }
-
- render() {
- return (
- <div>
- <Helmet title={translate('user_groups.page')} />
- <div ref="container" />
- </div>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/Header.tsx b/server/sonar-web/src/main/js/apps/groups/components/Header.tsx
new file mode 100644
index 00000000000..973ccb981dd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/Header.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 Form from './Form';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ loading: boolean;
+ onCreate: (data: { description: string; name: string }) => Promise<void>;
+}
+
+interface State {
+ createModal: boolean;
+}
+
+export default class Header extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { createModal: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleCreateClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.setState({ createModal: true });
+ };
+
+ handleClose = () => {
+ if (this.mounted) {
+ this.setState({ createModal: false });
+ }
+ };
+
+ handleSubmit = (data: { name: string; description: string }) => {
+ return this.props.onCreate(data);
+ };
+
+ render() {
+ return (
+ <>
+ <header className="page-header" id="groups-header">
+ <h1 className="page-title">{translate('user_groups.page')}</h1>
+
+ <DeferredSpinner loading={this.props.loading} />
+
+ <div className="page-actions">
+ <button id="groups-create" onClick={this.handleCreateClick}>
+ {translate('groups.create_group')}
+ </button>
+ </div>
+
+ <p className="page-description">{translate('user_groups.page.description')}</p>
+ </header>
+ {this.state.createModal && (
+ <Form
+ confirmButtonText={translate('create')}
+ header={translate('groups.create_group')}
+ onClose={this.handleClose}
+ onSubmit={this.handleSubmit}
+ />
+ )}
+ </>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/List.tsx b/server/sonar-web/src/main/js/apps/groups/components/List.tsx
new file mode 100644
index 00000000000..c4207f141d5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/List.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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 { sortBy } from 'lodash';
+import { Group } from '../../../app/types';
+import ListItem from './ListItem';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ groups: Group[];
+ onDelete: (name: string) => Promise<void>;
+ onEdit: (data: { description?: string; id: number; name?: string }) => Promise<void>;
+ onEditMembers: () => void;
+ organization: string | undefined;
+ showAnyone: boolean;
+}
+
+export default function List(props: Props) {
+ return (
+ <div className="boxed-group boxed-group-inner">
+ <table id="groups-list" className="data zebra zebra-hover">
+ <thead>
+ <tr>
+ <th />
+ <th className="nowrap">{translate('members')}</th>
+ <th className="nowrap">{translate('description')}</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {props.showAnyone && (
+ <tr className="js-anyone" key="anyone">
+ <td className="width-20">
+ <strong className="js-group-name">{translate('groups.anyone')}</strong>
+ </td>
+ <td className="width-10" />
+ <td className="width-40" colSpan={2}>
+ <span className="js-group-description">
+ {translate('user_groups.anyone.description')}
+ </span>
+ </td>
+ </tr>
+ )}
+
+ {sortBy(props.groups, group => group.name.toLowerCase()).map(group => (
+ <ListItem
+ group={group}
+ key={group.id}
+ onDelete={props.onDelete}
+ onEdit={props.onEdit}
+ onEditMembers={props.onEditMembers}
+ organization={props.organization}
+ />
+ ))}
+ </tbody>
+ </table>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
new file mode 100644
index 00000000000..651066f64b0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
@@ -0,0 +1,103 @@
+/*
+ * 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 EditGroup from './EditGroup';
+import EditMembers from './EditMembers';
+import { Group } from '../../../app/types';
+import ActionsDropdown, {
+ ActionsDropdownItem,
+ ActionsDropdownDivider
+} from '../../../components/controls/ActionsDropdown';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ group: Group;
+ onDelete: (name: string) => Promise<void>;
+ onEdit: (data: { description?: string; id: number; name?: string }) => Promise<void>;
+ onEditMembers: () => void;
+ organization: string | undefined;
+}
+
+export default class ListItem extends React.PureComponent<Props> {
+ handleDelete = () => {
+ return this.props.onDelete(this.props.group.name);
+ };
+
+ render() {
+ const { group } = this.props;
+
+ return (
+ <tr data-id={group.id}>
+ <td className=" width-20">
+ <strong className="js-group-name">{group.name}</strong>
+ {group.default && <span className="little-spacer-left">({translate('default')})</span>}
+ </td>
+
+ <td className="width-10">
+ <div className="display-flex-center">
+ <span className="spacer-right">{group.membersCount}</span>
+ {!group.default && (
+ <EditMembers
+ group={group}
+ onEdit={this.props.onEditMembers}
+ organization={this.props.organization}
+ />
+ )}
+ </div>
+ </td>
+
+ <td className="width-40">
+ <span className="js-group-description">{group.description}</span>
+ </td>
+
+ <td className="thin nowrap text-right">
+ {!group.default && (
+ <ActionsDropdown>
+ <EditGroup group={group} onEdit={this.props.onEdit}>
+ {({ onClick }) => (
+ <ActionsDropdownItem className="js-group-update" onClick={onClick}>
+ {translate('update_details')}
+ </ActionsDropdownItem>
+ )}
+ </EditGroup>
+ <ActionsDropdownDivider />
+ <ConfirmButton
+ confirmButtonText={translate('delete')}
+ isDestructive={true}
+ modalBody={translateWithParameters('groups.delete_group.confirmation', group.name)}
+ modalHeader={translate('groups.delete_group')}
+ onConfirm={this.handleDelete}>
+ {({ onClick }) => (
+ <ActionsDropdownItem
+ className="js-group-delete"
+ destructive={true}
+ onClick={onClick}>
+ {translate('delete')}
+ </ActionsDropdownItem>
+ )}
+ </ConfirmButton>
+ </ActionsDropdown>
+ )}
+ </td>
+ </tr>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx
new file mode 100644
index 00000000000..578b79d356d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 EditGroup from '../EditGroup';
+
+it('should edit group', () => {
+ const group = { id: 3, name: 'Foo', membersCount: 5 };
+ const onEdit = jest.fn();
+ const newDescription = 'bla bla';
+ let onClick: any;
+
+ const wrapper = shallow(
+ <EditGroup group={group} onEdit={onEdit}>
+ {props => {
+ ({ onClick } = props);
+ return <button />;
+ }}
+ </EditGroup>
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ onClick();
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+
+ // change name
+ wrapper.find('Form').prop<Function>('onSubmit')({ name: 'Bar', description: newDescription });
+ expect(onEdit).lastCalledWith({ description: newDescription, id: 3, name: 'Bar' });
+
+ // change description
+ wrapper.find('Form').prop<Function>('onSubmit')({
+ name: group.name,
+ description: newDescription
+ });
+ expect(onEdit).lastCalledWith({ description: newDescription, id: group.id });
+
+ wrapper.find('Form').prop<Function>('onClose')();
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/groups/list-footer-view.js b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx
index faa8fab224c..a4a9297d329 100644
--- a/server/sonar-web/src/main/js/apps/groups/list-footer-view.js
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx
@@ -17,35 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Marionette from 'backbone.marionette';
-import Template from './templates/groups-list-footer.hbs';
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import EditMembers from '../EditMembers';
+import { click } from '../../../../helpers/testUtils';
-export default Marionette.ItemView.extend({
- template: Template,
+it('should edit members', () => {
+ const group = { id: 3, name: 'Foo', membersCount: 5 };
+ const onEdit = jest.fn();
- collectionEvents: {
- all: 'render'
- },
+ const wrapper = shallow(<EditMembers group={group} onEdit={onEdit} organization="org" />);
+ expect(wrapper).toMatchSnapshot();
- events: {
- 'click #groups-fetch-more': 'onMoreClick'
- },
+ wrapper.find('ButtonIcon').prop<Function>('onClick')();
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
- onMoreClick(e) {
- e.preventDefault();
- this.fetchMore();
- },
-
- fetchMore() {
- this.collection.fetchMore();
- },
-
- serializeData() {
- return {
- ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
- total: this.collection.total,
- count: this.collection.length,
- more: this.collection.hasMore()
- };
- }
+ click(wrapper.find('button[type="reset"]'));
+ expect(onEdit).toBeCalled();
+ expect(wrapper).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.tsx
new file mode 100644
index 00000000000..b89f7249231
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.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 { shallow } from 'enzyme';
+import Form from '../Form';
+import { change, submit, click } from '../../../../helpers/testUtils';
+
+it('should render form', async () => {
+ const onClose = jest.fn();
+ const onSubmit = jest.fn(() => Promise.resolve());
+ const wrapper = shallow(
+ <Form
+ confirmButtonText="confirmButtonText"
+ header="header"
+ onClose={onClose}
+ onSubmit={onSubmit}
+ />
+ ).dive();
+ expect(wrapper).toMatchSnapshot();
+
+ change(wrapper.find('[name="name"]'), 'foo');
+ change(wrapper.find('[name="description"]'), 'bar');
+ submit(wrapper.find('form'));
+ expect(onSubmit).toBeCalledWith({ description: 'bar', name: 'foo' });
+
+ await new Promise(setImmediate);
+ expect(onClose).toBeCalled();
+
+ onClose.mockClear();
+ click(wrapper.find('button[type="reset"]'));
+ expect(onClose).toBeCalled();
+});
diff --git a/server/sonar-web/src/main/js/apps/groups/form-view.js b/server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx
index a9493bcac8d..a237e8bafec 100644
--- a/server/sonar-web/src/main/js/apps/groups/form-view.js
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx
@@ -17,24 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import ModalForm from '../../components/common/modal-form';
-import Template from './templates/groups-form.hbs';
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Header from '../Header';
+import { click } from '../../../../helpers/testUtils';
-export default ModalForm.extend({
- template: Template,
+it('should create new group', () => {
+ const onCreate = jest.fn(() => Promise.resolve());
+ const wrapper = shallow(<Header loading={false} onCreate={onCreate} />);
+ expect(wrapper).toMatchSnapshot();
- onRender() {
- ModalForm.prototype.onRender.apply(this, arguments);
- this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
- },
+ click(wrapper.find('#groups-create'));
+ expect(wrapper).toMatchSnapshot();
- onDestroy() {
- ModalForm.prototype.onDestroy.apply(this, arguments);
- this.$('[data-toggle="tooltip"]').tooltip('destroy');
- },
-
- onFormSubmit() {
- ModalForm.prototype.onFormSubmit.apply(this, arguments);
- this.sendRequest();
- }
+ wrapper.find('Form').prop<Function>('onSubmit')({ name: 'foo', description: 'bar' });
+ expect(onCreate).toBeCalledWith({ name: 'foo', description: 'bar' });
});
diff --git a/server/sonar-web/src/main/js/apps/groups/header-view.js b/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx
index 42a317140f5..30afe06e472 100644
--- a/server/sonar-web/src/main/js/apps/groups/header-view.js
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx
@@ -17,38 +17,36 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Marionette from 'backbone.marionette';
-import CreateView from './create-view';
-import Template from './templates/groups-header.hbs';
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import List from '../List';
-export default Marionette.ItemView.extend({
- template: Template,
-
- collectionEvents: {
- request: 'showSpinner',
- sync: 'hideSpinner'
- },
-
- events: {
- 'click #groups-create': 'onCreateClick'
- },
-
- showSpinner() {
- this.$('.spinner').removeClass('hidden');
- },
-
- hideSpinner() {
- this.$('.spinner').addClass('hidden');
- },
-
- onCreateClick(e) {
- e.preventDefault();
- this.createGroup();
- },
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
- createGroup() {
- new CreateView({
- collection: this.collection
- }).render();
- }
+it('should not render "Anyone"', () => {
+ expect(
+ shallowRender(false)
+ .find('.js-anyone')
+ .exists()
+ ).toBeFalsy();
});
+
+function shallowRender(showAnyone = true) {
+ const groups = [
+ { id: 1, name: 'sonar-users', description: '', membersCount: 55, default: true },
+ { id: 2, name: 'foo', description: 'foobar', membersCount: 0, default: false },
+ { id: 3, name: 'bar', description: 'barbar', membersCount: 1, default: false }
+ ];
+ return shallow(
+ <List
+ groups={groups}
+ onDelete={jest.fn()}
+ onEdit={jest.fn()}
+ onEditMembers={jest.fn()}
+ organization="org"
+ showAnyone={showAnyone}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx
new file mode 100644
index 00000000000..28c9bb1617a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 ListItem from '../ListItem';
+
+it('should delete group', () => {
+ const group = { id: 3, name: 'Foo', membersCount: 5 };
+ const onDelete = jest.fn();
+ const wrapper = shallow(
+ <ListItem
+ group={group}
+ onDelete={onDelete}
+ onEdit={jest.fn()}
+ onEditMembers={jest.fn()}
+ organization="org"
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(onDelete).toBeCalledWith('Foo');
+});
+
+it('should render default group', () => {
+ const group = { default: true, id: 3, name: 'Foo', membersCount: 5 };
+ const wrapper = shallow(
+ <ListItem
+ group={group}
+ onDelete={jest.fn()}
+ onEdit={jest.fn()}
+ onEditMembers={jest.fn()}
+ organization="org"
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap
new file mode 100644
index 00000000000..8b54df832b3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should edit group 1`] = `
+<React.Fragment>
+ <button />
+</React.Fragment>
+`;
+
+exports[`should edit group 2`] = `
+<React.Fragment>
+ <button />
+ <Form
+ confirmButtonText="update_verb"
+ group={
+ Object {
+ "id": 3,
+ "membersCount": 5,
+ "name": "Foo",
+ }
+ }
+ header="groups.update_group"
+ onClose={[Function]}
+ onSubmit={[Function]}
+ />
+</React.Fragment>
+`;
+
+exports[`should edit group 3`] = `
+<React.Fragment>
+ <button />
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap
new file mode 100644
index 00000000000..8c846140be8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should edit members 1`] = `
+<React.Fragment>
+ <ButtonIcon
+ className="button-small"
+ onClick={[Function]}
+ >
+ <BulletListIcon />
+ </ButtonIcon>
+</React.Fragment>
+`;
+
+exports[`should edit members 2`] = `
+<React.Fragment>
+ <ButtonIcon
+ className="button-small"
+ onClick={[Function]}
+ >
+ <BulletListIcon />
+ </ButtonIcon>
+ <Modal
+ contentLabel="users.update"
+ onRequestClose={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ users.update
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ >
+ <div
+ id="groups-users"
+ />
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="reset"
+ >
+ Done
+ </button>
+ </footer>
+ </Modal>
+</React.Fragment>
+`;
+
+exports[`should edit members 3`] = `
+<React.Fragment>
+ <ButtonIcon
+ className="button-small"
+ onClick={[Function]}
+ >
+ <BulletListIcon />
+ </ButtonIcon>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap
new file mode 100644
index 00000000000..e43777b754e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap
@@ -0,0 +1,86 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render form 1`] = `
+<Modal
+ contentLabel="header"
+ onRequestClose={[MockFunction]}
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ header
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <label
+ htmlFor="create-group-name"
+ >
+ name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoFocus={true}
+ id="create-group-name"
+ maxLength={255}
+ name="name"
+ onChange={[Function]}
+ required={true}
+ size={50}
+ type="text"
+ value=""
+ />
+ </div>
+ <div
+ className="modal-field"
+ >
+ <label
+ htmlFor="create-group-description"
+ >
+ description
+ </label>
+ <textarea
+ id="create-group-description"
+ name="description"
+ onChange={[Function]}
+ value=""
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ timeout={100}
+ />
+ <button
+ disabled={false}
+ type="submit"
+ >
+ confirmButtonText
+ </button>
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="reset"
+ >
+ cancel
+ </button>
+ </footer>
+ </form>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Header-test.tsx.snap
new file mode 100644
index 00000000000..6578cf61fe8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Header-test.tsx.snap
@@ -0,0 +1,75 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should create new group 1`] = `
+<React.Fragment>
+ <header
+ className="page-header"
+ id="groups-header"
+ >
+ <h1
+ className="page-title"
+ >
+ user_groups.page
+ </h1>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <div
+ className="page-actions"
+ >
+ <button
+ id="groups-create"
+ onClick={[Function]}
+ >
+ groups.create_group
+ </button>
+ </div>
+ <p
+ className="page-description"
+ >
+ user_groups.page.description
+ </p>
+ </header>
+</React.Fragment>
+`;
+
+exports[`should create new group 2`] = `
+<React.Fragment>
+ <header
+ className="page-header"
+ id="groups-header"
+ >
+ <h1
+ className="page-title"
+ >
+ user_groups.page
+ </h1>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <div
+ className="page-actions"
+ >
+ <button
+ id="groups-create"
+ onClick={[Function]}
+ >
+ groups.create_group
+ </button>
+ </div>
+ <p
+ className="page-description"
+ >
+ user_groups.page.description
+ </p>
+ </header>
+ <Form
+ confirmButtonText="create"
+ header="groups.create_group"
+ onClose={[Function]}
+ onSubmit={[Function]}
+ />
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap
new file mode 100644
index 00000000000..a600ab849b1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap
@@ -0,0 +1,106 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="boxed-group boxed-group-inner"
+>
+ <table
+ className="data zebra zebra-hover"
+ id="groups-list"
+ >
+ <thead>
+ <tr>
+ <th />
+ <th
+ className="nowrap"
+ >
+ members
+ </th>
+ <th
+ className="nowrap"
+ >
+ description
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ className="js-anyone"
+ key="anyone"
+ >
+ <td
+ className="width-20"
+ >
+ <strong
+ className="js-group-name"
+ >
+ groups.anyone
+ </strong>
+ </td>
+ <td
+ className="width-10"
+ />
+ <td
+ className="width-40"
+ colSpan={2}
+ >
+ <span
+ className="js-group-description"
+ >
+ user_groups.anyone.description
+ </span>
+ </td>
+ </tr>
+ <ListItem
+ group={
+ Object {
+ "default": false,
+ "description": "barbar",
+ "id": 3,
+ "membersCount": 1,
+ "name": "bar",
+ }
+ }
+ key="3"
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ onEditMembers={[MockFunction]}
+ organization="org"
+ />
+ <ListItem
+ group={
+ Object {
+ "default": false,
+ "description": "foobar",
+ "id": 2,
+ "membersCount": 0,
+ "name": "foo",
+ }
+ }
+ key="2"
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ onEditMembers={[MockFunction]}
+ organization="org"
+ />
+ <ListItem
+ group={
+ Object {
+ "default": true,
+ "description": "",
+ "id": 1,
+ "membersCount": 55,
+ "name": "sonar-users",
+ }
+ }
+ key="1"
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ onEditMembers={[MockFunction]}
+ organization="org"
+ />
+ </tbody>
+ </table>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap
new file mode 100644
index 00000000000..2146de899da
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap
@@ -0,0 +1,118 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should delete group 1`] = `
+<tr
+ data-id={3}
+>
+ <td
+ className=" width-20"
+ >
+ <strong
+ className="js-group-name"
+ >
+ Foo
+ </strong>
+ </td>
+ <td
+ className="width-10"
+ >
+ <div
+ className="display-flex-center"
+ >
+ <span
+ className="spacer-right"
+ >
+ 5
+ </span>
+ <EditMembers
+ group={
+ Object {
+ "id": 3,
+ "membersCount": 5,
+ "name": "Foo",
+ }
+ }
+ onEdit={[MockFunction]}
+ organization="org"
+ />
+ </div>
+ </td>
+ <td
+ className="width-40"
+ >
+ <span
+ className="js-group-description"
+ />
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <ActionsDropdown>
+ <EditGroup
+ group={
+ Object {
+ "id": 3,
+ "membersCount": 5,
+ "name": "Foo",
+ }
+ }
+ onEdit={[MockFunction]}
+ />
+ <ActionsDropdownDivider />
+ <ConfirmButton
+ confirmButtonText="delete"
+ isDestructive={true}
+ modalBody="groups.delete_group.confirmation.Foo"
+ modalHeader="groups.delete_group"
+ onConfirm={[Function]}
+ />
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
+
+exports[`should render default group 1`] = `
+<tr
+ data-id={3}
+>
+ <td
+ className=" width-20"
+ >
+ <strong
+ className="js-group-name"
+ >
+ Foo
+ </strong>
+ <span
+ className="little-spacer-left"
+ >
+ (
+ default
+ )
+ </span>
+ </td>
+ <td
+ className="width-10"
+ >
+ <div
+ className="display-flex-center"
+ >
+ <span
+ className="spacer-right"
+ >
+ 5
+ </span>
+ </div>
+ </td>
+ <td
+ className="width-40"
+ >
+ <span
+ className="js-group-description"
+ />
+ </td>
+ <td
+ className="thin nowrap text-right"
+ />
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/groups/create-view.js b/server/sonar-web/src/main/js/apps/groups/create-view.js
deleted file mode 100644
index dbd5978633b..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/create-view.js
+++ /dev/null
@@ -1,48 +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 Group from './group';
-import FormView from './form-view';
-
-export default FormView.extend({
- sendRequest() {
- const that = this;
- const group = new Group({
- name: this.$('#create-group-name').val(),
- description: this.$('#create-group-description').val()
- });
- this.disableForm();
- return group
- .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/delete-view.js b/server/sonar-web/src/main/js/apps/groups/delete-view.js
deleted file mode 100644
index 61d1031fa1c..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/delete-view.js
+++ /dev/null
@@ -1,57 +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 ModalForm from '../../components/common/modal-form';
-import Template from './templates/groups-delete.hbs';
-
-export default ModalForm.extend({
- template: Template,
-
- onFormSubmit() {
- ModalForm.prototype.onFormSubmit.apply(this, arguments);
- this.sendRequest();
- },
-
- sendRequest() {
- const that = this;
- const collection = this.model.collection;
- return this.model
- .destroy({
- organization: collection.organization,
- wait: true,
- statusCode: {
- // do not show global error
- 400: null
- }
- })
- .done(() => {
- collection.total--;
- that.destroy();
- })
- .fail(jqXHR => {
- that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
- });
- },
-
- showErrors(errors, warnings) {
- this.$('.js-modal-text').addClass('hidden');
- this.disableForm();
- ModalForm.prototype.showErrors.call(this, errors, warnings);
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/groups/group.js b/server/sonar-web/src/main/js/apps/groups/group.js
deleted file mode 100644
index 9aa93b59deb..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/group.js
+++ /dev/null
@@ -1,62 +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 { defaults, pick } from 'lodash';
-import Backbone from 'backbone';
-
-export default Backbone.Model.extend({
- urlRoot() {
- return window.baseUrl + '/api/user_groups';
- },
-
- sync(method, model, options) {
- const opts = options || {};
- if (method === 'create') {
- const data = pick(model.toJSON(), 'name', 'description');
- if (options.organization) {
- Object.assign(data, { organization: options.organization.key });
- }
- defaults(opts, {
- url: this.urlRoot() + '/create',
- type: 'POST',
- data
- });
- }
- if (method === 'update') {
- const data = {
- ...pick(model.changed, 'name', 'description'),
- id: model.id
- };
- defaults(opts, {
- url: this.urlRoot() + '/update',
- type: 'POST',
- data
- });
- }
- if (method === 'delete') {
- const data = { id: this.id };
- defaults(opts, {
- url: this.urlRoot() + '/delete',
- type: 'POST',
- data
- });
- }
- return Backbone.ajax(opts);
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/groups/groups.js b/server/sonar-web/src/main/js/apps/groups/groups.js
deleted file mode 100644
index ccdb24e2ae4..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/groups.js
+++ /dev/null
@@ -1,65 +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 Backbone from 'backbone';
-import Group from './group';
-
-export default Backbone.Collection.extend({
- model: Group,
-
- initialize({ organization }) {
- this.organization = organization;
- },
-
- url() {
- return window.baseUrl + '/api/user_groups/search';
- },
-
- parse(r) {
- this.total = +r.paging.total;
- this.p = +r.paging.pageIndex;
- this.ps = +r.paging.pageSize;
- return r.groups;
- },
-
- fetch(options) {
- const data = (options && options.data) || {};
- this.q = data.q;
- const finalOptions = this.organization
- ? {
- ...options,
- data: { ...data, organization: this.organization.key }
- }
- : options;
- return Backbone.Collection.prototype.fetch.call(this, finalOptions);
- },
-
- fetchMore() {
- const p = this.p + 1;
- return this.fetch({ add: true, remove: false, data: { p, ps: this.ps, q: this.q } });
- },
-
- refresh() {
- return this.fetch({ reset: true, data: { q: this.q } });
- },
-
- hasMore() {
- return this.total > this.p * this.ps;
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/groups/init.js b/server/sonar-web/src/main/js/apps/groups/init.js
deleted file mode 100644
index 26321f42656..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/init.js
+++ /dev/null
@@ -1,63 +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 Marionette from 'backbone.marionette';
-import Layout from './layout';
-import Groups from './groups';
-import HeaderView from './header-view';
-import SearchView from './search-view';
-import ListView from './list-view';
-import ListFooterView from './list-footer-view';
-
-const App = new Marionette.Application();
-const init = function({ el, organization }) {
- // Layout
- this.layout = new Layout({ el });
- this.layout.render();
-
- // Collection
- this.groups = new Groups({ organization });
-
- // Header View
- this.headerView = new HeaderView({ collection: this.groups });
- this.layout.headerRegion.show(this.headerView);
-
- // Search View
- this.searchView = new SearchView({ collection: this.groups });
- this.layout.searchRegion.show(this.searchView);
-
- // List View
- this.listView = new ListView({ collection: this.groups });
- this.layout.listRegion.show(this.listView);
-
- // List Footer View
- this.listFooterView = new ListFooterView({ collection: this.groups });
- this.layout.listFooterRegion.show(this.listFooterView);
-
- // Go!
- this.groups.fetch();
-};
-
-App.on('start', options => {
- init.call(App, options);
-});
-
-export default function(el, organization) {
- App.start({ el, organization });
-}
diff --git a/server/sonar-web/src/main/js/apps/groups/layout.js b/server/sonar-web/src/main/js/apps/groups/layout.js
deleted file mode 100644
index a6fe01225c2..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/layout.js
+++ /dev/null
@@ -1,32 +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 Marionette from 'backbone.marionette';
-import Template from './templates/groups-layout.hbs';
-
-export default Marionette.LayoutView.extend({
- template: Template,
-
- regions: {
- headerRegion: '#groups-header',
- searchRegion: '#groups-search',
- listRegion: '#groups-list',
- listFooterRegion: '#groups-list-footer'
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/groups/list-item-view.js b/server/sonar-web/src/main/js/apps/groups/list-item-view.js
deleted file mode 100644
index 55a76e50de8..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/list-item-view.js
+++ /dev/null
@@ -1,83 +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 $ from 'jquery';
-import Marionette from 'backbone.marionette';
-import UpdateView from './update-view';
-import DeleteView from './delete-view';
-import UsersView from './users-view';
-import Template from './templates/groups-list-item.hbs';
-
-export default Marionette.ItemView.extend({
- tagName: 'li',
- className: 'panel panel-vertical',
- template: Template,
-
- events: {
- 'click .js-group-update': 'onUpdateClick',
- 'click .js-group-delete': 'onDeleteClick',
- 'click .js-group-users': 'onUsersClick'
- },
-
- onRender() {
- this.$el.attr('data-id', this.model.id);
- this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
- },
-
- onDestroy() {
- this.$('[data-toggle="tooltip"]').tooltip('destroy');
- },
-
- onUpdateClick(e) {
- e.preventDefault();
- if (!this.model.get('default')) {
- this.updateGroup();
- }
- },
-
- onDeleteClick(e) {
- e.preventDefault();
- if (!this.model.get('default')) {
- this.deleteGroup();
- }
- },
-
- onUsersClick(e) {
- e.preventDefault();
- $('.tooltip').remove();
- if (!this.model.get('default')) {
- this.showUsers();
- }
- },
-
- updateGroup() {
- new UpdateView({
- model: this.model,
- collection: this.model.collection
- }).render();
- },
-
- deleteGroup() {
- new DeleteView({ model: this.model }).render();
- },
-
- showUsers() {
- new UsersView({ model: this.model, organization: this.model.collection.organization }).render();
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/groups/list-view.js b/server/sonar-web/src/main/js/apps/groups/list-view.js
deleted file mode 100644
index c5c7a99360c..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/list-view.js
+++ /dev/null
@@ -1,53 +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 Marionette from 'backbone.marionette';
-import ListItemView from './list-item-view';
-import Template from './templates/groups-list.hbs';
-
-export default Marionette.CompositeView.extend({
- childView: ListItemView,
- childViewContainer: '.js-list',
- template: Template,
-
- collectionEvents: {
- request: 'showLoading',
- sync: 'hideLoading'
- },
-
- showLoading() {
- this.$el.addClass('new-loading');
- },
-
- hideLoading() {
- this.$el.removeClass('new-loading');
-
- const query = this.collection.q || '';
- const shouldHideAnyone =
- this.collection.organization || !'anyone'.includes(query.toLowerCase());
- this.$('.js-anyone').toggleClass('hidden', shouldHideAnyone);
- },
-
- serializeData() {
- return {
- ...Marionette.CompositeView.prototype.serializeData.apply(this, arguments),
- organization: this.collection.organization
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/groups/routes.ts b/server/sonar-web/src/main/js/apps/groups/routes.ts
index 6222c04d6e0..88584bbe9df 100644
--- a/server/sonar-web/src/main/js/apps/groups/routes.ts
+++ b/server/sonar-web/src/main/js/apps/groups/routes.ts
@@ -23,10 +23,10 @@ const routes = [
{
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
Promise.all([
- import('./components/GroupsAppContainer').then(i => i.default),
+ import('./components/App').then(i => i.default),
import('../organizations/forSingleOrganization').then(i => i.default)
- ]).then(([GroupsAppContainer, forSingleOrganization]) =>
- callback(null, { component: forSingleOrganization(GroupsAppContainer) })
+ ]).then(([App, forSingleOrganization]) =>
+ callback(null, { component: forSingleOrganization(App) })
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/groups/search-view.js b/server/sonar-web/src/main/js/apps/groups/search-view.js
deleted file mode 100644
index acd2338f6e9..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/search-view.js
+++ /dev/null
@@ -1,86 +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 { debounce } from 'lodash';
-import Marionette from 'backbone.marionette';
-import Template from './templates/groups-search.hbs';
-
-export default Marionette.ItemView.extend({
- template: Template,
-
- ui: {
- reset: '.js-reset'
- },
-
- events: {
- 'submit #groups-search-form': 'onFormSubmit',
- 'search #groups-search-query': 'initialOnKeyUp',
- 'keyup #groups-search-query': 'initialOnKeyUp',
- 'click .js-reset': 'onResetClick'
- },
-
- initialize() {
- this._bufferedValue = null;
- this.debouncedOnKeyUp = debounce(this.onKeyUp, 400);
- },
-
- onRender() {
- this.delegateEvents();
- },
-
- onFormSubmit(e) {
- e.preventDefault();
- this.debouncedOnKeyUp();
- },
-
- initialOnKeyUp() {
- const q = this.getQuery();
- this.ui.reset.toggleClass('hidden', q.length === 0);
- this.debouncedOnKeyUp();
- },
-
- onKeyUp() {
- const q = this.getQuery();
- if (q === this._bufferedValue) {
- return;
- }
- this._bufferedValue = this.getQuery();
- if (this.searchRequest != null) {
- this.searchRequest.abort();
- }
- this.searchRequest = this.search(q);
- },
-
- getQuery() {
- return this.$('#groups-search-query').val();
- },
-
- search(q) {
- return this.collection.fetch({ reset: true, data: { q } });
- },
-
- onResetClick(e) {
- e.preventDefault();
- e.currentTarget.blur();
- this.$('#groups-search-query')
- .val('')
- .focus();
- this.onKeyUp();
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs
deleted file mode 100644
index 00d92c0a983..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-<form id="delete-group-form" autocomplete="off">
- <div class="modal-head">
- <h2>{{t 'groups.delete_group'}}</h2>
- </div>
- <div class="modal-body">
- <div class="js-modal-messages"></div>
- <div class="js-modal-text">{{tp 'groups.delete_group.confirmation' name}}</div>
- </div>
- <div class="modal-foot">
- <button id="delete-group-submit">{{t 'delete'}}</button>
- <a href="#" class="js-modal-close" id="delete-group-cancel">{{t 'cancel'}}</a>
- </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs
deleted file mode 100644
index c31e8598cd7..00000000000
--- a/server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs
+++ /dev/null
@@ -1,24 +0,0 @@
-<form id="create-group-form" autocomplete="off">
- <div class="modal-head">
- <h2>{{#if id}}{{t 'groups.update_group'}}{{else}}{{t 'groups.create_group'}}{{/if}}</h2>
- </div>
- <div class="modal-body">
- <div class="js-modal-messages"></div>
- <div class="modal-field">
- <label for="create-group-name">{{t 'name'}}<em class="mandatory">*</em></label>
- {{! keep this fake field to hack browser autofill }}
- <input id="create-group-name-fake" name="name-fake" type="text" class="hidden">
- <input id="create-group-name" name="name" type="text" size="50" maxlength="255" required value="{{name}}">
- </div>
- <div class="modal-field">
- <label for="create-group-description">{{t 'description'}}</label>
- {{! keep this fake field to hack browser autofill }}
- <textarea id="create-group-description-fake" name="description-fake" class="hidden"></textarea>
- <textarea id="create-group-description" name="description">{{description}}</textarea>
- </div>
- </div>
- <div class="modal-foot">
- <button id="create-group-submit">{{#if id}}{{t 'update_verb'}}{{else}}{{t 'create'}}{{/if}}</button>
- <a href="#" class="js-modal-close" id="create-group-cancel">{{t 'cancel'}}</a>
- </div>
-</form>
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 @@
-<header class="page-header">
- <h1 class="page-title">{{t 'user_groups.page'}}</h1>
- <i class="spinner hidden"></i>
- <div class="page-actions">
- <button id="groups-create">{{t 'groups.create_group'}}</button>
- </div>
- <p class="page-description">{{t 'user_groups.page.description'}}</p>
-</header>
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 @@
-<div class="page page-limited">
- <div id="groups-header"></div>
- <div id="groups-search"></div>
- <div id="groups-list"></div>
- <div id="groups-list-footer"></div>
-</div>
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 @@
-<footer class="spacer-top note text-center">
- {{tp 'x_of_y_shown' count total}}
- {{#if more}}
- <a id="groups-fetch-more" class="spacer-left" href="#">{{t 'show_more'}}</a>
- {{/if}}
-</footer>
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 @@
-<div class="pull-right big-spacer-left nowrap">
- {{#unless default}}
- <div class="dropdown">
- <button class="dropdown-toggle" data-toggle="dropdown">
- {{settingsIcon}}<i class="icon-dropdown little-spacer-left" />
- </button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li>
- <a class="js-group-update" href="#">{{t 'update_details'}}</a>
- </li>
- <li class="divider" />
- <li>
- <a class="js-group-delete text-danger" href="#">{{t 'delete'}}</a>
- </li>
- </ul>
- </div>
- {{/unless}}
-</div>
-
-<div class="display-inline-block text-top width-20">
- <strong class="js-group-name">{{name}}</strong>
- {{#if default}}
- <span class="little-spacer-left">({{t 'default'}})</span>
- {{/if}}
-</div>
-
-<div class="display-inline-block text-top big-spacer-left width-25">
- <div class="pull-left spacer-right">
- <strong>{{t 'members'}}</strong>
- </div>
- <div class="overflow-hidden bordered-left">
- <span class="spacer-left spacer-right">{{membersCount}}</span>
- {{#unless default}}
- <a class="js-group-users icon-bullet-list" title="{{t 'users.update'}}" data-toggle="tooltip" href="#"></a>
- {{/unless}}
- </div>
-</div>
-
-<div class="display-inline-block text-top big-spacer-left width-40">
- <span class="js-group-description">{{description}}</span>
-</div>
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 @@
-<div class="boxed-group boxed-group-inner">
- {{#isNull organization}}
- <div class="panel panel-vertical js-anyone">
- <div class="display-inline-block text-top width-20">
- <strong class="js-group-name">{{t 'groups.anyone'}}</strong>
- </div>
-
- <div class="display-inline-block text-top big-spacer-left width-25">
-
- </div>
-
- <div class="display-inline-block text-top big-spacer-left width-40">
- <span class="js-group-description">{{t 'user_groups.anyone.description'}}</span>
- </div>
- </div>
- {{/isNull}}
-
- <ul class="js-list"></ul>
-</div>
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 @@
-<div class="big-spacer-bottom">
- <form id="groups-search-form" class="search-box">
- <input id="groups-search-query" class="search-box-input" type="text" name="q" placeholder="{{t 'search.search_by_name'}}" maxlength="100">
- <svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
- <g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
- <path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
- </g>
- </svg>
- <button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
- <svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
- <path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
- </svg>
- </button>
- </form>
-</div>
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 @@
-<div class="modal-head">
- <h2>{{t 'users.update'}}</h2>
-</div>
-<div class="modal-body">
- <div class="js-modal-messages"></div>
- <div id="groups-users"></div>
-</div>
-<div class="modal-foot">
- <a href="#" class="js-modal-close" id="groups-users-done">{{t 'Done'}}</a>
-</div>
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)}<br><span class="note">${escapeHtml(item.login)}</span>`;
- },
- 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 (
- <div>
- <Helmet title={translate('global_permissions.groups')} />
- <div ref="container" />
- </div>
- );
- }
-}
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
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<HTMLButtonElement>) => void }
+ props: { onClick: (event?: React.SyntheticEvent<HTMLButtonElement>) => 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<Props, State> {
+ mounted: boolean;
state: State = { modal: false };
- handleButtonClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
- event.preventDefault();
- event.currentTarget.blur();
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleButtonClick = (event?: React.SyntheticEvent<HTMLButtonElement>) => {
+ 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<Props, State> {
<div className="modal-body">{modalBody}</div>
<footer className="modal-foot">
- {submitting && <i className="spinner spacer-right" />}
+ <DeferredSpinner className="spacer-right" loading={submitting} />
<button
className={isDestructive ? 'button-red' : undefined}
disabled={submitting}
diff --git a/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx b/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx
index 23769377d7b..1359c584e8a 100644
--- a/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx
+++ b/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx
@@ -22,6 +22,7 @@ import Modal from '../../components/controls/Modal';
export interface ChildrenProps {
onCloseClick: (event: React.SyntheticEvent<HTMLElement>) => void;
+ onFormSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void;
onSubmitClick: (event: React.SyntheticEvent<HTMLElement>) => void;
submitting: boolean;
}
@@ -61,9 +62,18 @@ export default class SimpleModal extends React.PureComponent<Props, State> {
this.props.onClose();
};
+ handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ this.submit();
+ };
+
handleSubmitClick = (event: React.SyntheticEvent<HTMLElement>) => {
event.preventDefault();
event.currentTarget.blur();
+ this.submit();
+ };
+
+ submit = () => {
const result = this.props.onSubmit();
if (result) {
this.setState({ submitting: true });
@@ -76,6 +86,7 @@ export default class SimpleModal extends React.PureComponent<Props, State> {
<Modal contentLabel={this.props.header} onRequestClose={this.props.onClose}>
{this.props.children({
onCloseClick: this.handleCloseClick,
+ onFormSubmit: this.handleFormSubmit,
onSubmitClick: this.handleSubmitClick,
submitting: this.state.submitting
})}