From 2b3e96b9eaa3f4bb38d685797dad8b7e14b9619f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Tue, 29 Aug 2023 10:03:56 +0200 Subject: [PATCH] fix: do not allow removal of last table view (#1366) Closes #1358 --- .../components/TableViewsDropdownButton.tsx | 17 ++++++---- .../src/core/view/resolvers/view.resolver.ts | 34 ++++++++++++++++++- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/front/src/modules/ui/table/options/components/TableViewsDropdownButton.tsx b/front/src/modules/ui/table/options/components/TableViewsDropdownButton.tsx index 20ec286b76..beed828e08 100644 --- a/front/src/modules/ui/table/options/components/TableViewsDropdownButton.tsx +++ b/front/src/modules/ui/table/options/components/TableViewsDropdownButton.tsx @@ -31,6 +31,7 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { assertNotNull } from '~/utils/assert'; import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; import { savedTableColumnsScopedState } from '../../states/savedTableColumnsScopedState'; @@ -184,12 +185,16 @@ export const TableViewsDropdownButton = ({ onClick={(event) => handleEditViewButtonClick(event, view.id)} icon={} />, - handleDeleteViewButtonClick(event, view.id)} - icon={} - />, - ]} + views.length > 1 ? ( + + handleDeleteViewButtonClick(event, view.id) + } + icon={} + /> + ) : null, + ].filter(assertNotNull)} onClick={() => handleViewSelect(view.id)} > diff --git a/server/src/core/view/resolvers/view.resolver.ts b/server/src/core/view/resolvers/view.resolver.ts index fd96564fff..ab439ad71e 100644 --- a/server/src/core/view/resolvers/view.resolver.ts +++ b/server/src/core/view/resolvers/view.resolver.ts @@ -1,4 +1,8 @@ -import { UseGuards } from '@nestjs/common'; +import { + BadRequestException, + ForbiddenException, + UseGuards, +} from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { accessibleBy } from '@casl/prisma'; @@ -94,7 +98,35 @@ export class ViewResolver { @CheckAbilities(DeleteViewAbilityHandler) async deleteManyView( @Args() args: DeleteManyViewArgs, + @AuthWorkspace() workspace: Workspace, ): Promise { + const viewsToDelete = await this.viewService.findMany({ + where: args.where, + }); + + if (!viewsToDelete.length) return { count: 0 }; + + const { objectId } = viewsToDelete[0]; + + if (viewsToDelete.some((view) => view.objectId !== objectId)) { + throw new BadRequestException( + `Views must have the same objectId '${objectId}'`, + ); + } + + const viewsNb = await this.viewService.count({ + where: { + objectId: { equals: objectId }, + workspaceId: { equals: workspace.id }, + }, + }); + + if (viewsNb - viewsToDelete.length <= 0) { + throw new ForbiddenException( + `Deleting last '${objectId}' view is not allowed`, + ); + } + return this.viewService.deleteMany({ where: args.where, });