From 64eed3eb804ecf58df4a135eb3b56e81c1d20b4a Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 4 Dec 2025 13:47:12 +0200 Subject: [PATCH 1/4] PM-2670 - fix checkoint screener --- .../ChallengeDetailsContent/TabContentCheckpoint.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentCheckpoint.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentCheckpoint.tsx index b29473055..f30b53dda 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentCheckpoint.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentCheckpoint.tsx @@ -36,6 +36,7 @@ export const TabContentCheckpoint: FC = (props: Props) => { checkpointReviewerResourceIds, checkpointScreenerResourceIds, isPrivilegedRole, + hasCheckpointScreenerRole, }: useRoleProps = useRole() const myMemberIds = useMemo>( @@ -78,7 +79,8 @@ export const TabContentCheckpoint: FC = (props: Props) => { () => { const baseRows = props.checkpoint ?? [] - if (isPrivilegedRole || (isChallengeCompleted && hasPassedCheckpointScreeningThreshold)) { + const canSeeAll = isPrivilegedRole || hasCheckpointScreenerRole + if (canSeeAll || (isChallengeCompleted && hasPassedCheckpointScreeningThreshold)) { return baseRows } From b8a59da10772f581829be6010748a2d525b70944 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 5 Dec 2025 09:50:48 +1100 Subject: [PATCH 2/4] Initial delete user functionality --- .circleci/config.yml | 1 + .../DialogDeleteUser.module.scss | 18 ++++ .../DialogDeleteUser/DialogDeleteUser.tsx | 96 ++++++++++++++++++ .../lib/components/DialogDeleteUser/index.ts | 1 + .../UsersTable/UsersTable.module.scss | 2 +- .../lib/components/UsersTable/UsersTable.tsx | 99 +++++++++++++------ .../admin/src/lib/hooks/useManageUsers.ts | 94 +++++++++++++++++- .../admin/src/lib/services/user.service.ts | 15 +++ .../UserManagementPage/UserManagementPage.tsx | 4 + 9 files changed, 299 insertions(+), 31 deletions(-) create mode 100644 src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.module.scss create mode 100644 src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.tsx create mode 100644 src/apps/admin/src/lib/components/DialogDeleteUser/index.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index db8617ea2..fbb4b55d3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -233,6 +233,7 @@ workflows: - feat/v6 - pm-2074_1 - feat/ai-workflows + - delete_user - deployQa: context: org-global diff --git a/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.module.scss b/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.module.scss new file mode 100644 index 000000000..c9bec7f6e --- /dev/null +++ b/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.module.scss @@ -0,0 +1,18 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; + gap: $sp-4; +} + +.description { + white-space: pre-line; +} + +.actions { + display: flex; + justify-content: flex-end; + gap: $sp-3; + margin-top: $sp-4; +} diff --git a/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.tsx b/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.tsx new file mode 100644 index 000000000..0ad6af2f1 --- /dev/null +++ b/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.tsx @@ -0,0 +1,96 @@ +import { FC, useCallback, useEffect, useState } from 'react' +import { BaseModal, Button, InputText } from '~/libs/ui' +import classNames from 'classnames' + +import { UserInfo } from '../../models' + +import styles from './DialogDeleteUser.module.scss' + +interface Props { + className?: string + open: boolean + setOpen: (isOpen: boolean) => void + userInfo: UserInfo + isLoading?: boolean + onDelete: (ticketUrl: string) => void +} + +export const DialogDeleteUser: FC = (props: Props) => { + const [ticketUrl, setTicketUrl] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + if (props.open) { + setTicketUrl('') + setError('') + } + }, [props.open]) + + const handleClose = useCallback(() => { + if (!props.isLoading) { + props.setOpen(false) + } + }, [props.isLoading, props.setOpen]) + + const handleConfirm = useCallback(() => { + if (!ticketUrl.trim()) { + setError('Delete ticket URL is required') + return + } + setError('') + props.onDelete(ticketUrl.trim()) + }, [props, ticketUrl]) + + const description = `Are you sure you want to DELETE user ${props.userInfo.handle} with email address ${props.userInfo.email}. If you are sure, please enter the associated delete request ticket URL below` + + return ( + +
+

{description}

+ { + setTicketUrl(event.target.value) + if (error) { + setError('') + } + }} + disabled={props.isLoading} + /> + +
+ + +
+
+
+ ) +} + +export default DialogDeleteUser diff --git a/src/apps/admin/src/lib/components/DialogDeleteUser/index.ts b/src/apps/admin/src/lib/components/DialogDeleteUser/index.ts new file mode 100644 index 000000000..d1271c6ba --- /dev/null +++ b/src/apps/admin/src/lib/components/DialogDeleteUser/index.ts @@ -0,0 +1 @@ +export { DialogDeleteUser } from './DialogDeleteUser' diff --git a/src/apps/admin/src/lib/components/UsersTable/UsersTable.module.scss b/src/apps/admin/src/lib/components/UsersTable/UsersTable.module.scss index d81f26842..0fd56a9bb 100644 --- a/src/apps/admin/src/lib/components/UsersTable/UsersTable.module.scss +++ b/src/apps/admin/src/lib/components/UsersTable/UsersTable.module.scss @@ -57,7 +57,7 @@ } .blockColumnAction { - width: 240px; + width: 320px; @include ltelg { width: 60px; diff --git a/src/apps/admin/src/lib/components/UsersTable/UsersTable.tsx b/src/apps/admin/src/lib/components/UsersTable/UsersTable.tsx index 8b481359f..1056702b3 100644 --- a/src/apps/admin/src/lib/components/UsersTable/UsersTable.tsx +++ b/src/apps/admin/src/lib/components/UsersTable/UsersTable.tsx @@ -26,6 +26,7 @@ import { DialogEditUserSSOLogin } from '../DialogEditUserSSOLogin' import { DialogEditUserTerms } from '../DialogEditUserTerms' import { DialogEditUserStatus } from '../DialogEditUserStatus' import { DialogUserStatusHistory } from '../DialogUserStatusHistory' +import { DialogDeleteUser } from '../DialogDeleteUser' import { DropdownMenuButton } from '../common/DropdownMenuButton' import { useTableFilterLocal, useTableFilterLocalProps } from '../../hooks' import { TABLE_DATE_FORMAT } from '../../../config/index.config' @@ -43,12 +44,18 @@ interface Props { totalPages: number onPageChange: (page: number) => void updatingStatus: { [key: string]: boolean } + deletingUsers: { [key: string]: boolean } doUpdateStatus: ( userInfo: UserInfo, newStatus: string, comment: string, onSuccess?: () => void, ) => void + doDeleteUser: ( + userInfo: UserInfo, + ticketUrl: string, + onSuccess?: () => void, + ) => void } export const UsersTable: FC = props => { @@ -100,6 +107,9 @@ export const UsersTable: FC = props => { const [showDialogStatusHistory, setShowDialogStatusHistory] = useState< UserInfo | undefined >() + const [showDialogDeleteUser, setShowDialogDeleteUser] = useState< + UserInfo | undefined + >() const { width: screenWidth }: WindowSize = useWindowSize() const updatingStatusBool = useMemo( @@ -294,6 +304,8 @@ export const UsersTable: FC = props => { columnId: 'Action', label: 'Action', renderer: (data: UserInfo) => { + const isDeleting = props.deletingUsers?.[data.id] === true + function onSelectOption(item: string): void { if (item === 'Primary Email') { setShowDialogEditUserEmail(data) @@ -321,27 +333,29 @@ export const UsersTable: FC = props => { data, message: confirmation, }) + } else if (item === 'Delete') { + setShowDialogDeleteUser(data) } } return (
{isTablet ? ( - - @@ -367,24 +381,34 @@ export const UsersTable: FC = props => { {data.active ? ( -
@@ -393,7 +417,7 @@ export const UsersTable: FC = props => { type: 'action', }, ], - [isTablet, isMobile], + [isTablet, isMobile, props.deletingUsers, props.updatingStatus], ) return ( @@ -473,6 +497,23 @@ export const UsersTable: FC = props => { isLoading={updatingStatusBool} /> )} + {showDialogDeleteUser && ( + setShowDialogDeleteUser(undefined), + ) + }} + /> + )} {showDialogStatusHistory && ( (userInfo.id !== item.id ? item : userInfo)), } } @@ -118,6 +132,39 @@ const reducer = ( } } + case UsersActionType.DELETE_USER_INIT: { + return { + ...previousState, + deletingUsers: { + ...previousState.deletingUsers, + [action.payload]: true, + }, + } + } + + case UsersActionType.DELETE_USER_DONE: { + const userId = action.payload + return { + ...previousState, + deletingUsers: { + ...previousState.deletingUsers, + [userId]: false, + }, + users: previousState.users.filter(user => user.id !== userId), + total: Math.max(previousState.total - 1, 0), + } + } + + case UsersActionType.DELETE_USER_FAILED: { + return { + ...previousState, + deletingUsers: { + ...previousState.deletingUsers, + [action.payload]: false, + }, + } + } + default: { return previousState } @@ -132,12 +179,18 @@ export interface useManageUsersProps { totalPages: number onPageChange: (page: number) => void updatingStatus: { [key: string]: boolean } + deletingUsers: { [key: string]: boolean } doUpdateStatus: ( userInfo: UserInfo, newStatus: string, comment: string, onSuccess?: () => void, ) => void + doDeleteUser: ( + userInfo: UserInfo, + ticketUrl: string, + onSuccess?: () => void, + ) => void } /** @@ -151,6 +204,7 @@ export function useManageUsers(): useManageUsersProps { total: 0, totalPages: 0, updatingStatus: {}, + deletingUsers: {}, users: [], }) const filterRef = useRef('') @@ -260,13 +314,51 @@ export function useManageUsers(): useManageUsersProps { [dispatch], ) + const doDeleteUser = useCallback( + (userInfo: UserInfo, ticketUrl: string, onSuccess?: () => void) => { + if (!ticketUrl) { + toast.error('Delete ticket URL is required', { + toastId: 'Delete user', + }) + return + } + + dispatch({ + payload: userInfo.id, + type: UsersActionType.DELETE_USER_INIT, + }) + + deleteUser(userInfo.handle, ticketUrl) + .then(() => { + dispatch({ + payload: userInfo.id, + type: UsersActionType.DELETE_USER_DONE, + }) + toast.success('User deleted successfully', { + toastId: 'Delete user', + }) + onSuccess?.() + }) + .catch(e => { + dispatch({ + payload: userInfo.id, + type: UsersActionType.DELETE_USER_FAILED, + }) + handleError(e) + }) + }, + [dispatch], + ) + return { doSearchUsers, + doDeleteUser, doUpdateStatus, isLoading: state.isLoading, onPageChange, page: state.page, totalPages: state.totalPages, + deletingUsers: state.deletingUsers, updatingStatus: state.updatingStatus, users: state.users, } diff --git a/src/apps/admin/src/lib/services/user.service.ts b/src/apps/admin/src/lib/services/user.service.ts index 8fc9a3e0b..a6f375dae 100644 --- a/src/apps/admin/src/lib/services/user.service.ts +++ b/src/apps/admin/src/lib/services/user.service.ts @@ -9,6 +9,7 @@ import { xhrPatchAsync, xhrPostAsync, xhrPutAsync, + xhrRequestAsync, } from '~/libs/core' import { @@ -342,3 +343,17 @@ export const deleteSSOUserLogin = async ( ) return response } + +/** + * Permanently delete a user profile. + * @param handle user handle. + * @param ticketUrl delete request ticket url. + */ +export const deleteUser = async ( + handle: string, + ticketUrl: string, +): Promise => xhrRequestAsync<{ ticketUrl: string }, void>({ + data: { ticketUrl }, + method: 'DELETE', + url: `${EnvironmentConfig.API.V6}/members/${encodeURIComponent(handle)}`, +}) diff --git a/src/apps/admin/src/user-management/UserManagementPage/UserManagementPage.tsx b/src/apps/admin/src/user-management/UserManagementPage/UserManagementPage.tsx index 005e2abca..64ac93d05 100644 --- a/src/apps/admin/src/user-management/UserManagementPage/UserManagementPage.tsx +++ b/src/apps/admin/src/user-management/UserManagementPage/UserManagementPage.tsx @@ -27,7 +27,9 @@ export const UserManagementPage: FC = (props: Props) => { doSearchUsers, isLoading, updatingStatus, + deletingUsers, doUpdateStatus, + doDeleteUser, page, totalPages, onPageChange, @@ -66,6 +68,8 @@ export const UserManagementPage: FC = (props: Props) => { onPageChange={onPageChange} updatingStatus={updatingStatus} doUpdateStatus={doUpdateStatus} + deletingUsers={deletingUsers} + doDeleteUser={doDeleteUser} /> )} From 37a0370b2b6a75598487c5391240c9f0336d8fbf Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 5 Dec 2025 10:54:50 +1100 Subject: [PATCH 3/4] Lint --- .../DialogDeleteUser/DialogDeleteUser.tsx | 28 ++++++--- .../lib/components/UsersTable/UsersTable.tsx | 58 +++++++++---------- .../admin/src/lib/hooks/useManageUsers.ts | 30 +++++----- 3 files changed, 63 insertions(+), 53 deletions(-) diff --git a/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.tsx b/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.tsx index 0ad6af2f1..7b4de06e5 100644 --- a/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.tsx +++ b/src/apps/admin/src/lib/components/DialogDeleteUser/DialogDeleteUser.tsx @@ -1,7 +1,8 @@ -import { FC, useCallback, useEffect, useState } from 'react' -import { BaseModal, Button, InputText } from '~/libs/ui' +import { ChangeEvent, FC, useCallback, useEffect, useState } from 'react' import classNames from 'classnames' +import { BaseModal, Button, InputText } from '~/libs/ui' + import { UserInfo } from '../../models' import styles from './DialogDeleteUser.module.scss' @@ -37,11 +38,25 @@ export const DialogDeleteUser: FC = (props: Props) => { setError('Delete ticket URL is required') return } + setError('') props.onDelete(ticketUrl.trim()) }, [props, ticketUrl]) - const description = `Are you sure you want to DELETE user ${props.userInfo.handle} with email address ${props.userInfo.email}. If you are sure, please enter the associated delete request ticket URL below` + const handleTicketUrlChange = useCallback( + (event: ChangeEvent) => { + if (error) { + setError('') + } + + setTicketUrl(event.target.value) + }, + [error], + ) + + const description + = `Are you sure you want to DELETE user ${props.userInfo.handle} with email address ${props.userInfo.email}. ` + + 'If you are sure, please enter the associated delete request ticket URL below' return ( = (props: Props) => { placeholder='https://' value={ticketUrl} error={error} - onChange={event => { - setTicketUrl(event.target.value) - if (error) { - setError('') - } - }} + onChange={handleTicketUrlChange} disabled={props.isLoading} /> diff --git a/src/apps/admin/src/lib/components/UsersTable/UsersTable.tsx b/src/apps/admin/src/lib/components/UsersTable/UsersTable.tsx index 1056702b3..6059722de 100644 --- a/src/apps/admin/src/lib/components/UsersTable/UsersTable.tsx +++ b/src/apps/admin/src/lib/components/UsersTable/UsersTable.tsx @@ -341,21 +341,21 @@ export const UsersTable: FC = props => { return (
{isTablet ? ( - - @@ -381,20 +381,20 @@ export const UsersTable: FC = props => { {data.active ? ( -