client: updates

* work on groups ui
* move settings to its own page
* other fixes and refactoring
This commit is contained in:
Philipinho
2024-04-04 22:19:15 +01:00
parent cab5e67055
commit 1412f1d982
64 changed files with 1770 additions and 474 deletions

View File

@ -0,0 +1,6 @@
.authBackground {
position: relative;
min-height: 100vh;
background-size: cover;
background-image: url(https://images.unsplash.com/photo-1701010063921-5f3255259e6d?q=80&w=3024&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D);
}

View File

@ -15,6 +15,7 @@ import {
PasswordInput,
} from '@mantine/core';
import { Link } from 'react-router-dom';
import classes from './auth.module.css';
const formSchema = z.object({
email: z
@ -40,17 +41,11 @@ export function LoginForm() {
return (
<Container size={420} my={40}>
<Title ta="center" fw={800}>
Login
</Title>
<Text c="dimmed" size="sm" ta="center" mt={5}>
Don't have an account yet?{' '}
<Anchor size="sm" component={Link} to="/signup">
Create account
</Anchor>
</Text>
<Paper shadow="md" p="lg" radius="md" mt={200}>
<Title ta="center" fw={800}>
Login
</Title>
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
<form onSubmit={form.onSubmit(onSubmit)}>
<TextInput
id="email"
@ -72,6 +67,14 @@ export function LoginForm() {
Sign In
</Button>
</form>
<Text c="dimmed" size="sm" ta="center" mt="sm">
Don't have an account yet?{' '}
<Anchor size="sm" component={Link} to="/signup">
Create account
</Anchor>
</Text>
</Paper>
</Container>
);

View File

@ -50,7 +50,7 @@ export function SignUpForm() {
</Anchor>
</Text>
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
<Paper shadow="md" p={30} mt={30} radius="md">
<form onSubmit={form.onSubmit(onSubmit)}>
<TextInput
id="email"

View File

@ -78,7 +78,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
<Stack gap={2}>
<Group>
<Avatar size="sm" color="blue">{currentUser.user.name.charAt(0)}</Avatar>
<Avatar size="sm" c="blue">{currentUser.user.name.charAt(0)}</Avatar>
<div style={{ flex: 1 }}>
<Group justify="space-between" wrap="nowrap">
<Text size="sm" fw={500} lineClamp={1}>{currentUser.user.name}</Text>

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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} />
</>
);
}

View 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} />
</>
);
}

View 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>
)}
</>
);
}

View 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>
)}
</>
);
}

View File

@ -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}
/>
);
}

View 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" });
},
});
}

View 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);
}

View 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;
}

View File

