diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 339358c..7fb5c59 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -11,6 +11,7 @@ import SettingsLayout from "@/components/layouts/settings/settings-layout.tsx"; import WorkspaceSettings from "@/pages/settings/workspace/workspace-settings"; import Groups from "@/pages/settings/group/groups"; import GroupInfo from "./pages/settings/group/group-info"; +import Spaces from "@/pages/settings/space/spaces.tsx"; export default function App() { return ( @@ -31,8 +32,7 @@ export default function App() { } /> } /> } /> - } /> - } /> + } /> diff --git a/apps/client/src/components/icons/icon-people-circle.tsx b/apps/client/src/components/icons/icon-people-circle.tsx new file mode 100644 index 0000000..860bf34 --- /dev/null +++ b/apps/client/src/components/icons/icon-people-circle.tsx @@ -0,0 +1,15 @@ +import { ActionIcon, rem } from "@mantine/core"; +import React from "react"; +import { IconUsersGroup } from "@tabler/icons-react"; + +interface IconPeopleCircleProps extends React.ComponentPropsWithoutRef<"svg"> { + size?: number | string; +} + +export function IconGroupCircle() { + return ( + + + + ); +} diff --git a/apps/client/src/components/layouts/settings/settings-sidebar.tsx b/apps/client/src/components/layouts/settings/settings-sidebar.tsx index 53570ac..ac88497 100644 --- a/apps/client/src/components/layouts/settings/settings-sidebar.tsx +++ b/apps/client/src/components/layouts/settings/settings-sidebar.tsx @@ -42,11 +42,6 @@ const groupedData: DataGroup[] = [ }, { label: "Groups", icon: IconUsersGroup, path: "/settings/groups" }, { label: "Spaces", icon: IconSpaces, path: "/settings/spaces" }, - { - label: "Security", - icon: IconFingerprint, - path: "/settings/security", - }, ], }, ]; diff --git a/apps/client/src/components/layouts/settings/settings.module.css b/apps/client/src/components/layouts/settings/settings.module.css index 3056511..aa4757a 100644 --- a/apps/client/src/components/layouts/settings/settings.module.css +++ b/apps/client/src/components/layouts/settings/settings.module.css @@ -31,7 +31,8 @@ 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); + padding-left: var(--mantine-spacing-xs) ; + min-height: 30px; border-radius: var(--mantine-radius-sm); font-weight: 500; user-select: none; diff --git a/apps/client/src/components/navbar/navbar.tsx b/apps/client/src/components/navbar/navbar.tsx index 0facf07..8087321 100644 --- a/apps/client/src/components/navbar/navbar.tsx +++ b/apps/client/src/components/navbar/navbar.tsx @@ -5,24 +5,23 @@ import { ActionIcon, Tooltip, rem, -} from '@mantine/core'; -import { spotlight } from '@mantine/spotlight'; +} from "@mantine/core"; +import { spotlight } from "@mantine/spotlight"; import { IconSearch, IconPlus, IconSettings, - IconFilePlus, IconHome, -} from '@tabler/icons-react'; +} from "@tabler/icons-react"; -import classes from './navbar.module.css'; -import { UserButton } from './user-button'; -import React from 'react'; -import { useAtom } from 'jotai'; -import { SearchSpotlight } from '@/features/search/search-spotlight'; -import { treeApiAtom } from '@/features/page/tree/atoms/tree-api-atom'; -import PageTree from '@/features/page/tree/page-tree'; -import { useNavigate } from 'react-router-dom'; +import classes from "./navbar.module.css"; +import { UserButton } from "./user-button"; +import React from "react"; +import { useAtom } from "jotai"; +import { SearchSpotlight } from "@/features/search/search-spotlight"; +import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom"; +import PageTree from "@/features/page/tree/page-tree"; +import { useNavigate } from "react-router-dom"; interface PrimaryMenuItem { icon: React.ElementType; @@ -31,9 +30,9 @@ interface PrimaryMenuItem { } const primaryMenu: PrimaryMenuItem[] = [ - { icon: IconHome, label: 'Home' }, - { icon: IconSearch, label: 'Search' }, - { icon: IconSettings, label: 'Settings' }, + { icon: IconHome, label: "Home" }, + { icon: IconSearch, label: "Search" }, + { icon: IconSettings, label: "Settings" }, // { icon: IconFilePlus, label: 'New Page' }, ]; @@ -42,21 +41,21 @@ export function Navbar() { const navigate = useNavigate(); const handleMenuItemClick = (label: string) => { - if (label === 'Home') { - navigate('/home'); + if (label === "Home") { + navigate("/home"); } - if (label === 'Search') { + if (label === "Search") { spotlight.open(); } - if (label === 'Settings') { - navigate('/settings/workspace'); + if (label === "Settings") { + navigate("/settings/workspace"); } }; function handleCreatePage() { - tree?.create({ parentId: null, type: 'internal', index: 0 }); + tree?.create({ parentId: null, type: "internal", index: 0 }); } const primaryMenuItems = primaryMenu.map((menuItem) => ( diff --git a/apps/client/src/components/ui/role-select-menu.tsx b/apps/client/src/components/ui/role-select-menu.tsx new file mode 100644 index 0000000..c38700b --- /dev/null +++ b/apps/client/src/components/ui/role-select-menu.tsx @@ -0,0 +1,63 @@ +import React, { forwardRef } from "react"; +import { IconCheck, IconChevronDown } from "@tabler/icons-react"; +import { Group, Text, Menu, Button } from "@mantine/core"; +import { IRoleData } from "@/lib/types.ts"; + +interface RoleButtonProps extends React.ComponentPropsWithoutRef<"button"> { + name: string; +} + +const RoleButton = forwardRef( + ({ name, ...others }: RoleButtonProps, ref) => ( + + ), +); + +interface SpaceRoleMenuProps { + roles: IRoleData[]; + roleName: string; + onChange?: (value: string) => void; +} + +export default function RoleSelectMenu({ + roles, + roleName, + onChange, +}: SpaceRoleMenuProps) { + return ( + + + + + + + {roles?.map((item) => ( + onChange && onChange(item.value)} + key={item.value} + > + +
+ {item.label} + + {item.description} + +
+ {item.label === roleName && } +
+
+ ))} +
+
+ ); +} diff --git a/apps/client/src/features/group/components/group-list.tsx b/apps/client/src/features/group/components/group-list.tsx index 674db2c..ae5d647 100644 --- a/apps/client/src/features/group/components/group-list.tsx +++ b/apps/client/src/features/group/components/group-list.tsx @@ -1,8 +1,8 @@ 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"; +import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx"; export default function GroupList() { const { data, isLoading } = useGetGroupsQuery(); @@ -33,7 +33,7 @@ export default function GroupList() { to={`/settings/groups/${group.id}`} > - +
{group.name} diff --git a/apps/client/src/features/search/queries/search-query.ts b/apps/client/src/features/search/queries/search-query.ts index 2b9bafa..338b799 100644 --- a/apps/client/src/features/search/queries/search-query.ts +++ b/apps/client/src/features/search/queries/search-query.ts @@ -1,11 +1,30 @@ -import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { searchPage } from '@/features/search/services/search-service'; -import { IPageSearch } from '@/features/search/types/search.types'; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; +import { + searchPage, + searchSuggestions, +} from "@/features/search/services/search-service"; +import { + IPageSearch, + ISuggestionResult, + SearchSuggestionParams, +} from "@/features/search/types/search.types"; -export function usePageSearchQuery(query: string): UseQueryResult { +export function usePageSearchQuery( + query: string, +): UseQueryResult { return useQuery({ - queryKey: ['page-history', query], + queryKey: ["page-search", query], queryFn: () => searchPage(query), enabled: !!query, }); } + +export function useSearchSuggestionsQuery( + params: SearchSuggestionParams, +): UseQueryResult { + return useQuery({ + queryKey: ["search-suggestion", params], + queryFn: () => searchSuggestions(params), + enabled: !!params.query, + }); +} diff --git a/apps/client/src/features/search/services/search-service.ts b/apps/client/src/features/search/services/search-service.ts index 2e75ec5..c44bf71 100644 --- a/apps/client/src/features/search/services/search-service.ts +++ b/apps/client/src/features/search/services/search-service.ts @@ -1,7 +1,18 @@ -import api from '@/lib/api-client'; -import { IPageSearch } from '@/features/search/types/search.types'; +import api from "@/lib/api-client"; +import { + IPageSearch, + ISuggestionResult, + SearchSuggestionParams, +} from "@/features/search/types/search.types"; export async function searchPage(query: string): Promise { - const req = await api.post('/search', { query }); - return req.data as any; + const req = await api.post("/search", { query }); + return req.data; +} + +export async function searchSuggestions( + params: SearchSuggestionParams, +): Promise { + const req = await api.post("/search/suggest", params); + return req.data; } diff --git a/apps/client/src/features/search/types/search.types.ts b/apps/client/src/features/search/types/search.types.ts index fc4f32e..ad1c903 100644 --- a/apps/client/src/features/search/types/search.types.ts +++ b/apps/client/src/features/search/types/search.types.ts @@ -1,3 +1,5 @@ +import { IUser } from "@/features/user/types/user.types.ts"; +import { IGroup } from "@/features/group/types/group.types.ts"; export interface IPageSearch { id: string; @@ -10,3 +12,14 @@ export interface IPageSearch { rank: string; highlight: string; } + +export interface SearchSuggestionParams { + query: string; + includeUsers?: boolean; + includeGroups?: boolean; +} + +export interface ISuggestionResult { + users?: Partial; + groups?: Partial; +} diff --git a/apps/client/src/features/space/components/add-space-members-modal.tsx b/apps/client/src/features/space/components/add-space-members-modal.tsx new file mode 100644 index 0000000..08847fe --- /dev/null +++ b/apps/client/src/features/space/components/add-space-members-modal.tsx @@ -0,0 +1,72 @@ +import { Button, Divider, Group, Modal, Stack } from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import React, { useState } from "react"; +import { useAddSpaceMemberMutation } from "@/features/space/queries/space-query.ts"; +import { MultiMemberSelect } from "@/features/space/components/multi-member-select.tsx"; +import { SpaceMemberRole } from "@/features/space/components/space-member-role.tsx"; +import { SpaceRole } from "@/lib/types.ts"; + +interface AddSpaceMemberModalProps { + spaceId: string; +} +export default function AddSpaceMembersModal({ + spaceId, +}: AddSpaceMemberModalProps) { + const [opened, { open, close }] = useDisclosure(false); + const [memberIds, setMemberIds] = useState([]); + const [role, setRole] = useState(SpaceRole.WRITER); + const addSpaceMemberMutation = useAddSpaceMemberMutation(); + + const handleMultiSelectChange = (value: string[]) => { + setMemberIds(value); + }; + + const handleRoleSelection = (role: string) => { + setRole(role); + }; + + const handleSubmit = async () => { + // member can be a users or groups + const userIds = memberIds + .map((id) => (id.startsWith("user-") ? id.split("user-")[1] : null)) + .filter((id) => id !== null); + + const groupIds = memberIds + .map((id) => (id.startsWith("group-") ? id.split("group-")[1] : null)) + .filter((id) => id !== null); + + const addSpaceMember = { + spaceId: spaceId, + userIds: userIds, + groupIds: groupIds, + role: role, + }; + + await addSpaceMemberMutation.mutateAsync(addSpaceMember); + close(); + }; + + return ( + <> + + + + + + + + + + + + + + + ); +} diff --git a/apps/client/src/features/space/components/edit-space-form.tsx b/apps/client/src/features/space/components/edit-space-form.tsx new file mode 100644 index 0000000..6cc684a --- /dev/null +++ b/apps/client/src/features/space/components/edit-space-form.tsx @@ -0,0 +1,78 @@ +import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core"; +import React from "react"; +import { useForm, zodResolver } from "@mantine/form"; +import * as z from "zod"; +import { useUpdateSpaceMutation } from "@/features/space/queries/space-query.ts"; +import { ISpace } from "@/features/space/types/space.types.ts"; + +const formSchema = z.object({ + name: z.string().min(2).max(50), + description: z.string().max(250), +}); + +type FormValues = z.infer; +interface EditSpaceFormProps { + space: ISpace; +} +export function EditSpaceForm({ space }: EditSpaceFormProps) { + const updateSpaceMutation = useUpdateSpaceMutation(); + + const form = useForm({ + validate: zodResolver(formSchema), + initialValues: { + name: space?.name, + description: space?.description || "", + }, + }); + + const handleSubmit = async (values: { + name?: string; + description?: string; + }) => { + const spaceData: Partial = { + spaceId: space.id, + }; + if (form.isDirty("name")) { + spaceData.name = values.name; + } + if (form.isDirty("description")) { + spaceData.description = values.description; + } + + await updateSpaceMutation.mutateAsync(spaceData); + form.resetDirty(); + }; + + return ( + <> + +
handleSubmit(values))}> + + + +