feat: init
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from 'app/atoms/button/button';
|
||||
import { Modal, ModalContent } from 'app/atoms/modal';
|
||||
import {
|
||||
invitationActiveTab,
|
||||
invitationEmailsState,
|
||||
invitationSentState,
|
||||
invitationState,
|
||||
} from 'app/features/invitation/state/invitation';
|
||||
import { useCurrentWorkspace } from 'app/features/workspaces/hooks/use-workspaces';
|
||||
import Tab from 'app/molecules/tabs';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import Bulk_invitation from './parts/bulk-invitation';
|
||||
import Custom_role_invitation from './parts/custom-role-invitation';
|
||||
import InvitationChannels from './parts/invitation-channels';
|
||||
import InvitationSent from './parts/invitation-sent';
|
||||
import { useInvitation } from 'app/features/invitation/hooks/use-invitation';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import WorkspaceLink from './parts/workspace-link';
|
||||
import InvitationTarget from './parts/invitation-target';
|
||||
import AllowAnyoneByEmail from './parts/allow-anyone-by-email';
|
||||
import { Info, Subtitle } from 'app/atoms/text';
|
||||
|
||||
enum InvitationTabs {
|
||||
custom = 0,
|
||||
bulk = 1,
|
||||
}
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
const [isOpen, setOpen] = useRecoilState(invitationState);
|
||||
const [activeTab, setInvitationTab] = useRecoilState(invitationActiveTab);
|
||||
const [invitations] = useRecoilState(invitationEmailsState);
|
||||
const workspace = useCurrentWorkspace();
|
||||
const [isInvitationSent] = useRecoilState(invitationSentState);
|
||||
const { send, reset } = useInvitation();
|
||||
const [sending, setSending] = useState<boolean>(false);
|
||||
|
||||
const handleSend = async () => {
|
||||
setSending(true);
|
||||
try {
|
||||
await send();
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
reset();
|
||||
}}
|
||||
className="sm:w-[60vw] sm:max-w-2xl"
|
||||
style={{ minHeight: 'calc(70vh - 98px)' }}
|
||||
>
|
||||
<ModalContent
|
||||
textCenter
|
||||
title={
|
||||
isInvitationSent
|
||||
? ''
|
||||
: Languages.t(
|
||||
'components.invitation.title',
|
||||
[workspace.workspace?.name],
|
||||
`Invite users to ${workspace.workspace?.name}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{!isInvitationSent ? (
|
||||
<>
|
||||
<AllowAnyoneByEmail />
|
||||
|
||||
<Subtitle className="mt-6 block">Manual invitation</Subtitle>
|
||||
<Info className="block">
|
||||
You can configure exactly where the user will be invited and what role they will have.
|
||||
</Info>
|
||||
<InvitationTarget />
|
||||
|
||||
<Info className="block mt-2 mb-1">
|
||||
Simply send this email to the users you want to invite.
|
||||
</Info>
|
||||
<WorkspaceLink />
|
||||
|
||||
<Info className="block mt-4">Or enter manually the emails here.</Info>
|
||||
|
||||
{/*<Tab
|
||||
tabs={[
|
||||
<div key="custom_role_invitation">
|
||||
{Languages.t('components.invitation.custom_role_invitation')}
|
||||
</div>,
|
||||
<div key="bulk_invitation">
|
||||
{Languages.t('components.invitation.bulk_invitation')}
|
||||
</div>,
|
||||
]}
|
||||
selected={activeTab}
|
||||
onClick={index => setInvitationTab(index)}
|
||||
className="w-full"
|
||||
parentClassName="basis-1/2 justify-center"
|
||||
/>
|
||||
{activeTab === InvitationTabs.custom && <Custom_role_invitation />}
|
||||
{activeTab === InvitationTabs.bulk && <Bulk_invitation />}*/}
|
||||
<Custom_role_invitation />
|
||||
<Button
|
||||
className="mt-2 justify-center w-full"
|
||||
disabled={!invitations.length || sending}
|
||||
onClick={() => handleSend()}
|
||||
loading={sending}
|
||||
>
|
||||
{Languages.t('components.invitation.button', [], 'Send invitations')}
|
||||
<div className="font-medium h-5 px-1.5 flex items-center justify-center text-sm rounded-full ml-1 bg-white text-blue-500">
|
||||
{invitations.length}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<InvitationChannels />
|
||||
</>
|
||||
) : (
|
||||
<InvitationSent />
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Info, Subtitle } from 'app/atoms/text';
|
||||
import Switch from 'app/components/inputs/switch';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { ToasterService } from 'app/features/global/services/toaster-service';
|
||||
import { useInvitationUsers } from 'app/features/invitation/hooks/use-invitation-users';
|
||||
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||
import workspaceApiClient from 'app/features/workspaces/api/workspace-api-client';
|
||||
import { useCurrentWorkspace } from 'app/features/workspaces/hooks/use-workspaces';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
const { user } = useCurrentUser();
|
||||
const { members_limit_reached } = useInvitationUsers();
|
||||
const { workspace } = useCurrentWorkspace();
|
||||
const [allow, setAllow] = useState<boolean>(!!workspace?.preferences?.invite_domain);
|
||||
|
||||
const currentUserDomain = user?.email.split('@').pop();
|
||||
|
||||
const handleChange = async (value: boolean): Promise<void> => {
|
||||
if (value) {
|
||||
try {
|
||||
await workspaceApiClient.setInvitationDomain(
|
||||
workspace?.company_id as string,
|
||||
workspace?.id as string,
|
||||
currentUserDomain as string,
|
||||
);
|
||||
setAllow(true);
|
||||
ToasterService.success('Invitation domain updated');
|
||||
} catch (error) {
|
||||
ToasterService.error('Failed to set invitation domain');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return !members_limit_reached ? (
|
||||
<>
|
||||
<Subtitle className="mt-4 block">Automatically invite my business</Subtitle>
|
||||
<Info className="block">
|
||||
If this is enabled, anyone creating a Twake account with your business email will
|
||||
automatically be added to this company and this workspace.
|
||||
</Info>
|
||||
<div className="flex flex-row space-x-2 mt-1 bg-zinc-200 px-3 py-2 rounded-md border-transparent w-full h-9">
|
||||
<div className="flex-1">
|
||||
{Languages.t(
|
||||
'components.invitation.allow_anyone_by_email.text',
|
||||
[workspace?.preferences?.invite_domain || currentUserDomain],
|
||||
`Let anyone with @${
|
||||
workspace?.preferences?.invite_domain || currentUserDomain
|
||||
} email join this workspace`,
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={allow}
|
||||
onChange={handleChange}
|
||||
disabled={!!workspace?.preferences?.invite_domain}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import InvitationInputBulk from './invitation-input-bulk';
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<InvitationInputBulk />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import InvitationInputList from './invitation-input-list';
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
return (
|
||||
<div className="flex flex-col mt-2">
|
||||
<InvitationInputList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'app/atoms/button/button';
|
||||
import { Modal, ModalContent } from 'app/atoms/modal';
|
||||
import { ChannelSelector } from 'app/components/channels-selector';
|
||||
import { ChannelType } from 'app/features/channels/types/channel';
|
||||
import { useInvitationChannels } from 'app/features/invitation/hooks/use-invitation-channels';
|
||||
import { uniqBy } from 'lodash';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
const { selectedChannels, closeSelection, setChannels, open } = useInvitationChannels();
|
||||
|
||||
const handleSelectionChange = (channels: ChannelType[]) => {
|
||||
setChannels(uniqBy(channels, 'id'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={() => closeSelection()} className="sm:w-[20vw] sm:max-w-xl">
|
||||
<ModalContent textCenter title="Choose channels">
|
||||
<ChannelSelector
|
||||
initialChannels={selectedChannels}
|
||||
onChange={handleSelectionChange}
|
||||
lockDefaultChannels={true}
|
||||
/>
|
||||
|
||||
<Button
|
||||
theme="primary"
|
||||
disabled={!selectedChannels.length}
|
||||
onClick={() => closeSelection()}
|
||||
className="w-full justify-center"
|
||||
>
|
||||
{Languages.t(
|
||||
'components.invitation.invitation_channels.button',
|
||||
[],
|
||||
'Invite to channels',
|
||||
)}
|
||||
<div className="font-medium h-5 px-1.5 flex items-center justify-center text-sm rounded-full ml-1 bg-white text-blue-500">
|
||||
{selectedChannels.length}
|
||||
</div>
|
||||
</Button>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from 'react';
|
||||
import {
|
||||
invitationEmailsState,
|
||||
invitationTypeState,
|
||||
InvitedUser,
|
||||
} from 'app/features/invitation/state/invitation';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import ChipInput from 'material-ui-chip-input';
|
||||
import { RemoveIcon } from 'app/atoms/icons-colored';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||
import { useInvitationUsers } from 'app/features/invitation/hooks/use-invitation-users';
|
||||
import ReachedLimit from './reached-limit';
|
||||
|
||||
const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
const [invitations, setInvitations] = useRecoilState(invitationEmailsState);
|
||||
const [role] = useRecoilState(invitationTypeState);
|
||||
const { user } = useCurrentUser();
|
||||
const { can_add_invitations, members_limit_reached } = useInvitationUsers();
|
||||
|
||||
const emails = (invitations || []).map(invitation => invitation.email);
|
||||
const currentUserEmailDomain = user?.email.split('@').pop();
|
||||
|
||||
const handleChange = (chips: string[]) => {
|
||||
const newInvitations: InvitedUser[] = chips
|
||||
.filter(chip => !invitations.find(invitation => invitation.email === chip))
|
||||
.map(chip => ({ email: chip, role }));
|
||||
const updatedInvitations = [...invitations, ...newInvitations];
|
||||
|
||||
setInvitations(updatedInvitations);
|
||||
};
|
||||
|
||||
const validate = (chip: string): boolean => {
|
||||
return emailRegex.test(chip) && currentUserEmailDomain === chip.split('@').pop();
|
||||
};
|
||||
|
||||
const handleDelete = (chip: string) => {
|
||||
const updatedInvitations = invitations.filter(({ email }) => email !== chip);
|
||||
|
||||
setInvitations(updatedInvitations);
|
||||
};
|
||||
|
||||
const rederChip = (
|
||||
{ chip, handleDelete }: { chip: string; handleDelete: React.EventHandler<any> },
|
||||
key: string,
|
||||
): React.ReactElement => {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex justify-center items-center m-2 my-2 px-3 py-2 space-x-2 h-9 rounded-md bg-white"
|
||||
>
|
||||
<RemoveIcon onClick={handleDelete} className="cursor-pointer" />
|
||||
<div className="flex-initial max-w-full leading-none text-xs font-normal">{chip}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PerfectScrollbar
|
||||
className="-mb-4 overflow-hidden px-2"
|
||||
style={{ maxHeight: 'calc(42vh - 98px)', minHeight: 'calc(42vh - 98px)' }}
|
||||
options={{ suppressScrollX: true, suppressScrollY: false }}
|
||||
>
|
||||
{ members_limit_reached && <ReachedLimit />}
|
||||
<ChipInput
|
||||
value={emails}
|
||||
disableUnderline={true}
|
||||
fullWidth={true}
|
||||
blurBehavior="ignore"
|
||||
newChipKeyCodes={[13, 188, 54, 190, 32]}
|
||||
onBeforeAdd={validate}
|
||||
variant="standard"
|
||||
className="bg-zinc-200 border-none hover:border-none rounded-md py-1"
|
||||
chipRenderer={rederChip}
|
||||
onChange={handleChange}
|
||||
onDelete={handleDelete}
|
||||
clearInputValueOnChange={false}
|
||||
disabled={!can_add_invitations}
|
||||
/>
|
||||
</PerfectScrollbar>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Button } from 'app/atoms/button/button';
|
||||
import { RemoveIcon } from 'app/atoms/icons-colored';
|
||||
import { Input } from 'app/atoms/input/input-text';
|
||||
import {
|
||||
invitationEmailsState,
|
||||
InvitationType,
|
||||
invitationTypeState,
|
||||
} from 'app/features/invitation/state/invitation';
|
||||
import React, { useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||
import { useInvitationUsers } from 'app/features/invitation/hooks/use-invitation-users';
|
||||
import ReachedLimit from './reached-limit';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { PlusIcon } from '@heroicons/react/outline';
|
||||
import Select from 'app/atoms/input/input-select';
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
const [invitations, setInvitations] = useRecoilState(invitationEmailsState);
|
||||
const [currentInput, setCurrentInput] = useState<string>('');
|
||||
const [notValidEmail, setNotValidEmail] = useState<boolean>(false);
|
||||
const [invitationTargetType] = useRecoilState(invitationTypeState);
|
||||
const { user } = useCurrentUser();
|
||||
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
const currentUserEmailDomain = user?.email.split('@').pop();
|
||||
const { can_add_invitations, members_limit_reached, allowed_members, allowed_guests } =
|
||||
useInvitationUsers();
|
||||
|
||||
const emailExists = (target: string): boolean =>
|
||||
!!invitations.find(invitation => invitation.email === target);
|
||||
|
||||
const handleEnter = (event: unknown): void => {
|
||||
if ((event as KeyboardEvent).key === 'Enter') {
|
||||
handleAdd();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = (): void => {
|
||||
const role = invitationTargetType;
|
||||
if (emailRegex.test(currentInput) && !emailExists(currentInput)) {
|
||||
setInvitations([...invitations, { email: currentInput, role }]);
|
||||
setCurrentInput('');
|
||||
setNotValidEmail(false);
|
||||
} else {
|
||||
setNotValidEmail(true);
|
||||
}
|
||||
};
|
||||
|
||||
const removeEmail = (targetEmail: string): void => {
|
||||
setInvitations(invitations.filter(invitation => invitation.email !== targetEmail));
|
||||
};
|
||||
|
||||
const handleInput = (event: any): void => {
|
||||
setCurrentInput(event.target.value);
|
||||
setNotValidEmail(!emailRegex.test(event.target.value) || emailExists(event.target.value));
|
||||
};
|
||||
|
||||
const handleRoleChange = (email: string, role: InvitationType): void => {
|
||||
if (role === InvitationType.guest && allowed_guests <= 0) return;
|
||||
if (role === InvitationType.member && allowed_members <= 0) return;
|
||||
|
||||
const changedInvitations = invitations.map(invitation => {
|
||||
if (invitation.email === email) {
|
||||
return {
|
||||
email,
|
||||
role,
|
||||
};
|
||||
}
|
||||
|
||||
return invitation;
|
||||
});
|
||||
|
||||
setInvitations(changedInvitations);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col border-transparent w-full">
|
||||
{can_add_invitations ? (
|
||||
<div className="flex flex-row">
|
||||
<Input
|
||||
className="rounded-r-none grow"
|
||||
theme="outline"
|
||||
placeholder={Languages.t('components.invitation.invitation_input_list.placeholder')}
|
||||
value={currentInput}
|
||||
onKeyDown={handleEnter}
|
||||
onChange={handleInput}
|
||||
hasError={notValidEmail}
|
||||
/>
|
||||
<Button
|
||||
theme="primary"
|
||||
disabled={notValidEmail || !currentInput.length}
|
||||
onClick={handleAdd}
|
||||
className="justify-center rounded-l-none"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-1.5 -ml-1" />
|
||||
{Languages.t('components.invitation.invitation_input_list.add')}
|
||||
</Button>
|
||||
</div>
|
||||
) : members_limit_reached ? (
|
||||
<ReachedLimit />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<PerfectScrollbar
|
||||
className="-mb-2 py-2 overflow-hidden -mx-2 px-2"
|
||||
style={{ maxHeight: 'calc(30vh - 100px)', minHeight: 'calc(30vh - 100px)' }}
|
||||
options={{ suppressScrollX: true, suppressScrollY: false }}
|
||||
>
|
||||
{invitations.map(invitation => (
|
||||
<div
|
||||
className="flex flex-row border-transparent items-center rounded-md py-0.5 my-0.5"
|
||||
key={invitation.email}
|
||||
>
|
||||
<RemoveIcon
|
||||
className="cursor-pointer shrink-0 mr-2"
|
||||
onClick={() => removeEmail(invitation.email)}
|
||||
/>
|
||||
<Input
|
||||
theme="outline"
|
||||
className="rounded-r-none grow w-full -mr-px"
|
||||
disabled={true}
|
||||
value={invitation.email}
|
||||
/>
|
||||
<Select
|
||||
theme="outline"
|
||||
className="rounded-l-none w-auto"
|
||||
onChange={e =>
|
||||
handleRoleChange(invitation.email, e.target.value as unknown as InvitationType)
|
||||
}
|
||||
value={invitation.role}
|
||||
disabled={
|
||||
(invitation.role === InvitationType.guest && allowed_members <= 0) ||
|
||||
(invitation.role === InvitationType.member && allowed_guests <= 0)
|
||||
}
|
||||
>
|
||||
<option value={InvitationType.member}>
|
||||
{Languages.t('components.invitation.invitation_input_list.member', [], 'Member')}
|
||||
</option>
|
||||
<option value={InvitationType.guest}>
|
||||
{Languages.t('components.invitation.invitation_input_list.guest', [], 'Guest')}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { SentIcon } from 'app/atoms/icons-colored';
|
||||
import { Title, Base } from 'app/atoms/text';
|
||||
import Link from 'app/atoms/link';
|
||||
import React from 'react';
|
||||
import { Button } from 'app/atoms/button/button';
|
||||
import { useInvitation } from 'app/features/invitation/hooks/use-invitation';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
const { reset } = useInvitation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col text-center h-full" style={{ minHeight: 'calc(66vh - 92px)' }}>
|
||||
<div className="flex justify-center text-center w-full">
|
||||
<SentIcon className="h-52 w-52" />
|
||||
</div>
|
||||
<div className="text-center w-full py-2 mx-4 px-6">
|
||||
<Title className="text-4xl">
|
||||
{Languages.t(
|
||||
'components.invitation.invitation_sent.title',
|
||||
[],
|
||||
'Invitations have successfully been sent',
|
||||
)}
|
||||
</Title>
|
||||
</div>
|
||||
<div className="text-center w-full py-3 mx-4 px-6">
|
||||
<Base>
|
||||
{Languages.t(
|
||||
'components.invitation.invitation_sent.subtitle_status',
|
||||
[],
|
||||
'You can track invitaion status in:',
|
||||
)}
|
||||
|
||||
{Languages.t(
|
||||
'components.invitation.invitation_sent.subtitle_location',
|
||||
[],
|
||||
'Workspace settings > Member management',
|
||||
)}
|
||||
</Base>
|
||||
</div>
|
||||
<div className="text-center w-full py-3">
|
||||
<Link>
|
||||
{Languages.t('components.invitation.invitation_sent.link', [], 'Check invitation status')}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex grow h-full flex-col-reverse">
|
||||
<Button className="mt-2 justify-center w-full" onClick={() => reset()}>
|
||||
{Languages.t('components.invitation.invitation_sent.button', [], 'Send more invitations')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { BaseSmall } from 'app/atoms/text';
|
||||
import Switch from 'app/components/inputs/switch';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { useInvitationChannels } from 'app/features/invitation/hooks/use-invitation-channels';
|
||||
import { useInvitationUsers } from 'app/features/invitation/hooks/use-invitation-users';
|
||||
import {
|
||||
invitationEmailsState,
|
||||
InvitationType,
|
||||
invitationTypeState,
|
||||
} from 'app/features/invitation/state/invitation';
|
||||
import React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
const [invitationType, setInvitationType] = useRecoilState(invitationTypeState);
|
||||
const { openSelection, selectedChannels } = useInvitationChannels();
|
||||
const [invitations, setInvitations] = useRecoilState(invitationEmailsState);
|
||||
const { allowed_guests, allowed_members } = useInvitationUsers();
|
||||
|
||||
const changeInvitationType = (type: InvitationType) => {
|
||||
setInvitationType(type);
|
||||
|
||||
if (type === InvitationType.guest && allowed_guests <= 0) return;
|
||||
if (type === InvitationType.member && allowed_members <= 0) return;
|
||||
setInvitations(invitations.map(({ email }) => ({ email, role: type })));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row space-x-3 rounded-md border-transparent w-full pt-2">
|
||||
<div className="bg-zinc-200 rounded-md py-2 px-3 flex flex-row justify-center items-center">
|
||||
<BaseSmall>
|
||||
{Languages.t('components.invitation.invitation_target.invite_as_guests')}
|
||||
</BaseSmall>
|
||||
<Switch
|
||||
checked={invitationType === InvitationType.guest}
|
||||
className="ml-2"
|
||||
onChange={e => changeInvitationType(e ? InvitationType.guest : InvitationType.member)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow bg-zinc-200 rounded-md py-2 ml-2 px-3 justify-center align-middle">
|
||||
<div
|
||||
className="cursor-pointer flex relative justify-center border-transparent hover:text-blue-600 transition-colors text-blue-500 font-semibold "
|
||||
onClick={() => openSelection()}
|
||||
>
|
||||
{Languages.t('components.invitation.invitation_target.channels_button')}
|
||||
<div className="font-medium h-5 px-1.5 flex items-center justify-center text-sm rounded-full ml-1 text-white bg-blue-500">
|
||||
{selectedChannels.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import Text from 'app/atoms/text';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import React from 'react';
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
return (
|
||||
<Text type="base" className="text-red-500" noColor={true}>
|
||||
{Languages.t(
|
||||
'components.invitation.reached_limit.text',
|
||||
[],
|
||||
'you reached the maximum number of users inside your company. Increase your subscription or add these users as guests.',
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Button } from 'app/atoms/button/button';
|
||||
import React, { useState } from 'react';
|
||||
import { CopyIcon } from '@atoms/icons-agnostic';
|
||||
import { Base } from 'app/atoms/text';
|
||||
import { useInvitation } from 'app/features/invitation/hooks/use-invitation';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import MagicLinks from 'app/views/client/popup/AddUser/MagicLinks';
|
||||
import { Input } from 'app/atoms/input/input-text';
|
||||
import { InputDecorationIcon } from 'app/atoms/input/input-decoration-icon';
|
||||
import { LinkIcon } from '@heroicons/react/outline';
|
||||
import { copyToClipboard } from 'app/features/global/utils/CopyClipboard';
|
||||
import { ToasterService } from 'app/features/global/services/toaster-service';
|
||||
|
||||
export default (): React.ReactElement => {
|
||||
const { generateInvitationLink } = useInvitation();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const copyLink = async () => {
|
||||
setLoading(true);
|
||||
const link = await generateInvitationLink();
|
||||
|
||||
if (link) {
|
||||
copyToClipboard(link);
|
||||
ToasterService.success('Link copied to clipboard');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row w-full">
|
||||
<InputDecorationIcon
|
||||
className="grow grow w-full"
|
||||
input={({ className }) => (
|
||||
<Input
|
||||
className={className + ' rounded-r-none pointer-events-none'}
|
||||
theme="plain"
|
||||
readOnly
|
||||
value={Languages.t(
|
||||
'components.invitation.workspace_link.text',
|
||||
[],
|
||||
'Workspace invitation link',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
prefix={LinkIcon}
|
||||
/>
|
||||
<Button
|
||||
className="h-7 2 rounded-l-none -ml-px"
|
||||
theme="primary"
|
||||
icon={CopyIcon}
|
||||
onClick={() => copyLink()}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{Languages.t('components.invitation.workspace_link.copy', [], 'Copy')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user