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

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

View File

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

View File

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

View File

@ -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 = (

View File

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

View File

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

View File

@ -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: [],
});

View File

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

View File

@ -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 [

View File

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

View File

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

View File

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

View File

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

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:
columnDefinition.fieldMetadataId ===
objectMetadataConfig?.mainIdentifierFieldMetadataId,
mainIdentifierMapper: objectMetadataConfig?.mainIdentifierMapper,
}}
>
<TableCell customHotkeyScope={{ scope: customHotkeyScope }} />

View File

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