* 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;
p?: number;
ps?: number;
q?: string;
-}) {
+}): Promise<{ groups: Group[]; paging: Paging }> {
return getJSON('/api/user_groups/search', 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);
+}
Inherited = 'INHERITED',
Overridden = 'OVERRIDES'
}
+
+export interface Group {
+ default?: boolean;
+ description?: string;
+ id: number;
+ membersCount: number;
+ name: string;
+}
+++ /dev/null
-/*
- * 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>
- )}
- </>
- );
- }
-}
* 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';
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 {
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';
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';
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';
--- /dev/null
+/*
+ * 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>
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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}
+ />
+ )}
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ )}
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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>
- );
- }
-}
--- /dev/null
+/*
+ * 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}
+ />
+ )}
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+/*
+ * 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' });
+});
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
+++ /dev/null
-/*
- * 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);
- });
- }
-});
+++ /dev/null
-/*
- * 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);
- }
-});
+++ /dev/null
-/*
- * 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();
- }
-});
+++ /dev/null
-/*
- * 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);
- }
-});
+++ /dev/null
-/*
- * 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;
- }
-});
+++ /dev/null
-/*
- * 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();
- }
-});
+++ /dev/null
-/*
- * 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 });
-}
+++ /dev/null
-/*
- * 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'
- }
-});
+++ /dev/null
-/*
- * 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()
- };
- }
-});
+++ /dev/null
-/*
- * 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();
- }
-});
+++ /dev/null
-/*
- * 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
- };
- }
-});
{
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) })
);
}
}
+++ /dev/null
-/*
- * 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();
- }
-});
+++ /dev/null
-<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>
+++ /dev/null
-<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>
+++ /dev/null
-<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>
+++ /dev/null
-<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>
+++ /dev/null
-<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>
+++ /dev/null
-<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>
+++ /dev/null
-<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>
+++ /dev/null
-<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>
+++ /dev/null
-<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>
+++ /dev/null
-/*
- * 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);
- });
- }
-});
+++ /dev/null
-/*
- * 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);
- }
-});
+++ /dev/null
-/*
- * 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>
- );
- }
-}
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';
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 = [
{
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 }
--- /dev/null
+/*
+ * 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>
+ )}
+ </>
+ );
+ }
+}
export interface ChildrenProps {
onCloseClick: (event: React.SyntheticEvent<HTMLElement>) => void;
+ onFormSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void;
onSubmitClick: (event: React.SyntheticEvent<HTMLElement>) => void;
submitting: boolean;
}
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 });
<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
})}