Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.container {
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
}

.blockForm {
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
}

.actionButtons {
display: flex;
justify-content: flex-end;
gap: 6px;
}

.dialogLoadingSpinnerContainer {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
Using position: absolute for .dialogLoadingSpinnerContainer may cause layout issues if the parent container's dimensions or position change. Consider using a more flexible layout strategy if the spinner's position is not fixed relative to the viewport or other elements.

position: absolute;
width: 64px;
display: flex;
align-items: center;
justify-content: center;
bottom: 0;
height: 64px;
left: 0;

.spinner {
background: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Dialog edit user handle.
*/
import { FC, useCallback, useState } from 'react'
import { useForm, UseFormReturn } from 'react-hook-form'
import { toast } from 'react-toastify'
import _ from 'lodash'
import classNames from 'classnames'

import {
BaseModal,
Button,
ConfirmModal,
InputText,
LoadingSpinner,
} from '~/libs/ui'
import { yupResolver } from '@hookform/resolvers/yup'

import { FormEditUserHandle, UserInfo } from '../../models'
import { formEditUserHandleSchema, handleError } from '../../utils'
import { changeUserHandle } from '../../services'

import styles from './DialogEditUserHandle.module.scss'

interface Props {
className?: string
open: boolean
setOpen: (isOpen: boolean) => void
userInfo: UserInfo
}

export const DialogEditUserHandle: FC<Props> = (props: Props) => {
const [isLoading, setIsLoading] = useState(false)
const handleClose = useCallback(() => {
if (!isLoading) {
props.setOpen(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The handleClose function depends on isLoading, but props.setOpen is also used inside it. Consider adding props.setOpen to the dependency array to ensure the function is updated correctly if props.setOpen changes.

const [showConfirm, setShowConfirm] = useState(false)
const {
register,
handleSubmit,
getValues,
formState: { errors, isValid, isDirty },
}: UseFormReturn<FormEditUserHandle> = useForm({
defaultValues: {
newHandle: '',
},
mode: 'all',
resolver: yupResolver(formEditUserHandleSchema),
})
const onSubmit = useCallback(() => {
setShowConfirm(true)
}, [])
const currentHandle = props.userInfo.handle
const newHandle = getValues().newHandle ?? ''
const confirmMessage = `Are you sure you want to change the handle from "${currentHandle}" `
+ `to "${newHandle}"?`

return (
<>
<BaseModal
allowBodyScroll
blockScroll
title={`Change handle for ${props.userInfo.handle}`}
onClose={handleClose}
open={props.open}
>
<form
className={classNames(styles.container, props.className)}
onSubmit={handleSubmit(onSubmit)}
>
<div className={styles.blockForm}>
<InputText
type='text'
name='currentHandle'
label='Current Handle'
placeholder='Enter'
tabIndex={0}
forceUpdateValue
onChange={_.noop}
disabled
value={props.userInfo.handle}
/>
<InputText
type='text'
name='newHandle'
label='New Handle'
placeholder='Enter'
tabIndex={0}
forceUpdateValue
onChange={_.noop}
error={_.get(errors, 'newHandle.message')}
inputControl={register('newHandle')}
dirty
disabled={isLoading}
/>
</div>
<div className={styles.actionButtons}>
<Button
secondary
size='lg'
onClick={handleClose}
disabled={isLoading}
>
Cancel
</Button>
<Button
type='submit'
primary
size='lg'
disabled={isLoading || !isValid || !isDirty}
>
Continue
</Button>
</div>

{isLoading && (
<div className={styles.dialogLoadingSpinnerContainer}>
<LoadingSpinner className={styles.spinner} />
</div>
)}
</form>
</BaseModal>
<ConfirmModal
allowBodyScroll
blockScroll
focusTrapped={false}
title='Confirm Handle Change'
action='Confirm'
onClose={function onClose() {
setShowConfirm(false)
}}
onConfirm={function onConfirm() {
setShowConfirm(false)
setIsLoading(true)
const data = getValues()
const nextHandle = (data.newHandle ?? '').trim()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The nextHandle is trimmed before being passed to changeUserHandle, but the form validation schema should ensure that the handle is already in the correct format. Consider validating and trimming the handle in the schema to ensure consistency and avoid redundant operations.


changeUserHandle(props.userInfo.handle, nextHandle)
.then(result => {
setIsLoading(false)
toast.success('Handle updated successfully')
props.userInfo.handle = result?.handle ?? nextHandle

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
Directly mutating props.userInfo.handle could lead to unexpected behavior if userInfo is used elsewhere. Consider using a state update or a callback to update the handle in a more controlled manner.

handleClose()
})
.catch(e => {
handleError(e)
setIsLoading(false)
})
}}
open={showConfirm}
>
<div>
<p>{confirmMessage}</p>
</div>
</ConfirmModal>
</>
)
}

export default DialogEditUserHandle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as DialogEditUserHandle } from './DialogEditUserHandle'
17 changes: 17 additions & 0 deletions src/apps/admin/src/lib/components/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {

import { CopyButton } from '../CopyButton'
import { DialogEditUserEmail } from '../DialogEditUserEmail'
import { DialogEditUserHandle } from '../DialogEditUserHandle'
import { DialogEditUserRoles } from '../DialogEditUserRoles'
import { DialogEditUserGroups } from '../DialogEditUserGroups'
import { DialogEditUserSSOLogin } from '../DialogEditUserSSOLogin'
Expand Down Expand Up @@ -82,6 +83,9 @@ export const UsersTable: FC<Props> = props => {
const [showDialogEditUserEmail, setShowDialogEditUserEmail] = useState<
UserInfo | undefined
>()
const [showDialogEditUserHandle, setShowDialogEditUserHandle] = useState<
UserInfo | undefined
>()
const [showDialogEditUserRoles, setShowDialogEditUserRoles] = useState<
UserInfo | undefined
>()
Expand Down Expand Up @@ -309,6 +313,8 @@ export const UsersTable: FC<Props> = props => {
function onSelectOption(item: string): void {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Consider using a switch statement instead of multiple else if conditions for better readability and maintainability. This will make it easier to add or modify options in the future.

if (item === 'Primary Email') {
setShowDialogEditUserEmail(data)
} else if (item === 'Change Handle') {
setShowDialogEditUserHandle(data)
} else if (item === 'Roles') {
setShowDialogEditUserRoles(data)
} else if (item === 'Groups') {
Expand Down Expand Up @@ -344,6 +350,7 @@ export const UsersTable: FC<Props> = props => {
<DropdownMenuButton
options={[
'Primary Email',
'Change Handle',
'Roles',
'Groups',
'Terms',
Expand All @@ -365,6 +372,7 @@ export const UsersTable: FC<Props> = props => {
<DropdownMenuButton
options={[
'Primary Email',
'Change Handle',
'Roles',
'Groups',
'Terms',
Expand Down Expand Up @@ -449,6 +457,15 @@ export const UsersTable: FC<Props> = props => {
userInfo={showDialogEditUserEmail}
/>
)}
{showDialogEditUserHandle && (
<DialogEditUserHandle
open
setOpen={function setOpen() {
setShowDialogEditUserHandle(undefined)
}}
userInfo={showDialogEditUserHandle}
/>
)}
{showDialogEditUserRoles && (
<DialogEditUserRoles
open
Expand Down
6 changes: 6 additions & 0 deletions src/apps/admin/src/lib/models/FormEditUserHandle.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Model for edit user handle form
*/
export interface FormEditUserHandle {
newHandle: string
}
1 change: 1 addition & 0 deletions src/apps/admin/src/lib/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './challenge-management'
export * from './FormEditUserEmail.model'
export * from './FormEditUserHandle.model'
export * from './FormEditUserGroup.model'
export * from './FormEditUserRole.model'
export * from './FormSearchByKey.model'
Expand Down
20 changes: 20 additions & 0 deletions src/apps/admin/src/lib/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,26 @@ export const updateUserEmail = async (
)
}

/**
* Update member handle.
* @param handle current handle.
* @param newHandle new handle.
* @returns resolves to member info
*/
export const changeUserHandle = async (
handle: string,
newHandle: string,
): Promise<MemberInfo> => {
const payload = {
newHandle: newHandle.trim(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider validating newHandle before trimming and sending it in the payload. This could prevent sending invalid data to the API and improve error handling.

}

return xhrPatchAsync<typeof payload, MemberInfo>(
`${EnvironmentConfig.API.V6}/members/${encodeURIComponent(handle)}/change_handle`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
Ensure that handle is validated before being used in the URL. This prevents potential issues with malformed URLs or unintended API calls.

payload,
)
}

/**
* Update user status.
* @param userId user id.
Expand Down
11 changes: 11 additions & 0 deletions src/apps/admin/src/lib/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
FormEditClient,
FormEditUserEmail,
FormEditUserGroup,
FormEditUserHandle,
FormEditUserRole,
FormGroupMembersFilters,
FormNewBillingAccountResource,
Expand Down Expand Up @@ -442,6 +443,16 @@ export const formEditUserEmailSchema: Yup.ObjectSchema<FormEditUserEmail>
.required('Email address is required.'),
})

/**
* validation schema for form edit user handle
*/
export const formEditUserHandleSchema: Yup.ObjectSchema<FormEditUserHandle>
= Yup.object({
newHandle: Yup.string()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider adding a .min(3, 'Handle must be at least 3 characters long.') constraint to ensure the handle is not too short, which could help prevent potential issues with handle uniqueness or readability.

.trim()
.required('Handle is required.'),
})

/**
* validation schema for form edit sso user login
*/
Expand Down
Loading