From ad8331aa8928bc467c2c0efecd7865f32ed0c8b9 Mon Sep 17 00:00:00 2001 From: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 21 Nov 2023 18:01:30 +0100 Subject: [PATCH] Board V2 - Part 1 (#2619) * improve useComputeDefinitionsFromFieldMetadata to prevent infinit loops * fix viewFields * improve initial seeding * fix height 100% * fix filters and sorts * allow filter on currency * remove probability from filter * fix opportunities count * fix persist filters and sorts --- .../board/components/CompanyBoard.tsx | 28 ++++- .../components/HooksCompanyBoardEffect.tsx | 101 +++++++----------- .../useComputeDefinitionsFromFieldMetadata.ts | 35 +++--- ...atFieldMetadataItemsAsFilterDefinitions.ts | 6 +- .../data-model/types/ObjectFieldDataType.ts | 9 -- .../ui/object/field/types/FieldType.ts | 3 - .../MultipleFiltersDropdownContent.tsx | 6 +- .../types/FilterType.ts | 7 +- .../utils/getOperandsForFilterType.ts | 1 + .../utils/turnFiltersIntoWhereClauseV2.ts | 17 +++ .../src/pages/opportunities/Opportunities.tsx | 1 + .../typeorm-seeds/workspace/opportunity.ts | 33 ++++++ 12 files changed, 152 insertions(+), 95 deletions(-) delete mode 100644 front/src/modules/settings/data-model/types/ObjectFieldDataType.ts diff --git a/front/src/modules/companies/board/components/CompanyBoard.tsx b/front/src/modules/companies/board/components/CompanyBoard.tsx index 69d36cb8f..98faf5f51 100644 --- a/front/src/modules/companies/board/components/CompanyBoard.tsx +++ b/front/src/modules/companies/board/components/CompanyBoard.tsx @@ -1,4 +1,5 @@ import styled from '@emotion/styled'; +import { useSetRecoilState } from 'recoil'; import { BoardContext } from '@/companies/states/contexts/BoardContext'; import { BoardOptionsDropdown } from '@/ui/layout/board/components/BoardOptionsDropdown'; @@ -10,6 +11,7 @@ import { import { EntityBoardActionBar } from '@/ui/layout/board/components/EntityBoardActionBar'; import { EntityBoardContextMenu } from '@/ui/layout/board/components/EntityBoardContextMenu'; import { ViewBar } from '@/views/components/ViewBar'; +import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates'; import { ViewScope } from '@/views/scopes/ViewScope'; import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions'; @@ -35,8 +37,32 @@ export const CompanyBoard = ({ onEditColumnTitle, }: CompanyBoardProps) => { const viewScopeId = 'company-board-view'; + + const { + currentViewFieldsState, + currentViewFiltersState, + currentViewSortsState, + } = useViewScopedStates({ + customViewScopeId: viewScopeId, + }); + + const setCurrentViewFields = useSetRecoilState(currentViewFieldsState); + const setCurrentViewFilters = useSetRecoilState(currentViewFiltersState); + const setCurrentViewSorts = useSetRecoilState(currentViewSortsState); + return ( - + { + setCurrentViewFields(viewFields); + }} + onViewFiltersChange={(viewFilters) => { + setCurrentViewFilters(viewFilters); + }} + onViewSortsChange={(viewSorts) => { + setCurrentViewSorts(viewSorts); + }} + > { setViewType, } = useView(); - const { currentViewFieldsState } = useViewScopedStates(); + const { + currentViewFieldsState, + currentViewFiltersState, + currentViewSortsState, + } = useViewScopedStates(); const [pipelineSteps, setPipelineSteps] = useState([]); const [opportunities, setOpportunities] = useState([]); const [companies, setCompanies] = useState([]); const currentViewFields = useRecoilValue(currentViewFieldsState); + const currentViewFilters = useRecoilValue(currentViewFiltersState); + const currentViewSorts = useRecoilValue(currentViewSortsState); const { objectMetadataItem } = useObjectMetadataItem({ objectNamePlural: 'opportunities', @@ -64,6 +72,11 @@ export const HooksCompanyBoardEffect = () => { const updateCompanyBoardCardIds = useUpdateCompanyBoardCardIds(); const updateCompanyBoard = useUpdateCompanyBoard(); + const setAvailableBoardCardFields = useSetRecoilScopedStateV2( + availableBoardCardFieldsScopedState, + 'company-board-view', + ); + useFindManyObjectRecords({ objectNamePlural: 'pipelineSteps', filter: {}, @@ -75,23 +88,21 @@ export const HooksCompanyBoardEffect = () => { ), }); - const whereFilters = useMemo(() => { - return { - and: [ - { - pipelineStepId: { - in: pipelineSteps.map((pipelineStep) => pipelineStep.id), - }, - }, - ...[], - ], - }; - }, [pipelineSteps]) as any; + const filter = turnFiltersIntoWhereClauseV2( + mapViewFiltersToFilters(currentViewFilters), + objectMetadataItem?.fields ?? [], + ); + + const orderBy = turnSortsIntoOrderByV2( + mapViewSortsToSorts(currentViewSorts), + objectMetadataItem?.fields ?? [], + ); useFindManyObjectRecords({ skip: !pipelineSteps.length, objectNamePlural: 'opportunities', - filter: whereFilters, + filter: filter, + orderBy: orderBy, onCompleted: useCallback( (data: PaginatedObjectTypeResults) => { const pipelineProgresses: Array = data.edges.map( @@ -137,39 +148,6 @@ export const HooksCompanyBoardEffect = () => { 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, @@ -179,11 +157,12 @@ export const HooksCompanyBoardEffect = () => { }, [columnDefinitions, setAvailableBoardCardFields]); useEffect(() => { - setViewObjectMetadataId?.('company'); + if (!objectMetadataItem) { + return; + } + setViewObjectMetadataId?.(objectMetadataItem.id); setViewType?.(ViewType.Kanban); - }, [setViewObjectMetadataId, setViewType]); - - const [searchParams] = useSearchParams(); + }, [objectMetadataItem, setViewObjectMetadataId, setViewType]); const loading = !companies; @@ -194,9 +173,8 @@ export const HooksCompanyBoardEffect = () => { if (!loading && opportunities && companies) { setActionBarEntries(); setContextMenuEntries(); - updateCompanyBoard(pipelineSteps, opportunities, companies); - setEntityCountInCurrentView(companies.length); + setEntityCountInCurrentView(opportunities.length); } }, [ companies, @@ -212,10 +190,13 @@ export const HooksCompanyBoardEffect = () => { useEffect(() => { if (currentViewFields) { setBoardCardFields( - mapViewFieldsToBoardFieldDefinitions(currentViewFields, []), + mapViewFieldsToBoardFieldDefinitions( + currentViewFields, + columnDefinitions, + ), ); } - }, [currentViewFields, setBoardCardFields]); + }, [columnDefinitions, currentViewFields, setBoardCardFields]); return <>; }; diff --git a/front/src/modules/object-metadata/hooks/useComputeDefinitionsFromFieldMetadata.ts b/front/src/modules/object-metadata/hooks/useComputeDefinitionsFromFieldMetadata.ts index f719b9c7d..432a75386 100644 --- a/front/src/modules/object-metadata/hooks/useComputeDefinitionsFromFieldMetadata.ts +++ b/front/src/modules/object-metadata/hooks/useComputeDefinitionsFromFieldMetadata.ts @@ -1,3 +1,5 @@ +import { useMemo } from 'react'; + import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata'; import { ColumnDefinition } from '@/ui/object/record-table/types/ColumnDefinition'; @@ -10,25 +12,24 @@ import { formatFieldMetadataItemsAsSortDefinitions } from '../utils/formatFieldM export const useComputeDefinitionsFromFieldMetadata = ( objectMetadataItem?: Nullable, ) => { - if (!objectMetadataItem) { - return { - columnDefinitions: [], - filterDefinitions: [], - sortDefinitions: [], - }; - } - - const activeFieldMetadataItems = objectMetadataItem.fields.filter( - ({ isActive }) => isActive, + const activeFieldMetadataItems = useMemo( + () => + objectMetadataItem + ? objectMetadataItem.fields.filter(({ isActive }) => isActive) + : [], + [objectMetadataItem], ); - const columnDefinitions: ColumnDefinition[] = - activeFieldMetadataItems.map((field, index) => - formatFieldMetadataItemAsColumnDefinition({ - position: index, - field, - }), - ); + const columnDefinitions: ColumnDefinition[] = useMemo( + () => + activeFieldMetadataItems.map((field, index) => + formatFieldMetadataItemAsColumnDefinition({ + position: index, + field, + }), + ), + [activeFieldMetadataItems], + ); const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({ fields: activeFieldMetadataItems, diff --git a/front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts b/front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts index cff9e7a8c..bdd1f4f08 100644 --- a/front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts +++ b/front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts @@ -13,8 +13,10 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({ ![ FieldMetadataType.DateTime, FieldMetadataType.Number, + FieldMetadataType.Currency, FieldMetadataType.Text, - ].includes(field.type) + ].includes(field.type) || + field.name === 'probability' ) { return acc; } @@ -34,5 +36,7 @@ const formatFieldMetadataItemAsFilterDefinition = ({ ? 'DATE_TIME' : field.type === FieldMetadataType.Number ? 'NUMBER' + : field.type === FieldMetadataType.Currency + ? 'CURRENCY' : 'TEXT', }); diff --git a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts b/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts deleted file mode 100644 index ba9b57e2e..000000000 --- a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type MetadataFieldDataType = - | 'BOOLEAN' - | 'DATE_TIME' - | 'ENUM' - | 'MONEY' - | 'NUMBER' - | 'RELATION' - | 'TEXT' - | 'URL'; diff --git a/front/src/modules/ui/object/field/types/FieldType.ts b/front/src/modules/ui/object/field/types/FieldType.ts index 7d1f08974..9cd6f00cd 100644 --- a/front/src/modules/ui/object/field/types/FieldType.ts +++ b/front/src/modules/ui/object/field/types/FieldType.ts @@ -9,9 +9,6 @@ export type FieldType = | 'DOUBLE_TEXT' | 'EMAIL' | 'ENUM' - | 'MONEY_AMOUNT_' - | 'MONEY_AMOUNT' - | 'MONEY' | 'NUMBER' | 'PHONE' | 'PROBABILITY' diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index 6a28785dc..46b4006ff 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -33,9 +33,9 @@ export const MultipleFiltersDropdownContent = () => { {filterDefinitionUsedInDropdown.type === 'TEXT' && ( )} - {filterDefinitionUsedInDropdown.type === 'NUMBER' && ( - - )} + {['NUMBER', 'CURRENCY'].includes( + filterDefinitionUsedInDropdown.type, + ) && } {filterDefinitionUsedInDropdown.type === 'DATE_TIME' && ( )} diff --git a/front/src/modules/ui/object/object-filter-dropdown/types/FilterType.ts b/front/src/modules/ui/object/object-filter-dropdown/types/FilterType.ts index bf9d3d58c..8d3c64cf0 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/types/FilterType.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/types/FilterType.ts @@ -1 +1,6 @@ -export type FilterType = 'TEXT' | 'DATE_TIME' | 'ENTITY' | 'NUMBER'; +export type FilterType = + | 'TEXT' + | 'DATE_TIME' + | 'ENTITY' + | 'NUMBER' + | 'CURRENCY'; diff --git a/front/src/modules/ui/object/object-filter-dropdown/utils/getOperandsForFilterType.ts b/front/src/modules/ui/object/object-filter-dropdown/utils/getOperandsForFilterType.ts index d3529c9f1..752249c20 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/utils/getOperandsForFilterType.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/utils/getOperandsForFilterType.ts @@ -8,6 +8,7 @@ export const getOperandsForFilterType = ( switch (filterType) { case 'TEXT': return [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]; + case 'CURRENCY': case 'NUMBER': case 'DATE_TIME': return [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan]; diff --git a/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2.ts b/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2.ts index 01f84c1f7..f2b5c02b2 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2.ts @@ -62,6 +62,23 @@ export const turnFiltersIntoWhereClauseV2 = ( `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, ); } + case 'CURRENCY': + switch (filter.operand) { + case ViewFilterOperand.GreaterThan: + whereClause[correspondingField.name] = { + amountMicros: { gte: parseFloat(filter.value) * 1000000 }, + }; + return; + case ViewFilterOperand.LessThan: + whereClause[correspondingField.name] = { + amountMicros: { lte: parseFloat(filter.value) * 1000000 }, + }; + return; + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } case 'DATE_TIME': switch (filter.operand) { case ViewFilterOperand.GreaterThan: diff --git a/front/src/pages/opportunities/Opportunities.tsx b/front/src/pages/opportunities/Opportunities.tsx index 54e77c9c1..9b34d0ef6 100644 --- a/front/src/pages/opportunities/Opportunities.tsx +++ b/front/src/pages/opportunities/Opportunities.tsx @@ -19,6 +19,7 @@ import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBo const StyledBoardContainer = styled.div` display: flex; + height: 100%; width: 100%; `; diff --git a/server/src/database/typeorm-seeds/workspace/opportunity.ts b/server/src/database/typeorm-seeds/workspace/opportunity.ts index fc1afaf55..3b476985e 100644 --- a/server/src/database/typeorm-seeds/workspace/opportunity.ts +++ b/server/src/database/typeorm-seeds/workspace/opportunity.ts @@ -33,6 +33,39 @@ export const seedOpportunity = async ( personId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5', companyId: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408', }, + { + id: '53f66647-0543-4cc2-9f96-95cc699960f2', + amountAmountMicros: 2000000, + amountCurrencyCode: 'USD', + closeDate: new Date(), + probability: 0.5, + pipelineStepId: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a', + pointOfContactId: '93c72d2e-f517-42fd-80ae-14173b3b70ae', + personId: '93c72d2e-f517-42fd-80ae-14173b3b70ae', + companyId: '118995f3-5d81-46d6-bf83-f7fd33ea6102', + }, + { + id: '81ab695d-2f89-406f-90ea-180f433b2445', + amountAmountMicros: 300000, + amountCurrencyCode: 'USD', + closeDate: new Date(), + probability: 0.5, + pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02', + pointOfContactId: '9b324a88-6784-4449-afdf-dc62cb8702f2', + personId: '9b324a88-6784-4449-afdf-dc62cb8702f2', + companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4', + }, + { + id: '9b059852-35b1-4045-9cde-42f715148954', + amountAmountMicros: 4000000, + amountCurrencyCode: 'USD', + closeDate: new Date(), + probability: 0.5, + pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02', + pointOfContactId: '98406e26-80f1-4dff-b570-a74942528de3', + personId: '98406e26-80f1-4dff-b570-a74942528de3', + companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4', + }, ]) .execute(); };