mirror of
https://github.com/docmost/docmost.git
synced 2025-11-14 03:51:12 +10:00
feat: UI pagination and members search (#724)
* feat: pagination (UI) * Fixes * feat: add search to member list page * responsiveness
This commit is contained in:
19
apps/client/src/components/common/no-table-results.tsx
Normal file
19
apps/client/src/components/common/no-table-results.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Table, Text } from "@mantine/core";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface NoTableResultsProps {
|
||||||
|
colSpan: number;
|
||||||
|
}
|
||||||
|
export default function NoTableResults({ colSpan }: NoTableResultsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={colSpan}>
|
||||||
|
<Text fw={500} c="dimmed" ta="center">
|
||||||
|
{t("No results found...")}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
apps/client/src/components/common/paginate.tsx
Normal file
40
apps/client/src/components/common/paginate.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Button, Group } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface PagePaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
hasPrevPage: boolean;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
onPageChange: (newPage: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Paginate({
|
||||||
|
currentPage,
|
||||||
|
hasPrevPage,
|
||||||
|
hasNextPage,
|
||||||
|
onPageChange,
|
||||||
|
}: PagePaginationProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group mt="md">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="compact-sm"
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={!hasPrevPage}
|
||||||
|
>
|
||||||
|
{t("Prev")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="compact-sm"
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={!hasNextPage}
|
||||||
|
>
|
||||||
|
{t("Next")}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
apps/client/src/components/common/search-input.tsx
Normal file
36
apps/client/src/components/common/search-input.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { TextInput, Group } from "@mantine/core";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import { IconSearch } from "@tabler/icons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface SearchInputProps {
|
||||||
|
placeholder?: string;
|
||||||
|
debounceDelay?: number;
|
||||||
|
onSearch: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchInput({
|
||||||
|
placeholder,
|
||||||
|
debounceDelay = 500,
|
||||||
|
onSearch,
|
||||||
|
}: SearchInputProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const [debouncedValue] = useDebouncedValue(value, debounceDelay);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSearch(debouncedValue);
|
||||||
|
}, [debouncedValue, onSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group mb="md">
|
||||||
|
<TextInput
|
||||||
|
placeholder={placeholder || t("Search...")}
|
||||||
|
leftSection={<IconSearch size={14} />}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -38,7 +38,7 @@ export default function TopMenu() {
|
|||||||
variant="filled"
|
variant="filled"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
<Text fw={500} size="sm" lh={1} mr={3} lineClamp={1}>
|
||||||
{workspace.name}
|
{workspace.name}
|
||||||
</Text>
|
</Text>
|
||||||
<IconChevronDown size={16} />
|
<IconChevronDown size={16} />
|
||||||
|
|||||||
@ -1,75 +1,84 @@
|
|||||||
import { Table, Group, Text, Anchor } from "@mantine/core";
|
import { Table, Group, Text, Anchor } from "@mantine/core";
|
||||||
import { useGetGroupsQuery } from "@/features/group/queries/group-query";
|
import { useGetGroupsQuery } from "@/features/group/queries/group-query";
|
||||||
import React from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { formatMemberCount } from "@/lib";
|
import { formatMemberCount } from "@/lib";
|
||||||
import { IGroup } from "@/features/group/types/group.types.ts";
|
import { IGroup } from "@/features/group/types/group.types.ts";
|
||||||
|
import Paginate from "@/components/common/paginate.tsx";
|
||||||
|
|
||||||
export default function GroupList() {
|
export default function GroupList() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, isLoading } = useGetGroupsQuery();
|
const [page, setPage] = useState(1);
|
||||||
|
const { data, isLoading } = useGetGroupsQuery({ page });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table.ScrollContainer minWidth={400}>
|
<Table highlightOnHover verticalSpacing="sm" layout="fixed">
|
||||||
<Table highlightOnHover verticalSpacing="sm">
|
<Table.Thead>
|
||||||
<Table.Thead>
|
<Table.Tr>
|
||||||
<Table.Tr>
|
<Table.Th>{t("Group")}</Table.Th>
|
||||||
<Table.Th>{t("Group")}</Table.Th>
|
<Table.Th>{t("Members")}</Table.Th>
|
||||||
<Table.Th>{t("Members")}</Table.Th>
|
</Table.Tr>
|
||||||
</Table.Tr>
|
</Table.Thead>
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{data?.items.map((group: IGroup, index: number) => (
|
{data?.items.map((group: IGroup, index: number) => (
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Anchor
|
<Anchor
|
||||||
size="sm"
|
size="sm"
|
||||||
underline="never"
|
underline="never"
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: "var(--mantine-color-text)",
|
color: "var(--mantine-color-text)",
|
||||||
}}
|
}}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/settings/groups/${group.id}`}
|
to={`/settings/groups/${group.id}`}
|
||||||
>
|
>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<IconGroupCircle />
|
<IconGroupCircle />
|
||||||
<div>
|
<div>
|
||||||
<Text fz="sm" fw={500} lineClamp={1}>
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
{group.name}
|
{group.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="xs" c="dimmed" lineClamp={2}>
|
<Text fz="xs" c="dimmed" lineClamp={2}>
|
||||||
{group.description}
|
{group.description}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Anchor
|
<Anchor
|
||||||
size="sm"
|
size="sm"
|
||||||
underline="never"
|
underline="never"
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: "var(--mantine-color-text)",
|
color: "var(--mantine-color-text)",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/settings/groups/${group.id}`}
|
to={`/settings/groups/${group.id}`}
|
||||||
>
|
>
|
||||||
{formatMemberCount(group.memberCount, t)}
|
{formatMemberCount(group.memberCount, t)}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Table.ScrollContainer>
|
</Table.ScrollContainer>
|
||||||
|
|
||||||
|
{data?.items.length > 0 && (
|
||||||
|
<Paginate
|
||||||
|
currentPage={page}
|
||||||
|
hasPrevPage={data?.meta.hasPrevPage}
|
||||||
|
hasNextPage={data?.meta.hasNextPage}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,18 +4,20 @@ import {
|
|||||||
useRemoveGroupMemberMutation,
|
useRemoveGroupMemberMutation,
|
||||||
} from "@/features/group/queries/group-query";
|
} from "@/features/group/queries/group-query";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { IconDots } from "@tabler/icons-react";
|
import { IconDots } from "@tabler/icons-react";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { IUser } from "@/features/user/types/user.types.ts";
|
import { IUser } from "@/features/user/types/user.types.ts";
|
||||||
|
import Paginate from "@/components/common/paginate.tsx";
|
||||||
|
|
||||||
export default function GroupMembersList() {
|
export default function GroupMembersList() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { groupId } = useParams();
|
const { groupId } = useParams();
|
||||||
const { data, isLoading } = useGroupMembersQuery(groupId);
|
const [page, setPage] = useState(1);
|
||||||
|
const { data, isLoading } = useGroupMembersQuery(groupId, { page });
|
||||||
const removeGroupMember = useRemoveGroupMemberMutation();
|
const removeGroupMember = useRemoveGroupMemberMutation();
|
||||||
const { isAdmin } = useUserRole();
|
const { isAdmin } = useUserRole();
|
||||||
|
|
||||||
@ -45,67 +47,71 @@ export default function GroupMembersList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table.ScrollContainer minWidth={500}>
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
<Table verticalSpacing="sm">
|
<Table.Thead>
|
||||||
<Table.Thead>
|
<Table.Tr>
|
||||||
<Table.Tr>
|
<Table.Th>{t("User")}</Table.Th>
|
||||||
<Table.Th>{t("User")}</Table.Th>
|
<Table.Th>{t("Status")}</Table.Th>
|
||||||
<Table.Th>{t("Status")}</Table.Th>
|
<Table.Th></Table.Th>
|
||||||
<Table.Th></Table.Th>
|
</Table.Tr>
|
||||||
</Table.Tr>
|
</Table.Thead>
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{data?.items.map((user: IUser, index: number) => (
|
{data?.items.map((user: IUser, index: number) => (
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap="sm">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<CustomAvatar
|
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name} />
|
||||||
avatarUrl={user.avatarUrl}
|
<div>
|
||||||
name={user.name}
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
/>
|
{user.name}
|
||||||
<div>
|
</Text>
|
||||||
<Text fz="sm" fw={500}>
|
<Text fz="xs" c="dimmed">
|
||||||
{user.name}
|
{user.email}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="xs" c="dimmed">
|
</div>
|
||||||
{user.email}
|
</Group>
|
||||||
</Text>
|
</Table.Td>
|
||||||
</div>
|
<Table.Td>
|
||||||
</Group>
|
<Badge variant="light">{t("Active")}</Badge>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Badge variant="light">{t("Active")}</Badge>
|
{isAdmin && (
|
||||||
</Table.Td>
|
<Menu
|
||||||
<Table.Td>
|
shadow="xl"
|
||||||
{isAdmin && (
|
position="bottom-end"
|
||||||
<Menu
|
offset={20}
|
||||||
shadow="xl"
|
width={200}
|
||||||
position="bottom-end"
|
withArrow
|
||||||
offset={20}
|
arrowPosition="center"
|
||||||
width={200}
|
>
|
||||||
withArrow
|
<Menu.Target>
|
||||||
arrowPosition="center"
|
<ActionIcon variant="subtle" c="gray">
|
||||||
>
|
<IconDots size={20} stroke={2} />
|
||||||
<Menu.Target>
|
</ActionIcon>
|
||||||
<ActionIcon variant="subtle" c="gray">
|
</Menu.Target>
|
||||||
<IconDots size={20} stroke={2} />
|
<Menu.Dropdown>
|
||||||
</ActionIcon>
|
<Menu.Item onClick={() => openRemoveModal(user.id)}>
|
||||||
</Menu.Target>
|
{t("Remove group member")}
|
||||||
<Menu.Dropdown>
|
</Menu.Item>
|
||||||
<Menu.Item onClick={() => openRemoveModal(user.id)}>
|
</Menu.Dropdown>
|
||||||
{t("Remove group member")}
|
</Menu>
|
||||||
</Menu.Item>
|
)}
|
||||||
</Menu.Dropdown>
|
</Table.Td>
|
||||||
</Menu>
|
</Table.Tr>
|
||||||
)}
|
))}
|
||||||
</Table.Td>
|
</Table.Tbody>
|
||||||
</Table.Tr>
|
</Table>
|
||||||
))}
|
</Table.ScrollContainer>
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
{data?.items.length > 0 && (
|
||||||
</Table.ScrollContainer>
|
<Paginate
|
||||||
|
currentPage={page}
|
||||||
|
hasPrevPage={data?.meta.hasPrevPage}
|
||||||
|
hasNextPage={data?.meta.hasNextPage}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,14 +14,14 @@ interface MultiUserSelectProps {
|
|||||||
const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||||
option,
|
option,
|
||||||
}) => (
|
}) => (
|
||||||
<Group gap="sm">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<CustomAvatar
|
<CustomAvatar
|
||||||
avatarUrl={option?.["avatarUrl"]}
|
avatarUrl={option?.["avatarUrl"]}
|
||||||
name={option.label}
|
name={option.label}
|
||||||
size={36}
|
size={36}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm">{option.label}</Text>
|
<Text size="sm" lineClamp={1}>{option.label}</Text>
|
||||||
<Text size="xs" opacity={0.5}>
|
<Text size="xs" opacity={0.5}>
|
||||||
{option?.["email"]}
|
{option?.["email"]}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import {
|
|||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
UseQueryResult,
|
UseQueryResult,
|
||||||
} from '@tanstack/react-query';
|
keepPreviousData,
|
||||||
import { IGroup } from '@/features/group/types/group.types';
|
} from "@tanstack/react-query";
|
||||||
|
import { IGroup } from "@/features/group/types/group.types";
|
||||||
import {
|
import {
|
||||||
addGroupMember,
|
addGroupMember,
|
||||||
createGroup,
|
createGroup,
|
||||||
@ -14,22 +15,24 @@ import {
|
|||||||
getGroups,
|
getGroups,
|
||||||
removeGroupMember,
|
removeGroupMember,
|
||||||
updateGroup,
|
updateGroup,
|
||||||
} from '@/features/group/services/group-service';
|
} from "@/features/group/services/group-service";
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from "@mantine/notifications";
|
||||||
import { QueryParams } from '@/lib/types.ts';
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||||
|
import { IUser } from "@/features/user/types/user.types.ts";
|
||||||
|
|
||||||
export function useGetGroupsQuery(
|
export function useGetGroupsQuery(
|
||||||
params?: QueryParams
|
params?: QueryParams,
|
||||||
): UseQueryResult<any, Error> {
|
): UseQueryResult<IPagination<IGroup>, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['groups', params],
|
queryKey: ["groups", params],
|
||||||
queryFn: () => getGroups(params),
|
queryFn: () => getGroups(params),
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
|
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['group', groupId],
|
queryKey: ["group", groupId],
|
||||||
queryFn: () => getGroupById(groupId),
|
queryFn: () => getGroupById(groupId),
|
||||||
enabled: !!groupId,
|
enabled: !!groupId,
|
||||||
});
|
});
|
||||||
@ -42,13 +45,13 @@ export function useCreateGroupMutation() {
|
|||||||
mutationFn: (data) => createGroup(data),
|
mutationFn: (data) => createGroup(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['groups'],
|
queryKey: ["groups"],
|
||||||
});
|
});
|
||||||
|
|
||||||
notifications.show({ message: 'Group created successfully' });
|
notifications.show({ message: "Group created successfully" });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
notifications.show({ message: 'Failed to create group', color: 'red' });
|
notifications.show({ message: "Failed to create group", color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -59,14 +62,14 @@ export function useUpdateGroupMutation() {
|
|||||||
return useMutation<IGroup, Error, Partial<IGroup>>({
|
return useMutation<IGroup, Error, Partial<IGroup>>({
|
||||||
mutationFn: (data) => updateGroup(data),
|
mutationFn: (data) => updateGroup(data),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: 'Group updated successfully' });
|
notifications.show({ message: "Group updated successfully" });
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['group', variables.groupId],
|
queryKey: ["group", variables.groupId],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error['response']?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({ message: errorMessage, color: 'red' });
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -77,28 +80,25 @@ export function useDeleteGroupMutation() {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (groupId: string) => deleteGroup({ groupId }),
|
mutationFn: (groupId: string) => deleteGroup({ groupId }),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: 'Group deleted successfully' });
|
notifications.show({ message: "Group deleted successfully" });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["groups"] });
|
||||||
const groups = queryClient.getQueryData(['groups']) as any;
|
|
||||||
if (groups) {
|
|
||||||
groups.items = groups.items?.filter(
|
|
||||||
(group: IGroup) => group.id !== variables
|
|
||||||
);
|
|
||||||
queryClient.setQueryData(['groups'], groups);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error['response']?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({ message: errorMessage, color: 'red' });
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGroupMembersQuery(groupId: string) {
|
export function useGroupMembersQuery(
|
||||||
|
groupId: string,
|
||||||
|
params?: QueryParams,
|
||||||
|
): UseQueryResult<IPagination<IUser>, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['groupMembers', groupId],
|
queryKey: ["groupMembers", groupId, params],
|
||||||
queryFn: () => getGroupMembers(groupId),
|
queryFn: () => getGroupMembers(groupId, params),
|
||||||
enabled: !!groupId,
|
enabled: !!groupId,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,15 +108,15 @@ export function useAddGroupMemberMutation() {
|
|||||||
return useMutation<void, Error, { groupId: string; userIds: string[] }>({
|
return useMutation<void, Error, { groupId: string; userIds: string[] }>({
|
||||||
mutationFn: (data) => addGroupMember(data),
|
mutationFn: (data) => addGroupMember(data),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: 'Added successfully' });
|
notifications.show({ message: "Added successfully" });
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['groupMembers', variables.groupId],
|
queryKey: ["groupMembers", variables.groupId],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
message: 'Failed to add group members',
|
message: "Failed to add group members",
|
||||||
color: 'red',
|
color: "red",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -135,14 +135,14 @@ export function useRemoveGroupMemberMutation() {
|
|||||||
>({
|
>({
|
||||||
mutationFn: (data) => removeGroupMember(data),
|
mutationFn: (data) => removeGroupMember(data),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: 'Removed successfully' });
|
notifications.show({ message: "Removed successfully" });
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['groupMembers', variables.groupId],
|
queryKey: ["groupMembers", variables.groupId],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error['response']?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({ message: errorMessage, color: 'red' });
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import api from "@/lib/api-client";
|
import api from "@/lib/api-client";
|
||||||
import { IGroup } from "@/features/group/types/group.types";
|
import { IGroup } from "@/features/group/types/group.types";
|
||||||
import { QueryParams } from "@/lib/types.ts";
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||||
|
import { IUser } from "@/features/user/types/user.types.ts";
|
||||||
|
|
||||||
export async function getGroups(params?: QueryParams): Promise<any> {
|
export async function getGroups(
|
||||||
// TODO: returns paginated. Fix type
|
params?: QueryParams,
|
||||||
const req = await api.post<any>("/groups", params);
|
): Promise<IPagination<IGroup>> {
|
||||||
|
const req = await api.post("/groups", params);
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,8 +29,11 @@ export async function deleteGroup(data: { groupId: string }): Promise<void> {
|
|||||||
await api.post("/groups/delete", data);
|
await api.post("/groups/delete", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGroupMembers(groupId: string) {
|
export async function getGroupMembers(
|
||||||
const req = await api.post("/groups/members", { groupId });
|
groupId: string,
|
||||||
|
params?: QueryParams,
|
||||||
|
): Promise<IPagination<IUser>> {
|
||||||
|
const req = await api.post("/groups/members", { groupId, params });
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ interface MultiMemberSelectProps {
|
|||||||
const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||||
option,
|
option,
|
||||||
}) => (
|
}) => (
|
||||||
<Group gap="sm">
|
<Group gap="sm" wrap="nowrap">
|
||||||
{option["type"] === "user" && (
|
{option["type"] === "user" && (
|
||||||
<CustomAvatar
|
<CustomAvatar
|
||||||
avatarUrl={option["avatarUrl"]}
|
avatarUrl={option["avatarUrl"]}
|
||||||
@ -25,7 +25,7 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
|||||||
)}
|
)}
|
||||||
{option["type"] === "group" && <IconGroupCircle />}
|
{option["type"] === "group" && <IconGroupCircle />}
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm">{option.label}</Text>
|
<Text size="sm" lineClamp={1}>{option.label}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,10 +12,10 @@ interface SpaceSelectProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderSelectOption: SelectProps["renderOption"] = ({ option }) => (
|
const renderSelectOption: SelectProps["renderOption"] = ({ option }) => (
|
||||||
<Group gap="sm">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Avatar color="initials" variant="filled" name={option.label} size={20} />
|
<Avatar color="initials" variant="filled" name={option.label} size={20} />
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm">{option.label}</Text>
|
<Text size="sm" lineClamp={1}>{option.label}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,10 +5,12 @@ import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
|||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { formatMemberCount } from "@/lib";
|
import { formatMemberCount } from "@/lib";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Paginate from "@/components/common/paginate.tsx";
|
||||||
|
|
||||||
export default function SpaceList() {
|
export default function SpaceList() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, isLoading } = useGetSpacesQuery();
|
const [page, setPage] = useState(1);
|
||||||
|
const { data, isLoading } = useGetSpacesQuery({ page });
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
||||||
|
|
||||||
@ -19,50 +21,57 @@ export default function SpaceList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table.ScrollContainer minWidth={400}>
|
<Table highlightOnHover verticalSpacing="sm" layout="fixed">
|
||||||
<Table highlightOnHover verticalSpacing="sm">
|
<Table.Thead>
|
||||||
<Table.Thead>
|
<Table.Tr>
|
||||||
<Table.Tr>
|
<Table.Th>{t("Space")}</Table.Th>
|
||||||
<Table.Th>{t("Space")}</Table.Th>
|
<Table.Th>{t("Members")}</Table.Th>
|
||||||
<Table.Th>{t("Members")}</Table.Th>
|
</Table.Tr>
|
||||||
</Table.Tr>
|
</Table.Thead>
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{data?.items.map((space, index) => (
|
{data?.items.map((space, index) => (
|
||||||
<Table.Tr
|
<Table.Tr
|
||||||
key={index}
|
key={index}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
onClick={() => handleClick(space.id)}
|
onClick={() => handleClick(space.id)}
|
||||||
>
|
>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Avatar
|
<Avatar
|
||||||
color="initials"
|
color="initials"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
name={space.name}
|
name={space.name}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Text fz="sm" fw={500} lineClamp={1}>
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
{space.name}
|
{space.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="xs" c="dimmed" lineClamp={2}>
|
<Text fz="xs" c="dimmed" lineClamp={2}>
|
||||||
{space.description}
|
{space.description}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||||
{formatMemberCount(space.memberCount, t)}
|
{formatMemberCount(space.memberCount, t)}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Table.ScrollContainer>
|
</Table.ScrollContainer>
|
||||||
|
|
||||||
|
{data?.items.length > 0 && (
|
||||||
|
<Paginate
|
||||||
|
currentPage={page}
|
||||||
|
hasPrevPage={data?.meta.hasPrevPage}
|
||||||
|
hasNextPage={data?.meta.hasNextPage}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedSpaceId && (
|
{selectedSpaceId && (
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Group, Table, Text, Menu, ActionIcon } from "@mantine/core";
|
import { Group, Table, Text, Menu, ActionIcon } from "@mantine/core";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { IconDots } from "@tabler/icons-react";
|
import { IconDots } from "@tabler/icons-react";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
} from "@/features/space/types/space-role-data.ts";
|
} from "@/features/space/types/space-role-data.ts";
|
||||||
import { formatMemberCount } from "@/lib";
|
import { formatMemberCount } from "@/lib";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Paginate from "@/components/common/paginate.tsx";
|
||||||
|
|
||||||
type MemberType = "user" | "group";
|
type MemberType = "user" | "group";
|
||||||
|
|
||||||
@ -30,8 +31,11 @@ export default function SpaceMembersList({
|
|||||||
readOnly,
|
readOnly,
|
||||||
}: SpaceMembersProps) {
|
}: SpaceMembersProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, isLoading } = useSpaceMembersQuery(spaceId);
|
const [page, setPage] = useState(1);
|
||||||
|
const { data, isLoading } = useSpaceMembersQuery(spaceId, {
|
||||||
|
page,
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
const removeSpaceMember = useRemoveSpaceMemberMutation();
|
const removeSpaceMember = useRemoveSpaceMemberMutation();
|
||||||
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
|
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
|
||||||
|
|
||||||
@ -98,94 +102,101 @@ export default function SpaceMembersList({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table.ScrollContainer minWidth={500}>
|
<Table highlightOnHover verticalSpacing={8}>
|
||||||
<Table verticalSpacing={8}>
|
<Table.Thead>
|
||||||
<Table.Thead>
|
<Table.Tr>
|
||||||
<Table.Tr>
|
<Table.Th>{t("Member")}</Table.Th>
|
||||||
<Table.Th>{t("Member")}</Table.Th>
|
<Table.Th>{t("Role")}</Table.Th>
|
||||||
<Table.Th>{t("Role")}</Table.Th>
|
<Table.Th></Table.Th>
|
||||||
<Table.Th></Table.Th>
|
</Table.Tr>
|
||||||
</Table.Tr>
|
</Table.Thead>
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{data?.items.map((member, index) => (
|
{data?.items.map((member, index) => (
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap="sm">
|
<Group gap="sm" wrap="nowrap">
|
||||||
{member.type === "user" && (
|
{member.type === "user" && (
|
||||||
<CustomAvatar
|
<CustomAvatar
|
||||||
avatarUrl={member?.avatarUrl}
|
avatarUrl={member?.avatarUrl}
|
||||||
name={member.name}
|
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" &&
|
|
||||||
`${t("Group")} - ${formatMemberCount(member?.memberCount, t)}`}
|
|
||||||
</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)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("Remove space member")}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
)}
|
)}
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
{member.type === "group" && <IconGroupCircle />}
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
<div>
|
||||||
</Table>
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
</Table.ScrollContainer>
|
{member?.name}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed">
|
||||||
|
{member.type == "user" && member?.email}
|
||||||
|
|
||||||
|
{member.type == "group" &&
|
||||||
|
`${t("Group")} - ${formatMemberCount(member?.memberCount, t)}`}
|
||||||
|
</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)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("Remove space member")}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
|
|
||||||
|
{data?.items.length > 0 && (
|
||||||
|
<Paginate
|
||||||
|
currentPage={page}
|
||||||
|
hasPrevPage={data?.meta.hasPrevPage}
|
||||||
|
hasNextPage={data?.meta.hasNextPage}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
|
keepPreviousData,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
UseQueryResult,
|
UseQueryResult,
|
||||||
} from '@tanstack/react-query';
|
} from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
IAddSpaceMember,
|
IAddSpaceMember,
|
||||||
IChangeSpaceMemberRole,
|
IChangeSpaceMemberRole,
|
||||||
IRemoveSpaceMember,
|
IRemoveSpaceMember,
|
||||||
ISpace,
|
ISpace,
|
||||||
ISpaceMember,
|
ISpaceMember,
|
||||||
} from '@/features/space/types/space.types';
|
} from "@/features/space/types/space.types";
|
||||||
import {
|
import {
|
||||||
addSpaceMember,
|
addSpaceMember,
|
||||||
changeMemberRole,
|
changeMemberRole,
|
||||||
@ -21,22 +22,23 @@ import {
|
|||||||
createSpace,
|
createSpace,
|
||||||
updateSpace,
|
updateSpace,
|
||||||
deleteSpace,
|
deleteSpace,
|
||||||
} from '@/features/space/services/space-service.ts';
|
} from "@/features/space/services/space-service.ts";
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from "@mantine/notifications";
|
||||||
import { IPagination, QueryParams } from '@/lib/types.ts';
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||||
|
|
||||||
export function useGetSpacesQuery(
|
export function useGetSpacesQuery(
|
||||||
params?: QueryParams
|
params?: QueryParams,
|
||||||
): UseQueryResult<IPagination<ISpace>, Error> {
|
): UseQueryResult<IPagination<ISpace>, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['spaces', params],
|
queryKey: ["spaces", params],
|
||||||
queryFn: () => getSpaces(params),
|
queryFn: () => getSpaces(params),
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
|
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['space', spaceId],
|
queryKey: ["space", spaceId],
|
||||||
queryFn: () => getSpaceById(spaceId),
|
queryFn: () => getSpaceById(spaceId),
|
||||||
enabled: !!spaceId,
|
enabled: !!spaceId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
@ -50,22 +52,22 @@ export function useCreateSpaceMutation() {
|
|||||||
mutationFn: (data) => createSpace(data),
|
mutationFn: (data) => createSpace(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['spaces'],
|
queryKey: ["spaces"],
|
||||||
});
|
});
|
||||||
notifications.show({ message: 'Space created successfully' });
|
notifications.show({ message: "Space created successfully" });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error['response']?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({ message: errorMessage, color: 'red' });
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGetSpaceBySlugQuery(
|
export function useGetSpaceBySlugQuery(
|
||||||
spaceId: string
|
spaceId: string,
|
||||||
): UseQueryResult<ISpace, Error> {
|
): UseQueryResult<ISpace, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['space', spaceId],
|
queryKey: ["space", spaceId],
|
||||||
queryFn: () => getSpaceById(spaceId),
|
queryFn: () => getSpaceById(spaceId),
|
||||||
enabled: !!spaceId,
|
enabled: !!spaceId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
@ -78,25 +80,25 @@ export function useUpdateSpaceMutation() {
|
|||||||
return useMutation<ISpace, Error, Partial<ISpace>>({
|
return useMutation<ISpace, Error, Partial<ISpace>>({
|
||||||
mutationFn: (data) => updateSpace(data),
|
mutationFn: (data) => updateSpace(data),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: 'Space updated successfully' });
|
notifications.show({ message: "Space updated successfully" });
|
||||||
|
|
||||||
const space = queryClient.getQueryData([
|
const space = queryClient.getQueryData([
|
||||||
'space',
|
"space",
|
||||||
variables.spaceId,
|
variables.spaceId,
|
||||||
]) as ISpace;
|
]) as ISpace;
|
||||||
if (space) {
|
if (space) {
|
||||||
const updatedSpace = { ...space, ...data };
|
const updatedSpace = { ...space, ...data };
|
||||||
queryClient.setQueryData(['space', variables.spaceId], updatedSpace);
|
queryClient.setQueryData(["space", variables.spaceId], updatedSpace);
|
||||||
queryClient.setQueryData(['space', data.slug], updatedSpace);
|
queryClient.setQueryData(["space", data.slug], updatedSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['spaces'],
|
queryKey: ["spaces"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error['response']?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({ message: errorMessage, color: 'red' });
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -107,37 +109,39 @@ export function useDeleteSpaceMutation() {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (data: Partial<ISpace>) => deleteSpace(data.id),
|
mutationFn: (data: Partial<ISpace>) => deleteSpace(data.id),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: 'Space deleted successfully' });
|
notifications.show({ message: "Space deleted successfully" });
|
||||||
|
|
||||||
if (variables.slug) {
|
if (variables.slug) {
|
||||||
queryClient.removeQueries({
|
queryClient.removeQueries({
|
||||||
queryKey: ['space', variables.slug],
|
queryKey: ["space", variables.slug],
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const spaces = queryClient.getQueryData(['spaces']) as any;
|
const spaces = queryClient.getQueryData(["spaces"]) as any;
|
||||||
if (spaces) {
|
if (spaces) {
|
||||||
spaces.items = spaces.items?.filter(
|
spaces.items = spaces.items?.filter(
|
||||||
(space: ISpace) => space.id !== variables.id
|
(space: ISpace) => space.id !== variables.id,
|
||||||
);
|
);
|
||||||
queryClient.setQueryData(['spaces'], spaces);
|
queryClient.setQueryData(["spaces"], spaces);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error['response']?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({ message: errorMessage, color: 'red' });
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSpaceMembersQuery(
|
export function useSpaceMembersQuery(
|
||||||
spaceId: string
|
spaceId: string,
|
||||||
|
params?: QueryParams,
|
||||||
): UseQueryResult<IPagination<ISpaceMember>, Error> {
|
): UseQueryResult<IPagination<ISpaceMember>, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['spaceMembers', spaceId],
|
queryKey: ["spaceMembers", spaceId, params],
|
||||||
queryFn: () => getSpaceMembers(spaceId),
|
queryFn: () => getSpaceMembers(spaceId, params),
|
||||||
enabled: !!spaceId,
|
enabled: !!spaceId,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,14 +151,14 @@ export function useAddSpaceMemberMutation() {
|
|||||||
return useMutation<void, Error, IAddSpaceMember>({
|
return useMutation<void, Error, IAddSpaceMember>({
|
||||||
mutationFn: (data) => addSpaceMember(data),
|
mutationFn: (data) => addSpaceMember(data),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: 'Members added successfully' });
|
notifications.show({ message: "Members added successfully" });
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['spaceMembers', variables.spaceId],
|
queryKey: ["spaceMembers", variables.spaceId],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error['response']?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({ message: errorMessage, color: 'red' });
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -165,14 +169,14 @@ export function useRemoveSpaceMemberMutation() {
|
|||||||
return useMutation<void, Error, IRemoveSpaceMember>({
|
return useMutation<void, Error, IRemoveSpaceMember>({
|
||||||
mutationFn: (data) => removeSpaceMember(data),
|
mutationFn: (data) => removeSpaceMember(data),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: 'Removed successfully' });
|
notifications.show({ message: "Removed successfully" });
|
||||||
queryClient.refetchQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['spaceMembers', variables.spaceId],
|
queryKey: ["spaceMembers", variables.spaceId],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error['response']?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({ message: errorMessage, color: 'red' });
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -183,15 +187,15 @@ export function useChangeSpaceMemberRoleMutation() {
|
|||||||
return useMutation<void, Error, IChangeSpaceMemberRole>({
|
return useMutation<void, Error, IChangeSpaceMemberRole>({
|
||||||
mutationFn: (data) => changeMemberRole(data),
|
mutationFn: (data) => changeMemberRole(data),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: 'Member role updated successfully' });
|
notifications.show({ message: "Member role updated successfully" });
|
||||||
// due to pagination levels, change in cache instead
|
// due to pagination levels, change in cache instead
|
||||||
queryClient.refetchQueries({
|
queryClient.refetchQueries({
|
||||||
queryKey: ['spaceMembers', variables.spaceId],
|
queryKey: ["spaceMembers", variables.spaceId],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error['response']?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({ message: errorMessage, color: 'red' });
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,14 @@ import {
|
|||||||
IExportSpaceParams,
|
IExportSpaceParams,
|
||||||
IRemoveSpaceMember,
|
IRemoveSpaceMember,
|
||||||
ISpace,
|
ISpace,
|
||||||
|
ISpaceMember,
|
||||||
} from "@/features/space/types/space.types";
|
} from "@/features/space/types/space.types";
|
||||||
import { IPagination, QueryParams } from "@/lib/types.ts";
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||||
import { IUser } from "@/features/user/types/user.types.ts";
|
import { IUser } from "@/features/user/types/user.types.ts";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
|
|
||||||
export async function getSpaces(
|
export async function getSpaces(
|
||||||
params?: QueryParams
|
params?: QueryParams,
|
||||||
): Promise<IPagination<ISpace>> {
|
): Promise<IPagination<ISpace>> {
|
||||||
const req = await api.post("/spaces", params);
|
const req = await api.post("/spaces", params);
|
||||||
return req.data;
|
return req.data;
|
||||||
@ -37,9 +38,10 @@ export async function deleteSpace(spaceId: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getSpaceMembers(
|
export async function getSpaceMembers(
|
||||||
spaceId: string
|
spaceId: string,
|
||||||
): Promise<IPagination<IUser>> {
|
params?: QueryParams,
|
||||||
const req = await api.post<any>("/spaces/members", { spaceId });
|
): Promise<IPagination<ISpaceMember>> {
|
||||||
|
const req = await api.post<any>("/spaces/members", { spaceId, params });
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,13 +50,13 @@ export async function addSpaceMember(data: IAddSpaceMember): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function removeSpaceMember(
|
export async function removeSpaceMember(
|
||||||
data: IRemoveSpaceMember
|
data: IRemoveSpaceMember,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await api.post("/spaces/members/remove", data);
|
await api.post("/spaces/members/remove", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function changeMemberRole(
|
export async function changeMemberRole(
|
||||||
data: IChangeSpaceMemberRole
|
data: IChangeSpaceMemberRole,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await api.post("/spaces/members/change-role", data);
|
await api.post("/spaces/members/change-role", data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,22 @@
|
|||||||
import {Group, Table, Avatar, Text, Alert} from "@mantine/core";
|
import { Group, Table, Avatar, Text, Alert } from "@mantine/core";
|
||||||
import {useWorkspaceInvitationsQuery} from "@/features/workspace/queries/workspace-query.ts";
|
import { useWorkspaceInvitationsQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||||
import React from "react";
|
import React, { useState } 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 InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx";
|
||||||
import {IconInfoCircle} from "@tabler/icons-react";
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
import {formattedDate, timeAgo} from "@/lib/time.ts";
|
import { timeAgo } from "@/lib/time.ts";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Paginate from "@/components/common/paginate.tsx";
|
||||||
|
|
||||||
export default function WorkspaceInvitesTable() {
|
export default function WorkspaceInvitesTable() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
const { data, isLoading } = useWorkspaceInvitationsQuery({
|
const { data, isLoading } = useWorkspaceInvitationsQuery({
|
||||||
|
page,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
const {isAdmin} = useUserRole();
|
const { isAdmin } = useUserRole();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -23,47 +26,50 @@ export default function WorkspaceInvitesTable() {
|
|||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{data && (
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<>
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
<Table.ScrollContainer minWidth={500}>
|
<Table.Thead>
|
||||||
<Table verticalSpacing="sm">
|
<Table.Tr>
|
||||||
<Table.Thead>
|
<Table.Th>{t("Email")}</Table.Th>
|
||||||
<Table.Tr>
|
<Table.Th>{t("Role")}</Table.Th>
|
||||||
<Table.Th>{t("Email")}</Table.Th>
|
<Table.Th>{t("Date")}</Table.Th>
|
||||||
<Table.Th>{t("Role")}</Table.Th>
|
</Table.Tr>
|
||||||
<Table.Th>{t("Date")}</Table.Th>
|
</Table.Thead>
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{data?.items.map((invitation, index) => (
|
{data?.items.map((invitation, index) => (
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap="sm">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Avatar name={invitation.email} color="initials"/>
|
<Avatar name={invitation.email} color="initials" />
|
||||||
<div>
|
<div>
|
||||||
<Text fz="sm" fw={500}>
|
<Text fz="sm" fw={500}>
|
||||||
{invitation.email}
|
{invitation.email}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
|
||||||
<Table.Td>{t(getUserRoleLabel(invitation.role))}</Table.Td>
|
<Table.Td>{t(getUserRoleLabel(invitation.role))}</Table.Td>
|
||||||
|
|
||||||
<Table.Td>{timeAgo(invitation.createdAt)}</Table.Td>
|
<Table.Td>{timeAgo(invitation.createdAt)}</Table.Td>
|
||||||
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{isAdmin && (
|
{isAdmin && <InviteActionMenu invitationId={invitation.id} />}
|
||||||
<InviteActionMenu invitationId={invitation.id}/>
|
</Table.Td>
|
||||||
)}
|
</Table.Tr>
|
||||||
</Table.Td>
|
))}
|
||||||
</Table.Tr>
|
</Table.Tbody>
|
||||||
))}
|
</Table>
|
||||||
</Table.Tbody>
|
</Table.ScrollContainer>
|
||||||
</Table>
|
|
||||||
</Table.ScrollContainer>
|
{data?.items.length > 0 && (
|
||||||
</>
|
<Paginate
|
||||||
|
currentPage={page}
|
||||||
|
hasPrevPage={data?.meta.hasPrevPage}
|
||||||
|
hasNextPage={data?.meta.hasNextPage}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import {Group, Table, Text, Badge} from "@mantine/core";
|
import { Group, Table, Text, Badge } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
useChangeMemberRoleMutation,
|
useChangeMemberRoleMutation,
|
||||||
useWorkspaceMembersQuery,
|
useWorkspaceMembersQuery,
|
||||||
} from "@/features/workspace/queries/workspace-query.ts";
|
} 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 React, { useState } from "react";
|
||||||
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
||||||
import {
|
import {
|
||||||
getUserRoleLabel,
|
getUserRoleLabel,
|
||||||
@ -13,12 +13,21 @@ import {
|
|||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
import { UserRole } from "@/lib/types.ts";
|
import { UserRole } from "@/lib/types.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Paginate from "@/components/common/paginate.tsx";
|
||||||
|
import { SearchInput } from "@/components/common/search-input.tsx";
|
||||||
|
import NoTableResults from "@/components/common/no-table-results.tsx";
|
||||||
|
|
||||||
export default function WorkspaceMembersTable() {
|
export default function WorkspaceMembersTable() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 });
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState(undefined);
|
||||||
|
const { data, isLoading } = useWorkspaceMembersQuery({
|
||||||
|
page,
|
||||||
|
limit: 100,
|
||||||
|
query: search,
|
||||||
|
});
|
||||||
const changeMemberRoleMutation = useChangeMemberRoleMutation();
|
const changeMemberRoleMutation = useChangeMemberRoleMutation();
|
||||||
const {isAdmin, isOwner} = useUserRole();
|
const { isAdmin, isOwner } = useUserRole();
|
||||||
|
|
||||||
const assignableUserRoles = isOwner
|
const assignableUserRoles = isOwner
|
||||||
? userRoleData
|
? userRoleData
|
||||||
@ -43,25 +52,34 @@ export default function WorkspaceMembersTable() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
<SearchInput
|
||||||
<Table.ScrollContainer minWidth={500}>
|
onSearch={(debouncedSearch) => {
|
||||||
<Table verticalSpacing="sm">
|
setSearch(debouncedSearch);
|
||||||
<Table.Thead>
|
setPage(1);
|
||||||
<Table.Tr>
|
}}
|
||||||
<Table.Th>{t("User")}</Table.Th>
|
/>
|
||||||
<Table.Th>{t("Status")}</Table.Th>
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table.Th>{t("Role")}</Table.Th>
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
</Table.Tr>
|
<Table.Thead>
|
||||||
</Table.Thead>
|
<Table.Tr>
|
||||||
|
<Table.Th>{t("User")}</Table.Th>
|
||||||
|
<Table.Th>{t("Status")}</Table.Th>
|
||||||
|
<Table.Th>{t("Role")}</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{data?.items.map((user, index) => (
|
{data?.items.length > 0 ? (
|
||||||
|
data?.items.map((user, index) => (
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap="sm">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name}/>
|
<CustomAvatar
|
||||||
|
avatarUrl={user.avatarUrl}
|
||||||
|
name={user.name}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Text fz="sm" fw={500}>
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
{user.name}
|
{user.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="xs" c="dimmed">
|
<Text fz="xs" c="dimmed">
|
||||||
@ -84,10 +102,21 @@ export default function WorkspaceMembersTable() {
|
|||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))
|
||||||
</Table.Tbody>
|
) : (
|
||||||
</Table>
|
<NoTableResults colSpan={3} />
|
||||||
</Table.ScrollContainer>
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
|
|
||||||
|
{data?.items.length > 0 && (
|
||||||
|
<Paginate
|
||||||
|
currentPage={page}
|
||||||
|
hasPrevPage={data?.meta.hasPrevPage}
|
||||||
|
hasNextPage={data?.meta.hasNextPage}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
keepPreviousData,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
@ -22,6 +23,7 @@ import {
|
|||||||
IInvitation,
|
IInvitation,
|
||||||
IWorkspace,
|
IWorkspace,
|
||||||
} from "@/features/workspace/types/workspace.types.ts";
|
} from "@/features/workspace/types/workspace.types.ts";
|
||||||
|
import { IUser } from "@/features/user/types/user.types.ts";
|
||||||
|
|
||||||
export function useWorkspaceQuery(): UseQueryResult<IWorkspace, Error> {
|
export function useWorkspaceQuery(): UseQueryResult<IWorkspace, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@ -40,10 +42,13 @@ export function useWorkspacePublicDataQuery(): UseQueryResult<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkspaceMembersQuery(params?: QueryParams) {
|
export function useWorkspaceMembersQuery(
|
||||||
|
params?: QueryParams,
|
||||||
|
): UseQueryResult<IPagination<IUser>, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["workspaceMembers", params],
|
queryKey: ["workspaceMembers", params],
|
||||||
queryFn: () => getWorkspaceMembers(params),
|
queryFn: () => getWorkspaceMembers(params),
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +58,6 @@ export function useChangeMemberRoleMutation() {
|
|||||||
return useMutation<any, Error, any>({
|
return useMutation<any, Error, any>({
|
||||||
mutationFn: (data) => changeMemberRole(data),
|
mutationFn: (data) => changeMemberRole(data),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
// TODO: change in cache instead
|
|
||||||
notifications.show({ message: "Member role updated successfully" });
|
notifications.show({ message: "Member role updated successfully" });
|
||||||
queryClient.refetchQueries({
|
queryClient.refetchQueries({
|
||||||
queryKey: ["workspaceMembers"],
|
queryKey: ["workspaceMembers"],
|
||||||
@ -72,6 +76,7 @@ export function useWorkspaceInvitationsQuery(
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["invitations", params],
|
queryKey: ["invitations", params],
|
||||||
queryFn: () => getPendingInvitations(params),
|
queryFn: () => getPendingInvitations(params),
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +87,6 @@ export function useCreateInvitationMutation() {
|
|||||||
mutationFn: (data) => createInvitation(data),
|
mutationFn: (data) => createInvitation(data),
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: "Invitation sent" });
|
notifications.show({ message: "Invitation sent" });
|
||||||
// TODO: mutate cache
|
|
||||||
queryClient.refetchQueries({
|
queryClient.refetchQueries({
|
||||||
queryKey: ["invitations"],
|
queryKey: ["invitations"],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,7 +18,6 @@ export async function getWorkspacePublicData(): Promise<IWorkspace> {
|
|||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: fix all paginated types
|
|
||||||
export async function getWorkspaceMembers(
|
export async function getWorkspaceMembers(
|
||||||
params?: QueryParams,
|
params?: QueryParams,
|
||||||
): Promise<IPagination<IUser>> {
|
): Promise<IPagination<IUser>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user