From 90ae750d48bdfce8135d7d007021727d7313ae8c Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 12 Apr 2024 19:38:58 +0100 Subject: [PATCH] space updates * space UI * space management * space permissions * other fixes --- apps/client/src/App.tsx | 4 +- .../components/icons/icon-people-circle.tsx | 15 + .../layouts/settings/settings-sidebar.tsx | 5 - .../layouts/settings/settings.module.css | 3 +- apps/client/src/components/navbar/navbar.tsx | 41 ++- .../src/components/ui/role-select-menu.tsx | 63 ++++ .../features/group/components/group-list.tsx | 4 +- .../features/search/queries/search-query.ts | 29 +- .../search/services/search-service.ts | 19 +- .../src/features/search/types/search.types.ts | 13 + .../components/add-space-members-modal.tsx | 72 +++++ .../space/components/edit-space-form.tsx | 78 +++++ .../space/components/multi-member-select.tsx | 117 +++++++ .../space/components/settings-modal.tsx | 82 +++++ .../space/components/space-details.tsx | 24 ++ .../features/space/components/space-list.tsx | 67 ++++ .../space/components/space-member-role.tsx | 52 ++++ .../space/components/space-members.tsx | 176 +++++++++++ .../src/features/space/queries/space-query.ts | 139 ++++++++- .../features/space/services/space-service.ts | 50 ++- .../features/space/types/space-role-data.ts | 24 ++ .../src/features/space/types/space.types.ts | 38 +++ .../components/workspace-members-table.tsx | 38 ++- .../workspace/queries/workspace-query.ts | 26 +- .../workspace/services/workspace-service.ts | 15 +- .../workspace/types/user-role-data.ts | 24 ++ apps/client/src/lib/types.ts | 29 ++ .../src/pages/settings/space/spaces.tsx | 11 + .../src/core/auth/services/token.service.ts | 7 +- .../casl/abilities/casl-ability.factory.ts | 2 +- .../casl/abilities/space-ability.factory.ts | 68 +++++ apps/server/src/core/casl/casl.module.ts | 5 +- .../casl/interfaces/space-ability.type.ts | 15 + .../server/src/core/group/group.controller.ts | 3 +- apps/server/src/core/search/dto/search.dto.ts | 15 +- .../src/core/search/search.controller.ts | 24 +- apps/server/src/core/search/search.service.ts | 36 ++- .../core/space/dto/add-space-members.dto.ts | 31 ++ .../core/space/dto/remove-space-member.dto.ts | 14 + .../space/dto/update-space-member-role.dto.ts | 18 ++ .../src/core/space/dto/update-space.dto.ts | 8 +- .../space/services/space-member.service.ts | 254 +++++++++++++--- .../src/core/space/services/space.service.ts | 25 +- .../server/src/core/space/space.controller.ts | 145 +++++++-- .../src/core/user/dto/update-user.dto.ts | 6 +- apps/server/src/core/user/user.service.ts | 6 - .../workspace/services/workspace.service.ts | 2 +- apps/server/src/helpers/types/permission.ts | 4 +- .../migrations/20240324T085900-spaces.ts | 2 +- .../kysely/repos/space/space-member.repo.ts | 285 ++++++------------ .../src/kysely/repos/space/space.repo.ts | 53 ++-- apps/server/src/kysely/repos/space/types.ts | 7 +- apps/server/src/kysely/repos/space/utils.ts | 23 ++ apps/server/src/kysely/types/db.d.ts | 15 +- 54 files changed, 1966 insertions(+), 365 deletions(-) create mode 100644 apps/client/src/components/icons/icon-people-circle.tsx create mode 100644 apps/client/src/components/ui/role-select-menu.tsx create mode 100644 apps/client/src/features/space/components/add-space-members-modal.tsx create mode 100644 apps/client/src/features/space/components/edit-space-form.tsx create mode 100644 apps/client/src/features/space/components/multi-member-select.tsx create mode 100644 apps/client/src/features/space/components/settings-modal.tsx create mode 100644 apps/client/src/features/space/components/space-details.tsx create mode 100644 apps/client/src/features/space/components/space-list.tsx create mode 100644 apps/client/src/features/space/components/space-member-role.tsx create mode 100644 apps/client/src/features/space/components/space-members.tsx create mode 100644 apps/client/src/features/space/types/space-role-data.ts create mode 100644 apps/client/src/features/workspace/types/user-role-data.ts create mode 100644 apps/client/src/pages/settings/space/spaces.tsx create mode 100644 apps/server/src/core/casl/abilities/space-ability.factory.ts create mode 100644 apps/server/src/core/casl/interfaces/space-ability.type.ts create mode 100644 apps/server/src/core/space/dto/add-space-members.dto.ts create mode 100644 apps/server/src/core/space/dto/remove-space-member.dto.ts create mode 100644 apps/server/src/core/space/dto/update-space-member-role.dto.ts create mode 100644 apps/server/src/kysely/repos/space/utils.ts diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 339358c3..7fb5c59d 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 00000000..860bf348 --- /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 53570ac1..ac884978 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 30565115..aa4757a8 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 0facf07f..8087321c 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 00000000..c38700b5 --- /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 674db2c8..ae5d6471 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 2b9bafa8..338b799f 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 2e75ec53..c44bf713 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 fc4f32e5..ad1c903d 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 00000000..08847fe6 --- /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 00000000..6cc684af --- /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))}> + + + +