From e04f0e8e93fd1a6ca34b770b16899d807401c1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs=20Guigon?= Date: Wed, 30 Aug 2023 11:41:38 +0200 Subject: [PATCH] refactor: create/update/delete one view instead of many Closes #1359 --- front/src/generated/graphql.tsx | 156 +++++++++++------- .../views/graphql/mutations/createView.ts | 10 ++ .../views/graphql/mutations/createViews.ts | 9 - .../views/graphql/mutations/deleteView.ts | 10 ++ .../views/graphql/mutations/deleteViews.ts | 9 - .../views/graphql/mutations/updateView.ts | 2 +- front/src/modules/views/hooks/useViews.ts | 84 ++++------ .../src/core/view/resolvers/view.resolver.ts | 70 +++++++- 8 files changed, 218 insertions(+), 132 deletions(-) create mode 100644 front/src/modules/views/graphql/mutations/createView.ts delete mode 100644 front/src/modules/views/graphql/mutations/createViews.ts create mode 100644 front/src/modules/views/graphql/mutations/deleteView.ts delete mode 100644 front/src/modules/views/graphql/mutations/deleteViews.ts diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index ec4bde229617..9a61fff772ae 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -1004,6 +1004,7 @@ export type Mutation = { createOneCompany: Company; createOnePerson: Person; createOnePipelineProgress: PipelineProgress; + createOneView: View; createOneViewField: ViewField; deleteCurrentWorkspace: Workspace; deleteFavorite: Favorite; @@ -1014,6 +1015,7 @@ export type Mutation = { deleteManyView: AffectedRows; deleteManyViewFilter: AffectedRows; deleteManyViewSort: AffectedRows; + deleteOneView: View; deleteUserAccount: User; deleteWorkspaceMember: WorkspaceMember; impersonate: Verify; @@ -1128,6 +1130,11 @@ export type MutationCreateOnePipelineProgressArgs = { }; +export type MutationCreateOneViewArgs = { + data: ViewCreateInput; +}; + + export type MutationCreateOneViewFieldArgs = { data: ViewFieldCreateInput; }; @@ -1173,6 +1180,11 @@ export type MutationDeleteManyViewSortArgs = { }; +export type MutationDeleteOneViewArgs = { + where: ViewWhereUniqueInput; +}; + + export type MutationDeleteWorkspaceMemberArgs = { where: WorkspaceMemberWhereUniqueInput; }; @@ -2423,6 +2435,16 @@ export type View = { type: ViewType; }; +export type ViewCreateInput = { + fields?: InputMaybe; + filters?: InputMaybe; + id?: InputMaybe; + name: Scalars['String']; + objectId: Scalars['String']; + sorts?: InputMaybe; + type: ViewType; +}; + export type ViewCreateManyInput = { id?: InputMaybe; name: Scalars['String']; @@ -2466,6 +2488,10 @@ export type ViewFieldCreateManyInput = { viewId?: InputMaybe; }; +export type ViewFieldCreateNestedManyWithoutViewInput = { + connect?: InputMaybe>; +}; + export type ViewFieldListRelationFilter = { every?: InputMaybe; none?: InputMaybe; @@ -2565,6 +2591,10 @@ export type ViewFilterCreateManyInput = { viewId: Scalars['String']; }; +export type ViewFilterCreateNestedManyWithoutViewInput = { + connect?: InputMaybe>; +}; + export type ViewFilterListRelationFilter = { every?: InputMaybe; none?: InputMaybe; @@ -2686,6 +2716,10 @@ export type ViewSortCreateManyInput = { viewId: Scalars['String']; }; +export type ViewSortCreateNestedManyWithoutViewInput = { + connect?: InputMaybe>; +}; + export enum ViewSortDirection { Asc = 'asc', Desc = 'desc' @@ -3380,6 +3414,13 @@ export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>; export type GetUsersQuery = { __typename?: 'Query', findManyUser: Array<{ __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null }> }; +export type CreateViewMutationVariables = Exact<{ + data: ViewCreateInput; +}>; + + +export type CreateViewMutation = { __typename?: 'Mutation', view: { __typename?: 'View', id: string, name: string } }; + export type CreateViewFieldsMutationVariables = Exact<{ data: Array | ViewFieldCreateManyInput; }>; @@ -3401,12 +3442,12 @@ export type CreateViewSortsMutationVariables = Exact<{ export type CreateViewSortsMutation = { __typename?: 'Mutation', createManyViewSort: { __typename?: 'AffectedRows', count: number } }; -export type CreateViewsMutationVariables = Exact<{ - data: Array | ViewCreateManyInput; +export type DeleteViewMutationVariables = Exact<{ + where: ViewWhereUniqueInput; }>; -export type CreateViewsMutation = { __typename?: 'Mutation', createManyView: { __typename?: 'AffectedRows', count: number } }; +export type DeleteViewMutation = { __typename?: 'Mutation', view: { __typename?: 'View', id: string, name: string } }; export type DeleteViewFiltersMutationVariables = Exact<{ where: ViewFilterWhereInput; @@ -3422,20 +3463,13 @@ export type DeleteViewSortsMutationVariables = Exact<{ export type DeleteViewSortsMutation = { __typename?: 'Mutation', deleteManyViewSort: { __typename?: 'AffectedRows', count: number } }; -export type DeleteViewsMutationVariables = Exact<{ - where: ViewWhereInput; -}>; - - -export type DeleteViewsMutation = { __typename?: 'Mutation', deleteManyView: { __typename?: 'AffectedRows', count: number } }; - export type UpdateViewMutationVariables = Exact<{ data: ViewUpdateInput; where: ViewWhereUniqueInput; }>; -export type UpdateViewMutation = { __typename?: 'Mutation', updateOneView: { __typename?: 'View', id: string, name: string } }; +export type UpdateViewMutation = { __typename?: 'Mutation', view: { __typename?: 'View', id: string, name: string } }; export type UpdateViewFieldMutationVariables = Exact<{ data: ViewFieldUpdateInput; @@ -6108,6 +6142,40 @@ export function useGetUsersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions; export type GetUsersLazyQueryHookResult = ReturnType; export type GetUsersQueryResult = Apollo.QueryResult; +export const CreateViewDocument = gql` + mutation CreateView($data: ViewCreateInput!) { + view: createOneView(data: $data) { + id + name + } +} + `; +export type CreateViewMutationFn = Apollo.MutationFunction; + +/** + * __useCreateViewMutation__ + * + * To run a mutation, you first call `useCreateViewMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateViewMutation` 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 [createViewMutation, { data, loading, error }] = useCreateViewMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useCreateViewMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateViewDocument, options); + } +export type CreateViewMutationHookResult = ReturnType; +export type CreateViewMutationResult = Apollo.MutationResult; +export type CreateViewMutationOptions = Apollo.BaseMutationOptions; export const CreateViewFieldsDocument = gql` mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) { createManyViewField(data: $data) { @@ -6207,39 +6275,40 @@ export function useCreateViewSortsMutation(baseOptions?: Apollo.MutationHookOpti export type CreateViewSortsMutationHookResult = ReturnType; export type CreateViewSortsMutationResult = Apollo.MutationResult; export type CreateViewSortsMutationOptions = Apollo.BaseMutationOptions; -export const CreateViewsDocument = gql` - mutation CreateViews($data: [ViewCreateManyInput!]!) { - createManyView(data: $data) { - count +export const DeleteViewDocument = gql` + mutation DeleteView($where: ViewWhereUniqueInput!) { + view: deleteOneView(where: $where) { + id + name } } `; -export type CreateViewsMutationFn = Apollo.MutationFunction; +export type DeleteViewMutationFn = Apollo.MutationFunction; /** - * __useCreateViewsMutation__ + * __useDeleteViewMutation__ * - * To run a mutation, you first call `useCreateViewsMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useCreateViewsMutation` returns a tuple that includes: + * To run a mutation, you first call `useDeleteViewMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteViewMutation` 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 [createViewsMutation, { data, loading, error }] = useCreateViewsMutation({ + * const [deleteViewMutation, { data, loading, error }] = useDeleteViewMutation({ * variables: { - * data: // value for 'data' + * where: // value for 'where' * }, * }); */ -export function useCreateViewsMutation(baseOptions?: Apollo.MutationHookOptions) { +export function useDeleteViewMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(CreateViewsDocument, options); + return Apollo.useMutation(DeleteViewDocument, options); } -export type CreateViewsMutationHookResult = ReturnType; -export type CreateViewsMutationResult = Apollo.MutationResult; -export type CreateViewsMutationOptions = Apollo.BaseMutationOptions; +export type DeleteViewMutationHookResult = ReturnType; +export type DeleteViewMutationResult = Apollo.MutationResult; +export type DeleteViewMutationOptions = Apollo.BaseMutationOptions; export const DeleteViewFiltersDocument = gql` mutation DeleteViewFilters($where: ViewFilterWhereInput!) { deleteManyViewFilter(where: $where) { @@ -6306,42 +6375,9 @@ export function useDeleteViewSortsMutation(baseOptions?: Apollo.MutationHookOpti export type DeleteViewSortsMutationHookResult = ReturnType; export type DeleteViewSortsMutationResult = Apollo.MutationResult; export type DeleteViewSortsMutationOptions = Apollo.BaseMutationOptions; -export const DeleteViewsDocument = gql` - mutation DeleteViews($where: ViewWhereInput!) { - deleteManyView(where: $where) { - count - } -} - `; -export type DeleteViewsMutationFn = Apollo.MutationFunction; - -/** - * __useDeleteViewsMutation__ - * - * To run a mutation, you first call `useDeleteViewsMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useDeleteViewsMutation` 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 [deleteViewsMutation, { data, loading, error }] = useDeleteViewsMutation({ - * variables: { - * where: // value for 'where' - * }, - * }); - */ -export function useDeleteViewsMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(DeleteViewsDocument, options); - } -export type DeleteViewsMutationHookResult = ReturnType; -export type DeleteViewsMutationResult = Apollo.MutationResult; -export type DeleteViewsMutationOptions = Apollo.BaseMutationOptions; export const UpdateViewDocument = gql` mutation UpdateView($data: ViewUpdateInput!, $where: ViewWhereUniqueInput!) { - updateOneView(data: $data, where: $where) { + view: updateOneView(data: $data, where: $where) { id name } diff --git a/front/src/modules/views/graphql/mutations/createView.ts b/front/src/modules/views/graphql/mutations/createView.ts new file mode 100644 index 000000000000..cc23af983254 --- /dev/null +++ b/front/src/modules/views/graphql/mutations/createView.ts @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; + +export const CREATE_VIEW = gql` + mutation CreateView($data: ViewCreateInput!) { + view: createOneView(data: $data) { + id + name + } + } +`; diff --git a/front/src/modules/views/graphql/mutations/createViews.ts b/front/src/modules/views/graphql/mutations/createViews.ts deleted file mode 100644 index afdf46e29da3..000000000000 --- a/front/src/modules/views/graphql/mutations/createViews.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { gql } from '@apollo/client'; - -export const CREATE_VIEWS = gql` - mutation CreateViews($data: [ViewCreateManyInput!]!) { - createManyView(data: $data) { - count - } - } -`; diff --git a/front/src/modules/views/graphql/mutations/deleteView.ts b/front/src/modules/views/graphql/mutations/deleteView.ts new file mode 100644 index 000000000000..6daf72c06078 --- /dev/null +++ b/front/src/modules/views/graphql/mutations/deleteView.ts @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; + +export const DELETE_VIEW = gql` + mutation DeleteView($where: ViewWhereUniqueInput!) { + view: deleteOneView(where: $where) { + id + name + } + } +`; diff --git a/front/src/modules/views/graphql/mutations/deleteViews.ts b/front/src/modules/views/graphql/mutations/deleteViews.ts deleted file mode 100644 index 22f3c813a9ee..000000000000 --- a/front/src/modules/views/graphql/mutations/deleteViews.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { gql } from '@apollo/client'; - -export const DELETE_VIEWS = gql` - mutation DeleteViews($where: ViewWhereInput!) { - deleteManyView(where: $where) { - count - } - } -`; diff --git a/front/src/modules/views/graphql/mutations/updateView.ts b/front/src/modules/views/graphql/mutations/updateView.ts index d6a02d6a77b8..c6b73b46a908 100644 --- a/front/src/modules/views/graphql/mutations/updateView.ts +++ b/front/src/modules/views/graphql/mutations/updateView.ts @@ -2,7 +2,7 @@ import { gql } from '@apollo/client'; export const UPDATE_VIEW = gql` mutation UpdateView($data: ViewUpdateInput!, $where: ViewWhereUniqueInput!) { - updateOneView(data: $data, where: $where) { + view: updateOneView(data: $data, where: $where) { id name } diff --git a/front/src/modules/views/hooks/useViews.ts b/front/src/modules/views/hooks/useViews.ts index 358312e7cd23..8ab2ead172ae 100644 --- a/front/src/modules/views/hooks/useViews.ts +++ b/front/src/modules/views/hooks/useViews.ts @@ -10,8 +10,8 @@ import { import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { - useCreateViewsMutation, - useDeleteViewsMutation, + useCreateViewMutation, + useDeleteViewMutation, useGetViewsQuery, useUpdateViewMutation, ViewType, @@ -38,60 +38,42 @@ export const useViews = ({ TableRecoilScopeContext, ); - const [createViewsMutation] = useCreateViewsMutation(); + const [createViewMutation] = useCreateViewMutation(); const [updateViewMutation] = useUpdateViewMutation(); - const [deleteViewsMutation] = useDeleteViewsMutation(); + const [deleteViewMutation] = useDeleteViewMutation(); - const createViews = useCallback( - async (views: TableView[]) => { - if (!views.length) return; - - await createViewsMutation({ + const createView = useCallback( + async (view: TableView) => { + const { data } = await createViewMutation({ variables: { - data: views.map((view) => ({ + data: { ...view, objectId, type: ViewType.Table, - })), + }, }, }); - await Promise.all(views.map((view) => onViewCreate(view.id))); + if (data?.view) await onViewCreate(data.view.id); }, - [createViewsMutation, objectId, onViewCreate], + [createViewMutation, objectId, onViewCreate], ); - const updateViews = useCallback( - (views: TableView[]) => { - if (!views.length) return; - - return Promise.all( - views.map((view) => - updateViewMutation({ - variables: { - data: { name: view.name }, - where: { id: view.id }, - }, - }), - ), - ); - }, + const updateView = useCallback( + (view: TableView) => + updateViewMutation({ + variables: { + data: { name: view.name }, + where: { id: view.id }, + }, + }), [updateViewMutation], ); - const deleteViews = useCallback( - (viewIds: string[]) => { - if (!viewIds.length) return; - - return deleteViewsMutation({ - variables: { - where: { - id: { in: viewIds }, - }, - }, - }); - }, - [deleteViewsMutation], + const deleteView = useCallback( + (viewId: string) => + deleteViewMutation({ variables: { where: { id: viewId } } }), + [deleteViewMutation], ); const { refetch } = useGetViewsQuery({ @@ -114,27 +96,33 @@ export const useViews = ({ const handleViewsChange = useCallback( async (nextViews: TableView[]) => { - const viewsToCreate = nextViews.filter( + const viewToCreate = nextViews.find( (nextView) => !viewsById[nextView.id], ); - await createViews(viewsToCreate); + if (viewToCreate) { + await createView(viewToCreate); + return refetch(); + } - const viewsToUpdate = nextViews.filter( + const viewToUpdate = nextViews.find( (nextView) => viewsById[nextView.id] && viewsById[nextView.id].name !== nextView.name, ); - await updateViews(viewsToUpdate); + if (viewToUpdate) { + await updateView(viewToUpdate); + return refetch(); + } const nextViewIds = nextViews.map((nextView) => nextView.id); - const viewIdsToDelete = Object.keys(viewsById).filter( + const viewIdToDelete = Object.keys(viewsById).find( (previousViewId) => !nextViewIds.includes(previousViewId), ); - await deleteViews(viewIdsToDelete); + if (viewIdToDelete) await deleteView(viewIdToDelete); return refetch(); }, - [createViews, deleteViews, refetch, updateViews, viewsById], + [createView, deleteView, refetch, updateView, viewsById], ); return { handleViewsChange }; diff --git a/server/src/core/view/resolvers/view.resolver.ts b/server/src/core/view/resolvers/view.resolver.ts index ab439ad71e11..ef2d10f93cbf 100644 --- a/server/src/core/view/resolvers/view.resolver.ts +++ b/server/src/core/view/resolvers/view.resolver.ts @@ -1,6 +1,7 @@ import { BadRequestException, ForbiddenException, + NotFoundException, UseGuards, } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; @@ -15,9 +16,16 @@ import { ReadViewAbilityHandler, UpdateViewAbilityHandler, } from 'src/ability/handlers/view.ability-handler'; +import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output'; +import { CreateManyViewArgs } from 'src/core/@generated/view/create-many-view.args'; +import { CreateOneViewArgs } from 'src/core/@generated/view/create-one-view.args'; +import { DeleteManyViewArgs } from 'src/core/@generated/view/delete-many-view.args'; +import { DeleteOneViewArgs } from 'src/core/@generated/view/delete-one-view.args'; import { FindManyViewArgs } from 'src/core/@generated/view/find-many-view.args'; +import { UpdateOneViewArgs } from 'src/core/@generated/view/update-one-view.args'; import { View } from 'src/core/@generated/view/view.model'; import { ViewService } from 'src/core/view/services/view.service'; +import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; import { PrismaSelect, @@ -26,17 +34,32 @@ import { import { UserAbility } from 'src/decorators/user-ability.decorator'; import { AbilityGuard } from 'src/guards/ability.guard'; import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; -import { UpdateOneViewArgs } from 'src/core/@generated/view/update-one-view.args'; -import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; -import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output'; -import { CreateManyViewArgs } from 'src/core/@generated/view/create-many-view.args'; -import { DeleteManyViewArgs } from 'src/core/@generated/view/delete-many-view.args'; @UseGuards(JwtAuthGuard) @Resolver(() => View) export class ViewResolver { constructor(private readonly viewService: ViewService) {} + @Mutation(() => View, { + nullable: false, + }) + @UseGuards(AbilityGuard) + @CheckAbilities(CreateViewAbilityHandler) + async createOneView( + @Args() args: CreateOneViewArgs, + @AuthWorkspace() workspace: Workspace, + @PrismaSelector({ modelName: 'View' }) + prismaSelect: PrismaSelect<'View'>, + ): Promise> { + return this.viewService.create({ + data: { + ...args.data, + workspace: { connect: { id: workspace.id } }, + }, + select: prismaSelect.value, + } as Prisma.ViewCreateArgs); + } + @Mutation(() => AffectedRows) @UseGuards(AbilityGuard) @CheckAbilities(CreateViewAbilityHandler) @@ -91,6 +114,43 @@ export class ViewResolver { } as Prisma.ViewUpdateArgs); } + @Mutation(() => View, { + nullable: false, + }) + @UseGuards(AbilityGuard) + @CheckAbilities(DeleteViewAbilityHandler) + async deleteOneView( + @Args() args: DeleteOneViewArgs, + @AuthWorkspace() workspace: Workspace, + ): Promise { + const viewToDelete = await this.viewService.findUnique({ + where: args.where, + }); + + if (!viewToDelete) { + throw new NotFoundException(); + } + + const { objectId } = viewToDelete; + + const viewsNb = await this.viewService.count({ + where: { + objectId: { equals: objectId }, + workspaceId: { equals: workspace.id }, + }, + }); + + if (viewsNb <= 1) { + throw new ForbiddenException( + `Deleting last '${objectId}' view is not allowed`, + ); + } + + return this.viewService.delete({ + where: args.where, + }); + } + @Mutation(() => AffectedRows, { nullable: false, })