From 0490c6b6ead301389f80f55d409748ae48bf76a0 Mon Sep 17 00:00:00 2001 From: Aditya Pimpalkar Date: Thu, 10 Aug 2023 23:24:45 +0100 Subject: [PATCH] feat: Favorites (#1094) * Adding the favorite button * favorites services and resolvers * favorites schema * favorite ability handler * favorite module export * front end UI * front end graphql additions * server ability handlers * server resolvers and services * css fix * Adding the favorite button * favorites services and resolvers * favorites schema * favorite ability handler * favorite module export * front end UI * front end graphql additions * server ability handlers * server resolvers and services * css fix * delete favorites handler and resolver * removed favorite from index list * chip avatar size props * index list additions * UI additions for favorites functionality * lint fixes * graphql codegen * UI fixes * favorite hook addition * moved to ~/modules * Favorite mapping to workspaceMember * graphql codegen * cosmetic changes * camel cased methods * graphql codegen --- front/src/AppNavbar.tsx | 3 + front/src/generated/graphql.tsx | 305 +++++++++++++++++- front/src/modules/companies/queries/show.ts | 9 + .../favorites/components/Favorites.tsx | 63 ++++ .../modules/favorites/hooks/useFavorites.ts | 84 +++++ front/src/modules/favorites/queries/show.ts | 25 ++ front/src/modules/favorites/queries/update.ts | 36 +++ front/src/modules/people/queries/show.ts | 9 + .../ui/button/components/IconButton.tsx | 13 +- front/src/modules/ui/icon/index.ts | 1 + .../layout/components/WithTopBarContainer.tsx | 6 + .../ui/layout/page-bar/components/PageBar.tsx | 43 ++- front/src/pages/companies/CompanyShow.tsx | 16 +- front/src/pages/people/PersonShow.tsx | 11 + server/src/ability/ability.factory.ts | 8 + server/src/ability/ability.module.ts | 13 + .../handlers/favorite.ability-handler.ts | 74 +++++ server/src/core/core.module.ts | 3 + server/src/core/favorite/favorite.module.ts | 10 + .../favorite/resolvers/favorite.resolver.ts | 143 ++++++++ .../favorite/services/favorite.service.ts | 39 +++ server/src/database/schema.prisma | 23 +- .../utils/prisma-select/model-select-map.ts | 1 + 23 files changed, 917 insertions(+), 21 deletions(-) create mode 100644 front/src/modules/favorites/components/Favorites.tsx create mode 100644 front/src/modules/favorites/hooks/useFavorites.ts create mode 100644 front/src/modules/favorites/queries/show.ts create mode 100644 front/src/modules/favorites/queries/update.ts create mode 100644 server/src/ability/handlers/favorite.ability-handler.ts create mode 100644 server/src/core/favorite/favorite.module.ts create mode 100644 server/src/core/favorite/resolvers/favorite.resolver.ts create mode 100644 server/src/core/favorite/services/favorite.service.ts diff --git a/front/src/AppNavbar.tsx b/front/src/AppNavbar.tsx index 2d273f6a1..976d689e5 100644 --- a/front/src/AppNavbar.tsx +++ b/front/src/AppNavbar.tsx @@ -2,6 +2,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { useTheme } from '@emotion/react'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { Favorites } from '@/favorites/components/Favorites'; import { SettingsNavbar } from '@/settings/components/SettingsNavbar'; import { IconBell, @@ -56,6 +57,8 @@ export function AppNavbar() { active={currentPath === '/tasks'} icon={} /> + + >; + Favorite?: Maybe>; PipelineProgress?: Maybe>; _activityCount: Scalars['Int']; accountOwner?: Maybe; @@ -659,6 +660,7 @@ export type Company = { export type CompanyCreateInput = { ActivityTarget?: InputMaybe; + Favorite?: InputMaybe; PipelineProgress?: InputMaybe; accountOwner?: InputMaybe; address: Scalars['String']; @@ -696,6 +698,7 @@ export type CompanyOrderByRelationAggregateInput = { export type CompanyOrderByWithRelationInput = { ActivityTarget?: InputMaybe; + Favorite?: InputMaybe; PipelineProgress?: InputMaybe; accountOwner?: InputMaybe; accountOwnerId?: InputMaybe; @@ -731,6 +734,7 @@ export enum CompanyScalarFieldEnum { export type CompanyUpdateInput = { ActivityTarget?: InputMaybe; + Favorite?: InputMaybe; PipelineProgress?: InputMaybe; accountOwner?: InputMaybe; address?: InputMaybe; @@ -769,6 +773,7 @@ export type CompanyUpdateOneWithoutPipelineProgressNestedInput = { export type CompanyWhereInput = { AND?: InputMaybe>; ActivityTarget?: InputMaybe; + Favorite?: InputMaybe; NOT?: InputMaybe>; OR?: InputMaybe>; PipelineProgress?: InputMaybe; @@ -860,6 +865,71 @@ export type EnumViewTypeFilter = { notIn?: InputMaybe>; }; +export type Favorite = { + __typename?: 'Favorite'; + company?: Maybe; + companyId?: Maybe; + id: Scalars['ID']; + person?: Maybe; + personId?: Maybe; + workspaceId?: Maybe; + workspaceMember?: Maybe; + workspaceMemberId?: Maybe; +}; + +export type FavoriteCreateNestedManyWithoutCompanyInput = { + connect?: InputMaybe>; +}; + +export type FavoriteCreateNestedManyWithoutPersonInput = { + connect?: InputMaybe>; +}; + +export type FavoriteListRelationFilter = { + every?: InputMaybe; + none?: InputMaybe; + some?: InputMaybe; +}; + +export type FavoriteMutationForCompanyArgs = { + companyId: Scalars['String']; +}; + +export type FavoriteMutationForPersonArgs = { + personId: Scalars['String']; +}; + +export type FavoriteOrderByRelationAggregateInput = { + _count?: InputMaybe; +}; + +export type FavoriteUpdateManyWithoutCompanyNestedInput = { + connect?: InputMaybe>; + disconnect?: InputMaybe>; + set?: InputMaybe>; +}; + +export type FavoriteUpdateManyWithoutPersonNestedInput = { + connect?: InputMaybe>; + disconnect?: InputMaybe>; + set?: InputMaybe>; +}; + +export type FavoriteWhereInput = { + AND?: InputMaybe>; + NOT?: InputMaybe>; + OR?: InputMaybe>; + companyId?: InputMaybe; + id?: InputMaybe; + personId?: InputMaybe; + workspaceId?: InputMaybe; + workspaceMemberId?: InputMaybe; +}; + +export type FavoriteWhereUniqueInput = { + id?: InputMaybe; +}; + export enum FileFolder { Attachment = 'Attachment', PersonPicture = 'PersonPicture', @@ -915,6 +985,8 @@ export type Mutation = { allowImpersonation: WorkspaceMember; challenge: LoginToken; createEvent: Analytics; + createFavoriteForCompany: Favorite; + createFavoriteForPerson: Favorite; createManyViewField: AffectedRows; createManyViewSort: AffectedRows; createOneActivity: Activity; @@ -924,6 +996,7 @@ export type Mutation = { createOnePipelineProgress: PipelineProgress; createOneViewField: ViewField; deleteCurrentWorkspace: Workspace; + deleteFavorite: Favorite; deleteManyActivities: AffectedRows; deleteManyCompany: AffectedRows; deleteManyPerson: AffectedRows; @@ -970,6 +1043,16 @@ export type MutationCreateEventArgs = { }; +export type MutationCreateFavoriteForCompanyArgs = { + data: FavoriteMutationForCompanyArgs; +}; + + +export type MutationCreateFavoriteForPersonArgs = { + data: FavoriteMutationForPersonArgs; +}; + + export type MutationCreateManyViewFieldArgs = { data: Array; skipDuplicates?: InputMaybe; @@ -1012,6 +1095,11 @@ export type MutationCreateOneViewFieldArgs = { }; +export type MutationDeleteFavoriteArgs = { + where: FavoriteWhereInput; +}; + + export type MutationDeleteManyActivitiesArgs = { where?: InputMaybe; }; @@ -1279,6 +1367,7 @@ export type NestedStringNullableFilter = { export type Person = { __typename?: 'Person'; ActivityTarget?: Maybe>; + Favorite?: Maybe>; PipelineProgress?: Maybe>; _activityCount: Scalars['Int']; activities: Array; @@ -1303,6 +1392,7 @@ export type Person = { export type PersonCreateInput = { ActivityTarget?: InputMaybe; + Favorite?: InputMaybe; PipelineProgress?: InputMaybe; avatarUrl?: InputMaybe; city?: InputMaybe; @@ -1348,6 +1438,7 @@ export type PersonOrderByRelationAggregateInput = { export type PersonOrderByWithRelationInput = { ActivityTarget?: InputMaybe; + Favorite?: InputMaybe; PipelineProgress?: InputMaybe; avatarUrl?: InputMaybe; city?: InputMaybe; @@ -1391,6 +1482,7 @@ export enum PersonScalarFieldEnum { export type PersonUpdateInput = { ActivityTarget?: InputMaybe; + Favorite?: InputMaybe; PipelineProgress?: InputMaybe; avatarUrl?: InputMaybe; city?: InputMaybe; @@ -1433,6 +1525,7 @@ export type PersonUpdateOneWithoutPipelineProgressNestedInput = { export type PersonWhereInput = { AND?: InputMaybe>; ActivityTarget?: InputMaybe; + Favorite?: InputMaybe; NOT?: InputMaybe>; OR?: InputMaybe>; PipelineProgress?: InputMaybe; @@ -1806,6 +1899,7 @@ export type Query = { clientConfig: ClientConfig; currentUser: User; currentWorkspace: Workspace; + findFavorites: Array; findManyActivities: Array; findManyCompany: Array; findManyPerson: Array; @@ -2507,6 +2601,7 @@ export type WorkspaceInviteHashValid = { export type WorkspaceMember = { __typename?: 'WorkspaceMember'; + Favorite?: Maybe>; allowImpersonation: Scalars['Boolean']; createdAt: Scalars['DateTime']; id: Scalars['ID']; @@ -2517,6 +2612,7 @@ export type WorkspaceMember = { }; export type WorkspaceMemberOrderByWithRelationInput = { + Favorite?: InputMaybe; allowImpersonation?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; @@ -2543,6 +2639,7 @@ export type WorkspaceMemberUpdateManyWithoutWorkspaceNestedInput = { export type WorkspaceMemberWhereInput = { AND?: InputMaybe>; + Favorite?: InputMaybe; NOT?: InputMaybe>; OR?: InputMaybe>; allowImpersonation?: InputMaybe; @@ -2734,7 +2831,7 @@ export type GetCompanyQueryVariables = Exact<{ }>; -export type GetCompanyQuery = { __typename?: 'Query', findUniqueCompany: { __typename?: 'Company', id: string, domainName: string, name: string, createdAt: string, address: string, linkedinUrl?: string | null, employees?: number | null, _activityCount: number, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, avatarUrl?: string | null } | null } }; +export type GetCompanyQuery = { __typename?: 'Query', findUniqueCompany: { __typename?: 'Company', id: string, domainName: string, name: string, createdAt: string, address: string, linkedinUrl?: string | null, employees?: number | null, _activityCount: number, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, avatarUrl?: string | null } | null, Favorite?: Array<{ __typename?: 'Favorite', id: string, person?: { __typename?: 'Person', id: string } | null, company?: { __typename?: 'Company', id: string } | null }> | null } }; export type UpdateOneCompanyMutationVariables = Exact<{ where: CompanyWhereUniqueInput; @@ -2760,6 +2857,32 @@ export type DeleteManyCompaniesMutationVariables = Exact<{ export type DeleteManyCompaniesMutation = { __typename?: 'Mutation', deleteManyCompany: { __typename?: 'AffectedRows', count: number } }; +export type GetFavoritesQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetFavoritesQuery = { __typename?: 'Query', findFavorites: Array<{ __typename?: 'Favorite', id: string, person?: { __typename?: 'Person', id: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } | null, company?: { __typename?: 'Company', id: string, name: string, domainName: string, accountOwner?: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } | null } | null }> }; + +export type InsertPersonFavoriteMutationVariables = Exact<{ + data: FavoriteMutationForPersonArgs; +}>; + + +export type InsertPersonFavoriteMutation = { __typename?: 'Mutation', createFavoriteForPerson: { __typename?: 'Favorite', id: string, person?: { __typename?: 'Person', id: string, firstName?: string | null, lastName?: string | null, displayName: string } | null } }; + +export type InsertCompanyFavoriteMutationVariables = Exact<{ + data: FavoriteMutationForCompanyArgs; +}>; + + +export type InsertCompanyFavoriteMutation = { __typename?: 'Mutation', createFavoriteForCompany: { __typename?: 'Favorite', id: string, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null } }; + +export type DeleteFavoriteMutationVariables = Exact<{ + where: FavoriteWhereInput; +}>; + + +export type DeleteFavoriteMutation = { __typename?: 'Mutation', deleteFavorite: { __typename?: 'Favorite', id: string } }; + export type GetPeopleQueryVariables = Exact<{ orderBy?: InputMaybe | PersonOrderByWithRelationInput>; where?: InputMaybe; @@ -2823,7 +2946,7 @@ export type GetPersonQueryVariables = Exact<{ }>; -export type GetPersonQuery = { __typename?: 'Query', findUniquePerson: { __typename?: 'Person', id: string, firstName?: string | null, lastName?: string | null, displayName: string, email?: string | null, createdAt: string, city?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, xUrl?: string | null, avatarUrl?: string | null, phone?: string | null, _activityCount: number, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null } }; +export type GetPersonQuery = { __typename?: 'Query', findUniquePerson: { __typename?: 'Person', id: string, firstName?: string | null, lastName?: string | null, displayName: string, email?: string | null, createdAt: string, city?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, xUrl?: string | null, avatarUrl?: string | null, phone?: string | null, _activityCount: number, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null, Favorite?: Array<{ __typename?: 'Favorite', id: string, person?: { __typename?: 'Person', id: string } | null, company?: { __typename?: 'Company', id: string } | null }> | null } }; export type UpdateOnePersonMutationVariables = Exact<{ where: PersonWhereUniqueInput; @@ -4097,6 +4220,15 @@ export const GetCompanyDocument = gql` displayName avatarUrl } + Favorite { + id + person { + id + } + company { + id + } + } } } `; @@ -4241,6 +4373,166 @@ export function useDeleteManyCompaniesMutation(baseOptions?: Apollo.MutationHook export type DeleteManyCompaniesMutationHookResult = ReturnType; export type DeleteManyCompaniesMutationResult = Apollo.MutationResult; export type DeleteManyCompaniesMutationOptions = Apollo.BaseMutationOptions; +export const GetFavoritesDocument = gql` + query GetFavorites { + findFavorites { + id + person { + id + firstName + lastName + avatarUrl + } + company { + id + name + domainName + accountOwner { + id + displayName + avatarUrl + } + } + } +} + `; + +/** + * __useGetFavoritesQuery__ + * + * To run a query within a React component, call `useGetFavoritesQuery` and pass it any options that fit your needs. + * When your component renders, `useGetFavoritesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetFavoritesQuery({ + * variables: { + * }, + * }); + */ +export function useGetFavoritesQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetFavoritesDocument, options); + } +export function useGetFavoritesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetFavoritesDocument, options); + } +export type GetFavoritesQueryHookResult = ReturnType; +export type GetFavoritesLazyQueryHookResult = ReturnType; +export type GetFavoritesQueryResult = Apollo.QueryResult; +export const InsertPersonFavoriteDocument = gql` + mutation InsertPersonFavorite($data: FavoriteMutationForPersonArgs!) { + createFavoriteForPerson(data: $data) { + id + person { + id + firstName + lastName + displayName + } + } +} + `; +export type InsertPersonFavoriteMutationFn = Apollo.MutationFunction; + +/** + * __useInsertPersonFavoriteMutation__ + * + * To run a mutation, you first call `useInsertPersonFavoriteMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useInsertPersonFavoriteMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [insertPersonFavoriteMutation, { data, loading, error }] = useInsertPersonFavoriteMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useInsertPersonFavoriteMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(InsertPersonFavoriteDocument, options); + } +export type InsertPersonFavoriteMutationHookResult = ReturnType; +export type InsertPersonFavoriteMutationResult = Apollo.MutationResult; +export type InsertPersonFavoriteMutationOptions = Apollo.BaseMutationOptions; +export const InsertCompanyFavoriteDocument = gql` + mutation InsertCompanyFavorite($data: FavoriteMutationForCompanyArgs!) { + createFavoriteForCompany(data: $data) { + id + company { + id + name + domainName + } + } +} + `; +export type InsertCompanyFavoriteMutationFn = Apollo.MutationFunction; + +/** + * __useInsertCompanyFavoriteMutation__ + * + * To run a mutation, you first call `useInsertCompanyFavoriteMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useInsertCompanyFavoriteMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [insertCompanyFavoriteMutation, { data, loading, error }] = useInsertCompanyFavoriteMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useInsertCompanyFavoriteMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(InsertCompanyFavoriteDocument, options); + } +export type InsertCompanyFavoriteMutationHookResult = ReturnType; +export type InsertCompanyFavoriteMutationResult = Apollo.MutationResult; +export type InsertCompanyFavoriteMutationOptions = Apollo.BaseMutationOptions; +export const DeleteFavoriteDocument = gql` + mutation DeleteFavorite($where: FavoriteWhereInput!) { + deleteFavorite(where: $where) { + id + } +} + `; +export type DeleteFavoriteMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteFavoriteMutation__ + * + * To run a mutation, you first call `useDeleteFavoriteMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteFavoriteMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [deleteFavoriteMutation, { data, loading, error }] = useDeleteFavoriteMutation({ + * variables: { + * where: // value for 'where' + * }, + * }); + */ +export function useDeleteFavoriteMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteFavoriteDocument, options); + } +export type DeleteFavoriteMutationHookResult = ReturnType; +export type DeleteFavoriteMutationResult = Apollo.MutationResult; +export type DeleteFavoriteMutationOptions = Apollo.BaseMutationOptions; export const GetPeopleDocument = gql` query GetPeople($orderBy: [PersonOrderByWithRelationInput!], $where: PersonWhereInput, $limit: Int) { people: findManyPerson(orderBy: $orderBy, where: $where, take: $limit) { @@ -4575,6 +4867,15 @@ export const GetPersonDocument = gql` name domainName } + Favorite { + id + person { + id + } + company { + id + } + } } } `; diff --git a/front/src/modules/companies/queries/show.ts b/front/src/modules/companies/queries/show.ts index 73fc25a12..ebc4d63ab 100644 --- a/front/src/modules/companies/queries/show.ts +++ b/front/src/modules/companies/queries/show.ts @@ -19,6 +19,15 @@ export const GET_COMPANY = gql` displayName avatarUrl } + Favorite { + id + person { + id + } + company { + id + } + } } } `; diff --git a/front/src/modules/favorites/components/Favorites.tsx b/front/src/modules/favorites/components/Favorites.tsx new file mode 100644 index 000000000..b9beca2fc --- /dev/null +++ b/front/src/modules/favorites/components/Favorites.tsx @@ -0,0 +1,63 @@ +import styled from '@emotion/styled'; + +import NavItem from '@/ui/navbar/components/NavItem'; +import { Avatar } from '@/users/components/Avatar'; +import { useGetFavoritesQuery } from '~/generated/graphql'; +import { getLogoUrlFromDomainName } from '~/utils'; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + overflow-x: auto; + width: 100%; +`; + +export function Favorites() { + const { data } = useGetFavoritesQuery(); + const favorites = data?.findFavorites; + + if (!favorites) return <>; + + return ( + + {favorites && + favorites.map( + ({ id, person, company }) => + (person && ( + + } + to={`/person/${person.id}`} + /> + )) || + (company && ( + + } + to={`/companies/${company.id}`} + /> + )), + )} + + ); +} diff --git a/front/src/modules/favorites/hooks/useFavorites.ts b/front/src/modules/favorites/hooks/useFavorites.ts new file mode 100644 index 000000000..345150caa --- /dev/null +++ b/front/src/modules/favorites/hooks/useFavorites.ts @@ -0,0 +1,84 @@ +import { getOperationName } from '@apollo/client/utilities'; + +import { GET_COMPANY } from '@/companies/queries'; +import { GET_PERSON } from '@/people/queries/show'; +import { + useDeleteFavoriteMutation, + useInsertCompanyFavoriteMutation, + useInsertPersonFavoriteMutation, +} from '~/generated/graphql'; + +import { GET_FAVORITES } from '../queries/show'; + +export function useFavorites() { + const [insertCompanyFavoriteMutation] = useInsertCompanyFavoriteMutation(); + const [insertPersonFavoriteMutation] = useInsertPersonFavoriteMutation(); + const [deleteFavoriteMutation] = useDeleteFavoriteMutation(); + + function insertCompanyFavorite(companyId: string) { + insertCompanyFavoriteMutation({ + variables: { + data: { + companyId, + }, + }, + refetchQueries: [ + getOperationName(GET_FAVORITES) ?? '', + getOperationName(GET_COMPANY) ?? '', + ], + }); + } + + function insertPersonFavorite(personId: string) { + insertPersonFavoriteMutation({ + variables: { + data: { + personId, + }, + }, + refetchQueries: [ + getOperationName(GET_FAVORITES) ?? '', + getOperationName(GET_PERSON) ?? '', + ], + }); + } + + function deleteCompanyFavorite(companyId: string) { + deleteFavoriteMutation({ + variables: { + where: { + companyId: { + equals: companyId, + }, + }, + }, + refetchQueries: [ + getOperationName(GET_FAVORITES) ?? '', + getOperationName(GET_COMPANY) ?? '', + ], + }); + } + + function deletePersonFavorite(personId: string) { + deleteFavoriteMutation({ + variables: { + where: { + personId: { + equals: personId, + }, + }, + }, + refetchQueries: [ + getOperationName(GET_FAVORITES) ?? '', + getOperationName(GET_PERSON) ?? '', + ], + }); + } + + return { + insertCompanyFavorite, + insertPersonFavorite, + deleteCompanyFavorite, + deletePersonFavorite, + }; +} diff --git a/front/src/modules/favorites/queries/show.ts b/front/src/modules/favorites/queries/show.ts new file mode 100644 index 000000000..a41de5b08 --- /dev/null +++ b/front/src/modules/favorites/queries/show.ts @@ -0,0 +1,25 @@ +import { gql } from '@apollo/client'; + +export const GET_FAVORITES = gql` + query GetFavorites { + findFavorites { + id + person { + id + firstName + lastName + avatarUrl + } + company { + id + name + domainName + accountOwner { + id + displayName + avatarUrl + } + } + } + } +`; diff --git a/front/src/modules/favorites/queries/update.ts b/front/src/modules/favorites/queries/update.ts new file mode 100644 index 000000000..b9ab1a17e --- /dev/null +++ b/front/src/modules/favorites/queries/update.ts @@ -0,0 +1,36 @@ +import { gql } from '@apollo/client'; + +export const INSERT_PERSON_FAVORITE = gql` + mutation InsertPersonFavorite($data: FavoriteMutationForPersonArgs!) { + createFavoriteForPerson(data: $data) { + id + person { + id + firstName + lastName + displayName + } + } + } +`; + +export const INSERT_COMPANY_FAVORITE = gql` + mutation InsertCompanyFavorite($data: FavoriteMutationForCompanyArgs!) { + createFavoriteForCompany(data: $data) { + id + company { + id + name + domainName + } + } + } +`; + +export const DELETE_FAVORITE = gql` + mutation DeleteFavorite($where: FavoriteWhereInput!) { + deleteFavorite(where: $where) { + id + } + } +`; diff --git a/front/src/modules/people/queries/show.ts b/front/src/modules/people/queries/show.ts index 8e28589ed..26c1ae3c4 100644 --- a/front/src/modules/people/queries/show.ts +++ b/front/src/modules/people/queries/show.ts @@ -23,6 +23,15 @@ export const GET_PERSON = gql` name domainName } + Favorite { + id + person { + id + } + company { + id + } + } } } `; diff --git a/front/src/modules/ui/button/components/IconButton.tsx b/front/src/modules/ui/button/components/IconButton.tsx index 2a3a67b87..c09971589 100644 --- a/front/src/modules/ui/button/components/IconButton.tsx +++ b/front/src/modules/ui/button/components/IconButton.tsx @@ -7,15 +7,18 @@ export type IconButtonSize = 'large' | 'medium' | 'small'; export type IconButtonFontColor = 'primary' | 'secondary' | 'tertiary'; +export type IconButtonAccent = 'regular' | 'red'; + export type ButtonProps = { icon?: React.ReactNode; variant?: IconButtonVariant; size?: IconButtonSize; textColor?: IconButtonFontColor; + accent?: IconButtonAccent; } & React.ComponentProps<'button'>; const StyledIconButton = styled.button< - Pick + Pick >` align-items: center; background: ${({ theme, variant }) => { @@ -66,12 +69,14 @@ const StyledIconButton = styled.button< return 'none'; } }}; - color: ${({ theme, disabled, textColor }) => { + color: ${({ theme, disabled, textColor, accent }) => { if (disabled) { return theme.font.color.extraLight; } - return theme.font.color[textColor ?? 'secondary']; + return accent + ? theme.color[accent] + : theme.font.color[textColor ?? 'secondary']; }}; cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; display: flex; @@ -121,6 +126,7 @@ export function IconButton({ size = 'medium', textColor = 'tertiary', disabled = false, + accent = 'regular', ...props }: ButtonProps) { return ( @@ -129,6 +135,7 @@ export function IconButton({ size={size} disabled={disabled} textColor={textColor} + accent={accent} {...props} > {icon} diff --git a/front/src/modules/ui/icon/index.ts b/front/src/modules/ui/icon/index.ts index a8b930519..9a70bbae2 100644 --- a/front/src/modules/ui/icon/index.ts +++ b/front/src/modules/ui/icon/index.ts @@ -51,6 +51,7 @@ export { IconUserCircle } from '@tabler/icons-react'; export { IconCalendar } from '@tabler/icons-react'; export { IconPencil } from '@tabler/icons-react'; export { IconCircleDot } from '@tabler/icons-react'; +export { IconHeart } from '@tabler/icons-react'; export { IconBrandX } from '@tabler/icons-react'; export { IconTag } from '@tabler/icons-react'; export { IconHelpCircle } from '@tabler/icons-react'; diff --git a/front/src/modules/ui/layout/components/WithTopBarContainer.tsx b/front/src/modules/ui/layout/components/WithTopBarContainer.tsx index 0f1e5b5ec..df01c384a 100644 --- a/front/src/modules/ui/layout/components/WithTopBarContainer.tsx +++ b/front/src/modules/ui/layout/components/WithTopBarContainer.tsx @@ -10,8 +10,10 @@ type OwnProps = { children: JSX.Element | JSX.Element[]; title: string; hasBackButton?: boolean; + isFavorite?: boolean; icon: ReactNode; onAddButtonClick?: () => void; + onFavouriteButtonClick?: () => void; }; const StyledContainer = styled.div` @@ -24,8 +26,10 @@ export function WithTopBarContainer({ children, title, hasBackButton, + isFavorite, icon, onAddButtonClick, + onFavouriteButtonClick, }: OwnProps) { return ( @@ -33,8 +37,10 @@ export function WithTopBarContainer({ {children} diff --git a/front/src/modules/ui/layout/page-bar/components/PageBar.tsx b/front/src/modules/ui/layout/page-bar/components/PageBar.tsx index 94fd4f890..774d3514a 100644 --- a/front/src/modules/ui/layout/page-bar/components/PageBar.tsx +++ b/front/src/modules/ui/layout/page-bar/components/PageBar.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import { IconButton } from '@/ui/button/components/IconButton'; -import { IconChevronLeft, IconPlus } from '@/ui/icon/index'; +import { IconChevronLeft, IconHeart, IconPlus } from '@/ui/icon/index'; import NavCollapseButton from '@/ui/navbar/components/NavCollapseButton'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; @@ -58,18 +58,27 @@ const StyledTopBarIconTitleContainer = styled.div` width: 100%; `; +const ActionButtonsContainer = styled.div` + display: inline-flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + type OwnProps = { title: string; hasBackButton?: boolean; + isFavorite?: boolean; icon: ReactNode; onAddButtonClick?: () => void; + onFavouriteButtonClick?: () => void; }; export function PageBar({ title, hasBackButton, + isFavorite, icon, onAddButtonClick, + onFavouriteButtonClick, }: OwnProps) { const navigate = useNavigate(); const navigateBack = useCallback(() => navigate(-1), [navigate]); @@ -104,16 +113,28 @@ export function PageBar({ - {onAddButtonClick && ( - } - size="large" - data-testid="add-button" - textColor="secondary" - onClick={onAddButtonClick} - variant="border" - /> - )} + + {onFavouriteButtonClick && ( + } + size="large" + data-testid="add-button" + accent={isFavorite ? 'red' : 'regular'} + onClick={onFavouriteButtonClick} + variant="border" + /> + )} + {onAddButtonClick && ( + } + size="large" + data-testid="add-button" + textColor="secondary" + onClick={onAddButtonClick} + variant="border" + /> + )} + ); diff --git a/front/src/pages/companies/CompanyShow.tsx b/front/src/pages/companies/CompanyShow.tsx index 147c54207..e11a5d0bf 100644 --- a/front/src/pages/companies/CompanyShow.tsx +++ b/front/src/pages/companies/CompanyShow.tsx @@ -9,6 +9,7 @@ import { CompanyCreatedAtEditableField } from '@/companies/editable-field/compon import { CompanyDomainNameEditableField } from '@/companies/editable-field/components/CompanyDomainNameEditableField'; import { CompanyEmployeesEditableField } from '@/companies/editable-field/components/CompanyEmployeesEditableField'; import { useCompanyQuery } from '@/companies/queries'; +import { useFavorites } from '@/favorites/hooks/useFavorites'; import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox'; import { IconBuildingSkyscraper } from '@/ui/icon'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; @@ -23,19 +24,28 @@ import { ShowPageContainer } from '../../modules/ui/layout/components/ShowPageCo export function CompanyShow() { const companyId = useParams().companyId ?? ''; - - const { data } = useCompanyQuery(companyId); - const company = data?.findUniqueCompany; + const { insertCompanyFavorite, deleteCompanyFavorite } = useFavorites(); const theme = useTheme(); + const { data } = useCompanyQuery(companyId); + const company = data?.findUniqueCompany; + const isFavorite = + company?.Favorite && company?.Favorite?.length > 0 ? true : false; if (!company) return <>; + async function handleFavoriteButtonClick() { + if (isFavorite) deleteCompanyFavorite(companyId); + else insertCompanyFavorite(companyId); + } + return ( } + onFavouriteButtonClick={handleFavoriteButtonClick} > diff --git a/front/src/pages/people/PersonShow.tsx b/front/src/pages/people/PersonShow.tsx index 4ed2755e0..3dc1a1aef 100644 --- a/front/src/pages/people/PersonShow.tsx +++ b/front/src/pages/people/PersonShow.tsx @@ -3,6 +3,7 @@ import { getOperationName } from '@apollo/client/utilities'; import { useTheme } from '@emotion/react'; import { Timeline } from '@/activities/timeline/components/Timeline'; +import { useFavorites } from '@/favorites/hooks/useFavorites'; import { PersonPropertyBox } from '@/people/components/PersonPropertyBox'; import { GET_PERSON, usePersonQuery } from '@/people/queries'; import { IconUser } from '@/ui/icon'; @@ -20,9 +21,12 @@ import { ShowPageContainer } from '../../modules/ui/layout/components/ShowPageCo export function PersonShow() { const personId = useParams().personId ?? ''; + const { insertPersonFavorite, deletePersonFavorite } = useFavorites(); const { data } = usePersonQuery(personId); const person = data?.findUniquePerson; + const isFavorite = + person?.Favorite && person?.Favorite?.length > 0 ? true : false; const theme = useTheme(); const [uploadPicture] = useUploadPersonPictureMutation(); @@ -40,11 +44,18 @@ export function PersonShow() { }); } + async function handleFavoriteButtonClick() { + if (isFavorite) deletePersonFavorite(personId); + else insertPersonFavorite(personId); + } + return ( } hasBackButton + isFavorite={isFavorite} + onFavouriteButtonClick={handleFavoriteButtonClick} > diff --git a/server/src/ability/ability.factory.ts b/server/src/ability/ability.factory.ts index 0eb1ffe93..39d26b657 100644 --- a/server/src/ability/ability.factory.ts +++ b/server/src/ability/ability.factory.ts @@ -19,6 +19,7 @@ import { UserSettings, View, ViewField, + Favorite, ViewSort, } from '@prisma/client'; @@ -41,6 +42,7 @@ type SubjectsAbility = Subjects<{ UserSettings: UserSettings; View: View; ViewField: ViewField; + Favorite: Favorite; ViewSort: ViewSort; }>; @@ -143,6 +145,12 @@ export class AbilityFactory { can(AbilityAction.Read, 'ViewField', { workspaceId: workspace.id }); can(AbilityAction.Create, 'ViewField', { workspaceId: workspace.id }); can(AbilityAction.Update, 'ViewField', { workspaceId: workspace.id }); + //Favorite + can(AbilityAction.Read, 'Favorite', { workspaceId: workspace.id }); + can(AbilityAction.Create, 'Favorite'); + can(AbilityAction.Delete, 'Favorite', { + workspaceId: workspace.id, + }); // ViewSort can(AbilityAction.Read, 'ViewSort', { workspaceId: workspace.id }); diff --git a/server/src/ability/ability.module.ts b/server/src/ability/ability.module.ts index af7269992..f98f54cd2 100644 --- a/server/src/ability/ability.module.ts +++ b/server/src/ability/ability.module.ts @@ -99,6 +99,11 @@ import { ReadViewFieldAbilityHandler, UpdateViewFieldAbilityHandler, } from './handlers/view-field.ability-handler'; +import { + CreateFavoriteAbilityHandler, + ReadFavoriteAbilityHandler, + DeleteFavoriteAbilityHandler, +} from './handlers/favorite.ability-handler'; import { CreateViewSortAbilityHandler, ReadViewSortAbilityHandler, @@ -193,6 +198,10 @@ import { ReadViewFieldAbilityHandler, CreateViewFieldAbilityHandler, UpdateViewFieldAbilityHandler, + //Favorite + ReadFavoriteAbilityHandler, + CreateFavoriteAbilityHandler, + DeleteFavoriteAbilityHandler, // ViewSort ReadViewSortAbilityHandler, CreateViewSortAbilityHandler, @@ -283,6 +292,10 @@ import { ReadViewFieldAbilityHandler, CreateViewFieldAbilityHandler, UpdateViewFieldAbilityHandler, + //Favorite + ReadFavoriteAbilityHandler, + CreateFavoriteAbilityHandler, + DeleteFavoriteAbilityHandler, // ViewSort ReadViewSortAbilityHandler, CreateViewSortAbilityHandler, diff --git a/server/src/ability/handlers/favorite.ability-handler.ts b/server/src/ability/handlers/favorite.ability-handler.ts new file mode 100644 index 000000000..e989e4d36 --- /dev/null +++ b/server/src/ability/handlers/favorite.ability-handler.ts @@ -0,0 +1,74 @@ +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +import { subject } from '@casl/ability'; + +import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface'; + +import { PrismaService } from 'src/database/prisma.service'; +import { AbilityAction } from 'src/ability/ability.action'; +import { AppAbility } from 'src/ability/ability.factory'; +import { relationAbilityChecker } from 'src/ability/ability.util'; +import { FavoriteWhereInput } from 'src/core/@generated/favorite/favorite-where.input'; +import { assert } from 'src/utils/assert'; + +class FavoriteArgs { + where?: FavoriteWhereInput; +} + +@Injectable() +export class ManageFavoriteAbilityHandler implements IAbilityHandler { + async handle(ability: AppAbility) { + return ability.can(AbilityAction.Manage, 'Favorite'); + } +} + +@Injectable() +export class ReadFavoriteAbilityHandler implements IAbilityHandler { + handle(ability: AppAbility) { + return ability.can(AbilityAction.Read, 'Favorite'); + } +} + +@Injectable() +export class CreateFavoriteAbilityHandler implements IAbilityHandler { + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + + const allowed = await relationAbilityChecker( + 'Favorite', + ability, + this.prismaService.client, + args, + ); + + if (!allowed) { + return false; + } + + return ability.can(AbilityAction.Create, 'Favorite'); + } +} + +@Injectable() +export class DeleteFavoriteAbilityHandler implements IAbilityHandler { + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const favorite = await this.prismaService.client.favorite.findFirst({ + where: args.where, + }); + assert(favorite, '', NotFoundException); + + return ability.can(AbilityAction.Delete, subject('Favorite', favorite)); + } +} diff --git a/server/src/core/core.module.ts b/server/src/core/core.module.ts index 63636a3a9..c250f24de 100644 --- a/server/src/core/core.module.ts +++ b/server/src/core/core.module.ts @@ -13,6 +13,7 @@ import { ClientConfigModule } from './client-config/client-config.module'; import { AttachmentModule } from './attachment/attachment.module'; import { ActivityModule } from './activity/activity.module'; import { ViewModule } from './view/view.module'; +import { FavoriteModule } from './favorite/favorite.module'; @Module({ imports: [ @@ -29,6 +30,7 @@ import { ViewModule } from './view/view.module'; AttachmentModule, ActivityModule, ViewModule, + FavoriteModule, ], exports: [ AuthModule, @@ -40,6 +42,7 @@ import { ViewModule } from './view/view.module'; WorkspaceModule, AnalyticsModule, AttachmentModule, + FavoriteModule, ], }) export class CoreModule {} diff --git a/server/src/core/favorite/favorite.module.ts b/server/src/core/favorite/favorite.module.ts new file mode 100644 index 000000000..07e68561e --- /dev/null +++ b/server/src/core/favorite/favorite.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { FavoriteResolver } from './resolvers/favorite.resolver'; +import { FavoriteService } from './services/favorite.service'; + +@Module({ + providers: [FavoriteService, FavoriteResolver], + exports: [FavoriteService], +}) +export class FavoriteModule {} diff --git a/server/src/core/favorite/resolvers/favorite.resolver.ts b/server/src/core/favorite/resolvers/favorite.resolver.ts new file mode 100644 index 000000000..71955ae99 --- /dev/null +++ b/server/src/core/favorite/resolvers/favorite.resolver.ts @@ -0,0 +1,143 @@ +import { Resolver, Query, Args, Mutation } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; +import { InputType } from '@nestjs/graphql'; +import { Field } from '@nestjs/graphql'; + +import { Workspace } from '@prisma/client'; + +import { + PrismaSelect, + PrismaSelector, +} from 'src/decorators/prisma-select.decorator'; +import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; +import { Favorite } from 'src/core/@generated/favorite/favorite.model'; +import { AbilityGuard } from 'src/guards/ability.guard'; +import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; +import { + CreateFavoriteAbilityHandler, + DeleteFavoriteAbilityHandler, + ReadFavoriteAbilityHandler, +} from 'src/ability/handlers/favorite.ability-handler'; +import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; +import { FavoriteService } from 'src/core/favorite/services/favorite.service'; +import { FavoriteWhereInput } from 'src/core/@generated/favorite/favorite-where.input'; + +@InputType() +class FavoriteMutationForPersonArgs { + @Field(() => String) + personId: string; +} + +@InputType() +class FavoriteMutationForCompanyArgs { + @Field(() => String) + companyId: string; +} + +@UseGuards(JwtAuthGuard) +@Resolver(() => Favorite) +export class FavoriteResolver { + constructor(private readonly favoriteService: FavoriteService) {} + + @Query(() => [Favorite]) + @UseGuards(AbilityGuard) + @CheckAbilities(ReadFavoriteAbilityHandler) + async findFavorites( + @AuthWorkspace() workspace: Workspace, + ): Promise[]> { + const favorites = await this.favoriteService.findMany({ + where: { + workspaceId: workspace.id, + }, + include: { + person: true, + company: { + include: { + accountOwner: true, + }, + }, + }, + }); + + return favorites; + } + + @Mutation(() => Favorite, { + nullable: false, + }) + @UseGuards(AbilityGuard) + @CheckAbilities(CreateFavoriteAbilityHandler) + async createFavoriteForPerson( + @Args('data') args: FavoriteMutationForPersonArgs, + @AuthWorkspace() workspace: Workspace, + @PrismaSelector({ modelName: 'Favorite' }) + prismaSelect: PrismaSelect<'Favorite'>, + ): Promise> { + //To avoid duplicates we first fetch all favorites assinged by workspace + const favorite = await this.favoriteService.findFirst({ + where: { workspaceId: workspace.id, personId: args.personId }, + }); + + if (favorite) return favorite; + + return this.favoriteService.create({ + data: { + person: { + connect: { id: args.personId }, + }, + workspaceId: workspace.id, + }, + select: prismaSelect.value, + }); + } + + @Mutation(() => Favorite, { + nullable: false, + }) + @UseGuards(AbilityGuard) + @CheckAbilities(CreateFavoriteAbilityHandler) + async createFavoriteForCompany( + @Args('data') args: FavoriteMutationForCompanyArgs, + @AuthWorkspace() workspace: Workspace, + @PrismaSelector({ modelName: 'Favorite' }) + prismaSelect: PrismaSelect<'Favorite'>, + ): Promise> { + //To avoid duplicates we first fetch all favorites assinged by workspace + const favorite = await this.favoriteService.findFirst({ + where: { workspaceId: workspace.id, companyId: args.companyId }, + }); + + if (favorite) return favorite; + + return this.favoriteService.create({ + data: { + company: { + connect: { id: args.companyId }, + }, + workspaceId: workspace.id, + }, + select: prismaSelect.value, + }); + } + + @Mutation(() => Favorite, { + nullable: false, + }) + @UseGuards(AbilityGuard) + @CheckAbilities(DeleteFavoriteAbilityHandler) + async deleteFavorite( + @Args('where') args: FavoriteWhereInput, + @AuthWorkspace() workspace: Workspace, + @PrismaSelector({ modelName: 'Favorite' }) + prismaSelect: PrismaSelect<'Favorite'>, + ): Promise> { + const favorite = await this.favoriteService.findFirst({ + where: { ...args, workspaceId: workspace.id }, + }); + + return this.favoriteService.delete({ + where: { id: favorite?.id }, + select: prismaSelect.value, + }); + } +} diff --git a/server/src/core/favorite/services/favorite.service.ts b/server/src/core/favorite/services/favorite.service.ts new file mode 100644 index 000000000..e1a14d034 --- /dev/null +++ b/server/src/core/favorite/services/favorite.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; + +import { PrismaService } from 'src/database/prisma.service'; + +@Injectable() +export class FavoriteService { + constructor(private readonly prismaService: PrismaService) {} + + // Find + findFirst = this.prismaService.client.favorite.findFirst; + findFirstOrThrow = this.prismaService.client.favorite.findFirstOrThrow; + + findUnique = this.prismaService.client.favorite.findUnique; + findUniqueOrThrow = this.prismaService.client.favorite.findUniqueOrThrow; + + findMany = this.prismaService.client.favorite.findMany; + + // Create + create = this.prismaService.client.favorite.create; + createMany = this.prismaService.client.favorite.createMany; + + // Update + update = this.prismaService.client.favorite.update; + upsert = this.prismaService.client.favorite.upsert; + updateMany = this.prismaService.client.favorite.updateMany; + + // Delete + delete = this.prismaService.client.favorite.delete; + deleteMany = this.prismaService.client.favorite.deleteMany; + + // Aggregate + aggregate = this.prismaService.client.favorite.aggregate; + + // Count + count = this.prismaService.client.favorite.count; + + // GroupBy + groupBy = this.prismaService.client.favorite.groupBy; +} diff --git a/server/src/database/schema.prisma b/server/src/database/schema.prisma index 24a42be02..e80af3a6b 100644 --- a/server/src/database/schema.prisma +++ b/server/src/database/schema.prisma @@ -205,8 +205,9 @@ model WorkspaceMember { /// @TypeGraphQL.omit(input: true, output: true) deletedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Favorite Favorite[] @@map("workspace_members") } @@ -246,6 +247,7 @@ model Company { updatedAt DateTime @updatedAt ActivityTarget ActivityTarget[] PipelineProgress PipelineProgress[] + Favorite Favorite[] @@map("companies") } @@ -297,6 +299,7 @@ model Person { updatedAt DateTime @updatedAt ActivityTarget ActivityTarget[] PipelineProgress PipelineProgress[] + Favorite Favorite[] @@map("people") } @@ -559,6 +562,22 @@ model Attachment { @@map("attachments") } +model Favorite { + id String @id @default(uuid()) + workspaceId String? + /// @TypeGraphQL.omit(input: true, output: false) + person Person? @relation(fields: [personId], references: [id]) + personId String? + /// @TypeGraphQL.omit(input: true, output: false) + company Company? @relation(fields: [companyId], references: [id]) + companyId String? + /// @TypeGraphQL.omit(input: true, output: false) + workspaceMember WorkspaceMember? @relation(fields: [workspaceMemberId], references: [id]) + workspaceMemberId String? + + @@map("favorites") +} + enum ViewType { Table Pipeline diff --git a/server/src/utils/prisma-select/model-select-map.ts b/server/src/utils/prisma-select/model-select-map.ts index 052371e71..e92832ccb 100644 --- a/server/src/utils/prisma-select/model-select-map.ts +++ b/server/src/utils/prisma-select/model-select-map.ts @@ -16,6 +16,7 @@ export type ModelSelectMap = { PipelineStage: Prisma.PipelineStageSelect; PipelineProgress: Prisma.PipelineProgressSelect; Attachment: Prisma.AttachmentSelect; + Favorite: Prisma.FavoriteSelect; View: Prisma.ViewSelect; ViewSort: Prisma.ViewSortSelect; ViewField: Prisma.ViewFieldSelect;