mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-23 05:01:05 +10:00
client: updates
* work on groups ui * move settings to its own page * other fixes and refactoring
This commit is contained in:
@ -0,0 +1,45 @@
|
||||
import { Button, Divider, Group, Modal, ScrollArea } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import React, { useState } from "react";
|
||||
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAddGroupMemberMutation } from "@/features/group/queries/group-query.ts";
|
||||
|
||||
export default function AddGroupMemberModal() {
|
||||
const { groupId } = useParams();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [userIds, setUserIds] = useState<string[]>([]);
|
||||
const addGroupMemberMutation = useAddGroupMemberMutation();
|
||||
|
||||
const handleMultiSelectChange = (value: string[]) => {
|
||||
setUserIds(value);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const addGroupMember = {
|
||||
groupId: groupId,
|
||||
userIds: userIds,
|
||||
};
|
||||
|
||||
await addGroupMemberMutation.mutateAsync(addGroupMember);
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={open}>Add group members</Button>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Add group members">
|
||||
<Divider size="xs" mb="xs" />
|
||||
|
||||
<MultiUserSelect onChange={handleMultiSelectChange} />
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button onClick={handleSubmit} type="submit">
|
||||
Add
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core";
|
||||
import React, { useState } from "react";
|
||||
import { useCreateGroupMutation } from "@/features/group/queries/group-query.ts";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import * as z from "zod";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
description: z.string().max(500),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export function CreateGroupForm() {
|
||||
const createGroupMutation = useCreateGroupMutation();
|
||||
const [userIds, setUserIds] = useState<string[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleMultiSelectChange = (value: string[]) => {
|
||||
setUserIds(value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (data: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
const groupData = {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
userIds: userIds,
|
||||
};
|
||||
|
||||
const createdGroup = await createGroupMutation.mutateAsync(groupData);
|
||||
navigate(`/settings/groups/${createdGroup.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box maw="500" mx="auto">
|
||||
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
id="name"
|
||||
label="Group name"
|
||||
placeholder="e.g Developers"
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
id="description"
|
||||
label="Group description"
|
||||
placeholder="e.g Group for developers"
|
||||
variant="filled"
|
||||
autosize
|
||||
minRows={2}
|
||||
maxRows={8}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
|
||||
<MultiUserSelect onChange={handleMultiSelectChange} />
|
||||
</Stack>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit">Create</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { Button, Divider, Modal } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { CreateGroupForm } from "@/features/group/components/create-group-form.tsx";
|
||||
|
||||
export default function CreateGroupModal() {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={open}>Create group</Button>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Create group">
|
||||
<Divider size="xs" mb="xs" />
|
||||
<CreateGroupForm />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
useGroupQuery,
|
||||
useUpdateGroupMutation,
|
||||
} from "@/features/group/queries/group-query.ts";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import * as z from "zod";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
description: z.string().max(500),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
interface EditGroupFormProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function EditGroupForm({ onClose }: EditGroupFormProps) {
|
||||
const updateGroupMutation = useUpdateGroupMutation();
|
||||
const { isSuccess } = updateGroupMutation;
|
||||
const { groupId } = useParams();
|
||||
const { data: group } = useGroupQuery(groupId);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
name: group?.name,
|
||||
description: group?.description,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
const groupData = {
|
||||
groupId: groupId,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
await updateGroupMutation.mutateAsync(groupData);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box maw="500" mx="auto">
|
||||
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
id="name"
|
||||
label="Group name"
|
||||
placeholder="e.g Developers"
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
id="description"
|
||||
label="Group description"
|
||||
placeholder="e.g Group for developers"
|
||||
variant="filled"
|
||||
autosize
|
||||
minRows={2}
|
||||
maxRows={8}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit">Edit</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { Divider, Modal } from "@mantine/core";
|
||||
import { EditGroupForm } from "@/features/group/components/edit-group-form.tsx";
|
||||
|
||||
interface EditGroupModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function EditGroupModal({
|
||||
opened,
|
||||
onClose,
|
||||
}: EditGroupModalProps) {
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={onClose} title="Edit group">
|
||||
<Divider size="xs" mb="xs" />
|
||||
<EditGroupForm onClose={onClose} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
import {
|
||||
useDeleteGroupMutation,
|
||||
useGroupQuery,
|
||||
} from "@/features/group/queries/group-query";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Menu, ActionIcon, Text } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { IconDots, IconTrash } from "@tabler/icons-react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import EditGroupModal from "@/features/group/components/edit-group-modal.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
export default function GroupActionMenu() {
|
||||
const { groupId } = useParams();
|
||||
const { data: group, isLoading } = useGroupQuery(groupId);
|
||||
const deleteGroupMutation = useDeleteGroupMutation();
|
||||
const navigate = useNavigate();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
const onDelete = async () => {
|
||||
await deleteGroupMutation.mutateAsync(groupId);
|
||||
navigate("/settings/groups");
|
||||
};
|
||||
|
||||
const openDeleteModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: "Delete group",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Are you sure you want to delete this group? Members will lose access
|
||||
to resources this group has access to.
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: onDelete,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{group && (
|
||||
<>
|
||||
<Menu
|
||||
shadow="xl"
|
||||
position="bottom-end"
|
||||
offset={20}
|
||||
width={200}
|
||||
withArrow
|
||||
arrowPosition="center"
|
||||
>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="light">
|
||||
<IconDots size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={open} disabled={group.isDefault}>
|
||||
Edit group
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
c="red"
|
||||
onClick={openDeleteModal}
|
||||
disabled={group.isDefault}
|
||||
leftSection={<IconTrash size={16} stroke={2} />}
|
||||
>
|
||||
Delete group
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EditGroupModal opened={opened} onClose={close} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
33
apps/client/src/features/group/components/group-details.tsx
Normal file
33
apps/client/src/features/group/components/group-details.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useGroupQuery } from "@/features/group/queries/group-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Group, Title, Text } from "@mantine/core";
|
||||
import AddGroupMemberModal from "@/features/group/components/add-group-member-modal";
|
||||
import React from "react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import EditGroupModal from "@/features/group/components/edit-group-modal.tsx";
|
||||
import GroupActionMenu from "@/features/group/components/group-action-menu.tsx";
|
||||
|
||||
export default function GroupDetails() {
|
||||
const { groupId } = useParams();
|
||||
const { data: group, isLoading } = useGroupQuery(groupId);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{group && (
|
||||
<div>
|
||||
{/* Todo: back navigation */}
|
||||
<Title order={3}>{group.name}</Title>
|
||||
<Text c="dimmed">{group.description}</Text>
|
||||
|
||||
<Group my="md" justify="flex-end">
|
||||
<AddGroupMemberModal />
|
||||
<GroupActionMenu />
|
||||
</Group>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EditGroupModal opened={opened} onClose={close} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
70
apps/client/src/features/group/components/group-list.tsx
Normal file
70
apps/client/src/features/group/components/group-list.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Table, Group, Text, Anchor } from "@mantine/core";
|
||||
import { useGetGroupsQuery } from "@/features/group/queries/group-query";
|
||||
import { IconUsersGroup } from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function GroupList() {
|
||||
const { data, isLoading } = useGetGroupsQuery();
|
||||
|
||||
return (
|
||||
<>
|
||||
{data && (
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Group</Table.Th>
|
||||
<Table.Th>Members</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((group, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Anchor
|
||||
size="sm"
|
||||
underline="never"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
color: "var(--mantine-color-text)",
|
||||
}}
|
||||
component={Link}
|
||||
to={`/settings/groups/${group.id}`}
|
||||
>
|
||||
<Group gap="sm">
|
||||
<IconUsersGroup stroke={1.5} />
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{group.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{group.description}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Anchor>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Anchor
|
||||
size="sm"
|
||||
underline="never"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
color: "var(--mantine-color-text)",
|
||||
}}
|
||||
component={Link}
|
||||
to={`/settings/groups/${group.id}`}
|
||||
>
|
||||
{group.memberCount} members
|
||||
</Anchor>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
102
apps/client/src/features/group/components/group-members.tsx
Normal file
102
apps/client/src/features/group/components/group-members.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { Group, Table, Text, Badge, Menu, ActionIcon } from "@mantine/core";
|
||||
import {
|
||||
useGroupMembersQuery,
|
||||
useRemoveGroupMemberMutation,
|
||||
} from "@/features/group/queries/group-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import React from "react";
|
||||
import { IconDots } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { UserAvatar } from "@/components/ui/user-avatar.tsx";
|
||||
|
||||
export default function GroupMembersList() {
|
||||
const { groupId } = useParams();
|
||||
const { data, isLoading } = useGroupMembersQuery(groupId);
|
||||
const removeGroupMember = useRemoveGroupMemberMutation();
|
||||
|
||||
const onRemove = async (userId: string) => {
|
||||
const memberToRemove = {
|
||||
groupId: groupId,
|
||||
userId: userId,
|
||||
};
|
||||
await removeGroupMember.mutateAsync(memberToRemove);
|
||||
};
|
||||
|
||||
const openRemoveModal = (userId: string) =>
|
||||
modals.openConfirmModal({
|
||||
title: "Remove group member",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Are you sure you want to remove this user from the group? The user
|
||||
will lose access to resources this group has access to.
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onRemove(userId),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{data && (
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>User</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{data?.items.map((user, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<UserAvatar avatarUrl={user.avatarUrl} name={user.name} />
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{user.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Badge variant="light">Active</Badge>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<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(user.id)}>
|
||||
Remove group member
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { useWorkspaceMembersQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||
import { IUser } from "@/features/user/types/user.types.ts";
|
||||
import {
|
||||
Avatar,
|
||||
Group,
|
||||
MultiSelect,
|
||||
MultiSelectProps,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
|
||||
interface MultiUserSelectProps {
|
||||
onChange: (value: string[]) => void;
|
||||
}
|
||||
|
||||
const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
option,
|
||||
}) => (
|
||||
<Group gap="sm">
|
||||
<Avatar src={option?.["avatarUrl"]} size={36} radius="xl" />
|
||||
<div>
|
||||
<Text size="sm">{option.label}</Text>
|
||||
<Text size="xs" opacity={0.5}>
|
||||
{option?.["email"]}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
|
||||
export function MultiUserSelect({ onChange }: MultiUserSelectProps) {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
||||
const { data: users, isLoading } = useWorkspaceMembersQuery({
|
||||
query: debouncedQuery,
|
||||
limit: 50,
|
||||
});
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (users) {
|
||||
const usersData = users?.items.map((user: IUser) => {
|
||||
return {
|
||||
value: user.id,
|
||||
label: user.name,
|
||||
avatarUrl: user.avatarUrl,
|
||||
email: user.email,
|
||||
};
|
||||
});
|
||||
if (usersData.length > 0) {
|
||||
setData(usersData);
|
||||
}
|
||||
}
|
||||
}, [users]);
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
data={data}
|
||||
renderOption={renderMultiSelectOption}
|
||||
hidePickedOptions
|
||||
maxDropdownHeight={300}
|
||||
label="Add group members"
|
||||
placeholder="Search for users"
|
||||
searchable
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
variant="filled"
|
||||
onChange={onChange}
|
||||
nothingFoundMessage="Nothing found..."
|
||||
maxValues={50}
|
||||
/>
|
||||
);
|
||||
}
|
||||
134
apps/client/src/features/group/queries/group-query.ts
Normal file
134
apps/client/src/features/group/queries/group-query.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import { IGroup } from "@/features/group/types/group.types";
|
||||
import {
|
||||
addGroupMember,
|
||||
createGroup,
|
||||
deleteGroup,
|
||||
getGroupById,
|
||||
getGroupMembers,
|
||||
getGroups,
|
||||
removeGroupMember,
|
||||
updateGroup,
|
||||
} from "@/features/group/services/group-service";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export function useGetGroupsQuery(): UseQueryResult<any, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["groups"],
|
||||
queryFn: () => getGroups(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["group", groupId],
|
||||
queryFn: () => getGroupById(groupId),
|
||||
enabled: !!groupId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroupMembersQuery(groupId: string) {
|
||||
return useQuery({
|
||||
queryKey: ["groupMembers", groupId],
|
||||
queryFn: () => getGroupMembers(groupId),
|
||||
enabled: !!groupId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateGroupMutation() {
|
||||
return useMutation<IGroup, Error, Partial<IGroup>>({
|
||||
mutationFn: (data) => createGroup(data),
|
||||
onSuccess: () => {
|
||||
notifications.show({ message: "Group created successfully" });
|
||||
},
|
||||
onError: () => {
|
||||
notifications.show({ message: "Failed to create group", color: "red" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateGroupMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<IGroup, Error, Partial<IGroup>>({
|
||||
mutationFn: (data) => updateGroup(data),
|
||||
onSuccess: (data, variables) => {
|
||||
notifications.show({ message: "Group updated successfully" });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["group", variables.groupId],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
notifications.show({ message: errorMessage, color: "red" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteGroupMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (groupId: string) => deleteGroup({ groupId }),
|
||||
onSuccess: (data, variables) => {
|
||||
notifications.show({ message: "Group deleted successfully" });
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["groups"],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
notifications.show({ message: errorMessage, color: "red" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddGroupMemberMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, Error, { groupId: string; userIds: string[] }>({
|
||||
mutationFn: (data) => addGroupMember(data),
|
||||
onSuccess: (data, variables) => {
|
||||
notifications.show({ message: "Added successfully" });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["groupMembers", variables.groupId],
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
notifications.show({
|
||||
message: "Failed to add group members",
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveGroupMemberMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
void,
|
||||
Error,
|
||||
{
|
||||
groupId: string;
|
||||
userId: string;
|
||||
}
|
||||
>({
|
||||
mutationFn: (data) => removeGroupMember(data),
|
||||
onSuccess: (data, variables) => {
|
||||
notifications.show({ message: "Removed successfully" });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["groupMembers", variables.groupId],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
notifications.show({ message: errorMessage, color: "red" });
|
||||
},
|
||||
});
|
||||
}
|
||||
46
apps/client/src/features/group/services/group-service.ts
Normal file
46
apps/client/src/features/group/services/group-service.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import api from "@/lib/api-client";
|
||||
import { IGroup } from "@/features/group/types/group.types";
|
||||
|
||||
export async function getGroups(): Promise<any> {
|
||||
// TODO: returns paginated. Fix type
|
||||
const req = await api.post<any>("/groups");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getGroupById(groupId: string): Promise<IGroup> {
|
||||
const req = await api.post<IGroup>("/groups/info", { groupId });
|
||||
return req.data as IGroup;
|
||||
}
|
||||
|
||||
export async function getGroupMembers(groupId: string) {
|
||||
const req = await api.post("/groups/members", { groupId });
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function createGroup(data: Partial<IGroup>): Promise<IGroup> {
|
||||
const req = await api.post<IGroup>("/groups/create", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function updateGroup(data: Partial<IGroup>): Promise<IGroup> {
|
||||
const req = await api.post<IGroup>("/groups/update", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function deleteGroup(data: { groupId: string }): Promise<void> {
|
||||
await api.post("/groups/delete", data);
|
||||
}
|
||||
|
||||
export async function addGroupMember(data: {
|
||||
groupId: string;
|
||||
userIds: string[];
|
||||
}): Promise<void> {
|
||||
await api.post<IGroup>("/groups/members/add", data);
|
||||
}
|
||||
|
||||
export async function removeGroupMember(data: {
|
||||
groupId: string;
|
||||
userId: string;
|
||||
}): Promise<void> {
|
||||
await api.post<IGroup>("/groups/members/remove", data);
|
||||
}
|
||||
12
apps/client/src/features/group/types/group.types.ts
Normal file
12
apps/client/src/features/group/types/group.types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface IGroup {
|
||||
groupId: string;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
isDefault: boolean;
|
||||
creatorId: string | null;
|
||||
workspaceId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
memberCount: number;
|
||||
}
|
||||
Reference in New Issue
Block a user