| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- import { ChangeEvent, FormEvent, useCallback, useState } from 'react'
- import { useTranslation } from 'react-i18next'
- import { deleteJSON, FetchError, postJSON } from '@/infrastructure/fetch-json'
- import getMeta from '../../../utils/meta'
- import { parseEmails } from '../utils/emails'
- import ErrorAlert, { APIError } from './error-alert'
- import UserRow from './user-row'
- import useUserSelection from '../hooks/use-user-selection'
- import { User } from '../../../../../types/group-management/user'
- import { debugConsole } from '@/utils/debugging'
- import OLRow from '@/features/ui/components/ol/ol-row'
- import OLCol from '@/features/ui/components/ol/ol-col'
- import BackButton from '@/features/group-management/components/back-button'
- import OLCard from '@/features/ui/components/ol/ol-card'
- import OLButton from '@/features/ui/components/ol/ol-button'
- import OLFormControl from '@/features/ui/components/ol/ol-form-control'
- import OLFormText from '@/features/ui/components/ol/ol-form-text'
- import OLTable from '@/features/ui/components/ol/ol-table'
- import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
- import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
- type ManagersPaths = {
- addMember: string
- removeMember: string
- }
- type UsersTableProps = {
- groupName: string
- paths: ManagersPaths
- translations: {
- title: string
- subtitle: string
- remove: string
- }
- }
- export function ManagersTable({
- groupName,
- translations,
- paths,
- }: UsersTableProps) {
- const { t } = useTranslation()
- const {
- users,
- setUsers,
- selectedUsers,
- selectAllUsers,
- unselectAllUsers,
- selectUser,
- unselectUser,
- } = useUserSelection(getMeta('ol-users') || [])
- const [emailString, setEmailString] = useState<string>('')
- const [inviteUserInflightCount, setInviteUserInflightCount] = useState(0)
- const [inviteError, setInviteError] = useState<APIError>()
- const [removeMemberInflightCount, setRemoveMemberInflightCount] = useState(0)
- const [removeMemberError, setRemoveMemberError] = useState<APIError>()
- const addManagers = useCallback(
- (e: FormEvent | React.MouseEvent) => {
- e.preventDefault()
- setInviteError(undefined)
- const emails = parseEmails(emailString)
- ;(async () => {
- for (const email of emails) {
- setInviteUserInflightCount(count => count + 1)
- try {
- const data = await postJSON<{ user: User }>(paths.addMember, {
- body: {
- email,
- },
- })
- if (data.user) {
- const alreadyListed = users.find(
- user => user.email === data.user.email
- )
- if (!alreadyListed) {
- setUsers(users => [...users, data.user])
- }
- }
- setEmailString('')
- } catch (error: unknown) {
- debugConsole.error(error)
- setInviteError((error as FetchError)?.data?.error || {})
- }
- setInviteUserInflightCount(count => count - 1)
- }
- })()
- },
- [emailString, paths.addMember, users, setUsers]
- )
- const removeManagers = useCallback(
- (e: React.MouseEvent) => {
- e.preventDefault()
- setRemoveMemberError(undefined)
- ;(async () => {
- for (const user of selectedUsers) {
- let url
- if (paths.removeMember && user._id) {
- url = `${paths.removeMember}/${user._id}`
- } else {
- return
- }
- setRemoveMemberInflightCount(count => count + 1)
- try {
- await deleteJSON(url, {})
- setUsers(users => users.filter(u => u !== user))
- unselectUser(user)
- } catch (error: unknown) {
- debugConsole.error(error)
- setRemoveMemberError((error as FetchError)?.data?.error || {})
- }
- setRemoveMemberInflightCount(count => count - 1)
- }
- })()
- },
- [selectedUsers, unselectUser, setUsers, paths.removeMember]
- )
- const handleSelectAllClick = useCallback(
- (e: React.ChangeEvent<HTMLInputElement>) => {
- if (e.target.checked) {
- selectAllUsers()
- } else {
- unselectAllUsers()
- }
- },
- [selectAllUsers, unselectAllUsers]
- )
- const handleEmailsChange = useCallback(
- (e: ChangeEvent<HTMLInputElement>) => {
- setEmailString(e.target.value)
- },
- [setEmailString]
- )
- return (
- <div className="container">
- <OLRow>
- <OLCol lg={{ span: 10, offset: 1 }}>
- <div className="group-heading" data-testid="group-heading">
- <BackButton
- href="/user/subscription"
- accessibilityLabel={t('back_to_subscription')}
- />
- <h1 className="heading">{groupName || translations.title}</h1>
- </div>
- <OLCard>
- <div
- className="page-header mb-4"
- data-testid="page-header-members-details"
- >
- <div className="pull-right">
- {removeMemberInflightCount > 0 ? (
- <OLButton variant="danger" disabled>
- {t('removing')}…
- </OLButton>
- ) : (
- <>
- {selectedUsers.length > 0 && (
- <OLButton variant="danger" onClick={removeManagers}>
- {translations.remove}
- </OLButton>
- )}
- </>
- )}
- </div>
- <h2 className="h3 mt-0">{translations.subtitle}</h2>
- </div>
- <div className="row-spaced-small">
- <ErrorAlert error={removeMemberError} />
- <OLTable
- className="managed-entities-table managed-entities-list structured-list"
- container={false}
- hover
- data-testid="managed-entities-table"
- >
- <thead>
- <tr>
- <th className="cell-checkbox">
- <OLFormCheckbox
- autoComplete="off"
- onChange={handleSelectAllClick}
- checked={selectedUsers.length === users.length}
- aria-label={t('select_all')}
- data-testid="select-all-checkbox"
- />
- </th>
- <th>{t('email')}</th>
- <th className="cell-name">{t('name')}</th>
- <th className="cell-last-active">
- <OLTooltip
- id="last-active-tooltip"
- description={t('last_active_description')}
- overlayProps={{
- placement: 'left',
- }}
- >
- <span>
- {t('last_active')}
- <sup>(?)</sup>
- </span>
- </OLTooltip>
- </th>
- <th className="cell-accepted-invite">
- {t('accepted_invite')}
- </th>
- </tr>
- </thead>
- <tbody>
- {users.length === 0 && (
- <tr>
- <td className="text-center" colSpan={5}>
- <small>{t('no_members')}</small>
- </td>
- </tr>
- )}
- {users.map(user => (
- <UserRow
- key={user.email}
- user={user}
- selectUser={selectUser}
- unselectUser={unselectUser}
- selected={selectedUsers.includes(user)}
- />
- ))}
- </tbody>
- </OLTable>
- </div>
- <hr />
- <div>
- <p className="small">{t('add_more_managers')}</p>
- <ErrorAlert error={inviteError} />
- <form onSubmit={addManagers} data-testid="add-members-form">
- <OLRow>
- <OLCol xs={6}>
- <OLFormControl
- type="input"
- placeholder="jane@example.com, joe@example.com"
- aria-describedby="add-members-description"
- value={emailString}
- onChange={handleEmailsChange}
- />
- </OLCol>
- <OLCol xs={4}>
- <OLButton
- variant="primary"
- onClick={addManagers}
- isLoading={inviteUserInflightCount > 0}
- >
- {t('add')}
- </OLButton>
- </OLCol>
- </OLRow>
- <OLRow>
- <OLCol xs={8}>
- <OLFormText>
- {t('add_comma_separated_emails_help')}
- </OLFormText>
- </OLCol>
- </OLRow>
- </form>
- </div>
- </OLCard>
- </OLCol>
- </OLRow>
- </div>
- )
- }
|