]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19979 Allow SCM account to be update on UI
authorMathieu Suen <mathieu.suen@sonarsource.com>
Fri, 11 Aug 2023 09:51:52 +0000 (11:51 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 11 Aug 2023 20:02:49 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts
server/sonar-web/src/main/js/api/users.ts
server/sonar-web/src/main/js/apps/users/Header.tsx
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.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/UserListItem.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index ad4cf06f83104087574d782fbbb86d2a7901e738..caca8dd61dcfe2a538081b08a2a626b56defb63a 100644 (file)
@@ -286,7 +286,7 @@ export default class UsersServiceMock {
       return Promise.reject('No such user');
     }
     Object.assign(user, {
-      ...omitBy({ name, email, scmAccount }, isUndefined),
+      ...omitBy({ name, email, scmAccounts: scmAccount }, isUndefined),
     });
     return this.reply({ user });
   };
index 73812ab046ffa6d868bc126b2d55e147c56f1ef2..e9e8ea5a8004b1e16eadf916bb50af6d67a83c46 100644 (file)
@@ -106,12 +106,16 @@ export function postUser(data: {
   return postJSONBody('/api/v2/users', data);
 }
 
-export function updateUser(data: {
-  email?: string;
-  login: string;
-  name?: string;
-  scmAccount: string[];
-}): Promise<{ user: User }> {
+type UpdateUserArg =
+  | {
+      email?: string;
+      login: string;
+      name?: string;
+      scmAccount: string[];
+    }
+  | { login: string; scmAccount: string[] };
+
+export function updateUser(data: UpdateUserArg): Promise<{ user: User }> {
   return postJSON('/api/users/update', {
     ...data,
     scmAccount: data.scmAccount.length > 0 ? data.scmAccount : '',
index 4dc19d14ff471ea2bb9d067b068fd1528e1f7d64..109802f6e3998ab39aa13d88d20661ab85958cf1 100644 (file)
@@ -65,7 +65,9 @@ export default function Header(props: Props) {
           />
         </Alert>
       )}
-      {openUserForm && <UserForm onClose={() => setOpenUserForm(false)} />}
+      {openUserForm && (
+        <UserForm onClose={() => setOpenUserForm(false)} isInstanceManaged={false} />
+      )}
     </div>
   );
 }
index 73e454c64a3d225a481a925a52dd83a8c519f4a2..702e348f7c40dd5f38dd8ee0354c748756dd2bce 100644 (file)
@@ -441,14 +441,36 @@ describe('in manage mode', () => {
     expect(ui.bobUpdateGroupButton.query()).not.toBeInTheDocument();
   });
 
-  it('should not be able to update / change password / deactivate a managed user', async () => {
+  it('should not be able to update scm account', async () => {
+    const user = userEvent.setup();
+
     renderUsersApp();
 
     await act(async () => expect(await ui.bobRow.find()).toBeInTheDocument());
-    expect(ui.bobUpdateButton.query()).not.toBeInTheDocument();
+    expect(ui.bobUpdateButton.get()).toBeInTheDocument();
+
+    await user.click(ui.bobUpdateButton.get());
+
+    expect(
+      ui.bobRow.byRole('button', { name: 'users.deactivate' }).query()
+    ).not.toBeInTheDocument();
+    expect(
+      ui.bobRow.byRole('button', { name: 'my_profile.password.title' }).query()
+    ).not.toBeInTheDocument();
+
+    await user.click(await ui.bobRow.byRole('button', { name: 'update_scm' }).get());
+
+    expect(ui.userNameInput.get()).toBeDisabled();
+    expect(ui.emailInput.get()).toBeDisabled();
+
+    await user.click(ui.scmAddButton.get());
+    await user.type(ui.dialogSCMInput().get(), 'SCM');
+    await act(() => user.click(ui.updateButton.get()));
+
+    expect(await ui.bobRow.byText(/SCM/).find()).toBeInTheDocument();
   });
 
-  it('should ONLY be able to deactivate a local user', async () => {
+  it('should be able to deactivate a local user', async () => {
     const user = userEvent.setup();
     renderUsersApp();
 
index 8d9937de08b3b6728919234d8901ea0e475f6c89..e71502ae7b90b6c28460e97803f026ae141dd865 100644 (file)
@@ -38,43 +38,27 @@ export default function UserActions(props: Props) {
 
   const [openForm, setOpenForm] = React.useState<string | undefined>(undefined);
 
-  const isInstanceManaged = () => {
-    return manageProvider !== undefined;
-  };
+  const isInstanceManaged = manageProvider !== undefined;
 
-  const isUserLocal = () => {
-    return isInstanceManaged() && !user.managed;
-  };
-
-  const isUserManaged = () => {
-    return isInstanceManaged() && user.managed;
-  };
-
-  if (isUserManaged()) {
-    return null;
-  }
+  const isUserLocal = isInstanceManaged && !user.managed;
 
   return (
     <>
       <ActionsDropdown label={translateWithParameters('users.manage_user', user.login)}>
-        {!isInstanceManaged() && (
-          <>
-            <ActionsDropdownItem className="js-user-update" onClick={() => setOpenForm('update')}>
-              {translate('update_details')}
-            </ActionsDropdownItem>
-            {user.local && (
-              <ActionsDropdownItem
-                className="js-user-change-password"
-                onClick={() => setOpenForm('password')}
-              >
-                {translate('my_profile.password.title')}
-              </ActionsDropdownItem>
-            )}
-          </>
+        <ActionsDropdownItem className="js-user-update" onClick={() => setOpenForm('update')}>
+          {isInstanceManaged ? translate('update_scm') : translate('update_details')}
+        </ActionsDropdownItem>
+        {!isInstanceManaged && user.local && (
+          <ActionsDropdownItem
+            className="js-user-change-password"
+            onClick={() => setOpenForm('password')}
+          >
+            {translate('my_profile.password.title')}
+          </ActionsDropdownItem>
         )}
 
-        {isUserActive(user) && !isInstanceManaged() && <ActionsDropdownDivider />}
-        {isUserActive(user) && (!isInstanceManaged() || isUserLocal()) && (
+        {isUserActive(user) && !isInstanceManaged && <ActionsDropdownDivider />}
+        {isUserActive(user) && (!isInstanceManaged || isUserLocal) && (
           <ActionsDropdownItem
             className="js-user-deactivate"
             destructive
@@ -90,7 +74,13 @@ export default function UserActions(props: Props) {
       {openForm === 'password' && (
         <PasswordForm onClose={() => setOpenForm(undefined)} user={user} />
       )}
-      {openForm === 'update' && <UserForm onClose={() => setOpenForm(undefined)} user={user} />}
+      {openForm === 'update' && (
+        <UserForm
+          onClose={() => setOpenForm(undefined)}
+          user={user}
+          isInstanceManaged={isInstanceManaged}
+        />
+      )}
     </>
   );
 }
index 798ec561001a23ef66bd70a5b90f5d0ab167bd4b..a605cbdd5c24a8764a2f9d4812f8c3408a90b568 100644 (file)
@@ -33,13 +33,14 @@ import UserScmAccountInput from './UserScmAccountInput';
 export interface Props {
   onClose: () => void;
   user?: RestUserDetailed;
+  isInstanceManaged: boolean;
 }
 
 const BAD_REQUEST = 400;
 const INTERNAL_SERVER_ERROR = 500;
 
 export default function UserForm(props: Props) {
-  const { user } = props;
+  const { user, isInstanceManaged } = props;
 
   const { mutate: createUser } = usePostUserMutation();
   const { mutate: updateUser } = useUpdateUserMutation();
@@ -76,12 +77,14 @@ export default function UserForm(props: Props) {
     const { user } = props;
 
     updateUser(
-      {
-        email: user?.local ? email : undefined,
-        login,
-        name: user?.local ? name : undefined,
-        scmAccount: scmAccounts,
-      },
+      isInstanceManaged
+        ? { scmAccount: scmAccounts, login }
+        : {
+            email: user?.local ? email : undefined,
+            login,
+            name: user?.local ? name : undefined,
+            scmAccount: scmAccounts,
+          },
       { onSuccess: props.onClose, onError: handleError }
     );
   };
@@ -140,7 +143,7 @@ export default function UserForm(props: Props) {
                   minLength={3}
                   name="login"
                   onChange={(e) => setLogin(e.currentTarget.value)}
-                  required
+                  required={!isInstanceManaged}
                   type="text"
                   value={login}
                 />
@@ -150,17 +153,17 @@ export default function UserForm(props: Props) {
             <div className="modal-field">
               <label htmlFor="create-user-name">
                 {translate('name')}
-                <MandatoryFieldMarker />
+                {!isInstanceManaged && <MandatoryFieldMarker />}
               </label>
               <input
                 autoComplete="off"
                 autoFocus={!!user}
-                disabled={user && !user.local}
+                disabled={(user && !user.local) || isInstanceManaged}
                 id="create-user-name"
                 maxLength={200}
                 name="name"
                 onChange={(e) => setName(e.currentTarget.value)}
-                required
+                required={!isInstanceManaged}
                 type="text"
                 value={name}
               />
@@ -169,7 +172,7 @@ export default function UserForm(props: Props) {
               <label htmlFor="create-user-email">{translate('users.email')}</label>
               <input
                 autoComplete="off"
-                disabled={user && !user.local}
+                disabled={(user && !user.local) || isInstanceManaged}
                 id="create-user-email"
                 maxLength={100}
                 name="email"
index e0f314eb7429badb125f640ac0132f4aa51f7249..e1b74efb7a4c62902b21f348abd3ebc52ba99f6e 100644 (file)
@@ -42,7 +42,6 @@ export default function UserListItem(props: UserListItemProps) {
   const {
     name,
     login,
-    managed,
     groupsCount,
     tokensCount,
     avatar,
@@ -100,11 +99,9 @@ export default function UserListItem(props: UserListItemProps) {
         </ButtonIcon>
       </td>
 
-      {(manageProvider === undefined || !managed) && (
-        <td className="thin nowrap text-right text-middle">
-          <UserActions user={user} manageProvider={manageProvider} />
-        </td>
-      )}
+      <td className="thin nowrap text-right text-middle">
+        <UserActions user={user} manageProvider={manageProvider} />
+      </td>
 
       {openTokenForm && <TokensFormModal onClose={() => setOpenTokenForm(false)} user={user} />}
       {openGroupForm && <GroupsForm onClose={() => setOpenGroupForm(false)} user={user} />}
index 4272c60e14893ce17081e6a80eafdc9dea1a3bb0..2fcfc18f68066892b8ce2208fe39134312c2dc67 100644 (file)
@@ -323,6 +323,7 @@ since_previous_version_detailed.short=\u0394 version ({0})
 this_name_is_already_taken=This name is already taken.
 tooltip_is_interactive=This is a tooltip with interactive elements. Use the TAB key to cycle through the interactive elements.
 update_details=Update details
+update_scm=Update SCM details
 work_duration.x_days={0}d
 work_duration.x_hours={0}h
 work_duration.x_minutes={0}min