From f29d843db96d2ae205e4caeaf7bdaa3c97db55fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Mon, 4 Sep 2023 11:37:31 +0200 Subject: [PATCH] feat: add board options dropdown and pipeline stage creation (#1399) * feat: add board options dropdown and pipeline stage creation Closes #1395 * refactor: code review - remove useCallback --- front/src/generated/graphql.tsx | 68 ++++++++ .../graphql/mutations/createPipelineStage.ts | 11 ++ .../pipeline/hooks/usePipelineStages.ts | 34 ++++ .../ui/board/components/BoardHeader.tsx | 81 +++++---- .../board/components/BoardOptionsDropdown.tsx | 26 ++- .../components/BoardOptionsDropdownButton.tsx | 4 +- .../BoardOptionsDropdownContent.tsx | 155 ++++++++++++++---- .../ui/board/components/EntityBoard.tsx | 4 + .../ui/board/types/BoardOptionsDropdownKey.ts | 1 + .../ui/board/types/BoardOptionsHotkeyScope.ts | 3 + front/src/modules/ui/icon/index.ts | 1 + .../src/pages/opportunities/Opportunities.tsx | 4 + server/src/ability/ability.factory.ts | 1 + .../resolvers/pipeline-stage.resolver.ts | 25 ++- 14 files changed, 351 insertions(+), 67 deletions(-) create mode 100644 front/src/modules/pipeline/graphql/mutations/createPipelineStage.ts create mode 100644 front/src/modules/pipeline/hooks/usePipelineStages.ts create mode 100644 front/src/modules/ui/board/types/BoardOptionsDropdownKey.ts create mode 100644 front/src/modules/ui/board/types/BoardOptionsHotkeyScope.ts diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 342066041..5cd56faef 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; + createOnePipelineStage: PipelineStage; createOneView: View; createOneViewField: ViewField; deleteCurrentWorkspace: Workspace; @@ -1130,6 +1131,11 @@ export type MutationCreateOnePipelineProgressArgs = { }; +export type MutationCreateOnePipelineStageArgs = { + data: PipelineStageCreateInput; +}; + + export type MutationCreateOneViewArgs = { data: ViewCreateInput; }; @@ -1654,6 +1660,10 @@ export type PipelineCreateNestedOneWithoutPipelineProgressesInput = { connect?: InputMaybe; }; +export type PipelineCreateNestedOneWithoutPipelineStagesInput = { + connect?: InputMaybe; +}; + export type PipelineOrderByWithRelationInput = { createdAt?: InputMaybe; icon?: InputMaybe; @@ -1707,6 +1717,10 @@ export type PipelineProgressCreateNestedManyWithoutPersonInput = { connect?: InputMaybe>; }; +export type PipelineProgressCreateNestedManyWithoutPipelineStageInput = { + connect?: InputMaybe>; +}; + export type PipelineProgressCreateNestedManyWithoutPointOfContactInput = { connect?: InputMaybe>; }; @@ -1861,6 +1875,18 @@ export type PipelineStage = { updatedAt: Scalars['DateTime']; }; +export type PipelineStageCreateInput = { + color: Scalars['String']; + createdAt?: InputMaybe; + id?: InputMaybe; + index?: InputMaybe; + name: Scalars['String']; + pipeline: PipelineCreateNestedOneWithoutPipelineStagesInput; + pipelineProgresses?: InputMaybe; + type: Scalars['String']; + updatedAt?: InputMaybe; +}; + export type PipelineStageCreateNestedOneWithoutPipelineProgressesInput = { connect?: InputMaybe; }; @@ -3285,6 +3311,13 @@ export type CreateOneCompanyPipelineProgressMutationVariables = Exact<{ export type CreateOneCompanyPipelineProgressMutation = { __typename?: 'Mutation', createOnePipelineProgress: { __typename?: 'PipelineProgress', id: string } }; +export type CreatePipelineStageMutationVariables = Exact<{ + data: PipelineStageCreateInput; +}>; + + +export type CreatePipelineStageMutation = { __typename?: 'Mutation', pipelineStage: { __typename?: 'PipelineStage', id: string, name: string, color: string } }; + export type DeleteManyPipelineProgressMutationVariables = Exact<{ ids?: InputMaybe | Scalars['String']>; }>; @@ -5453,6 +5486,41 @@ export function useCreateOneCompanyPipelineProgressMutation(baseOptions?: Apollo export type CreateOneCompanyPipelineProgressMutationHookResult = ReturnType; export type CreateOneCompanyPipelineProgressMutationResult = Apollo.MutationResult; export type CreateOneCompanyPipelineProgressMutationOptions = Apollo.BaseMutationOptions; +export const CreatePipelineStageDocument = gql` + mutation CreatePipelineStage($data: PipelineStageCreateInput!) { + pipelineStage: createOnePipelineStage(data: $data) { + id + name + color + } +} + `; +export type CreatePipelineStageMutationFn = Apollo.MutationFunction; + +/** + * __useCreatePipelineStageMutation__ + * + * To run a mutation, you first call `useCreatePipelineStageMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreatePipelineStageMutation` 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 [createPipelineStageMutation, { data, loading, error }] = useCreatePipelineStageMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useCreatePipelineStageMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreatePipelineStageDocument, options); + } +export type CreatePipelineStageMutationHookResult = ReturnType; +export type CreatePipelineStageMutationResult = Apollo.MutationResult; +export type CreatePipelineStageMutationOptions = Apollo.BaseMutationOptions; export const DeleteManyPipelineProgressDocument = gql` mutation DeleteManyPipelineProgress($ids: [String!]) { deleteManyPipelineProgress(where: {id: {in: $ids}}) { diff --git a/front/src/modules/pipeline/graphql/mutations/createPipelineStage.ts b/front/src/modules/pipeline/graphql/mutations/createPipelineStage.ts new file mode 100644 index 000000000..c41adbad4 --- /dev/null +++ b/front/src/modules/pipeline/graphql/mutations/createPipelineStage.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const CREATE_PIPELINE_STAGE = gql` + mutation CreatePipelineStage($data: PipelineStageCreateInput!) { + pipelineStage: createOnePipelineStage(data: $data) { + id + name + color + } + } +`; diff --git a/front/src/modules/pipeline/hooks/usePipelineStages.ts b/front/src/modules/pipeline/hooks/usePipelineStages.ts new file mode 100644 index 000000000..057ad00b2 --- /dev/null +++ b/front/src/modules/pipeline/hooks/usePipelineStages.ts @@ -0,0 +1,34 @@ +import { getOperationName } from '@apollo/client/utilities'; +import { useRecoilValue } from 'recoil'; + +import type { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition'; +import { useCreatePipelineStageMutation } from '~/generated/graphql'; + +import { GET_PIPELINES } from '../graphql/queries/getPipelines'; +import { currentPipelineState } from '../states/currentPipelineState'; + +export const usePipelineStages = () => { + const currentPipeline = useRecoilValue(currentPipelineState); + + const [createPipelineStageMutation] = useCreatePipelineStageMutation(); + + const handlePipelineStageAdd = async (boardColumn: BoardColumnDefinition) => { + if (!currentPipeline?.id) return; + + return createPipelineStageMutation({ + variables: { + data: { + color: boardColumn.colorCode, + id: boardColumn.id, + index: boardColumn.index, + name: boardColumn.title, + pipeline: { connect: { id: currentPipeline.id } }, + type: 'ongoing', + }, + }, + refetchQueries: [getOperationName(GET_PIPELINES) ?? ''], + }); + }; + + return { handlePipelineStageAdd }; +}; diff --git a/front/src/modules/ui/board/components/BoardHeader.tsx b/front/src/modules/ui/board/components/BoardHeader.tsx index 4f9cc5eb5..51cf90047 100644 --- a/front/src/modules/ui/board/components/BoardHeader.tsx +++ b/front/src/modules/ui/board/components/BoardHeader.tsx @@ -1,18 +1,26 @@ import { Context, ReactNode, useCallback, useState } from 'react'; import styled from '@emotion/styled'; +import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext'; import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton'; import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar'; import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton'; import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope'; import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface'; import { TopBar } from '@/ui/top-bar/TopBar'; +import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; + +import type { BoardColumnDefinition } from '../types/BoardColumnDefinition'; +import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope'; + +import { BoardOptionsDropdown } from './BoardOptionsDropdown'; type OwnProps = { viewName: string; viewIcon?: ReactNode; availableSorts?: Array>; onSortsUpdate?: (sorts: Array>) => void; + onStageAdd?: (boardColumn: BoardColumnDefinition) => void; context: Context; }; @@ -31,6 +39,7 @@ export function BoardHeader({ viewIcon, availableSorts, onSortsUpdate, + onStageAdd, context, }: OwnProps) { const [sorts, innerSetSorts] = useState>>( @@ -56,41 +65,47 @@ export function BoardHeader({ ); return ( - - {viewIcon} - {viewName} - - } - rightComponent={ - <> - + + {viewIcon} + {viewName} + + } + rightComponent={ + <> + + + context={context} + isSortSelected={sorts.length > 0} + availableSorts={availableSorts || []} + onSortSelect={sortSelect} + HotkeyScope={FiltersHotkeyScope.FilterDropdownButton} + /> + + + } + bottomComponent={ + { + innerSetSorts([]); + onSortsUpdate?.([]); + }} /> - - context={context} - isSortSelected={sorts.length > 0} - availableSorts={availableSorts || []} - onSortSelect={sortSelect} - HotkeyScope={FiltersHotkeyScope.FilterDropdownButton} - /> - - } - bottomComponent={ - { - innerSetSorts([]); - onSortsUpdate && onSortsUpdate([]); - }} - /> - } - /> + } + /> + ); } diff --git a/front/src/modules/ui/board/components/BoardOptionsDropdown.tsx b/front/src/modules/ui/board/components/BoardOptionsDropdown.tsx index ca9ff0732..d43525519 100644 --- a/front/src/modules/ui/board/components/BoardOptionsDropdown.tsx +++ b/front/src/modules/ui/board/components/BoardOptionsDropdown.tsx @@ -1,14 +1,32 @@ import { DropdownButton } from '@/ui/dropdown/components/DropdownButton'; +import type { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; + +import { BoardColumnDefinition } from '../types/BoardColumnDefinition'; +import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey'; import { BoardOptionsDropdownButton } from './BoardOptionsDropdownButton'; import { BoardOptionsDropdownContent } from './BoardOptionsDropdownContent'; -export function BoardOptionsDropdown() { +type BoardOptionsDropdownProps = { + customHotkeyScope: HotkeyScope; + onStageAdd?: (boardColumn: BoardColumnDefinition) => void; +}; + +export function BoardOptionsDropdown({ + customHotkeyScope, + onStageAdd, +}: BoardOptionsDropdownProps) { return ( } - dropdownComponents={} - > + dropdownComponents={ + + } + dropdownHotkeyScope={customHotkeyScope} + dropdownKey={BoardOptionsDropdownKey} + /> ); } diff --git a/front/src/modules/ui/board/components/BoardOptionsDropdownButton.tsx b/front/src/modules/ui/board/components/BoardOptionsDropdownButton.tsx index 6b9f6223e..b4e218418 100644 --- a/front/src/modules/ui/board/components/BoardOptionsDropdownButton.tsx +++ b/front/src/modules/ui/board/components/BoardOptionsDropdownButton.tsx @@ -1,9 +1,11 @@ import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton'; import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; +import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey'; + export function BoardOptionsDropdownButton() { const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({ - key: 'options', + key: BoardOptionsDropdownKey, }); function handleClick() { diff --git a/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx b/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx index 9d06a33ee..77b02eae9 100644 --- a/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx +++ b/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx @@ -1,54 +1,153 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; +import { Key } from 'ts-key-enum'; +import { v4 } from 'uuid'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; +import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; -import { IconChevronLeft } from '@/ui/icon'; +import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; +import { + IconChevronLeft, + IconChevronRight, + IconLayoutKanban, + IconPlus, + IconSettings, +} from '@/ui/icon'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -type BoardOptionsDropdownMenu = 'options' | 'fields'; +import { boardColumnsState } from '../states/boardColumnsState'; +import type { BoardColumnDefinition } from '../types/BoardColumnDefinition'; +import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey'; -export function BoardOptionsDropdownContent() { +type BoardOptionsDropdownContentProps = { + customHotkeyScope: HotkeyScope; + onStageAdd?: (boardColumn: BoardColumnDefinition) => void; +}; + +const StyledIconSettings = styled(IconSettings)` + margin-right: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledIconChevronRight = styled(IconChevronRight)` + color: ${({ theme }) => theme.font.color.tertiary}; + margin-left: auto; +`; + +enum BoardOptionsMenu { + StageCreation = 'StageCreation', + Stages = 'Stages', +} + +export function BoardOptionsDropdownContent({ + customHotkeyScope, + onStageAdd, +}: BoardOptionsDropdownContentProps) { const theme = useTheme(); - const [menuShown, setMenuShown] = - useState('options'); + const stageInputRef = useRef(null); - function handleFieldsClick() { - setMenuShown('fields'); - } + const [currentMenu, setCurrentMenu] = useState< + BoardOptionsMenu | undefined + >(); - function handleMenuHeaderClick() { - setMenuShown('options'); - } + const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState); + + const resetMenu = () => setCurrentMenu(undefined); + + const handleStageSubmit = () => { + if ( + currentMenu !== BoardOptionsMenu.StageCreation || + !stageInputRef?.current?.value + ) + return; + + const columnToCreate = { + id: v4(), + colorCode: 'gray', + index: boardColumns.length, + title: stageInputRef.current.value, + }; + + setBoardColumns((previousBoardColumns) => [ + ...previousBoardColumns, + columnToCreate, + ]); + onStageAdd?.(columnToCreate); + }; + + const { closeDropdownButton } = useDropdownButton({ + key: BoardOptionsDropdownKey, + }); + + useScopedHotkeys( + Key.Escape, + () => { + closeDropdownButton(); + }, + customHotkeyScope.scope, + ); + + useScopedHotkeys( + Key.Enter, + () => { + handleStageSubmit(); + closeDropdownButton(); + }, + customHotkeyScope.scope, + ); return ( - {menuShown === 'options' ? ( + {!currentMenu && ( <> - Options + + + Settings + - - Fields + setCurrentMenu(BoardOptionsMenu.Stages)} + > + + Stages + - ) : ( - menuShown === 'fields' && ( - <> - } - onClick={handleMenuHeaderClick} + )} + {currentMenu === BoardOptionsMenu.Stages && ( + <> + } + onClick={resetMenu} + > + Stages + + + + setCurrentMenu(BoardOptionsMenu.StageCreation)} > - Fields - - - {} - - ) + + Add stage + + + + )} + {currentMenu === BoardOptionsMenu.StageCreation && ( + )} ); diff --git a/front/src/modules/ui/board/components/EntityBoard.tsx b/front/src/modules/ui/board/components/EntityBoard.tsx index 951598565..ebd58ce3e 100644 --- a/front/src/modules/ui/board/components/EntityBoard.tsx +++ b/front/src/modules/ui/board/components/EntityBoard.tsx @@ -30,6 +30,7 @@ import { useSetCardSelected } from '../hooks/useSetCardSelected'; import { useUpdateBoardCardIds } from '../hooks/useUpdateBoardCardIds'; import { boardColumnsState } from '../states/boardColumnsState'; import { BoardColumnRecoilScopeContext } from '../states/recoil-scope-contexts/BoardColumnRecoilScopeContext'; +import type { BoardColumnDefinition } from '../types/BoardColumnDefinition'; import { BoardOptions } from '../types/BoardOptions'; import { EntityBoardColumn } from './EntityBoardColumn'; @@ -44,12 +45,14 @@ export function EntityBoard({ boardOptions, updateSorts, onEditColumnTitle, + onStageAdd, }: { boardOptions: BoardOptions; updateSorts: ( sorts: Array>, ) => void; onEditColumnTitle: (columnId: string, title: string, color: string) => void; + onStageAdd?: (boardColumn: BoardColumnDefinition) => void; }) { const [boardColumns] = useRecoilState(boardColumnsState); const setCardSelected = useSetCardSelected(); @@ -130,6 +133,7 @@ export function EntityBoard({ viewIcon={} availableSorts={boardOptions.sorts} onSortsUpdate={updateSorts} + onStageAdd={onStageAdd} context={CompanyBoardRecoilScopeContext} /> diff --git a/front/src/modules/ui/board/types/BoardOptionsDropdownKey.ts b/front/src/modules/ui/board/types/BoardOptionsDropdownKey.ts new file mode 100644 index 000000000..12c0f9aa6 --- /dev/null +++ b/front/src/modules/ui/board/types/BoardOptionsDropdownKey.ts @@ -0,0 +1 @@ +export const BoardOptionsDropdownKey = 'board-options'; diff --git a/front/src/modules/ui/board/types/BoardOptionsHotkeyScope.ts b/front/src/modules/ui/board/types/BoardOptionsHotkeyScope.ts new file mode 100644 index 000000000..f726bc66f --- /dev/null +++ b/front/src/modules/ui/board/types/BoardOptionsHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum BoardOptionsHotkeyScope { + Dropdown = 'board-options-dropdown', +} diff --git a/front/src/modules/ui/icon/index.ts b/front/src/modules/ui/icon/index.ts index b8ed49431..7bb41d1e4 100644 --- a/front/src/modules/ui/icon/index.ts +++ b/front/src/modules/ui/icon/index.ts @@ -41,6 +41,7 @@ export { IconHeart, IconHelpCircle, IconInbox, + IconLayoutKanban, IconLayoutSidebarLeftCollapse, IconLayoutSidebarRightCollapse, IconLayoutSidebarRightExpand, diff --git a/front/src/pages/opportunities/Opportunities.tsx b/front/src/pages/opportunities/Opportunities.tsx index 4f6732dbf..e3b6b1635 100644 --- a/front/src/pages/opportunities/Opportunities.tsx +++ b/front/src/pages/opportunities/Opportunities.tsx @@ -4,6 +4,7 @@ import { useTheme } from '@emotion/react'; import { HooksCompanyBoard } from '@/companies/components/HooksCompanyBoard'; import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext'; import { PipelineAddButton } from '@/pipeline/components/PipelineAddButton'; +import { usePipelineStages } from '@/pipeline/hooks/usePipelineStages'; import { EntityBoard } from '@/ui/board/components/EntityBoard'; import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar'; import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu'; @@ -43,6 +44,8 @@ export function Opportunities() { [], ); + const { handlePipelineStageAdd } = usePipelineStages(); + const [updatePipelineStage] = useUpdatePipelineStageMutation(); function handleEditColumnTitle( @@ -86,6 +89,7 @@ export function Opportunities() { boardOptions={opportunitiesBoardOptions} updateSorts={updateSorts} onEditColumnTitle={handleEditColumnTitle} + onStageAdd={handlePipelineStageAdd} /> diff --git a/server/src/ability/ability.factory.ts b/server/src/ability/ability.factory.ts index 090f2547e..06195ff8c 100644 --- a/server/src/ability/ability.factory.ts +++ b/server/src/ability/ability.factory.ts @@ -126,6 +126,7 @@ export class AbilityFactory { // PipelineStage can(AbilityAction.Read, 'PipelineStage', { workspaceId: workspace.id }); + can(AbilityAction.Create, 'PipelineStage', { workspaceId: workspace.id }); can(AbilityAction.Update, 'PipelineStage', { workspaceId: workspace.id }); // PipelineProgress diff --git a/server/src/core/pipeline/resolvers/pipeline-stage.resolver.ts b/server/src/core/pipeline/resolvers/pipeline-stage.resolver.ts index 3e1e53d64..4f0335c6c 100644 --- a/server/src/core/pipeline/resolvers/pipeline-stage.resolver.ts +++ b/server/src/core/pipeline/resolvers/pipeline-stage.resolver.ts @@ -2,7 +2,7 @@ import { Resolver, Args, Query, Mutation } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; import { accessibleBy } from '@casl/prisma'; -import { Prisma } from '@prisma/client'; +import { Prisma, Workspace } from '@prisma/client'; import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; import { PipelineStage } from 'src/core/@generated/pipeline-stage/pipeline-stage.model'; @@ -11,6 +11,7 @@ import { PipelineStageService } from 'src/core/pipeline/services/pipeline-stage. import { AbilityGuard } from 'src/guards/ability.guard'; import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; import { + CreatePipelineStageAbilityHandler, ReadPipelineStageAbilityHandler, UpdatePipelineStageAbilityHandler, } from 'src/ability/handlers/pipeline-stage.ability-handler'; @@ -21,12 +22,34 @@ import { PrismaSelect, } from 'src/decorators/prisma-select.decorator'; 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 { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; @UseGuards(JwtAuthGuard) @Resolver(() => PipelineStage) export class PipelineStageResolver { constructor(private readonly pipelineStageService: PipelineStageService) {} + @Mutation(() => PipelineStage, { + nullable: false, + }) + @UseGuards(AbilityGuard) + @CheckAbilities(CreatePipelineStageAbilityHandler) + async createOnePipelineStage( + @Args() args: CreateOnePipelineStageArgs, + @AuthWorkspace() workspace: Workspace, + @PrismaSelector({ modelName: 'PipelineStage' }) + prismaSelect: PrismaSelect<'PipelineStage'>, + ): Promise> { + return this.pipelineStageService.create({ + data: { + ...args.data, + workspace: { connect: { id: workspace.id } }, + }, + select: prismaSelect.value, + } as Prisma.PipelineStageCreateArgs); + } + @Query(() => [PipelineStage]) @UseGuards(AbilityGuard) @CheckAbilities(ReadPipelineStageAbilityHandler)