From f7323f47c2fc4735159d27695e965003f8e386c3 Mon Sep 17 00:00:00 2001 From: Subina Date: Thu, 25 Jan 2024 12:11:54 +0545 Subject: [PATCH] Add pinned projects to home page --- app/gqlFragments.ts | 54 ++++++ app/views/Home/ProjectItem/index.tsx | 142 +++++++++++++- app/views/Home/index.tsx | 271 ++++++++++++++++----------- app/views/Home/styles.css | 6 + 4 files changed, 367 insertions(+), 106 deletions(-) diff --git a/app/gqlFragments.ts b/app/gqlFragments.ts index 51ed209006..9b1b1cba6e 100644 --- a/app/gqlFragments.ts +++ b/app/gqlFragments.ts @@ -292,6 +292,7 @@ export const LAST_ACTIVE_PROJECT_FRAGMENT = gql` hasAssessmentTemplate id isPrivate + isProjectPinned title isVisualizationEnabled isVisualizationAvailable @@ -328,3 +329,56 @@ export const BORDER_STYLE_FRAGMENT = gql` width } `; + +export const PROJECT_DETAIL_FRAGMENT = gql` + fragment ProjectDetail on ProjectDetailType { + id + title + isPrivate + isProjectPinned + description + startDate + endDate + analysisFramework { + id + title + } + createdBy { + displayName + } + leads { + totalCount + } + topTaggers { + userId + count + name + } + topSourcers { + userId + count + name + } + recentActiveUsers { + userId + name + date + } + stats { + entriesActivity { + count + date + } + leadsActivity { + count + date + } + numberOfEntries + numberOfLeads + numberOfLeadsTagged + numberOfLeadsInProgress + numberOfUsers + } + allowedPermissions + } +`; diff --git a/app/views/Home/ProjectItem/index.tsx b/app/views/Home/ProjectItem/index.tsx index 27fbc81ecd..5e3f2a46dc 100644 --- a/app/views/Home/ProjectItem/index.tsx +++ b/app/views/Home/ProjectItem/index.tsx @@ -3,11 +3,16 @@ import { FiEdit2 } from 'react-icons/fi'; import { _cs, compareDate, + isDefined, } from '@togglecorp/fujs'; import { IoBookmarkOutline, IoLockOpenOutline, } from 'react-icons/io5'; +import { + RiPushpinFill, + RiUnpinFill, +} from 'react-icons/ri'; import { AreaChart, XAxis, @@ -29,7 +34,10 @@ import { DateRangeOutput, Message, Kraken, + QuickActionButton, + useAlert, } from '@the-deep/deep-ui'; +import { useMutation, gql } from '@apollo/client'; import SmartButtonLikeLink from '#base/components/SmartButtonLikeLink'; import SmartQuickActionLink from '#base/components/SmartQuickActionLink'; @@ -42,12 +50,40 @@ import { UserEntityCountType, UserEntityDateType, ProjectPermission, + PinProjectMutation, + PinProjectMutationVariables, + UnpinProjectMutation, + UnpinProjectMutationVariables, } from '#generated/types'; import _ts from '#ts'; import styles from './styles.css'; +const PIN_PROJECT = gql` +mutation PinProject ($projectId: ID!) { + createUserPinnedProject( + data: { + project: $projectId + } + ) { + errors + ok + } +} +`; + +const UNPIN_PROJECT = gql` +mutation UnpinProject ($projectId: ID!) { + deleteUserPinnedProject ( + id: $projectId + ) { + errors + ok + } +} +`; + const tickFormatter = (value: number | string) => { const date = new Date(value); return date.toDateString(); @@ -66,7 +102,7 @@ const activeUserKeySelector = (d: UserEntityDateType) => d?.userId; export interface RecentProjectItemProps { className?: string; - projectId?: string; + projectId: string | undefined; title?: string; isPrivate?: boolean; startDate: string | null | undefined; @@ -84,6 +120,9 @@ export interface RecentProjectItemProps { topSourcers: UserEntityCountType[] | null | undefined; allowedPermissions: ProjectPermission[] | null | undefined; recentActiveUsers: UserEntityDateType[] | null | undefined; + isPinned?: boolean; + onProjectPinChange: () => void; + disablePinButton: boolean; } function ProjectItem(props: RecentProjectItemProps) { @@ -107,8 +146,71 @@ function ProjectItem(props: RecentProjectItemProps) { entriesActivity, allowedPermissions, recentActiveUsers, + isPinned, + onProjectPinChange, + disablePinButton, } = props; + const alert = useAlert(); + + const [ + pinProject, + ] = useMutation( + PIN_PROJECT, + { + onCompleted: (response) => { + const pinProjectResponse = response?.createUserPinnedProject; + if (pinProjectResponse?.ok) { + onProjectPinChange(); + alert.show( + 'Project successfully pinned.', + { variant: 'success' }, + ); + } else { + alert.show( + 'An error occured while pinning a project.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'An error occured while pinning a project.', + { variant: 'error' }, + ); + }, + }, + ); + + const [ + unpinProject, + ] = useMutation( + UNPIN_PROJECT, + { + onCompleted: (response) => { + const unpinProjectResponse = response?.deleteUserPinnedProject; + if (unpinProjectResponse?.ok) { + onProjectPinChange(); + alert.show( + 'Project successfully unpinned.', + { variant: 'success' }, + ); + } else { + alert.show( + 'An error occured while unpinning a project.', + { variant: 'error' }, + ); + } + }, + onError: () => { + alert.show( + 'An error occured while pinning a project.', + { variant: 'error' }, + ); + }, + }, + ); + const activeUserRendererParams = useCallback((_: unknown, data: UserEntityDateType) => ({ className: styles.recentlyActiveItem, label: data.name, @@ -146,6 +248,26 @@ function ProjectItem(props: RecentProjectItemProps) { const canEditProject = allowedPermissions?.includes('UPDATE_PROJECT'); + const handleUnpinProject = useCallback((id: string) => { + unpinProject({ + variables: { + projectId: id, + }, + }); + }, [ + unpinProject, + ]); + + const handlePinProject = useCallback((id: string) => { + pinProject({ + variables: { + projectId: id, + }, + }); + }, [ + pinProject, + ]); + return ( + {isDefined(projectId) && (isPinned ? ( + + + + ) : ( + + + + ))} ['project']; +type PinnedProjectDetailType = NonNullable['userPinnedProjects'][number]; const recentProjectKeySelector = (d: ProjectDetail) => d?.id ?? ''; +const pinnedProjectKeySelector = (d: PinnedProjectDetailType) => d.project?.id ?? ''; + +const MAX_PINNED_PROJECT_LIMIT = 5; interface ViewProps { className?: string; @@ -158,6 +92,10 @@ function Home(props: ViewProps) { const [projects, setProjects] = useState< Pick[] | undefined | null >(undefined); + const [ + pinButtonDisabled, + setPinButtonDisabled, + ] = useState(false); const { data: recentProjectsResponse, @@ -183,6 +121,20 @@ function Home(props: ViewProps) { }, ); + const { + data: pinnedProjectsResponse, + loading: pinnedProjectsPending, + refetch: retriggerPinnedProjectsList, + } = useQuery( + USER_PINNED_PROJECTS, + { + onCompleted: (response) => { + const count = response?.userPinnedProjects.length; + setPinButtonDisabled(count >= MAX_PINNED_PROJECT_LIMIT); + }, + }, + ); + const { response: summaryResponse, } = useRequest({ @@ -190,6 +142,28 @@ function Home(props: ViewProps) { method: 'GET', }); + const recentProjects: ProjectDetail[] | undefined = useMemo(() => { + /* + if (selectedProject && selectedProjectResponse?.project) { + return [selectedProjectResponse.project]; + } + */ + if (recentProjectsResponse?.recentProjects) { + return recentProjectsResponse.recentProjects; + } + return undefined; + }, [recentProjectsResponse]); + + const selectedProjectDetail: ProjectDetail | undefined = useMemo(() => { + if (selectedProject && selectedProjectResponse?.project) { + return selectedProjectResponse.project; + } + return undefined; + }, [ + selectedProject, + selectedProjectResponse, + ]); + const recentProjectsRendererParams = useCallback( (_: string, data: ProjectDetail): RecentProjectItemProps => ({ projectId: data?.id, @@ -210,19 +184,47 @@ function Home(props: ViewProps) { topTaggers: data?.topTaggers, topSourcers: data?.topSourcers, allowedPermissions: data?.allowedPermissions, + isPinned: data?.isProjectPinned, + onProjectPinChange: retriggerPinnedProjectsList, + disablePinButton: pinButtonDisabled, }), - [], + [ + retriggerPinnedProjectsList, + pinButtonDisabled, + ], ); - const recentProjects: ProjectDetail[] | undefined = useMemo(() => { - if (selectedProject && selectedProjectResponse?.project) { - return [selectedProjectResponse.project]; - } - if (recentProjectsResponse?.recentProjects) { - return recentProjectsResponse.recentProjects; - } - return undefined; - }, [selectedProject, selectedProjectResponse, recentProjectsResponse]); + const pinnedProjects = pinnedProjectsResponse?.userPinnedProjects; + + const pinnedProjectsRendererParams = useCallback( + (_: string, data: PinnedProjectDetailType): RecentProjectItemProps => ({ + projectId: data.project?.id, + title: data.project?.title, + isPrivate: data.project?.isPrivate, + startDate: data.project?.startDate, + endDate: data.project?.endDate, + description: data.project?.description, + projectOwnerName: data.project?.createdBy?.displayName, + analysisFrameworkTitle: data.project?.analysisFramework?.title, + analysisFramework: data.project?.analysisFramework?.id, + totalUsers: data.project?.stats?.numberOfUsers, + totalSources: data.project?.stats?.numberOfLeads, + totalSourcesInProgress: data.project?.stats?.numberOfLeadsInProgress, + totalSourcesTagged: data.project?.stats?.numberOfLeadsTagged, + entriesActivity: data.project?.stats?.entriesActivity, + recentActiveUsers: data.project?.recentActiveUsers, + topTaggers: data.project?.topTaggers, + topSourcers: data.project?.topSourcers, + allowedPermissions: data.project?.allowedPermissions, + isPinned: true, + onProjectPinChange: retriggerPinnedProjectsList, + disablePinButton: pinButtonDisabled, + }), + [ + retriggerPinnedProjectsList, + pinButtonDisabled, + ], + ); return ( )} + contentClassName={styles.content} > + {isDefined(selectedProject) && ( + + )} + + Pinned Projects + + + )} + emptyMessage={( +
+ {/* FIXME: use strings with appropriate wording */} + Looks like you do not have any recent project, +
+ please select a project to view it's details +
+ )} + messageIconShown + messageShown + /> + + Recent Projects +