diff --git a/apps/client/src/components/common/no-table-results.tsx b/apps/client/src/components/common/no-table-results.tsx new file mode 100644 index 00000000..124bbb9b --- /dev/null +++ b/apps/client/src/components/common/no-table-results.tsx @@ -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 ( + + + + {t("No results found...")} + + + + ); +} diff --git a/apps/client/src/components/common/paginate.tsx b/apps/client/src/components/common/paginate.tsx new file mode 100644 index 00000000..4f632c95 --- /dev/null +++ b/apps/client/src/components/common/paginate.tsx @@ -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 ( + + + + + + ); +} diff --git a/apps/client/src/components/common/search-input.tsx b/apps/client/src/components/common/search-input.tsx new file mode 100644 index 00000000..e1b64df8 --- /dev/null +++ b/apps/client/src/components/common/search-input.tsx @@ -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 ( + + } + value={value} + onChange={(e) => setValue(e.currentTarget.value)} + /> + + ); +} diff --git a/apps/client/src/components/layouts/global/top-menu.tsx b/apps/client/src/components/layouts/global/top-menu.tsx index 6e09be73..28c64660 100644 --- a/apps/client/src/components/layouts/global/top-menu.tsx +++ b/apps/client/src/components/layouts/global/top-menu.tsx @@ -38,7 +38,7 @@ export default function TopMenu() { variant="filled" size="sm" /> - + {workspace.name} diff --git a/apps/client/src/features/group/components/group-list.tsx b/apps/client/src/features/group/components/group-list.tsx index 46394d02..1d7ad768 100644 --- a/apps/client/src/features/group/components/group-list.tsx +++ b/apps/client/src/features/group/components/group-list.tsx @@ -1,75 +1,84 @@ import { Table, Group, Text, Anchor } from "@mantine/core"; import { useGetGroupsQuery } from "@/features/group/queries/group-query"; -import React from "react"; +import { useState } from "react"; import { Link } from "react-router-dom"; import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx"; import { useTranslation } from "react-i18next"; import { formatMemberCount } from "@/lib"; import { IGroup } from "@/features/group/types/group.types.ts"; +import Paginate from "@/components/common/paginate.tsx"; export default function GroupList() { const { t } = useTranslation(); - const { data, isLoading } = useGetGroupsQuery(); + const [page, setPage] = useState(1); + const { data, isLoading } = useGetGroupsQuery({ page }); return ( <> - {data && ( - - - - - {t("Group")} - {t("Members")} - - + +
+ + + {t("Group")} + {t("Members")} + + - - {data?.items.map((group: IGroup, index: number) => ( - - - - - -
- - {group.name} - - - {group.description} - -
-
-
-
- - - {formatMemberCount(group.memberCount, t)} - - -
- ))} -
-
-
+ + {data?.items.map((group: IGroup, index: number) => ( + + + + + +
+ + {group.name} + + + {group.description} + +
+
+
+
+ + + {formatMemberCount(group.memberCount, t)} + + +
+ ))} +
+ + + + {data?.items.length > 0 && ( + )} ); diff --git a/apps/client/src/features/group/components/group-members.tsx b/apps/client/src/features/group/components/group-members.tsx index f4d5de1b..45dc91dc 100644 --- a/apps/client/src/features/group/components/group-members.tsx +++ b/apps/client/src/features/group/components/group-members.tsx @@ -4,18 +4,20 @@ import { useRemoveGroupMemberMutation, } from "@/features/group/queries/group-query"; import { useParams } from "react-router-dom"; -import React from "react"; +import React, { useState } from "react"; 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"; import { useTranslation } from "react-i18next"; import { IUser } from "@/features/user/types/user.types.ts"; +import Paginate from "@/components/common/paginate.tsx"; export default function GroupMembersList() { const { t } = useTranslation(); const { groupId } = useParams(); - const { data, isLoading } = useGroupMembersQuery(groupId); + const [page, setPage] = useState(1); + const { data, isLoading } = useGroupMembersQuery(groupId, { page }); const removeGroupMember = useRemoveGroupMemberMutation(); const { isAdmin } = useUserRole(); @@ -45,67 +47,71 @@ export default function GroupMembersList() { return ( <> - {data && ( - - - - - {t("User")} - {t("Status")} - - - + +
+ + + {t("User")} + {t("Status")} + + + - - {data?.items.map((user: IUser, index: number) => ( - - - - -
- - {user.name} - - - {user.email} - -
-
-
- - {t("Active")} - - - {isAdmin && ( - - - - - - - - openRemoveModal(user.id)}> - {t("Remove group member")} - - - - )} - -
- ))} -
-
-
+ + {data?.items.map((user: IUser, index: number) => ( + + + + +
+ + {user.name} + + + {user.email} + +
+
+
+ + {t("Active")} + + + {isAdmin && ( + + + + + + + + openRemoveModal(user.id)}> + {t("Remove group member")} + + + + )} + +
+ ))} +
+ + + + {data?.items.length > 0 && ( + )} ); diff --git a/apps/client/src/features/group/components/multi-user-select.tsx b/apps/client/src/features/group/components/multi-user-select.tsx index e020e6c3..bb9272bf 100644 --- a/apps/client/src/features/group/components/multi-user-select.tsx +++ b/apps/client/src/features/group/components/multi-user-select.tsx @@ -14,14 +14,14 @@ interface MultiUserSelectProps { const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({ option, }) => ( - +
- {option.label} + {option.label} {option?.["email"]} diff --git a/apps/client/src/features/group/queries/group-query.ts b/apps/client/src/features/group/queries/group-query.ts index ac113f87..6fa8f6d5 100644 --- a/apps/client/src/features/group/queries/group-query.ts +++ b/apps/client/src/features/group/queries/group-query.ts @@ -3,8 +3,9 @@ import { useQuery, useQueryClient, UseQueryResult, -} from '@tanstack/react-query'; -import { IGroup } from '@/features/group/types/group.types'; + keepPreviousData, +} from "@tanstack/react-query"; +import { IGroup } from "@/features/group/types/group.types"; import { addGroupMember, createGroup, @@ -14,22 +15,24 @@ import { getGroups, removeGroupMember, updateGroup, -} from '@/features/group/services/group-service'; -import { notifications } from '@mantine/notifications'; -import { QueryParams } from '@/lib/types.ts'; +} from "@/features/group/services/group-service"; +import { notifications } from "@mantine/notifications"; +import { IPagination, QueryParams } from "@/lib/types.ts"; +import { IUser } from "@/features/user/types/user.types.ts"; export function useGetGroupsQuery( - params?: QueryParams -): UseQueryResult { + params?: QueryParams, +): UseQueryResult, Error> { return useQuery({ - queryKey: ['groups', params], + queryKey: ["groups", params], queryFn: () => getGroups(params), + placeholderData: keepPreviousData, }); } export function useGroupQuery(groupId: string): UseQueryResult { return useQuery({ - queryKey: ['group', groupId], + queryKey: ["group", groupId], queryFn: () => getGroupById(groupId), enabled: !!groupId, }); @@ -42,13 +45,13 @@ export function useCreateGroupMutation() { mutationFn: (data) => createGroup(data), onSuccess: () => { queryClient.invalidateQueries({ - queryKey: ['groups'], + queryKey: ["groups"], }); - notifications.show({ message: 'Group created successfully' }); + notifications.show({ message: "Group created successfully" }); }, 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>({ mutationFn: (data) => updateGroup(data), onSuccess: (data, variables) => { - notifications.show({ message: 'Group updated successfully' }); + notifications.show({ message: "Group updated successfully" }); queryClient.invalidateQueries({ - queryKey: ['group', variables.groupId], + queryKey: ["group", variables.groupId], }); }, onError: (error) => { - const errorMessage = error['response']?.data?.message; - notifications.show({ message: errorMessage, color: 'red' }); + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); }, }); } @@ -77,28 +80,25 @@ export function useDeleteGroupMutation() { return useMutation({ mutationFn: (groupId: string) => deleteGroup({ groupId }), onSuccess: (data, variables) => { - notifications.show({ message: 'Group deleted successfully' }); - - const groups = queryClient.getQueryData(['groups']) as any; - if (groups) { - groups.items = groups.items?.filter( - (group: IGroup) => group.id !== variables - ); - queryClient.setQueryData(['groups'], groups); - } + notifications.show({ message: "Group deleted successfully" }); + queryClient.refetchQueries({ queryKey: ["groups"] }); }, onError: (error) => { - const errorMessage = error['response']?.data?.message; - notifications.show({ message: errorMessage, color: 'red' }); + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); }, }); } -export function useGroupMembersQuery(groupId: string) { +export function useGroupMembersQuery( + groupId: string, + params?: QueryParams, +): UseQueryResult, Error> { return useQuery({ - queryKey: ['groupMembers', groupId], - queryFn: () => getGroupMembers(groupId), + queryKey: ["groupMembers", groupId, params], + queryFn: () => getGroupMembers(groupId, params), enabled: !!groupId, + placeholderData: keepPreviousData, }); } @@ -108,15 +108,15 @@ export function useAddGroupMemberMutation() { return useMutation({ mutationFn: (data) => addGroupMember(data), onSuccess: (data, variables) => { - notifications.show({ message: 'Added successfully' }); + notifications.show({ message: "Added successfully" }); queryClient.invalidateQueries({ - queryKey: ['groupMembers', variables.groupId], + queryKey: ["groupMembers", variables.groupId], }); }, onError: () => { notifications.show({ - message: 'Failed to add group members', - color: 'red', + message: "Failed to add group members", + color: "red", }); }, }); @@ -135,14 +135,14 @@ export function useRemoveGroupMemberMutation() { >({ mutationFn: (data) => removeGroupMember(data), onSuccess: (data, variables) => { - notifications.show({ message: 'Removed successfully' }); + notifications.show({ message: "Removed successfully" }); queryClient.invalidateQueries({ - queryKey: ['groupMembers', variables.groupId], + queryKey: ["groupMembers", variables.groupId], }); }, onError: (error) => { - const errorMessage = error['response']?.data?.message; - notifications.show({ message: errorMessage, color: 'red' }); + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); }, }); } diff --git a/apps/client/src/features/group/services/group-service.ts b/apps/client/src/features/group/services/group-service.ts index a5e97605..dfd9e4f4 100644 --- a/apps/client/src/features/group/services/group-service.ts +++ b/apps/client/src/features/group/services/group-service.ts @@ -1,10 +1,12 @@ import api from "@/lib/api-client"; 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 { - // TODO: returns paginated. Fix type - const req = await api.post("/groups", params); +export async function getGroups( + params?: QueryParams, +): Promise> { + const req = await api.post("/groups", params); return req.data; } @@ -27,8 +29,11 @@ export async function deleteGroup(data: { groupId: string }): Promise { await api.post("/groups/delete", data); } -export async function getGroupMembers(groupId: string) { - const req = await api.post("/groups/members", { groupId }); +export async function getGroupMembers( + groupId: string, + params?: QueryParams, +): Promise> { + const req = await api.post("/groups/members", { groupId, params }); return req.data; } diff --git a/apps/client/src/features/space/components/multi-member-select.tsx b/apps/client/src/features/space/components/multi-member-select.tsx index 86dd2f2a..efa2142f 100644 --- a/apps/client/src/features/space/components/multi-member-select.tsx +++ b/apps/client/src/features/space/components/multi-member-select.tsx @@ -15,7 +15,7 @@ interface MultiMemberSelectProps { const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({ option, }) => ( - + {option["type"] === "user" && ( }
- {option.label} + {option.label}
); diff --git a/apps/client/src/features/space/components/sidebar/space-select.tsx b/apps/client/src/features/space/components/sidebar/space-select.tsx index 20c66013..961b0cf0 100644 --- a/apps/client/src/features/space/components/sidebar/space-select.tsx +++ b/apps/client/src/features/space/components/sidebar/space-select.tsx @@ -12,10 +12,10 @@ interface SpaceSelectProps { } const renderSelectOption: SelectProps["renderOption"] = ({ option }) => ( - +
- {option.label} + {option.label}
); diff --git a/apps/client/src/features/space/components/space-list.tsx b/apps/client/src/features/space/components/space-list.tsx index 1528869d..6bb29128 100644 --- a/apps/client/src/features/space/components/space-list.tsx +++ b/apps/client/src/features/space/components/space-list.tsx @@ -5,10 +5,12 @@ import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx"; import { useDisclosure } from "@mantine/hooks"; import { formatMemberCount } from "@/lib"; import { useTranslation } from "react-i18next"; +import Paginate from "@/components/common/paginate.tsx"; export default function SpaceList() { const { t } = useTranslation(); - const { data, isLoading } = useGetSpacesQuery(); + const [page, setPage] = useState(1); + const { data, isLoading } = useGetSpacesQuery({ page }); const [opened, { open, close }] = useDisclosure(false); const [selectedSpaceId, setSelectedSpaceId] = useState(null); @@ -19,50 +21,57 @@ export default function SpaceList() { return ( <> - {data && ( - - - - - {t("Space")} - {t("Members")} - - + +
+ + + {t("Space")} + {t("Members")} + + - - {data?.items.map((space, index) => ( - handleClick(space.id)} - > - - - -
- - {space.name} - - - {space.description} - -
-
-
- - - {formatMemberCount(space.memberCount, t)} - - -
- ))} -
-
-
+ + {data?.items.map((space, index) => ( + handleClick(space.id)} + > + + + +
+ + {space.name} + + + {space.description} + +
+
+
+ + + {formatMemberCount(space.memberCount, t)} + + +
+ ))} +
+ + + + {data?.items.length > 0 && ( + )} {selectedSpaceId && ( diff --git a/apps/client/src/features/space/components/space-members.tsx b/apps/client/src/features/space/components/space-members.tsx index dc0bc1f7..b1c0e026 100644 --- a/apps/client/src/features/space/components/space-members.tsx +++ b/apps/client/src/features/space/components/space-members.tsx @@ -1,5 +1,5 @@ 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 { modals } from "@mantine/modals"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; @@ -17,6 +17,7 @@ import { } from "@/features/space/types/space-role-data.ts"; import { formatMemberCount } from "@/lib"; import { useTranslation } from "react-i18next"; +import Paginate from "@/components/common/paginate.tsx"; type MemberType = "user" | "group"; @@ -30,8 +31,11 @@ export default function SpaceMembersList({ readOnly, }: SpaceMembersProps) { 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 changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation(); @@ -98,94 +102,101 @@ export default function SpaceMembersList({ return ( <> - {data && ( - - - - - {t("Member")} - {t("Role")} - - - + +
+ + + {t("Member")} + {t("Role")} + + + - - {data?.items.map((member, index) => ( - - - - {member.type === "user" && ( - - )} - - {member.type === "group" && } - -
- - {member?.name} - - - {member.type == "user" && member?.email} - - {member.type == "group" && - `${t("Group")} - ${formatMemberCount(member?.memberCount, t)}`} - -
-
-
- - - - handleRoleChange( - member.id, - member.type, - newRole, - member.role, - ) - } - disabled={readOnly} - /> - - - - {!readOnly && ( - - - - - - - - - - openRemoveModal(member.id, member.type) - } - > - {t("Remove space member")} - - - + + {data?.items.map((member, index) => ( + + + + {member.type === "user" && ( + )} - - - ))} - -
-
+ + {member.type === "group" && } + +
+ + {member?.name} + + + {member.type == "user" && member?.email} + + {member.type == "group" && + `${t("Group")} - ${formatMemberCount(member?.memberCount, t)}`} + +
+
+ + + + + handleRoleChange( + member.id, + member.type, + newRole, + member.role, + ) + } + disabled={readOnly} + /> + + + + {!readOnly && ( + + + + + + + + + + openRemoveModal(member.id, member.type) + } + > + {t("Remove space member")} + + + + )} + + + ))} + + + + + {data?.items.length > 0 && ( + )} ); diff --git a/apps/client/src/features/space/queries/space-query.ts b/apps/client/src/features/space/queries/space-query.ts index e1fa7a12..f0e5f5b5 100644 --- a/apps/client/src/features/space/queries/space-query.ts +++ b/apps/client/src/features/space/queries/space-query.ts @@ -1,16 +1,17 @@ import { + keepPreviousData, useMutation, useQuery, useQueryClient, UseQueryResult, -} from '@tanstack/react-query'; +} from "@tanstack/react-query"; import { IAddSpaceMember, IChangeSpaceMemberRole, IRemoveSpaceMember, ISpace, ISpaceMember, -} from '@/features/space/types/space.types'; +} from "@/features/space/types/space.types"; import { addSpaceMember, changeMemberRole, @@ -21,22 +22,23 @@ import { createSpace, updateSpace, deleteSpace, -} from '@/features/space/services/space-service.ts'; -import { notifications } from '@mantine/notifications'; -import { IPagination, QueryParams } from '@/lib/types.ts'; +} from "@/features/space/services/space-service.ts"; +import { notifications } from "@mantine/notifications"; +import { IPagination, QueryParams } from "@/lib/types.ts"; export function useGetSpacesQuery( - params?: QueryParams + params?: QueryParams, ): UseQueryResult, Error> { return useQuery({ - queryKey: ['spaces', params], + queryKey: ["spaces", params], queryFn: () => getSpaces(params), + placeholderData: keepPreviousData, }); } export function useSpaceQuery(spaceId: string): UseQueryResult { return useQuery({ - queryKey: ['space', spaceId], + queryKey: ["space", spaceId], queryFn: () => getSpaceById(spaceId), enabled: !!spaceId, staleTime: 5 * 60 * 1000, @@ -50,22 +52,22 @@ export function useCreateSpaceMutation() { mutationFn: (data) => createSpace(data), onSuccess: () => { queryClient.invalidateQueries({ - queryKey: ['spaces'], + queryKey: ["spaces"], }); - notifications.show({ message: 'Space created successfully' }); + notifications.show({ message: "Space created successfully" }); }, onError: (error) => { - const errorMessage = error['response']?.data?.message; - notifications.show({ message: errorMessage, color: 'red' }); + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); }, }); } export function useGetSpaceBySlugQuery( - spaceId: string + spaceId: string, ): UseQueryResult { return useQuery({ - queryKey: ['space', spaceId], + queryKey: ["space", spaceId], queryFn: () => getSpaceById(spaceId), enabled: !!spaceId, staleTime: 5 * 60 * 1000, @@ -78,25 +80,25 @@ export function useUpdateSpaceMutation() { return useMutation>({ mutationFn: (data) => updateSpace(data), onSuccess: (data, variables) => { - notifications.show({ message: 'Space updated successfully' }); + notifications.show({ message: "Space updated successfully" }); const space = queryClient.getQueryData([ - 'space', + "space", variables.spaceId, ]) as ISpace; if (space) { const updatedSpace = { ...space, ...data }; - queryClient.setQueryData(['space', variables.spaceId], updatedSpace); - queryClient.setQueryData(['space', data.slug], updatedSpace); + queryClient.setQueryData(["space", variables.spaceId], updatedSpace); + queryClient.setQueryData(["space", data.slug], updatedSpace); } queryClient.invalidateQueries({ - queryKey: ['spaces'], + queryKey: ["spaces"], }); }, onError: (error) => { - const errorMessage = error['response']?.data?.message; - notifications.show({ message: errorMessage, color: 'red' }); + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); }, }); } @@ -107,37 +109,39 @@ export function useDeleteSpaceMutation() { return useMutation({ mutationFn: (data: Partial) => deleteSpace(data.id), onSuccess: (data, variables) => { - notifications.show({ message: 'Space deleted successfully' }); + notifications.show({ message: "Space deleted successfully" }); if (variables.slug) { queryClient.removeQueries({ - queryKey: ['space', variables.slug], + queryKey: ["space", variables.slug], exact: true, }); } - const spaces = queryClient.getQueryData(['spaces']) as any; + const spaces = queryClient.getQueryData(["spaces"]) as any; if (spaces) { 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) => { - const errorMessage = error['response']?.data?.message; - notifications.show({ message: errorMessage, color: 'red' }); + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); }, }); } export function useSpaceMembersQuery( - spaceId: string + spaceId: string, + params?: QueryParams, ): UseQueryResult, Error> { return useQuery({ - queryKey: ['spaceMembers', spaceId], - queryFn: () => getSpaceMembers(spaceId), + queryKey: ["spaceMembers", spaceId, params], + queryFn: () => getSpaceMembers(spaceId, params), enabled: !!spaceId, + placeholderData: keepPreviousData, }); } @@ -147,14 +151,14 @@ export function useAddSpaceMemberMutation() { return useMutation({ mutationFn: (data) => addSpaceMember(data), onSuccess: (data, variables) => { - notifications.show({ message: 'Members added successfully' }); + notifications.show({ message: "Members added successfully" }); queryClient.invalidateQueries({ - queryKey: ['spaceMembers', variables.spaceId], + queryKey: ["spaceMembers", variables.spaceId], }); }, onError: (error) => { - const errorMessage = error['response']?.data?.message; - notifications.show({ message: errorMessage, color: 'red' }); + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); }, }); } @@ -165,14 +169,14 @@ export function useRemoveSpaceMemberMutation() { return useMutation({ mutationFn: (data) => removeSpaceMember(data), onSuccess: (data, variables) => { - notifications.show({ message: 'Removed successfully' }); - queryClient.refetchQueries({ - queryKey: ['spaceMembers', variables.spaceId], + notifications.show({ message: "Removed successfully" }); + queryClient.invalidateQueries({ + queryKey: ["spaceMembers", variables.spaceId], }); }, onError: (error) => { - const errorMessage = error['response']?.data?.message; - notifications.show({ message: errorMessage, color: 'red' }); + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); }, }); } @@ -183,15 +187,15 @@ export function useChangeSpaceMemberRoleMutation() { return useMutation({ mutationFn: (data) => changeMemberRole(data), 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 queryClient.refetchQueries({ - queryKey: ['spaceMembers', variables.spaceId], + queryKey: ["spaceMembers", variables.spaceId], }); }, onError: (error) => { - const errorMessage = error['response']?.data?.message; - notifications.show({ message: errorMessage, color: 'red' }); + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); }, }); } diff --git a/apps/client/src/features/space/services/space-service.ts b/apps/client/src/features/space/services/space-service.ts index cd0c247e..74dc57f9 100644 --- a/apps/client/src/features/space/services/space-service.ts +++ b/apps/client/src/features/space/services/space-service.ts @@ -5,13 +5,14 @@ import { IExportSpaceParams, IRemoveSpaceMember, ISpace, + ISpaceMember, } from "@/features/space/types/space.types"; import { IPagination, QueryParams } from "@/lib/types.ts"; import { IUser } from "@/features/user/types/user.types.ts"; import { saveAs } from "file-saver"; export async function getSpaces( - params?: QueryParams + params?: QueryParams, ): Promise> { const req = await api.post("/spaces", params); return req.data; @@ -37,9 +38,10 @@ export async function deleteSpace(spaceId: string): Promise { } export async function getSpaceMembers( - spaceId: string -): Promise> { - const req = await api.post("/spaces/members", { spaceId }); + spaceId: string, + params?: QueryParams, +): Promise> { + const req = await api.post("/spaces/members", { spaceId, params }); return req.data; } @@ -48,13 +50,13 @@ export async function addSpaceMember(data: IAddSpaceMember): Promise { } export async function removeSpaceMember( - data: IRemoveSpaceMember + data: IRemoveSpaceMember, ): Promise { await api.post("/spaces/members/remove", data); } export async function changeMemberRole( - data: IChangeSpaceMemberRole + data: IChangeSpaceMemberRole, ): Promise { await api.post("/spaces/members/change-role", data); } diff --git a/apps/client/src/features/workspace/components/members/components/workspace-invites-table.tsx b/apps/client/src/features/workspace/components/members/components/workspace-invites-table.tsx index 7b3464bf..bc7b9db5 100644 --- a/apps/client/src/features/workspace/components/members/components/workspace-invites-table.tsx +++ b/apps/client/src/features/workspace/components/members/components/workspace-invites-table.tsx @@ -1,19 +1,22 @@ -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 { Group, Table, Avatar, Text, Alert } from "@mantine/core"; +import { useWorkspaceInvitationsQuery } from "@/features/workspace/queries/workspace-query.ts"; +import React, { useState } from "react"; +import { getUserRoleLabel } from "@/features/workspace/types/user-role-data.ts"; import InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx"; -import {IconInfoCircle} from "@tabler/icons-react"; -import {formattedDate, timeAgo} from "@/lib/time.ts"; +import { IconInfoCircle } from "@tabler/icons-react"; +import { timeAgo } from "@/lib/time.ts"; import useUserRole from "@/hooks/use-user-role.tsx"; import { useTranslation } from "react-i18next"; +import Paginate from "@/components/common/paginate.tsx"; export default function WorkspaceInvitesTable() { const { t } = useTranslation(); + const [page, setPage] = useState(1); const { data, isLoading } = useWorkspaceInvitationsQuery({ + page, limit: 100, }); - const {isAdmin} = useUserRole(); + const { isAdmin } = useUserRole(); return ( <> @@ -23,47 +26,50 @@ export default function WorkspaceInvitesTable() { )} - {data && ( - <> - - - - - {t("Email")} - {t("Role")} - {t("Date")} - - + +
+ + + {t("Email")} + {t("Role")} + {t("Date")} + + - - {data?.items.map((invitation, index) => ( - - - - -
- - {invitation.email} - -
-
-
+ + {data?.items.map((invitation, index) => ( + + + + +
+ + {invitation.email} + +
+
+
- {t(getUserRoleLabel(invitation.role))} + {t(getUserRoleLabel(invitation.role))} - {timeAgo(invitation.createdAt)} + {timeAgo(invitation.createdAt)} - - {isAdmin && ( - - )} - -
- ))} -
-
-
- + + {isAdmin && } + + + ))} + + + + + {data?.items.length > 0 && ( + )} ); diff --git a/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx b/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx index 0c4bd679..ed5f8172 100644 --- a/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx +++ b/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx @@ -1,10 +1,10 @@ -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 React from "react"; +import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import React, { useState } from "react"; import RoleSelectMenu from "@/components/ui/role-select-menu.tsx"; import { getUserRoleLabel, @@ -13,12 +13,21 @@ import { import useUserRole from "@/hooks/use-user-role.tsx"; import { UserRole } from "@/lib/types.ts"; 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() { 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 {isAdmin, isOwner} = useUserRole(); + const { isAdmin, isOwner } = useUserRole(); const assignableUserRoles = isOwner ? userRoleData @@ -43,25 +52,34 @@ export default function WorkspaceMembersTable() { return ( <> - {data && ( - - - - - {t("User")} - {t("Status")} - {t("Role")} - - + { + setSearch(debouncedSearch); + setPage(1); + }} + /> + +
+ + + {t("User")} + {t("Status")} + {t("Role")} + + - - {data?.items.map((user, index) => ( + + {data?.items.length > 0 ? ( + data?.items.map((user, index) => ( - - + +
- + {user.name} @@ -84,10 +102,21 @@ export default function WorkspaceMembersTable() { /> - ))} - -
-
+ )) + ) : ( + + )} + + + + + {data?.items.length > 0 && ( + )} ); diff --git a/apps/client/src/features/workspace/queries/workspace-query.ts b/apps/client/src/features/workspace/queries/workspace-query.ts index 204b83b4..c9f288de 100644 --- a/apps/client/src/features/workspace/queries/workspace-query.ts +++ b/apps/client/src/features/workspace/queries/workspace-query.ts @@ -1,4 +1,5 @@ import { + keepPreviousData, useMutation, useQuery, useQueryClient, @@ -22,6 +23,7 @@ import { IInvitation, IWorkspace, } from "@/features/workspace/types/workspace.types.ts"; +import { IUser } from "@/features/user/types/user.types.ts"; export function useWorkspaceQuery(): UseQueryResult { return useQuery({ @@ -40,10 +42,13 @@ export function useWorkspacePublicDataQuery(): UseQueryResult< }); } -export function useWorkspaceMembersQuery(params?: QueryParams) { +export function useWorkspaceMembersQuery( + params?: QueryParams, +): UseQueryResult, Error> { return useQuery({ queryKey: ["workspaceMembers", params], queryFn: () => getWorkspaceMembers(params), + placeholderData: keepPreviousData, }); } @@ -53,7 +58,6 @@ export function useChangeMemberRoleMutation() { return useMutation({ mutationFn: (data) => changeMemberRole(data), onSuccess: (data, variables) => { - // TODO: change in cache instead notifications.show({ message: "Member role updated successfully" }); queryClient.refetchQueries({ queryKey: ["workspaceMembers"], @@ -72,6 +76,7 @@ export function useWorkspaceInvitationsQuery( return useQuery({ queryKey: ["invitations", params], queryFn: () => getPendingInvitations(params), + placeholderData: keepPreviousData, }); } @@ -82,7 +87,6 @@ export function useCreateInvitationMutation() { mutationFn: (data) => createInvitation(data), onSuccess: (data, variables) => { notifications.show({ message: "Invitation sent" }); - // TODO: mutate cache queryClient.refetchQueries({ queryKey: ["invitations"], }); diff --git a/apps/client/src/features/workspace/services/workspace-service.ts b/apps/client/src/features/workspace/services/workspace-service.ts index c8e9c893..e1bc2071 100644 --- a/apps/client/src/features/workspace/services/workspace-service.ts +++ b/apps/client/src/features/workspace/services/workspace-service.ts @@ -18,7 +18,6 @@ export async function getWorkspacePublicData(): Promise { return req.data; } -// Todo: fix all paginated types export async function getWorkspaceMembers( params?: QueryParams, ): Promise> {