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:
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -5,6 +5,7 @@ export const settings: Settings = {
|
||||
imageCropSizes: {
|
||||
'profile-picture': ['original'],
|
||||
'workspace-logo': ['original'],
|
||||
'person-picture': ['original'],
|
||||
},
|
||||
maxFileSize: '10MB',
|
||||
},
|
||||
|
||||
@ -4,6 +4,7 @@ export enum FileFolder {
|
||||
ProfilePicture = 'profile-picture',
|
||||
WorkspaceLogo = 'workspace-logo',
|
||||
Attachment = 'attachment',
|
||||
PersonPicture = 'person-picture',
|
||||
}
|
||||
|
||||
registerEnumType(FileFolder, {
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user