@ -22,7 +22,7 @@ export default function HomeTabs() {
<Tabs.Panel value="recent">
<RecentChanges />
{/* <RecentChanges /> */}
</Tabs.Panel>

View File

@ -24,7 +24,7 @@ function HistoryItem({ historyItem, onSelect, isActive }: HistoryItemProps) {
<div style={{ flex: 1 }}>
<Group gap={4} wrap="nowrap">
<UserAvatar color="blue" size="sm" avatarUrl={historyItem.lastUpdatedBy.avatarUrl}
<UserAvatar c="blue" size="sm" avatarUrl={historyItem.lastUpdatedBy.avatarUrl}
name={historyItem.lastUpdatedBy.name} />
<Text size="sm" c="dimmed" lineClamp={1}>
{historyItem.lastUpdatedBy.name}

View File

@ -7,7 +7,7 @@ export async function createPage(data: Partial<IPage>): Promise<IPage> {
}
export async function getPageById(id: string): Promise<IPage> {
const req = await api.post<IPage>('/pages/details', { id });
const req = await api.post<IPage>('/pages/info', { id });
return req.data as IPage;
}

View File

@ -1,4 +1,4 @@
import { NodeApi, NodeRendererProps, Tree, TreeApi } from 'react-arborist';
import { NodeApi, NodeRendererProps, Tree, TreeApi } from "react-arborist";
import {
IconArrowsLeftRight,
IconChevronDown,
@ -11,24 +11,27 @@ import {
IconPlus,
IconStar,
IconTrash,
} from '@tabler/icons-react';
} from "@tabler/icons-react";
import React, { useEffect, useRef } from 'react';
import clsx from 'clsx';
import React, { useEffect, useRef } from "react";
import clsx from "clsx";
import classes from './styles/tree.module.css';
import { ActionIcon, Menu, rem } from '@mantine/core';
import { useAtom } from 'jotai';
import { FillFlexParent } from './components/fill-flex-parent';
import { TreeNode } from './types';
import { treeApiAtom } from './atoms/tree-api-atom';
import { usePersistence } from '@/features/page/tree/hooks/use-persistence';
import useWorkspacePageOrder from '@/features/page/tree/hooks/use-workspace-page-order';
import { useNavigate, useParams } from 'react-router-dom';
import { convertToTree, updateTreeNodeIcon } from '@/features/page/tree/utils';
import { useGetPagesQuery, useUpdatePageMutation } from '@/features/page/queries/page-query';
import EmojiPicker from '@/components/emoji-picker';
import { treeDataAtom } from '@/features/page/tree/atoms/tree-data-atom';
import classes from "./styles/tree.module.css";
import { ActionIcon, Menu, rem } from "@mantine/core";
import { useAtom } from "jotai";
import { FillFlexParent } from "./components/fill-flex-parent";
import { TreeNode } from "./types";
import { treeApiAtom } from "./atoms/tree-api-atom";
import { usePersistence } from "@/features/page/tree/hooks/use-persistence";
import useWorkspacePageOrder from "@/features/page/tree/hooks/use-workspace-page-order";
import { useNavigate, useParams } from "react-router-dom";
import { convertToTree, updateTreeNodeIcon } from "@/features/page/tree/utils";
import {
useGetPagesQuery,
useUpdatePageMutation,
} from "@/features/page/queries/page-query";
import EmojiPicker from "@/components/ui/emoji-picker.tsx";
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
export default function PageTree() {
const { data, setData, controllers } = usePersistence<TreeApi<TreeNode>>();
@ -46,7 +49,7 @@ export default function PageTree() {
setData(treeData);
}
} catch (err) {
console.error('Error fetching tree data: ', err);
console.error("Error fetching tree data: ", err);
}
}
};
@ -58,7 +61,7 @@ export default function PageTree() {
useEffect(() => {
setTimeout(() => {
tree?.select(pageId);
tree?.scrollTo(pageId, 'center');
tree?.scrollTo(pageId, "center");
}, 200);
}, [tree, pageId]);
@ -106,7 +109,7 @@ function Node({ node, style, dragHandle }: NodeRendererProps<any>) {
const handleEmojiIconClick = (e) => {
e.preventDefault();
e.stopPropagation();
}
};
const handleEmojiSelect = (emoji) => {
handleUpdateNodeIcon(node.id, emoji.native);
@ -134,19 +137,25 @@ function Node({ node, style, dragHandle }: NodeRendererProps<any>) {
>
<PageArrow node={node} />
<div onClick={handleEmojiIconClick} style={{ marginRight: '4px' }}>
<EmojiPicker onEmojiSelect={handleEmojiSelect} icon={
node.data.icon ? node.data.icon :
<IconFileDescription size="18px" />
} removeEmojiAction={handleRemoveEmoji}/>
<div onClick={handleEmojiIconClick} style={{ marginRight: "4px" }}>
<EmojiPicker
onEmojiSelect={handleEmojiSelect}
icon={
node.data.icon ? (
node.data.icon
) : (
<IconFileDescription size="18px" />
)
}
removeEmojiAction={handleRemoveEmoji}
/>
</div>
<span className={classes.text}>
{node.isEditing ? (
<Input node={node} />
) : (
node.data.name || 'untitled'
node.data.name || "untitled"
)}
</span>
@ -163,15 +172,19 @@ function CreateNode({ node }: { node: NodeApi<TreeNode> }) {
const [tree] = useAtom(treeApiAtom);
function handleCreate() {
tree?.create({ type: 'internal', parentId: node.id, index: 0 });
tree?.create({ type: "internal", parentId: node.id, index: 0 });
}
return (
<ActionIcon variant="transparent" color="gray" onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCreate();
}}>
<ActionIcon
variant="transparent"
c="gray"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCreate();
}}
>
<IconPlus style={{ width: rem(20), height: rem(20) }} stroke={2} />
</ActionIcon>
);
@ -187,10 +200,14 @@ function NodeMenu({ node }: { node: NodeApi<TreeNode> }) {
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<ActionIcon variant="transparent" color="gray" onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}>
<ActionIcon
variant="transparent"
c="gray"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<IconDotsVertical
style={{ width: rem(20), height: rem(20) }}
stroke={2}
@ -240,7 +257,7 @@ function NodeMenu({ node }: { node: NodeApi<TreeNode> }) {
Archive
</Menu.Item>
<Menu.Item
color="red"
c="red"
leftSection={
<IconTrash style={{ width: rem(14), height: rem(14) }} />
}
@ -255,13 +272,16 @@ function NodeMenu({ node }: { node: NodeApi<TreeNode> }) {
function PageArrow({ node }: { node: NodeApi<TreeNode> }) {
return (
<ActionIcon size={20} variant="subtle" color="gray"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
node.toggle();
}}>
<ActionIcon
size={20}
variant="subtle"
c="gray"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
node.toggle();
}}
>
{node.isInternal ? (
node.children && node.children.length > 0 ? (
node.isOpen ? (
@ -270,7 +290,7 @@ function PageArrow({ node }: { node: NodeApi<TreeNode> }) {
<IconChevronRight stroke={2} size={18} />
)
) : (
<IconChevronRight size={18} style={{ visibility: 'hidden' }} />
<IconChevronRight size={18} style={{ visibility: "hidden" }} />
)
) : null}
</ActionIcon>
@ -278,7 +298,6 @@ function PageArrow({ node }: { node: NodeApi<TreeNode> }) {
}
function Input({ node }: { node: NodeApi<TreeNode> }) {
return (
<input
autoFocus
@ -289,10 +308,9 @@ function Input({ node }: { node: NodeApi<TreeNode> }) {
onFocus={(e) => e.currentTarget.select()}
onBlur={() => node.reset()}
onKeyDown={(e) => {
if (e.key === 'Escape') node.reset();
if (e.key === 'Enter') node.submit(e.currentTarget.value);
if (e.key === "Escape") node.reset();
if (e.key === "Enter") node.submit(e.currentTarget.value);
}}
/>
);
}

View File

@ -1,3 +0,0 @@
import { atom } from "jotai";
export const settingsModalAtom = atom<boolean>(false);

View File

@ -1,76 +0,0 @@
.sidebar {
max-height: rem(700px);
width: rem(180px);
padding: var(--mantine-spacing-sm);
display: flex;
flex-direction: column;
border-right: rem(1px) solid
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.sidebarFlex {
display: flex;
}
.sidebarMain {
flex: 1;
}
.sidebarRightSection {
flex: 1;
padding: rem(16px) rem(40px);
}
.sidebarItemHeader {
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
}
.sidebarItem {
cursor: pointer;
display: flex;
align-items: center;
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
border-radius: var(--mantine-radius-sm);
font-weight: 500;
user-select: none;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-6)
);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
.sidebarItemIcon {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
}
&[data-active] {
&,
& :hover {
background-color: var(--mantine-color-blue-light);
color: var(--mantine-color-blue-light-color);
.sidebarItemIcon {
color: var(--mantine-color-blue-light-color);
}
}
}
}
.sidebarItemIcon {
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
margin-right: var(--mantine-spacing-sm);
width: rem(20px);
height: rem(20px);
}

View File

@ -1,30 +0,0 @@
import { Modal, Text } from '@mantine/core';
import React from 'react';
import SettingsSidebar from '@/features/settings/modal/settings-sidebar';
import { useAtom } from 'jotai';
import { settingsModalAtom } from '@/features/settings/modal/atoms/settings-modal-atom';
export default function SettingsModal() {
const [isModalOpen, setModalOpen] = useAtom(settingsModalAtom);
return (
<>
<Modal.Root size={1000} opened={isModalOpen} onClose={() => setModalOpen(false)}>
<Modal.Overlay />
<Modal.Content style={{ overflow: 'hidden' }}>
<Modal.Header>
<Modal.Title>
<Text size="md" fw={500}>Settings</Text>
</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<SettingsSidebar />
</Modal.Body>
</Modal.Content>
</Modal.Root>
</>
);
}

View File

@ -1,101 +0,0 @@
import React, { useState } from 'react';
import classes from '@/features/settings/modal/modal.module.css';
import { IconBell, IconFingerprint, IconReceipt, IconSettingsCog, IconUser, IconUsers } from '@tabler/icons-react';
import { Loader, ScrollArea, Text } from '@mantine/core';
const AccountSettings = React.lazy(() => import('@/features/settings/account/settings/account-settings'));
const WorkspaceSettings = React.lazy(() => import('@/features/settings/workspace/settings/workspace-settings'));
const WorkspaceMembers = React.lazy(() => import('@/features/settings/workspace/members/workspace-members'));
interface DataItem {
label: string;
icon: React.ElementType;
}
interface DataGroup {
heading: string;
items: DataItem[];
}
const groupedData: DataGroup[] = [
{
heading: 'Account',
items: [
{ label: 'Account', icon: IconUser },
{ label: 'Notifications', icon: IconBell },
],
},
{
heading: 'Workspace',
items: [
{ label: 'General', icon: IconSettingsCog },
{ label: 'Members', icon: IconUsers },
{ label: 'Security', icon: IconFingerprint },
{ label: 'Billing', icon: IconReceipt },
],
},
];
export default function SettingsSidebar() {
const [active, setActive] = useState('Account');
const menu = groupedData.map((group) => (
<div key={group.heading}>
<Text c="dimmed" className={classes.sidebarItemHeader}>{group.heading}</Text>
{group.items.map((item) => (
<div
className={classes.sidebarItem}
data-active={item.label === active || undefined}
key={item.label}
onClick={(event) => {
event.preventDefault();
setActive(item.label);
}}
>
<item.icon className={classes.sidebarItemIcon} stroke={1.5} />
<span>{item.label}</span>
</div>
))}
</div>
));
let ActiveComponent;
switch (active) {
case 'Account':
ActiveComponent = AccountSettings;
break;
case 'General':
ActiveComponent = WorkspaceSettings;
break;
case 'Members':
ActiveComponent = WorkspaceMembers;
break;
default:
ActiveComponent = null;
}
return (
<div className={classes.sidebarFlex}>
<nav className={classes.sidebar}>
<div className={classes.sidebarMain}>
{menu}
</div>
</nav>
<ScrollArea h="650" w="100%" scrollbarSize={4}>
<div className={classes.sidebarRightSection}>
<React.Suspense fallback={<Loader size="sm" color="gray" />}>
{ActiveComponent && <ActiveComponent />}
</React.Suspense>
</div>
</ScrollArea>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { atomWithStorage } from "jotai/utils";
import { ICurrentUserResponse } from "@/features/user/types/user.types";
import { ICurrentUser } from "@/features/user/types/user.types";
export const currentUserAtom = atomWithStorage<ICurrentUserResponse | null>("currentUser", null);
export const currentUserAtom = atomWithStorage<ICurrentUser | null>("currentUser", null);

View File

@ -0,0 +1,60 @@
import { focusAtom } from "jotai-optics";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useState } from "react";
import { useAtom } from "jotai";
import { UserAvatar } from "@/components/ui/user-avatar.tsx";
import { FileButton, Button, Text, Popover, Tooltip } from "@mantine/core";
import { uploadAvatar } from "@/features/user/services/user-service.ts";
const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop("user"));
export default function AccountAvatar() {
const [isLoading, setIsLoading] = useState(false);
const [currentUser] = useAtom(currentUserAtom);
const [, setUser] = useAtom(userAtom);
const [file, setFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const handleFileChange = async (selectedFile: File) => {
if (!selectedFile) {
return;
}
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
setFile(selectedFile);
setPreviewUrl(URL.createObjectURL(selectedFile));
try {
setIsLoading(true);
const upload = await uploadAvatar(selectedFile);
console.log(upload);
} catch (err) {
console.log(err);
} finally {
setIsLoading(false);
}
};
return (
<>
<FileButton onChange={handleFileChange} accept="image/png,image/jpeg">
{(props) => (
<Tooltip label="Change photo" position="bottom">
<UserAvatar
{...props}
component="button"
radius="xl"
size="60px"
avatarUrl={previewUrl || currentUser.user.avatarUrl}
name={currentUser.user.name}
style={{ cursor: "pointer" }}
/>
</Tooltip>
)}
</FileButton>
</>
);
}

View File

@ -0,0 +1,76 @@
import { useAtom } from "jotai";
import { focusAtom } from "jotai-optics";
import * as z from "zod";
import { useForm, zodResolver } from "@mantine/form";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
import { updateUser } from "@/features/user/services/user-service.ts";
import { IUser } from "@/features/user/types/user.types.ts";
import { useState } from "react";
import { TextInput, Button } from "@mantine/core";
import { notifications } from "@mantine/notifications";
const formSchema = z.object({
name: z.string().min(2).max(40).nonempty("Your name cannot be blank"),
});
type FormValues = z.infer<typeof formSchema>;
const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop("user"));
export default function AccountNameForm() {
const [isLoading, setIsLoading] = useState(false);
const [currentUser] = useAtom(currentUserAtom);
const [, setUser] = useAtom(userAtom);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
initialValues: {
name: currentUser?.user.name,
},
});
async function handleSubmit(data: Partial<IUser>) {
setIsLoading(true);
try {
const updatedUser = await updateUser(data);
setUser(updatedUser);
notifications.show({
message: "Updated successfully",
});
} catch (err) {
console.log(err);
notifications.show({
message: "Failed to update data",
color: "red",
});
}
setIsLoading(false);
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
id="name"
label="Name"
placeholder="Your name"
variant="filled"
{...form.getInputProps("name")}
/>
<Button type="submit" mt="sm" disabled={isLoading} loading={isLoading}>
Save
</Button>
</form>
);
}
/*
<div className={classes.controls}>
<TextInput
placeholder="Your email"
classNames={{ input: classes.input, root: classes.inputWrapper }}
/>
<Button className={classes.control}>Subscribe</Button>
</div>
*/

