]> source.dussan.org Git - sonarqube.git/commitdiff
rewrite groups app with react (#3017)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 6 Feb 2018 15:11:41 +0000 (16:11 +0100)
committerGitHub <noreply@github.com>
Tue, 6 Feb 2018 15:11:41 +0000 (16:11 +0100)
55 files changed:
server/sonar-web/src/main/js/api/user_groups.ts
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx
server/sonar-web/src/main/js/apps/groups/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/EditGroup.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/Form.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/groups/components/Header.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/List.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/EditGroup-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditGroup-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Header-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/create-view.js [deleted file]
server/sonar-web/src/main/js/apps/groups/delete-view.js [deleted file]
server/sonar-web/src/main/js/apps/groups/form-view.js [deleted file]
server/sonar-web/src/main/js/apps/groups/group.js [deleted file]
server/sonar-web/src/main/js/apps/groups/groups.js [deleted file]
server/sonar-web/src/main/js/apps/groups/header-view.js [deleted file]
server/sonar-web/src/main/js/apps/groups/init.js [deleted file]
server/sonar-web/src/main/js/apps/groups/layout.js [deleted file]
server/sonar-web/src/main/js/apps/groups/list-footer-view.js [deleted file]
server/sonar-web/src/main/js/apps/groups/list-item-view.js [deleted file]
server/sonar-web/src/main/js/apps/groups/list-view.js [deleted file]
server/sonar-web/src/main/js/apps/groups/routes.ts
server/sonar-web/src/main/js/apps/groups/search-view.js [deleted file]
server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs [deleted file]
server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs [deleted file]
server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs [deleted file]
server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs [deleted file]
server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs [deleted file]
server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs [deleted file]
server/sonar-web/src/main/js/apps/groups/templates/groups-list.hbs [deleted file]
server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs [deleted file]
server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs [deleted file]
server/sonar-web/src/main/js/apps/groups/update-view.js [deleted file]
server/sonar-web/src/main/js/apps/groups/users-view.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroups.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/routes.ts
server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/SimpleModal.tsx

index 7fbddde3133c01e21646e673ca5d32a600a03fbe..2e1b85c5cdf2c8c1ef48858695431694b16eed55 100644 (file)
@@ -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);
+}
index ef1f97b8cf14a8407aba4db2456db1f2d5cce59a..f0c37fbe7a386ba9e0fe60de60398e57fee6f25c 100644 (file)
@@ -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/ConfirmButton.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx
deleted file mode 100644 (file)
index 775fe1a..0000000
+++ /dev/null
@@ -1,99 +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 * as React from 'react';
-import SimpleModal from '../../../components/controls/SimpleModal';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  children: (
-    props: { onClick: (event: React.SyntheticEvent<HTMLButtonElement>) => void }
-  ) => React.ReactNode;
-  confirmButtonText: string;
-  confirmData?: string;
-  isDestructive?: boolean;
-  modalBody: React.ReactNode;
-  modalHeader: string;
-  onConfirm: (data?: string) => void | Promise<void>;
-}
-
-interface State {
-  modal: boolean;
-}
-
-// TODO move this component to components/ and use everywhere!
-export default class ConfirmButton extends React.PureComponent<Props, State> {
-  state: State = { modal: false };
-
-  handleButtonClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.setState({ modal: true });
-  };
-
-  handleSubmit = () => {
-    const result = this.props.onConfirm(this.props.confirmData);
-    if (result) {
-      result.then(this.handleCloseModal, () => {});
-    } else {
-      this.handleCloseModal();
-    }
-  };
-
-  handleCloseModal = () => this.setState({ modal: false });
-
-  render() {
-    const { confirmButtonText, isDestructive, modalBody, modalHeader } = this.props;
-
-    return (
-      <>
-        {this.props.children({ onClick: this.handleButtonClick })}
-        {this.state.modal && (
-          <SimpleModal
-            header={modalHeader}
-            onClose={this.handleCloseModal}
-            onSubmit={this.handleSubmit}>
-            {({ onCloseClick, onSubmitClick, submitting }) => (
-              <>
-                <header className="modal-head">
-                  <h2>{modalHeader}</h2>
-                </header>
-
-                <div className="modal-body">{modalBody}</div>
-
-                <footer className="modal-foot">
-                  {submitting && <i className="spinner spacer-right" />}
-                  <button
-                    className={isDestructive ? 'button-red' : undefined}
-                    disabled={submitting}
-                    onClick={onSubmitClick}>
-                    {confirmButtonText}
-                  </button>
-                  <a href="#" onClick={onCloseClick}>
-                    {translate('cancel')}
-                  </a>
-                </footer>
-              </>
-            )}
-          </SimpleModal>
-        )}
-      </>
-    );
-  }
-}
index d9c9007287f5d46f92e6de4dd024dd637cc47795..16a758b4256abe03ee0cb0d2c3d11026123a27f1 100644 (file)
@@ -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 {
index 9d7cd5281670756d0fb0d4c392c681258545ead9..2804fbde9e9eb6c9e8f31da0a14c74ae789f1362 100644 (file)
 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';
index f8dce629e1d7a9417f7e5f0852a76714cb52cb49..162863838656912d2eeca4b830aebdb9f198424d 100644 (file)
@@ -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';
index 2c97ea8e94645ad3a629085e0a6801d2e018de8b..e713523ac8f56e54f876faa5e1d4e459bebf86be 100644 (file)
@@ -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 (file)
index 0000000..5aebe16
--- /dev/null
@@ -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 (file)
index 0000000..8cf6bf7
--- /dev/null
@@ -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 (file)
index 0000000..6ab1d85
--- /dev/null
@@ -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 (file)
index 0000000..23452ce
--- /dev/null
@@ -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 (file)
index 30411e9..0000000
+++ /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 (file)
index 0000000..973ccb9
--- /dev/null
@@ -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 (file)
index 0000000..c4207f1
--- /dev/null
@@ -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 (file)
index 0000000..651066f
--- /dev/null
@@ -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 (file)
index 0000000..578b79d
--- /dev/null
@@ -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/components/__tests__/EditMembers-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx
new file mode 100644 (file)
index 0000000..a4a9297
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 EditMembers from '../EditMembers';
+import { click } from '../../../../helpers/testUtils';
+
+it('should edit members', () => {
+  const group = { id: 3, name: 'Foo', membersCount: 5 };
+  const onEdit = jest.fn();
+
+  const wrapper = shallow(<EditMembers group={group} onEdit={onEdit} organization="org" />);
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('ButtonIcon').prop<Function>('onClick')();
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  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 (file)
index 0000000..b89f724
--- /dev/null
@@ -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/components/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx
new file mode 100644 (file)
index 0000000..a237e8b
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Header from '../Header';
+import { click } from '../../../../helpers/testUtils';
+
+it('should create new group', () => {
+  const onCreate = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(<Header loading={false} onCreate={onCreate} />);
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('#groups-create'));
+  expect(wrapper).toMatchSnapshot();
+
+  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/components/__tests__/List-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx
new file mode 100644 (file)
index 0000000..30afe06
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import List from '../List';
+
+it('should render', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+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 (file)
index 0000000..28c9bb1
--- /dev/null
@@ -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 (file)
index 0000000..8b54df8
--- /dev/null
@@ -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 (file)
index 0000000..8c84614
--- /dev/null
@@ -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 (file)
index 0000000..e43777b
--- /dev/null
@@ -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 (file)
index 0000000..6578cf6
--- /dev/null
@@ -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 (file)
index 0000000..a600ab8
--- /dev/null
@@ -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 (file)
index 0000000..2146de8
--- /dev/null
@@ -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 (file)
index dbd5978..0000000
+++ /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 (file)
index 61d1031..0000000
+++ /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/form-view.js b/server/sonar-web/src/main/js/apps/groups/form-view.js
deleted file mode 100644 (file)
index a9493bc..0000000
+++ /dev/null
@@ -1,40 +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-form.hbs';
-
-export default ModalForm.extend({
-  template: Template,
-
-  onRender() {
-    ModalForm.prototype.onRender.apply(this, arguments);
-    this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
-  },
-
-  onDestroy() {
-    ModalForm.prototype.onDestroy.apply(this, arguments);
-    this.$('[data-toggle="tooltip"]').tooltip('destroy');
-  },
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    this.sendRequest();
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/groups/group.js b/server/sonar-web/src/main/js/apps/groups/group.js
deleted file mode 100644 (file)
index 9aa93b5..0000000
+++ /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 (file)
index ccdb24e..0000000
+++ /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/header-view.js b/server/sonar-web/src/main/js/apps/groups/header-view.js
deleted file mode 100644 (file)
index 42a3171..0000000
+++ /dev/null
@@ -1,54 +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 CreateView from './create-view';
-import Template from './templates/groups-header.hbs';
-
-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();
-  },
-
-  createGroup() {
-    new CreateView({
-      collection: this.collection
-    }).render();
-  }
-});
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 (file)
index 26321f4..0000000
+++ /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 (file)
index a6fe012..0000000
+++ /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-footer-view.js b/server/sonar-web/src/main/js/apps/groups/list-footer-view.js
deleted file mode 100644 (file)
index faa8fab..0000000
+++ /dev/null
@@ -1,51 +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-list-footer.hbs';
-
-export default Marionette.ItemView.extend({
-  template: Template,
-
-  collectionEvents: {
-    all: 'render'
-  },
-
-  events: {
-    'click #groups-fetch-more': 'onMoreClick'
-  },
-
-  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()
-    };
-  }
-});
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 (file)
index 55a76e5..0000000
+++ /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 (file)
index c5c7a99..0000000
+++ /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
-    };
-  }
-});
index 6222c04d6e066c2e74c4a3d9b195cc9ae42b8ea3..88584bbe9df5eb8878ae55bed9f89f464319071a 100644 (file)
@@ -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 (file)
index acd2338..0000000
+++ /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 (file)
index 00d92c0..0000000
+++ /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 (file)
index c31e859..0000000
+++ /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 (file)
index 95ba488..0000000
+++ /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 (file)
index 92fd355..0000000
+++ /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 (file)
index 16d1bde..0000000
+++ /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 (file)
index 6d2600c..0000000
+++ /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 (file)
index ac23f3c..0000000
+++ /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 (file)
index ecf034d..0000000
+++ /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 (file)
index 7f9db58..0000000
+++ /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 (file)
index 76023ba..0000000
+++ /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 (file)
index 980974b..0000000
+++ /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 (file)
index 7005366..0000000
+++ /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>
-    );
-  }
-}
index e6d2aa793adfaae4e388cd5feb79219e6b40f005..6546097f9819df2c0a3dc94c726e153c133f8f6d 100644 (file)
@@ -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/components/controls/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
new file mode 100644 (file)
index 0000000..777c576
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * 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 SimpleModal from './SimpleModal';
+import DeferredSpinner from '../common/DeferredSpinner';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+  children: (
+    props: { onClick: (event?: React.SyntheticEvent<HTMLButtonElement>) => void }
+  ) => React.ReactNode;
+  confirmButtonText: string;
+  confirmData?: string;
+  isDestructive?: boolean;
+  modalBody: React.ReactNode;
+  modalHeader: string;
+  onConfirm: (data?: string) => void | Promise<void>;
+}
+
+interface State {
+  modal: boolean;
+}
+
+export default class ConfirmButton extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { modal: false };
+
+  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) {
+      return result.then(this.handleCloseModal, () => {});
+    } else {
+      this.handleCloseModal();
+      return undefined;
+    }
+  };
+
+  handleCloseModal = () => {
+    if (this.mounted) {
+      this.setState({ modal: false });
+    }
+  };
+
+  render() {
+    const { confirmButtonText, isDestructive, modalBody, modalHeader } = this.props;
+
+    return (
+      <>
+        {this.props.children({ onClick: this.handleButtonClick })}
+        {this.state.modal && (
+          <SimpleModal
+            header={modalHeader}
+            onClose={this.handleCloseModal}
+            onSubmit={this.handleSubmit}>
+            {({ onCloseClick, onSubmitClick, submitting }) => (
+              <>
+                <header className="modal-head">
+                  <h2>{modalHeader}</h2>
+                </header>
+
+                <div className="modal-body">{modalBody}</div>
+
+                <footer className="modal-foot">
+                  <DeferredSpinner className="spacer-right" loading={submitting} />
+                  <button
+                    className={isDestructive ? 'button-red' : undefined}
+                    disabled={submitting}
+                    onClick={onSubmitClick}>
+                    {confirmButtonText}
+                  </button>
+                  <a href="#" onClick={onCloseClick}>
+                    {translate('cancel')}
+                  </a>
+                </footer>
+              </>
+            )}
+          </SimpleModal>
+        )}
+      </>
+    );
+  }
+}
index 23769377d7b054bdabc10e07f7956ce4ede22984..1359c584e8abc3c88f2c9b30010fc7f0ece34549 100644 (file)
@@ -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
         })}