-
Notifications
You must be signed in to change notification settings - Fork 21
Initial change handle functionality #1434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
| 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]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| 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')} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| 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' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' | ||
|
|
@@ -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 | ||
| >() | ||
|
|
@@ -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') { | ||
|
|
@@ -344,6 +350,7 @@ export const UsersTable: FC<Props> = props => { | |
| <DropdownMenuButton | ||
| options={[ | ||
| 'Primary Email', | ||
| 'Change Handle', | ||
| 'Roles', | ||
| 'Groups', | ||
| 'Terms', | ||
|
|
@@ -365,6 +372,7 @@ export const UsersTable: FC<Props> = props => { | |
| <DropdownMenuButton | ||
| options={[ | ||
| 'Primary Email', | ||
| 'Change Handle', | ||
| 'Roles', | ||
| 'Groups', | ||
| 'Terms', | ||
|
|
@@ -449,6 +457,15 @@ export const UsersTable: FC<Props> = props => { | |
| userInfo={showDialogEditUserEmail} | ||
| /> | ||
| )} | ||
| {showDialogEditUserHandle && ( | ||
| <DialogEditUserHandle | ||
| open | ||
| setOpen={function setOpen() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| setShowDialogEditUserHandle(undefined) | ||
| }} | ||
| userInfo={showDialogEditUserHandle} | ||
| /> | ||
| )} | ||
| {showDialogEditUserRoles && ( | ||
| <DialogEditUserRoles | ||
| open | ||
|
|
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| } | ||
|
|
||
| return xhrPatchAsync<typeof payload, MemberInfo>( | ||
| `${EnvironmentConfig.API.V6}/members/${encodeURIComponent(handle)}/change_handle`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| payload, | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Update user status. | ||
| * @param userId user id. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { | |
| FormEditClient, | ||
| FormEditUserEmail, | ||
| FormEditUserGroup, | ||
| FormEditUserHandle, | ||
| FormEditUserRole, | ||
| FormGroupMembersFilters, | ||
| FormNewBillingAccountResource, | ||
|
|
@@ -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() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| .trim() | ||
| .required('Handle is required.'), | ||
| }) | ||
|
|
||
| /** | ||
| * validation schema for form edit sso user login | ||
| */ | ||
|
|
||
There was a problem hiding this comment.
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.dialogLoadingSpinnerContainerwithout specifying atop,right, orbottomvalue other thanbottom: 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.