View File

@ -0,0 +1,94 @@
import {
Modal,
TextInput,
Button,
Text,
Group,
PasswordInput,
} from "@mantine/core";
import * as z from "zod";
import { useState } from "react";
import { useAtom } from "jotai";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useDisclosure } from "@mantine/hooks";
import * as React from "react";
import { useForm, zodResolver } from "@mantine/form";
export default function ChangeEmail() {
const [currentUser] = useAtom(currentUserAtom);
const [opened, { open, close }] = useDisclosure(false);
return (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Text size="md">Email</Text>
<Text size="sm" c="dimmed">
{currentUser?.user.email}
</Text>
</div>
<Button onClick={open} variant="default">
Change email
</Button>
<Modal opened={opened} onClose={close} title="Change email" centered>
<Text mb="md">
To change your email, you have to enter your password and new email.
</Text>
<ChangePasswordForm />
</Modal>
</Group>
);
}
const formSchema = z.object({
email: z.string({ required_error: "New email is required" }).email(),
password: z
.string({ required_error: "your current password is required" })
.min(8),
});
type FormValues = z.infer<typeof formSchema>;
function ChangePasswordForm() {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
initialValues: {
password: "",
email: "",
},
});
function handleSubmit(data: FormValues) {
setIsLoading(true);
console.log(data);
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<PasswordInput
label="Password"
placeholder="Enter your password"
variant="filled"
mb="md"
{...form.getInputProps("password")}
/>
<TextInput
id="email"
label="Email"
description="Enter your new preferred email"
placeholder="New email"
variant="filled"
mb="md"
{...form.getInputProps("email")}
/>
<Button type="submit" disabled={isLoading} loading={isLoading}>
Change email
</Button>
</form>
);
}

