]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19967 Refactor User call using React Query
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Mon, 24 Jul 2023 10:16:13 +0000 (12:16 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 31 Jul 2023 20:03:31 +0000 (20:03 +0000)
15 files changed:
server/sonar-web/src/main/js/api/queries/users.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/users.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/queries/identity-provider.ts
server/sonar-web/src/main/js/apps/users/Header.tsx
server/sonar-web/src/main/js/apps/users/UsersApp.tsx
server/sonar-web/src/main/js/apps/users/UsersList.tsx
server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx
server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx
server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx
server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
server/sonar-web/src/main/js/apps/users/components/UserForm.tsx
server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx
server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx

diff --git a/server/sonar-web/src/main/js/api/queries/users.ts b/server/sonar-web/src/main/js/api/queries/users.ts
new file mode 100644 (file)
index 0000000..916c86b
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 {
+  QueryFunctionContext,
+  useMutation,
+  useQueries,
+  useQueryClient,
+} from '@tanstack/react-query';
+import { range } from 'lodash';
+import { User } from '../../types/users';
+import {
+  CreateUserParams,
+  DeactivateUserParams,
+  SearchUsersParams,
+  UpdateUserParams,
+  createUser,
+  deactivateUser,
+  searchUsers,
+  updateUser,
+} from '../users';
+
+export function useUsersQueries(
+  searchParam: Omit<SearchUsersParams, 'p' | 'ps'>,
+  numberOfPages: number
+) {
+  type QueryKey = ['user', 'list', number, Omit<SearchUsersParams, 'p' | 'ps'>];
+  const results = useQueries({
+    queries: range(1, numberOfPages + 1).map((page: number) => ({
+      queryKey: ['user', 'list', page, searchParam],
+      queryFn: ({ queryKey: [_u, _l, page, searchParam] }: QueryFunctionContext<QueryKey>) =>
+        searchUsers({ ...searchParam, p: page }),
+    })),
+  });
+
+  return results.reduce(
+    (acc, { data, isLoading }) => ({
+      users: acc.users.concat(data?.users ?? []),
+      total: data?.paging.total,
+      isLoading: acc.isLoading || isLoading,
+    }),
+    { users: [] as User[], total: 0, isLoading: false }
+  );
+}
+
+export function useInvalidateUsersList() {
+  const queryClient = useQueryClient();
+
+  return () => queryClient.invalidateQueries({ queryKey: ['user', 'list'] });
+}
+
+export function useCreateUserMutation() {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: async (data: CreateUserParams) => {
+      await createUser(data);
+    },
+    onSuccess() {
+      queryClient.invalidateQueries({ queryKey: ['user', 'list'] });
+    },
+  });
+}
+
+export function useUpdateUserMutation() {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: async (data: UpdateUserParams) => {
+      await updateUser(data);
+    },
+    onSuccess() {
+      queryClient.invalidateQueries({ queryKey: ['user', 'list'] });
+    },
+  });
+}
+
+export function useDeactivateUserMutation() {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: async (data: DeactivateUserParams) => {
+      await deactivateUser(data);
+    },
+    onSuccess() {
+      queryClient.invalidateQueries({ queryKey: ['user', 'list'] });
+    },
+  });
+}
index 5afe097a7bb3960abf0466a8f58aa7fd107dd782..d7f331590957ccfe7b4b3df3a361d4f90036aeed 100644 (file)
@@ -53,13 +53,17 @@ export interface UserGroup {
   selected: boolean;
 }
 
