diff --git a/packages/twenty-front/src/modules/companies/components/HooksCompanyBoardEffect.tsx b/packages/twenty-front/src/modules/companies/components/HooksCompanyBoardEffect.tsx index 5374263c9..d81812d30 100644 --- a/packages/twenty-front/src/modules/companies/components/HooksCompanyBoardEffect.tsx +++ b/packages/twenty-front/src/modules/companies/components/HooksCompanyBoardEffect.tsx @@ -42,7 +42,7 @@ export const HooksCompanyBoardEffect = ({ const setAvailableBoardCardFields = useSetRecoilScopedStateV2( availableRecordBoardCardFieldsScopedState, - 'company-board-view', + 'company-board', ); useEffect(() => { diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx index f8c4bd401..ba6b68dfe 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx @@ -55,7 +55,7 @@ export const ObjectMetadataItemsRelationPickerEffect = () => { if (['opportunity'].includes(objectMetadataItemSingularName)) { return { id: record.id, - name: record?.company?.name, + name: record?.company?.name ?? record.name, avatarUrl: record.avatarUrl, avatarType: 'rounded', record: record, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts index 6cc39a24f..4d2fabc15 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts @@ -18,7 +18,7 @@ export const getObjectRecordIdentifier = ({ case CoreObjectNameSingular.Opportunity: return { id: record.id, - name: record?.company?.name, + name: record?.company?.name ?? record.name, avatarUrl: record.avatarUrl, avatarType: 'rounded', linkToShowPage: `/opportunities/${record.id}`, diff --git a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx b/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx index 79904eaa4..426c2a4bc 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; @@ -70,11 +71,13 @@ export const RecordShowPage = () => { const { record, loading } = useFindOneRecord({ objectRecordId, objectNameSingular, - onCompleted: (data) => { - setEntityFields(data); - }, }); + useEffect(() => { + if (!record) return; + setEntityFields(record); + }, [record, setEntityFields]); + const [uploadImage] = useUploadImageMutation(); const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular }); @@ -285,6 +288,7 @@ export const RecordShowPage = () => { if (!relationObjectMetadataItem) { return false; } + return isObjectMetadataAvailableForRelation( relationObjectMetadataItem, ); diff --git a/packages/twenty-front/src/modules/object-record/field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/field/components/FieldDisplay.tsx index f8a3d96c9..96461bb95 100644 --- a/packages/twenty-front/src/modules/object-record/field/components/FieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/field/components/FieldDisplay.tsx @@ -27,7 +27,6 @@ import { isFieldUuid } from '../types/guards/isFieldUuid'; export const FieldDisplay = () => { const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext); - if ( isLabelIdentifier && (isFieldText(fieldDefinition) || isFieldFullName(fieldDefinition)) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useModifyRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/hooks/useModifyRecordFromCache.ts index 8a39edf45..43850e494 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useModifyRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useModifyRecordFromCache.ts @@ -1,5 +1,5 @@ import { useApolloClient } from '@apollo/client'; -import { Modifiers } from '@apollo/client/cache'; +import { Modifier, Reference } from '@apollo/client/cache'; import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -12,7 +12,10 @@ export const useModifyRecordFromCache = ({ }) => { const apolloClient = useApolloClient(); - return (recordId: string, fieldModifiers: Modifiers) => { + return ( + recordId: string, + fieldModifiers: Record>, + ) => { if (!objectMetadataItem) { return EMPTY_MUTATION; } @@ -23,7 +26,7 @@ export const useModifyRecordFromCache = ({ id: recordId, }); - cache.modify({ + cache.modify>({ id: cachedRecordId, fields: fieldModifiers, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useDeleteSelectedRecordBoardCardsInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useDeleteSelectedRecordBoardCardsInternal.test.tsx index cc8114ada..5ad7fe8a2 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useDeleteSelectedRecordBoardCardsInternal.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useDeleteSelectedRecordBoardCardsInternal.test.tsx @@ -46,6 +46,7 @@ const mocks = [ variables: { input: { id: mockedUuid, + name: 'Opportunity', pipelineStepId: 'pipelineStepId', companyId: 'New Opportunity', }, diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardCardFieldsInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardCardFieldsInternal.test.tsx index 3a7b5645e..8086a672d 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardCardFieldsInternal.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardCardFieldsInternal.test.tsx @@ -1,5 +1,5 @@ import { act } from 'react-dom/test-utils'; -import { renderHook } from '@testing-library/react'; +import { renderHook, waitFor } from '@testing-library/react'; import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil'; import { FieldType } from '@/object-record/field/types/FieldType'; @@ -48,10 +48,15 @@ describe('useRecordBoardCardFieldsInternal', () => { expect(result.current.cardFieldsList[0].isVisible).toBe(true); act(() => { - result.current.boardCardFields.handleFieldVisibilityChange(field); + result.current.boardCardFields.handleFieldVisibilityChange({ + ...field, + isVisible: true, + }); }); - expect(result.current.cardFieldsList[0].isVisible).toBe(false); + waitFor(() => { + expect(result.current.cardFieldsList[0].isVisible).toBe(false); + }); act(() => { result.current.boardCardFields.handleFieldVisibilityChange({ @@ -60,7 +65,9 @@ describe('useRecordBoardCardFieldsInternal', () => { }); }); - expect(result.current.cardFieldsList[0].isVisible).toBe(true); + waitFor(() => { + expect(result.current.cardFieldsList[0].isVisible).toBe(true); + }); }); it('should call the onFieldsChange callback and update board card states', async () => { diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useCreateOpportunity.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useCreateOpportunity.ts index ebb75e326..de3cd48b5 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useCreateOpportunity.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useCreateOpportunity.ts @@ -24,6 +24,7 @@ export const useCreateOpportunity = () => { await createOneOpportunity?.({ id: newUuid, + name: 'Opportunity', pipelineStepId, companyId: companyId, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardCardFieldsInternal.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardCardFieldsInternal.ts index 3a9395738..59f656adb 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardCardFieldsInternal.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardCardFieldsInternal.ts @@ -30,17 +30,46 @@ export const useRecordBoardCardFieldsInternal = ( savedRecordBoardCardFieldsScopedState({ scopeId }), ); - const handleFieldVisibilityChange = ( - field: Omit, 'size' | 'position'>, - ) => { - setBoardCardFields((previousFields) => - previousFields.map((previousField) => - previousField.fieldMetadataId === field.fieldMetadataId - ? { ...previousField, isVisible: !field.isVisible } - : previousField, - ), - ); - }; + const handleFieldVisibilityChange = useRecoilCallback( + ({ snapshot }) => + async ( + field: Omit, 'size' | 'position'>, + ) => { + const existingFields = await snapshot + .getLoadable(recordBoardCardFieldsScopedState({ scopeId })) + .getValue(); + + const existingFieldsUpdated = existingFields.map((previousField) => + previousField.fieldMetadataId === field.fieldMetadataId + ? { ...previousField, isVisible: !field.isVisible } + : previousField, + ); + + const isNewField = !existingFields.find( + ({ fieldMetadataId }) => field.fieldMetadataId === fieldMetadataId, + ); + + const fields = isNewField + ? [ + ...existingFieldsUpdated, + { + ...field, + position: existingFieldsUpdated.length, + }, + ] + : existingFieldsUpdated; + + setSavedBoardCardFields(fields); + setBoardCardFields(fields); + + const onFieldsChange = snapshot + .getLoadable(onFieldsChangeScopedState({ scopeId })) + .getValue(); + + onFieldsChange?.(fields); + }, + [scopeId, setBoardCardFields, setSavedBoardCardFields], + ); const handleFieldsChange = useRecoilCallback( ({ snapshot }) => diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/hiddenRecordBoardCardFieldsScopedSelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/hiddenRecordBoardCardFieldsScopedSelector.ts index 7924b73c6..28a6a4bb0 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/hiddenRecordBoardCardFieldsScopedSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/hiddenRecordBoardCardFieldsScopedSelector.ts @@ -11,6 +11,7 @@ export const hiddenRecordBoardCardFieldsScopedSelector = createSelectorScopeMap( ({ get }) => { const fields = get(recordBoardCardFieldsScopedState({ scopeId })); const fieldKeys = fields.map(({ fieldMetadataId }) => fieldMetadataId); + const otherAvailableKeys = get( availableRecordBoardCardFieldsScopedState({ scopeId }), ).filter(({ fieldMetadataId }) => !fieldKeys.includes(fieldMetadataId)); diff --git a/packages/twenty-front/src/modules/object-record/record-relation-card/components/RecordRelationFieldCardContent.tsx b/packages/twenty-front/src/modules/object-record/record-relation-card/components/RecordRelationFieldCardContent.tsx index 8545e61aa..4f38d501d 100644 --- a/packages/twenty-front/src/modules/object-record/record-relation-card/components/RecordRelationFieldCardContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-relation-card/components/RecordRelationFieldCardContent.tsx @@ -1,14 +1,19 @@ -import { useContext } from 'react'; +import { useContext, useEffect } from 'react'; +import { Reference } from '@apollo/client'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import { useSetRecoilState } from 'recoil'; import { LightIconButton, MenuItem } from 'tsup.ui.index'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { FieldDisplay } from '@/object-record/field/components/FieldDisplay'; import { FieldContext } from '@/object-record/field/contexts/FieldContext'; import { usePersistField } from '@/object-record/field/hooks/usePersistField'; +import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState'; import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata'; import { useFieldContext } from '@/object-record/hooks/useFieldContext'; +import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { IconDotsVertical, IconUnlink } from '@/ui/display/icon'; @@ -56,12 +61,24 @@ export const RecordRelationFieldCardContent = ({ divider, relationRecord, }: RecordRelationFieldCardContentProps) => { - const { fieldDefinition } = useContext(FieldContext); + const { fieldDefinition, entityId } = useContext(FieldContext); + const { relationFieldMetadataId, relationObjectMetadataNameSingular, relationType, + fieldName, + objectMetadataNameSingular, } = fieldDefinition.metadata as FieldRelationMetadata; + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: objectMetadataNameSingular ?? '', + }); + + const modifyObjectMetadataInCache = useModifyRecordFromCache({ + objectMetadataItem, + }); + const isToOneObject = relationType === 'TO_ONE_OBJECT'; const { labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata, @@ -86,6 +103,15 @@ export const RecordRelationFieldCardContent = ({ const { closeDropdown, isDropdownOpen } = useDropdown(dropdownScopeId); + // TODO: temporary as ChipDisplay expect to find the entity in the entityFieldsFamilyState + const setEntityFields = useSetRecoilState( + entityFieldsFamilyState(relationRecord.id), + ); + + useEffect(() => { + setEntityFields(relationRecord); + }, [relationRecord, setEntityFields]); + if (!FieldContextProvider) return null; const handleDetach = () => { @@ -109,38 +135,66 @@ export const RecordRelationFieldCardContent = ({ [relationFieldMetadataItem.name]: null, }, }); + + modifyObjectMetadataInCache(entityId, { + [fieldName]: (relationRef, { readField }) => { + const edges = readField<{ node: Reference }[]>('edges', relationRef); + + if (!edges) { + return relationRef; + } + + return { + ...relationRef, + edges: edges.filter(({ node }) => { + const id = readField('id', node); + return id !== relationRecord.id; + }), + }; + }, + }); }; + const isOpportunityCompanyRelation = + (objectMetadataNameSingular === CoreObjectNameSingular.Opportunity && + relationObjectMetadataNameSingular === CoreObjectNameSingular.Company) || + (objectMetadataNameSingular === CoreObjectNameSingular.Company && + relationObjectMetadataNameSingular === + CoreObjectNameSingular.Opportunity); + return ( - - - } - dropdownComponents={ - - + - - } - dropdownHotkeyScope={{ - scope: dropdownScopeId, - }} - /> - + } + dropdownComponents={ + + + + } + dropdownHotkeyScope={{ + scope: dropdownScopeId, + }} + /> + + )} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-relation-card/components/RecordRelationFieldCardSection.tsx b/packages/twenty-front/src/modules/object-record/record-relation-card/components/RecordRelationFieldCardSection.tsx index 0df52e980..f7c4511fa 100644 --- a/packages/twenty-front/src/modules/object-record/record-relation-card/components/RecordRelationFieldCardSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-relation-card/components/RecordRelationFieldCardSection.tsx @@ -1,5 +1,6 @@ -import { useCallback, useContext, useEffect, useMemo } from 'react'; +import { useCallback, useContext } from 'react'; import { Link } from 'react-router-dom'; +import { Reference } from '@apollo/client'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; import qs from 'qs'; @@ -12,10 +13,8 @@ import { usePersistField } from '@/object-record/field/hooks/usePersistField'; import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState'; import { entityFieldsFamilySelector } from '@/object-record/field/states/selectors/entityFieldsFamilySelector'; import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; -import { useUpsertRecordFromState } from '@/object-record/hooks/useUpsertRecordFromState'; import { RecordRelationFieldCardContent } from '@/object-record/record-relation-card/components/RecordRelationFieldCardContent'; import { SingleEntitySelectMenuItemsWithSearch } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch'; import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; @@ -87,6 +86,7 @@ export const RecordRelationFieldCardSection = () => { relationFieldMetadataId, relationObjectMetadataNameSingular, relationType, + objectMetadataNameSingular, } = fieldDefinition.metadata as FieldRelationMetadata; const record = useRecoilValue(entityFieldsFamilyState(entityId)); @@ -97,6 +97,10 @@ export const RecordRelationFieldCardSection = () => { objectNameSingular: relationObjectMetadataNameSingular, }); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: objectMetadataNameSingular ?? '', + }); + const relationFieldMetadataItem = relationObjectMetadataItem.fields.find( ({ id }) => id === relationFieldMetadataId, ); @@ -107,48 +111,12 @@ export const RecordRelationFieldCardSection = () => { const isToOneObject = relationType === 'TO_ONE_OBJECT'; - const { record: relationRecordFromFieldValue } = useFindOneRecord({ - objectNameSingular: relationObjectMetadataNameSingular, - objectRecordId: fieldValue?.id, - skip: !relationLabelIdentifierFieldMetadata || !isToOneObject, - }); - - // ONE_TO_MANY records cannot be retrieved from the field value, - // as the record's field is an empty "Connection" object. - // TODO: maybe the backend could return an array of related records instead? - const { records: relationRecordsFromQuery } = useFindManyRecords({ - objectNameSingular: relationObjectMetadataNameSingular, - filter: { - // TODO: this won't work for MANY_TO_MANY relations. - [`${relationFieldMetadataItem?.name}Id`]: { - eq: entityId, - }, - }, - skip: - !relationLabelIdentifierFieldMetadata || - !relationFieldMetadataItem?.name || - isToOneObject, - }); - - const relationRecords = useMemo( - () => - relationRecordFromFieldValue - ? [relationRecordFromFieldValue] - : relationRecordsFromQuery, - [relationRecordFromFieldValue, relationRecordsFromQuery], - ); - const relationRecordIds = useMemo( - () => relationRecords.map(({ id }) => id), - [relationRecords], - ); - - const upsertRecordFromState = useUpsertRecordFromState(); - - useEffect(() => { - relationRecords.forEach((relationRecord) => - upsertRecordFromState(relationRecord), - ); - }, [relationRecords, upsertRecordFromState]); + const relationRecords = !isToOneObject + ? fieldValue?.edges.map(({ node }: { node: any }) => node) ?? [] + : fieldValue + ? [fieldValue] + : []; + const relationRecordIds = relationRecords.map(({ id }: { id: string }) => id); const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}`; @@ -186,6 +154,10 @@ export const RecordRelationFieldCardSection = () => { objectNameSingular: relationObjectMetadataNameSingular, }); + const modifyObjectMetadataInCache = useModifyRecordFromCache({ + objectMetadataItem, + }); + const handleRelationPickerEntitySelected = ( selectedRelationEntity?: EntityForSelect, ) => { @@ -207,9 +179,22 @@ export const RecordRelationFieldCardSection = () => { [relationFieldMetadataItem.name]: record, }, }); - }; - if (!relationLabelIdentifierFieldMetadata) return null; + modifyObjectMetadataInCache(entityId, { + [fieldName]: (relationRef, { readField }) => { + const edges = readField<{ node: Reference }[]>('edges', relationRef); + + if (!edges) { + return relationRef; + } + + return { + ...relationRef, + edges: [...edges, { node: record }], + }; + }, + }); + }; const filterQueryParams: FilterQueryParams = { filter: { @@ -263,13 +248,15 @@ export const RecordRelationFieldCardSection = () => { {!!relationRecords.length && ( - {relationRecords.slice(0, 5).map((relationRecord, index) => ( - - ))} + {relationRecords + .slice(0, 5) + .map((relationRecord: any, index: number) => ( + + ))} )} diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/RelationPicker.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/RelationPicker.tsx index 2cb353c2f..cb50951b1 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/RelationPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/RelationPicker.tsx @@ -1,20 +1,13 @@ -import { useContext, useEffect, useState } from 'react'; -import { useRecoilState } from 'recoil'; +import { useEffect } from 'react'; -import { AddPersonToCompany } from '@/companies/components/AddPersonToCompany'; -import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { FieldDefinition } from '@/object-record/field/types/FieldDefinition'; import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata'; -import { BoardCardIdContext } from '@/object-record/record-board/contexts/BoardCardIdContext'; import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; -import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; import { IconForbid } from '@/ui/display/icon'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; -import { isDefined } from '~/utils/isDefined'; export type RelationPickerProps = { recordId?: string; @@ -40,26 +33,12 @@ export const RelationPicker = ({ setRelationPickerSearchFilter, identifiersMapper, searchQuery, - } = useRelationPicker(); - - const [showAddNewDropdown, setShowAddNewDropdown] = useState(false); - - const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + } = useRelationPicker({ relationPickerScopeId: 'relation-picker' }); useEffect(() => { setRelationPickerSearchFilter(initialSearchFilter ?? ''); }, [initialSearchFilter, setRelationPickerSearchFilter]); - const boardCardId = useContext(BoardCardIdContext); - const weAreInOpportunitiesPageCard = isDefined(boardCardId); - - const [companyProgress] = useRecoilState( - companyProgressesFamilyState(boardCardId ?? ''), - ); - - const { company } = companyProgress ?? {}; - const companyId = company?.id; - const { objectNameSingular: relationObjectNameSingular } = useObjectNameSingularFromPlural({ objectNamePlural: @@ -90,41 +69,18 @@ export const RelationPicker = ({ const handleEntitySelected = (selectedEntity: any | null | undefined) => onSubmit(selectedEntity ?? null); - const entitiesToSelect = entities.entitiesToSelect.filter((entity) => - weAreInOpportunitiesPageCard ? entity.record.companyId === companyId : true, - ); - - const weAreAddingNewPerson = - weAreInOpportunitiesPageCard && showAddNewDropdown && companyId; - return ( <> - {!weAreAddingNewPerson ? ( - { - if (weAreInOpportunitiesPageCard) { - setShowAddNewDropdown(true); - setHotkeyScopeAndMemorizePreviousScope( - RelationPickerHotkeyScope.AddNew, - ); - } - }} - /> - ) : ( - setShowAddNewDropdown(false)} - /> - )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItems.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItems.tsx index 05356e89d..84a74d35d 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItems.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItems.tsx @@ -1,8 +1,7 @@ -import { useContext, useRef } from 'react'; +import { useRef } from 'react'; import { isNonEmptyString } from '@sniptt/guards'; import { Key } from 'ts-key-enum'; -import { BoardCardIdContext } from '@/object-record/record-board/contexts/BoardCardIdContext'; import { SelectableMenuItemSelect } from '@/object-record/relation-picker/components/SelectableMenuItemSelect'; import { IconPlus } from '@/ui/display/icon'; import { IconComponent } from '@/ui/display/icon/types/IconComponent'; @@ -15,7 +14,6 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { assertNotNull } from '~/utils/assert'; -import { isDefined } from '~/utils/isDefined'; import { EntityForSelect } from '../types/EntityForSelect'; import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope'; @@ -71,12 +69,6 @@ export const SingleEntitySelectMenuItems = ({ const selectableItemIds = entitiesInDropdown.map((entity) => entity.id); - const boardCardId = useContext(BoardCardIdContext); - const weAreInOpportunitiesPageCard = isDefined(boardCardId); - - const hideSearchResults = - weAreInOpportunitiesPageCard && !entitiesInDropdown.length; - return (
- {!hideSearchResults && ( - <> - - {loading ? ( - - ) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? ( - - ) : ( - <> - {isAllEntitySelectShown && - selectAllLabel && - onAllEntitySelected && ( - onAllEntitySelected()} - LeftIcon={SelectAllIcon} - text={selectAllLabel} - selected={!!isAllEntitySelected} - /> - )} - {emptyLabel && ( + <> + + {loading ? ( + + ) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? ( + + ) : ( + <> + {isAllEntitySelectShown && + selectAllLabel && + onAllEntitySelected && ( onEntitySelected()} - LeftIcon={EmptyIcon} - text={emptyLabel} - selected={!selectedEntity} + key="select-all" + onClick={() => onAllEntitySelected()} + LeftIcon={SelectAllIcon} + text={selectAllLabel} + selected={!!isAllEntitySelected} /> )} - - )} - - - {entitiesInDropdown?.map((entity) => ( - - ))} - - - )} - {(hideSearchResults || showCreateButton) && !loading && ( + {emptyLabel && ( + onEntitySelected()} + LeftIcon={EmptyIcon} + text={emptyLabel} + selected={!selectedEntity} + /> + )} + + )} + + + {entitiesInDropdown?.map((entity) => ( + + ))} + + + {showCreateButton && !loading && ( {entitiesToSelect.length > 0 && } - {!hideSearchInput && ( - - )} + { } modifyRecordFromCache(viewIdToPersist ?? '', { - viewFields: () => ({ - edges: viewFieldsToPersist.map((viewField) => ({ - node: viewField, - cursor: '', - })), - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, - }), + viewFields: (viewFieldsRef, { readField }) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + viewFieldsRef, + ); + + if (!edges) return viewFieldsRef; + + return { + ...viewFieldsRef, + edges: viewFieldsToPersist.map((viewField) => ({ + node: viewField, + cursor: '', + })), + }; + }, }); onViewFieldsChange?.(viewFieldsToPersist); diff --git a/packages/twenty-front/src/modules/views/hooks/internal/useViewFilters.ts b/packages/twenty-front/src/modules/views/hooks/internal/useViewFilters.ts index 6d651dc78..a490c9575 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/useViewFilters.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/useViewFilters.ts @@ -1,4 +1,4 @@ -import { useApolloClient } from '@apollo/client'; +import { Reference, useApolloClient } from '@apollo/client'; import { produce } from 'immer'; import { useRecoilCallback } from 'recoil'; @@ -145,18 +145,22 @@ export const useViewFilters = (viewScopeId: string) => { } modifyRecordFromCache(existingViewId, { - viewFilters: () => ({ - edges: currentViewFilters.map((viewFilter) => ({ - node: viewFilter, - cursor: '', - })), - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, - }), + viewFilters: (viewFiltersRef, { readField }) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + viewFiltersRef, + ); + + if (!edges) return viewFiltersRef; + + return { + ...viewFiltersRef, + edges: currentViewFilters.map((viewFilter) => ({ + node: viewFilter, + cursor: '', + })), + }; + }, }); }, [ diff --git a/packages/twenty-front/src/modules/views/hooks/internal/useViewSorts.ts b/packages/twenty-front/src/modules/views/hooks/internal/useViewSorts.ts index 0598f5326..989d93677 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/useViewSorts.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/useViewSorts.ts @@ -1,4 +1,4 @@ -import { useApolloClient } from '@apollo/client'; +import { Reference, useApolloClient } from '@apollo/client'; import { produce } from 'immer'; import { useRecoilCallback } from 'recoil'; @@ -138,18 +138,22 @@ export const useViewSorts = (viewScopeId: string) => { } modifyRecordFromCache(existingViewId, { - viewSorts: () => ({ - edges: currentViewSorts.map((viewSort) => ({ - node: viewSort, - cursor: '', - })), - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, - }), + viewSorts: (viewSortsRef, { readField }) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + viewSortsRef, + ); + + if (!edges) return viewSortsRef; + + return { + ...viewSortsRef, + edges: currentViewSorts.map((viewSort) => ({ + node: viewSort, + cursor: '', + })), + }; + }, }); }, [ diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx index e00f115fb..7039b338c 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { Reference } from '@apollo/client'; import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; @@ -163,15 +164,17 @@ export const SettingsObjectNewFieldStep2 = () => { modifyViewFromCache(view.id, { // Todo fix typing - viewFields: (viewFields: any) => { + viewFields: (viewFieldsRef, { readField }) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + viewFieldsRef, + ); + + if (!edges) return viewFieldsRef; + return { - edges: viewFields.edges.concat({ node: viewFieldToCreate }), - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, + ...viewFieldsRef, + edges: [...edges, { node: viewFieldToCreate }], }; }, }); @@ -188,16 +191,17 @@ export const SettingsObjectNewFieldStep2 = () => { size: 100, }; modifyViewFromCache(view.id, { - // Todo fix typing - viewFields: (viewFields: any) => { + viewFields: (viewFieldsRef, { readField }) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + viewFieldsRef, + ); + + if (!edges) return viewFieldsRef; + return { - edges: viewFields.edges.concat({ node: viewFieldToCreate }), - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, + ...viewFieldsRef, + edges: [...edges, { node: viewFieldToCreate }], }; }, }); @@ -232,16 +236,17 @@ export const SettingsObjectNewFieldStep2 = () => { }; modifyViewFromCache(view.id, { - // Todo fix typing - viewFields: (viewFields: any) => { + viewFields: (viewFieldsRef, { readField }) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + viewFieldsRef, + ); + + if (!edges) return viewFieldsRef; + return { - edges: viewFields.edges.concat({ node: viewFieldToCreate }), - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, + ...viewFieldsRef, + edges: [...edges, { node: viewFieldToCreate }], }; }, }); diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunity.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunity.ts index 3b476985e..c6ec05117 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunity.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunity.ts @@ -11,59 +11,59 @@ export const seedOpportunity = async ( .insert() .into(`${schemaName}.${tableName}`, [ 'id', + 'name', 'amountAmountMicros', 'amountCurrencyCode', 'closeDate', 'probability', 'pipelineStepId', 'pointOfContactId', - 'personId', 'companyId', ]) .orIgnore() .values([ { id: '7c887ee3-be10-412b-a663-16bd3c2228e1', + name: 'Opportunity 1', amountAmountMicros: 100000, amountCurrencyCode: 'USD', closeDate: new Date(), probability: 0.5, pipelineStepId: '6edf4ead-006a-46e1-9c6d-228f1d0143c9', pointOfContactId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5', - personId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5', companyId: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408', }, { id: '53f66647-0543-4cc2-9f96-95cc699960f2', + name: 'Opportunity 2', 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', + name: 'Opportunity 3', 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', + name: 'Opportunity 4', 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', }, ]) diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata.ts index 51ab1bef9..c8b7a72ae 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata.ts @@ -6,6 +6,7 @@ import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators import { ActivityObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity.object-metadata'; import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata'; +import { OpportunityObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata'; import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata'; @ObjectMetadata({ @@ -46,4 +47,14 @@ export class ActivityTargetObjectMetadata extends BaseObjectMetadata { }) @IsNullable() company: CompanyObjectMetadata; + + @FieldMetadata({ + type: FieldMetadataType.RELATION, + label: 'Opportunity', + description: 'ActivityTarget opportunity', + icon: 'IconTargetArrow', + joinColumn: 'opportunityId', + }) + @IsNullable() + opportunity: OpportunityObjectMetadata; } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata.ts index 4ce781818..7bd141df4 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata.ts @@ -1,8 +1,11 @@ import { CurrencyMetadata } from 'src/metadata/field-metadata/composite-types/currency.composite-type'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator'; +import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator'; +import { ActivityTargetObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata'; import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata'; import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata'; @@ -16,6 +19,14 @@ import { PipelineStepObjectMetadata } from 'src/workspace/workspace-sync-metadat icon: 'IconTargetArrow', }) export class OpportunityObjectMetadata extends BaseObjectMetadata { + @FieldMetadata({ + type: FieldMetadataType.TEXT, + label: 'Name', + description: 'The opportunity name', + icon: 'IconTargetArrow', + }) + name: string; + @FieldMetadata({ type: FieldMetadataType.CURRENCY, label: 'Amount', @@ -65,16 +76,6 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata { @IsNullable() pointOfContact: PersonObjectMetadata; - @FieldMetadata({ - type: FieldMetadataType.RELATION, - label: 'Person', - description: 'Opportunity person', - icon: 'IconUser', - joinColumn: 'personId', - }) - @IsNullable() - person: PersonObjectMetadata; - @FieldMetadata({ type: FieldMetadataType.RELATION, label: 'Company', @@ -84,4 +85,17 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata { }) @IsNullable() company: CompanyObjectMetadata; + + @FieldMetadata({ + type: FieldMetadataType.RELATION, + label: 'Activities', + description: 'Activities tied to the opportunity', + icon: 'IconCheckbox', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + objectName: 'activityTarget', + }) + @IsNullable() + activityTargets: ActivityTargetObjectMetadata[]; } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata.ts index bcdacf251..604db4d2a 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata.ts @@ -135,19 +135,6 @@ export class PersonObjectMetadata extends BaseObjectMetadata { @IsNullable() activityTargets: ActivityTargetObjectMetadata[]; - @FieldMetadata({ - type: FieldMetadataType.RELATION, - label: 'Opportunities', - description: 'Opportunities linked to the contact.', - icon: 'IconTargetArrow', - }) - @RelationMetadata({ - type: RelationMetadataType.ONE_TO_MANY, - objectName: 'opportunity', - }) - @IsNullable() - opportunities: OpportunityObjectMetadata[]; - @FieldMetadata({ type: FieldMetadataType.RELATION, label: 'Favorites',