feat: delete pipeline stage (#1412)

* feat: delete pipeline stage

Closes #1396

* refactor: code review

- Use string literal instead of enum

* docs: disable CircularProgressBar Chromatic snapshots
This commit is contained in:
Thaïs
2023-09-04 16:39:01 +02:00
committed by GitHub
parent 1a71f61d24
commit 96a0f30e98
11 changed files with 205 additions and 45 deletions

View File

@ -1016,6 +1016,7 @@ export type Mutation = {
deleteManyView: AffectedRows; deleteManyView: AffectedRows;
deleteManyViewFilter: AffectedRows; deleteManyViewFilter: AffectedRows;
deleteManyViewSort: AffectedRows; deleteManyViewSort: AffectedRows;
deleteOnePipelineStage: PipelineStage;
deleteOneView: View; deleteOneView: View;
deleteUserAccount: User; deleteUserAccount: User;
deleteWorkspaceMember: WorkspaceMember; deleteWorkspaceMember: WorkspaceMember;
@ -1186,6 +1187,11 @@ export type MutationDeleteManyViewSortArgs = {
}; };
export type MutationDeleteOnePipelineStageArgs = {
where: PipelineStageWhereUniqueInput;
};
export type MutationDeleteOneViewArgs = { export type MutationDeleteOneViewArgs = {
where: ViewWhereUniqueInput; where: ViewWhereUniqueInput;
}; };
@ -3325,6 +3331,13 @@ export type DeleteManyPipelineProgressMutationVariables = Exact<{
export type DeleteManyPipelineProgressMutation = { __typename?: 'Mutation', deleteManyPipelineProgress: { __typename?: 'AffectedRows', count: number } }; export type DeleteManyPipelineProgressMutation = { __typename?: 'Mutation', deleteManyPipelineProgress: { __typename?: 'AffectedRows', count: number } };
export type DeletePipelineStageMutationVariables = Exact<{
where: PipelineStageWhereUniqueInput;
}>;
export type DeletePipelineStageMutation = { __typename?: 'Mutation', pipelineStage: { __typename?: 'PipelineStage', id: string, name: string, color: string } };
export type UpdateOnePipelineProgressMutationVariables = Exact<{ export type UpdateOnePipelineProgressMutationVariables = Exact<{
data: PipelineProgressUpdateInput; data: PipelineProgressUpdateInput;
where: PipelineProgressWhereUniqueInput; where: PipelineProgressWhereUniqueInput;
@ -5554,6 +5567,41 @@ export function useDeleteManyPipelineProgressMutation(baseOptions?: Apollo.Mutat
export type DeleteManyPipelineProgressMutationHookResult = ReturnType<typeof useDeleteManyPipelineProgressMutation>; export type DeleteManyPipelineProgressMutationHookResult = ReturnType<typeof useDeleteManyPipelineProgressMutation>;
export type DeleteManyPipelineProgressMutationResult = Apollo.MutationResult<DeleteManyPipelineProgressMutation>; export type DeleteManyPipelineProgressMutationResult = Apollo.MutationResult<DeleteManyPipelineProgressMutation>;
export type DeleteManyPipelineProgressMutationOptions = Apollo.BaseMutationOptions<DeleteManyPipelineProgressMutation, DeleteManyPipelineProgressMutationVariables>; export type DeleteManyPipelineProgressMutationOptions = Apollo.BaseMutationOptions<DeleteManyPipelineProgressMutation, DeleteManyPipelineProgressMutationVariables>;
export const DeletePipelineStageDocument = gql`
mutation DeletePipelineStage($where: PipelineStageWhereUniqueInput!) {
pipelineStage: deleteOnePipelineStage(where: $where) {
id
name
color
}
}
`;
export type DeletePipelineStageMutationFn = Apollo.MutationFunction<DeletePipelineStageMutation, DeletePipelineStageMutationVariables>;
/**
* __useDeletePipelineStageMutation__
*
* To run a mutation, you first call `useDeletePipelineStageMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeletePipelineStageMutation` 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 [deletePipelineStageMutation, { data, loading, error }] = useDeletePipelineStageMutation({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useDeletePipelineStageMutation(baseOptions?: Apollo.MutationHookOptions<DeletePipelineStageMutation, DeletePipelineStageMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeletePipelineStageMutation, DeletePipelineStageMutationVariables>(DeletePipelineStageDocument, options);
}
export type DeletePipelineStageMutationHookResult = ReturnType<typeof useDeletePipelineStageMutation>;
export type DeletePipelineStageMutationResult = Apollo.MutationResult<DeletePipelineStageMutation>;
export type DeletePipelineStageMutationOptions = Apollo.BaseMutationOptions<DeletePipelineStageMutation, DeletePipelineStageMutationVariables>;
export const UpdateOnePipelineProgressDocument = gql` export const UpdateOnePipelineProgressDocument = gql`
mutation UpdateOnePipelineProgress($data: PipelineProgressUpdateInput!, $where: PipelineProgressWhereUniqueInput!) { mutation UpdateOnePipelineProgress($data: PipelineProgressUpdateInput!, $where: PipelineProgressWhereUniqueInput!) {
updateOnePipelineProgress(where: $where, data: $data) { updateOnePipelineProgress(where: $where, data: $data) {

View File

@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const DELETE_PIPELINE_STAGE = gql`
mutation DeletePipelineStage($where: PipelineStageWhereUniqueInput!) {
pipelineStage: deleteOnePipelineStage(where: $where) {
id
name
color
}
}
`;

View File

@ -2,7 +2,10 @@ import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import type { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition'; import type { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
import { useCreatePipelineStageMutation } from '~/generated/graphql'; import {
useCreatePipelineStageMutation,
useDeletePipelineStageMutation,
} from '~/generated/graphql';
import { GET_PIPELINES } from '../graphql/queries/getPipelines'; import { GET_PIPELINES } from '../graphql/queries/getPipelines';
import { currentPipelineState } from '../states/currentPipelineState'; import { currentPipelineState } from '../states/currentPipelineState';
@ -11,6 +14,7 @@ export const usePipelineStages = () => {
const currentPipeline = useRecoilValue(currentPipelineState); const currentPipeline = useRecoilValue(currentPipelineState);
const [createPipelineStageMutation] = useCreatePipelineStageMutation(); const [createPipelineStageMutation] = useCreatePipelineStageMutation();
const [deletePipelineStageMutation] = useDeletePipelineStageMutation();
const handlePipelineStageAdd = async (boardColumn: BoardColumnDefinition) => { const handlePipelineStageAdd = async (boardColumn: BoardColumnDefinition) => {
if (!currentPipeline?.id) return; if (!currentPipeline?.id) return;
@ -30,5 +34,14 @@ export const usePipelineStages = () => {
}); });
}; };
return { handlePipelineStageAdd }; const handlePipelineStageDelete = async (boardColumnId: string) => {
if (!currentPipeline?.id) return;
return deletePipelineStageMutation({
variables: { where: { id: boardColumnId } },
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
});
};
return { handlePipelineStageAdd, handlePipelineStageDelete };
}; };

View File

@ -54,6 +54,7 @@ const StyledNumChildren = styled.div`
export type BoardColumnProps = { export type BoardColumnProps = {
color: string; color: string;
title: string; title: string;
onDelete?: (id: string) => void;
onTitleEdit: (title: string, color: string) => void; onTitleEdit: (title: string, color: string) => void;
totalAmount?: number; totalAmount?: number;
children: React.ReactNode; children: React.ReactNode;
@ -65,6 +66,7 @@ export type BoardColumnProps = {
export function BoardColumn({ export function BoardColumn({
color, color,
title, title,
onDelete,
onTitleEdit, onTitleEdit,
totalAmount, totalAmount,
children, children,
@ -102,6 +104,7 @@ export function BoardColumn({
{isBoardColumnMenuOpen && ( {isBoardColumnMenuOpen && (
<BoardColumnMenu <BoardColumnMenu
onClose={handleClose} onClose={handleClose}
onDelete={onDelete}
onTitleEdit={onTitleEdit} onTitleEdit={onTitleEdit}
title={title} title={title}
color={color} color={color}

View File

@ -1,5 +1,6 @@
import { useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useCreateCompanyProgress } from '@/companies/hooks/useCreateCompanyProgress'; import { useCreateCompanyProgress } from '@/companies/hooks/useCreateCompanyProgress';
@ -7,7 +8,7 @@ import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSear
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { IconPencil, IconPlus } from '@/ui/icon'; import { IconPencil, IconPlus, IconTrash } from '@/ui/icon';
import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
@ -19,6 +20,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { boardColumnsState } from '../states/boardColumnsState';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope'; import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu'; import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu';
@ -30,22 +32,30 @@ const StyledMenuContainer = styled.div`
`; `;
type OwnProps = { type OwnProps = {
onClose: () => void;
title: string;
color: string; color: string;
onClose: () => void;
onDelete?: (id: string) => void;
onTitleEdit: (title: string, color: string) => void; onTitleEdit: (title: string, color: string) => void;
stageId: string; stageId: string;
title: string;
}; };
type Menu = 'actions' | 'add' | 'title';
export function BoardColumnMenu({ export function BoardColumnMenu({
onClose,
onTitleEdit,
title,
color, color,
onClose,
onDelete,
onTitleEdit,
stageId, stageId,
title,
}: OwnProps) { }: OwnProps) {
const [openMenu, setOpenMenu] = useState('actions'); const [currentMenu, setCurrentMenu] = useState('actions');
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
const boardColumnMenuRef = useRef(null); const boardColumnMenuRef = useRef(null);
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const createCompanyProgress = useCreateCompanyProgress(); const createCompanyProgress = useCreateCompanyProgress();
@ -70,23 +80,31 @@ export function BoardColumnMenu({
closeMenu(); closeMenu();
} }
function closeMenu() {
goBackToPreviousHotkeyScope();
onClose();
}
const { const {
setHotkeyScopeAndMemorizePreviousScope, setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope, goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope(); } = usePreviousHotkeyScope();
function setMenu(menu: string) { const closeMenu = useCallback(() => {
goBackToPreviousHotkeyScope();
onClose();
}, [goBackToPreviousHotkeyScope, onClose]);
const handleDelete = useCallback(() => {
setBoardColumns((previousBoardColumns) =>
previousBoardColumns.filter((column) => column.id !== stageId),
);
onDelete?.(stageId);
closeMenu();
}, [closeMenu, onDelete, setBoardColumns, stageId]);
function setMenu(menu: Menu) {
if (menu === 'add') { if (menu === 'add') {
setHotkeyScopeAndMemorizePreviousScope( setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker, RelationPickerHotkeyScope.RelationPicker,
); );
} }
setOpenMenu(menu); setCurrentMenu(menu);
} }
const [searchFilter] = useRecoilScopedState( const [searchFilter] = useRecoilScopedState(
relationPickerSearchFilterScopedState, relationPickerSearchFilterScopedState,
@ -108,19 +126,26 @@ export function BoardColumnMenu({
return ( return (
<StyledMenuContainer ref={boardColumnMenuRef}> <StyledMenuContainer ref={boardColumnMenuRef}>
<StyledDropdownMenu> <StyledDropdownMenu>
{openMenu === 'actions' && ( {currentMenu === 'actions' && (
<StyledDropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
<DropdownMenuSelectableItem onClick={() => setMenu('title')}> <DropdownMenuSelectableItem onClick={() => setMenu('title')}>
<IconPencil size={icon.size.md} stroke={icon.stroke.sm} /> <IconPencil size={icon.size.md} stroke={icon.stroke.sm} />
Rename Rename
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
<DropdownMenuSelectableItem
disabled={boardColumns.length <= 1}
onClick={handleDelete}
>
<IconTrash size={icon.size.md} stroke={icon.stroke.sm} />
Delete
</DropdownMenuSelectableItem>
<DropdownMenuSelectableItem onClick={() => setMenu('add')}> <DropdownMenuSelectableItem onClick={() => setMenu('add')}>
<IconPlus size={icon.size.md} stroke={icon.stroke.sm} /> <IconPlus size={icon.size.md} stroke={icon.stroke.sm} />
New opportunity New opportunity
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
</StyledDropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
)} )}
{openMenu === 'title' && ( {currentMenu === 'title' && (
<BoardColumnEditTitleMenu <BoardColumnEditTitleMenu
color={color} color={color}
onClose={closeMenu} onClose={closeMenu}
@ -128,8 +153,7 @@ export function BoardColumnMenu({
title={title} title={title}
/> />
)} )}
{currentMenu === 'add' && (
{openMenu === 'add' && (
<SingleEntitySelect <SingleEntitySelect
onEntitySelected={(value) => handleCompanySelected(value)} onEntitySelected={(value) => handleCompanySelected(value)}
onCancel={closeMenu} onCancel={closeMenu}

View File

@ -43,16 +43,18 @@ const StyledWrapper = styled.div`
export function EntityBoard({ export function EntityBoard({
boardOptions, boardOptions,
updateSorts, onColumnAdd,
onColumnDelete,
onEditColumnTitle, onEditColumnTitle,
onStageAdd, updateSorts,
}: { }: {
boardOptions: BoardOptions; boardOptions: BoardOptions;
onColumnAdd?: (boardColumn: BoardColumnDefinition) => void;
onColumnDelete?: (boardColumnId: string) => void;
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
updateSorts: ( updateSorts: (
sorts: Array<SelectedSortType<PipelineProgressOrderByWithRelationInput>>, sorts: Array<SelectedSortType<PipelineProgressOrderByWithRelationInput>>,
) => void; ) => void;
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
}) { }) {
const [boardColumns] = useRecoilState(boardColumnsState); const [boardColumns] = useRecoilState(boardColumnsState);
const setCardSelected = useSetCardSelected(); const setCardSelected = useSetCardSelected();
@ -133,7 +135,7 @@ export function EntityBoard({
viewIcon={<IconList size={theme.icon.size.md} />} viewIcon={<IconList size={theme.icon.size.md} />}
availableSorts={boardOptions.sorts} availableSorts={boardOptions.sorts}
onSortsUpdate={updateSorts} onSortsUpdate={updateSorts}
onStageAdd={onStageAdd} onStageAdd={onColumnAdd}
context={CompanyBoardRecoilScopeContext} context={CompanyBoardRecoilScopeContext}
/> />
<ScrollWrapper> <ScrollWrapper>
@ -148,7 +150,8 @@ export function EntityBoard({
<EntityBoardColumn <EntityBoardColumn
boardOptions={boardOptions} boardOptions={boardOptions}
column={column} column={column}
onEditColumnTitle={onEditColumnTitle} onTitleEdit={onEditColumnTitle}
onDelete={onColumnDelete}
/> />
</RecoilScope> </RecoilScope>
</BoardColumnIdContext.Provider> </BoardColumnIdContext.Provider>

View File

@ -50,11 +50,13 @@ const BoardColumnCardsContainer = ({
export function EntityBoardColumn({ export function EntityBoardColumn({
column, column,
boardOptions, boardOptions,
onEditColumnTitle, onDelete,
onTitleEdit,
}: { }: {
column: BoardColumnDefinition; column: BoardColumnDefinition;
boardOptions: BoardOptions; boardOptions: BoardOptions;
onEditColumnTitle: (columnId: string, title: string, color: string) => void; onDelete?: (columnId: string) => void;
onTitleEdit: (columnId: string, title: string, color: string) => void;
}) { }) {
const boardColumnId = useContext(BoardColumnIdContext) ?? ''; const boardColumnId = useContext(BoardColumnIdContext) ?? '';
@ -66,15 +68,16 @@ export function EntityBoardColumn({
boardCardIdsByColumnIdFamilyState(boardColumnId ?? ''), boardCardIdsByColumnIdFamilyState(boardColumnId ?? ''),
); );
function handleEditColumnTitle(title: string, color: string) { function handleTitleEdit(title: string, color: string) {
onEditColumnTitle(boardColumnId, title, color); onTitleEdit(boardColumnId, title, color);
} }
return ( return (
<Droppable droppableId={column.id}> <Droppable droppableId={column.id}>
{(droppableProvided) => ( {(droppableProvided) => (
<BoardColumn <BoardColumn
onTitleEdit={handleEditColumnTitle} onTitleEdit={handleTitleEdit}
onDelete={onDelete}
title={column.title} title={column.title}
color={column.colorCode} color={column.colorCode}
totalAmount={boardColumnTotal} totalAmount={boardColumnTotal}

View File

@ -11,27 +11,23 @@ const meta: Meta<typeof CircularProgressBar> = {
args: { args: {
size: 50, size: 50,
}, },
parameters: {
chromatic: { disableSnapshot: true },
},
}; };
export default meta; export default meta;
type Story = StoryObj<typeof CircularProgressBar>; type Story = StoryObj<typeof CircularProgressBar>;
const args = {};
const defaultArgTypes = {
control: false,
};
export const Default: Story = { export const Default: Story = {
args,
decorators: [ComponentDecorator], decorators: [ComponentDecorator],
}; };
export const Catalog = { export const Catalog = {
args: {
...args,
},
argTypes: { argTypes: {
strokeWidth: defaultArgTypes, strokeWidth: { control: false },
segmentColor: defaultArgTypes, segmentColor: { control: false },
}, },
parameters: { parameters: {
catalog: { catalog: {

View File

@ -44,7 +44,8 @@ export function Opportunities() {
[], [],
); );
const { handlePipelineStageAdd } = usePipelineStages(); const { handlePipelineStageAdd, handlePipelineStageDelete } =
usePipelineStages();
const [updatePipelineStage] = useUpdatePipelineStageMutation(); const [updatePipelineStage] = useUpdatePipelineStageMutation();
@ -89,7 +90,8 @@ export function Opportunities() {
boardOptions={opportunitiesBoardOptions} boardOptions={opportunitiesBoardOptions}
updateSorts={updateSorts} updateSorts={updateSorts}
onEditColumnTitle={handleEditColumnTitle} onEditColumnTitle={handleEditColumnTitle}
onStageAdd={handlePipelineStageAdd} onColumnAdd={handlePipelineStageAdd}
onColumnDelete={handlePipelineStageDelete}
/> />
<EntityBoardActionBar /> <EntityBoardActionBar />
<EntityBoardContextMenu /> <EntityBoardContextMenu />

View File

@ -128,6 +128,7 @@ export class AbilityFactory {
can(AbilityAction.Read, 'PipelineStage', { workspaceId: workspace.id }); can(AbilityAction.Read, 'PipelineStage', { workspaceId: workspace.id });
can(AbilityAction.Create, 'PipelineStage', { workspaceId: workspace.id }); can(AbilityAction.Create, 'PipelineStage', { workspaceId: workspace.id });
can(AbilityAction.Update, 'PipelineStage', { workspaceId: workspace.id }); can(AbilityAction.Update, 'PipelineStage', { workspaceId: workspace.id });
can(AbilityAction.Delete, 'PipelineStage', { workspaceId: workspace.id });
// PipelineProgress // PipelineProgress
can(AbilityAction.Read, 'PipelineProgress', { workspaceId: workspace.id }); can(AbilityAction.Read, 'PipelineProgress', { workspaceId: workspace.id });

View File

@ -1,5 +1,9 @@
import { Resolver, Args, Query, Mutation } from '@nestjs/graphql'; import { Resolver, Args, Query, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common'; import {
ForbiddenException,
NotFoundException,
UseGuards,
} from '@nestjs/common';
import { accessibleBy } from '@casl/prisma'; import { accessibleBy } from '@casl/prisma';
import { Prisma, Workspace } from '@prisma/client'; import { Prisma, Workspace } from '@prisma/client';
@ -12,6 +16,7 @@ import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import { import {
CreatePipelineStageAbilityHandler, CreatePipelineStageAbilityHandler,
DeletePipelineStageAbilityHandler,
ReadPipelineStageAbilityHandler, ReadPipelineStageAbilityHandler,
UpdatePipelineStageAbilityHandler, UpdatePipelineStageAbilityHandler,
} from 'src/ability/handlers/pipeline-stage.ability-handler'; } from 'src/ability/handlers/pipeline-stage.ability-handler';
@ -24,6 +29,7 @@ import {
import { UpdateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/update-one-pipeline-stage.args'; import { UpdateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/update-one-pipeline-stage.args';
import { CreateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/create-one-pipeline-stage.args'; import { CreateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/create-one-pipeline-stage.args';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { DeleteOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/delete-one-pipeline-stage.args';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Resolver(() => PipelineStage) @Resolver(() => PipelineStage)
@ -90,4 +96,54 @@ export class PipelineStageResolver {
select: prismaSelect.value, select: prismaSelect.value,
} as Prisma.PipelineProgressUpdateArgs); } as Prisma.PipelineProgressUpdateArgs);
} }
@Mutation(() => PipelineStage, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeletePipelineStageAbilityHandler)
async deleteOnePipelineStage(
@Args() args: DeleteOnePipelineStageArgs,
): Promise<PipelineStage> {
const pipelineStageToDelete = await this.pipelineStageService.findUnique({
where: args.where,
});
if (!pipelineStageToDelete) {
throw new NotFoundException();
}
const { pipelineId } = pipelineStageToDelete;
const remainingPipelineStages = await this.pipelineStageService.findMany({
orderBy: { index: 'asc' },
where: {
pipelineId,
NOT: { id: pipelineStageToDelete.id },
},
});
if (!remainingPipelineStages.length) {
throw new ForbiddenException(
`Deleting last pipeline stage is not allowed`,
);
}
const deletedPipelineStage = await this.pipelineStageService.delete({
where: args.where,
});
await Promise.all(
remainingPipelineStages.map((pipelineStage, index) => {
if (pipelineStage.index === index) return;
return this.pipelineStageService.update({
data: { index },
where: { id: pipelineStage.id },
});
}),
);
return deletedPipelineStage;
}
} }