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:
Lucas Bordeau
2023-08-09 05:08:37 +02:00
committed by GitHub
parent 77d356f78a
commit 3666980ccc
103 changed files with 1551 additions and 922 deletions

View File

@ -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>
);
}

View File

@ -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 <></>;
}