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:
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -1,47 +1,34 @@
|
||||
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';
|
||||
|
||||
export const useObjectMainIdentifier = (
|
||||
objectMetadataItem?: Nullable<ObjectMetadataItem>,
|
||||
) => {
|
||||
if (!objectMetadataItem) {
|
||||
return {
|
||||
mainIdentifierMapper: undefined,
|
||||
mainIdentifierFieldMetadataId: undefined,
|
||||
basePathToShowPage: undefined,
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
const mainIdentifierMapper: MainIdentifierMapper = (record: any) => {
|
||||
if (objectMetadataItem.nameSingular === 'company') {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
avatarUrl: record.avatarUrl,
|
||||
avatarType: 'squared',
|
||||
record: record,
|
||||
};
|
||||
}
|
||||
|
||||
if (objectMetadataItem.nameSingular === 'workspaceMember') {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name.firstName + ' ' + record.name.lastName,
|
||||
avatarUrl: record.avatarUrl,
|
||||
avatarType: 'rounded',
|
||||
record: record,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
avatarUrl: record.avatarUrl,
|
||||
avatarType: 'rounded',
|
||||
record: record,
|
||||
};
|
||||
};
|
||||
const labelIdentifierFieldPaths = ['person', 'workspaceMember'].includes(
|
||||
objectMetadataItem.nameSingular,
|
||||
)
|
||||
? ['name.firstName', 'name.lastName']
|
||||
: ['name'];
|
||||
const imageIdentifierFormat: AvatarType = ['company'].includes(
|
||||
objectMetadataItem.nameSingular,
|
||||
)
|
||||
? 'squared'
|
||||
: 'rounded';
|
||||
const imageIdentifierUrlPrefix = ['company'].includes(
|
||||
objectMetadataItem.nameSingular,
|
||||
)
|
||||
? 'https://favicon.twenty.com/'
|
||||
: '';
|
||||
const imageIdentifierUrlField = ['company'].includes(
|
||||
objectMetadataItem.nameSingular,
|
||||
)
|
||||
? 'domainName'
|
||||
: 'avatarUrl';
|
||||
|
||||
const mainIdentifierFieldMetadataId = objectMetadataItem.fields.find(
|
||||
({ name }) => name === 'name',
|
||||
@ -50,7 +37,10 @@ export const useObjectMainIdentifier = (
|
||||
const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`;
|
||||
|
||||
return {
|
||||
mainIdentifierMapper,
|
||||
labelIdentifierFieldPaths,
|
||||
imageIdentifierUrlField,
|
||||
imageIdentifierUrlPrefix,
|
||||
imageIdentifierFormat,
|
||||
mainIdentifierFieldMetadataId,
|
||||
basePathToShowPage,
|
||||
};
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
|
||||
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 { getLogoUrlFromDomainName } from '~/utils';
|
||||
import { AvatarType } from '@/users/components/Avatar';
|
||||
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
@ -18,34 +17,26 @@ export const formatFieldMetadataItemAsColumnDefinition = ({
|
||||
const relationObjectMetadataItem =
|
||||
field.toRelationMetadata?.fromObjectMetadata;
|
||||
|
||||
const mainIdentifierMapper: MainIdentifierMapper = (record: any) => {
|
||||
if (relationObjectMetadataItem?.nameSingular === 'company') {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
avatarUrl: getLogoUrlFromDomainName(record.domainName),
|
||||
avatarType: 'squared',
|
||||
record: record,
|
||||
};
|
||||
}
|
||||
if (relationObjectMetadataItem?.nameSingular === 'workspaceMember') {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name.firstName + ' ' + record.name.lastName,
|
||||
avatarUrl: record.avatarUrl,
|
||||
avatarType: 'rounded',
|
||||
record: record,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
avatarUrl: record.avatarUrl,
|
||||
avatarType: 'rounded',
|
||||
record: record,
|
||||
};
|
||||
};
|
||||
const labelIdentifierFieldPaths = ['person', 'workspaceMember'].includes(
|
||||
relationObjectMetadataItem?.nameSingular ?? '',
|
||||
)
|
||||
? ['name.firstName', 'name.lastName']
|
||||
: ['name'];
|
||||
const imageIdentifierFormat: AvatarType = ['company'].includes(
|
||||
relationObjectMetadataItem?.nameSingular ?? '',
|
||||
)
|
||||
? 'squared'
|
||||
: 'rounded';
|
||||
const imageIdentifierUrlPrefix = ['company'].includes(
|
||||
relationObjectMetadataItem?.nameSingular ?? '',
|
||||
)
|
||||
? 'https://favicon.twenty.com/'
|
||||
: '';
|
||||
const imageIdentifierUrlField = ['company'].includes(
|
||||
relationObjectMetadataItem?.nameSingular ?? '',
|
||||
)
|
||||
? 'domainName'
|
||||
: 'avatarUrl';
|
||||
|
||||
return {
|
||||
position,
|
||||
@ -56,11 +47,15 @@ export const formatFieldMetadataItemAsColumnDefinition = ({
|
||||
metadata: {
|
||||
fieldName: field.name,
|
||||
placeHolder: field.label,
|
||||
mainIdentifierMapper: mainIdentifierMapper,
|
||||
labelIdentifierFieldPaths,
|
||||
imageIdentifierUrlField,
|
||||
imageIdentifierUrlPrefix,
|
||||
imageIdentifierFormat,
|
||||
relationType: parseFieldRelationType(field),
|
||||
searchFields: ['name'],
|
||||
objectMetadataNamePlural: relationObjectMetadataItem?.namePlural,
|
||||
objectMetadataNameSingular: relationObjectMetadataItem?.nameSingular,
|
||||
objectMetadataNamePlural: relationObjectMetadataItem?.namePlural ?? '',
|
||||
objectMetadataNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular ?? '',
|
||||
},
|
||||
iconName: field.icon ?? 'Icon123',
|
||||
isVisible: true,
|
||||
|
||||
@ -22,9 +22,12 @@ export const RecordTableEffect = () => {
|
||||
});
|
||||
|
||||
const {
|
||||
mainIdentifierMapper,
|
||||
basePathToShowPage,
|
||||
mainIdentifierFieldMetadataId,
|
||||
labelIdentifierFieldPaths,
|
||||
imageIdentifierUrlField,
|
||||
imageIdentifierUrlPrefix,
|
||||
imageIdentifierFormat,
|
||||
} = useObjectMainIdentifier(objectMetadataItem);
|
||||
|
||||
const { columnDefinitions, filterDefinitions, sortDefinitions } =
|
||||
@ -40,13 +43,12 @@ export const RecordTableEffect = () => {
|
||||
} = useView();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
mainIdentifierMapper &&
|
||||
basePathToShowPage &&
|
||||
mainIdentifierFieldMetadataId
|
||||
) {
|
||||
if (basePathToShowPage && mainIdentifierFieldMetadataId) {
|
||||
setObjectMetadataConfig?.({
|
||||
mainIdentifierMapper,
|
||||
labelIdentifierFieldPaths,
|
||||
imageIdentifierUrlField,
|
||||
imageIdentifierUrlPrefix,
|
||||
imageIdentifierFormat,
|
||||
basePathToShowPage,
|
||||
mainIdentifierFieldMetadataId,
|
||||
});
|
||||
@ -55,8 +57,11 @@ export const RecordTableEffect = () => {
|
||||
basePathToShowPage,
|
||||
objectMetadataItem,
|
||||
mainIdentifierFieldMetadataId,
|
||||
mainIdentifierMapper,
|
||||
setObjectMetadataConfig,
|
||||
labelIdentifierFieldPaths,
|
||||
imageIdentifierUrlField,
|
||||
imageIdentifierUrlPrefix,
|
||||
imageIdentifierFormat,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
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>
|
||||
const [mutate] = useMutation(deleteOneMutation);
|
||||
|
||||
const deleteOneObject =
|
||||
objectNameSingular && foundObjectMetadataItem
|
||||
? async (idToDelete: string) => {
|
||||
const deletedObject = await mutate({
|
||||
variables: {
|
||||
idToDelete,
|
||||
},
|
||||
refetchQueries: [getOperationName(findManyQuery) ?? ''],
|
||||
});
|
||||
return deletedObject.data[
|
||||
`create${capitalize(objectNameSingular)}`
|
||||
] as T;
|
||||
}
|
||||
: undefined;
|
||||
const deleteOneObject = useCallback(
|
||||
async (idToDelete: string) => {
|
||||
if (objectNameSingular && foundObjectMetadataItem) {
|
||||
const deletedObject = await mutate({
|
||||
variables: {
|
||||
idToDelete,
|
||||
},
|
||||
refetchQueries: [getOperationName(findManyQuery) ?? ''],
|
||||
});
|
||||
|
||||
return deletedObject.data[
|
||||
`create${capitalize(objectNameSingular)}`
|
||||
] as T;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[foundObjectMetadataItem, mutate, objectNameSingular, findManyQuery],
|
||||
);
|
||||
|
||||
return {
|
||||
deleteOneObject,
|
||||
|
||||
@ -16,5 +16,12 @@ export const filterAvailableTableColumns = (
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
isFieldRelation(columnDefinition) &&
|
||||
columnDefinition.metadata?.fieldName === 'pipelineStep'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -19,7 +19,7 @@ export const PipelineAddButton = () => {
|
||||
|
||||
const handleCompanySelected = (
|
||||
selectedCompany: EntityForSelect | null,
|
||||
selectedPipelineStageId: string | null,
|
||||
selectedPipelineStepId: string | null,
|
||||
) => {
|
||||
if (!selectedCompany?.id) {
|
||||
enqueueSnackBar(
|
||||
@ -33,7 +33,7 @@ export const PipelineAddButton = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPipelineStageId) {
|
||||
if (!selectedPipelineStepId) {
|
||||
enqueueSnackBar(
|
||||
'There was a problem with the pipeline stage selection, please retry.',
|
||||
{
|
||||
@ -45,7 +45,7 @@ export const PipelineAddButton = () => {
|
||||
return;
|
||||
}
|
||||
closeDropdown();
|
||||
//createCompanyProgress(selectedCompany.id, selectedPipelineStageId);
|
||||
//createCompanyProgress(selectedCompany.id, selectedPipelineStepId);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -7,7 +7,7 @@ import { BoardColumnDefinition } from '@/ui/layout/board/types/BoardColumnDefini
|
||||
|
||||
import { currentPipelineState } from '../states/currentPipelineState';
|
||||
|
||||
export const usePipelineStages = () => {
|
||||
export const usePipelineSteps = () => {
|
||||
const currentPipeline = useRecoilValue(currentPipelineState);
|
||||
|
||||
const { createOneObject: createOnePipelineStep } =
|
||||
@ -20,7 +20,7 @@ export const usePipelineStages = () => {
|
||||
objectNameSingular: 'pipelineStep',
|
||||
});
|
||||
|
||||
const handlePipelineStageAdd = async (boardColumn: BoardColumnDefinition) => {
|
||||
const handlePipelineStepAdd = async (boardColumn: BoardColumnDefinition) => {
|
||||
if (!currentPipeline?.id) return;
|
||||
|
||||
return createOnePipelineStep?.({
|
||||
@ -37,11 +37,11 @@ export const usePipelineStages = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handlePipelineStageDelete = async (boardColumnId: string) => {
|
||||
const handlePipelineStepDelete = async (boardColumnId: string) => {
|
||||
if (!currentPipeline?.id) return;
|
||||
|
||||
return deleteOnePipelineStep?.(boardColumnId);
|
||||
};
|
||||
|
||||
return { handlePipelineStageAdd, handlePipelineStageDelete };
|
||||
return { handlePipelineStepAdd, handlePipelineStepDelete };
|
||||
};
|
||||
@ -2,6 +2,7 @@ import { Person } from '@/people/types/Person';
|
||||
import { PipelineStep } from '@/pipeline/types/PipelineStep';
|
||||
|
||||
export type Opportunity = {
|
||||
[key: string]: any;
|
||||
id: string;
|
||||
amount: {
|
||||
amountMicros: number;
|
||||
@ -13,5 +14,4 @@ export type Opportunity = {
|
||||
pipelineStep: PipelineStep;
|
||||
pointOfContactId: string;
|
||||
pointOfContact: Pick<Person, 'id' | 'name' | 'avatarUrl'>;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
@ -92,7 +92,10 @@ export const SettingsObjectFieldPreview = ({
|
||||
|
||||
const {
|
||||
defaultValue: relationDefaultValue,
|
||||
recordMapper: mainIdentifierMapper,
|
||||
labelIdentifierFieldPaths,
|
||||
imageIdentifierUrlField,
|
||||
imageIdentifierUrlPrefix,
|
||||
imageIdentifierFormat,
|
||||
} = useRelationFieldPreview({
|
||||
relationObjectMetadataId,
|
||||
skipDefaultValue:
|
||||
@ -104,6 +107,15 @@ export const SettingsObjectFieldPreview = ({
|
||||
? relationDefaultValue
|
||||
: dataTypes[fieldMetadata.type].defaultValue;
|
||||
|
||||
if (
|
||||
!labelIdentifierFieldPaths ||
|
||||
!imageIdentifierUrlField ||
|
||||
!imageIdentifierUrlPrefix ||
|
||||
!imageIdentifierFormat
|
||||
) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer className={className}>
|
||||
<StyledObjectSummary>
|
||||
@ -146,7 +158,13 @@ export const SettingsObjectFieldPreview = ({
|
||||
iconName: 'FieldIcon',
|
||||
fieldMetadataId: fieldMetadata.id || '',
|
||||
label: fieldMetadata.label,
|
||||
metadata: { fieldName, mainIdentifierMapper },
|
||||
metadata: {
|
||||
fieldName,
|
||||
labelIdentifierFieldPaths,
|
||||
imageIdentifierUrlField,
|
||||
imageIdentifierUrlPrefix,
|
||||
imageIdentifierFormat,
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'field-preview',
|
||||
}}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useObjectMainIdentifier } from '@/object-metadata/hooks/useObjectMainIdentifier';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
|
||||
import { MainIdentifierMapper } from '@/ui/object/field/types/MainIdentifierMapper';
|
||||
|
||||
export const useRelationFieldPreview = ({
|
||||
relationObjectMetadataId,
|
||||
@ -21,24 +20,18 @@ export const useRelationFieldPreview = ({
|
||||
skip: skipDefaultValue || !relationObjectMetadataItem,
|
||||
});
|
||||
|
||||
const { mainIdentifierMapper } = useObjectMainIdentifier(
|
||||
relationObjectMetadataItem,
|
||||
);
|
||||
|
||||
const recordMapper: MainIdentifierMapper | undefined =
|
||||
relationObjectMetadataItem && mainIdentifierMapper
|
||||
? (record: { id: string }) => {
|
||||
const mappedRecord = mainIdentifierMapper(record);
|
||||
|
||||
return {
|
||||
...mappedRecord,
|
||||
name: mappedRecord.name || relationObjectMetadataItem.labelSingular,
|
||||
};
|
||||
}
|
||||
: undefined;
|
||||
const {
|
||||
labelIdentifierFieldPaths,
|
||||
imageIdentifierUrlField,
|
||||
imageIdentifierUrlPrefix,
|
||||
imageIdentifierFormat,
|
||||
} = useObjectMainIdentifier(relationObjectMetadataItem);
|
||||
|
||||
return {
|
||||
defaultValue: relationObjects?.[0],
|
||||
recordMapper,
|
||||
labelIdentifierFieldPaths,
|
||||
imageIdentifierUrlField,
|
||||
imageIdentifierUrlPrefix,
|
||||
imageIdentifierFormat,
|
||||
};
|
||||
};
|
||||
|
||||
@ -74,6 +74,7 @@ export const BoardOptionsDropdownContent = ({
|
||||
hiddenBoardCardFieldsScopedSelector,
|
||||
BoardRecoilScopeContext,
|
||||
);
|
||||
|
||||
const hasHiddenFields = hiddenBoardCardFields.length > 0;
|
||||
const visibleBoardCardFields = useRecoilScopedValue(
|
||||
visibleBoardCardFieldsScopedSelector,
|
||||
|
||||
@ -65,11 +65,11 @@ export const EntityBoard = ({
|
||||
const { unselectAllActiveCards } = useCurrentCardSelected();
|
||||
|
||||
const updatePipelineProgressStageInDB = useCallback(
|
||||
async (pipelineProgressId: string, pipelineStageId: string) => {
|
||||
async (pipelineProgressId: string, pipelineStepId: string) => {
|
||||
await updateOneOpportunity?.({
|
||||
idToUpdate: pipelineProgressId,
|
||||
input: {
|
||||
pipelineStepId: pipelineStageId,
|
||||
pipelineStepId: pipelineStepId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -80,7 +80,7 @@ export const EntityBoard = ({
|
||||
__typename: 'PipelineProgress',
|
||||
}),
|
||||
fields: {
|
||||
pipelineStageId: () => pipelineStageId,
|
||||
pipelineStepId: () => pipelineStepId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
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';
|
||||
|
||||
export const useBoardActionBarEntries = () => {
|
||||
const setActionBarEntries = useSetRecoilState(actionBarEntriesState);
|
||||
const setActionBarEntriesRecoil = useSetRecoilState(actionBarEntriesState);
|
||||
|
||||
const deleteSelectedBoardCards = useDeleteSelectedBoardCards();
|
||||
|
||||
const setActionBarEntries = useCallback(() => {
|
||||
setActionBarEntriesRecoil([
|
||||
{
|
||||
label: 'Delete',
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
onClick: deleteSelectedBoardCards,
|
||||
},
|
||||
]);
|
||||
}, [deleteSelectedBoardCards, setActionBarEntriesRecoil]);
|
||||
|
||||
return {
|
||||
setActionBarEntries: () =>
|
||||
setActionBarEntries([
|
||||
{
|
||||
label: 'Delete',
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
onClick: deleteSelectedBoardCards,
|
||||
},
|
||||
]),
|
||||
setActionBarEntries,
|
||||
};
|
||||
};
|
||||
|
||||
@ -17,7 +17,7 @@ export const useBoardColumns = () => {
|
||||
objectNameSingular: 'pipelineStep',
|
||||
});
|
||||
|
||||
const updatedPipelineStages = (stages: BoardColumnDefinition[]) => {
|
||||
const updatedPipelineSteps = (stages: BoardColumnDefinition[]) => {
|
||||
if (!stages.length) return;
|
||||
|
||||
return Promise.all(
|
||||
@ -33,7 +33,7 @@ export const useBoardColumns = () => {
|
||||
};
|
||||
|
||||
const persistBoardColumns = async () => {
|
||||
await updatedPipelineStages(boardColumns);
|
||||
await updatedPipelineSteps(boardColumns);
|
||||
};
|
||||
|
||||
const handleMoveBoardColumn = (
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
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';
|
||||
|
||||
export const useBoardContextMenuEntries = () => {
|
||||
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
|
||||
const setContextMenuEntriesRecoil = useSetRecoilState(
|
||||
contextMenuEntriesState,
|
||||
);
|
||||
|
||||
const deleteSelectedBoardCards = useDeleteSelectedBoardCards();
|
||||
|
||||
const setContextMenuEntries = useCallback(() => {
|
||||
setContextMenuEntriesRecoil([
|
||||
{
|
||||
label: 'Delete',
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
onClick: deleteSelectedBoardCards,
|
||||
},
|
||||
]);
|
||||
}, [deleteSelectedBoardCards, setContextMenuEntriesRecoil]);
|
||||
|
||||
return {
|
||||
setContextMenuEntries: () =>
|
||||
setContextMenuEntries([
|
||||
{
|
||||
label: 'Delete',
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
onClick: () => deleteSelectedBoardCards(),
|
||||
},
|
||||
]),
|
||||
setContextMenuEntries,
|
||||
};
|
||||
};
|
||||
|
||||
@ -34,7 +34,7 @@ export const useDeleteSelectedBoardCards = () => {
|
||||
apolloClient.cache.evict({ id: `Opportunity:${id}` });
|
||||
});
|
||||
},
|
||||
[apolloClient.cache, deleteOneOpportunity, removeCardIds],
|
||||
[apolloClient.cache, removeCardIds, deleteOneOpportunity],
|
||||
);
|
||||
|
||||
return deleteSelectedBoardCards;
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
|
||||
|
||||
export const availableBoardCardFieldsScopedState = atomFamily<
|
||||
BoardFieldDefinition<FieldMetadata>[],
|
||||
string
|
||||
export const availableBoardCardFieldsScopedState = createScopedState<
|
||||
BoardFieldDefinition<FieldMetadata>[]
|
||||
>({
|
||||
key: 'availableBoardCardFieldsScopedState',
|
||||
default: [],
|
||||
defaultValue: [],
|
||||
});
|
||||
|
||||
@ -9,20 +9,21 @@ import { boardCardIdsByColumnIdFamilyState } from '../boardCardIdsByColumnIdFami
|
||||
export const boardColumnTotalsFamilySelector = selectorFamily({
|
||||
key: 'boardColumnTotalsFamilySelector',
|
||||
get:
|
||||
(pipelineStageId: string) =>
|
||||
(pipelineStepId: string) =>
|
||||
({ get }) => {
|
||||
const cardIds = get(boardCardIdsByColumnIdFamilyState(pipelineStageId));
|
||||
const cardIds = get(boardCardIdsByColumnIdFamilyState(pipelineStepId));
|
||||
|
||||
const pipelineProgresses = cardIds.map((pipelineProgressId: string) =>
|
||||
get(companyProgressesFamilyState(pipelineProgressId)),
|
||||
const opportunities = cardIds.map((opportunityId: string) =>
|
||||
get(companyProgressesFamilyState(opportunityId)),
|
||||
);
|
||||
|
||||
const pipelineStageTotal: number =
|
||||
pipelineProgresses?.reduce(
|
||||
(acc: number, curr: any) => acc + curr?.pipelineProgress.amount,
|
||||
const pipelineStepTotal: number =
|
||||
opportunities?.reduce(
|
||||
(acc: number, curr: any) =>
|
||||
acc + curr?.opportunity.amount.amountMicros / 1000000,
|
||||
0,
|
||||
) || 0;
|
||||
|
||||
return pipelineStageTotal;
|
||||
return pipelineStepTotal;
|
||||
},
|
||||
});
|
||||
|
||||
@ -11,7 +11,7 @@ export const hiddenBoardCardFieldsScopedSelector = selectorFamily({
|
||||
const fields = get(boardCardFieldsScopedState(scopeId));
|
||||
const fieldKeys = fields.map(({ fieldMetadataId }) => fieldMetadataId);
|
||||
const otherAvailableKeys = get(
|
||||
availableBoardCardFieldsScopedState(scopeId),
|
||||
availableBoardCardFieldsScopedState({ scopeId }),
|
||||
).filter(({ fieldMetadataId }) => !fieldKeys.includes(fieldMetadataId));
|
||||
|
||||
return [
|
||||
|
||||
@ -5,19 +5,20 @@ import { useRelationField } from '../../hooks/useRelationField';
|
||||
export const RelationFieldDisplay = () => {
|
||||
const { fieldValue, fieldDefinition } = useRelationField();
|
||||
|
||||
const { mapToObjectIdentifiers } = useRelationField();
|
||||
|
||||
if (!fieldValue || !fieldDefinition) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const mainIdentifierMapped =
|
||||
fieldDefinition.metadata.mainIdentifierMapper(fieldValue);
|
||||
const objectIdentifiers = mapToObjectIdentifiers(fieldValue);
|
||||
|
||||
return (
|
||||
<EntityChip
|
||||
entityId={fieldValue.id}
|
||||
name={mainIdentifierMapped.name}
|
||||
avatarUrl={mainIdentifierMapped.avatarUrl}
|
||||
avatarType={mainIdentifierMapped.avatarType}
|
||||
name={objectIdentifiers.name}
|
||||
avatarUrl={objectIdentifiers.avatarUrl}
|
||||
avatarType={objectIdentifiers.avatarType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -30,11 +30,40 @@ export const useRelationField = () => {
|
||||
|
||||
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 {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
initialValue,
|
||||
initialSearchValue,
|
||||
setFieldValue,
|
||||
mapToObjectIdentifiers,
|
||||
};
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ import { IconUserCircle } from '@/ui/display/icon';
|
||||
import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
|
||||
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||
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 { FieldRelationMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
@ -41,6 +42,8 @@ export const RelationPicker = ({
|
||||
|
||||
const useFindManyQuery = (options: any) => useQuery(findManyQuery, options);
|
||||
|
||||
const { mapToObjectIdentifiers } = useRelationField();
|
||||
|
||||
const workspaceMembers = useFilteredSearchEntityQuery({
|
||||
queryHook: useFindManyQuery,
|
||||
filters: [
|
||||
@ -50,7 +53,7 @@ export const RelationPicker = ({
|
||||
},
|
||||
],
|
||||
orderByField: 'createdAt',
|
||||
mappingFunction: fieldDefinition.metadata.mainIdentifierMapper,
|
||||
mappingFunction: mapToObjectIdentifiers,
|
||||
selectedIds: recordId ? [recordId] : [],
|
||||
objectNamePlural: fieldDefinition.metadata.objectMetadataNamePlural,
|
||||
});
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
|
||||
import { MainIdentifierMapper } from '@/ui/object/field/types/MainIdentifierMapper';
|
||||
|
||||
export type FieldUuidMetadata = {
|
||||
fieldName: string;
|
||||
@ -65,7 +64,10 @@ export type FieldRelationMetadata = {
|
||||
fieldName: string;
|
||||
useEditButton?: boolean;
|
||||
relationType?: FieldDefinitionRelationType;
|
||||
mainIdentifierMapper: MainIdentifierMapper;
|
||||
labelIdentifierFieldPaths: string[];
|
||||
imageIdentifierUrlField: string;
|
||||
imageIdentifierUrlPrefix: string;
|
||||
imageIdentifierFormat: 'squared' | 'rounded';
|
||||
searchFields: string[];
|
||||
objectMetadataNameSingular: string;
|
||||
objectMetadataNamePlural: string;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -63,7 +63,6 @@ export const RecordTableCell = ({ cellIndex }: { cellIndex: number }) => {
|
||||
isMainIdentifier:
|
||||
columnDefinition.fieldMetadataId ===
|
||||
objectMetadataConfig?.mainIdentifierFieldMetadataId,
|
||||
mainIdentifierMapper: objectMetadataConfig?.mainIdentifierMapper,
|
||||
}}
|
||||
>
|
||||
<TableCell customHotkeyScope={{ scope: customHotkeyScope }} />
|
||||
|
||||
@ -2,10 +2,9 @@ import { AvatarType } from '@/users/components/Avatar';
|
||||
|
||||
export type ObjectMetadataConfig = {
|
||||
mainIdentifierFieldMetadataId: string;
|
||||
mainIdentifierMapper: (record: any) => {
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
avatarType: AvatarType;
|
||||
};
|
||||
labelIdentifierFieldPaths: string[];
|
||||
imageIdentifierUrlField: string;
|
||||
imageIdentifierUrlPrefix: string;
|
||||
imageIdentifierFormat: AvatarType;
|
||||
basePathToShowPage: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user