mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-10 04:22:00 +10:00
fix: bug fixes (#397)
* Add more html page titles * Make tables responsive * fix react query keys * Add tooltip to sidebar toggle * fix: trim inputs * fix inputs
This commit is contained in:
@ -4,25 +4,25 @@ import {
|
|||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
Badge,
|
Badge,
|
||||||
Table,
|
Table,
|
||||||
ScrollArea,
|
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Link } from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx';
|
import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx';
|
||||||
import { buildPageUrl } from '@/features/page/page.utils.ts';
|
import {buildPageUrl} from '@/features/page/page.utils.ts';
|
||||||
import { formattedDate } from '@/lib/time.ts';
|
import {formattedDate} from '@/lib/time.ts';
|
||||||
import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts';
|
import {useRecentChangesQuery} from '@/features/page/queries/page-query.ts';
|
||||||
import { IconFileDescription } from '@tabler/icons-react';
|
import {IconFileDescription} from '@tabler/icons-react';
|
||||||
import { getSpaceUrl } from '@/lib/config.ts';
|
import {getSpaceUrl} from '@/lib/config.ts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
spaceId?: string;
|
spaceId?: string;
|
||||||
}
|
}
|
||||||
export default function RecentChanges({ spaceId }: Props) {
|
|
||||||
const { data: pages, isLoading, isError } = useRecentChangesQuery(spaceId);
|
export default function RecentChanges({spaceId}: Props) {
|
||||||
|
const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <PageListSkeleton />;
|
return <PageListSkeleton/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
@ -30,7 +30,7 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return pages && pages.items.length > 0 ? (
|
return pages && pages.items.length > 0 ? (
|
||||||
<ScrollArea>
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table highlightOnHover verticalSpacing="sm">
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{pages.items.map((page) => (
|
{pages.items.map((page) => (
|
||||||
@ -43,7 +43,7 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
<Group wrap="nowrap">
|
<Group wrap="nowrap">
|
||||||
{page.icon || (
|
{page.icon || (
|
||||||
<ActionIcon variant='transparent' color='gray' size={18}>
|
<ActionIcon variant='transparent' color='gray' size={18}>
|
||||||
<IconFileDescription size={18} />
|
<IconFileDescription size={18}/>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -60,14 +60,14 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
variant="light"
|
variant="light"
|
||||||
component={Link}
|
component={Link}
|
||||||
to={getSpaceUrl(page?.space.slug)}
|
to={getSpaceUrl(page?.space.slug)}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{cursor: 'pointer'}}
|
||||||
>
|
>
|
||||||
{page?.space.name}
|
{page?.space.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
)}
|
)}
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text c="dimmed" size="xs" fw={500}>
|
<Text c="dimmed" style={{whiteSpace: 'nowrap'}} size="xs" fw={500}>
|
||||||
{formattedDate(page.updatedAt)}
|
{formattedDate(page.updatedAt)}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
@ -75,7 +75,7 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</ScrollArea>
|
</Table.ScrollContainer>
|
||||||
) : (
|
) : (
|
||||||
<Text size="md" ta="center">
|
<Text size="md" ta="center">
|
||||||
No pages yet
|
No pages yet
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import { Group, Text } from "@mantine/core";
|
import {Group, Text, Tooltip} from "@mantine/core";
|
||||||
import classes from "./app-header.module.css";
|
import classes from "./app-header.module.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import TopMenu from "@/components/layouts/global/top-menu.tsx";
|
import TopMenu from "@/components/layouts/global/top-menu.tsx";
|
||||||
import { Link } from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import APP_ROUTE from "@/lib/app-route.ts";
|
import APP_ROUTE from "@/lib/app-route.ts";
|
||||||
import { useAtom } from "jotai/index";
|
import {useAtom} from "jotai/index";
|
||||||
import {
|
import {
|
||||||
desktopSidebarAtom,
|
desktopSidebarAtom,
|
||||||
mobileSidebarAtom,
|
mobileSidebarAtom,
|
||||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
import {useToggleSidebar} from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||||
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
||||||
|
|
||||||
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
|
const links = [{link: APP_ROUTE.HOME, label: "Home"}];
|
||||||
|
|
||||||
export function AppHeader() {
|
export function AppHeader() {
|
||||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||||
@ -35,28 +35,33 @@ export function AppHeader() {
|
|||||||
<Group wrap="nowrap">
|
<Group wrap="nowrap">
|
||||||
{!isHomeRoute && (
|
{!isHomeRoute && (
|
||||||
<>
|
<>
|
||||||
<SidebarToggle
|
<Tooltip label="Sidebar toggle">
|
||||||
aria-label="sidebar toggle"
|
|
||||||
opened={mobileOpened}
|
|
||||||
onClick={toggleMobile}
|
|
||||||
hiddenFrom="sm"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SidebarToggle
|
<SidebarToggle
|
||||||
aria-label="sidebar toggle"
|
aria-label="Sidebar toggle"
|
||||||
opened={desktopOpened}
|
opened={mobileOpened}
|
||||||
onClick={toggleDesktop}
|
onClick={toggleMobile}
|
||||||
visibleFrom="sm"
|
hiddenFrom="sm"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Sidebar toggle">
|
||||||
|
<SidebarToggle
|
||||||
|
aria-label="Sidebar toggle"
|
||||||
|
opened={desktopOpened}
|
||||||
|
onClick={toggleDesktop}
|
||||||
|
visibleFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
size="lg"
|
size="lg"
|
||||||
fw={600}
|
fw={600}
|
||||||
style={{ cursor: "pointer", userSelect: "none" }}
|
style={{cursor: "pointer", userSelect: "none"}}
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/home"
|
to="/home"
|
||||||
>
|
>
|
||||||
@ -69,7 +74,7 @@ export function AppHeader() {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group px={"xl"}>
|
<Group px={"xl"}>
|
||||||
<TopMenu />
|
<TopMenu/>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,15 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
IconLayoutSidebarRightCollapse,
|
IconLayoutSidebarRightCollapse,
|
||||||
IconLayoutSidebarRightExpand,
|
IconLayoutSidebarRightExpand
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import {
|
import { ActionIcon, BoxProps, ElementProps, MantineColor, MantineSize } from "@mantine/core";
|
||||||
ActionIcon,
|
|
||||||
BoxProps,
|
|
||||||
ElementProps,
|
|
||||||
MantineColor,
|
|
||||||
MantineSize,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
||||||
size?: MantineSize | `compact-${MantineSize}` | (string & {});
|
size?: MantineSize | `compact-${MantineSize}` | (string & {});
|
||||||
@ -17,18 +11,18 @@ export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
|||||||
opened?: boolean;
|
opened?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SidebarToggle({
|
const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
|
||||||
opened,
|
({ opened, size = "sm", ...others }, ref) => {
|
||||||
size = "sm",
|
return (
|
||||||
...others
|
<ActionIcon size={size} {...others} variant="subtle" color="gray" ref={ref}>
|
||||||
}: SidebarToggleProps) {
|
{opened ? (
|
||||||
return (
|
<IconLayoutSidebarRightExpand />
|
||||||
<ActionIcon size={size} {...others} variant="subtle" color="gray">
|
) : (
|
||||||
{opened ? (
|
<IconLayoutSidebarRightCollapse />
|
||||||
<IconLayoutSidebarRightExpand />
|
)}
|
||||||
) : (
|
</ActionIcon>
|
||||||
<IconLayoutSidebarRightCollapse />
|
);
|
||||||
)}
|
}
|
||||||
</ActionIcon>
|
);
|
||||||
);
|
|
||||||
}
|
export default SidebarToggle;
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-qu
|
|||||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(2),
|
name: z.string().trim().min(1),
|
||||||
password: z.string().min(8),
|
password: z.string().min(8),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -15,8 +15,8 @@ import useAuth from "@/features/auth/hooks/use-auth";
|
|||||||
import classes from "@/features/auth/components/auth.module.css";
|
import classes from "@/features/auth/components/auth.module.css";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
workspaceName: z.string().min(2).max(60),
|
workspaceName: z.string().trim().min(3).max(50),
|
||||||
name: z.string().min(2).max(60),
|
name: z.string().min(1).max(50),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: "email is required" })
|
.min(1, { message: "email is required" })
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(2).max(50),
|
name: z.string().trim().min(2).max(50),
|
||||||
description: z.string().max(500),
|
description: z.string().max(500),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,69 +1,72 @@
|
|||||||
import { Table, Group, Text, Anchor } from "@mantine/core";
|
import {Table, Group, Text, Anchor} from "@mantine/core";
|
||||||
import { useGetGroupsQuery } from "@/features/group/queries/group-query";
|
import {useGetGroupsQuery} from "@/features/group/queries/group-query";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
import {IconGroupCircle} from "@/components/icons/icon-people-circle.tsx";
|
||||||
|
|
||||||
export default function GroupList() {
|
export default function GroupList() {
|
||||||
const { data, isLoading } = useGetGroupsQuery();
|
const {data, isLoading} = useGetGroupsQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
{data && (
|
||||||
<Table highlightOnHover verticalSpacing="sm">
|
<Table.ScrollContainer minWidth={400}>
|
||||||
<Table.Thead>
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
<Table.Tr>
|
<Table.Thead>
|
||||||
<Table.Th>Group</Table.Th>
|
<Table.Tr>
|
||||||
<Table.Th>Members</Table.Th>
|
<Table.Th>Group</Table.Th>
|
||||||
</Table.Tr>
|
<Table.Th>Members</Table.Th>
|
||||||
</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">
|
|
||||||
<IconGroupCircle />
|
|
||||||
<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.Tr>
|
||||||
))}
|
</Table.Thead>
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
<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" wrap="nowrap">
|
||||||
|
<IconGroupCircle/>
|
||||||
|
<div>
|
||||||
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
|
{group.name}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed" lineClamp={2}>
|
||||||
|
{group.description}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Anchor>
|
||||||
|
</Table.Td>
|
||||||
|
|
||||||
|
<Table.Td>
|
||||||
|
<Anchor
|
||||||
|
size="sm"
|
||||||
|
underline="never"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "var(--mantine-color-text)",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}
|
||||||
|
component={Link}
|
||||||
|
to={`/settings/groups/${group.id}`}
|
||||||
|
>
|
||||||
|
{group.memberCount} members
|
||||||
|
</Anchor>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
import { Group, Table, Text, Badge, Menu, ActionIcon } from "@mantine/core";
|
import {Group, Table, Text, Badge, Menu, ActionIcon} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
useGroupMembersQuery,
|
useGroupMembersQuery,
|
||||||
useRemoveGroupMemberMutation,
|
useRemoveGroupMemberMutation,
|
||||||
} from "@/features/group/queries/group-query";
|
} from "@/features/group/queries/group-query";
|
||||||
import { useParams } from "react-router-dom";
|
import {useParams} from "react-router-dom";
|
||||||
import React from "react";
|
import React 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 useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
|
|
||||||
export default function GroupMembersList() {
|
export default function GroupMembersList() {
|
||||||
const { groupId } = useParams();
|
const {groupId} = useParams();
|
||||||
const { data, isLoading } = useGroupMembersQuery(groupId);
|
const {data, isLoading} = useGroupMembersQuery(groupId);
|
||||||
const removeGroupMember = useRemoveGroupMemberMutation();
|
const removeGroupMember = useRemoveGroupMemberMutation();
|
||||||
const { isAdmin } = useUserRole();
|
const {isAdmin} = useUserRole();
|
||||||
|
|
||||||
const onRemove = async (userId: string) => {
|
const onRemove = async (userId: string) => {
|
||||||
const memberToRemove = {
|
const memberToRemove = {
|
||||||
@ -34,72 +34,74 @@ export default function GroupMembersList() {
|
|||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
centered: true,
|
centered: true,
|
||||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
labels: {confirm: "Delete", cancel: "Cancel"},
|
||||||
confirmProps: { color: "red" },
|
confirmProps: {color: "red"},
|
||||||
onConfirm: () => onRemove(userId),
|
onConfirm: () => onRemove(userId),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
{data && (
|
||||||
<Table verticalSpacing="sm">
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table.Thead>
|
<Table verticalSpacing="sm">
|
||||||
<Table.Tr>
|
<Table.Thead>
|
||||||
<Table.Th>User</Table.Th>
|
<Table.Tr>
|
||||||
<Table.Th>Status</Table.Th>
|
<Table.Th>User</Table.Th>
|
||||||
<Table.Th></Table.Th>
|
<Table.Th>Status</Table.Th>
|
||||||
</Table.Tr>
|
<Table.Th></Table.Th>
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
|
||||||
{data?.items.map((user, index) => (
|
|
||||||
<Table.Tr key={index}>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="sm">
|
|
||||||
<CustomAvatar 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>
|
|
||||||
{isAdmin && (
|
|
||||||
<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.Tr>
|
||||||
))}
|
</Table.Thead>
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
<Table.Tbody>
|
||||||
|
{data?.items.map((user, index) => (
|
||||||
|
<Table.Tr key={index}>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="sm">
|
||||||
|
<CustomAvatar 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>
|
||||||
|
{isAdmin && (
|
||||||
|
<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>
|
||||||
|
</Table.ScrollContainer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -29,24 +29,22 @@ export function useGetGroupsQuery(
|
|||||||
|
|
||||||
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
|
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['groups', groupId],
|
queryKey: ['group', groupId],
|
||||||
queryFn: () => getGroupById(groupId),
|
queryFn: () => getGroupById(groupId),
|
||||||
enabled: !!groupId,
|
enabled: !!groupId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGroupMembersQuery(groupId: string) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['groupMembers', groupId],
|
|
||||||
queryFn: () => getGroupMembers(groupId),
|
|
||||||
enabled: !!groupId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCreateGroupMutation() {
|
export function useCreateGroupMutation() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<IGroup, Error, Partial<IGroup>>({
|
return useMutation<IGroup, Error, Partial<IGroup>>({
|
||||||
mutationFn: (data) => createGroup(data),
|
mutationFn: (data) => createGroup(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['groups'],
|
||||||
|
});
|
||||||
|
|
||||||
notifications.show({ message: 'Group created successfully' });
|
notifications.show({ message: 'Group created successfully' });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@ -96,6 +94,14 @@ export function useDeleteGroupMutation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useGroupMembersQuery(groupId: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['groupMembers', groupId],
|
||||||
|
queryFn: () => getGroupMembers(groupId),
|
||||||
|
enabled: !!groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useAddGroupMemberMutation() {
|
export function useAddGroupMemberMutation() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,10 @@ import { computeSpaceSlug } from "@/lib";
|
|||||||
import { getSpaceUrl } from "@/lib/config.ts";
|
import { getSpaceUrl } from "@/lib/config.ts";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(2).max(50),
|
name: z.string().trim().min(2).max(50),
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
|
.trim()
|
||||||
.min(2)
|
.min(2)
|
||||||
.max(50)
|
.max(50)
|
||||||
.regex(
|
.regex(
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { Modal, Tabs, rem, Group, ScrollArea } from "@mantine/core";
|
import {Modal, Tabs, rem, Group, ScrollArea, Text} from "@mantine/core";
|
||||||
import SpaceMembersList from "@/features/space/components/space-members.tsx";
|
import SpaceMembersList from "@/features/space/components/space-members.tsx";
|
||||||
import AddSpaceMembersModal from "@/features/space/components/add-space-members-modal.tsx";
|
import AddSpaceMembersModal from "@/features/space/components/add-space-members-modal.tsx";
|
||||||
import React, { useMemo } from "react";
|
import React, {useMemo} from "react";
|
||||||
import SpaceDetails from "@/features/space/components/space-details.tsx";
|
import SpaceDetails from "@/features/space/components/space-details.tsx";
|
||||||
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
|
import {useSpaceQuery} from "@/features/space/queries/space-query.ts";
|
||||||
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
|
import {useSpaceAbility} from "@/features/space/permissions/use-space-ability.ts";
|
||||||
import {
|
import {
|
||||||
SpaceCaslAction,
|
SpaceCaslAction,
|
||||||
SpaceCaslSubject,
|
SpaceCaslSubject,
|
||||||
@ -17,11 +17,11 @@ interface SpaceSettingsModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SpaceSettingsModal({
|
export default function SpaceSettingsModal({
|
||||||
spaceId,
|
spaceId,
|
||||||
opened,
|
opened,
|
||||||
onClose,
|
onClose,
|
||||||
}: SpaceSettingsModalProps) {
|
}: SpaceSettingsModalProps) {
|
||||||
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
const {data: space, isLoading} = useSpaceQuery(spaceId);
|
||||||
|
|
||||||
const spaceRules = space?.membership?.permissions;
|
const spaceRules = space?.membership?.permissions;
|
||||||
const spaceAbility = useMemo(() => useSpaceAbility(spaceRules), [spaceRules]);
|
const spaceAbility = useMemo(() => useSpaceAbility(spaceRules), [spaceRules]);
|
||||||
@ -37,14 +37,16 @@ export default function SpaceSettingsModal({
|
|||||||
xOffset={0}
|
xOffset={0}
|
||||||
mah={400}
|
mah={400}
|
||||||
>
|
>
|
||||||
<Modal.Overlay />
|
<Modal.Overlay/>
|
||||||
<Modal.Content style={{ overflow: "hidden" }}>
|
<Modal.Content style={{overflow: "hidden"}}>
|
||||||
<Modal.Header py={0}>
|
<Modal.Header py={0}>
|
||||||
<Modal.Title fw={500}>{space?.name}</Modal.Title>
|
<Modal.Title>
|
||||||
<Modal.CloseButton />
|
<Text fw={500} lineClamp={1}>{space?.name}</Text>
|
||||||
|
</Modal.Title>
|
||||||
|
<Modal.CloseButton/>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<div style={{ height: rem("600px") }}>
|
<div style={{height: rem(600)}}>
|
||||||
<Tabs defaultValue="members">
|
<Tabs defaultValue="members">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Tab fw={500} value="general">
|
<Tabs.Tab fw={500} value="general">
|
||||||
@ -55,34 +57,32 @@ export default function SpaceSettingsModal({
|
|||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<ScrollArea h="600" w="100%" scrollbarSize={5}>
|
<Tabs.Panel value="general">
|
||||||
<Tabs.Panel value="general">
|
<SpaceDetails
|
||||||
<SpaceDetails
|
spaceId={space?.id}
|
||||||
spaceId={space?.id}
|
readOnly={spaceAbility.cannot(
|
||||||
readOnly={spaceAbility.cannot(
|
SpaceCaslAction.Manage,
|
||||||
SpaceCaslAction.Manage,
|
SpaceCaslSubject.Settings,
|
||||||
SpaceCaslSubject.Settings,
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</Tabs.Panel>
|
||||||
</Tabs.Panel>
|
|
||||||
|
|
||||||
<Tabs.Panel value="members">
|
<Tabs.Panel value="members">
|
||||||
<Group my="md" justify="flex-end">
|
<Group my="md" justify="flex-end">
|
||||||
{spaceAbility.can(
|
{spaceAbility.can(
|
||||||
SpaceCaslAction.Manage,
|
SpaceCaslAction.Manage,
|
||||||
SpaceCaslSubject.Member,
|
SpaceCaslSubject.Member,
|
||||||
) && <AddSpaceMembersModal spaceId={space?.id} />}
|
) && <AddSpaceMembersModal spaceId={space?.id}/>}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<SpaceMembersList
|
<SpaceMembersList
|
||||||
spaceId={space?.id}
|
spaceId={space?.id}
|
||||||
readOnly={spaceAbility.cannot(
|
readOnly={spaceAbility.cannot(
|
||||||
SpaceCaslAction.Manage,
|
SpaceCaslAction.Manage,
|
||||||
SpaceCaslSubject.Member,
|
SpaceCaslSubject.Member,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
</ScrollArea>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { Table, Group, Text, Avatar } from "@mantine/core";
|
import {Table, Group, Text, Avatar} from "@mantine/core";
|
||||||
import React, { useState } from "react";
|
import React, {useState} from "react";
|
||||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
import {useGetSpacesQuery} from "@/features/space/queries/space-query.ts";
|
||||||
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import {useDisclosure} from "@mantine/hooks";
|
||||||
import { formatMemberCount } from "@/lib";
|
import {formatMemberCount} from "@/lib";
|
||||||
|
|
||||||
export default function SpaceList() {
|
export default function SpaceList() {
|
||||||
const { data, isLoading } = useGetSpacesQuery();
|
const {data, isLoading} = useGetSpacesQuery();
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, {open, close}] = useDisclosure(false);
|
||||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
||||||
|
|
||||||
const handleClick = (spaceId: string) => {
|
const handleClick = (spaceId: string) => {
|
||||||
@ -18,44 +18,48 @@ export default function SpaceList() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
{data && (
|
||||||
<Table highlightOnHover verticalSpacing="sm">
|
<Table.ScrollContainer minWidth={400}>
|
||||||
<Table.Thead>
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
<Table.Tr>
|
<Table.Thead>
|
||||||
<Table.Th>Space</Table.Th>
|
<Table.Tr>
|
||||||
<Table.Th>Members</Table.Th>
|
<Table.Th>Space</Table.Th>
|
||||||
</Table.Tr>
|
<Table.Th>Members</Table.Th>
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
|
||||||
{data?.items.map((space, index) => (
|
|
||||||
<Table.Tr
|
|
||||||
key={index}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={() => handleClick(space.id)}
|
|
||||||
>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="sm">
|
|
||||||
<Avatar
|
|
||||||
color="initials"
|
|
||||||
variant="filled"
|
|
||||||
name={space.name}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Text fz="sm" fw={500}>
|
|
||||||
{space.name}
|
|
||||||
</Text>
|
|
||||||
<Text fz="xs" c="dimmed">
|
|
||||||
{space.description}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
|
|
||||||
<Table.Td>{formatMemberCount(space.memberCount)}</Table.Td>
|
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
</Table.Thead>
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
<Table.Tbody>
|
||||||
|
{data?.items.map((space, index) => (
|
||||||
|
<Table.Tr
|
||||||
|
key={index}
|
||||||
|
style={{cursor: "pointer"}}
|
||||||
|
onClick={() => handleClick(space.id)}
|
||||||
|
>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
<Avatar
|
||||||
|
color="initials"
|
||||||
|
variant="filled"
|
||||||
|
name={space.name}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
|
{space.name}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed" lineClamp={2}>
|
||||||
|
{space.description}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" style={{whiteSpace: 'nowrap'}}>{formatMemberCount(space.memberCount)}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedSpaceId && (
|
{selectedSpaceId && (
|
||||||
|
|||||||
@ -1,32 +1,34 @@
|
|||||||
import { Group, Table, Text, Menu, ActionIcon } from "@mantine/core";
|
import {Group, Table, Text, Menu, ActionIcon} from "@mantine/core";
|
||||||
import React from "react";
|
import React 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,
|
useSpaceMembersQuery,
|
||||||
} 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";
|
||||||
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
||||||
import {
|
import {
|
||||||
getSpaceRoleLabel,
|
getSpaceRoleLabel,
|
||||||
spaceRoleData,
|
spaceRoleData,
|
||||||
} from "@/features/space/types/space-role-data.ts";
|
} from "@/features/space/types/space-role-data.ts";
|
||||||
import { formatMemberCount } from "@/lib";
|
import {formatMemberCount} from "@/lib";
|
||||||
|
|
||||||
type MemberType = "user" | "group";
|
type MemberType = "user" | "group";
|
||||||
|
|
||||||
interface SpaceMembersProps {
|
interface SpaceMembersProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SpaceMembersList({
|
export default function SpaceMembersList({
|
||||||
spaceId,
|
spaceId,
|
||||||
readOnly,
|
readOnly,
|
||||||
}: SpaceMembersProps) {
|
}: SpaceMembersProps) {
|
||||||
const { data, isLoading } = useSpaceMembersQuery(spaceId);
|
const {data, isLoading} = useSpaceMembersQuery(spaceId);
|
||||||
const removeSpaceMember = useRemoveSpaceMemberMutation();
|
const removeSpaceMember = useRemoveSpaceMemberMutation();
|
||||||
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
|
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
|
||||||
|
|
||||||
@ -85,99 +87,101 @@ export default function SpaceMembersList({
|
|||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
centered: true,
|
centered: true,
|
||||||
labels: { confirm: "Remove", cancel: "Cancel" },
|
labels: {confirm: "Remove", cancel: "Cancel"},
|
||||||
confirmProps: { color: "red" },
|
confirmProps: {color: "red"},
|
||||||
onConfirm: () => onRemove(memberId, type),
|
onConfirm: () => onRemove(memberId, type),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
{data && (
|
||||||
<Table verticalSpacing={8}>
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table.Thead>
|
<Table verticalSpacing={8}>
|
||||||
<Table.Tr>
|
<Table.Thead>
|
||||||
<Table.Th>Member</Table.Th>
|
<Table.Tr>
|
||||||
<Table.Th>Role</Table.Th>
|
<Table.Th>Member</Table.Th>
|
||||||
<Table.Th></Table.Th>
|
<Table.Th>Role</Table.Th>
|
||||||
</Table.Tr>
|
<Table.Th></Table.Th>
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
|
||||||
{data?.items.map((member, index) => (
|
|
||||||
<Table.Tr key={index}>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="sm">
|
|
||||||
{member.type === "user" && (
|
|
||||||
<CustomAvatar
|
|
||||||
avatarUrl={member?.avatarUrl}
|
|
||||||
name={member.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{member.type === "group" && <IconGroupCircle />}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text fz="sm" fw={500}>
|
|
||||||
{member?.name}
|
|
||||||
</Text>
|
|
||||||
<Text fz="xs" c="dimmed">
|
|
||||||
{member.type == "user" && member?.email}
|
|
||||||
|
|
||||||
{member.type == "group" &&
|
|
||||||
`Group - ${formatMemberCount(member?.memberCount)}`}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
|
|
||||||
<Table.Td>
|
|
||||||
<RoleSelectMenu
|
|
||||||
roles={spaceRoleData}
|
|
||||||
roleName={getSpaceRoleLabel(member.role)}
|
|
||||||
onChange={(newRole) =>
|
|
||||||
handleRoleChange(
|
|
||||||
member.id,
|
|
||||||
member.type,
|
|
||||||
newRole,
|
|
||||||
member.role,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={readOnly}
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
|
|
||||||
<Table.Td>
|
|
||||||
{!readOnly && (
|
|
||||||
<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(member.id, member.type)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Remove space member
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
</Table.Thead>
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
<Table.Tbody>
|
||||||
|
{data?.items.map((member, index) => (
|
||||||
|
<Table.Tr key={index}>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="sm">
|
||||||
|
{member.type === "user" && (
|
||||||
|
<CustomAvatar
|
||||||
|
avatarUrl={member?.avatarUrl}
|
||||||
|
name={member.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{member.type === "group" && <IconGroupCircle/>}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text fz="sm" fw={500}>
|
||||||
|
{member?.name}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed">
|
||||||
|
{member.type == "user" && member?.email}
|
||||||
|
|
||||||
|
{member.type == "group" &&
|
||||||
|
`Group - ${formatMemberCount(member?.memberCount)}`}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
|
||||||
|
<Table.Td>
|
||||||
|
<RoleSelectMenu
|
||||||
|
roles={spaceRoleData}
|
||||||
|
roleName={getSpaceRoleLabel(member.role)}
|
||||||
|
onChange={(newRole) =>
|
||||||
|
handleRoleChange(
|
||||||
|
member.id,
|
||||||
|
member.type,
|
||||||
|
newRole,
|
||||||
|
member.role,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
|
||||||
|
<Table.Td>
|
||||||
|
{!readOnly && (
|
||||||
|
<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(member.id, member.type)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Remove space member
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export function useGetSpacesQuery(
|
|||||||
|
|
||||||
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
|
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['spaces', spaceId],
|
queryKey: ['space', spaceId],
|
||||||
queryFn: () => getSpaceById(spaceId),
|
queryFn: () => getSpaceById(spaceId),
|
||||||
enabled: !!spaceId,
|
enabled: !!spaceId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
@ -65,7 +65,7 @@ export function useGetSpaceBySlugQuery(
|
|||||||
spaceId: string
|
spaceId: string
|
||||||
): UseQueryResult<ISpace, Error> {
|
): UseQueryResult<ISpace, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['spaces', spaceId],
|
queryKey: ['space', spaceId],
|
||||||
queryFn: () => getSpaceById(spaceId),
|
queryFn: () => getSpaceById(spaceId),
|
||||||
enabled: !!spaceId,
|
enabled: !!spaceId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
@ -111,7 +111,7 @@ export function useDeleteSpaceMutation() {
|
|||||||
|
|
||||||
if (variables.slug) {
|
if (variables.slug) {
|
||||||
queryClient.removeQueries({
|
queryClient.removeQueries({
|
||||||
queryKey: ['spaces', variables.slug],
|
queryKey: ['space', variables.slug],
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,62 +1,64 @@
|
|||||||
import { Group, Table, Avatar, Text, Alert } from "@mantine/core";
|
import {Group, Table, Avatar, Text, Alert} from "@mantine/core";
|
||||||
import { useWorkspaceInvitationsQuery } from "@/features/workspace/queries/workspace-query.ts";
|
import {useWorkspaceInvitationsQuery} from "@/features/workspace/queries/workspace-query.ts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { getUserRoleLabel } from "@/features/workspace/types/user-role-data.ts";
|
import {getUserRoleLabel} from "@/features/workspace/types/user-role-data.ts";
|
||||||
import InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx";
|
import InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx";
|
||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
import {IconInfoCircle} from "@tabler/icons-react";
|
||||||
import { formattedDate } from "@/lib/time.ts";
|
import {formattedDate, timeAgo} from "@/lib/time.ts";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
|
|
||||||
export default function WorkspaceInvitesTable() {
|
export default function WorkspaceInvitesTable() {
|
||||||
const { data, isLoading } = useWorkspaceInvitationsQuery({
|
const {data, isLoading} = useWorkspaceInvitationsQuery({
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
const { isAdmin } = useUserRole();
|
const {isAdmin} = useUserRole();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Alert variant="light" color="blue" icon={<IconInfoCircle />}>
|
<Alert variant="light" color="blue" icon={<IconInfoCircle/>}>
|
||||||
Invited members who are yet to accept their invitation will appear here.
|
Invited members who are yet to accept their invitation will appear here.
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{data && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
<Table verticalSpacing="sm">
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table.Thead>
|
<Table verticalSpacing="sm">
|
||||||
<Table.Tr>
|
<Table.Thead>
|
||||||
<Table.Th>Email</Table.Th>
|
<Table.Tr>
|
||||||
<Table.Th>Role</Table.Th>
|
<Table.Th>Email</Table.Th>
|
||||||
<Table.Th>Date</Table.Th>
|
<Table.Th>Role</Table.Th>
|
||||||
</Table.Tr>
|
<Table.Th>Date</Table.Th>
|
||||||
</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.Td>{getUserRoleLabel(invitation.role)}</Table.Td>
|
|
||||||
|
|
||||||
<Table.Td>{formattedDate(invitation.createdAt)}</Table.Td>
|
|
||||||
|
|
||||||
<Table.Td>
|
|
||||||
{isAdmin && (
|
|
||||||
<InviteActionMenu invitationId={invitation.id} />
|
|
||||||
)}
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
</Table.Thead>
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
<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.Td>{getUserRoleLabel(invitation.role)}</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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Group, Table, Text, Badge } from "@mantine/core";
|
import {Group, Table, Text, Badge} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
useChangeMemberRoleMutation,
|
useChangeMemberRoleMutation,
|
||||||
useWorkspaceMembersQuery,
|
useWorkspaceMembersQuery,
|
||||||
} from "@/features/workspace/queries/workspace-query.ts";
|
} from "@/features/workspace/queries/workspace-query.ts";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import {CustomAvatar} from "@/components/ui/custom-avatar.tsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
||||||
import {
|
import {
|
||||||
@ -11,14 +11,14 @@ import {
|
|||||||
userRoleData,
|
userRoleData,
|
||||||
} from "@/features/workspace/types/user-role-data.ts";
|
} from "@/features/workspace/types/user-role-data.ts";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
import { UserRole } from "@/lib/types.ts";
|
import {UserRole} from "@/lib/types.ts";
|
||||||
|
|
||||||
export default function WorkspaceMembersTable() {
|
export default function WorkspaceMembersTable() {
|
||||||
const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 });
|
const {data, isLoading} = useWorkspaceMembersQuery({limit: 100});
|
||||||
const changeMemberRoleMutation = useChangeMemberRoleMutation();
|
const changeMemberRoleMutation = useChangeMemberRoleMutation();
|
||||||
const { isAdmin, isOwner } = useUserRole();
|
const {isAdmin, isOwner} = useUserRole();
|
||||||
|
|
||||||
const assignableUserRoles = isOwner ? userRoleData : userRoleData.filter((role) => role.value !== UserRole.OWNER);
|
const assignableUserRoles = isOwner ? userRoleData : userRoleData.filter((role) => role.value !== UserRole.OWNER);
|
||||||
|
|
||||||
const handleRoleChange = async (
|
const handleRoleChange = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
@ -40,50 +40,52 @@ export default function WorkspaceMembersTable() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
{data && (
|
||||||
<Table verticalSpacing="sm">
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table.Thead>
|
<Table verticalSpacing="sm">
|
||||||
<Table.Tr>
|
<Table.Thead>
|
||||||
<Table.Th>User</Table.Th>
|
<Table.Tr>
|
||||||
<Table.Th>Status</Table.Th>
|
<Table.Th>User</Table.Th>
|
||||||
<Table.Th>Role</Table.Th>
|
<Table.Th>Status</Table.Th>
|
||||||
</Table.Tr>
|
<Table.Th>Role</Table.Th>
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
|
||||||
{data?.items.map((user, index) => (
|
|
||||||
<Table.Tr key={index}>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="sm">
|
|
||||||
<CustomAvatar 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>
|
|
||||||
<RoleSelectMenu
|
|
||||||
roles={assignableUserRoles}
|
|
||||||
roleName={getUserRoleLabel(user.role)}
|
|
||||||
onChange={(newRole) =>
|
|
||||||
handleRoleChange(user.id, user.role, newRole)
|
|
||||||
}
|
|
||||||
disabled={!isAdmin}
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
</Table.Thead>
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
<Table.Tbody>
|
||||||
|
{data?.items.map((user, index) => (
|
||||||
|
<Table.Tr key={index}>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="sm">
|
||||||
|
<CustomAvatar 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>
|
||||||
|
<RoleSelectMenu
|
||||||
|
roles={assignableUserRoles}
|
||||||
|
roleName={getUserRoleLabel(user.role)}
|
||||||
|
onChange={(newRole) =>
|
||||||
|
handleRoleChange(user.id, user.role, newRole)
|
||||||
|
}
|
||||||
|
disabled={!isAdmin}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,10 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAppName(): string{
|
||||||
|
return 'Docmost';
|
||||||
|
}
|
||||||
|
|
||||||
export function getAppUrl(): string {
|
export function getAppUrl(): string {
|
||||||
//let appUrl = window.CONFIG?.APP_URL || process.env.APP_URL;
|
//let appUrl = window.CONFIG?.APP_URL || process.env.APP_URL;
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { ForgotPasswordForm } from "@/features/auth/components/forgot-password-form";
|
import { ForgotPasswordForm } from "@/features/auth/components/forgot-password-form";
|
||||||
|
import { getAppName } from "@/lib/config";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
|
|
||||||
export default function ForgotPassword() {
|
export default function ForgotPassword() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Forgot Password - Docmost</title>
|
<title>Forgot Password - {getAppName()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<ForgotPasswordForm />
|
<ForgotPasswordForm />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { InviteSignUpForm } from "@/features/auth/components/invite-sign-up-form.tsx";
|
import { InviteSignUpForm } from "@/features/auth/components/invite-sign-up-form.tsx";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
|
||||||
export default function InviteSignup() {
|
export default function InviteSignup() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Invitation Signup - Docmost</title>
|
<title>Invitation Signuo - {getAppName()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<InviteSignUpForm />
|
<InviteSignUpForm />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { LoginForm } from "@/features/auth/components/login-form";
|
import { LoginForm } from "@/features/auth/components/login-form";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Login - Docmost</title>
|
<title>Login - {getAppName()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Link, useSearchParams } from "react-router-dom";
|
|||||||
import { useVerifyUserTokenQuery } from "@/features/auth/queries/auth-query";
|
import { useVerifyUserTokenQuery } from "@/features/auth/queries/auth-query";
|
||||||
import { Button, Container, Group, Text } from "@mantine/core";
|
import { Button, Container, Group, Text } from "@mantine/core";
|
||||||
import APP_ROUTE from "@/lib/app-route";
|
import APP_ROUTE from "@/lib/app-route";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
|
||||||
export default function PasswordReset() {
|
export default function PasswordReset() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@ -21,7 +22,7 @@ export default function PasswordReset() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Password Reset - Docmost</title>
|
<title>Password Reset - {getAppName()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Container my={40}>
|
<Container my={40}>
|
||||||
<Text size="lg" ta="center">
|
<Text size="lg" ta="center">
|
||||||
@ -45,7 +46,7 @@ export default function PasswordReset() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Password Reset - Docmost</title>
|
<title>Password Reset - {getAppName()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<PasswordResetForm resetToken={resetToken} />
|
<PasswordResetForm resetToken={resetToken} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { SetupWorkspaceForm } from "@/features/auth/components/setup-workspace-f
|
|||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
|
||||||
export default function SetupWorkspace() {
|
export default function SetupWorkspace() {
|
||||||
const {
|
const {
|
||||||
@ -32,7 +33,7 @@ export default function SetupWorkspace() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Setup Workspace - Docmost</title>
|
<title>Setup Workspace - {getAppName()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<SetupWorkspaceForm />
|
<SetupWorkspaceForm />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
import { Container, Space } from "@mantine/core";
|
import {Container, Space} from "@mantine/core";
|
||||||
import HomeTabs from "@/features/home/components/home-tabs";
|
import HomeTabs from "@/features/home/components/home-tabs";
|
||||||
import SpaceGrid from "@/features/space/components/space-grid.tsx";
|
import SpaceGrid from "@/features/space/components/space-grid.tsx";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
import {Helmet} from "react-helmet-async";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<Container size={"800"} pt="xl">
|
<>
|
||||||
<SpaceGrid />
|
<Helmet>
|
||||||
|
<title>Home - {getAppName()}</title>
|
||||||
|
</Helmet>
|
||||||
|
<Container size={"800"} pt="xl">
|
||||||
|
<SpaceGrid/>
|
||||||
|
|
||||||
<Space h="xl" />
|
<Space h="xl"/>
|
||||||
|
|
||||||
<HomeTabs />
|
<HomeTabs/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
import AccountTheme from "@/features/user/components/account-theme.tsx";
|
import AccountTheme from "@/features/user/components/account-theme.tsx";
|
||||||
import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
|
import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
|
||||||
import { Divider } from "@mantine/core";
|
import {Divider} from "@mantine/core";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
import {Helmet} from "react-helmet-async";
|
||||||
|
|
||||||
export default function AccountPreferences() {
|
export default function AccountPreferences() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsTitle title="Preferences" />
|
<Helmet>
|
||||||
<AccountTheme />
|
<title>Preferences - {getAppName()}</title>
|
||||||
<Divider my={"md"} />
|
</Helmet>
|
||||||
<PageWidthPref />
|
<SettingsTitle title="Preferences"/>
|
||||||
</>
|
<AccountTheme/>
|
||||||
);
|
<Divider my={"md"}/>
|
||||||
|
<PageWidthPref/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,15 @@ import ChangePassword from "@/features/user/components/change-password";
|
|||||||
import { Divider } from "@mantine/core";
|
import { Divider } from "@mantine/core";
|
||||||
import AccountAvatar from "@/features/user/components/account-avatar";
|
import AccountAvatar from "@/features/user/components/account-avatar";
|
||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
import {Helmet} from "react-helmet-async";
|
||||||
|
|
||||||
export default function AccountSettings() {
|
export default function AccountSettings() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>My Profile - {getAppName()}</title>
|
||||||
|
</Helmet>
|
||||||
<SettingsTitle title="My Profile" />
|
<SettingsTitle title="My Profile" />
|
||||||
|
|
||||||
<AccountAvatar />
|
<AccountAvatar />
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
import GroupMembersList from "@/features/group/components/group-members";
|
import GroupMembersList from "@/features/group/components/group-members";
|
||||||
import GroupDetails from "@/features/group/components/group-details";
|
import GroupDetails from "@/features/group/components/group-details";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
import {Helmet} from "react-helmet-async";
|
||||||
|
|
||||||
export default function GroupInfo() {
|
export default function GroupInfo() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsTitle title="Manage Group" />
|
<Helmet>
|
||||||
<GroupDetails />
|
<title>Manage Group - {getAppName()}</title>
|
||||||
<GroupMembersList />
|
</Helmet>
|
||||||
</>
|
<SettingsTitle title="Manage Group"/>
|
||||||
);
|
<GroupDetails/>
|
||||||
|
<GroupMembersList/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,17 @@ import SettingsTitle from "@/components/settings/settings-title.tsx";
|
|||||||
import { Group } from "@mantine/core";
|
import { Group } from "@mantine/core";
|
||||||
import CreateGroupModal from "@/features/group/components/create-group-modal";
|
import CreateGroupModal from "@/features/group/components/create-group-modal";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
import {Helmet} from "react-helmet-async";
|
||||||
|
|
||||||
export default function Groups() {
|
export default function Groups() {
|
||||||
const { isAdmin } = useUserRole();
|
const { isAdmin } = useUserRole();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Groups - {getAppName()}</title>
|
||||||
|
</Helmet>
|
||||||
<SettingsTitle title="Groups" />
|
<SettingsTitle title="Groups" />
|
||||||
|
|
||||||
<Group my="md" justify="flex-end">
|
<Group my="md" justify="flex-end">
|
||||||
|
|||||||
@ -1,21 +1,26 @@
|
|||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
import SpaceList from "@/features/space/components/space-list.tsx";
|
import SpaceList from "@/features/space/components/space-list.tsx";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
import { Group } from "@mantine/core";
|
import {Group} from "@mantine/core";
|
||||||
import CreateSpaceModal from "@/features/space/components/create-space-modal.tsx";
|
import CreateSpaceModal from "@/features/space/components/create-space-modal.tsx";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
import {Helmet} from "react-helmet-async";
|
||||||
|
|
||||||
export default function Spaces() {
|
export default function Spaces() {
|
||||||
const { isAdmin } = useUserRole();
|
const {isAdmin} = useUserRole();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsTitle title="Spaces" />
|
<Helmet>
|
||||||
|
<title>Spaces - {getAppName()}</title>
|
||||||
|
</Helmet>
|
||||||
|
<SettingsTitle title="Spaces"/>
|
||||||
|
|
||||||
<Group my="md" justify="flex-end">
|
<Group my="md" justify="flex-end">
|
||||||
{isAdmin && <CreateSpaceModal />}
|
{isAdmin && <CreateSpaceModal/>}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<SpaceList />
|
<SpaceList/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,62 +1,67 @@
|
|||||||
import WorkspaceInviteModal from "@/features/workspace/components/members/components/workspace-invite-modal";
|
import WorkspaceInviteModal from "@/features/workspace/components/members/components/workspace-invite-modal";
|
||||||
import { Group, SegmentedControl, Space, Text } from "@mantine/core";
|
import {Group, SegmentedControl, Space, Text} from "@mantine/core";
|
||||||
import WorkspaceMembersTable from "@/features/workspace/components/members/components/workspace-members-table";
|
import WorkspaceMembersTable from "@/features/workspace/components/members/components/workspace-members-table";
|
||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||||
import WorkspaceInvitesTable from "@/features/workspace/components/members/components/workspace-invites-table.tsx";
|
import WorkspaceInvitesTable from "@/features/workspace/components/members/components/workspace-invites-table.tsx";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
import {Helmet} from "react-helmet-async";
|
||||||
|
|
||||||
export default function WorkspaceMembers() {
|
export default function WorkspaceMembers() {
|
||||||
const [segmentValue, setSegmentValue] = useState("members");
|
const [segmentValue, setSegmentValue] = useState("members");
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { isAdmin } = useUserRole();
|
const {isAdmin} = useUserRole();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentTab = searchParams.get("tab");
|
const currentTab = searchParams.get("tab");
|
||||||
if (currentTab === "invites") {
|
if (currentTab === "invites") {
|
||||||
setSegmentValue(currentTab);
|
setSegmentValue(currentTab);
|
||||||
}
|
}
|
||||||
}, [searchParams.get("tab")]);
|
}, [searchParams.get("tab")]);
|
||||||
|
|
||||||
const handleSegmentChange = (value: string) => {
|
const handleSegmentChange = (value: string) => {
|
||||||
setSegmentValue(value);
|
setSegmentValue(value);
|
||||||
if (value === "invites") {
|
if (value === "invites") {
|
||||||
navigate(`?tab=${value}`);
|
navigate(`?tab=${value}`);
|
||||||
} else {
|
} else {
|
||||||
navigate("");
|
navigate("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsTitle title="Members" />
|
<Helmet>
|
||||||
|
<title>Members - {getAppName()}</title>
|
||||||
|
</Helmet>
|
||||||
|
<SettingsTitle title="Members"/>
|
||||||
|
|
||||||
{/* <WorkspaceInviteSection /> */}
|
{/* <WorkspaceInviteSection /> */}
|
||||||
{/* <Divider my="lg" /> */}
|
{/* <Divider my="lg" /> */}
|
||||||
|
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
value={segmentValue}
|
value={segmentValue}
|
||||||
onChange={handleSegmentChange}
|
onChange={handleSegmentChange}
|
||||||
data={[
|
data={[
|
||||||
{ label: "Members", value: "members" },
|
{label: "Members", value: "members"},
|
||||||
{ label: "Pending", value: "invites" },
|
{label: "Pending", value: "invites"},
|
||||||
]}
|
]}
|
||||||
withItemsBorders={false}
|
withItemsBorders={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isAdmin && <WorkspaceInviteModal />}
|
{isAdmin && <WorkspaceInviteModal/>}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Space h="lg" />
|
<Space h="lg"/>
|
||||||
|
|
||||||
{segmentValue === "invites" ? (
|
{segmentValue === "invites" ? (
|
||||||
<WorkspaceInvitesTable />
|
<WorkspaceInvitesTable/>
|
||||||
) : (
|
) : (
|
||||||
<WorkspaceMembersTable />
|
<WorkspaceMembersTable/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
import WorkspaceNameForm from "@/features/workspace/components/settings/components/workspace-name-form";
|
import WorkspaceNameForm from "@/features/workspace/components/settings/components/workspace-name-form";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
import {Helmet} from "react-helmet-async";
|
||||||
|
|
||||||
export default function WorkspaceSettings() {
|
export default function WorkspaceSettings() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsTitle title="General" />
|
<Helmet>
|
||||||
<WorkspaceNameForm />
|
<title>Workspace Settings - {getAppName()}</title>
|
||||||
</>
|
</Helmet>
|
||||||
);
|
<SettingsTitle title="General"/>
|
||||||
|
<WorkspaceNameForm/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
import { Container } from "@mantine/core";
|
import {Container} from "@mantine/core";
|
||||||
import SpaceHomeTabs from "@/features/space/components/space-home-tabs.tsx";
|
import SpaceHomeTabs from "@/features/space/components/space-home-tabs.tsx";
|
||||||
import { useParams } from "react-router-dom";
|
import {useParams} from "react-router-dom";
|
||||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
import {useGetSpaceBySlugQuery} from "@/features/space/queries/space-query.ts";
|
||||||
|
import {getAppName} from "@/lib/config.ts";
|
||||||
|
import {Helmet} from "react-helmet-async";
|
||||||
|
|
||||||
export default function SpaceHome() {
|
export default function SpaceHome() {
|
||||||
const { spaceSlug } = useParams();
|
const {spaceSlug} = useParams();
|
||||||
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
const {data: space} = useGetSpaceBySlugQuery(spaceSlug);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size={"800"} pt="xl">
|
<>
|
||||||
{space && <SpaceHomeTabs />}
|
<Helmet>
|
||||||
</Container>
|
<title>{space?.name || 'Overview'} - {getAppName()}</title>
|
||||||
);
|
</Helmet>
|
||||||
|
<Container size={"800"} pt="xl">
|
||||||
|
{space && <SpaceHomeTabs/>}
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import { Title, Text, Stack } from '@mantine/core';
|
|
||||||
import { ThemeToggle } from '@/components/theme-toggle';
|
|
||||||
|
|
||||||
export function Welcome() {
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Title ta="center" mt={100}>
|
|
||||||
<Text
|
|
||||||
inherit
|
|
||||||
variant="gradient"
|
|
||||||
component="span"
|
|
||||||
gradient={{ from: 'pink', to: 'yellow' }}
|
|
||||||
>
|
|
||||||
Welcome
|
|
||||||
</Text>
|
|
||||||
</Title>
|
|
||||||
<Text ta="center" size="lg" maw={580} mx="auto" mt="xl">
|
|
||||||
Welcome to something new and interesting.
|
|
||||||
</Text>
|
|
||||||
<ThemeToggle />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,15 +1,18 @@
|
|||||||
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
|
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
import { CreateUserDto } from './create-user.dto';
|
import { CreateUserDto } from './create-user.dto';
|
||||||
|
import {Transform, TransformFnParams} from "class-transformer";
|
||||||
|
|
||||||
export class CreateAdminUserDto extends CreateUserDto {
|
export class CreateAdminUserDto extends CreateUserDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MinLength(3)
|
@MinLength(1)
|
||||||
@MaxLength(35)
|
@MaxLength(50)
|
||||||
|
@Transform(({ value }: TransformFnParams) => value?.trim())
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MinLength(4)
|
@MinLength(3)
|
||||||
@MaxLength(35)
|
@MaxLength(50)
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@Transform(({ value }: TransformFnParams) => value?.trim())
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import {
|
|||||||
MaxLength,
|
MaxLength,
|
||||||
MinLength,
|
MinLength,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import {Transform, TransformFnParams} from "class-transformer";
|
||||||
|
|
||||||
export class CreateUserDto {
|
export class CreateUserDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@MinLength(2)
|
@MinLength(1)
|
||||||
@MaxLength(60)
|
@MaxLength(50)
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@Transform(({ value }: TransformFnParams) => value?.trim())
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { IsString, MinLength } from 'class-validator';
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
export class VerifyUserTokenDto {
|
export class VerifyUserTokenDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import {
|
|||||||
MaxLength,
|
MaxLength,
|
||||||
MinLength,
|
MinLength,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import {Transform, TransformFnParams} from "class-transformer";
|
||||||
|
|
||||||
export class CreateGroupDto {
|
export class CreateGroupDto {
|
||||||
@MinLength(2)
|
@MinLength(2)
|
||||||
@MaxLength(50)
|
@MaxLength(50)
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@Transform(({ value }: TransformFnParams) => value?.trim())
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import {
|
|||||||
MaxLength,
|
MaxLength,
|
||||||
MinLength,
|
MinLength,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import {Transform, TransformFnParams} from "class-transformer";
|
||||||
|
|
||||||
export class CreateSpaceDto {
|
export class CreateSpaceDto {
|
||||||
@MinLength(2)
|
@MinLength(2)
|
||||||
@MaxLength(50)
|
@MaxLength(50)
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@Transform(({ value }: TransformFnParams) => value?.trim())
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
|
import {IsAlphanumeric, IsOptional, IsString, MaxLength, MinLength} from 'class-validator';
|
||||||
|
import {Transform, TransformFnParams} from "class-transformer";
|
||||||
|
|
||||||
export class CreateWorkspaceDto {
|
export class CreateWorkspaceDto {
|
||||||
@MinLength(4)
|
@MinLength(4)
|
||||||
@MaxLength(64)
|
@MaxLength(64)
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@Transform(({ value }: TransformFnParams) => value?.trim())
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@MinLength(4)
|
@MinLength(4)
|
||||||
@MaxLength(30)
|
@MaxLength(30)
|
||||||
@IsString()
|
@IsAlphanumeric()
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
Reference in New Issue
Block a user