mirror of
https://github.com/docmost/docmost.git
synced 2025-11-20 14:01:08 +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:
@ -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() {
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
{data && (
|
||||
<>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t("Email")}</Table.Th>
|
||||
<Table.Th>{t("Role")}</Table.Th>
|
||||
<Table.Th>{t("Date")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t("Email")}</Table.Th>
|
||||
<Table.Th>{t("Role")}</Table.Th>
|
||||
<Table.Th>{t("Date")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((invitation, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<Avatar name={invitation.email} color="initials"/>
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{invitation.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Tbody>
|
||||
{data?.items.map((invitation, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Avatar name={invitation.email} color="initials" />
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{invitation.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</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>
|
||||
{isAdmin && (
|
||||
<InviteActionMenu invitationId={invitation.id}/>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
</>
|
||||
<Table.Td>
|
||||
{isAdmin && <InviteActionMenu invitationId={invitation.id} />}
|
||||
</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,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 && (
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table verticalSpacing="sm">
|
||||
<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>
|
||||
<SearchInput
|
||||
onSearch={(debouncedSearch) => {
|
||||
setSearch(debouncedSearch);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<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>
|
||||
{data?.items.map((user, index) => (
|
||||
<Table.Tbody>
|
||||
{data?.items.length > 0 ? (
|
||||
data?.items.map((user, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name}/>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<CustomAvatar
|
||||
avatarUrl={user.avatarUrl}
|
||||
name={user.name}
|
||||
/>
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
<Text fz="sm" fw={500} lineClamp={1}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
@ -84,10 +102,21 @@ export default function WorkspaceMembersTable() {
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
))
|
||||
) : (
|
||||
<NoTableResults colSpan={3} />
|
||||
)}
|
||||
</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 {
|
||||
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<IWorkspace, Error> {
|
||||
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({
|
||||
queryKey: ["workspaceMembers", params],
|
||||
queryFn: () => getWorkspaceMembers(params),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
}
|
||||
|
||||
@ -53,7 +58,6 @@ export function useChangeMemberRoleMutation() {
|
||||
return useMutation<any, Error, any>({
|
||||
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"],
|
||||
});
|
||||
|
||||
@ -18,7 +18,6 @@ export async function getWorkspacePublicData(): Promise<IWorkspace> {
|
||||
return req.data;
|
||||
}
|
||||
|
||||
// Todo: fix all paginated types
|
||||
export async function getWorkspaceMembers(
|
||||
params?: QueryParams,
|
||||
): Promise<IPagination<IUser>> {
|
||||
|
||||
Reference in New Issue
Block a user