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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +1,34 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { MainIdentifierMapper } from '@/ui/object/field/types/MainIdentifierMapper'; import { AvatarType } from '@/users/components/Avatar';
import { Nullable } from '~/types/Nullable'; import { Nullable } from '~/types/Nullable';
export const useObjectMainIdentifier = ( export const useObjectMainIdentifier = (
objectMetadataItem?: Nullable<ObjectMetadataItem>, objectMetadataItem?: Nullable<ObjectMetadataItem>,
) => { ) => {
if (!objectMetadataItem) { if (!objectMetadataItem) {
return { return {};
mainIdentifierMapper: undefined,
mainIdentifierFieldMetadataId: undefined,
basePathToShowPage: undefined,
};
} }
const mainIdentifierMapper: MainIdentifierMapper = (record: any) => { const labelIdentifierFieldPaths = ['person', 'workspaceMember'].includes(
if (objectMetadataItem.nameSingular === 'company') { objectMetadataItem.nameSingular,
return { )
id: record.id, ? ['name.firstName', 'name.lastName']
name: record.name, : ['name'];
avatarUrl: record.avatarUrl, const imageIdentifierFormat: AvatarType = ['company'].includes(
avatarType: 'squared', objectMetadataItem.nameSingular,
record: record, )
}; ? 'squared'
} : 'rounded';
const imageIdentifierUrlPrefix = ['company'].includes(
if (objectMetadataItem.nameSingular === 'workspaceMember') { objectMetadataItem.nameSingular,
return { )
id: record.id, ? 'https://favicon.twenty.com/'
name: record.name.firstName + ' ' + record.name.lastName, : '';
avatarUrl: record.avatarUrl, const imageIdentifierUrlField = ['company'].includes(
avatarType: 'rounded', objectMetadataItem.nameSingular,
record: record, )
}; ? 'domainName'
} : 'avatarUrl';
return {
id: record.id,
name: record.name,
avatarUrl: record.avatarUrl,
avatarType: 'rounded',
record: record,
};
};
const mainIdentifierFieldMetadataId = objectMetadataItem.fields.find( const mainIdentifierFieldMetadataId = objectMetadataItem.fields.find(
({ name }) => name === 'name', ({ name }) => name === 'name',
@ -50,7 +37,10 @@ export const useObjectMainIdentifier = (
const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`; const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`;
return { return {
mainIdentifierMapper, labelIdentifierFieldPaths,
imageIdentifierUrlField,
imageIdentifierUrlPrefix,
imageIdentifierFormat,
mainIdentifierFieldMetadataId, mainIdentifierFieldMetadataId,
basePathToShowPage, basePathToShowPage,
}; };

View File

@ -1,8 +1,7 @@
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType'; import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata'; import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
import { MainIdentifierMapper } from '@/ui/object/field/types/MainIdentifierMapper';
import { ColumnDefinition } from '@/ui/object/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/ui/object/record-table/types/ColumnDefinition';
import { getLogoUrlFromDomainName } from '~/utils'; import { AvatarType } from '@/users/components/Avatar';
import { FieldMetadataItem } from '../types/FieldMetadataItem'; import { FieldMetadataItem } from '../types/FieldMetadataItem';
@ -18,34 +17,26 @@ export const formatFieldMetadataItemAsColumnDefinition = ({
const relationObjectMetadataItem = const relationObjectMetadataItem =
field.toRelationMetadata?.fromObjectMetadata; field.toRelationMetadata?.fromObjectMetadata;
const mainIdentifierMapper: MainIdentifierMapper = (record: any) => { const labelIdentifierFieldPaths = ['person', 'workspaceMember'].includes(
if (relationObjectMetadataItem?.nameSingular === 'company') { relationObjectMetadataItem?.nameSingular ?? '',
return { )
id: record.id, ? ['name.firstName', 'name.lastName']
name: record.name, : ['name'];
avatarUrl: getLogoUrlFromDomainName(record.domainName), const imageIdentifierFormat: AvatarType = ['company'].includes(
avatarType: 'squared', relationObjectMetadataItem?.nameSingular ?? '',
record: record, )
}; ? 'squared'
} : 'rounded';
if (relationObjectMetadataItem?.nameSingular === 'workspaceMember') { const imageIdentifierUrlPrefix = ['company'].includes(
return { relationObjectMetadataItem?.nameSingular ?? '',
id: record.id, )
name: record.name.firstName + ' ' + record.name.lastName, ? 'https://favicon.twenty.com/'
avatarUrl: record.avatarUrl, : '';
avatarType: 'rounded', const imageIdentifierUrlField = ['company'].includes(
record: record, relationObjectMetadataItem?.nameSingular ?? '',
}; )
} ? 'domainName'
: 'avatarUrl';
return {
id: record.id,
name: record.name,
avatarUrl: record.avatarUrl,
avatarType: 'rounded',
record: record,
};
};
return { return {
position, position,
@ -56,11 +47,15 @@ export const formatFieldMetadataItemAsColumnDefinition = ({
metadata: { metadata: {
fieldName: field.name, fieldName: field.name,
placeHolder: field.label, placeHolder: field.label,
mainIdentifierMapper: mainIdentifierMapper, labelIdentifierFieldPaths,
imageIdentifierUrlField,
imageIdentifierUrlPrefix,
imageIdentifierFormat,
relationType: parseFieldRelationType(field), relationType: parseFieldRelationType(field),
searchFields: ['name'], searchFields: ['name'],
objectMetadataNamePlural: relationObjectMetadataItem?.namePlural, objectMetadataNamePlural: relationObjectMetadataItem?.namePlural ?? '',
objectMetadataNameSingular: relationObjectMetadataItem?.nameSingular, objectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular ?? '',
}, },
iconName: field.icon ?? 'Icon123', iconName: field.icon ?? 'Icon123',
isVisible: true, isVisible: true,

View File

@ -22,9 +22,12 @@ export const RecordTableEffect = () => {
}); });
const { const {
mainIdentifierMapper,
basePathToShowPage, basePathToShowPage,
mainIdentifierFieldMetadataId, mainIdentifierFieldMetadataId,
labelIdentifierFieldPaths,
imageIdentifierUrlField,
imageIdentifierUrlPrefix,
imageIdentifierFormat,
} = useObjectMainIdentifier(objectMetadataItem); } = useObjectMainIdentifier(objectMetadataItem);
const { columnDefinitions, filterDefinitions, sortDefinitions } = const { columnDefinitions, filterDefinitions, sortDefinitions } =
@ -40,13 +43,12 @@ export const RecordTableEffect = () => {
} = useView(); } = useView();
useEffect(() => { useEffect(() => {
if ( if (basePathToShowPage && mainIdentifierFieldMetadataId) {
mainIdentifierMapper &&
basePathToShowPage &&
mainIdentifierFieldMetadataId
) {
setObjectMetadataConfig?.({ setObjectMetadataConfig?.({
mainIdentifierMapper, labelIdentifierFieldPaths,
imageIdentifierUrlField,
imageIdentifierUrlPrefix,
imageIdentifierFormat,
basePathToShowPage, basePathToShowPage,
mainIdentifierFieldMetadataId, mainIdentifierFieldMetadataId,
}); });
@ -55,8 +57,11 @@ export const RecordTableEffect = () => {
basePathToShowPage, basePathToShowPage,
objectMetadataItem, objectMetadataItem,
mainIdentifierFieldMetadataId, mainIdentifierFieldMetadataId,
mainIdentifierMapper,
setObjectMetadataConfig, setObjectMetadataConfig,
labelIdentifierFieldPaths,
imageIdentifierUrlField,
imageIdentifierUrlPrefix,
imageIdentifierFormat,
]); ]);
useEffect(() => { useEffect(() => {

View File

@ -1,3 +1,4 @@
import { useCallback } from 'react';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
@ -20,20 +21,25 @@ export const useDeleteOneObjectRecord = <T>({
// TODO: type this with a minimal type at least with Record<string, any> // TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(deleteOneMutation); const [mutate] = useMutation(deleteOneMutation);
const deleteOneObject = const deleteOneObject = useCallback(
objectNameSingular && foundObjectMetadataItem async (idToDelete: string) => {
? async (idToDelete: string) => { if (objectNameSingular && foundObjectMetadataItem) {
const deletedObject = await mutate({ const deletedObject = await mutate({
variables: { variables: {
idToDelete, idToDelete,
}, },
refetchQueries: [getOperationName(findManyQuery) ?? ''], refetchQueries: [getOperationName(findManyQuery) ?? ''],
}); });
return deletedObject.data[
`create${capitalize(objectNameSingular)}` return deletedObject.data[
] as T; `create${capitalize(objectNameSingular)}`
} ] as T;
: undefined; }
return null;
},
[foundObjectMetadataItem, mutate, objectNameSingular, findManyQuery],
);
return { return {
deleteOneObject, deleteOneObject,

View File

@ -16,5 +16,12 @@ export const filterAvailableTableColumns = (
return false; return false;
} }
if (
isFieldRelation(columnDefinition) &&
columnDefinition.metadata?.fieldName === 'pipelineStep'
) {
return false;
}
return true; return true;
}; };

View File

@ -19,7 +19,7 @@ export const PipelineAddButton = () => {
const handleCompanySelected = ( const handleCompanySelected = (
selectedCompany: EntityForSelect | null, selectedCompany: EntityForSelect | null,
selectedPipelineStageId: string | null, selectedPipelineStepId: string | null,
) => { ) => {
if (!selectedCompany?.id) { if (!selectedCompany?.id) {
enqueueSnackBar( enqueueSnackBar(
@ -33,7 +33,7 @@ export const PipelineAddButton = () => {
return; return;
} }
if (!selectedPipelineStageId) { if (!selectedPipelineStepId) {
enqueueSnackBar( enqueueSnackBar(
'There was a problem with the pipeline stage selection, please retry.', 'There was a problem with the pipeline stage selection, please retry.',
{ {
@ -45,7 +45,7 @@ export const PipelineAddButton = () => {
return; return;
} }
closeDropdown(); closeDropdown();
//createCompanyProgress(selectedCompany.id, selectedPipelineStageId); //createCompanyProgress(selectedCompany.id, selectedPipelineStepId);
}; };
return ( return (

View File

@ -7,7 +7,7 @@ import { BoardColumnDefinition } from '@/ui/layout/board/types/BoardColumnDefini
import { currentPipelineState } from '../states/currentPipelineState'; import { currentPipelineState } from '../states/currentPipelineState';
export const usePipelineStages = () => { export const usePipelineSteps = () => {
const currentPipeline = useRecoilValue(currentPipelineState); const currentPipeline = useRecoilValue(currentPipelineState);
const { createOneObject: createOnePipelineStep } = const { createOneObject: createOnePipelineStep } =
@ -20,7 +20,7 @@ export const usePipelineStages = () => {
objectNameSingular: 'pipelineStep', objectNameSingular: 'pipelineStep',
}); });
const handlePipelineStageAdd = async (boardColumn: BoardColumnDefinition) => { const handlePipelineStepAdd = async (boardColumn: BoardColumnDefinition) => {
if (!currentPipeline?.id) return; if (!currentPipeline?.id) return;
return createOnePipelineStep?.({ return createOnePipelineStep?.({
@ -37,11 +37,11 @@ export const usePipelineStages = () => {
}); });
}; };
const handlePipelineStageDelete = async (boardColumnId: string) => { const handlePipelineStepDelete = async (boardColumnId: string) => {
if (!currentPipeline?.id) return; if (!currentPipeline?.id) return;
return deleteOnePipelineStep?.(boardColumnId); return deleteOnePipelineStep?.(boardColumnId);
}; };
return { handlePipelineStageAdd, handlePipelineStageDelete }; return { handlePipelineStepAdd, handlePipelineStepDelete };
}; };

View File

@ -2,6 +2,7 @@ import { Person } from '@/people/types/Person';
import { PipelineStep } from '@/pipeline/types/PipelineStep'; import { PipelineStep } from '@/pipeline/types/PipelineStep';
export type Opportunity = { export type Opportunity = {
[key: string]: any;
id: string; id: string;
amount: { amount: {
amountMicros: number; amountMicros: number;
@ -13,5 +14,4 @@ export type Opportunity = {
pipelineStep: PipelineStep; pipelineStep: PipelineStep;
pointOfContactId: string; pointOfContactId: string;
pointOfContact: Pick<Person, 'id' | 'name' | 'avatarUrl'>; pointOfContact: Pick<Person, 'id' | 'name' | 'avatarUrl'>;
[key: string]: any;
}; };

View File

@ -92,7 +92,10 @@ export const SettingsObjectFieldPreview = ({
const { const {
defaultValue: relationDefaultValue, defaultValue: relationDefaultValue,
recordMapper: mainIdentifierMapper, labelIdentifierFieldPaths,
imageIdentifierUrlField,
imageIdentifierUrlPrefix,
imageIdentifierFormat,
} = useRelationFieldPreview({ } = useRelationFieldPreview({
relationObjectMetadataId, relationObjectMetadataId,
skipDefaultValue: skipDefaultValue:
@ -104,6 +107,15 @@ export const SettingsObjectFieldPreview = ({
? relationDefaultValue ? relationDefaultValue
: dataTypes[fieldMetadata.type].defaultValue; : dataTypes[fieldMetadata.type].defaultValue;
if (
!labelIdentifierFieldPaths ||
!imageIdentifierUrlField ||
!imageIdentifierUrlPrefix ||
!imageIdentifierFormat
) {
return <></>;
}
return ( return (
<StyledContainer className={className}> <StyledContainer className={className}>
<StyledObjectSummary> <StyledObjectSummary>
@ -146,7 +158,13 @@ export const SettingsObjectFieldPreview = ({
iconName: 'FieldIcon', iconName: 'FieldIcon',
fieldMetadataId: fieldMetadata.id || '', fieldMetadataId: fieldMetadata.id || '',
label: fieldMetadata.label, label: fieldMetadata.label,
metadata: { fieldName, mainIdentifierMapper }, metadata: {
fieldName,
labelIdentifierFieldPaths,
imageIdentifierUrlField,
imageIdentifierUrlPrefix,
imageIdentifierFormat,
},
}, },
hotkeyScope: 'field-preview', hotkeyScope: 'field-preview',
}} }}

View File

@ -1,7 +1,6 @@
import { useObjectMainIdentifier } from '@/object-metadata/hooks/useObjectMainIdentifier'; import { useObjectMainIdentifier } from '@/object-metadata/hooks/useObjectMainIdentifier';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { MainIdentifierMapper } from '@/ui/object/field/types/MainIdentifierMapper';
export const useRelationFieldPreview = ({ export const useRelationFieldPreview = ({
relationObjectMetadataId, relationObjectMetadataId,
@ -21,24 +20,18 @@ export const useRelationFieldPreview = ({
skip: skipDefaultValue || !relationObjectMetadataItem, skip: skipDefaultValue || !relationObjectMetadataItem,
}); });
const { mainIdentifierMapper } = useObjectMainIdentifier( const {
relationObjectMetadataItem, labelIdentifierFieldPaths,
); imageIdentifierUrlField,
imageIdentifierUrlPrefix,
const recordMapper: MainIdentifierMapper | undefined = imageIdentifierFormat,
relationObjectMetadataItem && mainIdentifierMapper } = useObjectMainIdentifier(relationObjectMetadataItem);
? (record: { id: string }) => {
const mappedRecord = mainIdentifierMapper(record);
return {
...mappedRecord,
name: mappedRecord.name || relationObjectMetadataItem.labelSingular,
};
}
: undefined;
return { return {
defaultValue: relationObjects?.[0], defaultValue: relationObjects?.[0],
recordMapper, labelIdentifierFieldPaths,
imageIdentifierUrlField,
imageIdentifierUrlPrefix,
imageIdentifierFormat,
}; };
}; };

View File

@ -74,6 +74,7 @@ export const BoardOptionsDropdownContent = ({
hiddenBoardCardFieldsScopedSelector, hiddenBoardCardFieldsScopedSelector,
BoardRecoilScopeContext, BoardRecoilScopeContext,
); );
const hasHiddenFields = hiddenBoardCardFields.length > 0; const hasHiddenFields = hiddenBoardCardFields.length > 0;
const visibleBoardCardFields = useRecoilScopedValue( const visibleBoardCardFields = useRecoilScopedValue(
visibleBoardCardFieldsScopedSelector, visibleBoardCardFieldsScopedSelector,

View File

@ -65,11 +65,11 @@ export const EntityBoard = ({
const { unselectAllActiveCards } = useCurrentCardSelected(); const { unselectAllActiveCards } = useCurrentCardSelected();
const updatePipelineProgressStageInDB = useCallback( const updatePipelineProgressStageInDB = useCallback(
async (pipelineProgressId: string, pipelineStageId: string) => { async (pipelineProgressId: string, pipelineStepId: string) => {
await updateOneOpportunity?.({ await updateOneOpportunity?.({
idToUpdate: pipelineProgressId, idToUpdate: pipelineProgressId,
input: { input: {
pipelineStepId: pipelineStageId, pipelineStepId: pipelineStepId,
}, },
}); });
@ -80,7 +80,7 @@ export const EntityBoard = ({
__typename: 'PipelineProgress', __typename: 'PipelineProgress',
}), }),
fields: { fields: {
pipelineStageId: () => pipelineStageId, pipelineStepId: () => pipelineStepId,
}, },
}); });
}, },

View File

@ -1,3 +1,4 @@
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { IconTrash } from '@/ui/display/icon'; import { IconTrash } from '@/ui/display/icon';
@ -5,19 +6,22 @@ import { useDeleteSelectedBoardCards } from '@/ui/layout/board/hooks/useDeleteSe
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState'; import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
export const useBoardActionBarEntries = () => { export const useBoardActionBarEntries = () => {
const setActionBarEntries = useSetRecoilState(actionBarEntriesState); const setActionBarEntriesRecoil = useSetRecoilState(actionBarEntriesState);
const deleteSelectedBoardCards = useDeleteSelectedBoardCards(); const deleteSelectedBoardCards = useDeleteSelectedBoardCards();
const setActionBarEntries = useCallback(() => {
setActionBarEntriesRecoil([
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: deleteSelectedBoardCards,
},
]);
}, [deleteSelectedBoardCards, setActionBarEntriesRecoil]);
return { return {
setActionBarEntries: () => setActionBarEntries,
setActionBarEntries([
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: deleteSelectedBoardCards,
},
]),
}; };
}; };

View File

@ -17,7 +17,7 @@ export const useBoardColumns = () => {
objectNameSingular: 'pipelineStep', objectNameSingular: 'pipelineStep',
}); });
const updatedPipelineStages = (stages: BoardColumnDefinition[]) => { const updatedPipelineSteps = (stages: BoardColumnDefinition[]) => {
if (!stages.length) return; if (!stages.length) return;
return Promise.all( return Promise.all(
@ -33,7 +33,7 @@ export const useBoardColumns = () => {
}; };
const persistBoardColumns = async () => { const persistBoardColumns = async () => {
await updatedPipelineStages(boardColumns); await updatedPipelineSteps(boardColumns);
}; };
const handleMoveBoardColumn = ( const handleMoveBoardColumn = (

View File

@ -1,3 +1,4 @@
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { IconTrash } from '@/ui/display/icon'; import { IconTrash } from '@/ui/display/icon';
@ -5,19 +6,24 @@ import { useDeleteSelectedBoardCards } from '@/ui/layout/board/hooks/useDeleteSe
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState'; import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
export const useBoardContextMenuEntries = () => { export const useBoardContextMenuEntries = () => {
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); const setContextMenuEntriesRecoil = useSetRecoilState(
contextMenuEntriesState,
);
const deleteSelectedBoardCards = useDeleteSelectedBoardCards(); const deleteSelectedBoardCards = useDeleteSelectedBoardCards();
const setContextMenuEntries = useCallback(() => {
setContextMenuEntriesRecoil([
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: deleteSelectedBoardCards,
},
]);
}, [deleteSelectedBoardCards, setContextMenuEntriesRecoil]);
return { return {
setContextMenuEntries: () => setContextMenuEntries,
setContextMenuEntries([
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: () => deleteSelectedBoardCards(),
},
]),
}; };
}; };

View File

@ -34,7 +34,7 @@ export const useDeleteSelectedBoardCards = () => {
apolloClient.cache.evict({ id: `Opportunity:${id}` }); apolloClient.cache.evict({ id: `Opportunity:${id}` });
}); });
}, },
[apolloClient.cache, deleteOneOpportunity, removeCardIds], [apolloClient.cache, removeCardIds, deleteOneOpportunity],
); );
return deleteSelectedBoardCards; return deleteSelectedBoardCards;

View File

@ -1,13 +1,11 @@
import { atomFamily } from 'recoil';
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata'; import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
import { BoardFieldDefinition } from '../types/BoardFieldDefinition'; import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
export const availableBoardCardFieldsScopedState = atomFamily< export const availableBoardCardFieldsScopedState = createScopedState<
BoardFieldDefinition<FieldMetadata>[], BoardFieldDefinition<FieldMetadata>[]
string
>({ >({
key: 'availableBoardCardFieldsScopedState', key: 'availableBoardCardFieldsScopedState',
default: [], defaultValue: [],
}); });

View File

@ -9,20 +9,21 @@ import { boardCardIdsByColumnIdFamilyState } from '../boardCardIdsByColumnIdFami
export const boardColumnTotalsFamilySelector = selectorFamily({ export const boardColumnTotalsFamilySelector = selectorFamily({
key: 'boardColumnTotalsFamilySelector', key: 'boardColumnTotalsFamilySelector',
get: get:
(pipelineStageId: string) => (pipelineStepId: string) =>
({ get }) => { ({ get }) => {
const cardIds = get(boardCardIdsByColumnIdFamilyState(pipelineStageId)); const cardIds = get(boardCardIdsByColumnIdFamilyState(pipelineStepId));
const pipelineProgresses = cardIds.map((pipelineProgressId: string) => const opportunities = cardIds.map((opportunityId: string) =>
get(companyProgressesFamilyState(pipelineProgressId)), get(companyProgressesFamilyState(opportunityId)),
); );
const pipelineStageTotal: number = const pipelineStepTotal: number =
pipelineProgresses?.reduce( opportunities?.reduce(
(acc: number, curr: any) => acc + curr?.pipelineProgress.amount, (acc: number, curr: any) =>
acc + curr?.opportunity.amount.amountMicros / 1000000,
0, 0,
) || 0; ) || 0;
return pipelineStageTotal; return pipelineStepTotal;
}, },
}); });

View File

@ -11,7 +11,7 @@ export const hiddenBoardCardFieldsScopedSelector = selectorFamily({
const fields = get(boardCardFieldsScopedState(scopeId)); const fields = get(boardCardFieldsScopedState(scopeId));
const fieldKeys = fields.map(({ fieldMetadataId }) => fieldMetadataId); const fieldKeys = fields.map(({ fieldMetadataId }) => fieldMetadataId);
const otherAvailableKeys = get( const otherAvailableKeys = get(
availableBoardCardFieldsScopedState(scopeId), availableBoardCardFieldsScopedState({ scopeId }),
).filter(({ fieldMetadataId }) => !fieldKeys.includes(fieldMetadataId)); ).filter(({ fieldMetadataId }) => !fieldKeys.includes(fieldMetadataId));
return [ return [

View File

@ -5,19 +5,20 @@ import { useRelationField } from '../../hooks/useRelationField';
export const RelationFieldDisplay = () => { export const RelationFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useRelationField(); const { fieldValue, fieldDefinition } = useRelationField();
const { mapToObjectIdentifiers } = useRelationField();
if (!fieldValue || !fieldDefinition) { if (!fieldValue || !fieldDefinition) {
return <></>; return <></>;
} }
const mainIdentifierMapped = const objectIdentifiers = mapToObjectIdentifiers(fieldValue);
fieldDefinition.metadata.mainIdentifierMapper(fieldValue);
return ( return (
<EntityChip <EntityChip
entityId={fieldValue.id} entityId={fieldValue.id}
name={mainIdentifierMapped.name} name={objectIdentifiers.name}
avatarUrl={mainIdentifierMapped.avatarUrl} avatarUrl={objectIdentifiers.avatarUrl}
avatarType={mainIdentifierMapped.avatarType} avatarType={objectIdentifiers.avatarType}
/> />
); );
}; };

View File

@ -30,11 +30,40 @@ export const useRelationField = () => {
const initialValue = fieldInitialValue?.isEmpty ? null : fieldValue; const initialValue = fieldInitialValue?.isEmpty ? null : fieldValue;
const mapToObjectIdentifiers = (record: any) => {
let name = '';
for (const fieldPath of fieldDefinition.metadata
.labelIdentifierFieldPaths) {
const fieldPathParts = fieldPath.split('.');
if (fieldPathParts.length === 1) {
name += record[fieldPathParts[0]];
} else if (fieldPathParts.length === 2) {
name += record[fieldPathParts[0]][fieldPathParts[1]];
} else {
throw new Error(
`Invalid field path ${fieldPath}. Relation picker only supports field paths with 1 or 2 parts.`,
);
}
}
return {
id: record.id,
name: record[name],
avatarUrl:
fieldDefinition.metadata.imageIdentifierUrlPrefix +
record[fieldDefinition.metadata.imageIdentifierUrlField],
avatarType: fieldDefinition.metadata.imageIdentifierFormat,
record: record,
};
};
return { return {
fieldDefinition, fieldDefinition,
fieldValue, fieldValue,
initialValue, initialValue,
initialSearchValue, initialSearchValue,
setFieldValue, setFieldValue,
mapToObjectIdentifiers,
}; };
}; };

View File

@ -7,6 +7,7 @@ import { IconUserCircle } from '@/ui/display/icon';
import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { useRelationField } from '@/ui/object/field/meta-types/hooks/useRelationField';
import { FieldDefinition } from '@/ui/object/field/types/FieldDefinition'; import { FieldDefinition } from '@/ui/object/field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/ui/object/field/types/FieldMetadata'; import { FieldRelationMetadata } from '@/ui/object/field/types/FieldMetadata';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
@ -41,6 +42,8 @@ export const RelationPicker = ({
const useFindManyQuery = (options: any) => useQuery(findManyQuery, options); const useFindManyQuery = (options: any) => useQuery(findManyQuery, options);
const { mapToObjectIdentifiers } = useRelationField();
const workspaceMembers = useFilteredSearchEntityQuery({ const workspaceMembers = useFilteredSearchEntityQuery({
queryHook: useFindManyQuery, queryHook: useFindManyQuery,
filters: [ filters: [
@ -50,7 +53,7 @@ export const RelationPicker = ({
}, },
], ],
orderByField: 'createdAt', orderByField: 'createdAt',
mappingFunction: fieldDefinition.metadata.mainIdentifierMapper, mappingFunction: mapToObjectIdentifiers,
selectedIds: recordId ? [recordId] : [], selectedIds: recordId ? [recordId] : [],
objectNamePlural: fieldDefinition.metadata.objectMetadataNamePlural, objectNamePlural: fieldDefinition.metadata.objectMetadataNamePlural,
}); });

View File

@ -1,5 +1,4 @@
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { MainIdentifierMapper } from '@/ui/object/field/types/MainIdentifierMapper';
export type FieldUuidMetadata = { export type FieldUuidMetadata = {
fieldName: string; fieldName: string;
@ -65,7 +64,10 @@ export type FieldRelationMetadata = {
fieldName: string; fieldName: string;
useEditButton?: boolean; useEditButton?: boolean;
relationType?: FieldDefinitionRelationType; relationType?: FieldDefinitionRelationType;
mainIdentifierMapper: MainIdentifierMapper; labelIdentifierFieldPaths: string[];
imageIdentifierUrlField: string;
imageIdentifierUrlPrefix: string;
imageIdentifierFormat: 'squared' | 'rounded';
searchFields: string[]; searchFields: string[];
objectMetadataNameSingular: string; objectMetadataNameSingular: string;
objectMetadataNamePlural: string; objectMetadataNamePlural: string;

View File

@ -1,9 +0,0 @@
import { AvatarType } from '@/users/components/Avatar';
export type MainIdentifierMapper = (record: any) => {
id: string;
name: string;
avatarUrl?: string;
avatarType: AvatarType;
record: any;
};

View File

@ -1,14 +0,0 @@
import { MainIdentifierMapper } from '@/ui/object/field/types/MainIdentifierMapper';
export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT'
| 'TO_MANY_OBJECTS'
| 'TO_ONE_OBJECT';
export type RelationFieldConfig = {
relationType?: FieldDefinitionRelationType;
mainIdentifierMapper: MainIdentifierMapper;
searchFields: string[];
objectMetadataNameSingular: string;
};

View File

@ -63,7 +63,6 @@ export const RecordTableCell = ({ cellIndex }: { cellIndex: number }) => {
isMainIdentifier: isMainIdentifier:
columnDefinition.fieldMetadataId === columnDefinition.fieldMetadataId ===
objectMetadataConfig?.mainIdentifierFieldMetadataId, objectMetadataConfig?.mainIdentifierFieldMetadataId,
mainIdentifierMapper: objectMetadataConfig?.mainIdentifierMapper,
}} }}
> >
<TableCell customHotkeyScope={{ scope: customHotkeyScope }} /> <TableCell customHotkeyScope={{ scope: customHotkeyScope }} />

View File

@ -2,10 +2,9 @@ import { AvatarType } from '@/users/components/Avatar';
export type ObjectMetadataConfig = { export type ObjectMetadataConfig = {
mainIdentifierFieldMetadataId: string; mainIdentifierFieldMetadataId: string;
mainIdentifierMapper: (record: any) => { labelIdentifierFieldPaths: string[];
name: string; imageIdentifierUrlField: string;
avatarUrl?: string; imageIdentifierUrlPrefix: string;
avatarType: AvatarType; imageIdentifierFormat: AvatarType;
};
basePathToShowPage: string; basePathToShowPage: string;
}; };

View File

@ -6,7 +6,7 @@ import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord'; import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord';
import { PipelineAddButton } from '@/pipeline/components/PipelineAddButton'; import { PipelineAddButton } from '@/pipeline/components/PipelineAddButton';
import { usePipelineStages } from '@/pipeline/hooks/usePipelineStages'; import { usePipelineSteps } from '@/pipeline/hooks/usePipelineSteps';
import { PipelineStep } from '@/pipeline/types/PipelineStep'; import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { IconTargetArrow } from '@/ui/display/icon'; import { IconTargetArrow } from '@/ui/display/icon';
import { BoardOptionsContext } from '@/ui/layout/board/contexts/BoardOptionsContext'; import { BoardOptionsContext } from '@/ui/layout/board/contexts/BoardOptionsContext';
@ -23,8 +23,8 @@ const StyledBoardContainer = styled.div`
`; `;
export const Opportunities = () => { export const Opportunities = () => {
const { handlePipelineStageAdd, handlePipelineStageDelete } = const { handlePipelineStepAdd, handlePipelineStepDelete } =
usePipelineStages(); usePipelineSteps();
const { updateOneObject: updateOnePipelineStep } = const { updateOneObject: updateOnePipelineStep } =
useUpdateOneObjectRecord<PipelineStep>({ useUpdateOneObjectRecord<PipelineStep>({
@ -68,8 +68,8 @@ export const Opportunities = () => {
<CompanyBoardRecoilScopeContext.Provider value="opportunities"> <CompanyBoardRecoilScopeContext.Provider value="opportunities">
<StyledBoardContainer> <StyledBoardContainer>
<CompanyBoard <CompanyBoard
onColumnAdd={handlePipelineStageAdd} onColumnAdd={handlePipelineStepAdd}
onColumnDelete={handlePipelineStageDelete} onColumnDelete={handlePipelineStepDelete}
onEditColumnTitle={handleEditColumnTitle} onEditColumnTitle={handleEditColumnTitle}
/> />
</StyledBoardContainer> </StyledBoardContainer>

View File

@ -43,17 +43,17 @@ export const AddCompanyFromHeader: Story = {
}); });
await step('Change pipeline stage', async () => { await step('Change pipeline stage', async () => {
const pipelineStageDropdownHeader = await canvas.findByRole( const pipelineStepDropdownHeader = await canvas.findByRole(
'listitem', 'listitem',
{ name: (_, element) => !!element?.textContent?.includes('New') }, { name: (_, element) => !!element?.textContent?.includes('New') },
{ timeout: 1000 }, { timeout: 1000 },
); );
const pipelineStageDropdownUnfoldButton = within( const pipelineStepDropdownUnfoldButton = within(
pipelineStageDropdownHeader, pipelineStepDropdownHeader,
).getByRole('button'); ).getByRole('button');
await userEvent.click(pipelineStageDropdownUnfoldButton); await userEvent.click(pipelineStepDropdownUnfoldButton);
const menuItem1 = await canvas.findByRole( const menuItem1 = await canvas.findByRole(
'listitem', 'listitem',