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.

[⚠️ maintainability]
Using position: absolute; for .dialogLoadingSpinnerContainer without specifying a top, right, or bottom value other than bottom: 0; can lead to layout issues if the parent container's size changes. Consider ensuring the parent container has a defined size or constraints to prevent unexpected behavior.

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 is missing props.setOpen in its dependency array. This could lead to stale closures if setOpen changes. Consider adding props.setOpen to the dependency array.

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

Choose a reason for hiding this comment

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

[⚠️ correctness]
The newHandle value is derived from getValues() outside of the form submission or confirmation logic. This could lead to stale data if the form state changes before submission. Consider moving this logic inside the onSubmit or onConfirm functions.

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')}

Choose a reason for hiding this comment

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

[💡 maintainability]
The inputControl prop is not a standard prop for InputText. If this is a custom implementation, ensure it is correctly handled within the InputText component. Otherwise, consider removing or renaming it to avoid confusion.

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()

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 side effects, especially if userInfo is used elsewhere. Consider using a state update function or a context provider to manage this state change.

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 {
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() {

Choose a reason for hiding this comment

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

[💡 maintainability]
Consider using a consistent naming convention for the setOpen function across all dialog components. Currently, the setOpen function is defined inline with a different style compared to other similar functions in the file. This could improve maintainability by ensuring consistency.

setShowDialogEditUserHandle(undefined)
}}
userInfo={showDialogEditUserHandle}
/>
)}
{showDialogEditUserRoles && (
<DialogEditUserRoles
open
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
border: $border-xs solid $black-40;
border-radius: 0 0 $sp-1 $sp-1;
padding: $sp-2 0;
max-height: 230px;
max-height: 320px;
overflow: auto;
:global {
ul > li {
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

Choose a reason for hiding this comment

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

[⚠️ correctness]
Consider adding validation logic to ensure that newHandle meets expected constraints (e.g., length, allowed characters) to prevent potential issues with invalid user handles.

}
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 ensures that the handle meets any necessary criteria (e.g., length, character restrictions) before making the API call.

}

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 encoding and using it in the URL. This prevents potential issues with invalid or malicious input.

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 has a minimum length. This can help prevent invalid or too short handles from being accepted.

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

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