-export function getUserGroups(data: {
+export interface UserGroupsParams {
   login: string;
   p?: number;
   ps?: number;
   q?: string;
   selected?: string;
-}): Promise<{ paging: Paging; groups: UserGroup[] }> {
+}
+
+export function getUserGroups(
+  data: UserGroupsParams
+): Promise<{ paging: Paging; groups: UserGroup[] }> {
   return getJSON('/api/users/groups', data);
 }
 
@@ -67,7 +71,7 @@ export function getIdentityProviders(): Promise<{ identityProviders: IdentityPro
   return getJSON('/api/users/identity_providers').catch(throwGlobalError);
 }
 
-export function searchUsers(data: {
+export interface SearchUsersParams {
   p?: number;
   ps?: number;
   q?: string;
@@ -76,38 +80,46 @@ export function searchUsers(data: {
   lastConnectedBefore?: string;
   slLastConnectedAfter?: string;
   slLastConnectedBefore?: string;
-}): Promise<{ paging: Paging; users: User[] }> {
+}
+
+export function searchUsers(data: SearchUsersParams): Promise<{ paging: Paging; users: User[] }> {
   data.q = data.q || undefined;
   return getJSON('/api/users/search', data).catch(throwGlobalError);
 }
 
-export function createUser(data: {
+export interface CreateUserParams {
   email?: string;
   local?: boolean;
   login: string;
   name: string;
   password?: string;
   scmAccount: string[];
-}): Promise<void | Response> {
+}
+
+export function createUser(data: CreateUserParams): Promise<void | Response> {
   return post('/api/users/create', data);
 }
 
-export function updateUser(data: {
+export interface UpdateUserParams {
   email?: string;
   login: string;
   name?: string;
   scmAccount: string[];
-}): Promise<{ user: User }> {
+}
+
+export function updateUser(data: UpdateUserParams): Promise<{ user: User }> {
   return postJSON('/api/users/update', {
     ...data,
     scmAccount: data.scmAccount.length > 0 ? data.scmAccount : '',
   });
 }
 
-export function deactivateUser(data: {
+export interface DeactivateUserParams {
   login: string;
   anonymize?: boolean;
-}): Promise<{ user: User }> {
+}
+
+export function deactivateUser(data: DeactivateUserParams): Promise<{ user: User }> {
   return postJSON('/api/users/deactivate', data).catch(throwGlobalError);
 }
 
index 7eb384a9bb08868d2abadd5d10b079d3ab68892f..1d8acf00f1630e6077d45d23b8e029bcffbda47c 100644 (file)
@@ -37,7 +37,7 @@ import AuthenticationFormField from './AuthenticationFormField';
 import ConfigurationForm from './ConfigurationForm';
 import GitHubConfigurationValidity from './GitHubConfigurationValidity';
 import useGithubConfiguration, { GITHUB_JIT_FIELDS } from './hook/useGithubConfiguration';
-import { useCheckGitHubConfigQuery, useIdentityProvierQuery } from './queries/identity-provider';
+import { useCheckGitHubConfigQuery, useIdentityProviderQuery } from './queries/identity-provider';
 
 interface GithubAuthenticationProps {
   definitions: ExtendedSettingDefinition[];
@@ -51,7 +51,7 @@ const GITHUB_EXCLUDED_FIELD = [
 
 export default function GithubAuthenticationTab(props: GithubAuthenticationProps) {
   const { definitions } = props;
-  const { data } = useIdentityProvierQuery();
+  const { data } = useIdentityProviderQuery();
   const [showEditModal, setShowEditModal] = useState(false);
   const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = useState(false);
 
index edd22a7a4d6fd0a6ac663f2a404f8f0fb9f45192..484485236937346636936b87f162488868eb38d5 100644 (file)
@@ -40,7 +40,7 @@ import useSamlConfiguration, {
   SAML_GROUP_NAME,
   SAML_SCIM_DEPRECATED,
 } from './hook/useSamlConfiguration';
-import { useIdentityProvierQuery, useToggleScimMutation } from './queries/identity-provider';
+import { useIdentityProviderQuery, useToggleScimMutation } from './queries/identity-provider';
 
 interface SamlAuthenticationProps {
   definitions: ExtendedSettingDefinition[];
@@ -76,7 +76,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
   } = useSamlConfiguration(definitions);
   const toggleScim = useToggleScimMutation();
 
-  const { data } = useIdentityProvierQuery();
+  const { data } = useIdentityProviderQuery();
   const { mutate: saveSetting } = useSaveValueMutation();
 
   const hasDifferentProvider = data?.provider !== undefined && data.provider !== Provider.Scim;
index b42d4bd4f50683448880431fc494ad59f1d15a3e..9c02a99a80ca30456546fc9112c5a8e9fd5e09ac 100644 (file)
@@ -35,7 +35,7 @@ import { useSyncStatusQuery } from '../../../../../queries/github-sync';
 import { Feature } from '../../../../../types/features';
 import { SysInfoCluster } from '../../../../../types/types';
 
-export function useIdentityProvierQuery() {
+export function useIdentityProviderQuery() {
   return useQuery(['identity_provider'], async () => {
     const info = (await getSystemInfo()) as SysInfoCluster;
     return { provider: info.System['External Users and Groups Provisioning'] };
index 559e388b30610bda513cb14e527fc5d38c5b9c93..4dc19d14ff471ea2bb9d067b068fd1528e1f7d64 100644 (file)
@@ -26,7 +26,6 @@ import { translate } from '../../helpers/l10n';
 import UserForm from './components/UserForm';
 
 interface Props {
-  onUpdateUsers: () => void;
   manageProvider?: string;
 }
 
@@ -66,9 +65,7 @@ export default function Header(props: Props) {
           />
         </Alert>
       )}
-      {openUserForm && (
-        <UserForm onClose={() => setOpenUserForm(false)} onUpdateUsers={props.onUpdateUsers} />
-      )}
+      {openUserForm && <UserForm onClose={() => setOpenUserForm(false)} />}
     </div>
   );
 }
index 50f5b15c847307395ab146c758715e9d10cfb067..348a19e2278190aaec72589bd4293ed7b809b322 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { subDays, subSeconds } from 'date-fns';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
 import { Helmet } from 'react-helmet-async';
-import { getIdentityProviders, searchUsers } from '../../api/users';
+import { useUsersQueries } from '../../api/queries/users';
+import { getIdentityProviders } from '../../api/users';
 import GitHubSynchronisationWarning from '../../app/components/GitHubSynchronisationWarning';
 import HelpTooltip from '../../components/controls/HelpTooltip';
 import ListFooter from '../../components/controls/ListFooter';
@@ -32,8 +33,7 @@ import { Provider, useManageProvider } from '../../components/hooks/useManagePro
 import DeferredSpinner from '../../components/ui/DeferredSpinner';
 import { now, toISO8601WithOffsetString } from '../../helpers/dates';
 import { translate } from '../../helpers/l10n';
-import { IdentityProvider, Paging } from '../../types/types';
-import { User } from '../../types/users';
+import { IdentityProvider } from '../../types/types';
 import Header from './Header';
 import UsersList from './UsersList';
 import { USERS_ACTIVITY_OPTIONS, USER_INACTIVITY_DAYS_THRESHOLD } from './constants';
@@ -41,17 +41,11 @@ import { UserActivity } from './types';
 
 export default function UsersApp() {
   const [identityProviders, setIdentityProviders] = useState<IdentityProvider[]>([]);
-
-  const [loading, setLoading] = useState(true);
-  const [paging, setPaging] = useState<Paging>();
-  const [users, setUsers] = useState<User[]>([]);
-
+  const [numberOfPages, setNumberOfPages] = useState<number>(1);
   const [search, setSearch] = useState('');
   const [usersActivity, setUsersActivity] = useState<UserActivity>(UserActivity.AnyActivity);
   const [managed, setManaged] = useState<boolean | undefined>(undefined);
 
-  const manageProvider = useManageProvider();
-
   const usersActivityParams = useMemo(() => {
     const nowDate = now();
     const nowDateMinus30Days = subDays(nowDate, USER_INACTIVITY_DAYS_THRESHOLD);
@@ -76,39 +70,16 @@ export default function UsersApp() {
     }
   }, [usersActivity]);
 
-  const fetchUsers = useCallback(async () => {
-    setLoading(true);
-    try {
-      const { paging, users } = await searchUsers({
-        q: search,
-        managed,
-        ...usersActivityParams,
-      });
-      setPaging(paging);
-      setUsers(users);
-    } finally {
-      setLoading(false);
-    }
-  }, [search, managed, usersActivityParams]);
+  const { users, total, isLoading } = useUsersQueries(
+    {
+      q: search,
+      managed,
+      ...usersActivityParams,
+    },
+    numberOfPages
+  );
 
-  const fetchMoreUsers = useCallback(async () => {
-    if (!paging) {
-      return;
-    }
-    setLoading(true);
-    try {
-      const { paging: nextPage, users: nextUsers } = await searchUsers({
-        q: search,
-        managed,
-        ...usersActivityParams,
-        p: paging.pageIndex + 1,
-      });
-      setPaging(nextPage);
-      setUsers([...users, ...nextUsers]);
-    } finally {
-      setLoading(false);
-    }
-  }, [search, managed, usersActivityParams, paging, users]);
+  const manageProvider = useManageProvider();
 
   useEffect(() => {
     (async () => {
@@ -117,27 +88,29 @@ export default function UsersApp() {
     })();
   }, []);
 
-  useEffect(() => {
-    fetchUsers();
-  }, [fetchUsers]);
-
   return (
     <main className="page page-limited" id="users-page">
       <Suggestions suggestions="users" />
       <Helmet defer={false} title={translate('users.page')} />
-      <Header onUpdateUsers={fetchUsers} manageProvider={manageProvider} />
+      <Header manageProvider={manageProvider} />
       {manageProvider === Provider.Github && <GitHubSynchronisationWarning short />}
       <div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
         <ManagedFilter
           manageProvider={manageProvider}
-          loading={loading}
+          loading={isLoading}
           managed={managed}
-          setManaged={setManaged}
+          setManaged={(m) => {
+            setManaged(m);
+            setNumberOfPages(1);
+          }}
         />
         <SearchBox
           id="users-search"
           minLength={2}
-          onChange={(search: string) => setSearch(search)}
+          onChange={(search: string) => {
+            setSearch(search);
+            setNumberOfPages(1);
+          }}
           placeholder={translate('search.search_by_login_or_name')}
           value={search}
         />
@@ -145,9 +118,10 @@ export default function UsersApp() {
           <Select
             id="users-activity-filter"
             className="input-large"
-            isDisabled={loading}
+            isDisabled={isLoading}
             onChange={(userActivity: LabelValueSelectOption<UserActivity>) => {
               setUsersActivity(userActivity.value);
+              setNumberOfPages(1);
             }}
             options={USERS_ACTIVITY_OPTIONS}
             isSearchable={false}
@@ -166,23 +140,20 @@ export default function UsersApp() {
           />
         </div>
       </div>
-      <DeferredSpinner loading={loading}>
+      <DeferredSpinner loading={isLoading}>
         <UsersList
           identityProviders={identityProviders}
-          onUpdateUsers={fetchUsers}
-          updateTokensCount={fetchUsers}
           users={users}
           manageProvider={manageProvider}
         />
       </DeferredSpinner>
-      {paging !== undefined && (
-        <ListFooter
-          count={users.length}
-          loadMore={fetchMoreUsers}
-          ready={!loading}
-          total={paging.total}
-        />
-      )}
+
+      <ListFooter
+        count={users.length}
+        loadMore={() => setNumberOfPages((n) => n + 1)}
+        ready={!isLoading}
+        total={total}
+      />
     </main>
   );
 }
index d1f3349c96050e31be9e06a247aa8c594e61ed1e..e3fed01d2195b66ac0d5e7253c30e0bfa234cbb5 100644 (file)
@@ -27,19 +27,11 @@ import UserListItem from './components/UserListItem';
 
 interface Props {
   identityProviders: IdentityProvider[];
-  onUpdateUsers: () => void;
-  updateTokensCount: (login: string, tokensCount: number) => void;
   users: User[];
   manageProvider: string | undefined;
 }
 
-export default function UsersList({
-  identityProviders,
-  onUpdateUsers,
-  updateTokensCount,
-  users,
-  manageProvider,
-}: Props) {
+export default function UsersList({ identityProviders, users, manageProvider }: Props) {
   const userContext = React.useContext(CurrentUserContext);
   const currentUser = userContext?.currentUser;
 
@@ -73,8 +65,6 @@ export default function UsersList({
               )}
               isCurrentUser={isLoggedIn(currentUser) && currentUser.login === user.login}
               key={user.login}
-              onUpdateUsers={onUpdateUsers}
-              updateTokensCount={updateTokensCount}
               user={user}
               manageProvider={manageProvider}
             />
index 796d52abdf9a70c7584048e0d06fd8ef33e5123a..aa334af4bb49555f217e5f071742e6d1475d4e39 100644 (file)
  */
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { deactivateUser } from '../../../api/users';
+import { useDeactivateUserMutation } from '../../../api/queries/users';
 import DocLink from '../../../components/common/DocLink';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Checkbox from '../../../components/controls/Checkbox';
 import Modal from '../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import { Alert } from '../../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { UserActive } from '../../../types/users';
 
 export interface Props {
   onClose: () => void;
-  onUpdateUsers: () => void;
   user: UserActive;
 }
 
-interface State {
-  submitting: boolean;
-  anonymize: boolean;
-}
-
-export default class DeactivateForm extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { submitting: false, anonymize: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
+export default function DeactivateForm(props: Props) {
+  const { user } = props;
+  const [anonymize, setAnonymize] = React.useState(false);
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  const { mutate: deactivateUser, isLoading } = useDeactivateUserMutation();
 
-  handleAnonymize = (checked: boolean) => {
-    this.setState({ anonymize: checked });
-  };
-
-  handleDeactivate = (event: React.SyntheticEvent<HTMLFormElement>) => {
+  const handleDeactivate = (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
-    this.setState({ submitting: true });
-    deactivateUser({ login: this.props.user.login, anonymize: this.state.anonymize }).then(
-      () => {
-        this.props.onUpdateUsers();
-        this.props.onClose();
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ submitting: false });
-        }
+    deactivateUser(
+      { login: user.login, anonymize },
+      {
+        onSuccess: props.onClose,
       }
     );
   };
 
-  render() {
-    const { user } = this.props;
-    const { submitting, anonymize } = this.state;
-
-    const header = translate('users.deactivate_user');
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <form autoComplete="off" id="deactivate-user-form" onSubmit={this.handleDeactivate}>
-          <header className="modal-head">
-            <h2>{header}</h2>
-          </header>
-          <div className="modal-body display-flex-column">
-            {translateWithParameters('users.deactivate_user.confirmation', user.name, user.login)}
-            <Checkbox
-              id="delete-user"
-              className="big-spacer-top"
-              checked={anonymize}
-              onCheck={this.handleAnonymize}
-            >
-              <label className="little-spacer-left" htmlFor="delete-user">
-                {translate('users.delete_user')}
-              </label>
-            </Checkbox>
-            {anonymize && (
-              <Alert variant="warning" className="big-spacer-top">
-                <FormattedMessage
-                  defaultMessage={translate('users.delete_user.help')}
-                  id="delete-user-warning"
-                  values={{
-                    link: (
-                      <DocLink to="/instance-administration/authentication/overview/">
-                        {translate('users.delete_user.help.link')}
-                      </DocLink>
-                    ),
-                  }}
-                />
-              </Alert>
-            )}
-          </div>
-          <footer className="modal-foot">
-            {submitting && <i className="spinner spacer-right" />}
-            <SubmitButton className="js-confirm button-red" disabled={submitting}>
-              {translate('users.deactivate')}
-            </SubmitButton>
-            <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </footer>
-        </form>
-      </Modal>
-    );
-  }
+  const header = translate('users.deactivate_user');
+  return (
+    <Modal contentLabel={header} onRequestClose={props.onClose}>
+      <form autoComplete="off" id="deactivate-user-form" onSubmit={handleDeactivate}>
+        <header className="modal-head">
+          <h2>{header}</h2>
+        </header>
+        <div className="modal-body display-flex-column">
+          {translateWithParameters('users.deactivate_user.confirmation', user.name, user.login)}
+          <Checkbox
+            id="delete-user"
+            className="big-spacer-top"
+            checked={anonymize}
+            onCheck={(checked) => setAnonymize(checked)}
+          >
+            <label className="little-spacer-left" htmlFor="delete-user">
+              {translate('users.delete_user')}
+            </label>
+          </Checkbox>
+          {anonymize && (
+            <Alert variant="warning" className="big-spacer-top">
+              <FormattedMessage
+                defaultMessage={translate('users.delete_user.help')}
+                id="delete-user-warning"
+                values={{
+                  link: (
+                    <DocLink to="/instance-administration/authentication/overview/">
+                      {translate('users.delete_user.help.link')}
+                    </DocLink>
+                  ),
+                }}
+              />
+            </Alert>
+          )}
+        </div>
+        <footer className="modal-foot">
+          {isLoading && <i className="spinner spacer-right" />}
+          <SubmitButton className="js-confirm button-red" disabled={isLoading}>
+            {translate('users.deactivate')}
+          </SubmitButton>
+          <ResetButtonLink className="js-modal-close" onClick={props.onClose}>
+            {translate('cancel')}
+          </ResetButtonLink>
+        </footer>
+      </form>
+    </Modal>
+  );
 }
index d590e7da07a1b77e85a66578d879f9a2437ac42e..478b9b733766572cbb7a67f27e5bd855638d599f 100644 (file)
  */
 import { find, without } from 'lodash';
 import * as React from 'react';
-import { getUserGroups, UserGroup } from '../../../api/users';
+import { useInvalidateUsersList } from '../../../api/queries/users';
 import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
-import { ResetButtonLink } from '../../../components/controls/buttons';
+import { UserGroup, getUserGroups } from '../../../api/users';
 import Modal from '../../../components/controls/Modal';
 import SelectList, {
   SelectListFilter,
   SelectListSearchParams,
 } from '../../../components/controls/SelectList';
+import { ResetButtonLink } from '../../../components/controls/buttons';
 import { translate } from '../../../helpers/l10n';
 import { User } from '../../../types/users';
 
 interface Props {
   onClose: () => void;
-  onUpdateUsers: () => void;
   user: User;
 }
 
-interface State {
-  needToReload: boolean;
-  lastSearchParams?: SelectListSearchParams;
-  groups: UserGroup[];
-  groupsTotalCount?: number;
-  selectedGroups: string[];
-}
-
-export default class GroupsForm extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-
-    this.state = {
-      needToReload: false,
-      groups: [],
-      selectedGroups: [],
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
+export default function GroupsForm(props: Props) {
+  const { user } = props;
+  const [needToReload, setNeedToReload] = React.useState<boolean>(false);
+  const [lastSearchParams, setLastSearchParams] = React.useState<
+    SelectListSearchParams | undefined
+  >(undefined);
+  const [groups, setGroups] = React.useState<UserGroup[]>([]);
+  const [groupsTotalCount, setGroupsTotalCount] = React.useState<number | undefined>(undefined);
+  const [selectedGroups, setSelectedGroups] = React.useState<string[]>([]);
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  const invalidateUserList = useInvalidateUsersList();
 
-  fetchUsers = (searchParams: SelectListSearchParams) =>
+  const fetchUsers = (searchParams: SelectListSearchParams) =>
     getUserGroups({
-      login: this.props.user.login,
+      login: user.login,
       p: searchParams.page,
       ps: searchParams.pageSize,
       q: searchParams.query !== '' ? searchParams.query : undefined,
       selected: searchParams.filter,
     }).then((data) => {
-      if (this.mounted) {
-        this.setState((prevState) => {
-          const more = searchParams.page != null && searchParams.page > 1;
-
-          const groups = more ? [...prevState.groups, ...data.groups] : data.groups;
-          const newSeletedGroups = data.groups.filter((gp) => gp.selected).map((gp) => gp.name);
-          const selectedGroups = more
-            ? [...prevState.selectedGroups, ...newSeletedGroups]
-            : newSeletedGroups;
-
-          return {
-            lastSearchParams: searchParams,
-            needToReload: false,
-            groups,
-            groupsTotalCount: data.paging.total,
-            selectedGroups,
-          };
-        });
-      }
+      const more = searchParams.page != null && searchParams.page > 1;
+      const allGroups = more ? [...groups, ...data.groups] : data.groups;
+      const newSeletedGroups = data.groups.filter((gp) => gp.selected).map((gp) => gp.name);
+      const allSelectedGroups = more ? [...selectedGroups, ...newSeletedGroups] : newSeletedGroups;
+
+      setLastSearchParams(searchParams);
+      setNeedToReload(false);
+      setGroups(allGroups);
+      setGroupsTotalCount(data.paging.total);
+      setSelectedGroups(allSelectedGroups);
     });
 
-  handleSelect = (name: string) =>
+  const handleSelect = (name: string) =>
     addUserToGroup({
       name,
-      login: this.props.user.login,
+      login: user.login,
     }).then(() => {
-      if (this.mounted) {
-        this.setState((state: State) => ({
-          needToReload: true,
-          selectedGroups: [...state.selectedGroups, name],
-        }));
-      }
+      setNeedToReload(true);
+      setSelectedGroups([...selectedGroups, name]);
     });
 
-  handleUnselect = (name: string) =>
+  const handleUnselect = (name: string) =>
     removeUserFromGroup({
       name,
-      login: this.props.user.login,
+      login: user.login,
     }).then(() => {
-      if (this.mounted) {
-        this.setState((state: State) => ({
-          needToReload: true,
-          selectedGroups: without(state.selectedGroups, name),
-        }));
-      }
+      setNeedToReload(true);
+      setSelectedGroups(without(selectedGroups, name));
     });
 
-  handleClose = () => {
-    this.props.onUpdateUsers();
-    this.props.onClose();
+  const handleClose = () => {
+    invalidateUserList();
+    props.onClose();
   };
 
-  renderElement = (name: string): React.ReactNode => {
-    const group = find(this.state.groups, { name });
+  const renderElement = (name: string): React.ReactNode => {
+    const group = find(groups, { name });
     return (
       <div className="select-list-list-item">
         {group === undefined ? (
@@ -142,37 +108,33 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
     );
   };
 
-  render() {
-    const header = translate('users.update_groups');
+  const header = translate('users.update_groups');
 
-    return (
-      <Modal contentLabel={header} onRequestClose={this.handleClose}>
-        <div className="modal-head">
-          <h2>{header}</h2>
-        </div>
+  return (
+    <Modal contentLabel={header} onRequestClose={handleClose}>
+      <div className="modal-head">
+        <h2>{header}</h2>
+      </div>
 
-        <div className="modal-body modal-container">
-          <SelectList
-            elements={this.state.groups.map((group) => group.name)}
-            elementsTotalCount={this.state.groupsTotalCount}
-            needToReload={
-              this.state.needToReload &&
-              this.state.lastSearchParams &&
-              this.state.lastSearchParams.filter !== SelectListFilter.All
-            }
-            onSearch={this.fetchUsers}
-            onSelect={this.handleSelect}
-            onUnselect={this.handleUnselect}
-            renderElement={this.renderElement}
-            selectedElements={this.state.selectedGroups}
-            withPaging
-          />
-        </div>
+      <div className="modal-body modal-container">
+        <SelectList
+          elements={groups.map((group) => group.name)}
+          elementsTotalCount={groupsTotalCount}
+          needToReload={
+            needToReload && lastSearchParams && lastSearchParams.filter !== SelectListFilter.All
+          }
+          onSearch={fetchUsers}
+          onSelect={handleSelect}
+          onUnselect={handleUnselect}
+          renderElement={renderElement}
+          selectedElements={selectedGroups}
+          withPaging
+        />
+      </div>
 
-        <footer className="modal-foot">
-          <ResetButtonLink onClick={this.handleClose}>{translate('done')}</ResetButtonLink>
-        </footer>
-      </Modal>
-    );
-  }
+      <footer className="modal-foot">
+        <ResetButtonLink onClick={handleClose}>{translate('done')}</ResetButtonLink>
+      </footer>
+    </Modal>
+  );
 }
index d924351573a2e8a5d6f970eece38df2b80d99b41..6d6f79f267e5c7588533d2cf10cfcb881350234f 100644 (file)
@@ -19,8 +19,9 @@
  */
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { ResetButtonLink } from '../../../components/controls/buttons';
+import { useInvalidateUsersList } from '../../../api/queries/users';
 import Modal from '../../../components/controls/Modal';
+import { ResetButtonLink } from '../../../components/controls/buttons';
 import { translate } from '../../../helpers/l10n';
 import { User } from '../../../types/users';
 import TokensForm from './TokensForm';
@@ -28,12 +29,21 @@ import TokensForm from './TokensForm';
 interface Props {
   user: User;
   onClose: () => void;
-  updateTokensCount: (login: string, tokensCount: number) => void;
 }
 
 export default function TokensFormModal(props: Props) {
+  const [hasTokenCountChanged, setHasTokenCountChanged] = React.useState(false);
+  const invalidateUserList = useInvalidateUsersList();
+
+  const handleClose = () => {
+    if (hasTokenCountChanged) {
+      invalidateUserList();
+    }
+    props.onClose();
+  };
+
   return (
-    <Modal size="large" contentLabel={translate('users.tokens')} onRequestClose={props.onClose}>
+    <Modal size="large" contentLabel={translate('users.tokens')} onRequestClose={handleClose}>
       <header className="modal-head">
         <h2>
           <FormattedMessage
@@ -47,12 +57,12 @@ export default function TokensFormModal(props: Props) {
         <TokensForm
           deleteConfirmation="inline"
           login={props.user.login}
-          updateTokensCount={props.updateTokensCount}
+          updateTokensCount={() => setHasTokenCountChanged(true)}
           displayTokenTypeInput={false}
         />
       </div>
       <footer className="modal-foot">
-        <ResetButtonLink onClick={props.onClose}>{translate('done')}</ResetButtonLink>
+        <ResetButtonLink onClick={handleClose}>{translate('done')}</ResetButtonLink>
       </footer>
     </Modal>
   );
index 627c8b45cfab886fd70bda9b0dfe5e1005a2e104..e1872825607996008bca0dea3db86fedfd2005e6 100644 (file)
@@ -23,67 +23,50 @@ import ActionsDropdown, {
   ActionsDropdownItem,
 } from '../../../components/controls/ActionsDropdown';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { isUserActive, User } from '../../../types/users';
+import { User, isUserActive } from '../../../types/users';
 import DeactivateForm from './DeactivateForm';
 import PasswordForm from './PasswordForm';
 import UserForm from './UserForm';
 
 interface Props {
   isCurrentUser: boolean;
-  onUpdateUsers: () => void;
   user: User;
   manageProvider: string | undefined;
 }
 
-interface State {
-  openForm?: string;
-}
-
-export default class UserActions extends React.PureComponent<Props, State> {
-  state: State = {};
+export default function UserActions(props: Props) {
+  const { isCurrentUser, user, manageProvider } = props;
 
-  handleOpenDeactivateForm = () => {
-    this.setState({ openForm: 'deactivate' });
-  };
-
-  handleOpenPasswordForm = () => {
-    this.setState({ openForm: 'password' });
-  };
-
-  handleOpenUpdateForm = () => {
-    this.setState({ openForm: 'update' });
-  };
-
-  handleCloseForm = () => {
-    this.setState({ openForm: undefined });
-  };
+  const [openForm, setOpenForm] = React.useState<string | undefined>(undefined);
 
-  isInstanceManaged = () => {
-    return this.props.manageProvider !== undefined;
+  const isInstanceManaged = () => {
+    return manageProvider !== undefined;
   };
 
-  isUserLocal = () => {
-    return this.isInstanceManaged() && !this.props.user.managed;
+  const isUserLocal = () => {
+    return isInstanceManaged() && !user.managed;
   };
 
-  isUserManaged = () => {
-    return this.isInstanceManaged() && this.props.user.managed;
+  const isUserManaged = () => {
+    return isInstanceManaged() && user.managed;
   };
 
-  renderActions = () => {
-    const { user } = this.props;
+  if (isUserManaged()) {
+    return null;
+  }
 
-    return (
+  return (
+    <>
       <ActionsDropdown label={translateWithParameters('users.manage_user', user.login)}>
-        {!this.isInstanceManaged() && (
+        {!isInstanceManaged() && (
           <>
-            <ActionsDropdownItem className="js-user-update" onClick={this.handleOpenUpdateForm}>
+            <ActionsDropdownItem className="js-user-update" onClick={() => setOpenForm('update')}>
               {translate('update_details')}
             </ActionsDropdownItem>
             {user.local && (
               <ActionsDropdownItem
                 className="js-user-change-password"
-                onClick={this.handleOpenPasswordForm}
+                onClick={() => setOpenForm('password')}
               >
                 {translate('my_profile.password.title')}
               </ActionsDropdownItem>
@@ -91,45 +74,28 @@ export default class UserActions extends React.PureComponent<Props, State> {
           </>
         )}
 
-        {isUserActive(user) && !this.isInstanceManaged() && <ActionsDropdownDivider />}
-        {isUserActive(user) && (!this.isInstanceManaged() || this.isUserLocal()) && (
+        {isUserActive(user) && !isInstanceManaged() && <ActionsDropdownDivider />}
+        {isUserActive(user) && (!isInstanceManaged() || isUserLocal()) && (
           <ActionsDropdownItem
             className="js-user-deactivate"
             destructive
-            onClick={this.handleOpenDeactivateForm}
+            onClick={() => setOpenForm('deactivate')}
           >
             {translate('users.deactivate')}
           </ActionsDropdownItem>
         )}
       </ActionsDropdown>
-    );
-  };
-
-  render() {
-    const { openForm } = this.state;
-    const { isCurrentUser, onUpdateUsers, user } = this.props;
-
-    if (this.isUserManaged()) {
-      return null;
-    }
-
-    return (
-      <>
-        {this.renderActions()}
-        {openForm === 'deactivate' && isUserActive(user) && (
-          <DeactivateForm
-            onClose={this.handleCloseForm}
-            onUpdateUsers={onUpdateUsers}
-            user={user}
-          />
-        )}
-        {openForm === 'password' && (
-          <PasswordForm isCurrentUser={isCurrentUser} onClose={this.handleCloseForm} user={user} />
-        )}
-        {openForm === 'update' && (
-          <UserForm onClose={this.handleCloseForm} onUpdateUsers={onUpdateUsers} user={user} />
-        )}
-      </>
-    );
-  }
+      {openForm === 'deactivate' && isUserActive(user) && (
+        <DeactivateForm onClose={() => setOpenForm(undefined)} user={user} />
+      )}
+      {openForm === 'password' && (
+        <PasswordForm
+          isCurrentUser={isCurrentUser}
+          onClose={() => setOpenForm(undefined)}
+          user={user}
+        />
+      )}
+      {openForm === 'update' && <UserForm onClose={() => setOpenForm(undefined)} user={user} />}
+    </>
+  );
 }
index 8896375624cc87a280d2284cf6b51b46be3e7f93..a1a4c11d7d4f1208884c0b0cd1d2421ddc60f887 100644 (file)
@@ -18,9 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { createUser, updateUser } from '../../../api/users';
-import { Button, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import { useCreateUserMutation, useUpdateUserMutation } from '../../../api/queries/users';
 import SimpleModal from '../../../components/controls/SimpleModal';
+import { Button, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import { Alert } from '../../../components/ui/Alert';
 import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
@@ -32,245 +32,196 @@ import UserScmAccountInput from './UserScmAccountInput';
 
 export interface Props {
   onClose: () => void;
-  onUpdateUsers: () => void;
   user?: User;
 }
 
-interface State {
-  email: string;
-  error?: string;
-  login: string;
-  name: string;
-  password: string;
-  scmAccounts: string[];
-}
-
-export default class UserForm extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-    const { user } = props;
-    if (user) {
-      this.state = {
-        email: user.email || '',
-        login: user.login,
-        name: user.name || '',
-        password: '',
-        scmAccounts: user.scmAccounts || [],
-      };
-    } else {
-      this.state = {
-        email: '',
-        login: '',
-        name: '',
-        password: '',
-        scmAccounts: [],
-      };
-    }
-  }
+export default function UserForm(props: Props) {
+  const { user } = props;
 
-  componentDidMount() {
-    this.mounted = true;
-  }
+  const { mutate: createUser } = useCreateUserMutation();
+  const { mutate: updateUser } = useUpdateUserMutation();
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  const [email, setEmail] = React.useState<string>(user?.email || '');
+  const [login, setLogin] = React.useState<string>(user?.login || '');
+  const [name, setName] = React.useState<string>(user?.name || '');
+  const [password, setPassword] = React.useState<string>('');
+  const [scmAccounts, setScmAccounts] = React.useState<string[]>(user?.scmAccounts || []);
+  const [error, setError] = React.useState<string | undefined>(undefined);
 
-  handleError = (response: Response) => {
-    if (!this.mounted || ![400, 500].includes(response.status)) {
+  const handleError = (response: Response) => {
+    if (![400, 500].includes(response.status)) {
       return throwGlobalError(response);
-    } else {
-      return parseError(response).then(
-        (errorMsg) => this.setState({ error: errorMsg }),
-        throwGlobalError
-      );
     }
+    return parseError(response).then((errorMsg) => setError(errorMsg), throwGlobalError);
   };
 
-  handleEmailChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
-    this.setState({ email: event.currentTarget.value });
-
-  handleLoginChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
-    this.setState({ login: event.currentTarget.value });
-
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
-    this.setState({ name: event.currentTarget.value });
-
-  handlePasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
-    this.setState({ password: event.currentTarget.value });
-
-  handleCreateUser = () => {
-    return createUser({
-      email: this.state.email || undefined,
-      login: this.state.login,
-      name: this.state.name,
-      password: this.state.password,
-      scmAccount: this.state.scmAccounts,
-    }).then(() => {
-      this.props.onUpdateUsers();
-      this.props.onClose();
-    }, this.handleError);
+  const handleCreateUser = () => {
+    createUser(
+      {
+        email: email || undefined,
+        login,
+        name,
+        password,
+        scmAccount: scmAccounts,
+      },
+      { onSuccess: props.onClose, onError: handleError }
+    );
   };
 
-  handleUpdateUser = () => {
-    const { user } = this.props;
-    return updateUser({
-      email: user!.local ? this.state.email : undefined,
-      login: this.state.login,
-      name: user!.local ? this.state.name : undefined,
-      scmAccount: this.state.scmAccounts,
-    }).then(() => {
-      this.props.onUpdateUsers();
-      this.props.onClose();
-    }, this.handleError);
+  const handleUpdateUser = () => {
+    const { user } = props;
+
+    updateUser(
+      {
+        email: user!.local ? email : undefined,
+        login,
+        name: user!.local ? name : undefined,
+        scmAccount: scmAccounts,
+      },
+      { onSuccess: props.onClose, onError: handleError }
+    );
   };
 
-  handleAddScmAccount = () => {
-    this.setState(({ scmAccounts }) => ({ scmAccounts: scmAccounts.concat('') }));
+  const handleAddScmAccount = () => {
+    setScmAccounts((scmAccounts) => scmAccounts.concat(''));
   };
 
-  handleUpdateScmAccount = (idx: number, scmAccount: string) =>
-    this.setState(({ scmAccounts: oldScmAccounts }) => {
-      const scmAccounts = oldScmAccounts.slice();
-      scmAccounts[idx] = scmAccount;
-      return { scmAccounts };
+  const handleUpdateScmAccount = (idx: number, scmAccount: string) => {
+    setScmAccounts((scmAccounts) => {
+      const newScmAccounts = scmAccounts.slice();
+      newScmAccounts[idx] = scmAccount;
+      return newScmAccounts;
     });
+  };
 
-  handleRemoveScmAccount = (idx: number) =>
-    this.setState(({ scmAccounts }) => ({
-      scmAccounts: scmAccounts.slice(0, idx).concat(scmAccounts.slice(idx + 1)),
-    }));
+  const handleRemoveScmAccount = (idx: number) => {
+    setScmAccounts((scmAccounts) => scmAccounts.slice(0, idx).concat(scmAccounts.slice(idx + 1)));
+  };
 
-  render() {
-    const { user } = this.props;
-    const { error } = this.state;
+  const header = user ? translate('users.update_user') : translate('users.create_user');
 
-    const header = user ? translate('users.update_user') : translate('users.create_user');
-    return (
-      <SimpleModal
-        header={header}
-        onClose={this.props.onClose}
-        onSubmit={user ? this.handleUpdateUser : this.handleCreateUser}
-        size="small"
-      >
-        {({ onCloseClick, onFormSubmit, submitting }) => (
-          <form autoComplete="off" id="user-form" onSubmit={onFormSubmit}>
-            <header className="modal-head">
-              <h2>{header}</h2>
-            </header>
+  return (
+    <SimpleModal
+      header={header}
+      onClose={props.onClose}
+      onSubmit={user ? handleUpdateUser : handleCreateUser}
+      size="small"
+    >
+      {({ onCloseClick, onFormSubmit, submitting }) => (
+        <form autoComplete="off" id="user-form" onSubmit={onFormSubmit}>
+          <header className="modal-head">
+            <h2>{header}</h2>
+          </header>
 
-            <div className="modal-body modal-container">
-              {error && <Alert variant="error">{error}</Alert>}
+          <div className="modal-body modal-container">
+            {error && <Alert variant="error">{error}</Alert>}
 
-              {!error && user && !user.local && (
-                <Alert variant="warning">{translate('users.cannot_update_delegated_user')}</Alert>
-              )}
+            {!error && user && !user.local && (
+              <Alert variant="warning">{translate('users.cannot_update_delegated_user')}</Alert>
+            )}
 
-              <MandatoryFieldsExplanation className="modal-field" />
+            <MandatoryFieldsExplanation className="modal-field" />
 
-              {!user && (
-                <div className="modal-field">
-                  <label htmlFor="create-user-login">
-                    {translate('login')}
-                    <MandatoryFieldMarker />
-                  </label>
-                  <input
-                    autoComplete="off"
-                    autoFocus
-                    id="create-user-login"
-                    maxLength={255}
-                    minLength={3}
-                    name="login"
-                    onChange={this.handleLoginChange}
-                    required
-                    type="text"
-                    value={this.state.login}
-                  />
-                  <p className="note">{translateWithParameters('users.minimum_x_characters', 3)}</p>
-                </div>
-              )}
+            {!user && (
               <div className="modal-field">
-                <label htmlFor="create-user-name">
-                  {translate('name')}
+                <label htmlFor="create-user-login">
+                  {translate('login')}
                   <MandatoryFieldMarker />
                 </label>
                 <input
                   autoComplete="off"
-                  autoFocus={!!user}
-                  disabled={user && !user.local}
-                  id="create-user-name"
-                  maxLength={200}
-                  name="name"
-                  onChange={this.handleNameChange}
+                  autoFocus
+                  id="create-user-login"
+                  maxLength={255}
+                  minLength={3}
+                  name="login"
+                  onChange={(e) => setLogin(e.currentTarget.value)}
                   required
                   type="text"
-                  value={this.state.name}
+                  value={login}
                 />
+                <p className="note">{translateWithParameters('users.minimum_x_characters', 3)}</p>
               </div>
+            )}
+            <div className="modal-field">
+              <label htmlFor="create-user-name">
+                {translate('name')}
+                <MandatoryFieldMarker />
+              </label>
+              <input
+                autoComplete="off"
+                autoFocus={!!user}
+                disabled={user && !user.local}
+                id="create-user-name"
+                maxLength={200}
+                name="name"
+                onChange={(e) => setName(e.currentTarget.value)}
+                required
+                type="text"
+                value={name}
+              />
+            </div>
+            <div className="modal-field">
+              <label htmlFor="create-user-email">{translate('users.email')}</label>
+              <input
+                autoComplete="off"
+                disabled={user && !user.local}
+                id="create-user-email"
+                maxLength={100}
+                name="email"
+                onChange={(e) => setEmail(e.currentTarget.value)}
+                type="email"
+                value={email}
+              />
+            </div>
+            {!user && (
               <div className="modal-field">
-                <label htmlFor="create-user-email">{translate('users.email')}</label>
+                <label htmlFor="create-user-password">
+                  {translate('password')}
+                  <MandatoryFieldMarker />
+                </label>
                 <input
                   autoComplete="off"
-                  disabled={user && !user.local}
-                  id="create-user-email"
-                  maxLength={100}
-                  name="email"
-                  onChange={this.handleEmailChange}
-                  type="email"
-                  value={this.state.email}
+                  id="create-user-password"
+                  name="password"
+                  onChange={(e) => setPassword(e.currentTarget.value)}
+                  required
+                  type="password"
+                  value={password}
                 />
               </div>
-              {!user && (
-                <div className="modal-field">
-                  <label htmlFor="create-user-password">
-                    {translate('password')}
-                    <MandatoryFieldMarker />
-                  </label>
-                  <input
-                    autoComplete="off"
-                    id="create-user-password"
-                    name="password"
-                    onChange={this.handlePasswordChange}
-                    required
-                    type="password"
-                    value={this.state.password}
+            )}
+            <div className="modal-field">
+              <fieldset>
+                <legend>{translate('my_profile.scm_accounts')}</legend>
+                {scmAccounts.map((scm, idx) => (
+                  <UserScmAccountInput
+                    idx={idx}
+                    key={idx}
+                    onChange={handleUpdateScmAccount}
+                    onRemove={handleRemoveScmAccount}
+                    scmAccount={scm}
                   />
+                ))}
+                <div className="spacer-bottom">
+                  <Button className="js-scm-account-add" onClick={handleAddScmAccount}>
+                    {translate('add_verb')}
+                  </Button>
                 </div>
-              )}
-              <div className="modal-field">
-                <fieldset>
-                  <legend>{translate('my_profile.scm_accounts')}</legend>
-                  {this.state.scmAccounts.map((scm, idx) => (
-                    <UserScmAccountInput
-                      idx={idx}
-                      key={idx}
-                      onChange={this.handleUpdateScmAccount}
-                      onRemove={this.handleRemoveScmAccount}
-                      scmAccount={scm}
-                    />
-                  ))}
-                  <div className="spacer-bottom">
-                    <Button className="js-scm-account-add" onClick={this.handleAddScmAccount}>
-                      {translate('add_verb')}
-                    </Button>
-                  </div>
-                </fieldset>
-                <p className="note">{translate('user.login_or_email_used_as_scm_account')}</p>
-              </div>
+              </fieldset>
+              <p className="note">{translate('user.login_or_email_used_as_scm_account')}</p>
             </div>
-
-            <footer className="modal-foot">
-              {submitting && <i className="spinner spacer-right" />}
-              <SubmitButton disabled={submitting}>
-                {user ? translate('update_verb') : translate('create')}
-              </SubmitButton>
-              <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink>
-            </footer>
-          </form>
-        )}
-      </SimpleModal>
-    );
-  }
+          </div>
+
+          <footer className="modal-foot">
+            {submitting && <i className="spinner spacer-right" />}
+            <SubmitButton disabled={submitting}>
+              {user ? translate('update_verb') : translate('create')}
+            </SubmitButton>
+            <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink>
+          </footer>
+        </form>
+      )}
+    </SimpleModal>
+  );
 }
index 73cfd23aff44df7f506e8c274f3402e2f1bc71a2..1b56b5e976470c34043c05a9144bc4fe4b7bd592 100644 (file)
@@ -26,71 +26,54 @@ import GroupsForm from './GroupsForm';
 
 interface Props {
   groups: string[];
-  onUpdateUsers: () => void;
   user: User;
   manageProvider: string | undefined;
 }
 
-interface State {
-  openForm: boolean;
-  showMore: boolean;
-}
-
 const GROUPS_LIMIT = 3;
 
-export default class UserGroups extends React.PureComponent<Props, State> {
-  state: State = { openForm: false, showMore: false };
-
-  handleOpenForm = () => this.setState({ openForm: true });
-  handleCloseForm = () => this.setState({ openForm: false });
+export default function UserGroups(props: Props) {
+  const { groups, user, manageProvider } = props;
 
-  toggleShowMore = () => {
-    this.setState((state) => ({ showMore: !state.showMore }));
-  };
+  const [openForm, setOpenForm] = React.useState(false);
+  const [showMore, setShowMore] = React.useState(false);
 
-  render() {
-    const { groups, user, manageProvider } = this.props;
-    const { showMore, openForm } = this.state;
-    const limit = groups.length > GROUPS_LIMIT ? GROUPS_LIMIT - 1 : GROUPS_LIMIT;
-    return (
-      <ul>
-        {groups.slice(0, limit).map((group) => (
+  const limit = groups.length > GROUPS_LIMIT ? GROUPS_LIMIT - 1 : GROUPS_LIMIT;
+  return (
+    <ul>
+      {groups.slice(0, limit).map((group) => (
+        <li className="little-spacer-bottom" key={group}>
+          {group}
+        </li>
+      ))}
+      {groups.length > GROUPS_LIMIT &&
+        showMore &&
+        groups.slice(limit).map((group) => (
           <li className="little-spacer-bottom" key={group}>
             {group}
           </li>
         ))}
-        {groups.length > GROUPS_LIMIT &&
-          showMore &&
-          groups.slice(limit).map((group) => (
-            <li className="little-spacer-bottom" key={group}>
-              {group}
-            </li>
-          ))}
-        <li className="little-spacer-bottom">
-          {groups.length > GROUPS_LIMIT && !showMore && (
-            <ButtonLink className="js-user-more-groups spacer-right" onClick={this.toggleShowMore}>
-              {translateWithParameters('more_x', groups.length - limit)}
-            </ButtonLink>
-          )}
-          {manageProvider === undefined && (
-            <ButtonIcon
-              aria-label={translateWithParameters('users.update_users_groups', user.login)}
-              className="js-user-groups button-small"
-              onClick={this.handleOpenForm}
-              tooltip={translate('users.update_groups')}
-            >
-              <BulletListIcon />
-            </ButtonIcon>
-          )}
-        </li>
-        {openForm && (
-          <GroupsForm
-            onClose={this.handleCloseForm}
-            onUpdateUsers={this.props.onUpdateUsers}
-            user={user}
-          />
+      <li className="little-spacer-bottom">
+        {groups.length > GROUPS_LIMIT && !showMore && (
+          <ButtonLink
+            className="js-user-more-groups spacer-right"
+            onClick={() => setShowMore(!showMore)}
+          >
+            {translateWithParameters('more_x', groups.length - limit)}
+          </ButtonLink>
+        )}
+        {manageProvider === undefined && (
+          <ButtonIcon
+            aria-label={translateWithParameters('users.update_users_groups', user.login)}
+            className="js-user-groups button-small"
+            onClick={() => setOpenForm(true)}
+            tooltip={translate('users.update_groups')}
+          >
+            <BulletListIcon />
+          </ButtonIcon>
         )}
-      </ul>
-    );
-  }
+      </li>
+      {openForm && <GroupsForm onClose={() => setOpenForm(false)} user={user} />}
+    </ul>
+  );
 }
index cd6bdef46f0429337c69a9f1d55e2ac63bcfa69c..d298d81b5301eb749551365e2078039ae7027b98 100644 (file)
@@ -34,8 +34,6 @@ import UserScmAccounts from './UserScmAccounts';
 export interface UserListItemProps {
   identityProvider?: IdentityProvider;
   isCurrentUser: boolean;
-  onUpdateUsers: () => void;
-  updateTokensCount: (login: string, tokensCount: number) => void;
   user: User;
   manageProvider: string | undefined;
 }
@@ -43,14 +41,7 @@ export interface UserListItemProps {
 export default function UserListItem(props: UserListItemProps) {
   const [openTokenForm, setOpenTokenForm] = React.useState(false);
 
-  const {
-    identityProvider,
-    onUpdateUsers,
-    user,
-    manageProvider,
-    isCurrentUser,
-    updateTokensCount,
-  } = props;
+  const { identityProvider, user, manageProvider, isCurrentUser } = props;
 
   return (
     <tr>
@@ -79,12 +70,7 @@ export default function UserListItem(props: UserListItemProps) {
         <DateFromNow date={user.sonarLintLastConnectionDate} hourPrecision />
       </td>
       <td className="thin nowrap text-middle">
-        <UserGroups
-          groups={user.groups ?? []}
-          manageProvider={manageProvider}
-          onUpdateUsers={onUpdateUsers}
-          user={user}
-        />
+        <UserGroups groups={user.groups ?? []} manageProvider={manageProvider} user={user} />
       </td>
       <td className="thin nowrap text-middle">
         {user.tokensCount}
@@ -100,22 +86,11 @@ export default function UserListItem(props: UserListItemProps) {
 
       {(manageProvider === undefined || !user.managed) && (
         <td className="thin nowrap text-right text-middle">
-          <UserActions
-            isCurrentUser={isCurrentUser}
-            onUpdateUsers={onUpdateUsers}
-            user={user}
-            manageProvider={manageProvider}
-          />
+          <UserActions isCurrentUser={isCurrentUser} user={user} manageProvider={manageProvider} />
         </td>
       )}
 
-      {openTokenForm && (
-        <TokensFormModal
-          onClose={() => setOpenTokenForm(false)}
-          updateTokensCount={updateTokensCount}
-          user={user}
-        />
-      )}
+      {openTokenForm && <TokensFormModal onClose={() => setOpenTokenForm(false)} user={user} />}
     </tr>
   );
 }