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