0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-03 22:15:32 -05:00

refactor(console,phrases): improve invite email input (#5661)

This commit is contained in:
Charles Zhao 2024-04-09 17:34:26 +08:00 committed by GitHub
parent 06083296c1
commit 6e980b3b50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 142 additions and 32 deletions

View file

@ -1,11 +1,12 @@
import { emailRegEx } from '@logto/core-kit'; import { emailRegEx } from '@logto/core-kit';
import { OrganizationInvitationStatus } from '@logto/schemas';
import { conditional, conditionalArray, conditionalString } from '@silverhand/essentials'; import { conditional, conditionalArray, conditionalString } from '@silverhand/essentials';
import { useCallback, useContext } from 'react'; import { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import useSWR from 'swr'; import useSWR from 'swr';
import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api'; 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 { TenantsContext } from '@/contexts/TenantsProvider';
import { type RequestError } from '@/hooks/use-api'; import { type RequestError } from '@/hooks/use-api';
@ -22,6 +23,12 @@ const useEmailInputUtils = () => {
cloudApi.get('/api/tenants/:tenantId/members', { params: { tenantId: currentTenantId } }) cloudApi.get('/api/tenants/:tenantId/members', { params: { tenantId: currentTenantId } })
); );
const { data: existingInvitations = [] } = useSWR<TenantInvitationResponse[], RequestError>(
'api/tenants/:tenantId/invitations',
async () =>
cloudApi.get('/api/tenants/:tenantId/invitations', { params: { tenantId: currentTenantId } })
);
/** /**
* Find duplicated and invalid formatted email addresses. * Find duplicated and invalid formatted email addresses.
* *
@ -31,18 +38,31 @@ const useEmailInputUtils = () => {
const findDuplicatedOrInvalidEmails = useCallback( const findDuplicatedOrInvalidEmails = useCallback(
(emails: string[] = []) => { (emails: string[] = []) => {
const duplicatedEmails = new Set<string>(); const duplicatedEmails = new Set<string>();
const conflictMemberEmails = new Set<string>();
const conflictInvitationEmails = new Set<string>();
const invalidEmails = new Set<string>(); const invalidEmails = new Set<string>();
const validEmails = new Set<string>( const validEmails = new Set<string>();
const existingMemberEmails = new Set<string>(
existingMembers.map(({ primaryEmail }) => primaryEmail ?? '').filter(Boolean) existingMembers.map(({ primaryEmail }) => primaryEmail ?? '').filter(Boolean)
); );
const existingInvitationEmails = new Set<string>(
existingInvitations
.filter(({ status }) => status === OrganizationInvitationStatus.Pending)
.map(({ invitee }) => invitee)
);
for (const email of emails) { for (const email of emails) {
if (!emailRegEx.test(email)) { if (!emailRegEx.test(email)) {
invalidEmails.add(email); invalidEmails.add(email);
} }
// Check email collisions
if (validEmails.has(email)) { if (validEmails.has(email)) {
duplicatedEmails.add(email); duplicatedEmails.add(email);
} else if (existingInvitationEmails.has(email)) {
conflictInvitationEmails.add(email);
} else if (existingMemberEmails.has(email)) {
conflictMemberEmails.add(email);
} else { } else {
validEmails.add(email); validEmails.add(email);
} }
@ -50,10 +70,12 @@ const useEmailInputUtils = () => {
return { return {
duplicatedEmails, duplicatedEmails,
conflictMemberEmails,
conflictInvitationEmails,
invalidEmails, invalidEmails,
}; };
}, },
[existingMembers] [existingInvitations, existingMembers]
); );
const parseEmailOptions = useCallback( const parseEmailOptions = useCallback(
@ -63,30 +85,44 @@ const useEmailInputUtils = () => {
values: InviteeEmailItem[]; values: InviteeEmailItem[];
errorMessage?: string; errorMessage?: string;
} => { } => {
const { duplicatedEmails, invalidEmails } = findDuplicatedOrInvalidEmails( const { duplicatedEmails, conflictInvitationEmails, conflictMemberEmails, invalidEmails } =
inputValues.map((email) => email.value) findDuplicatedOrInvalidEmails(inputValues.map((email) => email.value));
);
// Show error message and update the inputs' status for error display. // 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 { return {
values: inputValues.map(({ status, ...rest }) => ({ values: inputValues.map(({ status, ...rest }) => ({
...rest, ...rest,
...conditional( ...conditional(
(duplicatedEmails.has(rest.value) || invalidEmails.has(rest.value)) && { (duplicatedEmails.has(rest.value) ||
status: 'info', conflictInvitationEmails.has(rest.value) ||
conflictMemberEmails.has(rest.value) ||
invalidEmails.has(rest.value)) && {
status: 'error',
} }
), ),
})), })),
errorMessage: conditionalArray( 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')) conditionalString(invalidEmails.size > 0 && t('tenant_members.errors.invalid_email'))
).join(' '), ).join('\n'),
}; };
} }
return { values: inputValues }; return { values: inputValues };
}, },
[findDuplicatedOrInvalidEmails] [findDuplicatedOrInvalidEmails, t]
); );
return { return {

View file

@ -39,7 +39,7 @@
background: var(--color-overlay-default-focused); background: var(--color-overlay-default-focused);
} }
&.info { &.error {
background: var(--color-error-container); background: var(--color-error-container);
} }
} }
@ -91,4 +91,5 @@ canvas {
font: var(--font-body-2); font: var(--font-body-2);
color: var(--color-error); color: var(--color-error);
margin-top: _.unit(1); margin-top: _.unit(1);
white-space: pre-wrap;
} }

View file

@ -71,7 +71,7 @@ function InviteEmailsInput({
{ {
value, value,
id: generateStandardShortId(), id: generateStandardShortId(),
...conditional(!emailRegEx.test(value) && { status: 'info' }), ...conditional(!emailRegEx.test(value) && { status: 'error' }),
}, },
]; ];
onChange(newValues); onChange(newValues);
@ -129,7 +129,7 @@ function InviteEmailsInput({
ref={ref} ref={ref}
placeholder={conditional(values.length === 0 && placeholder)} placeholder={conditional(values.length === 0 && placeholder)}
value={currentValue} value={currentValue}
style={{ minWidth: `${minInputWidth}px` }} style={{ minWidth: `${minInputWidth + 10}px` }}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Backspace' && currentValue === '') { if (event.key === 'Backspace' && currentValue === '') {
if (focusedValueId) { if (focusedValueId) {

View file

@ -14,7 +14,7 @@ export type InviteeEmailItem = {
* - undefined: valid email * - undefined: valid email
* - 'info': duplicated email or invalid email format. * - 'info': duplicated email or invalid email format.
*/ */
status?: Extract<TagProps['status'], 'info'>; status?: Extract<TagProps['status'], 'error'>;
}; };
export type InviteMemberForm = { export type InviteMemberForm = {

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -50,7 +50,10 @@ const tenant_members = {
}, },
errors: { errors: {
email_required: 'Invitee email is required.', 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.', 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.', max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
}, },

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */

View file

@ -87,7 +87,12 @@ const tenant_members = {
/** UNTRANSLATED */ /** UNTRANSLATED */
email_required: 'Invitee email is required.', email_required: 'Invitee email is required.',
/** UNTRANSLATED */ /** 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 */ /** UNTRANSLATED */
invalid_email: 'Email address is invalid. Please make sure it is in the right format.', invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
/** UNTRANSLATED */ /** UNTRANSLATED */