feat: I can upload a photo on person show page (#1103)

* I can upload a photo on person show page

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>

* Add requested changes

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>

---------

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>
This commit is contained in:
gitstart-twenty
2023-08-10 02:29:10 +08:00
committed by GitHub
parent 1f4df67a89
commit b557766eb0
9 changed files with 205 additions and 11 deletions

View File

@ -848,6 +848,7 @@ export type EnumPipelineProgressableTypeFilter = {
export enum FileFolder {
Attachment = 'Attachment',
PersonPicture = 'PersonPicture',
ProfilePicture = 'ProfilePicture',
WorkspaceLogo = 'WorkspaceLogo'
}
@ -928,6 +929,7 @@ export type Mutation = {
uploadAttachment: Scalars['String'];
uploadFile: Scalars['String'];
uploadImage: Scalars['String'];
uploadPersonPicture: Scalars['String'];
uploadProfilePicture: Scalars['String'];
uploadWorkspaceLogo: Scalars['String'];
verify: Verify;
@ -1094,6 +1096,12 @@ export type MutationUploadImageArgs = {
};
export type MutationUploadPersonPictureArgs = {
file: Scalars['Upload'];
id: Scalars['String'];
};
export type MutationUploadProfilePictureArgs = {
file: Scalars['Upload'];
};
@ -2582,6 +2590,21 @@ export type DeleteManyPersonMutationVariables = Exact<{
export type DeleteManyPersonMutation = { __typename?: 'Mutation', deleteManyPerson: { __typename?: 'AffectedRows', count: number } };
export type UploadPersonPictureMutationVariables = Exact<{
id: Scalars['String'];
file: Scalars['Upload'];
}>;
export type UploadPersonPictureMutation = { __typename?: 'Mutation', uploadPersonPicture: string };
export type RemovePersonPictureMutationVariables = Exact<{
where: PersonWhereUniqueInput;
}>;
export type RemovePersonPictureMutation = { __typename?: 'Mutation', updateOnePerson?: { __typename?: 'Person', id: string, avatarUrl?: string | null } | null };
export type GetPipelinesQueryVariables = Exact<{
where?: InputMaybe<PipelineWhereInput>;
}>;
@ -4398,6 +4421,72 @@ export function useDeleteManyPersonMutation(baseOptions?: Apollo.MutationHookOpt
export type DeleteManyPersonMutationHookResult = ReturnType<typeof useDeleteManyPersonMutation>;
export type DeleteManyPersonMutationResult = Apollo.MutationResult<DeleteManyPersonMutation>;
export type DeleteManyPersonMutationOptions = Apollo.BaseMutationOptions<DeleteManyPersonMutation, DeleteManyPersonMutationVariables>;
export const UploadPersonPictureDocument = gql`
mutation UploadPersonPicture($id: String!, $file: Upload!) {
uploadPersonPicture(id: $id, file: $file)
}
`;
export type UploadPersonPictureMutationFn = Apollo.MutationFunction<UploadPersonPictureMutation, UploadPersonPictureMutationVariables>;
/**
* __useUploadPersonPictureMutation__
*
* To run a mutation, you first call `useUploadPersonPictureMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUploadPersonPictureMutation` 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 [uploadPersonPictureMutation, { data, loading, error }] = useUploadPersonPictureMutation({
* variables: {
* id: // value for 'id'
* file: // value for 'file'
* },
* });
*/
export function useUploadPersonPictureMutation(baseOptions?: Apollo.MutationHookOptions<UploadPersonPictureMutation, UploadPersonPictureMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UploadPersonPictureMutation, UploadPersonPictureMutationVariables>(UploadPersonPictureDocument, options);
}
export type UploadPersonPictureMutationHookResult = ReturnType<typeof useUploadPersonPictureMutation>;
export type UploadPersonPictureMutationResult = Apollo.MutationResult<UploadPersonPictureMutation>;
export type UploadPersonPictureMutationOptions = Apollo.BaseMutationOptions<UploadPersonPictureMutation, UploadPersonPictureMutationVariables>;
export const RemovePersonPictureDocument = gql`
mutation RemovePersonPicture($where: PersonWhereUniqueInput!) {
updateOnePerson(data: {avatarUrl: null}, where: $where) {
id
avatarUrl
}
}
`;
export type RemovePersonPictureMutationFn = Apollo.MutationFunction<RemovePersonPictureMutation, RemovePersonPictureMutationVariables>;
/**
* __useRemovePersonPictureMutation__
*
* To run a mutation, you first call `useRemovePersonPictureMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRemovePersonPictureMutation` 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 [removePersonPictureMutation, { data, loading, error }] = useRemovePersonPictureMutation({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useRemovePersonPictureMutation(baseOptions?: Apollo.MutationHookOptions<RemovePersonPictureMutation, RemovePersonPictureMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemovePersonPictureMutation, RemovePersonPictureMutationVariables>(RemovePersonPictureDocument, options);
}
export type RemovePersonPictureMutationHookResult = ReturnType<typeof useRemovePersonPictureMutation>;
export type RemovePersonPictureMutationResult = Apollo.MutationResult<RemovePersonPictureMutation>;
export type RemovePersonPictureMutationOptions = Apollo.BaseMutationOptions<RemovePersonPictureMutation, RemovePersonPictureMutationVariables>;
export const GetPipelinesDocument = gql`
query GetPipelines($where: PipelineWhereInput) {
findManyPipeline(where: $where) {

View File

@ -54,3 +54,18 @@ export const DELETE_MANY_PERSON = gql`
}
}
`;
export const UPDATE_PERSON_PICTURE = gql`
mutation UploadPersonPicture($id: String!, $file: Upload!) {
uploadPersonPicture(id: $id, file: $file)
}
`;
export const REMOVE_PERSON_PICTURE = gql`
mutation RemovePersonPicture($where: PersonWhereUniqueInput!) {
updateOnePerson(data: { avatarUrl: null }, where: $where) {
id
avatarUrl
}
}
`;

View File

@ -1,3 +1,4 @@
import { ChangeEvent, useRef } from 'react';
import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { v4 as uuidV4 } from 'uuid';
@ -16,6 +17,7 @@ type OwnProps = {
title: string;
date: string;
renderTitleEditComponent?: () => JSX.Element;
onUploadPicture?: (file: File) => void;
};
const StyledShowPageSummaryCard = styled.div`
@ -62,27 +64,52 @@ const StyledTooltip = styled(Tooltip)`
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledAvatarWrapper = styled.div`
cursor: pointer;
`;
const StyledFileInput = styled.input`
display: none;
`;
export function ShowPageSummaryCard({
id,
logoOrAvatar,
title,
date,
renderTitleEditComponent,
onUploadPicture,
}: OwnProps) {
const beautifiedCreatedAt =
date !== '' ? beautifyPastDateRelativeToNow(date) : '';
const exactCreatedAt = date !== '' ? beautifyExactDateTime(date) : '';
const dateElementId = `date-id-${uuidV4()}`;
const inputFileRef = useRef<HTMLInputElement>(null);
const onFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) onUploadPicture?.(e.target.files[0]);
};
const onAvatarClick = () => {
if (onUploadPicture) inputFileRef?.current?.click?.();
};
return (
<StyledShowPageSummaryCard>
<Avatar
avatarUrl={logoOrAvatar}
size="xl"
colorId={id}
placeholder={title}
type="rounded"
/>
<StyledAvatarWrapper onClick={onAvatarClick}>
<Avatar
avatarUrl={logoOrAvatar}
size="xl"
colorId={id}
placeholder={title}
type="rounded"
/>
<StyledFileInput
ref={inputFileRef}
onChange={onFileChange}
type="file"
/>
</StyledAvatarWrapper>
<StyledInfoContainer>
<StyledTitle>
{renderTitleEditComponent ? (

View File

@ -1,15 +1,19 @@
import { useParams } from 'react-router-dom';
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import { Timeline } from '@/activities/timeline/components/Timeline';
import { PersonPropertyBox } from '@/people/components/PersonPropertyBox';
import { usePersonQuery } from '@/people/queries';
import { GET_PERSON, usePersonQuery } from '@/people/queries';
import { IconUser } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { CommentableType } from '~/generated/graphql';
import {
CommentableType,
useUploadPersonPictureMutation,
} from '~/generated/graphql';
import { PeopleFullNameEditableField } from '../../modules/people/editable-field/components/PeopleFullNameEditableField';
import { ShowPageContainer } from '../../modules/ui/layout/components/ShowPageContainer';
@ -21,6 +25,20 @@ export function PersonShow() {
const person = data?.findUniquePerson;
const theme = useTheme();
const [uploadPicture] = useUploadPersonPictureMutation();
async function onUploadPicture(file: File) {
if (!file || !person?.id) {
return;
}
await uploadPicture({
variables: {
file,
id: person?.id,
},
refetchQueries: [getOperationName(GET_PERSON) ?? ''],
});
}
return (
<WithTopBarContainer
@ -38,6 +56,7 @@ export function PersonShow() {
renderTitleEditComponent={() =>
person ? <PeopleFullNameEditableField people={person} /> : <></>
}
onUploadPicture={onUploadPicture}
/>
{person && <PersonPropertyBox person={person} />}
</ShowPageLeftContainer>

View File

@ -5,6 +5,7 @@ export const settings: Settings = {
imageCropSizes: {
'profile-picture': ['original'],
'workspace-logo': ['original'],
'person-picture': ['original'],
},
maxFileSize: '10MB',
},

View File

@ -4,6 +4,7 @@ export enum FileFolder {
ProfilePicture = 'profile-picture',
WorkspaceLogo = 'workspace-logo',
Attachment = 'attachment',
PersonPicture = 'person-picture',
}
registerEnumType(FileFolder, {

View File

@ -2,13 +2,14 @@ import { Module } from '@nestjs/common';
import { CommentModule } from 'src/core/comment/comment.module';
import { ActivityModule } from 'src/core/activity/activity.module';
import { FileModule } from 'src/core/file/file.module';
import { PersonService } from './person.service';
import { PersonResolver } from './person.resolver';
import { PersonRelationsResolver } from './person-relations.resolver';
@Module({
imports: [CommentModule, ActivityModule],
imports: [CommentModule, ActivityModule, FileModule],
providers: [PersonService, PersonResolver, PersonRelationsResolver],
exports: [PersonService],
})

View File

@ -1,6 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AbilityFactory } from 'src/ability/ability.factory';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { PersonService } from './person.service';
import { PersonResolver } from './person.resolver';
@ -20,6 +21,10 @@ describe('PersonResolver', () => {
provide: AbilityFactory,
useValue: {},
},
{
provide: FileUploadService,
useValue: {},
},
],
}).compile();

View File

@ -10,6 +10,9 @@ import { UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { Prisma } from '@prisma/client';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Person } from 'src/core/@generated/person/person.model';
@ -34,13 +37,18 @@ import {
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { PersonService } from './person.service';
@UseGuards(JwtAuthGuard)
@Resolver(() => Person)
export class PersonResolver {
constructor(private readonly personService: PersonService) {}
constructor(
private readonly personService: PersonService,
private readonly fileUploadService: FileUploadService,
) {}
@Query(() => [Person], {
nullable: false,
@ -156,4 +164,32 @@ export class PersonResolver {
select: prismaSelect.value,
} as Prisma.PersonCreateArgs);
}
@Mutation(() => String)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdatePersonAbilityHandler)
async uploadPersonPicture(
@Args('id') id: string,
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename, mimetype }: FileUpload,
): Promise<string> {
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
const { paths } = await this.fileUploadService.uploadImage({
file: buffer,
filename,
mimeType: mimetype,
fileFolder: FileFolder.PersonPicture,
});
await this.personService.update({
where: { id },
data: {
avatarUrl: paths[0],
},
});
return paths[0];
}
}