View File

@ -0,0 +1,84 @@
import { Button, Group, Text, Modal, PasswordInput } from '@mantine/core';
import * as z from 'zod';
import { useState } from 'react';
import { useDisclosure } from '@mantine/hooks';
import * as React from 'react';
import { useForm, zodResolver } from '@mantine/form';
export default function ChangePassword() {
const [opened, { open, close }] = useDisclosure(false);
return (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Text size="md">Password</Text>
<Text size="sm" c="dimmed">
You can change your password here.
</Text>
</div>
<Button onClick={open} variant="default">Change password</Button>
<Modal opened={opened} onClose={close} title="Change password" centered>
<Text mb="md">Your password must be a minimum of 8 characters.</Text>
<ChangePasswordForm />
</Modal>
</Group>
);
}
const formSchema = z.object({
current: z.string({ required_error: 'your current password is required' }).min(1),
password: z.string({ required_error: 'New password is required' }).min(8),
confirm_password: z.string({ required_error: 'Password confirmation is required' }).min(8),
}).refine(data => data.password === data.confirm_password, {
message: 'Your new password and confirmation does not match.',
path: ['confirm_password'],
});
type FormValues = z.infer<typeof formSchema>
function ChangePasswordForm() {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
initialValues: {
current: '',
password: '',
confirm_password: '',
},
});
function handleSubmit(data: FormValues) {
setIsLoading(true);
console.log(data);
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<PasswordInput
label="Current password"
name="current"
placeholder="Enter your current password"
variant="filled"
mb="md"
{...form.getInputProps('current')}
/>
<PasswordInput
label="New password"
placeholder="Enter your new password"
variant="filled"
mb="md"
{...form.getInputProps('password')}
/>
<Button type="submit" disabled={isLoading} loading={isLoading}>
Change password
</Button>
</form>
);
}

