From 91c8068db1cdc92e3e8327bc56c0365f28058112 Mon Sep 17 00:00:00 2001 From: Emilien Chauvet Date: Sat, 15 Jul 2023 19:32:16 -0700 Subject: [PATCH] Enable column edition, and fix ordering (#683) * Enable column edition, and fix ordering * Move queries to services * Add total amounts for board columns * Refactor totals selector as a family * Fix 0-index issue * Lint * Rename selector * Remove useless header * Address PR comments * Optimistically update board column names --- front/src/generated/graphql.tsx | 62 +++++++++++++++++++ .../command-menu/components/CommandMenu.tsx | 2 +- .../CommentThreadRelationPicker.tsx | 2 +- .../right-drawer/CommentThreadActionBar.tsx | 2 +- ...andleCheckableCommentThreadTargetChange.ts | 2 +- ...penCreateCommentDrawerForSelectedRowIds.ts | 2 +- .../hooks/useOpenCreateCommentThreadDrawer.ts | 2 +- .../companies/components/CompanyBoardCard.tsx | 4 +- .../FilterDropdownCompanySearchSelect.tsx | 2 +- .../components/NewCompanyBoardCard.tsx | 2 +- .../components/NewCompanyProgressButton.tsx | 6 +- .../__tests__/select.test.ts | 0 .../companies/{services => queries}/index.ts | 0 .../companies/{services => queries}/select.ts | 0 .../companies/{services => queries}/show.ts | 0 .../companies/{services => queries}/update.ts | 0 .../states}/companyProgressesFamilyState.ts | 0 .../components/CompanyEntityTableData.tsx | 2 +- .../people/components/PeopleCompanyPicker.tsx | 2 +- ...dActionBarButtonDeletePipelineProgress.tsx | 2 +- .../components/EntityBoard.tsx | 8 ++- .../components/EntityBoardColumn.tsx | 37 ++++++++++- .../pipeline-progress/services/index.ts | 1 + .../{queries/index.ts => services/select.ts} | 0 .../states/boardColumnTotalsFamilySelector.ts | 31 ++++++++++ .../modules/pipeline-stages/services/index.ts | 1 + .../pipeline-stages/services/update.ts | 10 +++ .../pipelines/components/PipelineChip.tsx | 33 ---------- .../src/modules/ui/board/components/Board.tsx | 1 + .../ui/board/components/BoardColumn.tsx | 46 ++++++++++++-- .../ui/board/components/ColumnHotkeyScope.ts | 3 + .../board/components/EditColumnTitleInput.tsx | 53 ++++++++++++++++ front/src/pages/companies/Companies.tsx | 2 +- front/src/pages/companies/CompanyShow.tsx | 2 +- front/src/pages/companies/CompanyTable.tsx | 5 +- .../companies/__stories__/Company.stories.tsx | 2 +- .../TableActionBarButtonDeleteCompanies.tsx | 2 +- .../pages/opportunities/HookCompanyBoard.tsx | 10 +-- .../opportunities/currentPipelineState.ts | 8 --- .../pages/opportunities/isBoardLoadedState.ts | 6 -- front/src/pages/people/PeopleTable.tsx | 2 +- front/src/testing/graphqlMocks.ts | 4 +- .../resolvers/pipeline-stage.resolver.ts | 26 +++++++- 43 files changed, 299 insertions(+), 88 deletions(-) rename front/src/modules/companies/{services => queries}/__tests__/select.test.ts (100%) rename front/src/modules/companies/{services => queries}/index.ts (100%) rename front/src/modules/companies/{services => queries}/select.ts (100%) rename front/src/modules/companies/{services => queries}/show.ts (100%) rename front/src/modules/companies/{services => queries}/update.ts (100%) rename front/src/{pages/opportunities => modules/companies/states}/companyProgressesFamilyState.ts (100%) rename front/src/modules/pipeline-progress/{queries/index.ts => services/select.ts} (100%) create mode 100644 front/src/modules/pipeline-progress/states/boardColumnTotalsFamilySelector.ts create mode 100644 front/src/modules/pipeline-stages/services/index.ts create mode 100644 front/src/modules/pipeline-stages/services/update.ts delete mode 100644 front/src/modules/pipelines/components/PipelineChip.tsx create mode 100644 front/src/modules/ui/board/components/ColumnHotkeyScope.ts create mode 100644 front/src/modules/ui/board/components/EditColumnTitleInput.tsx delete mode 100644 front/src/pages/opportunities/currentPipelineState.ts delete mode 100644 front/src/pages/opportunities/isBoardLoadedState.ts diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 16bc6d7d9..b294cf3d3 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -1168,6 +1168,7 @@ export type Mutation = { updateOneCompany?: Maybe; updateOnePerson?: Maybe; updateOnePipelineProgress?: Maybe; + updateOnePipelineStage?: Maybe; updateUser: User; updateWorkspace: Workspace; uploadFile: Scalars['String']; @@ -1276,6 +1277,12 @@ export type MutationUpdateOnePipelineProgressArgs = { }; +export type MutationUpdateOnePipelineStageArgs = { + data: PipelineStageUpdateInput; + where: PipelineStageWhereUniqueInput; +}; + + export type MutationUpdateUserArgs = { data: UserUpdateInput; where: UserWhereUniqueInput; @@ -2232,6 +2239,18 @@ export type PipelineStageScalarWhereInput = { updatedAt?: InputMaybe; }; +export type PipelineStageUpdateInput = { + color?: InputMaybe; + createdAt?: InputMaybe; + id?: InputMaybe; + index?: InputMaybe; + name?: InputMaybe; + pipeline?: InputMaybe; + pipelineProgresses?: InputMaybe; + type?: InputMaybe; + updatedAt?: InputMaybe; +}; + export type PipelineStageUpdateManyMutationInput = { color?: InputMaybe; createdAt?: InputMaybe; @@ -3410,6 +3429,14 @@ export type DeleteManyPipelineProgressMutationVariables = Exact<{ export type DeleteManyPipelineProgressMutation = { __typename?: 'Mutation', deleteManyPipelineProgress: { __typename?: 'AffectedRows', count: number } }; +export type UpdatePipelineStageMutationVariables = Exact<{ + id?: InputMaybe; + name?: InputMaybe; +}>; + + +export type UpdatePipelineStageMutation = { __typename?: 'Mutation', updateOnePipelineStage?: { __typename?: 'PipelineStage', id: string, name: string } | null }; + export type SearchPeopleQueryVariables = Exact<{ where?: InputMaybe; limit?: InputMaybe; @@ -5187,6 +5214,41 @@ export function useDeleteManyPipelineProgressMutation(baseOptions?: Apollo.Mutat export type DeleteManyPipelineProgressMutationHookResult = ReturnType; export type DeleteManyPipelineProgressMutationResult = Apollo.MutationResult; export type DeleteManyPipelineProgressMutationOptions = Apollo.BaseMutationOptions; +export const UpdatePipelineStageDocument = gql` + mutation UpdatePipelineStage($id: String, $name: String) { + updateOnePipelineStage(where: {id: $id}, data: {name: {set: $name}}) { + id + name + } +} + `; +export type UpdatePipelineStageMutationFn = Apollo.MutationFunction; + +/** + * __useUpdatePipelineStageMutation__ + * + * To run a mutation, you first call `useUpdatePipelineStageMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdatePipelineStageMutation` 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 [updatePipelineStageMutation, { data, loading, error }] = useUpdatePipelineStageMutation({ + * variables: { + * id: // value for 'id' + * name: // value for 'name' + * }, + * }); + */ +export function useUpdatePipelineStageMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdatePipelineStageDocument, options); + } +export type UpdatePipelineStageMutationHookResult = ReturnType; +export type UpdatePipelineStageMutationResult = Apollo.MutationResult; +export type UpdatePipelineStageMutationOptions = Apollo.BaseMutationOptions; export const SearchPeopleDocument = gql` query SearchPeople($where: PersonWhereInput, $limit: Int, $orderBy: [PersonOrderByWithRelationInput!]) { searchResults: findManyPerson(where: $where, take: $limit, orderBy: $orderBy) { diff --git a/front/src/modules/command-menu/components/CommandMenu.tsx b/front/src/modules/command-menu/components/CommandMenu.tsx index 585876d1e..e3d2c015b 100644 --- a/front/src/modules/command-menu/components/CommandMenu.tsx +++ b/front/src/modules/command-menu/components/CommandMenu.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useTheme } from '@emotion/react'; import { useRecoilValue } from 'recoil'; -import { useFilteredSearchCompanyQuery } from '@/companies/services'; +import { useFilteredSearchCompanyQuery } from '@/companies/queries'; import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys'; import { AppHotkeyScope } from '@/lib/hotkeys/types/AppHotkeyScope'; import { useFilteredSearchPeopleQuery } from '@/people/services'; diff --git a/front/src/modules/comments/components/comment-thread/CommentThreadRelationPicker.tsx b/front/src/modules/comments/components/comment-thread/CommentThreadRelationPicker.tsx index 50802e142..7a9a6c773 100644 --- a/front/src/modules/comments/components/comment-thread/CommentThreadRelationPicker.tsx +++ b/front/src/modules/comments/components/comment-thread/CommentThreadRelationPicker.tsx @@ -10,7 +10,7 @@ import { import { useHandleCheckableCommentThreadTargetChange } from '@/comments/hooks/useHandleCheckableCommentThreadTargetChange'; import { CompanyChip } from '@/companies/components/CompanyChip'; -import { useFilteredSearchCompanyQuery } from '@/companies/services'; +import { useFilteredSearchCompanyQuery } from '@/companies/queries'; import { usePreviousHotkeyScope } from '@/lib/hotkeys/hooks/usePreviousHotkeyScope'; import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys'; import { PersonChip } from '@/people/components/PersonChip'; diff --git a/front/src/modules/comments/components/right-drawer/CommentThreadActionBar.tsx b/front/src/modules/comments/components/right-drawer/CommentThreadActionBar.tsx index 30bb7e4d2..72bb09c5d 100644 --- a/front/src/modules/comments/components/right-drawer/CommentThreadActionBar.tsx +++ b/front/src/modules/comments/components/right-drawer/CommentThreadActionBar.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import { useRecoilState } from 'recoil'; import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services'; -import { GET_COMPANIES } from '@/companies/services'; +import { GET_COMPANIES } from '@/companies/queries'; import { GET_PEOPLE } from '@/people/services'; import { Button } from '@/ui/components/buttons/Button'; import { IconTrash } from '@/ui/icons'; diff --git a/front/src/modules/comments/hooks/useHandleCheckableCommentThreadTargetChange.ts b/front/src/modules/comments/hooks/useHandleCheckableCommentThreadTargetChange.ts index b54487829..6c0566e7f 100644 --- a/front/src/modules/comments/hooks/useHandleCheckableCommentThreadTargetChange.ts +++ b/front/src/modules/comments/hooks/useHandleCheckableCommentThreadTargetChange.ts @@ -1,7 +1,7 @@ import { getOperationName } from '@apollo/client/utilities'; import { v4 } from 'uuid'; -import { GET_COMPANIES } from '@/companies/services'; +import { GET_COMPANIES } from '@/companies/queries'; import { GET_PEOPLE } from '@/people/services'; import { CommentThread, diff --git a/front/src/modules/comments/hooks/useOpenCreateCommentDrawerForSelectedRowIds.ts b/front/src/modules/comments/hooks/useOpenCreateCommentDrawerForSelectedRowIds.ts index 71aea58b6..e7f427d99 100644 --- a/front/src/modules/comments/hooks/useOpenCreateCommentDrawerForSelectedRowIds.ts +++ b/front/src/modules/comments/hooks/useOpenCreateCommentDrawerForSelectedRowIds.ts @@ -3,7 +3,7 @@ import { useRecoilState, useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { currentUserState } from '@/auth/states/currentUserState'; -import { GET_COMPANIES } from '@/companies/services'; +import { GET_COMPANIES } from '@/companies/queries'; import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope'; import { GET_PEOPLE } from '@/people/services'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; diff --git a/front/src/modules/comments/hooks/useOpenCreateCommentThreadDrawer.ts b/front/src/modules/comments/hooks/useOpenCreateCommentThreadDrawer.ts index f6afdf335..2d1878235 100644 --- a/front/src/modules/comments/hooks/useOpenCreateCommentThreadDrawer.ts +++ b/front/src/modules/comments/hooks/useOpenCreateCommentThreadDrawer.ts @@ -3,7 +3,7 @@ import { useRecoilState, useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { currentUserState } from '@/auth/states/currentUserState'; -import { GET_COMPANIES } from '@/companies/services'; +import { GET_COMPANIES } from '@/companies/queries'; import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope'; import { GET_PEOPLE } from '@/people/services'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; diff --git a/front/src/modules/companies/components/CompanyBoardCard.tsx b/front/src/modules/companies/components/CompanyBoardCard.tsx index f2a9a668b..b8360c3b5 100644 --- a/front/src/modules/companies/components/CompanyBoardCard.tsx +++ b/front/src/modules/companies/components/CompanyBoardCard.tsx @@ -5,7 +5,8 @@ import styled from '@emotion/styled'; import { IconCurrencyDollar } from '@tabler/icons-react'; import { useRecoilState } from 'recoil'; -import { GET_PIPELINES } from '@/pipeline-progress/queries'; +import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState'; +import { GET_PIPELINES } from '@/pipeline-progress/services'; import { BoardCardContext } from '@/pipeline-progress/states/BoardCardContext'; import { pipelineProgressIdScopedState } from '@/pipeline-progress/states/pipelineProgressIdScopedState'; import { selectedBoardCardsState } from '@/pipeline-progress/states/selectedBoardCardsState'; @@ -19,7 +20,6 @@ import { PipelineProgress, useUpdateOnePipelineProgressMutation, } from '~/generated/graphql'; -import { companyProgressesFamilyState } from '~/pages/opportunities/companyProgressesFamilyState'; const StyledBoardCard = styled.div<{ selected: boolean }>` background-color: ${({ theme, selected }) => diff --git a/front/src/modules/companies/components/FilterDropdownCompanySearchSelect.tsx b/front/src/modules/companies/components/FilterDropdownCompanySearchSelect.tsx index b3db8c4bc..f22bbacd3 100644 --- a/front/src/modules/companies/components/FilterDropdownCompanySearchSelect.tsx +++ b/front/src/modules/companies/components/FilterDropdownCompanySearchSelect.tsx @@ -5,7 +5,7 @@ import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState' import { useRecoilScopedValue } from '@/recoil-scope/hooks/useRecoilScopedValue'; import { TableContext } from '@/ui/tables/states/TableContext'; -import { useFilteredSearchCompanyQuery } from '../services'; +import { useFilteredSearchCompanyQuery } from '../queries'; export function FilterDropdownCompanySearchSelect() { const filterDropdownSearchInput = useRecoilScopedValue( diff --git a/front/src/modules/companies/components/NewCompanyBoardCard.tsx b/front/src/modules/companies/components/NewCompanyBoardCard.tsx index 4e4ed04a2..3290cee5e 100644 --- a/front/src/modules/companies/components/NewCompanyBoardCard.tsx +++ b/front/src/modules/companies/components/NewCompanyBoardCard.tsx @@ -4,7 +4,7 @@ import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState' import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect'; import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState'; -import { useFilteredSearchCompanyQuery } from '../services'; +import { useFilteredSearchCompanyQuery } from '../queries'; export function NewCompanyBoardCard() { const [searchFilter] = useRecoilScopedState( diff --git a/front/src/modules/companies/components/NewCompanyProgressButton.tsx b/front/src/modules/companies/components/NewCompanyProgressButton.tsx index 25132bb41..944e1ca62 100644 --- a/front/src/modules/companies/components/NewCompanyProgressButton.tsx +++ b/front/src/modules/companies/components/NewCompanyProgressButton.tsx @@ -4,9 +4,10 @@ import { useRecoilState } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; import { usePreviousHotkeyScope } from '@/lib/hotkeys/hooks/usePreviousHotkeyScope'; -import { GET_PIPELINES } from '@/pipeline-progress/queries'; +import { GET_PIPELINES } from '@/pipeline-progress/services'; import { BoardColumnContext } from '@/pipeline-progress/states/BoardColumnContext'; import { boardState } from '@/pipeline-progress/states/boardState'; +import { currentPipelineState } from '@/pipeline-progress/states/currentPipelineState'; import { pipelineStageIdScopedState } from '@/pipeline-progress/states/pipelineStageIdScopedState'; import { RecoilScope } from '@/recoil-scope/components/RecoilScope'; import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState'; @@ -19,9 +20,8 @@ import { PipelineProgressableType, useCreateOnePipelineProgressMutation, } from '~/generated/graphql'; -import { currentPipelineState } from '~/pages/opportunities/currentPipelineState'; -import { useFilteredSearchCompanyQuery } from '../services'; +import { useFilteredSearchCompanyQuery } from '../queries'; export function NewCompanyProgressButton() { const [isCreatingCard, setIsCreatingCard] = useState(false); diff --git a/front/src/modules/companies/services/__tests__/select.test.ts b/front/src/modules/companies/queries/__tests__/select.test.ts similarity index 100% rename from front/src/modules/companies/services/__tests__/select.test.ts rename to front/src/modules/companies/queries/__tests__/select.test.ts diff --git a/front/src/modules/companies/services/index.ts b/front/src/modules/companies/queries/index.ts similarity index 100% rename from front/src/modules/companies/services/index.ts rename to front/src/modules/companies/queries/index.ts diff --git a/front/src/modules/companies/services/select.ts b/front/src/modules/companies/queries/select.ts similarity index 100% rename from front/src/modules/companies/services/select.ts rename to front/src/modules/companies/queries/select.ts diff --git a/front/src/modules/companies/services/show.ts b/front/src/modules/companies/queries/show.ts similarity index 100% rename from front/src/modules/companies/services/show.ts rename to front/src/modules/companies/queries/show.ts diff --git a/front/src/modules/companies/services/update.ts b/front/src/modules/companies/queries/update.ts similarity index 100% rename from front/src/modules/companies/services/update.ts rename to front/src/modules/companies/queries/update.ts diff --git a/front/src/pages/opportunities/companyProgressesFamilyState.ts b/front/src/modules/companies/states/companyProgressesFamilyState.ts similarity index 100% rename from front/src/pages/opportunities/companyProgressesFamilyState.ts rename to front/src/modules/companies/states/companyProgressesFamilyState.ts diff --git a/front/src/modules/companies/table/components/CompanyEntityTableData.tsx b/front/src/modules/companies/table/components/CompanyEntityTableData.tsx index 0cf6aaa3b..ced7988b0 100644 --- a/front/src/modules/companies/table/components/CompanyEntityTableData.tsx +++ b/front/src/modules/companies/table/components/CompanyEntityTableData.tsx @@ -1,6 +1,6 @@ import { useRecoilState } from 'recoil'; -import { defaultOrderBy } from '@/companies/services'; +import { defaultOrderBy } from '@/companies/queries'; import { isFetchingEntityTableDataState } from '@/ui/tables/states/isFetchingEntityTableDataState'; import { tableRowIdsState } from '@/ui/tables/states/tableRowIdsState'; import { diff --git a/front/src/modules/people/components/PeopleCompanyPicker.tsx b/front/src/modules/people/components/PeopleCompanyPicker.tsx index 6696dc391..02c984fa5 100644 --- a/front/src/modules/people/components/PeopleCompanyPicker.tsx +++ b/front/src/modules/people/components/PeopleCompanyPicker.tsx @@ -1,6 +1,6 @@ import { Key } from 'ts-key-enum'; -import { useFilteredSearchCompanyQuery } from '@/companies/services'; +import { useFilteredSearchCompanyQuery } from '@/companies/queries'; import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys'; import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope'; import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState'; diff --git a/front/src/modules/pipeline-progress/components/BoardActionBarButtonDeletePipelineProgress.tsx b/front/src/modules/pipeline-progress/components/BoardActionBarButtonDeletePipelineProgress.tsx index 80e0c663b..08b13de40 100644 --- a/front/src/modules/pipeline-progress/components/BoardActionBarButtonDeletePipelineProgress.tsx +++ b/front/src/modules/pipeline-progress/components/BoardActionBarButtonDeletePipelineProgress.tsx @@ -6,7 +6,7 @@ import { EntityTableActionBarButton } from '@/ui/components/table/action-bar/Ent import { IconTrash } from '@/ui/icons/index'; import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql'; -import { GET_PIPELINES } from '../queries'; +import { GET_PIPELINES } from '../services'; import { selectedBoardCardsState } from '../states/selectedBoardCardsState'; export function BoardActionBarButtonDeletePipelineProgress() { diff --git a/front/src/modules/pipeline-progress/components/EntityBoard.tsx b/front/src/modules/pipeline-progress/components/EntityBoard.tsx index e0a4b3754..2c83f839a 100644 --- a/front/src/modules/pipeline-progress/components/EntityBoard.tsx +++ b/front/src/modules/pipeline-progress/components/EntityBoard.tsx @@ -62,10 +62,16 @@ export function EntityBoard({ boardOptions }: { boardOptions: BoardOptions }) { [board, updatePipelineProgressStageInDB, setBoard], ); + const sortedBoard = board + ? [...board].sort((a, b) => { + return a.index - b.index; + }) + : []; + return (board?.length ?? 0) > 0 ? ( - {board?.map((column) => ( + {sortedBoard.map((column) => ( { if (pipelineStageId !== column.pipelineStageId) { @@ -58,10 +66,37 @@ export function EntityBoardColumn({ } }, [column, setPipelineStageId, pipelineStageId]); + const [updatePipelineStage] = useUpdatePipelineStageMutation(); + function handleEditColumnTitle(value: string) { + updatePipelineStage({ + variables: { + id: pipelineStageId, + name: value, + }, + }); + setBoard([ + ...(board || []).map((pipelineStage) => { + if (pipelineStage.pipelineStageId === pipelineStageId) { + return { + ...pipelineStage, + name: value, + }; + } + return pipelineStage; + }), + ]); + } + return ( {(droppableProvided) => ( - + {column.pipelineProgressIds.map((pipelineProgressId, index) => ( + ({ get }) => { + const board = get(boardState); + const pipelineStage = board?.find( + (pipelineStage: BoardPipelineStageColumn) => + pipelineStage.pipelineStageId === pipelineStageId, + ); + + const pipelineProgresses = pipelineStage?.pipelineProgressIds.map( + (pipelineProgressId: string) => + get(companyProgressesFamilyState(pipelineProgressId)), + ); + const pipelineStageTotal: number = + pipelineProgresses?.reduce( + (acc: number, curr: any) => acc + curr?.pipelineProgress.amount, + 0, + ) || 0; + + return pipelineStageTotal; + }, +}); diff --git a/front/src/modules/pipeline-stages/services/index.ts b/front/src/modules/pipeline-stages/services/index.ts new file mode 100644 index 000000000..c37c258c7 --- /dev/null +++ b/front/src/modules/pipeline-stages/services/index.ts @@ -0,0 +1 @@ +export * from './update'; diff --git a/front/src/modules/pipeline-stages/services/update.ts b/front/src/modules/pipeline-stages/services/update.ts new file mode 100644 index 000000000..e82790024 --- /dev/null +++ b/front/src/modules/pipeline-stages/services/update.ts @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_PIPELINE_STAGE = gql` + mutation UpdatePipelineStage($id: String, $name: String) { + updateOnePipelineStage(where: { id: $id }, data: { name: { set: $name } }) { + id + name + } + } +`; diff --git a/front/src/modules/pipelines/components/PipelineChip.tsx b/front/src/modules/pipelines/components/PipelineChip.tsx deleted file mode 100644 index 630f46e42..000000000 --- a/front/src/modules/pipelines/components/PipelineChip.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import styled from '@emotion/styled'; - -import { Pipeline } from '~/generated/graphql'; - -type OwnProps = { - opportunity: Pipeline; -}; - -const StyledContainer = styled.span` - align-items: center; - background-color: ${({ theme }) => theme.background.tertiary}; - border-radius: ${({ theme }) => theme.spacing(1)}; - color: ${({ theme }) => theme.font.color.primary}; - display: inline-flex; - gap: ${({ theme }) => theme.spacing(1)}; - padding: ${({ theme }) => theme.spacing(1)}; - - :hover { - filter: brightness(95%); - } -`; - -function PipelineChip({ opportunity }: OwnProps) { - return ( - - {opportunity.icon && {opportunity.icon}} - {opportunity.name} - - ); -} - -export default PipelineChip; diff --git a/front/src/modules/ui/board/components/Board.tsx b/front/src/modules/ui/board/components/Board.tsx index 0894bd9a0..da30f45bf 100644 --- a/front/src/modules/ui/board/components/Board.tsx +++ b/front/src/modules/ui/board/components/Board.tsx @@ -12,6 +12,7 @@ export const StyledBoard = styled.div` export type BoardPipelineStageColumn = { pipelineStageId: string; title: string; + index: number; colorCode?: string; pipelineProgressIds: string[]; }; diff --git a/front/src/modules/ui/board/components/BoardColumn.tsx b/front/src/modules/ui/board/components/BoardColumn.tsx index a28ac304c..3ab2dfdbe 100644 --- a/front/src/modules/ui/board/components/BoardColumn.tsx +++ b/front/src/modules/ui/board/components/BoardColumn.tsx @@ -1,6 +1,10 @@ -import React from 'react'; +import React, { ChangeEvent } from 'react'; import styled from '@emotion/styled'; +import { debounce } from '@/utils/debounce'; + +import { EditColumnTitleInput } from './EditColumnTitleInput'; + export const StyledColumn = styled.div` background-color: ${({ theme }) => theme.background.primary}; display: flex; @@ -26,21 +30,53 @@ export const StyledColumnTitle = styled.h3` margin-bottom: ${({ theme }) => theme.spacing(2)}; `; -export const StyledAmount = styled.div` +const StyledAmount = styled.div` color: ${({ theme }) => theme.font.color.light}; `; type OwnProps = { colorCode?: string; title: string; + pipelineStageId?: string; + onTitleEdit: (title: string) => void; + totalAmount?: number; children: React.ReactNode; }; -export function BoardColumn({ colorCode, title, children }: OwnProps) { +export function BoardColumn({ + colorCode, + title, + onTitleEdit, + totalAmount, + children, +}: OwnProps) { + const [isEditing, setIsEditing] = React.useState(false); + const [internalValue, setInternalValue] = React.useState(title); + + function toggleEditMode() { + setIsEditing(!isEditing); + } + + const debouncedOnUpdate = debounce(onTitleEdit, 200); + const handleChange = (event: ChangeEvent) => { + setInternalValue(event.target.value); + debouncedOnUpdate(event.target.value); + }; + return ( - - • {title} + + {isEditing ? ( + + ) : ( + • {title} + )} + {!!totalAmount && ${totalAmount}} {children} diff --git a/front/src/modules/ui/board/components/ColumnHotkeyScope.ts b/front/src/modules/ui/board/components/ColumnHotkeyScope.ts new file mode 100644 index 000000000..9e490fe40 --- /dev/null +++ b/front/src/modules/ui/board/components/ColumnHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum ColumnHotkeyScope { + EditColumnName = 'EditColumnNameHotkeyScope', +} diff --git a/front/src/modules/ui/board/components/EditColumnTitleInput.tsx b/front/src/modules/ui/board/components/EditColumnTitleInput.tsx new file mode 100644 index 000000000..44ef9eb46 --- /dev/null +++ b/front/src/modules/ui/board/components/EditColumnTitleInput.tsx @@ -0,0 +1,53 @@ +import styled from '@emotion/styled'; + +import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys'; +import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope'; + +import { ColumnHotkeyScope } from './ColumnHotkeyScope'; + +const StyledEditTitleInput = styled.input` + background-color: transparent; + border: none; + &::placeholder, + &::-webkit-input-placeholder { + color: ${({ theme }) => theme.font.color.light}; + font-family: ${({ theme }) => theme.font.family}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + } + color: ${({ color }) => color}; + &:focus { + color: ${({ color }) => color}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + line-height: ${({ theme }) => theme.text.lineHeight}; + } + margin: 0; + margin-bottom: ${({ theme }) => theme.spacing(2)}; + outline: none; +`; + +export function EditColumnTitleInput({ + color, + value, + onChange, + toggleEditMode, +}: { + color?: string; + value: string; + onChange: (event: React.ChangeEvent) => void; + toggleEditMode: () => void; +}) { + const setHotkeyScope = useSetHotkeyScope(); + setHotkeyScope(ColumnHotkeyScope.EditColumnName, { goto: false }); + + useScopedHotkeys('enter', toggleEditMode, ColumnHotkeyScope.EditColumnName); + useScopedHotkeys('esc', toggleEditMode, ColumnHotkeyScope.EditColumnName); + return ( + + ); +} diff --git a/front/src/pages/companies/Companies.tsx b/front/src/pages/companies/Companies.tsx index 38d6f56bb..fd5964aeb 100644 --- a/front/src/pages/companies/Companies.tsx +++ b/front/src/pages/companies/Companies.tsx @@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { v4 as uuidv4 } from 'uuid'; -import { GET_COMPANIES } from '@/companies/services'; +import { GET_COMPANIES } from '@/companies/queries'; import { RecoilScope } from '@/recoil-scope/components/RecoilScope'; import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar'; import { IconBuildingSkyscraper } from '@/ui/icons/index'; diff --git a/front/src/pages/companies/CompanyShow.tsx b/front/src/pages/companies/CompanyShow.tsx index 4da987e95..e0474745f 100644 --- a/front/src/pages/companies/CompanyShow.tsx +++ b/front/src/pages/companies/CompanyShow.tsx @@ -7,7 +7,7 @@ import { CompanyAddressEditableField } from '@/companies/fields/components/Compa import { CompanyCreatedAtEditableField } from '@/companies/fields/components/CompanyCreatedAtEditableField'; import { CompanyDomainNameEditableField } from '@/companies/fields/components/CompanyDomainNameEditableField'; import { CompanyEmployeesEditableField } from '@/companies/fields/components/CompanyEmployeesEditableField'; -import { useCompanyQuery } from '@/companies/services'; +import { useCompanyQuery } from '@/companies/queries'; import { PropertyBox } from '@/ui/components/property-box/PropertyBox'; import { IconBuildingSkyscraper } from '@/ui/icons/index'; import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer'; diff --git a/front/src/pages/companies/CompanyTable.tsx b/front/src/pages/companies/CompanyTable.tsx index 94ffa7f09..a12a63877 100644 --- a/front/src/pages/companies/CompanyTable.tsx +++ b/front/src/pages/companies/CompanyTable.tsx @@ -1,10 +1,7 @@ import { useCallback, useMemo, useState } from 'react'; import { IconList } from '@tabler/icons-react'; -import { - CompaniesSelectedSortType, - defaultOrderBy, -} from '@/companies/services'; +import { CompaniesSelectedSortType, defaultOrderBy } from '@/companies/queries'; import { companyColumns } from '@/companies/table/components/companyColumns'; import { CompanyEntityTableData } from '@/companies/table/components/CompanyEntityTableData'; import { reduceSortsToOrderBy } from '@/lib/filters-and-sorts/helpers'; diff --git a/front/src/pages/companies/__stories__/Company.stories.tsx b/front/src/pages/companies/__stories__/Company.stories.tsx index fe57955f9..a230f83b4 100644 --- a/front/src/pages/companies/__stories__/Company.stories.tsx +++ b/front/src/pages/companies/__stories__/Company.stories.tsx @@ -8,7 +8,7 @@ import { GET_COMMENT_THREADS_BY_TARGETS, } from '@/comments/services'; import { CREATE_COMMENT_THREAD_WITH_COMMENT } from '@/comments/services/create'; -import { GET_COMPANY } from '@/companies/services'; +import { GET_COMPANY } from '@/companies/queries'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { mockedCommentThreads } from '~/testing/mock-data/comment-threads'; import { mockedCompaniesData } from '~/testing/mock-data/companies'; diff --git a/front/src/pages/companies/table/TableActionBarButtonDeleteCompanies.tsx b/front/src/pages/companies/table/TableActionBarButtonDeleteCompanies.tsx index e6dbdba8c..7bda91cb7 100644 --- a/front/src/pages/companies/table/TableActionBarButtonDeleteCompanies.tsx +++ b/front/src/pages/companies/table/TableActionBarButtonDeleteCompanies.tsx @@ -1,7 +1,7 @@ import { getOperationName } from '@apollo/client/utilities'; import { useRecoilValue } from 'recoil'; -import { GET_COMPANIES } from '@/companies/services'; +import { GET_COMPANIES } from '@/companies/queries'; import { EntityTableActionBarButton } from '@/ui/components/table/action-bar/EntityTableActionBarButton'; import { IconTrash } from '@/ui/icons/index'; import { useResetTableRowSelection } from '@/ui/tables/hooks/useResetTableRowSelection'; diff --git a/front/src/pages/opportunities/HookCompanyBoard.tsx b/front/src/pages/opportunities/HookCompanyBoard.tsx index 8dbd20ac4..f84ea9617 100644 --- a/front/src/pages/opportunities/HookCompanyBoard.tsx +++ b/front/src/pages/opportunities/HookCompanyBoard.tsx @@ -1,11 +1,15 @@ import { useEffect } from 'react'; import { useRecoilCallback, useRecoilState } from 'recoil'; +import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState'; import { CompanyForBoard, CompanyProgress, PipelineProgressForBoard, } from '@/companies/types/CompanyProgress'; +import { boardState } from '@/pipeline-progress/states/boardState'; +import { currentPipelineState } from '@/pipeline-progress/states/currentPipelineState'; +import { isBoardLoadedState } from '@/pipeline-progress/states/isBoardLoadedState'; import { BoardPipelineStageColumn } from '@/ui/board/components/Board'; import { Pipeline, @@ -15,11 +19,6 @@ import { useGetPipelinesQuery, } from '~/generated/graphql'; -import { boardState } from '../../modules/pipeline-progress/states/boardState'; - -import { companyProgressesFamilyState } from './companyProgressesFamilyState'; -import { currentPipelineState } from './currentPipelineState'; -import { isBoardLoadedState } from './isBoardLoadedState'; export function HookCompanyBoard() { const [currentPipeline, setCurrentPipeline] = useRecoilState(currentPipelineState); @@ -44,6 +43,7 @@ export function HookCompanyBoard() { pipelineStageId: pipelineStage.id, title: pipelineStage.name, colorCode: pipelineStage.color, + index: pipelineStage.index || 0, pipelineProgressIds: pipelineStage.pipelineProgresses?.map( (item) => item.id as string, diff --git a/front/src/pages/opportunities/currentPipelineState.ts b/front/src/pages/opportunities/currentPipelineState.ts deleted file mode 100644 index c80fb0782..000000000 --- a/front/src/pages/opportunities/currentPipelineState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from 'recoil'; - -import { Pipeline } from '~/generated/graphql'; - -export const currentPipelineState = atom({ - key: 'currentPipelineState', - default: undefined, -}); diff --git a/front/src/pages/opportunities/isBoardLoadedState.ts b/front/src/pages/opportunities/isBoardLoadedState.ts deleted file mode 100644 index 541c5e90e..000000000 --- a/front/src/pages/opportunities/isBoardLoadedState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from 'recoil'; - -export const isBoardLoadedState = atom({ - key: 'isBoardLoadedState', - default: false, -}); diff --git a/front/src/pages/people/PeopleTable.tsx b/front/src/pages/people/PeopleTable.tsx index afb64693b..b62070563 100644 --- a/front/src/pages/people/PeopleTable.tsx +++ b/front/src/pages/people/PeopleTable.tsx @@ -1,7 +1,7 @@ import { useCallback, useMemo, useState } from 'react'; import { IconList } from '@tabler/icons-react'; -import { defaultOrderBy } from '@/companies/services'; +import { defaultOrderBy } from '@/companies/queries'; import { reduceSortsToOrderBy } from '@/lib/filters-and-sorts/helpers'; import { filtersScopedState } from '@/lib/filters-and-sorts/states/filtersScopedState'; import { turnFilterIntoWhereClause } from '@/lib/filters-and-sorts/utils/turnFilterIntoWhereClause'; diff --git a/front/src/testing/graphqlMocks.ts b/front/src/testing/graphqlMocks.ts index bcedb2ab8..f7d264d33 100644 --- a/front/src/testing/graphqlMocks.ts +++ b/front/src/testing/graphqlMocks.ts @@ -3,12 +3,12 @@ import { graphql } from 'msw'; import { CREATE_EVENT } from '@/analytics/services'; import { GET_CLIENT_CONFIG } from '@/client-config/queries'; -import { GET_COMPANIES } from '@/companies/services'; +import { GET_COMPANIES } from '@/companies/queries'; import { GET_PEOPLE, UPDATE_PERSON } from '@/people/services'; import { GET_PIPELINE_PROGRESS, GET_PIPELINES, -} from '@/pipeline-progress/queries'; +} from '@/pipeline-progress/services'; import { SEARCH_COMPANY_QUERY, SEARCH_USER_QUERY, diff --git a/server/src/core/pipeline/resolvers/pipeline-stage.resolver.ts b/server/src/core/pipeline/resolvers/pipeline-stage.resolver.ts index 88bde1b20..84a87356a 100644 --- a/server/src/core/pipeline/resolvers/pipeline-stage.resolver.ts +++ b/server/src/core/pipeline/resolvers/pipeline-stage.resolver.ts @@ -1,4 +1,4 @@ -import { Resolver, Args, Query } from '@nestjs/graphql'; +import { Resolver, Args, Query, Mutation } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; import { accessibleBy } from '@casl/prisma'; import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; @@ -7,13 +7,18 @@ import { FindManyPipelineStageArgs } from '../../../core/@generated/pipeline-sta import { PipelineStageService } from '../services/pipeline-stage.service'; import { AbilityGuard } from 'src/guards/ability.guard'; import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; -import { ReadPipelineStageAbilityHandler } from 'src/ability/handlers/pipeline-stage.ability-handler'; +import { + ReadPipelineStageAbilityHandler, + UpdatePipelineStageAbilityHandler, +} from 'src/ability/handlers/pipeline-stage.ability-handler'; import { UserAbility } from 'src/decorators/user-ability.decorator'; import { AppAbility } from 'src/ability/ability.factory'; import { PrismaSelector, PrismaSelect, } from 'src/decorators/prisma-select.decorator'; +import { UpdateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/update-one-pipeline-stage.args'; +import { Prisma } from '@prisma/client'; @UseGuards(JwtAuthGuard) @Resolver(() => PipelineStage) @@ -43,4 +48,21 @@ export class PipelineStageResolver { select: prismaSelect.value, }); } + + @Mutation(() => PipelineStage, { + nullable: true, + }) + @UseGuards(AbilityGuard) + @CheckAbilities(UpdatePipelineStageAbilityHandler) + async updateOnePipelineStage( + @Args() args: UpdateOnePipelineStageArgs, + @PrismaSelector({ modelName: 'PipelineProgress' }) + prismaSelect: PrismaSelect<'PipelineProgress'>, + ): Promise | null> { + return this.pipelineStageService.update({ + where: args.where, + data: args.data, + select: prismaSelect.value, + } as Prisma.PipelineProgressUpdateArgs); + } }