mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
feat(console): invite members dialog (#5531)
This commit is contained in:
parent
bca0ce98aa
commit
c7a23dfe92
23 changed files with 940 additions and 56 deletions
|
@ -3,7 +3,6 @@ import { getUserDisplayName } from '@logto/shared/universal';
|
|||
import { useContext, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type TenantMemberResponse } from '@/cloud/types/router';
|
||||
|
@ -11,7 +10,7 @@ import { TenantsContext } from '@/contexts/TenantsProvider';
|
|||
import Button from '@/ds-components/Button';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
import Select from '@/ds-components/Select';
|
||||
import Select, { type Option } from '@/ds-components/Select';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -28,7 +27,7 @@ function EditMemberModal({ user, isOpen, onClose }: Props) {
|
|||
const [role, setRole] = useState(TenantRole.Member);
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
|
||||
const roleOptions = useMemo(
|
||||
const roleOptions: Array<Option<TenantRole>> = useMemo(
|
||||
() => [
|
||||
{ value: TenantRole.Admin, title: t('admin') },
|
||||
{ value: TenantRole.Member, title: t('member') },
|
||||
|
@ -74,8 +73,9 @@ function EditMemberModal({ user, isOpen, onClose }: Props) {
|
|||
options={roleOptions}
|
||||
value={role}
|
||||
onChange={(value) => {
|
||||
const guardResult = z.nativeEnum(TenantRole).safeParse(value);
|
||||
setRole(guardResult.success ? guardResult.data : TenantRole.Member);
|
||||
if (value) {
|
||||
setRole(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
import { OrganizationInvitationStatus } from '@logto/schemas';
|
||||
import { format } from 'date-fns';
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Delete from '@/assets/icons/delete.svg';
|
||||
import Invite from '@/assets/icons/invitation.svg';
|
||||
import More from '@/assets/icons/more.svg';
|
||||
import Plus from '@/assets/icons/plus.svg';
|
||||
import Redo from '@/assets/icons/redo.svg';
|
||||
import UsersEmptyDark from '@/assets/images/users-empty-dark.svg';
|
||||
import UsersEmpty from '@/assets/images/users-empty.svg';
|
||||
import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type TenantInvitationResponse } from '@/cloud/types/router';
|
||||
import ActionsButton from '@/components/ActionsButton';
|
||||
import { RoleOption } from '@/components/OrganizationRolesSelect';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import ActionMenu, { ActionMenuItem } from '@/ds-components/ActionMenu';
|
||||
import Button from '@/ds-components/Button';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import Table from '@/ds-components/Table';
|
||||
import TablePlaceholder from '@/ds-components/Table/TablePlaceholder';
|
||||
import Tag, { type Props as TagProps } from '@/ds-components/Tag';
|
||||
import { type RequestError } from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
|
||||
import InviteMemberModal from '../InviteMemberModal';
|
||||
|
||||
const convertInvitationStatusToTagStatus = (
|
||||
status: OrganizationInvitationStatus
|
||||
|
@ -49,6 +58,42 @@ function Invitations() {
|
|||
);
|
||||
|
||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||
const { show } = useConfirmModal();
|
||||
|
||||
const handleRevoke = async (invitationId: string) => {
|
||||
const [result] = await show({
|
||||
ModalContent: t('revoke_invitation_confirm'),
|
||||
confirmButtonText: 'general.confirm',
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
await cloudApi.patch(`/api/tenants/:tenantId/invitations/:invitationId/status`, {
|
||||
params: { tenantId: currentTenantId, invitationId },
|
||||
body: { status: OrganizationInvitationStatus.Revoked },
|
||||
});
|
||||
void mutate();
|
||||
toast.success(t('messages.invitation_revoked'));
|
||||
};
|
||||
|
||||
const handleDelete = async (invitationId: string) => {
|
||||
const [result] = await show({
|
||||
ModalContent: t('delete_user_confirm'),
|
||||
confirmButtonText: 'general.delete',
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
await cloudApi.delete(`/api/tenants/:tenantId/invitations/:invitationId`, {
|
||||
params: { tenantId: currentTenantId, invitationId },
|
||||
});
|
||||
void mutate();
|
||||
toast.success(t('messages.invitation_deleted'));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -62,7 +107,7 @@ function Invitations() {
|
|||
description="tenant_members.invitation_empty_placeholder.description"
|
||||
action={
|
||||
<Button
|
||||
title="tenant_members.invite_member"
|
||||
title="tenant_members.invite_members"
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<Plus />}
|
||||
|
@ -124,29 +169,65 @@ function Invitations() {
|
|||
{
|
||||
dataIndex: 'actions',
|
||||
title: null,
|
||||
render: (invitation) => (
|
||||
<ActionsButton
|
||||
deleteConfirmation="tenant_members.delete_user_confirm"
|
||||
fieldName="tenant_members.user"
|
||||
textOverrides={{
|
||||
edit: 'tenant_members.menu_options.resend_invite',
|
||||
delete: 'tenant_members.menu_options.revoke',
|
||||
deleteConfirmation: 'general.remove',
|
||||
}}
|
||||
onDelete={async () => {
|
||||
await cloudApi.delete(`/api/tenants/:tenantId/invitations/:invitationId`, {
|
||||
params: { tenantId: currentTenantId, invitationId: invitation.id },
|
||||
});
|
||||
void mutate();
|
||||
}}
|
||||
/>
|
||||
render: ({ id, status }) => (
|
||||
<ActionMenu
|
||||
icon={<More />}
|
||||
iconSize="small"
|
||||
title={<DynamicT forKey="general.more_options" />}
|
||||
>
|
||||
{status !== OrganizationInvitationStatus.Accepted && (
|
||||
<ActionMenuItem
|
||||
icon={<Invite />}
|
||||
onClick={async () => {
|
||||
await cloudApi.post(
|
||||
'/api/tenants/:tenantId/invitations/:invitationId/message',
|
||||
{
|
||||
params: { tenantId: currentTenantId, invitationId: id },
|
||||
}
|
||||
);
|
||||
toast.success(t('messages.invitation_sent'));
|
||||
}}
|
||||
>
|
||||
{t('menu_options.resend_invite')}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
{status === OrganizationInvitationStatus.Pending && (
|
||||
<ActionMenuItem
|
||||
icon={<Redo />}
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
void handleRevoke(id);
|
||||
}}
|
||||
>
|
||||
{t('menu_options.revoke')}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
{status !== OrganizationInvitationStatus.Pending && (
|
||||
<ActionMenuItem
|
||||
icon={<Delete />}
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
void handleDelete(id);
|
||||
}}
|
||||
>
|
||||
{t('menu_options.delete_invitation_record')}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
</ActionMenu>
|
||||
),
|
||||
},
|
||||
]}
|
||||
rowIndexKey="id"
|
||||
/>
|
||||
{/* TODO: Implemented in the follow-up PR */}
|
||||
{/* {showInviteModal && <InviteModal isOpen={showInviteModal} />} */}
|
||||
{showInviteModal && (
|
||||
<InviteMemberModal
|
||||
isOpen={showInviteModal}
|
||||
onClose={() => {
|
||||
setShowInviteModal(false);
|
||||
void mutate();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
min-height: 96px;
|
||||
padding: 0 _.unit(2) 0 _.unit(3);
|
||||
background: var(--color-layer-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
outline: 3px solid transparent;
|
||||
transition-property: outline, border;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 0.2s;
|
||||
font: var(--font-body-2);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&.multiple {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: _.unit(2);
|
||||
padding: _.unit(1.5) _.unit(3);
|
||||
cursor: text;
|
||||
|
||||
.tag {
|
||||
cursor: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: _.unit(1);
|
||||
position: relative;
|
||||
|
||||
&.focused::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--color-overlay-default-focused);
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: var(--color-error-container);
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.delete {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: _.unit(-0.5);
|
||||
}
|
||||
|
||||
input {
|
||||
color: var(--color-text);
|
||||
font: var(--font-body-2);
|
||||
background: transparent;
|
||||
flex-grow: 1;
|
||||
padding: _.unit(0.5);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
outline-color: var(--color-focused-variant);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--color-error);
|
||||
|
||||
&:focus-within {
|
||||
outline-color: var(--color-danger-focused);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-error);
|
||||
margin-top: _.unit(1);
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
import { emailRegEx } from '@logto/core-kit';
|
||||
import { generateStandardShortId } from '@logto/shared/universal';
|
||||
import { conditional, type Nullable } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import Close from '@/assets/icons/close.svg';
|
||||
import IconButton from '@/ds-components/IconButton';
|
||||
import Tag from '@/ds-components/Tag';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import type { InviteeEmailItem, InviteMemberForm } from '../types';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
import { emailOptionsParser } from './utils';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
values: InviteeEmailItem[];
|
||||
onChange: (values: InviteeEmailItem[]) => void;
|
||||
error?: string | boolean;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
function InviteEmailsInput({
|
||||
className,
|
||||
values,
|
||||
onChange: rawOnChange,
|
||||
error,
|
||||
placeholder,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [focusedValueId, setFocusedValueId] = useState<Nullable<string>>(null);
|
||||
const [currentValue, setCurrentValue] = useState('');
|
||||
const { setError, clearErrors } = useFormContext<InviteMemberForm>();
|
||||
|
||||
const onChange = (values: InviteeEmailItem[]) => {
|
||||
const { values: parsedValues, errorMessage } = emailOptionsParser(values);
|
||||
if (errorMessage) {
|
||||
setError('emails', { type: 'custom', message: errorMessage });
|
||||
} else {
|
||||
clearErrors('emails');
|
||||
}
|
||||
rawOnChange(parsedValues);
|
||||
};
|
||||
|
||||
const handleAdd = (value: string) => {
|
||||
const newValues: InviteeEmailItem[] = [
|
||||
...values,
|
||||
{
|
||||
value,
|
||||
id: generateStandardShortId(),
|
||||
...conditional(!emailRegEx.test(value) && { status: 'info' }),
|
||||
},
|
||||
];
|
||||
onChange(newValues);
|
||||
setCurrentValue('');
|
||||
ref.current?.focus();
|
||||
};
|
||||
|
||||
const handleDelete = (option: InviteeEmailItem) => {
|
||||
onChange(values.filter(({ id }) => id !== option.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.input,
|
||||
styles.multiple,
|
||||
Boolean(error) && styles.error,
|
||||
className
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
ref.current?.focus();
|
||||
})}
|
||||
onClick={() => {
|
||||
ref.current?.focus();
|
||||
}}
|
||||
>
|
||||
{values.map((option) => (
|
||||
<Tag
|
||||
key={option.id}
|
||||
variant="cell"
|
||||
className={classNames(
|
||||
styles.tag,
|
||||
option.status && styles[option.status],
|
||||
option.id === focusedValueId && styles.focused
|
||||
)}
|
||||
onClick={() => {
|
||||
ref.current?.focus();
|
||||
}}
|
||||
>
|
||||
{option.value}
|
||||
<IconButton
|
||||
className={styles.delete}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
handleDelete(option);
|
||||
}}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
handleDelete(option);
|
||||
})}
|
||||
>
|
||||
<Close className={styles.close} />
|
||||
</IconButton>
|
||||
</Tag>
|
||||
))}
|
||||
<input
|
||||
ref={ref}
|
||||
placeholder={conditional(values.length === 0 && placeholder)}
|
||||
value={currentValue}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Backspace' && currentValue === '') {
|
||||
if (focusedValueId) {
|
||||
onChange(values.filter(({ id }) => id !== focusedValueId));
|
||||
setFocusedValueId(null);
|
||||
} else {
|
||||
setFocusedValueId(values.at(-1)?.id ?? null);
|
||||
}
|
||||
ref.current?.focus();
|
||||
}
|
||||
if (event.key === ' ' || event.code === 'Space' || event.key === 'Enter') {
|
||||
// Focusing on input
|
||||
if (currentValue !== '' && document.activeElement === ref.current) {
|
||||
handleAdd(currentValue);
|
||||
}
|
||||
// Do not react to "Enter"
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
onChange={({ currentTarget: { value } }) => {
|
||||
setCurrentValue(value);
|
||||
setFocusedValueId(null);
|
||||
}}
|
||||
onFocus={() => {
|
||||
ref.current?.focus();
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (currentValue !== '') {
|
||||
handleAdd(currentValue);
|
||||
}
|
||||
setFocusedValueId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{Boolean(error) && typeof error === 'string' && (
|
||||
<div className={styles.errorMessage}>{error}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteEmailsInput;
|
|
@ -0,0 +1,68 @@
|
|||
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<string>();
|
||||
const invalidEmails = new Set<string>();
|
||||
const validEmails = new Set<string>();
|
||||
|
||||
for (const email of emails) {
|
||||
if (!emailRegEx.test(email)) {
|
||||
invalidEmails.add(email);
|
||||
}
|
||||
|
||||
if (validEmails.has(email)) {
|
||||
duplicatedEmails.add(email);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
duplicatedEmails,
|
||||
invalidEmails,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,159 @@
|
|||
import { ReservedPlanId, TenantRole } from '@logto/schemas';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
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 { type InviteMemberForm } from '../types';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: (isSuccessful?: boolean) => void;
|
||||
};
|
||||
|
||||
function InviteMemberModal({ isOpen, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenant_members' });
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const { currentTenantId, isDevTenant } = useContext(TenantsContext);
|
||||
const tenantMembersMaxLimit = useMemo(() => {
|
||||
if (isDevTenant) {
|
||||
return 10;
|
||||
}
|
||||
if (currentPlan.id === ReservedPlanId.Pro) {
|
||||
return 3;
|
||||
}
|
||||
// Free plan can only have 1 admin, no other members allowed.
|
||||
return 1;
|
||||
}, [currentPlan.id, isDevTenant]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
|
||||
const formMethods = useForm<InviteMemberForm>({
|
||||
defaultValues: {
|
||||
emails: [],
|
||||
role: TenantRole.Member,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = formMethods;
|
||||
|
||||
const roleOptions: Array<Option<TenantRole>> = useMemo(
|
||||
() => [
|
||||
{ value: TenantRole.Admin, title: t('admin') },
|
||||
{ value: TenantRole.Member, title: t('member') },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const onSubmit = handleSubmit(async ({ emails, role }) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Count the current tenant members
|
||||
const members = await cloudApi.get(`/api/tenants/:tenantId/members`, {
|
||||
params: { tenantId: currentTenantId },
|
||||
});
|
||||
// Check if it will exceed the tenant member limit
|
||||
if (emails.length + members.length > tenantMembersMaxLimit) {
|
||||
setError('emails', {
|
||||
type: 'custom',
|
||||
message: t('errors.max_member_limit', { limit: tenantMembersMaxLimit }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
emails.map(async (email) =>
|
||||
cloudApi.post('/api/tenants/:tenantId/invitations', {
|
||||
params: { tenantId: currentTenantId },
|
||||
body: { invitee: email.value, roleName: role },
|
||||
})
|
||||
)
|
||||
);
|
||||
toast.success(t('messages.invitation_sent'));
|
||||
onClose(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
onRequestClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ModalLayout
|
||||
title="tenant_members.invite_modal.title"
|
||||
footer={
|
||||
<Button
|
||||
size="large"
|
||||
type="primary"
|
||||
title="tenant_members.invite_members"
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<FormProvider {...formMethods}>
|
||||
<FormField isRequired title="tenant_members.invite_modal.to">
|
||||
<Controller
|
||||
name="emails"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (value.length === 0) {
|
||||
return t('errors.email_required');
|
||||
}
|
||||
const { errorMessage } = emailOptionsParser(value);
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
return errorMessage || true;
|
||||
},
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<InviteEmailsInput
|
||||
values={value}
|
||||
error={errors.emails?.message}
|
||||
placeholder={t('invite_modal.email_input_placeholder')}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="tenant_members.roles">
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select options={roleOptions} value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</FormProvider>
|
||||
</ModalLayout>
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteMemberModal;
|
|
@ -1,4 +1,5 @@
|
|||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import InvitationIcon from '@/assets/icons/invitation.svg';
|
||||
|
@ -11,6 +12,7 @@ import useTenantPathname from '@/hooks/use-tenant-pathname';
|
|||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
import Invitations from './Invitations';
|
||||
import InviteMemberModal from './InviteMemberModal';
|
||||
import Members from './Members';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -18,6 +20,7 @@ const invitationsRoute = 'invitations';
|
|||
|
||||
function TenantMembers() {
|
||||
const { navigate, match } = useTenantPathname();
|
||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||
|
||||
const isInvitationTab = match(
|
||||
`/tenant-settings/${TenantSettingsTabs.Members}/${invitationsRoute}`
|
||||
|
@ -47,7 +50,10 @@ function TenantMembers() {
|
|||
type="primary"
|
||||
size="large"
|
||||
icon={<PlusIcon />}
|
||||
title="tenant_members.invite_member"
|
||||
title="tenant_members.invite_members"
|
||||
onClick={() => {
|
||||
setShowInviteModal(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Routes>
|
||||
|
@ -55,6 +61,17 @@ function TenantMembers() {
|
|||
<Route index element={<Members />} />
|
||||
<Route path={invitationsRoute} element={<Invitations />} />
|
||||
</Routes>
|
||||
{showInviteModal && (
|
||||
<InviteMemberModal
|
||||
isOpen={showInviteModal}
|
||||
onClose={(isSuccessful) => {
|
||||
setShowInviteModal(false);
|
||||
if (isSuccessful) {
|
||||
navigate('invitations');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { type TenantRole } from '@logto/schemas';
|
||||
|
||||
import { type Props as TagProps } from '@/ds-components/Tag';
|
||||
|
||||
export type InviteeEmailItem = {
|
||||
/**
|
||||
* Generate a random unique id for each option to handle deletion.
|
||||
* Sometimes we may have options with the same value, which is allowed when inputting but prohibited when submitting.
|
||||
*/
|
||||
id: string;
|
||||
value: string;
|
||||
/**
|
||||
* The `status` is used to indicate the input status of the email item (could fall into following categories):
|
||||
* - undefined: valid email
|
||||
* - 'info': duplicated email or invalid email format.
|
||||
*/
|
||||
status?: Extract<TagProps['status'], 'info'>;
|
||||
};
|
||||
|
||||
export type InviteMemberForm = {
|
||||
emails: InviteeEmailItem[];
|
||||
role: TenantRole;
|
||||
};
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const tenant_members = {
|
||||
members: 'Members',
|
||||
invitations: 'Invitations',
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
user: 'User',
|
||||
roles: 'Roles',
|
||||
admin: 'Admin',
|
||||
|
@ -14,6 +14,7 @@ const tenant_members = {
|
|||
subtitle: 'To invite members to an organization, they must accept the invitation.',
|
||||
to: 'To',
|
||||
added_as: 'Added as roles',
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
pending: 'Pending',
|
||||
|
@ -39,9 +40,19 @@ const tenant_members = {
|
|||
delete_user_confirm: 'Are you sure you want to remove this user from this tenant?',
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
invitation_sent: 'Invitation sent.',
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
invitation_resend: 'Invitation resent.',
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
user_exists: 'This user is already in 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.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
invitations: 'Invitations',
|
||||
/** UNTRANSLATED */
|
||||
invite_member: 'Invite member',
|
||||
invite_members: 'Invite members',
|
||||
/** UNTRANSLATED */
|
||||
user: 'User',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -28,6 +28,8 @@ const tenant_members = {
|
|||
to: 'To',
|
||||
/** UNTRANSLATED */
|
||||
added_as: 'Added as roles',
|
||||
/** UNTRANSLATED */
|
||||
email_input_placeholder: 'johndoe@example.com',
|
||||
},
|
||||
invitation_statuses: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -67,11 +69,29 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
assign_admin_confirm:
|
||||
'Are you sure you want to make the selected user(s) admin? Granting admin access will give the user(s) the following permissions.<ul><li>Change the tenant billing plan</li><li>Add or remove collaborators</li><li>Delete the tenant</li></ul>',
|
||||
/** UNTRANSLATED */
|
||||
revoke_invitation_confirm: 'Are you sure you want to revoke this invitation?',
|
||||
/** UNTRANSLATED */
|
||||
delete_invitation_confirm: 'Are you sure you want to delete this invitation record?',
|
||||
messages: {
|
||||
/** UNTRANSLATED */
|
||||
invitation_sent: 'Invitation sent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_revoked: 'Invitation revoked.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_resend: 'Invitation resent.',
|
||||
/** UNTRANSLATED */
|
||||
invitation_deleted: 'Invitation record deleted.',
|
||||
},
|
||||
errors: {
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization',
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue