mirror of
https://github.com/docmost/docmost.git
synced 2025-11-20 21:41:09 +10:00
fix: bug fixes (#397)
* Add more html page titles * Make tables responsive * fix react query keys * Add tooltip to sidebar toggle * fix: trim inputs * fix inputs
This commit is contained in:
@ -19,7 +19,7 @@ import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-qu
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
name: z.string().trim().min(1),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
|
||||
@ -15,8 +15,8 @@ import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import classes from "@/features/auth/components/auth.module.css";
|
||||
|
||||
const formSchema = z.object({
|
||||
workspaceName: z.string().min(2).max(60),
|
||||
name: z.string().min(2).max(60),
|
||||
workspaceName: z.string().trim().min(3).max(50),
|
||||
name: z.string().min(1).max(50),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, { message: "email is required" })
|
||||
|
||||
@ -7,7 +7,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
name: z.string().trim().min(2).max(50),
|
||||
description: z.string().max(500),
|
||||
});
|
||||
|
||||
|
||||
@ -1,69 +1,72 @@
|
||||
import { Table, Group, Text, Anchor } from "@mantine/core";
|
||||
import { useGetGroupsQuery } from "@/features/group/queries/group-query";
|
||||
import {Table, Group, Text, Anchor} from "@mantine/core";
|
||||
import {useGetGroupsQuery} from "@/features/group/queries/group-query";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||
import {Link} from "react-router-dom";
|
||||
import {IconGroupCircle} from "@/components/icons/icon-people-circle.tsx";
|
||||
|
||||
export default function GroupList() {
|
||||
const { data, isLoading } = useGetGroupsQuery();
|
||||
const {data, isLoading} = useGetGroupsQuery();
|
||||
|
||||
return (
|
||||
<>
|
||||
{data && (
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Group</Table.Th>
|
||||
<Table.Th>Members</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((group, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Anchor
|
||||
size="sm"
|
||||
underline="never"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
color: "var(--mantine-color-text)",
|
||||
}}
|
||||
component={Link}
|
||||
to={`/settings/groups/${group.id}`}
|
||||
>
|
||||
<Group gap="sm">
|
||||
<IconGroupCircle />
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{group.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{group.description}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Anchor>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Anchor
|
||||
size="sm"
|
||||
underline="never"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
color: "var(--mantine-color-text)",
|
||||
}}
|
||||
component={Link}
|
||||
to={`/settings/groups/${group.id}`}
|
||||
>
|
||||
{group.memberCount} members
|
||||
</Anchor>
|
||||
</Table.Td>
|
||||
<Table.ScrollContainer minWidth={400}>
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Group</Table.Th>
|
||||
<Table.Th>Members</Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((group, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Anchor
|
||||
size="sm"
|
||||
underline="never"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
color: "var(--mantine-color-text)",
|
||||
}}
|
||||
component={Link}
|
||||
to={`/settings/groups/${group.id}`}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<IconGroupCircle/>
|
||||
<div>
|
||||
<Text fz="sm" fw={500} lineClamp={1}>
|
||||
{group.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed" lineClamp={2}>
|
||||
{group.description}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Anchor>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Anchor
|
||||
size="sm"
|
||||
underline="never"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
color: "var(--mantine-color-text)",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
component={Link}
|
||||
to={`/settings/groups/${group.id}`}
|
||||
>
|
||||
{group.memberCount} members
|
||||
</Anchor>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
import { Group, Table, Text, Badge, Menu, ActionIcon } from "@mantine/core";
|
||||
import {Group, Table, Text, Badge, Menu, ActionIcon} from "@mantine/core";
|
||||
import {
|
||||
useGroupMembersQuery,
|
||||
useRemoveGroupMemberMutation,
|
||||
} from "@/features/group/queries/group-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {useParams} from "react-router-dom";
|
||||
import React from "react";
|
||||
import { IconDots } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import {IconDots} from "@tabler/icons-react";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {CustomAvatar} from "@/components/ui/custom-avatar.tsx";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
|
||||
export default function GroupMembersList() {
|
||||
const { groupId } = useParams();
|
||||
const { data, isLoading } = useGroupMembersQuery(groupId);
|
||||
const {groupId} = useParams();
|
||||
const {data, isLoading} = useGroupMembersQuery(groupId);
|
||||
const removeGroupMember = useRemoveGroupMemberMutation();
|
||||
const { isAdmin } = useUserRole();
|
||||
const {isAdmin} = useUserRole();
|
||||
|
||||
const onRemove = async (userId: string) => {
|
||||
const memberToRemove = {
|
||||
@ -34,72 +34,74 @@ export default function GroupMembersList() {
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
confirmProps: { color: "red" },
|
||||
labels: {confirm: "Delete", cancel: "Cancel"},
|
||||
confirmProps: {color: "red"},
|
||||
onConfirm: () => onRemove(userId),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{data && (
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>User</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((user, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name} />
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{user.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Badge variant="light">Active</Badge>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
{isAdmin && (
|
||||
<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={() => openRemoveModal(user.id)}>
|
||||
Remove group member
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>User</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((user, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name}/>
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{user.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Badge variant="light">Active</Badge>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
{isAdmin && (
|
||||
<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={() => openRemoveModal(user.id)}>
|
||||
Remove group member
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -29,24 +29,22 @@ export function useGetGroupsQuery(
|
||||
|
||||
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
|
||||
return useQuery({
|
||||
queryKey: ['groups', groupId],
|
||||
queryKey: ['group', groupId],
|
||||
queryFn: () => getGroupById(groupId),
|
||||
enabled: !!groupId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroupMembersQuery(groupId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['groupMembers', groupId],
|
||||
queryFn: () => getGroupMembers(groupId),
|
||||
enabled: !!groupId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateGroupMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<IGroup, Error, Partial<IGroup>>({
|
||||
mutationFn: (data) => createGroup(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['groups'],
|
||||
});
|
||||
|
||||
notifications.show({ message: 'Group created successfully' });
|
||||
},
|
||||
onError: () => {
|
||||
@ -96,6 +94,14 @@ export function useDeleteGroupMutation() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroupMembersQuery(groupId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['groupMembers', groupId],
|
||||
queryFn: () => getGroupMembers(groupId),
|
||||
enabled: !!groupId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddGroupMemberMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@ -8,9 +8,10 @@ import { computeSpaceSlug } from "@/lib";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
name: z.string().trim().min(2).max(50),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2)
|
||||
.max(50)
|
||||
.regex(
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Modal, Tabs, rem, Group, ScrollArea } from "@mantine/core";
|
||||
import {Modal, Tabs, rem, Group, ScrollArea, Text} from "@mantine/core";
|
||||
import SpaceMembersList from "@/features/space/components/space-members.tsx";
|
||||
import AddSpaceMembersModal from "@/features/space/components/add-space-members-modal.tsx";
|
||||
import React, { useMemo } from "react";
|
||||
import React, {useMemo} from "react";
|
||||
import SpaceDetails from "@/features/space/components/space-details.tsx";
|
||||
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
|
||||
import {useSpaceQuery} from "@/features/space/queries/space-query.ts";
|
||||
import {useSpaceAbility} from "@/features/space/permissions/use-space-ability.ts";
|
||||
import {
|
||||
SpaceCaslAction,
|
||||
SpaceCaslSubject,
|
||||
@ -17,11 +17,11 @@ interface SpaceSettingsModalProps {
|
||||
}
|
||||
|
||||
export default function SpaceSettingsModal({
|
||||
spaceId,
|
||||
opened,
|
||||
onClose,
|
||||
}: SpaceSettingsModalProps) {
|
||||
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
||||
spaceId,
|
||||
opened,
|
||||
onClose,
|
||||
}: SpaceSettingsModalProps) {
|
||||
const {data: space, isLoading} = useSpaceQuery(spaceId);
|
||||
|
||||
const spaceRules = space?.membership?.permissions;
|
||||
const spaceAbility = useMemo(() => useSpaceAbility(spaceRules), [spaceRules]);
|
||||
@ -37,14 +37,16 @@ export default function SpaceSettingsModal({
|
||||
xOffset={0}
|
||||
mah={400}
|
||||
>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Overlay/>
|
||||
<Modal.Content style={{overflow: "hidden"}}>
|
||||
<Modal.Header py={0}>
|
||||
<Modal.Title fw={500}>{space?.name}</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
<Modal.Title>
|
||||
<Text fw={500} lineClamp={1}>{space?.name}</Text>
|
||||
</Modal.Title>
|
||||
<Modal.CloseButton/>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div style={{ height: rem("600px") }}>
|
||||
<div style={{height: rem(600)}}>
|
||||
<Tabs defaultValue="members">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab fw={500} value="general">
|
||||
@ -55,34 +57,32 @@ export default function SpaceSettingsModal({
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<ScrollArea h="600" w="100%" scrollbarSize={5}>
|
||||
<Tabs.Panel value="general">
|
||||
<SpaceDetails
|
||||
spaceId={space?.id}
|
||||
readOnly={spaceAbility.cannot(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Settings,
|
||||
)}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="general">
|
||||
<SpaceDetails
|
||||
spaceId={space?.id}
|
||||
readOnly={spaceAbility.cannot(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Settings,
|
||||
)}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="members">
|
||||
<Group my="md" justify="flex-end">
|
||||
{spaceAbility.can(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Member,
|
||||
) && <AddSpaceMembersModal spaceId={space?.id} />}
|
||||
</Group>
|
||||
<Tabs.Panel value="members">
|
||||
<Group my="md" justify="flex-end">
|
||||
{spaceAbility.can(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Member,
|
||||
) && <AddSpaceMembersModal spaceId={space?.id}/>}
|
||||
</Group>
|
||||
|
||||
<SpaceMembersList
|
||||
spaceId={space?.id}
|
||||
readOnly={spaceAbility.cannot(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Member,
|
||||
)}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</ScrollArea>
|
||||
<SpaceMembersList
|
||||
spaceId={space?.id}
|
||||
readOnly={spaceAbility.cannot(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Member,
|
||||
)}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { Table, Group, Text, Avatar } from "@mantine/core";
|
||||
import React, { useState } from "react";
|
||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
||||
import {Table, Group, Text, Avatar} from "@mantine/core";
|
||||
import React, {useState} from "react";
|
||||
import {useGetSpacesQuery} from "@/features/space/queries/space-query.ts";
|
||||
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import {formatMemberCount} from "@/lib";
|
||||
|
||||
export default function SpaceList() {
|
||||
const { data, isLoading } = useGetSpacesQuery();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const {data, isLoading} = useGetSpacesQuery();
|
||||
const [opened, {open, close}] = useDisclosure(false);
|
||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
||||
|
||||
const handleClick = (spaceId: string) => {
|
||||
@ -18,44 +18,48 @@ export default function SpaceList() {
|
||||
return (
|
||||
<>
|
||||
{data && (
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Space</Table.Th>
|
||||
<Table.Th>Members</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((space, index) => (
|
||||
<Table.Tr
|
||||
key={index}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleClick(space.id)}
|
||||
>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<Avatar
|
||||
color="initials"
|
||||
variant="filled"
|
||||
name={space.name}
|
||||
/>
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{space.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{space.description}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>{formatMemberCount(space.memberCount)}</Table.Td>
|
||||
<Table.ScrollContainer minWidth={400}>
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Space</Table.Th>
|
||||
<Table.Th>Members</Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((space, index) => (
|
||||
<Table.Tr
|
||||
key={index}
|
||||
style={{cursor: "pointer"}}
|
||||
onClick={() => handleClick(space.id)}
|
||||
>
|
||||
<Table.Td>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Avatar
|
||||
color="initials"
|
||||
variant="filled"
|
||||
name={space.name}
|
||||
/>
|
||||
<div>
|
||||
<Text fz="sm" fw={500} lineClamp={1}>
|
||||
{space.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed" lineClamp={2}>
|
||||
{space.description}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Text size="sm" style={{whiteSpace: 'nowrap'}}>{formatMemberCount(space.memberCount)}</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
|
||||
{selectedSpaceId && (
|
||||
|
||||
@ -1,32 +1,34 @@
|
||||
import { Group, Table, Text, Menu, ActionIcon } from "@mantine/core";
|
||||
import {Group, Table, Text, Menu, ActionIcon} from "@mantine/core";
|
||||
import React from "react";
|
||||
import { IconDots } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import {IconDots} from "@tabler/icons-react";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {CustomAvatar} from "@/components/ui/custom-avatar.tsx";
|
||||
import {
|
||||
useChangeSpaceMemberRoleMutation,
|
||||
useRemoveSpaceMemberMutation,
|
||||
useSpaceMembersQuery,
|
||||
} from "@/features/space/queries/space-query.ts";
|
||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||
import { IRemoveSpaceMember } from "@/features/space/types/space.types.ts";
|
||||
import {IconGroupCircle} from "@/components/icons/icon-people-circle.tsx";
|
||||
import {IRemoveSpaceMember} from "@/features/space/types/space.types.ts";
|
||||
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
||||
import {
|
||||
getSpaceRoleLabel,
|
||||
spaceRoleData,
|
||||
} from "@/features/space/types/space-role-data.ts";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import {formatMemberCount} from "@/lib";
|
||||
|
||||
type MemberType = "user" | "group";
|
||||
|
||||
interface SpaceMembersProps {
|
||||
spaceId: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function SpaceMembersList({
|
||||
spaceId,
|
||||
readOnly,
|
||||
}: SpaceMembersProps) {
|
||||
const { data, isLoading } = useSpaceMembersQuery(spaceId);
|
||||
spaceId,
|
||||
readOnly,
|
||||
}: SpaceMembersProps) {
|
||||
const {data, isLoading} = useSpaceMembersQuery(spaceId);
|
||||
const removeSpaceMember = useRemoveSpaceMemberMutation();
|
||||
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
|
||||
|
||||
@ -85,99 +87,101 @@ export default function SpaceMembersList({
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: { confirm: "Remove", cancel: "Cancel" },
|
||||
confirmProps: { color: "red" },
|
||||
labels: {confirm: "Remove", cancel: "Cancel"},
|
||||
confirmProps: {color: "red"},
|
||||
onConfirm: () => onRemove(memberId, type),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{data && (
|
||||
<Table verticalSpacing={8}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Member</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((member, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
{member.type === "user" && (
|
||||
<CustomAvatar
|
||||
avatarUrl={member?.avatarUrl}
|
||||
name={member.name}
|
||||
/>
|
||||
)}
|
||||
|
||||
{member.type === "group" && <IconGroupCircle />}
|
||||
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{member?.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{member.type == "user" && member?.email}
|
||||
|
||||
{member.type == "group" &&
|
||||
`Group - ${formatMemberCount(member?.memberCount)}`}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<RoleSelectMenu
|
||||
roles={spaceRoleData}
|
||||
roleName={getSpaceRoleLabel(member.role)}
|
||||
onChange={(newRole) =>
|
||||
handleRoleChange(
|
||||
member.id,
|
||||
member.type,
|
||||
newRole,
|
||||
member.role,
|
||||
)
|
||||
}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
{!readOnly && (
|
||||
<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={() =>
|
||||
openRemoveModal(member.id, member.type)
|
||||
}
|
||||
>
|
||||
Remove space member
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table verticalSpacing={8}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Member</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((member, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
{member.type === "user" && (
|
||||
<CustomAvatar
|
||||
avatarUrl={member?.avatarUrl}
|
||||
name={member.name}
|
||||
/>
|
||||
)}
|
||||
|
||||
{member.type === "group" && <IconGroupCircle/>}
|
||||
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{member?.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{member.type == "user" && member?.email}
|
||||
|
||||
{member.type == "group" &&
|
||||
`Group - ${formatMemberCount(member?.memberCount)}`}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<RoleSelectMenu
|
||||
roles={spaceRoleData}
|
||||
roleName={getSpaceRoleLabel(member.role)}
|
||||
onChange={(newRole) =>
|
||||
handleRoleChange(
|
||||
member.id,
|
||||
member.type,
|
||||
newRole,
|
||||
member.role,
|
||||
)
|
||||
}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
{!readOnly && (
|
||||
<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={() =>
|
||||
openRemoveModal(member.id, member.type)
|
||||
}
|
||||
>
|
||||
Remove space member
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -36,7 +36,7 @@ export function useGetSpacesQuery(
|
||||
|
||||
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
|
||||
return useQuery({
|
||||
queryKey: ['spaces', spaceId],
|
||||
queryKey: ['space', spaceId],
|
||||
queryFn: () => getSpaceById(spaceId),
|
||||
enabled: !!spaceId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
@ -65,7 +65,7 @@ export function useGetSpaceBySlugQuery(
|
||||
spaceId: string
|
||||
): UseQueryResult<ISpace, Error> {
|
||||
return useQuery({
|
||||
queryKey: ['spaces', spaceId],
|
||||
queryKey: ['space', spaceId],
|
||||
queryFn: () => getSpaceById(spaceId),
|
||||
enabled: !!spaceId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
@ -111,7 +111,7 @@ export function useDeleteSpaceMutation() {
|
||||
|
||||
if (variables.slug) {
|
||||
queryClient.removeQueries({
|
||||
queryKey: ['spaces', variables.slug],
|
||||
queryKey: ['space', variables.slug],
|
||||
exact: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,62 +1,64 @@
|
||||
import { Group, Table, Avatar, Text, Alert } from "@mantine/core";
|
||||
import { useWorkspaceInvitationsQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||
import {Group, Table, Avatar, Text, 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 {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 { formattedDate } from "@/lib/time.ts";
|
||||
import {IconInfoCircle} from "@tabler/icons-react";
|
||||
import {formattedDate, timeAgo} from "@/lib/time.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
|
||||
export default function WorkspaceInvitesTable() {
|
||||
const { data, isLoading } = useWorkspaceInvitationsQuery({
|
||||
const {data, isLoading} = useWorkspaceInvitationsQuery({
|
||||
limit: 100,
|
||||
});
|
||||
const { isAdmin } = useUserRole();
|
||||
const {isAdmin} = useUserRole();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert variant="light" color="blue" icon={<IconInfoCircle />}>
|
||||
<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 name={invitation.email} color="initials" />
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{invitation.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>{getUserRoleLabel(invitation.role)}</Table.Td>
|
||||
|
||||
<Table.Td>{formattedDate(invitation.createdAt)}</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
{isAdmin && (
|
||||
<InviteActionMenu invitationId={invitation.id} />
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<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.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((invitation, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<Avatar name={invitation.email} color="initials"/>
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{invitation.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>{getUserRoleLabel(invitation.role)}</Table.Td>
|
||||
|
||||
<Table.Td>{timeAgo(invitation.createdAt)}</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
{isAdmin && (
|
||||
<InviteActionMenu invitationId={invitation.id}/>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Group, Table, Text, Badge } from "@mantine/core";
|
||||
import {Group, Table, Text, Badge} from "@mantine/core";
|
||||
import {
|
||||
useChangeMemberRoleMutation,
|
||||
useWorkspaceMembersQuery,
|
||||
} from "@/features/workspace/queries/workspace-query.ts";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import {CustomAvatar} from "@/components/ui/custom-avatar.tsx";
|
||||
import React from "react";
|
||||
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
||||
import {
|
||||
@ -11,14 +11,14 @@ import {
|
||||
userRoleData,
|
||||
} from "@/features/workspace/types/user-role-data.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { UserRole } from "@/lib/types.ts";
|
||||
import {UserRole} from "@/lib/types.ts";
|
||||
|
||||
export default function WorkspaceMembersTable() {
|
||||
const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 });
|
||||
const {data, isLoading} = useWorkspaceMembersQuery({limit: 100});
|
||||
const changeMemberRoleMutation = useChangeMemberRoleMutation();
|
||||
const { isAdmin, isOwner } = useUserRole();
|
||||
const {isAdmin, isOwner} = useUserRole();
|
||||
|
||||
const assignableUserRoles = isOwner ? userRoleData : userRoleData.filter((role) => role.value !== UserRole.OWNER);
|
||||
const assignableUserRoles = isOwner ? userRoleData : userRoleData.filter((role) => role.value !== UserRole.OWNER);
|
||||
|
||||
const handleRoleChange = async (
|
||||
userId: string,
|
||||
@ -40,50 +40,52 @@ export default function WorkspaceMembersTable() {
|
||||
return (
|
||||
<>
|
||||
{data && (
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>User</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((user, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name} />
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{user.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Badge variant="light">Active</Badge>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<RoleSelectMenu
|
||||
roles={assignableUserRoles}
|
||||
roleName={getUserRoleLabel(user.role)}
|
||||
onChange={(newRole) =>
|
||||
handleRoleChange(user.id, user.role, newRole)
|
||||
}
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>User</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((user, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name}/>
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{user.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Badge variant="light">Active</Badge>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<RoleSelectMenu
|
||||
roles={assignableUserRoles}
|
||||
roleName={getUserRoleLabel(user.role)}
|
||||
onChange={(newRole) =>
|
||||
handleRoleChange(user.id, user.role, newRole)
|
||||
}
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user