diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts index b90592a86..0ff9610b7 100644 --- a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts +++ b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts @@ -1,11 +1,12 @@ import { emailRegEx } from '@logto/core-kit'; +import { OrganizationInvitationStatus } from '@logto/schemas'; 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 { type TenantInvitationResponse, type TenantMemberResponse } from '@/cloud/types/router'; import { TenantsContext } from '@/contexts/TenantsProvider'; import { type RequestError } from '@/hooks/use-api'; @@ -22,6 +23,12 @@ const useEmailInputUtils = () => { cloudApi.get('/api/tenants/:tenantId/members', { params: { tenantId: currentTenantId } }) ); + const { data: existingInvitations = [] } = useSWR( + 'api/tenants/:tenantId/invitations', + async () => + cloudApi.get('/api/tenants/:tenantId/invitations', { params: { tenantId: currentTenantId } }) + ); + /** * Find duplicated and invalid formatted email addresses. * @@ -31,18 +38,31 @@ const useEmailInputUtils = () => { const findDuplicatedOrInvalidEmails = useCallback( (emails: string[] = []) => { const duplicatedEmails = new Set(); + const conflictMemberEmails = new Set(); + const conflictInvitationEmails = new Set(); const invalidEmails = new Set(); - const validEmails = new Set( + const validEmails = new Set(); + + const existingMemberEmails = new Set( existingMembers.map(({ primaryEmail }) => primaryEmail ?? '').filter(Boolean) ); + const existingInvitationEmails = new Set( + existingInvitations + .filter(({ status }) => status === OrganizationInvitationStatus.Pending) + .map(({ invitee }) => invitee) + ); for (const email of emails) { if (!emailRegEx.test(email)) { invalidEmails.add(email); } - + // Check email collisions if (validEmails.has(email)) { duplicatedEmails.add(email); + } else if (existingInvitationEmails.has(email)) { + conflictInvitationEmails.add(email); + } else if (existingMemberEmails.has(email)) { + conflictMemberEmails.add(email); } else { validEmails.add(email); } @@ -50,10 +70,12 @@ const useEmailInputUtils = () => { return { duplicatedEmails, + conflictMemberEmails, + conflictInvitationEmails, invalidEmails, }; }, - [existingMembers] + [existingInvitations, existingMembers] ); const parseEmailOptions = useCallback( @@ -63,30 +85,44 @@ const useEmailInputUtils = () => { values: InviteeEmailItem[]; errorMessage?: string; } => { - const { duplicatedEmails, invalidEmails } = findDuplicatedOrInvalidEmails( - inputValues.map((email) => email.value) - ); + const { duplicatedEmails, conflictInvitationEmails, conflictMemberEmails, 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) { + if ( + duplicatedEmails.size > 0 || + conflictInvitationEmails.size > 0 || + conflictMemberEmails.size > 0 || + invalidEmails.size > 0 + ) { return { values: inputValues.map(({ status, ...rest }) => ({ ...rest, ...conditional( - (duplicatedEmails.has(rest.value) || invalidEmails.has(rest.value)) && { - status: 'info', + (duplicatedEmails.has(rest.value) || + conflictInvitationEmails.has(rest.value) || + conflictMemberEmails.has(rest.value) || + invalidEmails.has(rest.value)) && { + status: 'error', } ), })), errorMessage: conditionalArray( - conditionalString(duplicatedEmails.size > 0 && t('tenant_members.errors.user_exists')), + conditionalString(duplicatedEmails.size > 0 && t('tenant_members.errors.email_exists')), + conditionalString( + conflictInvitationEmails.size > 0 && + t('tenant_members.errors.pending_invitation_exists') + ), + conditionalString( + conflictMemberEmails.size > 0 && t('tenant_members.errors.member_exists') + ), conditionalString(invalidEmails.size > 0 && t('tenant_members.errors.invalid_email')) - ).join(' '), + ).join('\n'), }; } return { values: inputValues }; }, - [findDuplicatedOrInvalidEmails] + [findDuplicatedOrInvalidEmails, t] ); return { diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.module.scss b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.module.scss index 389e0f1e3..a3158db2f 100644 --- a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.module.scss +++ b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.module.scss @@ -39,7 +39,7 @@ background: var(--color-overlay-default-focused); } - &.info { + &.error { background: var(--color-error-container); } } @@ -91,4 +91,5 @@ canvas { font: var(--font-body-2); color: var(--color-error); margin-top: _.unit(1); + white-space: pre-wrap; } diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.tsx b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.tsx index 25512f51f..77ae650bd 100644 --- a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/index.tsx @@ -71,7 +71,7 @@ function InviteEmailsInput({ { value, id: generateStandardShortId(), - ...conditional(!emailRegEx.test(value) && { status: 'info' }), + ...conditional(!emailRegEx.test(value) && { status: 'error' }), }, ]; onChange(newValues); @@ -129,7 +129,7 @@ function InviteEmailsInput({ ref={ref} placeholder={conditional(values.length === 0 && placeholder)} value={currentValue} - style={{ minWidth: `${minInputWidth}px` }} + style={{ minWidth: `${minInputWidth + 10}px` }} onKeyDown={(event) => { if (event.key === 'Backspace' && currentValue === '') { if (focusedValueId) { diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/types.ts b/packages/console/src/pages/TenantSettings/TenantMembers/types.ts index 3b91a8999..9ec730819 100644 --- a/packages/console/src/pages/TenantSettings/TenantMembers/types.ts +++ b/packages/console/src/pages/TenantSettings/TenantMembers/types.ts @@ -14,7 +14,7 @@ export type InviteeEmailItem = { * - undefined: valid email * - 'info': duplicated email or invalid email format. */ - status?: Extract; + status?: Extract; }; export type InviteMemberForm = { 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 5f1cf508e..b5c7137ad 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,10 @@ const tenant_members = { }, errors: { email_required: 'Invitee email is required.', - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + member_exists: 'This user is already a member of this organization.', + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** 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 2c8f036ae..c2947d0ce 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,12 @@ const tenant_members = { /** UNTRANSLATED */ email_required: 'Invitee email is required.', /** UNTRANSLATED */ - user_exists: 'This user is already invited to this organization.', + email_exists: 'Email address already exists.', + /** UNTRANSLATED */ + member_exists: 'This user is already a member of this organization.', + /** UNTRANSLATED */ + pending_invitation_exists: + 'Pending invitation exists. Delete related email or revoke the invitation.', /** UNTRANSLATED */ invalid_email: 'Email address is invalid. Please make sure it is in the right format.', /** UNTRANSLATED */