From 96dac5fbf604ebe460fc704684e98f3cc495b83f Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Sun, 25 Sep 2022 15:16:42 +0300 Subject: [PATCH] update: members permissions UIs --- .../ProjectCardMini.Skeleton.tsx | 2 +- .../FormContainer/FormContainer.tsx | 109 ++++++++++++------ .../FormContainer/updateProjectContext.tsx | 31 +++++ .../Components/TeamMembersInput/MemberRow.tsx | 56 +++++++++ .../TeamMembersInput/TeamMembersInput.tsx | 64 +++++----- .../Components/TeamTab/TeamTab.tsx | 5 +- .../pages/ListProjectPage/ListProjectPage.tsx | 10 ++ .../ProjectDetailsCard/ProjectDetailsCard.tsx | 7 +- src/utils/routing/routes.ts | 6 + 9 files changed, 212 insertions(+), 78 deletions(-) create mode 100644 src/features/Projects/pages/ListProjectPage/Components/FormContainer/updateProjectContext.tsx create mode 100644 src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/MemberRow.tsx diff --git a/src/features/Projects/Components/ProjectCardMini/ProjectCardMini.Skeleton.tsx b/src/features/Projects/Components/ProjectCardMini/ProjectCardMini.Skeleton.tsx index 0fe212d..d4824d8 100644 --- a/src/features/Projects/Components/ProjectCardMini/ProjectCardMini.Skeleton.tsx +++ b/src/features/Projects/Components/ProjectCardMini/ProjectCardMini.Skeleton.tsx @@ -3,7 +3,7 @@ import Skeleton from 'react-loading-skeleton' export default function ProjectCardMiniSkeleton() { return ( -
+

diff --git a/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx index a80cc57..8d69855 100644 --- a/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx +++ b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx @@ -1,7 +1,7 @@ import { FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form" import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; -import { IsValidProjectHashtagDocument, ProjectLaunchStatusEnum, Team_Member_Role, UpdateProjectInput, useProjectDetailsQuery } from "src/graphql"; +import { IsValidProjectHashtagDocument, ProjectDetailsQuery, ProjectLaunchStatusEnum, ProjectPermissionEnum, Team_Member_Role, UpdateProjectInput, useProjectDetailsQuery } from "src/graphql"; import { PropsWithChildren } from "react"; import { useSearchParams } from "react-router-dom"; import { usePrompt } from "src/utils/hooks"; @@ -9,6 +9,10 @@ import { imageSchema } from "src/utils/validation"; import { Override } from "src/utils/interfaces"; import LoadingPage from "src/Components/LoadingPage/LoadingPage"; import { apolloClient } from "src/utils/apollo"; +import { store } from "src/redux/store"; +import UpdateProjectContextProvider from './updateProjectContext' +import { useNavigate } from 'react-router-dom' +import { createRoute } from 'src/utils/routing' interface Props { @@ -16,13 +20,7 @@ interface Props { } export type IListProjectForm = Override + members: NestedValue capabilities: NestedValue recruit_roles: NestedValue tournaments: NestedValue @@ -30,6 +28,14 @@ export type IListProjectForm = Override }> +export type ProjectMember = { + id: number, + name: string, + jobTitle: string | null, + avatar: string, + role: Team_Member_Role, +} + const schema: yup.SchemaOf = yup.object({ id: yup.number().optional(), title: yup.string().trim().required().min(2), @@ -85,7 +91,7 @@ export default function FormContainer(props: PropsWithChildren) { const id = Number(params.get('id')); const isUpdating = !Number.isNaN(id); - + const navigate = useNavigate() const methods = useForm({ @@ -103,7 +109,7 @@ export default function FormContainer(props: PropsWithChildren) { category_id: undefined, capabilities: [], screenshots: [], - members: [], + members: prepareMembers([]), recruit_roles: [], launch_status: ProjectLaunchStatusEnum.Wip, tournaments: [], @@ -120,30 +126,33 @@ export default function FormContainer(props: PropsWithChildren) { onCompleted: (res) => { if (res.getProject) { const data = res.getProject - methods.reset({ - id: data.id, - title: data.title, - cover_image: { url: data.cover_image }, - thumbnail_image: { url: data.thumbnail_image }, - tagline: data.tagline, - website: data.website, - description: data.description, - hashtag: data.hashtag, - twitter: data.twitter, - discord: data.discord, - slack: data.slack, - telegram: data.telegram, - github: data.github, - category_id: data.category.id, - capabilities: data.capabilities.map(c => c.id), - screenshots: data.screenshots.map(url => ({ url })), + if (!res.getProject.permissions.includes(ProjectPermissionEnum.UpdateInfo)) + navigate({ pathname: createRoute({ type: "projects-page" }) }) + else + methods.reset({ + id: data.id, + title: data.title, + cover_image: { url: data.cover_image }, + thumbnail_image: { url: data.thumbnail_image }, + tagline: data.tagline, + website: data.website, + description: data.description, + hashtag: data.hashtag, + twitter: data.twitter, + discord: data.discord, + slack: data.slack, + telegram: data.telegram, + github: data.github, + category_id: data.category.id, + capabilities: data.capabilities.map(c => c.id), + screenshots: data.screenshots.map(url => ({ url })), - members: data.members.map(({ role, user }) => ({ role, id: user.id, avatar: user.avatar, name: user.name, jobTitle: user.jobTitle })), - recruit_roles: data.recruit_roles.map(r => r.id), + members: prepareMembers(data.members), + recruit_roles: data.recruit_roles.map(r => r.id), - tournaments: [], - launch_status: data.launch_status, - }) + tournaments: [], + launch_status: data.launch_status, + }) } } }) @@ -159,9 +168,39 @@ export default function FormContainer(props: PropsWithChildren) { return ( -
- {props.children} -
+ +
+ {props.children} +
+
) } + + +function prepareMembers(members: ProjectDetailsQuery['getProject']['members']): ProjectMember[] { + + const me = store.getState().user.me; + + if (!me) { + window.location.href = '/login'; + return []; + } + + if (members.length === 0) + return [{ + id: me.id, + avatar: me.avatar, + name: me.name, + jobTitle: me.jobTitle, + role: Team_Member_Role.Owner, + }] + + const _members = members.map(({ role, user }) => ({ role, id: user.id, avatar: user.avatar, name: user.name, jobTitle: user.jobTitle })) + + const myMember = _members.find(m => m.id === me.id); + + if (!myMember) throw new Error("Not a member of the project") + + return [myMember, ..._members.filter(m => m.id !== me.id)] +} \ No newline at end of file diff --git a/src/features/Projects/pages/ListProjectPage/Components/FormContainer/updateProjectContext.tsx b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/updateProjectContext.tsx new file mode 100644 index 0000000..08144b9 --- /dev/null +++ b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/updateProjectContext.tsx @@ -0,0 +1,31 @@ +import React, { PropsWithChildren } from 'react' +import { ProjectDetailsQuery } from 'src/graphql' + +interface Props { + permissions: ProjectDetailsQuery['getProject']['permissions'] +} + +interface State { + permissions: ProjectDetailsQuery['getProject']['permissions'] +} + +const context = React.createContext(undefined) + +const UpdateProjectContextProvider = React.memo(function (props: PropsWithChildren) { + + return ( + + {props.children} + + ) +}) + +export default UpdateProjectContextProvider; + +export const useUpdateProjectContext = () => { + const res = React.useContext(context); + + if (!res) throw new Error("No context provider was found for useUpdateProjectContext") + + return res; +} diff --git a/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/MemberRow.tsx b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/MemberRow.tsx new file mode 100644 index 0000000..b606cc9 --- /dev/null +++ b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/MemberRow.tsx @@ -0,0 +1,56 @@ +import { Menu, MenuButton, MenuItem } from '@szhsin/react-menu'; +import { ComponentProps } from 'react' +import { NestedValue } from 'react-hook-form' +import { FaChevronDown, FaRegTrashAlt, } from 'react-icons/fa'; +import UsersInput from 'src/Components/Inputs/UsersInput/UsersInput' +import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'; +import { Team_Member_Role } from 'src/graphql'; +import { Value } from './TeamMembersInput' + +interface Props { + user: Value[number] + onRemove: () => void; + onUpdateRole: (role: Team_Member_Role) => void + disabled?: boolean; + canUpdateRole?: boolean; + canDelete?: boolean; + +} + +export default function MemberRow({ user, onRemove, onUpdateRole, disabled, canUpdateRole, canDelete }: Props) { + return ( +
+ + +
+

+ {user.name} +

+

+ {user.jobTitle} +

+
+ +
+ {canUpdateRole ? {user.role} } transition> + {[Team_Member_Role.Admin, Team_Member_Role.Maker].map(role => + onUpdateRole(role)} + key={role}>{role} + )} + + : + {user.role} + } + {canDelete && } +
+
+ ) +} diff --git a/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.tsx b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.tsx index 28f6f37..d4895ab 100644 --- a/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.tsx +++ b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.tsx @@ -1,13 +1,13 @@ -import { Menu, MenuButton, MenuItem } from '@szhsin/react-menu'; + import { ComponentProps } from 'react' import { NestedValue } from 'react-hook-form' -import { FaChevronDown, FaRegTrashAlt, } from 'react-icons/fa'; import UsersInput from 'src/Components/Inputs/UsersInput/UsersInput' -import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'; -import { Team_Member_Role } from 'src/graphql'; +import { ProjectPermissionEnum, Team_Member_Role } from 'src/graphql'; import { IListProjectForm } from '../FormContainer/FormContainer'; +import { useUpdateProjectContext } from '../FormContainer/updateProjectContext'; +import MemberRow from './MemberRow'; -type Value = IListProjectForm['members'] extends NestedValue ? U : never; +export type Value = IListProjectForm['members'] extends NestedValue ? U : never; type Props = { value: Value, @@ -16,6 +16,13 @@ type Props = { export default function TeamMembersInput({ value, onChange = () => { } }: Props) { + + const { permissions } = useUpdateProjectContext() + + const canAddNew = permissions.includes(ProjectPermissionEnum.UpdateAdmins) + const canUpdateMembers = permissions.includes(ProjectPermissionEnum.UpdateMembers) + const canUpdateAdmins = permissions.includes(ProjectPermissionEnum.UpdateAdmins) + const addMember: ComponentProps['onSelect'] = (user) => { if (value.some(u => u.id === user.id)) return; @@ -40,45 +47,30 @@ export default function TeamMembersInput({ value, onChange = () => { } }: Props) })) } - const removeMemeber = (id: number) => { + const removeMember = (id: number) => { onChange(value.filter(u => u.id !== id)); } return ( <> - + {canAddNew && } {value.length > 0 &&
- {value.map(user => { - return
+ {value.map(member => { - -
-

- {user.name} -

-

- {user.jobTitle} -

-
-
- {user.role} } transition> - {Object.values(Team_Member_Role).map(role => - setMemberRole(user.id, role)} - key={role}>{role} - )} - - -
-
+ let canEdit = false; + + if (member.role === Team_Member_Role.Admin) canEdit = canUpdateAdmins; + if (member.role === Team_Member_Role.Maker) canEdit = canUpdateMembers; + + return removeMember(member.id)} + onUpdateRole={role => setMemberRole(member.id, role)} + /> })}
} diff --git a/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.tsx b/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.tsx index d94a888..d11470c 100644 --- a/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.tsx +++ b/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.tsx @@ -8,7 +8,7 @@ import { IListProjectForm } from "../FormContainer/FormContainer"; interface Props { } -export default function TeamTab(props: Props) { +export default function TeamTab() { const { formState: { errors, }, control } = useFormContext(); @@ -16,8 +16,6 @@ export default function TeamTab(props: Props) { return (
- -

⚡️ Team

Let us know who is on this product’s team.

@@ -29,6 +27,7 @@ export default function TeamTab(props: Props) { )} /> diff --git a/src/features/Projects/pages/ListProjectPage/ListProjectPage.tsx b/src/features/Projects/pages/ListProjectPage/ListProjectPage.tsx index 9f70496..0129d15 100644 --- a/src/features/Projects/pages/ListProjectPage/ListProjectPage.tsx +++ b/src/features/Projects/pages/ListProjectPage/ListProjectPage.tsx @@ -2,12 +2,15 @@ import { useCarousel, useMediaQuery, } from "src/utils/hooks"; import { Helmet } from 'react-helmet' import { MEDIA_QUERIES } from "src/utils/theme"; import Card from "src/Components/Card/Card"; +import LoadingPage from "src/Components/LoadingPage/LoadingPage"; import ProjectDetailsTab from "./Components/ProjectDetailsTab/ProjectDetailsTab"; import TeamTab from "./Components/TeamTab/TeamTab"; import ExtrasTab from "./Components/ExtrasTab/ExtrasTab"; import FormContainer from "./Components/FormContainer/FormContainer"; import { useState } from "react"; import SaveChangesCard from "./Components/SaveChangesCard/SaveChangesCard"; +import { useMeQuery } from "src/graphql"; +import { Navigate, useLocation } from 'react-router-dom' export const tabs = { @@ -38,8 +41,15 @@ export default function ListProjectPage() { align: 'start', slidesToScroll: 2, containScroll: "trimSnaps", }) + const location = useLocation() + const meQuery = useMeQuery({ + }); + + if (meQuery.loading) return + + if (meQuery.error || !meQuery.data?.me) return return ( <> diff --git a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.tsx b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.tsx index 5e14a66..a70ec37 100644 --- a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.tsx +++ b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.tsx @@ -146,9 +146,10 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
-
- -
+ {project.permissions.includes(ProjectPermissionEnum.UpdateInfo) && +
+ +
} {/* Title & Basic Info */}
diff --git a/src/utils/routing/routes.ts b/src/utils/routing/routes.ts index 329bdfe..91b514d 100644 --- a/src/utils/routing/routes.ts +++ b/src/utils/routing/routes.ts @@ -35,6 +35,9 @@ type RouteOptions = id: string | number, username?: string, } + | { + type: "projects-page" + } | { type: "edit-project", id?: number, @@ -82,6 +85,9 @@ export function createRoute(options: RouteOptions) { if (options.type === 'tournament') return `/tournaments/${options.id}` + (options.tab ? `/${options.tab}` : "") + if (options.type === 'projects-page') + return '/projects' + if (options.type === 'edit-project') return `/projects/list-project` + (options.id ? `?id=${options.id}` : '')