diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts new file mode 100644 index 00000000000..b90592a86c4 --- /dev/null +++ b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts @@ -0,0 +1,97 @@ +import { emailRegEx } from '@logto/core-kit'; +import { conditional, conditionalArray, conditionalString } from '@silverhand/essentials'; +import { useCallback, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import useSWR from 'swr'; + +import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api'; +import { type TenantMemberResponse } from '@/cloud/types/router'; +import { TenantsContext } from '@/contexts/TenantsProvider'; +import { type RequestError } from '@/hooks/use-api'; + +import { type InviteeEmailItem } from '../types'; + +const useEmailInputUtils = () => { + const cloudApi = useAuthedCloudApi(); + const { currentTenantId } = useContext(TenantsContext); + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const { data: existingMembers = [] } = useSWR( + `api/tenant/${currentTenantId}/members`, + async () => + cloudApi.get('/api/tenants/:tenantId/members', { params: { tenantId: currentTenantId } }) + ); + + /** + * Find duplicated and invalid formatted email addresses. + * + * @param emails Array of email emails. + * @returns + */ + const findDuplicatedOrInvalidEmails = useCallback( + (emails: string[] = []) => { + const duplicatedEmails = new Set(); + const invalidEmails = new Set(); + const validEmails = new Set( + existingMembers.map(({ primaryEmail }) => primaryEmail ?? '').filter(Boolean) + ); + + for (const email of emails) { + if (!emailRegEx.test(email)) { + invalidEmails.add(email); + } + + if (validEmails.has(email)) { + duplicatedEmails.add(email); + } else { + validEmails.add(email); + } + } + + return { + duplicatedEmails, + invalidEmails, + }; + }, + [existingMembers] + ); + + const parseEmailOptions = useCallback( + ( + inputValues: InviteeEmailItem[] + ): { + values: InviteeEmailItem[]; + errorMessage?: string; + } => { + const { duplicatedEmails, invalidEmails } = findDuplicatedOrInvalidEmails( + inputValues.map((email) => email.value) + ); + // Show error message and update the inputs' status for error display. + if (duplicatedEmails.size > 0 || invalidEmails.size > 0) { + return { + values: inputValues.map(({ status, ...rest }) => ({ + ...rest, + ...conditional( + (duplicatedEmails.has(rest.value) || invalidEmails.has(rest.value)) && { + status: 'info', + } + ), + })), + errorMessage: conditionalArray( + conditionalString(duplicatedEmails.size > 0 && t('tenant_members.errors.user_exists')), + conditionalString(invalidEmails.size > 0 && t('tenant_members.errors.invalid_email')) + ).join(' '), + }; + } + + return { values: inputValues }; + }, + [findDuplicatedOrInvalidEmails] + ); + + return { + parseEmailOptions, + }; +}; + +export default useEmailInputUtils; diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.tsx b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.tsx index 393e55035c1..78cd717c93e 100644 --- a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.tsx @@ -12,8 +12,8 @@ import { onKeyDownHandler } from '@/utils/a11y'; import type { InviteeEmailItem, InviteMemberForm } from '../types'; +import useEmailInputUtils from './hooks'; import * as styles from './index.module.scss'; -import { emailOptionsParser } from './utils'; type Props = { className?: string; @@ -34,9 +34,10 @@ function InviteEmailsInput({ const [focusedValueId, setFocusedValueId] = useState>(null); const [currentValue, setCurrentValue] = useState(''); const { setError, clearErrors } = useFormContext(); + const { parseEmailOptions } = useEmailInputUtils(); const onChange = (values: InviteeEmailItem[]) => { - const { values: parsedValues, errorMessage } = emailOptionsParser(values); + const { values: parsedValues, errorMessage } = parseEmailOptions(values); if (errorMessage) { setError('emails', { type: 'custom', message: errorMessage }); } else { diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/utils.ts b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/utils.ts deleted file mode 100644 index 41edc4645de..00000000000 --- a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/utils.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { emailRegEx } from '@logto/core-kit'; -import { conditional, conditionalArray, conditionalString } from '@silverhand/essentials'; -import { t as globalTranslate } from 'i18next'; - -import { type InviteeEmailItem } from '../types'; - -export const emailOptionsParser = ( - inputValues: InviteeEmailItem[] -): { - values: InviteeEmailItem[]; - errorMessage?: string; -} => { - const { duplicatedEmails, invalidEmails } = findDuplicatedOrInvalidEmails( - inputValues.map((email) => email.value) - ); - // Show error message and update the inputs' status for error display. - if (duplicatedEmails.size > 0 || invalidEmails.size > 0) { - return { - values: inputValues.map(({ status, ...rest }) => ({ - ...rest, - ...conditional( - (duplicatedEmails.has(rest.value) || invalidEmails.has(rest.value)) && { - status: 'info', - } - ), - })), - errorMessage: conditionalArray( - conditionalString( - duplicatedEmails.size > 0 && - globalTranslate('admin_console.tenant_members.errors.user_exists') - ), - conditionalString( - invalidEmails.size > 0 && - globalTranslate('admin_console.tenant_members.errors.invalid_email') - ) - ).join(' '), - }; - } - - return { values: inputValues }; -}; - -/** - * Find duplicated and invalid formatted email addresses. - * - * @param emails Array of email emails. - * @returns - */ -const findDuplicatedOrInvalidEmails = (emails: string[] = []) => { - const duplicatedEmails = new Set(); - const invalidEmails = new Set(); - const validEmails = new Set(); - - for (const email of emails) { - if (!emailRegEx.test(email)) { - invalidEmails.add(email); - } - - if (validEmails.has(email)) { - duplicatedEmails.add(email); - } - } - - return { - duplicatedEmails, - invalidEmails, - }; -}; diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/InviteMemberModal/index.tsx b/packages/console/src/pages/TenantSettings/TenantMembers/InviteMemberModal/index.tsx index a3197807e6a..69db6b0520f 100644 --- a/packages/console/src/pages/TenantSettings/TenantMembers/InviteMemberModal/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantMembers/InviteMemberModal/index.tsx @@ -15,7 +15,7 @@ import Select, { type Option } from '@/ds-components/Select'; import * as modalStyles from '@/scss/modal.module.scss'; import InviteEmailsInput from '../InviteEmailsInput'; -import { emailOptionsParser } from '../InviteEmailsInput/utils'; +import useEmailInputUtils from '../InviteEmailsInput/hooks'; import { type InviteMemberForm } from '../types'; type Props = { @@ -37,6 +37,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) { const [isLoading, setIsLoading] = useState(false); const cloudApi = useAuthedCloudApi(); + const { parseEmailOptions } = useEmailInputUtils(); const formMethods = useForm({ defaultValues: { @@ -123,7 +124,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) { if (value.length === 0) { return t('errors.email_required'); } - const { errorMessage } = emailOptionsParser(value); + const { errorMessage } = parseEmailOptions(value); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return errorMessage || true; }, diff --git a/packages/phrases/src/locales/de/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/de/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/en/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/en/translation/admin-console/tenant-members.ts index 5acf64b2fa0..c4bdafe913e 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/tenant-members.ts @@ -50,7 +50,7 @@ const tenant_members = { }, errors: { email_required: 'Invitee email is required.', - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.', max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.', }, diff --git a/packages/phrases/src/locales/es/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/es/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/es/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/es/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/fr/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/fr/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/it/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/it/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/it/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/it/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/ja/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/ja/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/ja/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/ja/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/ko/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/ko/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/pl-pl/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/pl-pl/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/pl-pl/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/pl-pl/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/ru/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/ru/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/ru/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/ru/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/zh-hk/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/zh-hk/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/zh-hk/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/zh-hk/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/zh-tw/translation/admin-console/tenant-members.ts b/packages/phrases/src/locales/zh-tw/translation/admin-console/tenant-members.ts index 9a9531f3542..0b64b99cd40 100644 --- a/packages/phrases/src/locales/zh-tw/translation/admin-console/tenant-members.ts +++ b/packages/phrases/src/locales/zh-tw/translation/admin-console/tenant-members.ts @@ -87,7 +87,7 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already in this organization.', + user_exists: 'This user is already invited to this organization.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */