mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
fix: space members view enhancement
This commit is contained in:
@@ -1,19 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
|
Center,
|
||||||
Group,
|
Group,
|
||||||
|
Loader,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Menu,
|
Menu,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import React from "react";
|
import React, { useCallback, useEffect, useRef, 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 {
|
import {
|
||||||
useChangeSpaceMemberRoleMutation,
|
useChangeSpaceMemberRoleMutation,
|
||||||
useRemoveSpaceMemberMutation,
|
useRemoveSpaceMemberMutation,
|
||||||
useSpaceMembersQuery,
|
useSpaceMembersInfiniteQuery,
|
||||||
} from "@/features/space/queries/space-query.ts";
|
} from "@/features/space/queries/space-query.ts";
|
||||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||||
import { IRemoveSpaceMember } from "@/features/space/types/space.types.ts";
|
import { IRemoveSpaceMember } from "@/features/space/types/space.types.ts";
|
||||||
@@ -24,9 +26,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";
|
|
||||||
import { SearchInput } from "@/components/common/search-input.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";
|
import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
|
||||||
|
|
||||||
type MemberType = "user" | "group";
|
type MemberType = "user" | "group";
|
||||||
@@ -41,12 +41,32 @@ export default function SpaceMembersList({
|
|||||||
readOnly,
|
readOnly,
|
||||||
}: SpaceMembersProps) {
|
}: SpaceMembersProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { search, cursor, goNext, goPrev, handleSearch } = usePaginateAndSearch();
|
const [search, setSearch] = useState("");
|
||||||
const { data, isLoading } = useSpaceMembersQuery(spaceId, {
|
const handleSearch = useCallback((query: string) => setSearch(query), []);
|
||||||
cursor,
|
|
||||||
limit: 100,
|
const { data, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
||||||
query: search,
|
useSpaceMembersInfiniteQuery(spaceId, search);
|
||||||
});
|
|
||||||
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||||
|
const viewportRef = useRef<HTMLDivElement>(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 removeSpaceMember = useRemoveSpaceMemberMutation();
|
||||||
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
|
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
|
||||||
|
|
||||||
@@ -111,10 +131,12 @@ export default function SpaceMembersList({
|
|||||||
onConfirm: () => onRemove(memberId, type),
|
onConfirm: () => onRemove(memberId, type),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const members = data?.pages.flatMap((page) => page.items) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchInput onSearch={handleSearch} />
|
<SearchInput onSearch={handleSearch} />
|
||||||
<ScrollArea h={450}>
|
<ScrollArea h={450} viewportRef={viewportRef}>
|
||||||
<Table.ScrollContainer minWidth={500}>
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table highlightOnHover verticalSpacing={8}>
|
<Table highlightOnHover verticalSpacing={8}>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
@@ -126,7 +148,7 @@ export default function SpaceMembersList({
|
|||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{data?.items.map((member, index) => (
|
{members.map((member, index) => (
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
@@ -154,19 +176,24 @@ export default function SpaceMembersList({
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RoleSelectMenu
|
{readOnly ? (
|
||||||
roles={spaceRoleData}
|
<Text fz="sm">
|
||||||
roleName={getSpaceRoleLabel(member.role)}
|
{t(getSpaceRoleLabel(member.role))}
|
||||||
onChange={(newRole) =>
|
</Text>
|
||||||
handleRoleChange(
|
) : (
|
||||||
member.id,
|
<RoleSelectMenu
|
||||||
member.type,
|
roles={spaceRoleData}
|
||||||
newRole,
|
roleName={getSpaceRoleLabel(member.role)}
|
||||||
member.role,
|
onChange={(newRole) =>
|
||||||
)
|
handleRoleChange(
|
||||||
}
|
member.id,
|
||||||
disabled={readOnly}
|
member.type,
|
||||||
/>
|
newRole,
|
||||||
|
member.role,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
@@ -202,16 +229,15 @@ export default function SpaceMembersList({
|
|||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Table.ScrollContainer>
|
</Table.ScrollContainer>
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
{data?.items.length > 0 && (
|
<div ref={sentinelRef} style={{ height: 1 }} />
|
||||||
<Paginate
|
|
||||||
hasPrevPage={data?.meta?.hasPrevPage}
|
{isFetchingNextPage && (
|
||||||
hasNextPage={data?.meta?.hasNextPage}
|
<Center py="xs">
|
||||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
<Loader size="xs" />
|
||||||
onPrev={goPrev}
|
</Center>
|
||||||
/>
|
)}
|
||||||
)}
|
</ScrollArea>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
keepPreviousData,
|
keepPreviousData,
|
||||||
|
useInfiniteQuery,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
@@ -10,7 +11,6 @@ import {
|
|||||||
IChangeSpaceMemberRole,
|
IChangeSpaceMemberRole,
|
||||||
IRemoveSpaceMember,
|
IRemoveSpaceMember,
|
||||||
ISpace,
|
ISpace,
|
||||||
ISpaceMember,
|
|
||||||
} from "@/features/space/types/space.types";
|
} from "@/features/space/types/space.types";
|
||||||
import {
|
import {
|
||||||
addSpaceMember,
|
addSpaceMember,
|
||||||
@@ -190,15 +190,19 @@ export function useDeleteSpaceMutation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSpaceMembersQuery(
|
export function useSpaceMembersInfiniteQuery(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
params?: QueryParams,
|
query?: string,
|
||||||
): UseQueryResult<IPagination<ISpaceMember>, Error> {
|
) {
|
||||||
return useQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: ["spaceMembers", spaceId, params],
|
queryKey: ["spaceMembers", spaceId, query],
|
||||||
queryFn: () => getSpaceMembers(spaceId, params),
|
queryFn: ({ pageParam }) =>
|
||||||
|
getSpaceMembers(spaceId, { cursor: pageParam, limit: 50, query }),
|
||||||
enabled: !!spaceId,
|
enabled: !!spaceId,
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
|
initialPageParam: undefined as string | undefined,
|
||||||
|
getNextPageParam: (lastPage) =>
|
||||||
|
lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export class SpaceMemberRepo {
|
|||||||
.leftJoin('users', 'users.id', 'spaceMembers.userId')
|
.leftJoin('users', 'users.id', 'spaceMembers.userId')
|
||||||
.leftJoin('groups', 'groups.id', 'spaceMembers.groupId')
|
.leftJoin('groups', 'groups.id', 'spaceMembers.groupId')
|
||||||
.select([
|
.select([
|
||||||
|
'spaceMembers.id as id',
|
||||||
'users.id as userId',
|
'users.id as userId',
|
||||||
'users.name as userName',
|
'users.name as userName',
|
||||||
'users.avatarUrl as userAvatarUrl',
|
'users.avatarUrl as userAvatarUrl',
|
||||||
@@ -120,6 +121,12 @@ export class SpaceMemberRepo {
|
|||||||
'isGroup',
|
'isGroup',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.select(
|
||||||
|
sql<number>`case "space_members"."role" when 'admin' then 1 when 'writer' then 2 when 'reader' then 3 else 4 end`.as(
|
||||||
|
'roleOrder',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.select(sql<string>`coalesce(users.name, groups.name)`.as('memberName'))
|
||||||
.where('spaceId', '=', spaceId);
|
.where('spaceId', '=', spaceId);
|
||||||
|
|
||||||
if (pagination.query) {
|
if (pagination.query) {
|
||||||
@@ -149,12 +156,16 @@ export class SpaceMemberRepo {
|
|||||||
cursor: pagination.cursor,
|
cursor: pagination.cursor,
|
||||||
beforeCursor: pagination.beforeCursor,
|
beforeCursor: pagination.beforeCursor,
|
||||||
fields: [
|
fields: [
|
||||||
|
{ expression: 'sub.roleOrder', direction: 'asc', key: 'roleOrder' },
|
||||||
{ expression: 'sub.isGroup', direction: 'desc', key: 'isGroup' },
|
{ 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) => ({
|
parseCursor: (cursor) => ({
|
||||||
|
roleOrder: parseInt(cursor.roleOrder, 10),
|
||||||
isGroup: parseInt(cursor.isGroup, 10),
|
isGroup: parseInt(cursor.isGroup, 10),
|
||||||
createdAt: new Date(cursor.createdAt),
|
memberName: cursor.memberName,
|
||||||
|
id: cursor.id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user