Feat/generic editable board card (#1089)
* Fixed BoardColumnMenu * Fixed naming * Optimized board loading * Added GenericEditableField * Introduce GenericEditableField for BoardCards * remove logs * delete unused files * fix stories --------- Co-authored-by: corentin <corentin@twenty.com>
This commit is contained in:
@ -1,27 +1,21 @@
|
||||
import { ReactNode, useCallback, useContext } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { ReactNode, useContext } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
|
||||
import { PipelineProgressPointOfContactEditableField } from '@/pipeline/editable-field/components/PipelineProgressPointOfContactEditableField';
|
||||
import { ProbabilityEditableField } from '@/pipeline/editable-field/components/ProbabilityEditableField';
|
||||
import { GET_PIPELINE_PROGRESS, GET_PIPELINES } from '@/pipeline/queries';
|
||||
import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext';
|
||||
import { fieldsDefinitionsState } from '@/ui/board/states/fieldsDefinitionsState';
|
||||
import { selectedBoardCardIdsState } from '@/ui/board/states/selectedBoardCardIdsState';
|
||||
import { EntityChipVariant } from '@/ui/chip/components/EntityChip';
|
||||
import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField';
|
||||
import { NumberEditableField } from '@/ui/editable-field/variants/components/NumberEditableField';
|
||||
import { IconCurrencyDollar, IconProgressCheck } from '@/ui/icon';
|
||||
import { IconCalendarEvent } from '@/ui/icon';
|
||||
import { GenericEditableField } from '@/ui/editable-field/components/GenericEditableField';
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxVariant,
|
||||
} from '@/ui/input/checkbox/components/Checkbox';
|
||||
import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
|
||||
import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
|
||||
import { PipelineProgressForBoard } from '../types/CompanyProgress';
|
||||
import { companyProgressesFamilyState } from '../states/companyProgressesFamilyState';
|
||||
|
||||
import { CompanyChip } from './CompanyChip';
|
||||
|
||||
@ -106,8 +100,6 @@ const StyledFieldContainer = styled.div`
|
||||
`;
|
||||
|
||||
export function CompanyBoardCard() {
|
||||
const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation();
|
||||
|
||||
const boardCardId = useContext(BoardCardIdContext);
|
||||
|
||||
const [companyProgress] = useRecoilState(
|
||||
@ -118,6 +110,7 @@ export function CompanyBoardCard() {
|
||||
const [selectedBoardCards, setSelectedBoardCards] = useRecoilState(
|
||||
selectedBoardCardIdsState,
|
||||
);
|
||||
const fieldsDefinitions = useRecoilValue(fieldsDefinitionsState);
|
||||
|
||||
const selected = selectedBoardCards.includes(boardCardId ?? '');
|
||||
|
||||
@ -131,25 +124,6 @@ export function CompanyBoardCard() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCardUpdate = useCallback(
|
||||
async (pipelineProgress: PipelineProgressForBoard) => {
|
||||
await updatePipelineProgress({
|
||||
variables: {
|
||||
id: pipelineProgress.id,
|
||||
amount: pipelineProgress.amount,
|
||||
closeDate: pipelineProgress.closeDate,
|
||||
probability: pipelineProgress.probability,
|
||||
pointOfContactId: pipelineProgress.pointOfContactId || undefined,
|
||||
},
|
||||
refetchQueries: [
|
||||
getOperationName(GET_PIPELINE_PROGRESS) ?? '',
|
||||
getOperationName(GET_PIPELINES) ?? '',
|
||||
],
|
||||
});
|
||||
},
|
||||
[updatePipelineProgress],
|
||||
);
|
||||
|
||||
if (!company || !pipelineProgress) {
|
||||
return null;
|
||||
}
|
||||
@ -171,71 +145,40 @@ export function CompanyBoardCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledBoardCardWrapper>
|
||||
<StyledBoardCard
|
||||
selected={selected}
|
||||
onClick={() => setSelected(!selected)}
|
||||
>
|
||||
<StyledBoardCardHeader>
|
||||
<CompanyChip
|
||||
id={company.id}
|
||||
name={company.name}
|
||||
pictureUrl={getLogoUrlFromDomainName(company.domainName)}
|
||||
variant={EntityChipVariant.Transparent}
|
||||
/>
|
||||
<StyledCheckboxContainer className="checkbox-container">
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
onChange={() => setSelected(!selected)}
|
||||
variant={CheckboxVariant.Secondary}
|
||||
<EntityUpdateMutationHookContext.Provider
|
||||
value={useUpdateOnePipelineProgressMutation}
|
||||
>
|
||||
<StyledBoardCardWrapper>
|
||||
<StyledBoardCard
|
||||
selected={selected}
|
||||
onClick={() => setSelected(!selected)}
|
||||
>
|
||||
<StyledBoardCardHeader>
|
||||
<CompanyChip
|
||||
id={company.id}
|
||||
name={company.name}
|
||||
pictureUrl={getLogoUrlFromDomainName(company.domainName)}
|
||||
variant={EntityChipVariant.Transparent}
|
||||
/>
|
||||
</StyledCheckboxContainer>
|
||||
</StyledBoardCardHeader>
|
||||
<StyledBoardCardBody>
|
||||
<PreventSelectOnClickContainer>
|
||||
<DateEditableField
|
||||
icon={<IconCalendarEvent />}
|
||||
value={pipelineProgress.closeDate}
|
||||
onSubmit={(value) =>
|
||||
handleCardUpdate({
|
||||
...pipelineProgress,
|
||||
closeDate: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</PreventSelectOnClickContainer>
|
||||
<PreventSelectOnClickContainer>
|
||||
<NumberEditableField
|
||||
icon={<IconCurrencyDollar />}
|
||||
placeholder="Opportunity amount"
|
||||
value={pipelineProgress.amount}
|
||||
onSubmit={(value) =>
|
||||
handleCardUpdate({
|
||||
...pipelineProgress,
|
||||
amount: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</PreventSelectOnClickContainer>
|
||||
<PreventSelectOnClickContainer>
|
||||
<ProbabilityEditableField
|
||||
icon={<IconProgressCheck />}
|
||||
value={pipelineProgress.probability}
|
||||
onSubmit={(value) => {
|
||||
handleCardUpdate({
|
||||
...pipelineProgress,
|
||||
probability: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</PreventSelectOnClickContainer>
|
||||
<PreventSelectOnClickContainer>
|
||||
<PipelineProgressPointOfContactEditableField
|
||||
pipelineProgress={pipelineProgress}
|
||||
/>
|
||||
</PreventSelectOnClickContainer>
|
||||
</StyledBoardCardBody>
|
||||
</StyledBoardCard>
|
||||
</StyledBoardCardWrapper>
|
||||
<StyledCheckboxContainer className="checkbox-container">
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
onChange={() => setSelected(!selected)}
|
||||
variant={CheckboxVariant.Secondary}
|
||||
/>
|
||||
</StyledCheckboxContainer>
|
||||
</StyledBoardCardHeader>
|
||||
<StyledBoardCardBody>
|
||||
{fieldsDefinitions.map((viewField) => {
|
||||
return (
|
||||
<PreventSelectOnClickContainer key={viewField.id}>
|
||||
<GenericEditableField viewField={viewField} />
|
||||
</PreventSelectOnClickContainer>
|
||||
);
|
||||
})}
|
||||
</StyledBoardCardBody>
|
||||
</StyledBoardCard>
|
||||
</StyledBoardCardWrapper>
|
||||
</EntityUpdateMutationHookContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,24 +1,13 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useInitializeCompanyBoardFilters } from '@/companies/hooks/useInitializeCompanyBoardFilters';
|
||||
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
|
||||
import {
|
||||
CompanyForBoard,
|
||||
CompanyProgress,
|
||||
PipelineProgressForBoard,
|
||||
} from '@/companies/types/CompanyProgress';
|
||||
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
|
||||
import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState';
|
||||
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
|
||||
import { pipelineViewFields } from '@/pipeline/constants/pipelineViewFields';
|
||||
import { fieldsDefinitionsState } from '@/ui/board/states/fieldsDefinitionsState';
|
||||
import { isBoardLoadedState } from '@/ui/board/states/isBoardLoadedState';
|
||||
import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import {
|
||||
GetPipelineProgressQuery,
|
||||
PipelineProgressableType,
|
||||
PipelineProgressOrderByWithRelationInput as PipelineProgresses_Order_By,
|
||||
} from '~/generated/graphql';
|
||||
@ -29,83 +18,44 @@ import {
|
||||
useGetPipelinesQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { useUpdateCompanyBoardCardIds } from '../hooks/useUpdateBoardCardIds';
|
||||
import { useUpdateCompanyBoard } from '../hooks/useUpdateCompanyBoardColumns';
|
||||
import { CompanyBoardContext } from '../states/CompanyBoardContext';
|
||||
|
||||
export function HooksCompanyBoard({
|
||||
availableFilters,
|
||||
orderBy,
|
||||
}: {
|
||||
availableFilters: FilterDefinition[];
|
||||
orderBy: PipelineProgresses_Order_By[];
|
||||
}) {
|
||||
useInitializeCompanyBoardFilters({
|
||||
availableFilters,
|
||||
});
|
||||
const setFieldsDefinitionsState = useSetRecoilState(fieldsDefinitionsState);
|
||||
|
||||
const [currentPipeline] = useRecoilState(currentPipelineState);
|
||||
const [, setBoardColumns] = useRecoilState(boardColumnsState);
|
||||
useEffect(() => {
|
||||
setFieldsDefinitionsState(pipelineViewFields);
|
||||
});
|
||||
|
||||
const [, setIsBoardLoaded] = useRecoilState(isBoardLoadedState);
|
||||
|
||||
const updateBoardColumns = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(pipeline: Pipeline) => {
|
||||
const currentPipeline = snapshot
|
||||
.getLoadable(currentPipelineState)
|
||||
.valueOrThrow();
|
||||
const filters = useRecoilScopedValue(filtersScopedState, CompanyBoardContext);
|
||||
|
||||
const currentBoardColumns = snapshot
|
||||
.getLoadable(boardColumnsState)
|
||||
.valueOrThrow();
|
||||
const updateCompanyBoard = useUpdateCompanyBoard();
|
||||
|
||||
if (JSON.stringify(pipeline) !== JSON.stringify(currentPipeline)) {
|
||||
set(currentPipelineState, pipeline);
|
||||
}
|
||||
|
||||
const pipelineStages = pipeline?.pipelineStages ?? [];
|
||||
|
||||
const orderedPipelineStages = [...pipelineStages].sort((a, b) => {
|
||||
if (!a.index || !b.index) return 0;
|
||||
return a.index - b.index;
|
||||
});
|
||||
|
||||
const newBoardColumns: BoardColumnDefinition[] =
|
||||
orderedPipelineStages?.map((pipelineStage) => ({
|
||||
id: pipelineStage.id,
|
||||
title: pipelineStage.name,
|
||||
colorCode: pipelineStage.color,
|
||||
index: pipelineStage.index ?? 0,
|
||||
}));
|
||||
|
||||
if (
|
||||
JSON.stringify(currentBoardColumns) !==
|
||||
JSON.stringify(newBoardColumns)
|
||||
) {
|
||||
setBoardColumns(newBoardColumns);
|
||||
}
|
||||
const { data: pipelineData, loading: loadingGetPipelines } =
|
||||
useGetPipelinesQuery({
|
||||
variables: {
|
||||
where: {
|
||||
pipelineProgressableType: {
|
||||
equals: PipelineProgressableType.Company,
|
||||
},
|
||||
},
|
||||
},
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
useGetPipelinesQuery({
|
||||
variables: {
|
||||
where: {
|
||||
pipelineProgressableType: { equals: PipelineProgressableType.Company },
|
||||
},
|
||||
},
|
||||
onCompleted: async (data) => {
|
||||
const pipeline = data?.findManyPipeline[0] as Pipeline;
|
||||
const pipeline = pipelineData?.findManyPipeline[0] as Pipeline | undefined;
|
||||
|
||||
updateBoardColumns(pipeline);
|
||||
},
|
||||
});
|
||||
|
||||
const pipelineStageIds = currentPipeline?.pipelineStages
|
||||
const pipelineStageIds = pipeline?.pipelineStages
|
||||
?.map((pipelineStage) => pipelineStage.id)
|
||||
.flat();
|
||||
|
||||
const filters = useRecoilScopedValue(filtersScopedState, CompanyBoardContext);
|
||||
|
||||
const whereFilters = useMemo(() => {
|
||||
return {
|
||||
AND: [
|
||||
@ -115,114 +65,52 @@ export function HooksCompanyBoard({
|
||||
};
|
||||
}, [filters, pipelineStageIds]) as any;
|
||||
|
||||
const updateBoardCardIds = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(
|
||||
pipelineProgresses: GetPipelineProgressQuery['findManyPipelineProgress'],
|
||||
) => {
|
||||
const boardColumns = snapshot
|
||||
.getLoadable(boardColumnsState)
|
||||
.valueOrThrow();
|
||||
const updateCompanyBoardCardIds = useUpdateCompanyBoardCardIds();
|
||||
|
||||
for (const boardColumn of boardColumns) {
|
||||
const boardCardIds = pipelineProgresses
|
||||
.filter(
|
||||
(pipelineProgressToFilter) =>
|
||||
pipelineProgressToFilter.pipelineStageId === boardColumn.id,
|
||||
)
|
||||
.map((pipelineProgress) => pipelineProgress.id);
|
||||
|
||||
set(boardCardIdsByColumnIdFamilyState(boardColumn.id), boardCardIds);
|
||||
}
|
||||
const { data: pipelineProgressData, loading: loadingGetPipelineProgress } =
|
||||
useGetPipelineProgressQuery({
|
||||
variables: {
|
||||
where: whereFilters,
|
||||
orderBy,
|
||||
},
|
||||
[],
|
||||
);
|
||||
onCompleted: (data) => {
|
||||
const pipelineProgresses = data?.findManyPipelineProgress || [];
|
||||
|
||||
const pipelineProgressesQuery = useGetPipelineProgressQuery({
|
||||
variables: {
|
||||
where: whereFilters,
|
||||
orderBy,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
const pipelineProgresses = data?.findManyPipelineProgress || [];
|
||||
updateCompanyBoardCardIds(pipelineProgresses);
|
||||
|
||||
updateBoardCardIds(pipelineProgresses);
|
||||
setIsBoardLoaded(true);
|
||||
},
|
||||
});
|
||||
|
||||
setIsBoardLoaded(true);
|
||||
},
|
||||
});
|
||||
const pipelineProgresses = useMemo(() => {
|
||||
return pipelineProgressData?.findManyPipelineProgress || [];
|
||||
}, [pipelineProgressData]);
|
||||
|
||||
const pipelineProgresses =
|
||||
pipelineProgressesQuery.data?.findManyPipelineProgress || [];
|
||||
|
||||
const entitiesQueryResult = useGetCompaniesQuery({
|
||||
variables: {
|
||||
where: {
|
||||
id: {
|
||||
in: pipelineProgresses.map((item) => item.companyId || ''),
|
||||
const { data: companiesData, loading: loadingGetCompanies } =
|
||||
useGetCompaniesQuery({
|
||||
variables: {
|
||||
where: {
|
||||
id: {
|
||||
in: pipelineProgresses.map((item) => item.companyId || ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const indexCompanyByIdReducer = (
|
||||
acc: { [key: string]: CompanyForBoard },
|
||||
company: CompanyForBoard,
|
||||
) => ({
|
||||
...acc,
|
||||
[company.id]: company,
|
||||
});
|
||||
|
||||
const companiesDict =
|
||||
entitiesQueryResult.data?.companies.reduce(
|
||||
indexCompanyByIdReducer,
|
||||
{} as { [key: string]: CompanyForBoard },
|
||||
) || {};
|
||||
|
||||
const indexPipelineProgressByIdReducer = (
|
||||
acc: {
|
||||
[key: string]: CompanyProgress;
|
||||
},
|
||||
pipelineProgress: PipelineProgressForBoard,
|
||||
) => {
|
||||
const company =
|
||||
pipelineProgress.companyId && companiesDict[pipelineProgress.companyId];
|
||||
if (!company) return acc;
|
||||
return {
|
||||
...acc,
|
||||
[pipelineProgress.id]: {
|
||||
pipelineProgress,
|
||||
company,
|
||||
},
|
||||
};
|
||||
};
|
||||
const companyBoardIndex = pipelineProgresses.reduce(
|
||||
indexPipelineProgressByIdReducer,
|
||||
{} as { [key: string]: CompanyProgress },
|
||||
);
|
||||
|
||||
const synchronizeCompanyProgresses = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(companyBoardIndex: { [key: string]: CompanyProgress }) => {
|
||||
Object.entries(companyBoardIndex).forEach(([id, companyProgress]) => {
|
||||
if (
|
||||
JSON.stringify(
|
||||
snapshot.getLoadable(companyProgressesFamilyState(id)).getValue(),
|
||||
) !== JSON.stringify(companyProgress)
|
||||
) {
|
||||
set(companyProgressesFamilyState(id), companyProgress);
|
||||
}
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
const loading =
|
||||
entitiesQueryResult.loading || pipelineProgressesQuery.loading;
|
||||
loadingGetPipelines || loadingGetPipelineProgress || loadingGetCompanies;
|
||||
|
||||
useEffect(() => {
|
||||
!loading && synchronizeCompanyProgresses(companyBoardIndex);
|
||||
}, [loading, companyBoardIndex, synchronizeCompanyProgresses]);
|
||||
if (!loading && pipeline && pipelineProgresses && companiesData) {
|
||||
updateCompanyBoard(pipeline, pipelineProgresses, companiesData.companies);
|
||||
}
|
||||
}, [
|
||||
loading,
|
||||
pipeline,
|
||||
pipelineProgresses,
|
||||
companiesData,
|
||||
updateCompanyBoard,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user