aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/Profile.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx125
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx148
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx150
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx71
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx86
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap118
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap128
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap19
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap87
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap56
-rw-r--r--server/sonar-web/src/main/js/apps/issues/utils.ts13
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap36
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsUser-test.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap14
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserActions.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserForm.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx30
41 files changed, 1398 insertions, 156 deletions
diff --git a/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx b/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx
index b752f8fa8b3..941371b710e 100644
--- a/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx
+++ b/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import UserExternalIdentity from './UserExternalIdentity';
+import UserDeleteAccount from './UserDeleteAccount';
import UserGroups from './UserGroups';
import UserScmAccounts from './UserScmAccounts';
import { isSonarCloud } from '../../../helpers/system';
@@ -59,6 +60,14 @@ export function Profile({ currentUser }: Props) {
<hr />
<UserScmAccounts scmAccounts={currentUser.scmAccounts} user={currentUser} />
+
+ {isSonarCloud() && (
+ <>
+ <hr />
+
+ <UserDeleteAccount user={currentUser} />
+ </>
+ )}
</div>
</div>
);
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx
new file mode 100644
index 00000000000..38351f002b4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { translate } from 'sonar-ui-common/helpers/l10n';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import UserDeleteAccountModal from './UserDeleteAccountModal';
+import UserDeleteAccountContent from './UserDeleteAccountContent';
+import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
+import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations';
+import { getOrganizationsThatPreventDeletion } from '../../../api/organizations';
+
+interface Props {
+ user: T.LoggedInUser;
+ userOrganizations: T.Organization[];
+}
+
+interface State {
+ loading: boolean;
+ organizationsToTransferOrDelete: T.Organization[];
+ showModal: boolean;
+}
+
+export class UserDeleteAccount extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ state: State = {
+ loading: true,
+ organizationsToTransferOrDelete: [],
+ showModal: false
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchOrganizationsThatPreventDeletion();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchOrganizationsThatPreventDeletion = () => {
+ getOrganizationsThatPreventDeletion().then(
+ ({ organizations }) => {
+ if (this.mounted) {
+ this.setState({
+ loading: false,
+ organizationsToTransferOrDelete: organizations
+ });
+ }
+ },
+ () => {}
+ );
+ };
+
+ toggleModal = () => {
+ if (this.mounted) {
+ this.setState(state => ({
+ showModal: !state.showModal
+ }));
+ }
+ };
+
+ render() {
+ const { user, userOrganizations } = this.props;
+ const { organizationsToTransferOrDelete, loading, showModal } = this.state;
+
+ const label = translate('my_profile.delete_account');
+
+ return (
+ <div>
+ <h2 className="spacer-bottom">{label}</h2>
+
+ <DeferredSpinner loading={loading} />
+
+ {!loading && (
+ <>
+ <UserDeleteAccountContent
+ className="list-styled no-padding big-spacer-top big-spacer-bottom"
+ organizationsSafeToDelete={userOrganizations}
+ organizationsToTransferOrDelete={organizationsToTransferOrDelete}
+ />
+
+ <Button
+ className="button-red"
+ disabled={organizationsToTransferOrDelete.length > 0}
+ onClick={this.toggleModal}
+ type="button">
+ {translate('delete')}
+ </Button>
+
+ {showModal && (
+ <UserDeleteAccountModal
+ label={label}
+ organizationsSafeToDelete={userOrganizations}
+ organizationsToTransferOrDelete={organizationsToTransferOrDelete}
+ toggleModal={this.toggleModal}
+ user={user}
+ />
+ )}
+ </>
+ )}
+ </div>
+ );
+ }
+}
+
+export default whenLoggedIn(withUserOrganizations(UserDeleteAccount));
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx
new file mode 100644
index 00000000000..aaad598307b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getOrganizationUrl } from '../../../helpers/urls';
+
+function getOrganizationLink(org: T.Organization, i: number, organizations: T.Organization[]) {
+ return (
+ <span key={org.key}>
+ <Link to={getOrganizationUrl(org.key)}>{org.name}</Link>
+ {i < organizations.length - 1 && ', '}
+ </span>
+ );
+}
+
+export function ShowOrganizationsToTransferOrDelete({
+ organizations
+}: {
+ organizations: T.Organization[];
+}) {
+ return (
+ <>
+ <p className="big-spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('my_profile.delete_account.info.orgs_to_transfer_or_delete')}
+ id="my_profile.delete_account.info.orgs_to_transfer_or_delete"
+ values={{
+ organizations: <>{organizations.map(getOrganizationLink)}</>
+ }}
+ />
+ </p>
+
+ <Alert className="big-spacer-bottom" variant="warning">
+ <FormattedMessage
+ defaultMessage={translate(
+ 'my_profile.delete_account.info.orgs_to_transfer_or_delete.info'
+ )}
+ id="my_profile.delete_account.info.orgs_to_transfer_or_delete.info"
+ values={{
+ link: (
+ <a
+ href="https://sieg.eu.ngrok.io/documentation/organizations/overview/#how-to-transfer-ownership-of-an-organization"
+ rel="noopener noreferrer"
+ target="_blank">
+ {translate('my_profile.delete_account.info.orgs_to_transfer_or_delete.info.link')}
+ </a>
+ )
+ }}
+ />
+ </Alert>
+ </>
+ );
+}
+
+export function ShowOrganizations({
+ className,
+ organizations
+}: {
+ className?: string;
+ organizations: T.Organization[];
+}) {
+ const organizationsIAdministrate = organizations.filter(o => o.actions && o.actions.admin);
+
+ return (
+ <ul className={className}>
+ <li className="spacer-bottom">{translate('my_profile.delete_account.info')}</li>
+
+ <li className="spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('my_profile.delete_account.data.info')}
+ id="my_profile.delete_account.data.info"
+ values={{
+ help: (
+ <a
+ href="/documentation/user-guide/user-account/#delete-your-user-account"
+ rel="noopener noreferrer"
+ target="_blank">
+ {translate('learn_more')}
+ </a>
+ )
+ }}
+ />
+ </li>
+
+ {organizations.length > 0 && (
+ <li className="spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('my_profile.delete_account.info.orgs.members')}
+ id="my_profile.delete_account.info.orgs.members"
+ values={{
+ organizations: <>{organizations.map(getOrganizationLink)}</>
+ }}
+ />
+ </li>
+ )}
+
+ {organizationsIAdministrate.length > 0 && (
+ <li className="spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('my_profile.delete_account.info.orgs.administrators')}
+ id="my_profile.delete_account.info.orgs.administrators"
+ values={{
+ organizations: <>{organizationsIAdministrate.map(getOrganizationLink)}</>
+ }}
+ />
+ </li>
+ )}
+ </ul>
+ );
+}
+
+interface UserDeleteAccountContentProps {
+ className?: string;
+ organizationsSafeToDelete: T.Organization[];
+ organizationsToTransferOrDelete: T.Organization[];
+}
+
+export default function UserDeleteAccountContent({
+ className,
+ organizationsSafeToDelete,
+ organizationsToTransferOrDelete
+}: UserDeleteAccountContentProps) {
+ if (organizationsToTransferOrDelete.length > 0) {
+ return <ShowOrganizationsToTransferOrDelete organizations={organizationsToTransferOrDelete} />;
+ }
+
+ return <ShowOrganizations className={className} organizations={organizationsSafeToDelete} />;
+}
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx
new file mode 100644
index 00000000000..566fad469f6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx
@@ -0,0 +1,150 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormikProps } from 'formik';
+import { connect } from 'react-redux';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import InputValidationField from 'sonar-ui-common/components/controls/InputValidationField';
+import UserDeleteAccountContent from './UserDeleteAccountContent';
+import RecentHistory from '../../../app/components/RecentHistory';
+import ValidationModal from '../../../components/controls/ValidationModal';
+import { deactivateUser } from '../../../api/users';
+import { Router, withRouter } from '../../../components/hoc/withRouter';
+import { doLogout } from '../../../store/rootActions';
+
+interface Values {
+ login: string;
+}
+
+interface DeleteModalProps {
+ doLogout: () => Promise<void>;
+ label: string;
+ organizationsSafeToDelete: T.Organization[];
+ organizationsToTransferOrDelete: T.Organization[];
+ router: Pick<Router, 'push'>;
+ toggleModal: VoidFunction;
+ user: T.LoggedInUser;
+}
+
+export class UserDeleteAccountModal extends React.PureComponent<DeleteModalProps> {
+ handleSubmit = () => {
+ const { user } = this.props;
+
+ return deactivateUser({ login: user.login })
+ .then(this.props.doLogout)
+ .then(() => {
+ RecentHistory.clear();
+ window.location.replace('/account-deleted');
+ });
+ };
+
+ handleValidate = ({ login }: Values) => {
+ const { user } = this.props;
+ const errors: { login?: string } = {};
+ const trimmedLogin = login.trim();
+
+ if (!trimmedLogin) {
+ errors.login = translate('my_profile.delete_account.login.required');
+ } else if (user.externalIdentity && trimmedLogin !== user.externalIdentity.trim()) {
+ errors.login = translate('my_profile.delete_account.login.wrong_value');
+ }
+
+ return errors;
+ };
+
+ render() {
+ const {
+ label,
+ organizationsSafeToDelete,
+ organizationsToTransferOrDelete,
+ toggleModal,
+ user
+ } = this.props;
+
+ return (
+ <ValidationModal
+ confirmButtonText={translate('delete')}
+ header={translateWithParameters(
+ 'my_profile.delete_account.modal.header',
+ label,
+ user.externalIdentity || ''
+ )}
+ initialValues={{
+ login: ''
+ }}
+ isDestructive={true}
+ onClose={toggleModal}
+ onSubmit={this.handleSubmit}
+ validate={this.handleValidate}>
+ {({
+ dirty,
+ errors,
+ handleBlur,
+ handleChange,
+ isSubmitting,
+ touched,
+ values
+ }: FormikProps<Values>) => (
+ <>
+ <Alert className="big-spacer-bottom" variant="error">
+ {translate('my_profile.warning_message')}
+ </Alert>
+
+ <UserDeleteAccountContent
+ className="list-styled no-padding big-spacer-bottom"
+ organizationsSafeToDelete={organizationsSafeToDelete}
+ organizationsToTransferOrDelete={organizationsToTransferOrDelete}
+ />
+
+ <InputValidationField
+ autoFocus={true}
+ dirty={dirty}
+ disabled={isSubmitting}
+ error={errors.login}
+ id="user-login"
+ label={
+ <label htmlFor="user-login">
+ {translate('my_profile.delete_account.verify')}
+ <em className="mandatory">*</em>
+ </label>
+ }
+ name="login"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ touched={touched.login}
+ type="text"
+ value={values.login}
+ />
+ </>
+ )}
+ </ValidationModal>
+ );
+ }
+}
+
+const mapStateToProps = () => ({});
+
+const mapDispatchToProps = { doLogout: doLogout as any };
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(withRouter(UserDeleteAccountModal));
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx
new file mode 100644
index 00000000000..1e4d4b217e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { mockLoggedInUser, mockOrganization } from '../../../../helpers/testMocks';
+import { UserDeleteAccount } from '../UserDeleteAccount';
+import { getOrganizationsThatPreventDeletion } from '../../../../api/organizations';
+
+jest.mock('../../../../api/organizations', () => ({
+ getOrganizationsThatPreventDeletion: jest.fn().mockResolvedValue({ organizations: [] })
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+const organizationToTransferOrDelete = {
+ key: 'luke-leia',
+ name: 'Luke and Leia'
+};
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+
+ click(wrapper.find('Button'));
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should get some organizations', async () => {
+ (getOrganizationsThatPreventDeletion as jest.Mock).mockResolvedValue({
+ organizations: [organizationToTransferOrDelete]
+ });
+
+ const wrapper = shallowRender();
+
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state('loading')).toBeFalsy();
+ expect(wrapper.state('organizationsToTransferOrDelete')).toEqual([
+ organizationToTransferOrDelete
+ ]);
+ expect(getOrganizationsThatPreventDeletion).toBeCalled();
+ expect(wrapper.find('Button').prop('disabled')).toBe(true);
+});
+
+it('should toggle modal', () => {
+ const wrapper = shallowRender();
+ wrapper.setState({ loading: false });
+ expect(wrapper.find('Connect(withRouter(UserDeleteAccountModal))').exists()).toBe(false);
+ click(wrapper.find('Button'));
+ expect(wrapper.find('Connect(withRouter(UserDeleteAccountModal))').exists()).toBe(true);
+});
+
+function shallowRender(props: Partial<UserDeleteAccount['props']> = {}) {
+ const user = mockLoggedInUser({ externalIdentity: 'luke' });
+
+ const userOrganizations = [
+ mockOrganization({ key: 'luke-leia', name: 'Luke and Leia' }),
+ mockOrganization({ key: 'luke', name: 'Luke Skywalker' })
+ ];
+
+ return shallow<UserDeleteAccount>(
+ <UserDeleteAccount user={user} userOrganizations={userOrganizations} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx
new file mode 100644
index 00000000000..c9e5e334d5e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 UserDeleteAccountContent, {
+ ShowOrganizations,
+ ShowOrganizationsToTransferOrDelete
+} from '../UserDeleteAccountContent';
+
+const organizationSafeToDelete = {
+ key: 'luke',
+ name: 'Luke Skywalker'
+};
+
+const organizationToTransferOrDelete = {
+ key: 'luke-leia',
+ name: 'Luke and Leia'
+};
+
+it('should render content correctly', () => {
+ expect(
+ shallow(
+ <UserDeleteAccountContent
+ className="my-class"
+ organizationsSafeToDelete={[organizationSafeToDelete]}
+ organizationsToTransferOrDelete={[organizationToTransferOrDelete]}
+ />
+ )
+ ).toMatchSnapshot();
+
+ expect(
+ shallow(
+ <UserDeleteAccountContent
+ className="my-class"
+ organizationsSafeToDelete={[organizationSafeToDelete]}
+ organizationsToTransferOrDelete={[]}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('should render correctly ShowOrganizationsToTransferOrDelete', () => {
+ expect(
+ shallow(
+ <ShowOrganizationsToTransferOrDelete organizations={[organizationToTransferOrDelete]} />
+ )
+ ).toMatchSnapshot();
+});
+
+it('should render correctly ShowOrganizations', () => {
+ expect(
+ shallow(<ShowOrganizations organizations={[organizationSafeToDelete]} />)
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx
new file mode 100644
index 00000000000..e820b6c0284
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
+import { UserDeleteAccountModal } from '../UserDeleteAccountModal';
+import { deactivateUser } from '../../../../api/users';
+
+jest.mock('../../../../api/users', () => ({
+ deactivateUser: jest.fn()
+}));
+
+const organizationSafeToDelete = {
+ key: 'luke',
+ name: 'Luke Skywalker'
+};
+
+const organizationToTransferOrDelete = {
+ key: 'luke-leia',
+ name: 'Luke and Leia'
+};
+
+it('should render modal correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should handle submit', async () => {
+ (deactivateUser as jest.Mock).mockResolvedValue(true);
+ window.location.replace = jest.fn();
+
+ const wrapper = shallowRender();
+ const instance = wrapper.instance();
+
+ instance.handleSubmit();
+ await waitAndUpdate(wrapper);
+
+ expect(deactivateUser).toBeCalled();
+ expect(window.location.replace).toHaveBeenCalledWith('/account-deleted');
+});
+
+it('should validate user input', () => {
+ const wrapper = shallowRender();
+ const instance = wrapper.instance();
+ const { handleValidate } = instance;
+
+ expect(handleValidate({ login: '' }).login).toBe('my_profile.delete_account.login.required');
+ expect(handleValidate({ login: 'abc' }).login).toBe(
+ 'my_profile.delete_account.login.wrong_value'
+ );
+ expect(handleValidate({ login: 'luke' }).login).toBeUndefined();
+});
+
+function shallowRender(props: Partial<UserDeleteAccountModal['props']> = {}) {
+ const user = mockLoggedInUser({ externalIdentity: 'luke' });
+
+ return shallow<UserDeleteAccountModal>(
+ <UserDeleteAccountModal
+ doLogout={jest.fn().mockResolvedValue(true)}
+ label="label"
+ organizationsSafeToDelete={[organizationSafeToDelete]}
+ organizationsToTransferOrDelete={[organizationToTransferOrDelete]}
+ router={mockRouter()}
+ toggleModal={jest.fn()}
+ user={user}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap
new file mode 100644
index 00000000000..05a4397a565
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap
@@ -0,0 +1,118 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div>
+ <h2
+ className="spacer-bottom"
+ >
+ my_profile.delete_account
+ </h2>
+ <DeferredSpinner
+ loading={true}
+ timeout={100}
+ />
+</div>
+`;
+
+exports[`should render correctly 2`] = `
+<div>
+ <h2
+ className="spacer-bottom"
+ >
+ my_profile.delete_account
+ </h2>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <UserDeleteAccountContent
+ className="list-styled no-padding big-spacer-top big-spacer-bottom"
+ organizationsSafeToDelete={
+ Array [
+ Object {
+ "key": "luke-leia",
+ "name": "Luke and Leia",
+ },
+ Object {
+ "key": "luke",
+ "name": "Luke Skywalker",
+ },
+ ]
+ }
+ organizationsToTransferOrDelete={Array []}
+ />
+ <Button
+ className="button-red"
+ disabled={false}
+ onClick={[Function]}
+ type="button"
+ >
+ delete
+ </Button>
+</div>
+`;
+
+exports[`should render correctly 3`] = `
+<div>
+ <h2
+ className="spacer-bottom"
+ >
+ my_profile.delete_account
+ </h2>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <UserDeleteAccountContent
+ className="list-styled no-padding big-spacer-top big-spacer-bottom"
+ organizationsSafeToDelete={
+ Array [
+ Object {
+ "key": "luke-leia",
+ "name": "Luke and Leia",
+ },
+ Object {
+ "key": "luke",
+ "name": "Luke Skywalker",
+ },
+ ]
+ }
+ organizationsToTransferOrDelete={Array []}
+ />
+ <Button
+ className="button-red"
+ disabled={false}
+ onClick={[Function]}
+ type="button"
+ >
+ delete
+ </Button>
+ <Connect(withRouter(UserDeleteAccountModal))
+ label="my_profile.delete_account"
+ organizationsSafeToDelete={
+ Array [
+ Object {
+ "key": "luke-leia",
+ "name": "Luke and Leia",
+ },
+ Object {
+ "key": "luke",
+ "name": "Luke Skywalker",
+ },
+ ]
+ }
+ organizationsToTransferOrDelete={Array []}
+ toggleModal={[Function]}
+ user={
+ Object {
+ "externalIdentity": "luke",
+ "groups": Array [],
+ "isLoggedIn": true,
+ "login": "luke",
+ "name": "Skywalker",
+ "scmAccounts": Array [],
+ }
+ }
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap
new file mode 100644
index 00000000000..56ac708fb53
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap
@@ -0,0 +1,128 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render content correctly 1`] = `
+<ShowOrganizationsToTransferOrDelete
+ organizations={
+ Array [
+ Object {
+ "key": "luke-leia",
+ "name": "Luke and Leia",
+ },
+ ]
+ }
+/>
+`;
+
+exports[`should render content correctly 2`] = `
+<ShowOrganizations
+ className="my-class"
+ organizations={
+ Array [
+ Object {
+ "key": "luke",
+ "name": "Luke Skywalker",
+ },
+ ]
+ }
+/>
+`;
+
+exports[`should render correctly ShowOrganizations 1`] = `
+<ul>
+ <li
+ className="spacer-bottom"
+ >
+ my_profile.delete_account.info
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="my_profile.delete_account.data.info"
+ id="my_profile.delete_account.data.info"
+ values={
+ Object {
+ "help": <a
+ href="/documentation/user-guide/user-account/#delete-your-user-account"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ learn_more
+ </a>,
+ }
+ }
+ />
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="my_profile.delete_account.info.orgs.members"
+ id="my_profile.delete_account.info.orgs.members"
+ values={
+ Object {
+ "organizations": <React.Fragment>
+ <span>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/luke"
+ >
+ Luke Skywalker
+ </Link>
+ </span>
+ </React.Fragment>,
+ }
+ }
+ />
+ </li>
+</ul>
+`;
+
+exports[`should render correctly ShowOrganizationsToTransferOrDelete 1`] = `
+<Fragment>
+ <p
+ className="big-spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="my_profile.delete_account.info.orgs_to_transfer_or_delete"
+ id="my_profile.delete_account.info.orgs_to_transfer_or_delete"
+ values={
+ Object {
+ "organizations": <React.Fragment>
+ <span>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/luke-leia"
+ >
+ Luke and Leia
+ </Link>
+ </span>
+ </React.Fragment>,
+ }
+ }
+ />
+ </p>
+ <Alert
+ className="big-spacer-bottom"
+ variant="warning"
+ >
+ <FormattedMessage
+ defaultMessage="my_profile.delete_account.info.orgs_to_transfer_or_delete.info"
+ id="my_profile.delete_account.info.orgs_to_transfer_or_delete.info"
+ values={
+ Object {
+ "link": <a
+ href="https://sieg.eu.ngrok.io/documentation/organizations/overview/#how-to-transfer-ownership-of-an-organization"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ my_profile.delete_account.info.orgs_to_transfer_or_delete.info.link
+ </a>,
+ }
+ }
+ />
+ </Alert>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap
new file mode 100644
index 00000000000..5ba1d10e4ad
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render modal correctly 1`] = `
+<ValidationModal
+ confirmButtonText="delete"
+ header="my_profile.delete_account.modal.header.label.luke"
+ initialValues={
+ Object {
+ "login": "",
+ }
+ }
+ isDestructive={true}
+ onClose={[MockFunction]}
+ onSubmit={[Function]}
+ validate={[Function]}
+>
+ <Component />
+</ValidationModal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx
index b7ceeab374d..92ec1433518 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { translate } from 'sonar-ui-common/helpers/l10n';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { formatMeasure } from 'sonar-ui-common/helpers/measures';
import ActionsDropdown, {
@@ -28,6 +28,7 @@ import ActionsDropdown, {
import DeleteForm from './DeleteForm';
import Form from './Form';
import MeasureDate from './MeasureDate';
+import { isUserActive } from '../../../helpers/users';
interface Props {
measure: T.CustomMeasure;
@@ -114,7 +115,11 @@ export default class Item extends React.PureComponent<Props, State> {
<td>
<MeasureDate measure={measure} /> {translate('by_')}{' '}
- <span className="js-custom-measure-user">{measure.user.name}</span>
+ <span className="js-custom-measure-user">
+ {isUserActive(measure.user)
+ ? measure.user.name || measure.user.login
+ : translateWithParameters('user.x_deleted', measure.user.login)}
+ </span>
</td>
<td className="thin nowrap">
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx
index 90fb5c29edc..667d1f38a88 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx
@@ -33,14 +33,12 @@ const measure = {
};
it('should render', () => {
- expect(
- shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={jest.fn()} />)
- ).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
});
it('should edit metric', () => {
const onEdit = jest.fn();
- const wrapper = shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={onEdit} />);
+ const wrapper = shallowRender({ onEdit });
click(wrapper.find('.js-custom-measure-update'));
wrapper.update();
@@ -55,7 +53,7 @@ it('should edit metric', () => {
it('should delete custom measure', () => {
const onDelete = jest.fn();
- const wrapper = shallow(<Item measure={measure} onDelete={onDelete} onEdit={jest.fn()} />);
+ const wrapper = shallowRender({ onDelete });
click(wrapper.find('.js-custom-measure-delete'));
wrapper.update();
@@ -63,3 +61,13 @@ it('should delete custom measure', () => {
wrapper.find('DeleteForm').prop<Function>('onSubmit')();
expect(onDelete).toBeCalledWith('1');
});
+
+it('should render correctly for deleted user', () => {
+ expect(
+ shallowRender({ measure: { ...measure, user: { active: false, login: 'user' } } })
+ ).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Item['props']> = {}) {
+ return shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={jest.fn()} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap
index 225ca0c587c..a763f649723 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap
@@ -87,3 +87,90 @@ exports[`should render 1`] = `
</td>
</tr>
`;
+
+exports[`should render correctly for deleted user 1`] = `
+<tr
+ data-metric="custom"
+>
+ <td
+ className="nowrap"
+ >
+ <div>
+ <span
+ className="js-custom-measure-metric-name"
+ >
+ custom-metric
+ </span>
+ </div>
+ <span
+ className="js-custom-measure-domain note"
+ />
+ </td>
+ <td
+ className="nowrap"
+ >
+ <strong
+ className="js-custom-measure-value"
+ >
+ custom-value
+ </strong>
+ </td>
+ <td>
+ <span
+ className="js-custom-measure-description"
+ >
+ my custom measure
+ </span>
+ </td>
+ <td>
+ <MeasureDate
+ measure={
+ Object {
+ "createdAt": "2017-01-01",
+ "description": "my custom measure",
+ "id": "1",
+ "metric": Object {
+ "key": "custom",
+ "name": "custom-metric",
+ "type": "STRING",
+ },
+ "projectKey": "foo",
+ "user": Object {
+ "active": false,
+ "login": "user",
+ },
+ "value": "custom-value",
+ }
+ }
+ />
+
+ by_
+
+ <span
+ className="js-custom-measure-user"
+ >
+ user.x_deleted.user
+ </span>
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ className="js-custom-measure-update"
+ onClick={[Function]}
+ >
+ update_verb
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem
+ className="js-custom-measure-delete"
+ destructive={true}
+ onClick={[Function]}
+ >
+ delete
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
index a0f00cf6d6e..712206e016f 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
@@ -24,12 +24,7 @@ import { ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import SelectList, { Filter } from '../../../components/SelectList/SelectList';
-import {
- addUserToGroup,
- getUsersInGroup,
- GroupUser,
- removeUserFromGroup
-} from '../../../api/user_groups';
+import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../api/user_groups';
interface Props {
group: T.Group;
@@ -50,7 +45,7 @@ interface State {
lastSearchParams: SearchParams;
listHasBeenTouched: boolean;
loading: boolean;
- users: GroupUser[];
+ users: T.UserSelected[];
usersTotalCount?: number;
selectedUsers: string[];
}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
index d4482a05dbf..4d3262d3fec 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
@@ -66,7 +66,6 @@ import {
ReferencedComponent,
ReferencedLanguage,
ReferencedRule,
- ReferencedUser,
saveMyIssues,
serializeQuery,
STANDARDS,
@@ -96,7 +95,7 @@ interface FetchIssuesPromise {
languages: ReferencedLanguage[];
paging: T.Paging;
rules: ReferencedRule[];
- users: ReferencedUser[];
+ users: T.UserBase[];
}
interface Props {
@@ -136,7 +135,7 @@ export interface State {
referencedComponentsByKey: T.Dict<ReferencedComponent>;
referencedLanguages: T.Dict<ReferencedLanguage>;
referencedRules: T.Dict<ReferencedRule>;
- referencedUsers: T.Dict<ReferencedUser>;
+ referencedUsers: T.Dict<T.UserBase>;
selected?: string;
selectedFlowIndex?: number;
selectedLocationIndex?: number;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
index 9599069623a..7a8fb72a717 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
@@ -36,7 +36,7 @@ import Select from '../../../components/controls/Select';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import { searchIssueTags, bulkChangeIssues } from '../../../api/issues';
-import { isLoggedIn } from '../../../helpers/users';
+import { isLoggedIn, isUserActive } from '../../../helpers/users';
interface AssigneeOption {
avatar?: string;
@@ -161,7 +161,11 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
handleAssigneeSearch = (query: string) => {
return searchAssignees(query, this.state.organization).then(({ results }) =>
- results.map(r => ({ avatar: r.avatar, label: r.name, value: r.login }))
+ results.map(r => ({
+ avatar: r.avatar,
+ label: isUserActive(r) ? r.name : translateWithParameters('user.x_deleted', r.login),
+ value: r.login
+ }))
);
};
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
index caf10424969..95c4446ff8d 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
@@ -20,12 +20,13 @@
import * as React from 'react';
import { omit, sortBy, without } from 'lodash';
import { highlightTerm } from 'sonar-ui-common/helpers/search';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { searchAssignees, Query, ReferencedUser, SearchedAssignee, Facet } from '../utils';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { searchAssignees, Query, Facet } from '../utils';
import Avatar from '../../../components/ui/Avatar';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
+import { isUserActive } from '../../../helpers/users';
-export interface Props {
+interface Props {
assigned: boolean;
assignees: string[];
fetching: boolean;
@@ -36,7 +37,7 @@ export interface Props {
organization: string | undefined;
query: Query;
stats: T.Dict<number> | undefined;
- referencedUsers: T.Dict<ReferencedUser>;
+ referencedUsers: T.Dict<T.UserBase>;
}
export default class AssigneeFacet extends React.PureComponent<Props> {
@@ -71,11 +72,14 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
return translate('unassigned');
} else {
const user = this.props.referencedUsers[assignee];
- return user ? user.name : assignee;
+ if (!user) {
+ return assignee;
+ }
+ return isUserActive(user) ? user.name : translateWithParameters('user.x_deleted', user.login);
}
};
- loadSearchResultCount = (assignees: SearchedAssignee[]) => {
+ loadSearchResultCount = (assignees: T.UserBase[]) => {
return this.props.loadSearchResultCount('assignees', {
assigned: undefined,
assignees: assignees.map(assignee => assignee.login)
@@ -99,28 +103,35 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
}
const user = this.props.referencedUsers[assignee];
+
return user ? (
<>
- <Avatar className="little-spacer-right" hash={user.avatar} name={user.name} size={16} />
- {user.name}
+ <Avatar
+ className="little-spacer-right"
+ hash={user.avatar}
+ name={user.name || user.login}
+ size={16}
+ />
+ {isUserActive(user) ? user.name : translateWithParameters('user.x_deleted', user.login)}
</>
) : (
assignee
);
};
- renderSearchResult = (result: SearchedAssignee, query: string) => {
+ renderSearchResult = (result: T.UserBase, query: string) => {
+ const displayName = isUserActive(result)
+ ? result.name
+ : translateWithParameters('user.x_deleted', result.login);
return (
<>
- {result.avatar !== undefined && (
- <Avatar
- className="little-spacer-right"
- hash={result.avatar}
- name={result.name}
- size={16}
- />
- )}
- {highlightTerm(result.name, query)}
+ <Avatar
+ className="little-spacer-right"
+ hash={result.avatar}
+ name={result.name || result.login}
+ size={16}
+ />
+ {highlightTerm(displayName, query)}
</>
);
};
@@ -132,12 +143,12 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
}
return (
- <ListStyleFacet<SearchedAssignee>
+ <ListStyleFacet<T.UserBase>
facetHeader={translate('issues.facet.assignees')}
fetching={this.props.fetching}
getFacetItemText={this.getAssigneeName}
getSearchResultKey={user => user.login}
- getSearchResultText={user => user.name}
+ getSearchResultText={user => user.name || user.login}
// put "not assigned" item first
getSortedItems={this.getSortedItems}
loadSearchResultCount={this.loadSearchResultCount}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
index 95bcfd24158..ade6f7080b8 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
@@ -32,14 +32,7 @@ import StandardFacet from './StandardFacet';
import StatusFacet from './StatusFacet';
import TagFacet from './TagFacet';
import TypeFacet from './TypeFacet';
-import {
- Query,
- Facet,
- ReferencedComponent,
- ReferencedUser,
- ReferencedLanguage,
- ReferencedRule
-} from '../utils';
+import { Query, Facet, ReferencedComponent, ReferencedLanguage, ReferencedRule } from '../utils';
export interface Props {
component: T.Component | undefined;
@@ -57,7 +50,7 @@ export interface Props {
referencedComponentsByKey: T.Dict<ReferencedComponent>;
referencedLanguages: T.Dict<ReferencedLanguage>;
referencedRules: T.Dict<ReferencedRule>;
- referencedUsers: T.Dict<ReferencedUser>;
+ referencedUsers: T.Dict<T.UserBase>;
}
export default class Sidebar extends React.PureComponent<Props> {
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
index 6bab64ec2a1..4ce377cd1be 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
@@ -19,18 +19,18 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import AssigneeFacet, { Props } from '../AssigneeFacet';
+import AssigneeFacet from '../AssigneeFacet';
import { Query } from '../../utils';
jest.mock('../../../../store/rootReducer', () => ({}));
it('should render', () => {
- expect(renderAssigneeFacet({ assignees: ['foo'] })).toMatchSnapshot();
+ expect(shallowRender({ assignees: ['foo'] })).toMatchSnapshot();
});
it('should select unassigned', () => {
expect(
- renderAssigneeFacet({ assigned: false })
+ shallowRender({ assigned: false })
.find('ListStyleFacet')
.prop('values')
).toEqual(['']);
@@ -38,7 +38,7 @@ it('should select unassigned', () => {
it('should call onChange', () => {
const onChange = jest.fn();
- const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange });
+ const wrapper = shallowRender({ assignees: ['foo'], onChange });
const itemOnClick = wrapper.find('ListStyleFacet').prop<Function>('onItemClick');
itemOnClick('');
@@ -51,8 +51,39 @@ it('should call onChange', () => {
expect(onChange).lastCalledWith({ assigned: true, assignees: ['baz', 'foo'] });
});
-function renderAssigneeFacet(props?: Partial<Props>) {
- return shallow(
+describe('test behavior', () => {
+ const instance = shallowRender({
+ assignees: ['foo', 'baz'],
+ referencedUsers: {
+ foo: { active: false, login: 'foo' },
+ baz: { active: true, login: 'baz', name: 'Name Baz' }
+ }
+ }).instance();
+
+ it('should correctly render assignee name', () => {
+ expect(instance.getAssigneeName('')).toBe('unassigned');
+ expect(instance.getAssigneeName('bar')).toBe('bar');
+ expect(instance.getAssigneeName('baz')).toBe('Name Baz');
+ expect(instance.getAssigneeName('foo')).toBe('user.x_deleted.foo');
+ });
+
+ it('should correctly render facet item', () => {
+ expect(instance.renderFacetItem('')).toBe('unassigned');
+ expect(instance.renderFacetItem('bar')).toBe('bar');
+ expect(instance.renderFacetItem('baz')).toMatchSnapshot();
+ expect(instance.renderFacetItem('foo')).toMatchSnapshot();
+ });
+
+ it('should correctly render search result correctly', () => {
+ expect(
+ instance.renderSearchResult({ active: true, login: 'bar', name: 'Name Bar' }, 'ba')
+ ).toMatchSnapshot();
+ expect(instance.renderSearchResult({ active: false, login: 'foo' }, 'fo')).toMatchSnapshot();
+ });
+});
+
+function shallowRender(props?: Partial<AssigneeFacet['props']>) {
+ return shallow<AssigneeFacet>(
<AssigneeFacet
assigned={true}
assignees={[]}
@@ -63,7 +94,7 @@ function renderAssigneeFacet(props?: Partial<Props>) {
open={true}
organization={undefined}
query={{} as Query}
- referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }}
+ referencedUsers={{ foo: { avatar: 'avatart-foo', login: 'name-foo', name: 'Name Foo' } }}
stats={{ '': 5, foo: 13, bar: 7, baz: 6 }}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
index 7dea30a849c..6e5f27e4e47 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
@@ -38,3 +38,59 @@ exports[`should render 1`] = `
}
/>
`;
+
+exports[`test behavior should correctly render facet item 1`] = `
+<React.Fragment>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ name="Name Baz"
+ size={16}
+ />
+ Name Baz
+</React.Fragment>
+`;
+
+exports[`test behavior should correctly render facet item 2`] = `
+<React.Fragment>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ name="foo"
+ size={16}
+ />
+ user.x_deleted.foo
+</React.Fragment>
+`;
+
+exports[`test behavior should correctly render search result correctly 1`] = `
+<React.Fragment>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ name="Name Bar"
+ size={16}
+ />
+ <React.Fragment>
+ Name
+ <mark>
+ Ba
+ </mark>
+ r
+ </React.Fragment>
+</React.Fragment>
+`;
+
+exports[`test behavior should correctly render search result correctly 2`] = `
+<React.Fragment>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ name="foo"
+ size={16}
+ />
+ <React.Fragment>
+ user.x_deleted.
+ <mark>
+ fo
+ </mark>
+ o
+ </React.Fragment>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts
index a6c644f645e..8e98b7919cb 100644
--- a/server/sonar-web/src/main/js/apps/issues/utils.ts
+++ b/server/sonar-web/src/main/js/apps/issues/utils.ts
@@ -199,11 +199,6 @@ export interface ReferencedComponent {
uuid: string;
}
-export interface ReferencedUser {
- avatar: string;
- name: string;
-}
-
export interface ReferencedLanguage {
name: string;
}
@@ -213,17 +208,11 @@ export interface ReferencedRule {
name: string;
}
-export interface SearchedAssignee {
- avatar?: string;
- login: string;
- name: string;
-}
-
export const searchAssignees = (
query: string,
organization: string | undefined,
page = 1
-): Promise<{ paging: T.Paging; results: SearchedAssignee[] }> => {
+): Promise<{ paging: T.Paging; results: T.UserBase[] }> => {
return organization
? searchMembers({ organization, p: page, ps: 50, q: query }).then(({ paging, users }) => ({
paging,
diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
index 7132c0bd98b..02a75616768 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
@@ -50,20 +50,25 @@ export function NoFavoriteProjects(props: StateProps & OwnProps) {
{translate('provisioning.analyze_new_project')}
</Button>
- <Dropdown
- className="display-inline-block big-spacer-left"
- overlay={
- <ul className="menu">
- {sortBy(props.organizations, org => org.name.toLowerCase()).map(organization => (
- <OrganizationListItem key={organization.key} organization={organization} />
- ))}
- </ul>
- }>
- <a className="button" href="#">
- {translate('projects.no_favorite_projects.favorite_projects_from_orgs')}
- <DropdownIcon className="little-spacer-left" />
- </a>
- </Dropdown>
+ {props.organizations.length > 0 && (
+ <Dropdown
+ className="display-inline-block big-spacer-left"
+ overlay={
+ <ul className="menu">
+ {sortBy(props.organizations, org => org.name.toLowerCase()).map(
+ organization => (
+ <OrganizationListItem key={organization.key} organization={organization} />
+ )
+ )}
+ </ul>
+ }>
+ <a className="button" href="#">
+ {translate('projects.no_favorite_projects.favorite_projects_from_orgs')}
+ <DropdownIcon className="little-spacer-left" />
+ </a>
+ </Dropdown>
+ )}
+
<Link className="button big-spacer-left" to="/explore/projects">
{translate('projects.no_favorite_projects.favorite_public_projects')}
</Link>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
index 8cdc9b4a4e7..0f8947329a5 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
@@ -31,7 +31,14 @@ it('renders', () => {
).toMatchSnapshot();
});
-it('renders for SonarCloud', () => {
+it('renders for SonarCloud without organizations', () => {
+ (isSonarCloud as jest.Mock).mockImplementation(() => true);
+ expect(
+ shallow(<NoFavoriteProjects openProjectOnboarding={jest.fn()} organizations={[]} />)
+ ).toMatchSnapshot();
+});
+
+it('renders for SonarCloud with organizations', () => {
(isSonarCloud as jest.Mock).mockImplementation(() => true);
const organizations: T.Organization[] = [
{ actions: { admin: true }, key: 'org1', name: 'org1', projectVisibility: 'public' },
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
index d1b0a82b9c6..0aa5d44907e 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
@@ -29,7 +29,7 @@ exports[`renders 1`] = `
</div>
`;
-exports[`renders for SonarCloud 1`] = `
+exports[`renders for SonarCloud with organizations 1`] = `
<div
className="projects-empty-list"
>
@@ -105,3 +105,37 @@ exports[`renders for SonarCloud 1`] = `
</div>
</div>
`;
+
+exports[`renders for SonarCloud without organizations 1`] = `
+<div
+ className="projects-empty-list"
+>
+ <h3>
+ projects.no_favorite_projects
+ </h3>
+ <div
+ className="spacer-top"
+ >
+ <p>
+ projects.no_favorite_projects.how_to_add_projects
+ </p>
+ <div
+ className="huge-spacer-top"
+ >
+ <Button
+ onClick={[MockFunction]}
+ >
+ provisioning.analyze_new_project
+ </Button>
+ <Link
+ className="button big-spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/explore/projects"
+ >
+ projects.no_favorite_projects.favorite_public_projects
+ </Link>
+ </div>
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
index 29b60956fca..7de9677ea73 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
@@ -31,7 +31,7 @@ import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import { getComponents, Project } from '../../api/components';
export interface Props {
- currentUser: { login: string };
+ currentUser: Pick<T.LoggedInUser, 'login'>;
hasProvisionPermission?: boolean;
onOrganizationUpgrade: () => void;
onVisibilityChange: (visibility: T.Visibility) => void;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
index 1273e30ffd3..4e99b7f715b 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
@@ -27,7 +27,7 @@ import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
import { Project } from '../../api/components';
interface Props {
- currentUser: { login: string };
+ currentUser: Pick<T.LoggedInUser, 'login'>;
onProjectCheck: (project: Project, checked: boolean) => void;
organization: string | undefined;
project: Project;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
index abff099d1ee..a2578797b24 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
@@ -29,7 +29,7 @@ import { getComponentNavigation } from '../../api/nav';
import { getComponentPermissionsUrl } from '../../helpers/urls';
export interface Props {
- currentUser: { login: string };
+ currentUser: Pick<T.LoggedInUser, 'login'>;
organization: string | undefined;
project: Project;
}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
index d644c5410d2..5c89aaac9b1 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
@@ -24,7 +24,7 @@ import ProjectRow from './ProjectRow';
import { Project } from '../../api/components';
interface Props {
- currentUser: { login: string };
+ currentUser: Pick<T.LoggedInUser, 'login'>;
onProjectDeselected: (project: string) => void;
onProjectSelected: (project: string) => void;
organization: T.Organization;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
index 89fc396db7d..ad11a0f3ded 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
@@ -26,7 +26,7 @@ import { grantPermissionToUser } from '../../api/permissions';
import { Project } from '../../api/components';
interface Props {
- currentUser: { login: string };
+ currentUser: Pick<T.LoggedInUser, 'login'>;
onClose: () => void;
onRestoreAccess: () => void;
project: Project;
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx
index a9816e9d4be..dc919c071df 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx
@@ -31,12 +31,6 @@ import {
} from '../../../api/quality-profiles';
import { Profile } from '../types';
-export interface User {
- avatar?: string;
- login: string;
- name: string;
-}
-
export interface Group {
name: string;
}
@@ -50,7 +44,7 @@ interface State {
addUserForm: boolean;
groups?: Group[];
loading: boolean;
- users?: User[];
+ users?: T.UserSelected[];
}
export default class ProfilePermissions extends React.PureComponent<Props, State> {
@@ -112,7 +106,7 @@ export default class ProfilePermissions extends React.PureComponent<Props, State
}
};
- handleUserAdd = (addedUser: User) => {
+ handleUserAdd = (addedUser: T.UserSelected) => {
if (this.mounted) {
this.setState((state: State) => ({
addUserForm: false,
@@ -121,7 +115,7 @@ export default class ProfilePermissions extends React.PureComponent<Props, State
}
};
- handleUserDelete = (removedUser: User) => {
+ handleUserDelete = (removedUser: T.UserSelected) => {
if (this.mounted) {
this.setState((state: State) => ({
users: state.users && state.users.filter(user => user !== removedUser)
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
index 574063aaa4a..4e93e4f12d4 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
@@ -21,8 +21,8 @@ import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { SubmitButton, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
-import { User, Group } from './ProfilePermissions';
import ProfilePermissionsFormSelect from './ProfilePermissionsFormSelect';
+import { Group } from './ProfilePermissions';
import {
searchUsers,
searchGroups,
@@ -34,13 +34,13 @@ import {
interface Props {
onClose: () => void;
onGroupAdd: (group: Group) => void;
- onUserAdd: (user: User) => void;
+ onUserAdd: (user: T.UserSelected) => void;
organization?: string;
profile: { language: string; name: string };
}
interface State {
- selected?: User | Group;
+ selected?: T.UserSelected | Group;
submitting: boolean;
}
@@ -62,7 +62,7 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
}
};
- handleUserAdd = (user: User) =>
+ handleUserAdd = (user: T.UserSelected) =>
addUser({
language: this.props.profile.language,
login: user.login,
@@ -83,8 +83,8 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
const { selected } = this.state;
if (selected) {
this.setState({ submitting: true });
- if ((selected as User).login !== undefined) {
- this.handleUserAdd(selected as User);
+ if ((selected as T.UserSelected).login !== undefined) {
+ this.handleUserAdd(selected as T.UserSelected);
} else {
this.handleGroupAdd(selected as Group);
}
@@ -105,7 +105,7 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
);
};
- handleValueChange = (selected: User | Group) => {
+ handleValueChange = (selected: T.UserSelected | Group) => {
this.setState({ selected });
};
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx
index 3599277ea52..7d3ef0b6a1d 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx
@@ -21,11 +21,11 @@ import * as React from 'react';
import GroupIcon from 'sonar-ui-common/components/icons/GroupIcon';
import { debounce, identity } from 'lodash';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-import { User, Group } from './ProfilePermissions';
+import { Group } from './ProfilePermissions';
import Select from '../../../components/controls/Select';
import Avatar from '../../../components/ui/Avatar';
-type Option = User | Group;
+type Option = T.UserSelected | Group;
type OptionWithValue = Option & { value: string };
interface Props {
@@ -112,8 +112,8 @@ export default class ProfilePermissionsFormSelect extends React.PureComponent<Pr
}
}
-function isUser(option: Option): option is User {
- return (option as User).login !== undefined;
+function isUser(option: Option): option is T.UserSelected {
+ return (option as T.UserSelected).login !== undefined;
}
function getStringValue(option: Option) {
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx
index 196ba45d7e9..5498efdc9b2 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx
@@ -26,15 +26,14 @@ import {
ResetButtonLink
} from 'sonar-ui-common/components/controls/buttons';
import SimpleModal, { ChildrenProps } from 'sonar-ui-common/components/controls/SimpleModal';
-import { User } from './ProfilePermissions';
import { removeUser } from '../../../api/quality-profiles';
import Avatar from '../../../components/ui/Avatar';
interface Props {
- onDelete: (user: User) => void;
+ onDelete: (user: T.UserSelected) => void;
organization?: string;
profile: { language: string; name: string };
- user: User;
+ user: T.UserSelected;
}
interface State {
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsUser-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsUser-test.tsx
index 027390687f1..e70e777f7d6 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsUser-test.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsUser-test.tsx
@@ -17,23 +17,21 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-/* eslint-disable import/first, import/order */
-jest.mock('../../../../api/quality-profiles', () => ({
- removeUser: jest.fn(() => Promise.resolve())
-}));
-
import * as React from 'react';
import { shallow } from 'enzyme';
-import ProfilePermissionsUser from '../ProfilePermissionsUser';
import { click } from 'sonar-ui-common/helpers/testUtils';
+import ProfilePermissionsUser from '../ProfilePermissionsUser';
+import { removeUser } from '../../../../api/quality-profiles';
-const removeUser = require('../../../../api/quality-profiles').removeUser as jest.Mock<any>;
+jest.mock('../../../../api/quality-profiles', () => ({
+ removeUser: jest.fn(() => Promise.resolve())
+}));
const profile = { language: 'js', name: 'Sonar way' };
-const user = { login: 'luke', name: 'Luke Skywalker' };
+const user: T.UserSelected = { login: 'luke', name: 'Luke Skywalker', selected: true };
beforeEach(() => {
- removeUser.mockClear();
+ jest.clearAllMocks();
});
it('renders', () => {
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx b/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
index 0ebfab02599..0bba08201fe 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
@@ -29,12 +29,12 @@ interface Props {
doLogout: () => Promise<void>;
}
-class Logout extends React.PureComponent<Props> {
+export class Logout extends React.PureComponent<Props> {
componentDidMount() {
this.props.doLogout().then(
() => {
RecentHistory.clear();
- window.location.href = getBaseUrl() + '/';
+ window.location.replace(getBaseUrl() + '/');
},
() => {}
);
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx
new file mode 100644
index 00000000000..dcd7df7c9e4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { Logout } from '../Logout';
+
+it('should logout correctly', async () => {
+ const doLogout = jest.fn().mockResolvedValue(true);
+ window.location.replace = jest.fn();
+
+ const wrapper = shallowRender({ doLogout });
+ await waitAndUpdate(wrapper);
+
+ expect(doLogout).toHaveBeenCalled();
+ expect(window.location.replace).toHaveBeenCalledWith('/');
+});
+
+it('should not redirect if logout fails', async () => {
+ const doLogout = jest.fn().mockRejectedValue(false);
+ window.location.replace = jest.fn();
+
+ const wrapper = shallowRender({ doLogout });
+ await waitAndUpdate(wrapper);
+
+ expect(doLogout).toHaveBeenCalled();
+ expect(window.location.replace).not.toHaveBeenCalled();
+ expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Logout['props']> = {}) {
+ return shallow(<Logout doLogout={jest.fn()} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap
new file mode 100644
index 00000000000..f510e29fcf7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not redirect if logout fails 1`] = `
+<div
+ className="page page-limited"
+>
+ <Connect(GlobalMessages) />
+ <div
+ className="text-center"
+ >
+ logging_out
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx b/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx
index bf7ea8f38d1..a193e71bcce 100644
--- a/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx
@@ -26,7 +26,7 @@ import { deactivateUser } from '../../../api/users';
export interface Props {
onClose: () => void;
onUpdateUsers: () => void;
- user: T.User;
+ user: T.UserActive;
}
interface State {
diff --git a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
index 43b4273c75c..d4248014214 100644
--- a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
@@ -26,6 +26,7 @@ import ActionsDropdown, {
import DeactivateForm from './DeactivateForm';
import PasswordForm from './PasswordForm';
import UserForm from './UserForm';
+import { isUserActive } from '../../../helpers/users';
interface Props {
isCurrentUser: boolean;
@@ -40,10 +41,21 @@ interface State {
export default class UserActions extends React.PureComponent<Props, State> {
state: State = {};
- handleOpenDeactivateForm = () => this.setState({ openForm: 'deactivate' });
- handleOpenPasswordForm = () => this.setState({ openForm: 'password' });
- handleOpenUpdateForm = () => this.setState({ openForm: 'update' });
- handleCloseForm = () => this.setState({ openForm: undefined });
+ handleOpenDeactivateForm = () => {
+ this.setState({ openForm: 'deactivate' });
+ };
+
+ handleOpenPasswordForm = () => {
+ this.setState({ openForm: 'password' });
+ };
+
+ handleOpenUpdateForm = () => {
+ this.setState({ openForm: 'update' });
+ };
+
+ handleCloseForm = () => {
+ this.setState({ openForm: undefined });
+ };
renderActions = () => {
const { user } = this.props;
@@ -60,12 +72,14 @@ export default class UserActions extends React.PureComponent<Props, State> {
</ActionsDropdownItem>
)}
<ActionsDropdownDivider />
- <ActionsDropdownItem
- className="js-user-deactivate"
- destructive={true}
- onClick={this.handleOpenDeactivateForm}>
- {translate('users.deactivate')}
- </ActionsDropdownItem>
+ {isUserActive(user) && (
+ <ActionsDropdownItem
+ className="js-user-deactivate"
+ destructive={true}
+ onClick={this.handleOpenDeactivateForm}>
+ {translate('users.deactivate')}
+ </ActionsDropdownItem>
+ )}
</ActionsDropdown>
);
};
@@ -77,7 +91,7 @@ export default class UserActions extends React.PureComponent<Props, State> {
return (
<>
{this.renderActions()}
- {openForm === 'deactivate' && (
+ {openForm === 'deactivate' && isUserActive(user) && (
<DeactivateForm
onClose={this.handleCloseForm}
onUpdateUsers={onUpdateUsers}
diff --git a/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx b/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx
index 06e487685e6..584095c3779 100644
--- a/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx
@@ -22,11 +22,11 @@ import { uniq } from 'lodash';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { parseError } from 'sonar-ui-common/helpers/request';
import { Button, ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal';
import UserScmAccountInput from './UserScmAccountInput';
-import { createUser, updateUser } from '../../../api/users';
import throwGlobalError from '../../../app/utils/throwGlobalError';
+import { createUser, updateUser } from '../../../api/users';
export interface Props {
onClose: () => void;
@@ -41,7 +41,6 @@ interface State {
name: string;
password: string;
scmAccounts: string[];
- submitting: boolean;
}
export default class UserForm extends React.PureComponent<Props, State> {
@@ -54,10 +53,9 @@ export default class UserForm extends React.PureComponent<Props, State> {
this.state = {
email: user.email || '',
login: user.login,
- name: user.name,
+ name: user.name || '',
password: '',
- scmAccounts: user.scmAccounts || [],
- submitting: false
+ scmAccounts: user.scmAccounts || []
};
} else {
this.state = {
@@ -65,8 +63,7 @@ export default class UserForm extends React.PureComponent<Props, State> {
login: '',
name: '',
password: '',
- scmAccounts: [],
- submitting: false
+ scmAccounts: []
};
}
}
@@ -84,7 +81,7 @@ export default class UserForm extends React.PureComponent<Props, State> {
return throwGlobalError(error);
} else {
return parseError(error).then(
- errorMsg => this.setState({ error: errorMsg, submitting: false }),
+ errorMsg => this.setState({ error: errorMsg }),
throwGlobalError
);
}
@@ -103,8 +100,7 @@ export default class UserForm extends React.PureComponent<Props, State> {
this.setState({ password: event.currentTarget.value });
handleCreateUser = () => {
- this.setState({ submitting: true });
- createUser({
+ return createUser({
email: this.state.email || undefined,
login: this.state.login,
name: this.state.name,
@@ -118,9 +114,7 @@ export default class UserForm extends React.PureComponent<Props, State> {
handleUpdateUser = () => {
const { user } = this.props;
-
- this.setState({ submitting: true });
- updateUser({
+ return updateUser({
email: user!.local ? this.state.email : undefined,
login: this.state.login,
name: user!.local ? this.state.name : undefined,
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx
index 6002475bbbe..58d79168075 100644
--- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx
@@ -34,23 +34,37 @@ it('should render correctly', () => {
expect(getWrapper()).toMatchSnapshot();
});
-it('should display change password action', () => {
+it('should open the update form', () => {
+ const wrapper = getWrapper();
+ click(wrapper.find('.js-user-update'));
expect(
- getWrapper({ user: { ...user, local: true } })
- .find('.js-user-change-password')
+ wrapper
+ .first()
+ .find('UserForm')
.exists()
- ).toBeTruthy();
+ ).toBe(true);
});
-it('should open the update form', () => {
+it('should open the password form', () => {
+ const wrapper = getWrapper({ user: { ...user, local: true } });
+ click(wrapper.find('.js-user-change-password'));
+ expect(
+ wrapper
+ .first()
+ .find('PasswordForm')
+ .exists()
+ ).toBe(true);
+});
+
+it('should open the deactivate form', () => {
const wrapper = getWrapper();
- click(wrapper.find('.js-user-update'));
+ click(wrapper.find('.js-user-deactivate'));
expect(
wrapper
.first()
- .find('UserForm')
+ .find('DeactivateForm')
.exists()
- ).toBeTruthy();
+ ).toBe(true);
});
function getWrapper(props = {}) {