V2 opportunities (#2565)

* changed isSystem to false

* wip

* wip

* wip

* add amount viewfield seed

* seed other viewFields

* upate tenant seeds

* Remove calls to old pipelines

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
bosiraphael
2023-11-17 19:12:22 +01:00
committed by GitHub
parent f62108d539
commit d481da183f
43 changed files with 454 additions and 864 deletions

View File

@ -20,7 +20,7 @@ export const useHandleCheckableActivityTargetChange = ({
objectNameSingular: 'activityTargetV2',
});
const { deleteOneObject } = useDeleteOneObjectRecord({
objectNamePlural: 'activityTargetV2',
objectNameSingular: 'activityTargetV2',
});
return async (

View File

@ -12,7 +12,7 @@ type ActivityActionBarProps = {
export const ActivityActionBar = ({ activityId }: ActivityActionBarProps) => {
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
const { deleteOneObject } = useDeleteOneObjectRecord({
objectNamePlural: 'activitiesV2',
objectNameSingular: 'activityV2',
});
const deleteActivity = () => {

View File

@ -2,6 +2,7 @@ import { ReactNode, useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord';
import { EntityChipVariant } from '@/ui/display/chip/components/EntityChip';
import { IconEye } from '@/ui/display/icon/index';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
@ -17,7 +18,6 @@ import { RecordInlineCell } from '@/ui/object/record-inline-cell/components/Reco
import { InlineCellHotkeyScope } from '@/ui/object/record-inline-cell/types/InlineCellHotkeyScope';
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
import { companyProgressesFamilyState } from '../states/companyProgressesFamilyState';
@ -150,6 +150,30 @@ export const CompanyBoardCard = () => {
BoardRecoilScopeContext,
);
const useUpdateOneObjectMutation: () => [(params: any) => any, any] = () => {
const { updateOneObject } = useUpdateOneObjectRecord({
objectNameSingular: 'opportunityV2',
});
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
updateOneObject?.({
idToUpdate: variables.where.id,
input: variables.data,
});
};
return [updateEntity, { loading: false }];
};
// boardCardId check can be moved to a wrapper to avoid unnecessary logic above
if (!company || !pipelineProgress || !boardCardId) {
return null;
@ -224,8 +248,7 @@ export const CompanyBoardCard = () => {
entityChipDisplayMapper:
viewField.entityChipDisplayMapper,
},
useUpdateEntityMutation:
useUpdateOnePipelineProgressMutation,
useUpdateEntityMutation: useUpdateOneObjectMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>

View File

@ -1,8 +1,12 @@
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { PaginatedObjectTypeResults } from '@/object-record/types/PaginatedObjectTypeResults';
import { pipelineAvailableFieldDefinitions } from '@/pipeline/constants/pipelineAvailableFieldDefinitions';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { useBoardActionBarEntries } from '@/ui/layout/board/hooks/useBoardActionBarEntries';
import { useBoardContext } from '@/ui/layout/board/hooks/useBoardContext';
import { useBoardContextMenuEntries } from '@/ui/layout/board/hooks/useBoardContextMenuEntries';
@ -15,13 +19,7 @@ import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates'
import { useView } from '@/views/hooks/useView';
import { ViewType } from '@/views/types/ViewType';
import { mapViewFieldsToBoardFieldDefinitions } from '@/views/utils/mapViewFieldsToBoardFieldDefinitions';
import {
Pipeline,
PipelineProgressableType,
useGetCompaniesQuery,
useGetPipelineProgressQuery,
useGetPipelinesQuery,
} from '~/generated/graphql';
import { Company } from '~/generated-metadata/graphql';
import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions';
import { useUpdateCompanyBoardCardIds } from '../hooks/useUpdateBoardCardIds';
@ -40,6 +38,10 @@ export const HooksCompanyBoardEffect = () => {
const { currentViewFiltersState, currentViewFieldsState } =
useViewScopedStates();
const [pipelineSteps, setPipelineSteps] = useState<PipelineStep[]>([]);
const [opportunities, setOpportunities] = useState<Opportunity[]>([]);
const [companies, setCompanies] = useState<Company[]>([]);
const currentViewFields = useRecoilValue(currentViewFieldsState);
const currentViewFilters = useRecoilValue(currentViewFiltersState);
@ -56,21 +58,62 @@ export const HooksCompanyBoardEffect = () => {
availableBoardCardFieldsScopedState,
BoardRecoilScopeContext,
);
const updateCompanyBoardCardIds = useUpdateCompanyBoardCardIds();
const updateCompanyBoard = useUpdateCompanyBoard();
const { data: pipelineData, loading: loadingGetPipelines } =
useGetPipelinesQuery({
variables: {
where: {
pipelineProgressableType: {
equals: PipelineProgressableType.Company,
useFindManyObjectRecords({
objectNamePlural: 'pipelineStepsV2',
filter: {},
onCompleted: useCallback(
(data: PaginatedObjectTypeResults<PipelineStep>) => {
setPipelineSteps(data.edges.map((edge) => edge.node));
},
[],
),
});
const whereFilters = useMemo(() => {
return {
AND: [
{
pipelineStageId: {
in: pipelineSteps.map((pipelineStep) => pipelineStep.id),
},
},
},
});
...(currentViewFilters?.map(turnFilterIntoWhereClause) || []),
],
};
}, [currentViewFilters, pipelineSteps]) as any;
const pipeline = pipelineData?.findManyPipeline[0] as Pipeline | undefined;
useFindManyObjectRecords({
skip: !pipelineSteps.length,
objectNamePlural: 'opportunitiesV2',
filter: whereFilters,
onCompleted: useCallback(
(_data: PaginatedObjectTypeResults<Opportunity>) => {
const pipelineProgresses: Array<Opportunity> = [];
updateCompanyBoardCardIds(pipelineProgresses);
setOpportunities(pipelineProgresses);
setIsBoardLoaded(true);
},
[setIsBoardLoaded, updateCompanyBoardCardIds],
),
});
useFindManyObjectRecords({
skip: !opportunities.length,
objectNamePlural: 'companiesV2',
filter: {
id: {
in: opportunities.map((opportuntiy) => opportuntiy.companyId || ''),
},
},
onCompleted: useCallback((data: PaginatedObjectTypeResults<Company>) => {
setCompanies(data.edges.map((edge) => edge.node));
}, []),
});
useEffect(() => {
setAvailableFilterDefinitions(opportunitiesBoardOptions.filterDefinitions);
@ -87,77 +130,32 @@ export const HooksCompanyBoardEffect = () => {
setViewType?.(ViewType.Kanban);
}, [setViewObjectMetadataId, setViewType]);
const pipelineStageIds = pipeline?.pipelineStages
?.map((pipelineStage) => pipelineStage.id)
.flat();
const whereFilters = useMemo(() => {
return {
AND: [
{ pipelineStageId: { in: pipelineStageIds } },
...(currentViewFilters?.map(turnFilterIntoWhereClause) || []),
],
};
}, [currentViewFilters, pipelineStageIds]) as any;
const updateCompanyBoardCardIds = useUpdateCompanyBoardCardIds();
const { data: pipelineProgressData, loading: loadingGetPipelineProgress } =
useGetPipelineProgressQuery({
variables: {
where: whereFilters,
},
onCompleted: (data) => {
const pipelineProgresses = data?.findManyPipelineProgress || [];
updateCompanyBoardCardIds(pipelineProgresses);
setIsBoardLoaded(true);
},
});
const pipelineProgresses = useMemo(() => {
return pipelineProgressData?.findManyPipelineProgress || [];
}, [pipelineProgressData]);
const { data: companiesData, loading: loadingGetCompanies } =
useGetCompaniesQuery({
variables: {
where: {
id: {
in: pipelineProgresses.map((item) => item.companyId || ''),
},
},
},
});
const [searchParams] = useSearchParams();
const loading =
loadingGetPipelines || loadingGetPipelineProgress || loadingGetCompanies;
const loading = !companies;
const { setActionBarEntries } = useBoardActionBarEntries();
const { setContextMenuEntries } = useBoardContextMenuEntries();
useEffect(() => {
if (!loading && pipeline && pipelineProgresses && companiesData) {
if (!loading && opportunities && companies) {
setActionBarEntries();
setContextMenuEntries();
setAvailableBoardCardFields(pipelineAvailableFieldDefinitions);
updateCompanyBoard(pipeline, pipelineProgresses, companiesData.companies);
setEntityCountInCurrentView(companiesData.companies.length);
updateCompanyBoard(pipelineSteps, opportunities, companies);
setEntityCountInCurrentView(companies.length);
}
}, [
loading,
pipeline,
pipelineProgresses,
companiesData,
updateCompanyBoard,
setActionBarEntries,
setContextMenuEntries,
searchParams,
setEntityCountInCurrentView,
setAvailableBoardCardFields,
opportunities,
companies,
pipelineSteps,
]);
useEffect(() => {

View File

@ -1,20 +1,15 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress';
import { GET_PIPELINES } from '@/pipeline/graphql/queries/getPipelines';
import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord';
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { boardCardIdsByColumnIdFamilyState } from '@/ui/layout/board/states/boardCardIdsByColumnIdFamilyState';
import { useCreateOneCompanyPipelineProgressMutation } from '~/generated/graphql';
export const useCreateCompanyProgress = () => {
const [createOneCompanyPipelineProgress] =
useCreateOneCompanyPipelineProgressMutation({
refetchQueries: [
getOperationName(GET_PIPELINE_PROGRESS) ?? '',
getOperationName(GET_PIPELINES) ?? '',
],
const { createOneObject: createOneOpportunity } =
useCreateOneObjectRecord<Opportunity>({
objectNameSingular: 'opportunityV2',
});
const [currentPipeline] = useRecoilState(currentPipelineState);
@ -33,15 +28,11 @@ export const useCreateCompanyProgress = () => {
newUuid,
]);
await createOneCompanyPipelineProgress({
variables: {
uuid: newUuid,
pipelineStageId: pipelineStageId,
pipelineId: currentPipeline?.id ?? '',
companyId: companyId,
},
await createOneOpportunity?.({
pipelineStepId: pipelineStageId,
companyId: companyId,
});
},
[createOneCompanyPipelineProgress, currentPipeline],
[createOneOpportunity, currentPipeline?.id],
);
};

View File

@ -1,15 +1,13 @@
import { useRecoilCallback } from 'recoil';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { boardCardIdsByColumnIdFamilyState } from '@/ui/layout/board/states/boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from '@/ui/layout/board/states/boardColumnsState';
import { GetPipelineProgressQuery } from '~/generated/graphql';
export const useUpdateCompanyBoardCardIds = () =>
useRecoilCallback(
({ snapshot, set }) =>
(
pipelineProgresses: GetPipelineProgressQuery['findManyPipelineProgress'],
) => {
(pipelineProgresses: Opportunity[]) => {
const boardColumns = snapshot
.getLoadable(boardColumnsState)
.valueOrThrow();

View File

@ -1,31 +1,26 @@
import { useRecoilCallback } from 'recoil';
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
import { currentPipelineStepsState } from '@/pipeline/states/currentPipelineStepsState';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { boardCardIdsByColumnIdFamilyState } from '@/ui/layout/board/states/boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from '@/ui/layout/board/states/boardColumnsState';
import { savedBoardColumnsState } from '@/ui/layout/board/states/savedBoardColumnsState';
import { BoardColumnDefinition } from '@/ui/layout/board/types/BoardColumnDefinition';
import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState';
import { isThemeColor } from '@/ui/theme/utils/castStringAsThemeColor';
import { Pipeline } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { logError } from '~/utils/logError';
import { companyProgressesFamilyState } from '../states/companyProgressesFamilyState';
import {
CompanyForBoard,
CompanyProgressDict,
PipelineProgressForBoard,
} from '../types/CompanyProgress';
import { CompanyForBoard, CompanyProgressDict } from '../types/CompanyProgress';
export const useUpdateCompanyBoard = () =>
useRecoilCallback(
({ set, snapshot }) =>
(
pipeline: Pipeline,
pipelineProgresses: (PipelineProgressForBoard & {
pipelineStageId: string;
})[],
pipelineSteps: PipelineStep[],
pipelineProgresses: Opportunity[],
companies: CompanyForBoard[],
) => {
const indexCompanyByIdReducer = (
@ -44,7 +39,7 @@ export const useUpdateCompanyBoard = () =>
const indexPipelineProgressByIdReducer = (
acc: CompanyProgressDict,
pipelineProgress: PipelineProgressForBoard,
pipelineProgress: Opportunity,
) => {
const company =
pipelineProgress.companyId &&
@ -77,21 +72,19 @@ export const useUpdateCompanyBoard = () =>
}
}
const currentPipeline = snapshot
.getLoadable(currentPipelineState)
const currentPipelineSteps = snapshot
.getLoadable(currentPipelineStepsState)
.valueOrThrow();
const currentBoardColumns = snapshot
.getLoadable(boardColumnsState)
.valueOrThrow();
if (!isDeeplyEqual(pipeline, currentPipeline)) {
set(currentPipelineState, pipeline);
if (!isDeeplyEqual(pipelineSteps, currentPipelineSteps)) {
set(currentPipelineStepsState, currentPipelineSteps);
}
const pipelineStages = pipeline?.pipelineStages ?? [];
const orderedPipelineStages = [...pipelineStages].sort((a, b) => {
const orderedPipelineStages = [...pipelineSteps].sort((a, b) => {
if (!a.position || !b.position) return 0;
return a.position - b.position;
});

View File

@ -1,17 +1,8 @@
import { Company, Person, PipelineProgress } from '~/generated/graphql';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { Company } from '~/generated/graphql';
export type CompanyForBoard = Pick<Company, 'id' | 'name' | 'domainName'>;
export type PipelineProgressForBoard = Pick<
PipelineProgress,
| 'id'
| 'amount'
| 'closeDate'
| 'companyId'
| 'probability'
| 'pointOfContactId'
> & {
pointOfContact?: Pick<Person, 'id' | 'displayName'> | null;
};
export type PipelineProgressForBoard = Opportunity;
export type CompanyProgress = {
company: CompanyForBoard;

View File

@ -22,6 +22,7 @@ export const ObjectMetadataNavItems = () => {
return (
<>
{objectMetadataItems.map((objectMetadataItem) => {
if (objectMetadataItem.nameSingular === 'opportunityV2') return null;
return (
<NavItem
key={objectMetadataItem.id}

View File

@ -3,32 +3,37 @@ import { getOperationName } from '@apollo/client/utilities';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { capitalize } from '~/utils/string/capitalize';
export const useDeleteOneObjectRecord = ({
objectNamePlural,
}: Pick<ObjectMetadataItemIdentifier, 'objectNamePlural'>) => {
export const useDeleteOneObjectRecord = <T>({
objectNameSingular,
}: Pick<ObjectMetadataItemIdentifier, 'objectNameSingular'>) => {
const {
foundObjectMetadataItem,
objectNotFoundInMetadata,
findManyQuery,
deleteOneMutation,
} = useFindOneObjectMetadataItem({
objectNamePlural,
objectNameSingular,
});
// TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(deleteOneMutation);
const deleteOneObject = foundObjectMetadataItem
? (idToDelete: string) => {
return mutate({
variables: {
idToDelete,
},
refetchQueries: [getOperationName(findManyQuery) ?? ''],
});
}
: undefined;
const deleteOneObject =
objectNameSingular && foundObjectMetadataItem
? async (idToDelete: string) => {
const deletedObject = await mutate({
variables: {
idToDelete,
},
refetchQueries: [getOperationName(findManyQuery) ?? ''],
});
return deletedObject.data[
`create${capitalize(objectNameSingular)}`
] as T;
}
: undefined;
return {
deleteOneObject,

View File

@ -3,6 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { useDeleteOneObjectRecord } from '@/object-record/hooks/useDeleteOneObjectRecord';
import {
IconCheckbox,
@ -28,6 +29,11 @@ export const useRecordTableContextMenuEntries = () => {
const { scopeId: objectNamePlural, resetTableRowSelection } =
useRecordTable();
const { data } = useGetFavoritesQuery();
const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({
objectNamePlural,
});
const { createFavorite, deleteFavorite, favorites } = useFavorites({
objectNamePlural,
});
@ -53,7 +59,7 @@ export const useRecordTableContextMenuEntries = () => {
});
const { deleteOneObject } = useDeleteOneObjectRecord({
objectNamePlural,
objectNameSingular: foundObjectMetadataItem?.nameSingular,
});
const handleDeleteClick = useRecoilCallback(({ snapshot }) => async () => {

View File

@ -1,21 +0,0 @@
import { gql } from '@apollo/client';
export const CREATE_COMPANY_PIPELINE_PROGRESS = gql`
mutation CreateOneCompanyPipelineProgress(
$uuid: String!
$companyId: String!
$pipelineId: String!
$pipelineStageId: String!
) {
createOnePipelineProgress(
data: {
id: $uuid
company: { connect: { id: $companyId } }
pipeline: { connect: { id: $pipelineId } }
pipelineStage: { connect: { id: $pipelineStageId } }
}
) {
id
}
}
`;

View File

@ -1,11 +0,0 @@
import { gql } from '@apollo/client';
export const CREATE_PIPELINE_STAGE = gql`
mutation CreatePipelineStage($data: PipelineStageCreateInput!) {
pipelineStage: createOnePipelineStage(data: $data) {
id
name
color
}
}
`;

View File

@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const DELETE_PIPELINE_PROGRESS = gql`
mutation DeleteManyPipelineProgress($ids: [String!]) {
deleteManyPipelineProgress(where: { id: { in: $ids } }) {
count
}
}
`;

View File

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

View File

@ -1,18 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_PIPELINE_PROGRESS = gql`
mutation UpdateOnePipelineProgress(
$data: PipelineProgressUpdateInput!
$where: PipelineProgressWhereUniqueInput!
) {
updateOnePipelineProgress(where: $where, data: $data) {
id
amount
closeDate
probability
pointOfContact {
id
}
}
}
`;

View File

@ -1,15 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_PIPELINE_PROGRESS_STAGE = gql`
mutation UpdateOnePipelineProgressStage(
$id: String
$pipelineStageId: String
) {
updateOnePipelineProgress(
where: { id: $id }
data: { pipelineStage: { connect: { id: $pipelineStageId } } }
) {
id
}
}
`;

View File

@ -1,11 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_PIPELINE_STAGE = gql`
mutation UpdatePipelineStage($id: String, $data: PipelineStageUpdateInput!) {
updateOnePipelineStage(where: { id: $id }, data: $data) {
id
name
color
}
}
`;

View File

@ -1,26 +0,0 @@
import { gql } from '@apollo/client';
export const GET_PIPELINE_PROGRESS = gql`
query GetPipelineProgress(
$where: PipelineProgressWhereInput
$orderBy: [PipelineProgressOrderByWithRelationInput!]
) {
findManyPipelineProgress(where: $where, orderBy: $orderBy) {
id
pipelineStageId
companyId
personId
amount
closeDate
pointOfContactId
pointOfContact {
id
firstName
lastName
displayName
avatarUrl
}
probability
}
}
`;

View File

@ -1,17 +0,0 @@
import { gql } from '@apollo/client';
export const GET_PIPELINES = gql`
query GetPipelines($where: PipelineWhereInput) {
findManyPipeline(where: $where) {
id
name
pipelineProgressableType
pipelineStages {
id
name
color
position
}
}
}
`;

View File

@ -1,25 +1,29 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil';
import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord';
import { useDeleteOneObjectRecord } from '@/object-record/hooks/useDeleteOneObjectRecord';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { BoardColumnDefinition } from '@/ui/layout/board/types/BoardColumnDefinition';
import {
useCreatePipelineStageMutation,
useDeletePipelineStageMutation,
} 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 [deletePipelineStageMutation] = useDeletePipelineStageMutation();
const { createOneObject: createOnePipelineStep } =
useCreateOneObjectRecord<PipelineStep>({
objectNameSingular: 'pipelineStepV2',
});
const { deleteOneObject: deleteOnePipelineStep } =
useDeleteOneObjectRecord<PipelineStep>({
objectNameSingular: 'pipelineStepV2',
});
const handlePipelineStageAdd = async (boardColumn: BoardColumnDefinition) => {
if (!currentPipeline?.id) return;
return createPipelineStageMutation({
return createOnePipelineStep?.({
variables: {
data: {
color: boardColumn.colorCode ?? 'gray',
@ -30,17 +34,13 @@ export const usePipelineStages = () => {
type: 'ongoing',
},
},
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
});
};
const handlePipelineStageDelete = async (boardColumnId: string) => {
if (!currentPipeline?.id) return;
return deletePipelineStageMutation({
variables: { where: { id: boardColumnId } },
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
});
return deleteOnePipelineStep?.(boardColumnId);
};
return { handlePipelineStageAdd, handlePipelineStageDelete };

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
export const currentPipelineStepsState = atom<PipelineStep[]>({
key: 'currentPipelineStepsState',
default: [],
});

View File

@ -0,0 +1,17 @@
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { Person } from '~/generated-metadata/graphql';
export type Opportunity = {
id: string;
amount: {
amountMicros: number;
currencyCode: string;
};
closeDate: Date;
probability: number;
pipelineStepId: string;
pipelineStep: PipelineStep;
pointOfContactId: string;
pointOfContact: Pick<Person, 'id' | 'firstName' | 'lastName' | 'avatarUrl'>;
[key: string]: any;
};

View File

@ -0,0 +1,6 @@
export type PipelineStep = {
id: string;
name: string;
color: string;
position: number;
};

View File

@ -1,10 +1,11 @@
import { useCallback, useRef } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useApolloClient } from '@apollo/client';
import styled from '@emotion/styled';
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
import { useRecoilValue } from 'recoil';
import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress';
import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { StyledBoard } from '@/ui/layout/board/components/StyledBoard';
import { BoardColumnContext } from '@/ui/layout/board/contexts/BoardColumnContext';
@ -13,11 +14,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import {
PipelineProgress,
PipelineStage,
useUpdateOnePipelineProgressStageMutation,
} from '~/generated/graphql';
import { PipelineProgress, PipelineStage } from '~/generated/graphql';
import { logError } from '~/utils/logError';
import { useCurrentCardSelected } from '../hooks/useCurrentCardSelected';
@ -59,8 +56,12 @@ export const EntityBoard = ({
const boardColumns = useRecoilValue(boardColumnsState);
const setCardSelected = useSetCardSelected();
const [updatePipelineProgressStage] =
useUpdateOnePipelineProgressStageMutation();
const { updateOneObject: updateOneOpportunity } =
useUpdateOneObjectRecord<Opportunity>({
objectNameSingular: 'opportunityV2',
});
const apolloClient = useApolloClient();
const { unselectAllActiveCards } = useCurrentCardSelected();
@ -69,33 +70,25 @@ export const EntityBoard = ({
pipelineProgressId: NonNullable<PipelineProgress['id']>,
pipelineStageId: NonNullable<PipelineStage['id']>,
) => {
updatePipelineProgressStage({
variables: {
await updateOneOpportunity?.({
idToUpdate: pipelineProgressId,
input: {
pipelineStepId: pipelineStageId,
},
});
const cache = apolloClient.cache;
cache.modify({
id: cache.identify({
id: pipelineProgressId,
pipelineStageId,
__typename: 'PipelineProgress',
}),
fields: {
pipelineStageId: () => pipelineStageId,
},
optimisticResponse: {
__typename: 'Mutation',
updateOnePipelineProgress: {
__typename: 'PipelineProgress',
id: pipelineProgressId,
},
},
update: (cache) => {
cache.modify({
id: cache.identify({
id: pipelineProgressId,
__typename: 'PipelineProgress',
}),
fields: {
pipelineStageId: () => pipelineStageId,
},
});
},
refetchQueries: [getOperationName(GET_PIPELINE_PROGRESS) ?? ''],
});
},
[updatePipelineProgressStage],
[apolloClient.cache, updateOneOpportunity],
);
useListenClickOutsideByClassName({

View File

@ -1,10 +1,9 @@
import { useSetRecoilState } from 'recoil';
import { IconTrash } from '@/ui/display/icon';
import { useDeleteSelectedBoardCards } from '@/ui/layout/board/hooks/useDeleteSelectedBoardCards';
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
import { useDeleteSelectedBoardCards } from './useDeleteSelectedBoardCards';
export const useBoardActionBarEntries = () => {
const setActionBarEntries = useSetRecoilState(actionBarEntriesState);

View File

@ -1,7 +1,8 @@
import { useRecoilState } from 'recoil';
import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
import { useUpdatePipelineStageMutation } from '~/generated/graphql';
import { boardColumnsState } from '../states/boardColumnsState';
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
@ -11,19 +12,20 @@ export const useBoardColumns = () => {
const { handleColumnMove } = useMoveViewColumns();
const [updatePipelineStageMutation] = useUpdatePipelineStageMutation();
const { updateOneObject: updateOnePipelineStep } =
useUpdateOneObjectRecord<PipelineStep>({
objectNameSingular: 'pipelineStepV2',
});
const updatedPipelineStages = (stages: BoardColumnDefinition[]) => {
if (!stages.length) return;
return Promise.all(
stages.map((stage) =>
updatePipelineStageMutation({
variables: {
data: {
position: stage.position,
},
id: stage.id,
updateOnePipelineStep?.({
idToUpdate: stage.id,
input: {
position: stage.position,
},
}),
),

View File

@ -1,10 +1,9 @@
import { useSetRecoilState } from 'recoil';
import { IconTrash } from '@/ui/display/icon';
import { useDeleteSelectedBoardCards } from '@/ui/layout/board/hooks/useDeleteSelectedBoardCards';
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
import { useDeleteSelectedBoardCards } from './useDeleteSelectedBoardCards';
export const useBoardContextMenuEntries = () => {
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);

View File

@ -1,40 +1,41 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil';
import { useApolloClient } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { GET_PIPELINES } from '@/pipeline/graphql/queries/getPipelines';
import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql';
import { useDeleteOneObjectRecord } from '@/object-record/hooks/useDeleteOneObjectRecord';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { selectedCardIdsSelector } from '../states/selectors/selectedCardIdsSelector';
import { useRemoveCardIds } from './useRemoveCardIds';
export const useDeleteSelectedBoardCards = () => {
const selectedCardIds = useRecoilValue(selectedCardIdsSelector);
const removeCardIds = useRemoveCardIds();
const apolloClient = useApolloClient();
const [deletePipelineProgress] = useDeleteManyPipelineProgressMutation({
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
});
const { deleteOneObject: deleteOneOpportunity } =
useDeleteOneObjectRecord<Opportunity>({
objectNameSingular: 'opportunityV2',
});
const deleteSelectedBoardCards = async () => {
await deletePipelineProgress({
variables: {
ids: selectedCardIds,
},
optimisticResponse: {
__typename: 'Mutation',
deleteManyPipelineProgress: {
count: selectedCardIds.length,
},
},
update: (cache) => {
const deleteSelectedBoardCards = useRecoilCallback(
({ snapshot }) =>
async () => {
const selectedCardIds = snapshot
.getLoadable(selectedCardIdsSelector)
.getValue();
await Promise.all(
selectedCardIds.map(async (id) => {
await deleteOneOpportunity?.(id);
}),
);
removeCardIds(selectedCardIds);
selectedCardIds.forEach((id) => {
cache.evict({ id: `PipelineProgress:${id}` });
apolloClient.cache.evict({ id: `Opportunity:${id}` });
});
},
});
};
[apolloClient.cache, deleteOneOpportunity, removeCardIds],
);
return deleteSelectedBoardCards;
};