implement new invitation system

* fix comments on the frontend
* move jwt token service to its own module
* other fixes and updates
This commit is contained in:
Philipinho
2024-05-14 22:55:11 +01:00
parent 525990d6e5
commit eefe63d1cd
75 changed files with 10965 additions and 7846 deletions
@@ -0,0 +1,70 @@
import { Menu, ActionIcon, Text } from "@mantine/core";
import React from "react";
import { IconDots, IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import {
useResendInvitationMutation,
useRevokeInvitationMutation,
} from "@/features/workspace/queries/workspace-query.ts";
interface Props {
invitationId: string;
}
export default function InviteActionMenu({ invitationId }: Props) {
const resendInvitationMutation = useResendInvitationMutation();
const revokeInvitationMutation = useRevokeInvitationMutation();
const onResend = async () => {
await resendInvitationMutation.mutateAsync({ invitationId });
};
const onRevoke = async () => {
await revokeInvitationMutation.mutateAsync({ invitationId });
};
const openRevokeModal = () =>
modals.openConfirmModal({
title: "Revoke invitation",
children: (
<Text size="sm">
Are you sure you want to revoke this invitation? The user will not be
able to join the workspace.
</Text>
),
centered: true,
labels: { confirm: "Revoke", cancel: "Don't" },
confirmProps: { color: "red" },
onConfirm: onRevoke,
});
return (
<>
<Menu
shadow="xl"
position="bottom-end"
offset={20}
width={200}
withArrow
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="subtle" c="gray">
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={onResend}>Resend invitation</Menu.Item>
<Menu.Divider />
<Menu.Item
c="red"
onClick={openRevokeModal}
leftSection={<IconTrash size={16} stroke={2} />}
>
Revoke invitation
</Menu.Item>
</Menu.Dropdown>
</Menu>
</>
);
}
@@ -1,50 +1,87 @@
import { Group, Box, Button, TagsInput, Space, Select } from "@mantine/core";
import { Group, Box, Button, TagsInput, Select } from "@mantine/core";
import WorkspaceInviteSection from "@/features/workspace/components/members/components/workspace-invite-section.tsx";
import React from "react";
import React, { useState } from "react";
import { MultiGroupSelect } from "@/features/group/components/multi-group-select.tsx";
import { UserRole } from "@/lib/types.ts";
import { userRoleData } from "@/features/workspace/types/user-role-data.ts";
import { useCreateInvitationMutation } from "@/features/workspace/queries/workspace-query.ts";
import { useNavigate } from "react-router-dom";
enum UserRole {
OWNER = "Owner",
ADMIN = "Admin",
MEMBER = "Member",
interface Props {
onClose: () => void;
}
export function WorkspaceInviteForm({ onClose }: Props) {
const [emails, setEmails] = useState<string[]>([]);
const [role, setRole] = useState<string | null>(UserRole.MEMBER);
const [groupIds, setGroupIds] = useState<string[]>([]);
const createInvitationMutation = useCreateInvitationMutation();
const navigate = useNavigate();
export function WorkspaceInviteForm() {
function handleSubmit(data) {
console.log(data);
async function handleSubmit() {
const validEmails = emails.filter((email) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
});
await createInvitationMutation.mutateAsync({
role: role.toLowerCase(),
emails: validEmails,
groupIds: groupIds,
});
onClose();
navigate("?tab=invites");
}
const handleGroupSelect = (value: string[]) => {
setGroupIds(value);
};
return (
<>
<Box maw="500" mx="auto">
<WorkspaceInviteSection />
<Space h="md" />
{/*<WorkspaceInviteSection /> */}
<TagsInput
description="Enter valid email addresses separated by comma or space"
label="Invite from email"
mt="sm"
description="Enter valid email addresses separated by comma or space [max: 50]"
label="Invite by email"
placeholder="enter valid emails addresses"
variant="filled"
splitChars={[",", " "]}
maxDropdownHeight={200}
maxTags={50}
onChange={setEmails}
/>
<Space h="md" />
<Select
mt="sm"
description="Select role to assign to all invited members"
label="Select role"
placeholder="Pick a role"
placeholder="Choose a role"
variant="filled"
data={Object.values(UserRole)}
data={userRoleData.filter((role) => role.value !== UserRole.OWNER)}
defaultValue={UserRole.MEMBER}
allowDeselect={false}
checkIconPosition="right"
onChange={setRole}
/>
<Group justify="center" mt="md">
<Button>Send invitation</Button>
<MultiGroupSelect
mt="sm"
description="Invited members will be granted access to spaces the groups can access"
label={"Add to groups"}
onChange={handleGroupSelect}
/>
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
loading={createInvitationMutation.isPending}
>
Send invitation
</Button>
</Group>
</Box>
</>
@@ -10,7 +10,7 @@ export default function WorkspaceInviteModal() {
<Button onClick={open}>Invite members</Button>
<Modal
size="600"
size="550"
opened={opened}
onClose={close}
title="Invite new members"
@@ -19,7 +19,7 @@ export default function WorkspaceInviteModal() {
<Divider size="xs" mb="xs" />
<ScrollArea h="80%">
<WorkspaceInviteForm />
<WorkspaceInviteForm onClose={close} />
</ScrollArea>
</Modal>
</>
@@ -0,0 +1,62 @@
import { Group, Table, Avatar, Text, Badge, Alert } from "@mantine/core";
import { useWorkspaceInvitationsQuery } from "@/features/workspace/queries/workspace-query.ts";
import React from "react";
import { getUserRoleLabel } from "@/features/workspace/types/user-role-data.ts";
import InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx";
import { IconInfoCircle } from "@tabler/icons-react";
import { format } from "date-fns";
export default function WorkspaceInvitesTable() {
const { data, isLoading } = useWorkspaceInvitationsQuery({
limit: 100,
});
return (
<>
<Alert variant="light" color="blue" icon={<IconInfoCircle />}>
Invited members who are yet to accept their invitation will appear here.
</Alert>
{data && (
<>
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>Email</Table.Th>
<Table.Th>Role</Table.Th>
<Table.Th>Date</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.items.map((invitation, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm">
<Avatar src={invitation.email} />
<div>
<Text fz="sm" fw={500}>
{invitation.email}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>{getUserRoleLabel(invitation.role)}</Table.Td>
<Table.Td>
{format(invitation.createdAt, "MM/dd/yyyy")}
</Table.Td>
<Table.Td>
<InviteActionMenu invitationId={invitation.id} />
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</>
)}
</>
);
}
@@ -12,7 +12,7 @@ import {
} from "@/features/workspace/types/user-role-data.ts";
export default function WorkspaceMembersTable() {
const { data, isLoading } = useWorkspaceMembersQuery();
const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 });
const changeMemberRoleMutation = useChangeMemberRoleMutation();
const handleRoleChange = async (