From 90c190df7884e9611adc27f9b4a58bb0998c7147 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:33:15 +0000 Subject: [PATCH] fix: space members view enhancement --- .../space/components/space-members.tsx | 94 ++++++++++++------- .../src/features/space/queries/space-query.ts | 18 ++-- .../database/repos/space/space-member.repo.ts | 15 ++- 3 files changed, 84 insertions(+), 43 deletions(-) diff --git a/apps/client/src/features/space/components/space-members.tsx b/apps/client/src/features/space/components/space-members.tsx index eadea590..7ee35a17 100644 --- a/apps/client/src/features/space/components/space-members.tsx +++ b/apps/client/src/features/space/components/space-members.tsx @@ -1,19 +1,21 @@ import { + Center, Group, + Loader, Table, Text, Menu, ActionIcon, ScrollArea, } from "@mantine/core"; -import React from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { IconDots } from "@tabler/icons-react"; import { modals } from "@mantine/modals"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { useChangeSpaceMemberRoleMutation, useRemoveSpaceMemberMutation, - useSpaceMembersQuery, + useSpaceMembersInfiniteQuery, } from "@/features/space/queries/space-query.ts"; import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx"; import { IRemoveSpaceMember } from "@/features/space/types/space.types.ts"; @@ -24,9 +26,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"; import { SearchInput } from "@/components/common/search-input.tsx"; -import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search.tsx"; import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx"; type MemberType = "user" | "group"; @@ -41,12 +41,32 @@ export default function SpaceMembersList({ readOnly, }: SpaceMembersProps) { const { t } = useTranslation(); - const { search, cursor, goNext, goPrev, handleSearch } = usePaginateAndSearch(); - const { data, isLoading } = useSpaceMembersQuery(spaceId, { - cursor, - limit: 100, - query: search, - }); + const [search, setSearch] = useState(""); + const handleSearch = useCallback((query: string) => setSearch(query), []); + + const { data, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage } = + useSpaceMembersInfiniteQuery(spaceId, search); + + const sentinelRef = useRef(null); + const viewportRef = useRef(null); + + useEffect(() => { + const sentinel = sentinelRef.current; + if (!sentinel) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + { root: viewportRef.current, threshold: 0.1 }, + ); + + observer.observe(sentinel); + return () => observer.disconnect(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + const removeSpaceMember = useRemoveSpaceMemberMutation(); const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation(); @@ -111,10 +131,12 @@ export default function SpaceMembersList({ onConfirm: () => onRemove(memberId, type), }); + const members = data?.pages.flatMap((page) => page.items) ?? []; + return ( <> - + @@ -126,7 +148,7 @@ export default function SpaceMembersList({ - {data?.items.map((member, index) => ( + {members.map((member, index) => ( @@ -154,19 +176,24 @@ export default function SpaceMembersList({ - - handleRoleChange( - member.id, - member.type, - newRole, - member.role, - ) - } - disabled={readOnly} - /> + {readOnly ? ( + + {t(getSpaceRoleLabel(member.role))} + + ) : ( + + handleRoleChange( + member.id, + member.type, + newRole, + member.role, + ) + } + /> + )} @@ -202,16 +229,15 @@ export default function SpaceMembersList({
-
- {data?.items.length > 0 && ( - goNext(data?.meta?.nextCursor)} - onPrev={goPrev} - /> - )} +
+ + {isFetchingNextPage && ( +
+ +
+ )} + ); } diff --git a/apps/client/src/features/space/queries/space-query.ts b/apps/client/src/features/space/queries/space-query.ts index e15320d6..2ab01154 100644 --- a/apps/client/src/features/space/queries/space-query.ts +++ b/apps/client/src/features/space/queries/space-query.ts @@ -1,5 +1,6 @@ import { keepPreviousData, + useInfiniteQuery, useMutation, useQuery, useQueryClient, @@ -10,7 +11,6 @@ import { IChangeSpaceMemberRole, IRemoveSpaceMember, ISpace, - ISpaceMember, } from "@/features/space/types/space.types"; import { addSpaceMember, @@ -190,15 +190,19 @@ export function useDeleteSpaceMutation() { }); } -export function useSpaceMembersQuery( +export function useSpaceMembersInfiniteQuery( spaceId: string, - params?: QueryParams, -): UseQueryResult, Error> { - return useQuery({ - queryKey: ["spaceMembers", spaceId, params], - queryFn: () => getSpaceMembers(spaceId, params), + query?: string, +) { + return useInfiniteQuery({ + queryKey: ["spaceMembers", spaceId, query], + queryFn: ({ pageParam }) => + getSpaceMembers(spaceId, { cursor: pageParam, limit: 50, query }), enabled: !!spaceId, placeholderData: keepPreviousData, + initialPageParam: undefined as string | undefined, + getNextPageParam: (lastPage) => + lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined, }); } diff --git a/apps/server/src/database/repos/space/space-member.repo.ts b/apps/server/src/database/repos/space/space-member.repo.ts index fb406ecd..908c8497 100644 --- a/apps/server/src/database/repos/space/space-member.repo.ts +++ b/apps/server/src/database/repos/space/space-member.repo.ts @@ -104,6 +104,7 @@ export class SpaceMemberRepo { .leftJoin('users', 'users.id', 'spaceMembers.userId') .leftJoin('groups', 'groups.id', 'spaceMembers.groupId') .select([ + 'spaceMembers.id as id', 'users.id as userId', 'users.name as userName', 'users.avatarUrl as userAvatarUrl', @@ -120,6 +121,12 @@ export class SpaceMemberRepo { 'isGroup', ), ) + .select( + sql`case "space_members"."role" when 'admin' then 1 when 'writer' then 2 when 'reader' then 3 else 4 end`.as( + 'roleOrder', + ), + ) + .select(sql`coalesce(users.name, groups.name)`.as('memberName')) .where('spaceId', '=', spaceId); if (pagination.query) { @@ -149,12 +156,16 @@ export class SpaceMemberRepo { cursor: pagination.cursor, beforeCursor: pagination.beforeCursor, fields: [ + { expression: 'sub.roleOrder', direction: 'asc', key: 'roleOrder' }, { expression: 'sub.isGroup', direction: 'desc', key: 'isGroup' }, - { expression: 'sub.createdAt', direction: 'asc', key: 'createdAt' }, + { expression: 'sub.memberName', direction: 'asc', key: 'memberName' }, + { expression: 'sub.id', direction: 'asc', key: 'id' }, ], parseCursor: (cursor) => ({ + roleOrder: parseInt(cursor.roleOrder, 10), isGroup: parseInt(cursor.isGroup, 10), - createdAt: new Date(cursor.createdAt), + memberName: cursor.memberName, + id: cursor.id, }), });