View File

@ -1,8 +1,8 @@
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { getUserInfo } from "@/features/user/services/user-service";
import { ICurrentUserResponse } from "@/features/user/types/user.types";
import { ICurrentUser } from "@/features/user/types/user.types";
export default function useCurrentUser(): UseQueryResult<ICurrentUserResponse> {
export default function useCurrentUser(): UseQueryResult<ICurrentUser> {
return useQuery({
queryKey: ["currentUser"],
queryFn: async () => {

View File

@ -1,19 +1,18 @@
import api from '@/lib/api-client';
import { ICurrentUserResponse, IUser } from '@/features/user/types/user.types';
import { ICurrentUser, IUser } from '@/features/user/types/user.types';
export async function getMe(): Promise<IUser> {
const req = await api.get<IUser>('/user/me');
const req = await api.post<IUser>('/users/me');
return req.data as IUser;
}
export async function getUserInfo(): Promise<ICurrentUserResponse> {
const req = await api.get<ICurrentUserResponse>('/user/info');
return req.data as ICurrentUserResponse;
export async function getUserInfo(): Promise<ICurrentUser> {
const req = await api.post<ICurrentUser>('/users/info');
return req.data as ICurrentUser;
}
export async function updateUser(data: Partial<IUser>): Promise<IUser> {
const req = await api.post<IUser>('/user/update', data);
const req = await api.post<IUser>('/users/update', data);
return req.data as IUser;
}

View File

@ -9,13 +9,13 @@ export interface IUser {
timezone: string;
settings: any;
lastLoginAt: string;
lastLoginIp: string;
createdAt: Date;
updatedAt: Date;
workspaceRole?: string;
role: string;
workspaceId: string;
}
export interface ICurrentUserResponse {
export interface ICurrentUser {
user: IUser,
workspace: IWorkspace
}

View File

@ -1,5 +1,5 @@
import { Group, Box, Button, TagsInput, Space, Select } from "@mantine/core";
import WorkspaceInviteSection from "@/features/settings/workspace/members/components/workspace-invite-section";
import WorkspaceInviteSection from "@/features/workspace/components/members/components/workspace-invite-section.tsx";
import React from "react";
enum UserRole {

View File

@ -1,27 +1,27 @@
import { WorkspaceInviteForm } from '@/features/settings/workspace/members/components/workspace-invite-form';
import { Button, Divider, Modal, ScrollArea } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { WorkspaceInviteForm } from "@/features/workspace/components/members/components/workspace-invite-form.tsx";
import { Button, Divider, Modal, ScrollArea } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
export default function WorkspaceInviteModal() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Button onClick={open}>
Invite members
</Button>
<Button onClick={open}>Invite members</Button>
<Modal size="600" opened={opened} onClose={close} title="Invite new members" centered>
<Divider size="xs" mb="xs"/>
<Modal
size="600"
opened={opened}
onClose={close}
title="Invite new members"
centered
>
<Divider size="xs" mb="xs" />
<ScrollArea h="80%">
<WorkspaceInviteForm />
</ScrollArea>
</Modal>
</>
);
}

View File

@ -1,5 +1,5 @@
import { useAtom } from "jotai";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useEffect, useState } from "react";
import { Button, CopyButton, Group, Text, TextInput } from "@mantine/core";

View File

@ -1,5 +1,5 @@
import { Group, Table, Avatar, Text, Badge } from "@mantine/core";
import { useWorkspaceMembersQuery } from "@/features/workspace/queries/workspace-query";
import { useWorkspaceMembersQuery } from "@/features/workspace/queries/workspace-query.ts";
import { UserAvatar } from "@/components/ui/user-avatar.tsx";
import React from "react";

View File

@ -1,10 +1,10 @@
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useAtom } from "jotai";
import * as z from "zod";
import { useState } from "react";
import { focusAtom } from "jotai-optics";
import { updateWorkspace } from "@/features/workspace/services/workspace-service";
import { IWorkspace } from "@/features/workspace/types/workspace.types";
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
import { TextInput, Button } from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import { notifications } from "@mantine/notifications";

View File

@ -0,0 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { getWorkspaceMembers } from "@/features/workspace/services/workspace-service";
import { QueryParams } from "@/lib/types.ts";
export function useWorkspaceMembersQuery(params?: QueryParams) {
return useQuery({
queryKey: ["workspaceMembers", params],
queryFn: () => getWorkspaceMembers(params),
});
}

View File

@ -1,19 +1,21 @@
import api from '@/lib/api-client';
import { ICurrentUserResponse, IUser } from '@/features/user/types/user.types';
import { IWorkspace } from '../types/workspace.types';
import api from "@/lib/api-client";
import { IUser } from "@/features/user/types/user.types";
import { IWorkspace } from "../types/workspace.types";
import { QueryParams } from "@/lib/types.ts";
export async function getWorkspace(): Promise<IWorkspace> {
const req = await api.get<IWorkspace>('/workspace');
const req = await api.post<IWorkspace>("/workspace/info");
return req.data as IWorkspace;
}
export async function getWorkspaceUsers(): Promise<IUser[]> {
const req = await api.get<IUser[]>('/workspace/members');
return req.data as IUser[];
// Todo: fix all paginated types
export async function getWorkspaceMembers(params?: QueryParams): Promise<any> {
const req = await api.post<any>("/workspace/members", params);
return req.data;
}
export async function updateWorkspace(data: Partial<IWorkspace>) {
const req = await api.post<IWorkspace>('/workspace/update', data);
const req = await api.post<IWorkspace>("/workspace/update", data);
return req.data as IWorkspace;
}

View File

@ -8,8 +8,6 @@ export interface IWorkspace {
enableInvite: boolean;
inviteCode: string;
settings: any;
creatorId: string;
pageOrder?:[]
createdAt: Date;
updatedAt: Date;
}