Fix/opportunities board (#2610)

* WIP

* wip

* update pipelineStepId

* rename pipeline stage to pipeline step

* rename pipelineProgress to Opportunity

* fix UUID type not queried

* fix boardColumnTotal

* fix micros

* fixing filters, sorts and fields

* wip

* wip

* Fix opportunity board re-render

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
Charles Bochet
2023-11-21 01:24:25 +01:00
committed by GitHub
parent a33d4c8b8d
commit 09533e286b
36 changed files with 364 additions and 277 deletions

View File

@ -143,7 +143,7 @@ export const CompanyBoardCard = () => {
const showCompactView = isCompactViewEnabled && isCardInCompactView;
const { pipelineProgress, company } = companyProgress ?? {};
const { opportunity, company } = companyProgress ?? {};
const visibleBoardCardFields = useRecoilScopedValue(
visibleBoardCardFieldsScopedSelector,
@ -175,7 +175,7 @@ export const CompanyBoardCard = () => {
};
// boardCardId check can be moved to a wrapper to avoid unnecessary logic above
if (!company || !pipelineProgress || !boardCardId) {
if (!company || !opportunity || !boardCardId) {
return null;
}

View File

@ -17,7 +17,7 @@ export type CompanyProgressPickerProps = {
companyId: string | null;
onSubmit: (
newCompanyId: EntityForSelect | null,
newPipelineStageId: string | null,
newPipelineStepId: string | null,
) => void;
onCancel?: () => void;
};
@ -39,40 +39,40 @@ export const CompanyProgressPicker = ({
const [isProgressSelectionUnfolded, setIsProgressSelectionUnfolded] =
useState(false);
const [selectedPipelineStageId, setSelectedPipelineStageId] = useState<
const [selectedPipelineStepId, setSelectedPipelineStepId] = useState<
string | null
>(null);
const [currentPipeline] = useRecoilState(currentPipelineState);
const currentPipelineStages = useMemo(
() => currentPipeline?.pipelineStages ?? [],
const currentPipelineSteps = useMemo(
() => currentPipeline?.pipelineSteps ?? [],
[currentPipeline],
);
const handlePipelineStageChange = (newPipelineStageId: string) => {
setSelectedPipelineStageId(newPipelineStageId);
const handlePipelineStepChange = (newPipelineStepId: string) => {
setSelectedPipelineStepId(newPipelineStepId);
setIsProgressSelectionUnfolded(false);
};
const handleEntitySelected = async (
selectedCompany: EntityForSelect | null | undefined,
) => {
onSubmit(selectedCompany ?? null, selectedPipelineStageId);
onSubmit(selectedCompany ?? null, selectedPipelineStepId);
};
useEffect(() => {
if (currentPipelineStages?.[0]?.id) {
setSelectedPipelineStageId(currentPipelineStages?.[0]?.id);
if (currentPipelineSteps?.[0]?.id) {
setSelectedPipelineStepId(currentPipelineSteps?.[0]?.id);
}
}, [currentPipelineStages]);
}, [currentPipelineSteps]);
const selectedPipelineStage = useMemo(
const selectedPipelineStep = useMemo(
() =>
currentPipelineStages.find(
(pipelineStage: any) => pipelineStage.id === selectedPipelineStageId,
currentPipelineSteps.find(
(pipelineStep: any) => pipelineStep.id === selectedPipelineStepId,
),
[currentPipelineStages, selectedPipelineStageId],
[currentPipelineSteps, selectedPipelineStepId],
);
return (
@ -82,14 +82,14 @@ export const CompanyProgressPicker = ({
>
{isProgressSelectionUnfolded ? (
<DropdownMenuItemsContainer>
{currentPipelineStages.map((pipelineStage: any, index: number) => (
{currentPipelineSteps.map((pipelineStep: any, index: number) => (
<MenuItem
key={pipelineStage.id}
key={pipelineStep.id}
testId={`select-pipeline-stage-${index}`}
onClick={() => {
handlePipelineStageChange(pipelineStage.id);
handlePipelineStepChange(pipelineStep.id);
}}
text={pipelineStage.name}
text={pipelineStep.name}
/>
))}
</DropdownMenuItemsContainer>
@ -100,7 +100,7 @@ export const CompanyProgressPicker = ({
EndIcon={IconChevronDown}
onClick={() => setIsProgressSelectionUnfolded(true)}
>
{selectedPipelineStage?.name}
{selectedPipelineStep?.name}
</DropdownMenuHeader>
<DropdownMenuSeparator />
<DropdownMenuSearchInput

View File

@ -1,10 +1,13 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { Company } from '@/companies/types/Company';
import { useComputeDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useComputeDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { PaginatedObjectTypeResults } from '@/object-record/types/PaginatedObjectTypeResults';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { useBoardActionBarEntries } from '@/ui/layout/board/hooks/useBoardActionBarEntries';
@ -14,11 +17,12 @@ import { availableBoardCardFieldsScopedState } from '@/ui/layout/board/states/av
import { boardCardFieldsScopedState } from '@/ui/layout/board/states/boardCardFieldsScopedState';
import { isBoardLoadedState } from '@/ui/layout/board/states/isBoardLoadedState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2';
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 { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useUpdateCompanyBoardCardIds } from '../hooks/useUpdateBoardCardIds';
import { useUpdateCompanyBoard } from '../hooks/useUpdateCompanyBoardColumns';
@ -41,6 +45,13 @@ export const HooksCompanyBoardEffect = () => {
const currentViewFields = useRecoilValue(currentViewFieldsState);
const { objectMetadataItem } = useObjectMetadataItem({
objectNamePlural: 'opportunities',
});
const { columnDefinitions, filterDefinitions, sortDefinitions } =
useComputeDefinitionsFromFieldMetadata(objectMetadataItem);
const [, setIsBoardLoaded] = useRecoilState(isBoardLoadedState);
const { BoardRecoilScopeContext } = useBoardContext();
@ -50,10 +61,6 @@ export const HooksCompanyBoardEffect = () => {
BoardRecoilScopeContext,
);
const [, setAvailableBoardCardFields] = useRecoilScopedState(
availableBoardCardFieldsScopedState,
BoardRecoilScopeContext,
);
const updateCompanyBoardCardIds = useUpdateCompanyBoardCardIds();
const updateCompanyBoard = useUpdateCompanyBoard();
@ -70,9 +77,9 @@ export const HooksCompanyBoardEffect = () => {
const whereFilters = useMemo(() => {
return {
AND: [
and: [
{
pipelineStageId: {
pipelineStepId: {
in: pipelineSteps.map((pipelineStep) => pipelineStep.id),
},
},
@ -86,8 +93,10 @@ export const HooksCompanyBoardEffect = () => {
objectNamePlural: 'opportunities',
filter: whereFilters,
onCompleted: useCallback(
(_data: PaginatedObjectTypeResults<Opportunity>) => {
const pipelineProgresses: Array<Opportunity> = [];
(data: PaginatedObjectTypeResults<Opportunity>) => {
const pipelineProgresses: Array<Opportunity> = data.edges.map(
(edge) => edge.node,
);
updateCompanyBoardCardIds(pipelineProgresses);
@ -112,15 +121,63 @@ export const HooksCompanyBoardEffect = () => {
});
useEffect(() => {
setAvailableFilterDefinitions(opportunitiesBoardOptions.filterDefinitions);
setAvailableSortDefinitions?.(opportunitiesBoardOptions.sortDefinitions);
setAvailableFieldDefinitions?.([]);
if (!objectMetadataItem) {
return;
}
setAvailableFilterDefinitions?.(filterDefinitions);
setAvailableSortDefinitions?.(sortDefinitions);
setAvailableFieldDefinitions?.(columnDefinitions);
}, [
columnDefinitions,
filterDefinitions,
objectMetadataItem,
setAvailableFieldDefinitions,
setAvailableFilterDefinitions,
setAvailableSortDefinitions,
sortDefinitions,
]);
const setAvailableBoardCardFields = useRecoilCallback(
({ snapshot, set }) =>
(availableBoardCardFields: any) => {
const availableBoardCardFieldsFromState = snapshot
.getLoadable(
availableBoardCardFieldsScopedState({
scopeId: 'company-board-view',
}),
)
.getValue();
if (
!isDeeplyEqual(
availableBoardCardFieldsFromState,
availableBoardCardFields,
)
) {
set(
availableBoardCardFieldsScopedState({
scopeId: 'company-board-view',
}),
availableBoardCardFields,
);
}
},
[],
);
useRecoilScopedStateV2(
availableBoardCardFieldsScopedState,
'company-board-view',
);
useEffect(() => {
const availableTableColumns = columnDefinitions.filter(
filterAvailableTableColumns,
);
setAvailableBoardCardFields(availableTableColumns);
}, [columnDefinitions, setAvailableBoardCardFields]);
useEffect(() => {
setViewObjectMetadataId?.('company');
setViewType?.(ViewType.Kanban);
@ -137,21 +194,19 @@ export const HooksCompanyBoardEffect = () => {
if (!loading && opportunities && companies) {
setActionBarEntries();
setContextMenuEntries();
setAvailableBoardCardFields([]);
updateCompanyBoard(pipelineSteps, opportunities, companies);
setEntityCountInCurrentView(companies.length);
}
}, [
companies,
loading,
updateCompanyBoard,
opportunities,
pipelineSteps,
setActionBarEntries,
setContextMenuEntries,
searchParams,
setEntityCountInCurrentView,
setAvailableBoardCardFields,
opportunities,
companies,
pipelineSteps,
updateCompanyBoard,
]);
useEffect(() => {

View File

@ -12,7 +12,7 @@ export const NewCompanyProgressButton = () => {
const [isCreatingCard, setIsCreatingCard] = useState(false);
const column = useContext(BoardColumnContext);
const pipelineStageId = column?.columnDefinition.id || '';
const pipelineStepId = column?.columnDefinition.id || '';
const { enqueueSnackBar } = useSnackBar();
@ -25,7 +25,7 @@ export const NewCompanyProgressButton = () => {
setIsCreatingCard(false);
goBackToPreviousHotkeyScope();
if (!pipelineStageId) {
if (!pipelineStepId) {
enqueueSnackBar('Pipeline stage id is not defined', {
variant: 'error',
});
@ -33,7 +33,7 @@ export const NewCompanyProgressButton = () => {
throw new Error('Pipeline stage id is not defined');
}
//createCompanyProgress(company.id, pipelineStageId);
//createCompanyProgress(company.id, pipelineStepId);
};
const handleNewClick = useCallback(() => {

View File

@ -7,7 +7,7 @@ import { boardColumnsState } from '@/ui/layout/board/states/boardColumnsState';
export const useUpdateCompanyBoardCardIds = () =>
useRecoilCallback(
({ snapshot, set }) =>
(pipelineProgresses: Opportunity[]) => {
(pipelineProgresses: Pick<Opportunity, 'pipelineStepId' | 'id'>[]) => {
const boardColumns = snapshot
.getLoadable(boardColumnsState)
.valueOrThrow();
@ -16,7 +16,7 @@ export const useUpdateCompanyBoardCardIds = () =>
const boardCardIds = pipelineProgresses
.filter(
(pipelineProgressToFilter) =>
pipelineProgressToFilter.pipelineStageId === boardColumn.id,
pipelineProgressToFilter.pipelineStepId === boardColumn.id,
)
.map((pipelineProgress) => pipelineProgress.id);

View File

@ -20,7 +20,7 @@ export const useUpdateCompanyBoard = () =>
({ set, snapshot }) =>
(
pipelineSteps: PipelineStep[],
pipelineProgresses: Opportunity[],
opportunities: Opportunity[],
companies: CompanyForBoard[],
) => {
const indexCompanyByIdReducer = (
@ -37,27 +37,26 @@ export const useUpdateCompanyBoard = () =>
{} as { [key: string]: CompanyForBoard },
) ?? {};
const indexPipelineProgressByIdReducer = (
const indexOpportunityByIdReducer = (
acc: CompanyProgressDict,
pipelineProgress: Opportunity,
opportunity: Opportunity,
) => {
const company =
pipelineProgress.companyId &&
companiesDict[pipelineProgress.companyId];
opportunity.companyId && companiesDict[opportunity.companyId];
if (!company) return acc;
return {
...acc,
[pipelineProgress.id]: {
pipelineProgress,
[opportunity.id]: {
opportunity,
company,
},
};
};
const companyBoardIndex = pipelineProgresses.reduce(
indexPipelineProgressByIdReducer,
const companyBoardIndex = opportunities.reduce(
indexOpportunityByIdReducer,
{} as CompanyProgressDict,
);
@ -68,7 +67,7 @@ export const useUpdateCompanyBoard = () =>
if (!isDeeplyEqual(currentCompanyProgress, companyProgress)) {
set(companyProgressesFamilyState(id), companyProgress);
set(entityFieldsFamilyState(id), companyProgress.pipelineProgress);
set(entityFieldsFamilyState(id), companyProgress.opportunity);
}
}
@ -81,29 +80,29 @@ export const useUpdateCompanyBoard = () =>
.valueOrThrow();
if (!isDeeplyEqual(pipelineSteps, currentPipelineSteps)) {
set(currentPipelineStepsState, currentPipelineSteps);
set(currentPipelineStepsState, pipelineSteps);
}
const orderedPipelineStages = [...pipelineSteps].sort((a, b) => {
const orderedPipelineSteps = [...pipelineSteps].sort((a, b) => {
if (!a.position || !b.position) return 0;
return a.position - b.position;
});
const newBoardColumns: BoardColumnDefinition[] =
orderedPipelineStages?.map((pipelineStage) => {
if (!isThemeColor(pipelineStage.color)) {
orderedPipelineSteps?.map((pipelineStep) => {
if (!isThemeColor(pipelineStep.color)) {
logError(
`Color ${pipelineStage.color} is not recognized in useUpdateCompanyBoard.`,
`Color ${pipelineStep.color} is not recognized in useUpdateCompanyBoard.`,
);
}
return {
id: pipelineStage.id,
title: pipelineStage.name,
colorCode: isThemeColor(pipelineStage.color)
? pipelineStage.color
id: pipelineStep.id,
title: pipelineStep.name,
colorCode: isThemeColor(pipelineStep.color)
? pipelineStep.color
: undefined,
position: pipelineStage.position ?? 0,
position: pipelineStep.position ?? 0,
};
});
if (currentBoardColumns.length === 0) {
@ -111,12 +110,12 @@ export const useUpdateCompanyBoard = () =>
set(savedBoardColumnsState, newBoardColumns);
}
for (const boardColumn of newBoardColumns) {
const boardCardIds = pipelineProgresses
const boardCardIds = opportunities
.filter(
(pipelineProgressToFilter) =>
pipelineProgressToFilter.pipelineStageId === boardColumn.id,
(opportunityToFilter) =>
opportunityToFilter.pipelineStepId === boardColumn.id,
)
.map((pipelineProgress) => pipelineProgress.id);
.map((opportunity) => opportunity.id);
const currentBoardCardIds = snapshot
.getLoadable(boardCardIdsByColumnIdFamilyState(boardColumn.id))

View File

@ -2,11 +2,10 @@ import { Company } from '@/companies/types/Company';
import { Opportunity } from '@/pipeline/types/Opportunity';
export type CompanyForBoard = Pick<Company, 'id' | 'name' | 'domainName'>;
export type PipelineProgressForBoard = Opportunity;
export type CompanyProgress = {
company: CompanyForBoard;
pipelineProgress: PipelineProgressForBoard;
opportunity: Opportunity;
};
export type CompanyProgressDict = {