diff --git a/packages/twenty-front/src/__stories__/App.stories.tsx b/packages/twenty-front/src/__stories__/App.stories.tsx index 9c9c3547ed..4ac0c1a6be 100644 --- a/packages/twenty-front/src/__stories__/App.stories.tsx +++ b/packages/twenty-front/src/__stories__/App.stories.tsx @@ -1,5 +1,4 @@ import { HelmetProvider } from 'react-helmet-async'; -import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; @@ -7,6 +6,7 @@ import { ObjectMetadataItemsProvider } from '@/object-metadata/components/Object import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { UserProvider } from '@/users/components/UserProvider'; import { App } from '~/App'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout'; import { graphqlMocks } from '~/testing/graphqlMocks'; @@ -14,20 +14,19 @@ const meta: Meta = { title: 'App/App', component: App, decorators: [ + MemoryRouterDecorator, (Story) => ( - - - - - - - - - - - + + + + + + + + + ), diff --git a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx b/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx index e7b4bbe740..ebeaa3f791 100644 --- a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx @@ -1,8 +1,8 @@ -import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; import { TaskList } from '@/activities/tasks/components/TaskList'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { mockedActivities } from '~/testing/mock-data/activities'; @@ -10,15 +10,7 @@ import { mockedActivities } from '~/testing/mock-data/activities'; const meta: Meta = { title: 'Modules/Activity/TaskList', component: TaskList, - decorators: [ - (Story) => ( - - - - ), - ComponentDecorator, - SnackBarDecorator, - ], + decorators: [MemoryRouterDecorator, ComponentDecorator, SnackBarDecorator], args: { title: 'Tasks', tasks: mockedActivities, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts index a9a282c73d..3a88f95ce8 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts @@ -3,6 +3,7 @@ import { useQuery } from '@apollo/client'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/hooks/useGenerateFindDuplicateRecordsQuery'; import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; @@ -29,6 +30,10 @@ export const useFindDuplicateRecords = ({ const { enqueueSnackBar } = useSnackBar(); + const queryResponseField = getFindDuplicateRecordsQueryResponseField( + objectMetadataItem.nameSingular, + ); + const { data, loading, error } = useQuery>( findDuplicateRecordsQuery, { @@ -36,7 +41,7 @@ export const useFindDuplicateRecords = ({ id: objectRecordId, }, onCompleted: (data) => { - onCompleted?.(data[objectMetadataItem.nameSingular]); + onCompleted?.(data[queryResponseField]); }, onError: (error) => { logError( @@ -53,8 +58,7 @@ export const useFindDuplicateRecords = ({ }, ); - const objectRecordConnection = - data?.[`${objectMetadataItem.nameSingular}Duplicates`]; + const objectRecordConnection = data?.[queryResponseField]; const mapConnectionToRecords = useMapConnectionToRecords(); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindDuplicateRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindDuplicateRecordsQuery.ts index 160d6ba2f7..0f5911b71c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindDuplicateRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindDuplicateRecordsQuery.ts @@ -4,6 +4,10 @@ import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMa import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { capitalize } from '~/utils/string/capitalize'; +export const getFindDuplicateRecordsQueryResponseField = ( + objectNameSingular: string, +) => `${objectNameSingular}Duplicates`; + export const useGenerateFindDuplicateRecordsQuery = () => { const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery(); @@ -15,7 +19,9 @@ export const useGenerateFindDuplicateRecordsQuery = () => { depth?: number; }) => gql` query FindDuplicate${capitalize(objectMetadataItem.nameSingular)}($id: ID) { - ${objectMetadataItem.nameSingular}Duplicates(id: $id){ + ${getFindDuplicateRecordsQueryResponseField( + objectMetadataItem.nameSingular, + )}(id: $id) { edges { node { id diff --git a/packages/twenty-front/src/modules/object-record/record-field/__mocks__/fieldDefinitions.ts b/packages/twenty-front/src/modules/object-record/record-field/__mocks__/fieldDefinitions.ts index 3dc12c5fd4..9c3405f197 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/__mocks__/fieldDefinitions.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/__mocks__/fieldDefinitions.ts @@ -18,7 +18,7 @@ const mockedPersonObjectMetadataItem = { ...mockedPeopleMetadata.node, fields: mockedPeopleMetadata.node.fields.edges.map(({ node }) => node), }; -const mockedCompanyObjectMetadataItem = { +export const mockedCompanyObjectMetadataItem = { ...mockedCompaniesMetadata.node, fields: mockedCompaniesMetadata.node.fields.edges.map(({ node }) => node), }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/ChipFieldDisplay.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/ChipFieldDisplay.stories.tsx index 09dd62aa2a..7b963b45c9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/ChipFieldDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/ChipFieldDisplay.stories.tsx @@ -1,13 +1,12 @@ import { useEffect } from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; import { useSetRecoilState } from 'recoil'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { ChipFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ChipFieldDisplay'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; - -import { FieldContext } from '../../../../contexts/FieldContext'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; const ChipFieldValueSetterEffect = () => { const setEntityFields = useSetRecoilState(recordStoreFamilyState('123')); @@ -28,30 +27,29 @@ const ChipFieldValueSetterEffect = () => { const meta: Meta = { title: 'UI/Data/Field/Display/ChipFieldDisplay', decorators: [ + MemoryRouterDecorator, (Story) => ( - - - - - - + }, + hotkeyScope: 'hotkey-scope', + }} + > + + + ), ComponentDecorator, ], diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx index 969d7341ec..902eefa4b3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx @@ -1,11 +1,11 @@ import { useEffect } from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { useEmailField } from '@/object-record/record-field/meta-types/hooks/useEmailField'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; -import { FieldContext } from '../../../../contexts/FieldContext'; -import { useEmailField } from '../../../hooks/useEmailField'; import { EmailFieldDisplay } from '../EmailFieldDisplay'; const EmailFieldValueSetterEffect = ({ value }: { value: string }) => { @@ -21,6 +21,7 @@ const EmailFieldValueSetterEffect = ({ value }: { value: string }) => { const meta: Meta = { title: 'UI/Data/Field/Display/EmailFieldDisplay', decorators: [ + MemoryRouterDecorator, (Story, { args }) => ( - - - - + + ), ComponentDecorator, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx index f7b431bd9c..1425b23afd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx @@ -1,11 +1,11 @@ import { useEffect } from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { usePhoneField } from '@/object-record/record-field/meta-types/hooks/usePhoneField'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; -import { FieldContext } from '../../../../contexts/FieldContext'; -import { usePhoneField } from '../../../hooks/usePhoneField'; import { PhoneFieldDisplay } from '../PhoneFieldDisplay'; const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => { @@ -21,6 +21,7 @@ const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => { const meta: Meta = { title: 'UI/Data/Field/Display/PhoneFieldDisplay', decorators: [ + MemoryRouterDecorator, (Story, { args }) => ( [() => undefined, {}], }} > - - - - + + ), ComponentDecorator, diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index 576b44ce53..6c39968d56 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -13,8 +13,8 @@ import { import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; -import { RecordDuplicatesFieldCardSection } from '@/object-record/record-show/record-detail-section/components/RecordDuplicatesFieldCardSection'; -import { RecordRelationFieldCardSection } from '@/object-record/record-show/record-detail-section/components/RecordRelationFieldCardSection'; +import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection'; +import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection'; import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector'; @@ -194,7 +194,7 @@ export const RecordShowContainer = ({ ))} - @@ -229,7 +229,7 @@ export const RecordShowContainer = ({ hotkeyScope: InlineCellHotkeyScope.InlineCell, }} > - + ))} diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection.tsx new file mode 100644 index 0000000000..3a82313b87 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection.tsx @@ -0,0 +1,37 @@ +import { RecordChip } from '@/object-record/components/RecordChip'; +import { useFindDuplicateRecords } from '@/object-record/hooks/useFindDuplicateRecords'; +import { RecordDetailRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsList'; +import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem'; +import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection'; +import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader'; + +export const RecordDetailDuplicatesSection = ({ + objectRecordId, + objectNameSingular, +}: { + objectRecordId: string; + objectNameSingular: string; +}) => { + const { records: duplicateRecords } = useFindDuplicateRecords({ + objectRecordId, + objectNameSingular, + }); + + if (!duplicateRecords.length) return null; + + return ( + + + + {duplicateRecords.slice(0, 5).map((duplicateRecord) => ( + + + + ))} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRecordsList.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRecordsList.tsx new file mode 100644 index 0000000000..d18c51bec9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRecordsList.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +const StyledRecordsList = styled.div` + color: ${({ theme }) => theme.font.color.secondary}; + overflow: hidden; +`; + +export { StyledRecordsList as RecordDetailRecordsList }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem.tsx new file mode 100644 index 0000000000..ca2762fca2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem.tsx @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +const StyledListItem = styled.div` + align-items: center; + justify-content: space-between; + gap: ${({ theme }) => theme.spacing(1)}; + display: flex; + height: ${({ theme }) => theme.spacing(10)}; +`; + +export { StyledListItem as RecordDetailRecordsListItem }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList.tsx new file mode 100644 index 0000000000..0c2df8ae8d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList.tsx @@ -0,0 +1,20 @@ +import { RecordDetailRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsList'; +import { RecordDetailRelationRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; + +type RecordDetailRelationRecordsListProps = { + relationRecords: ObjectRecord[]; +}; + +export const RecordDetailRelationRecordsList = ({ + relationRecords, +}: RecordDetailRelationRecordsListProps) => ( + + {relationRecords.slice(0, 5).map((relationRecord) => ( + + ))} + +); diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListEmptyState.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListEmptyState.tsx new file mode 100644 index 0000000000..3cd8696435 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListEmptyState.tsx @@ -0,0 +1,35 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useIcons } from '@/ui/display/icon/hooks/useIcons'; + +type RecordDetailRelationRecordsListEmptyStateProps = { + relationObjectMetadataItem: ObjectMetadataItem; +}; + +const StyledRelationRecordsListEmptyState = styled.div` + color: ${({ theme }) => theme.font.color.light}; + align-items: center; + justify-content: center; + gap: ${({ theme }) => theme.spacing(2)}; + display: flex; + height: ${({ theme }) => theme.spacing(10)}; + text-transform: capitalize; +`; + +export const RecordDetailRelationRecordsListEmptyState = ({ + relationObjectMetadataItem, +}: RecordDetailRelationRecordsListEmptyStateProps) => { + const theme = useTheme(); + + const { getIcon } = useIcons(); + const Icon = getIcon(relationObjectMetadataItem.icon); + + return ( + + +
No {relationObjectMetadataItem.labelSingular}
+
+ ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordRelationFieldCardContent.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx similarity index 85% rename from packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordRelationFieldCardContent.tsx rename to packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx index 1d7b1067df..dd801414b6 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordRelationFieldCardContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx @@ -11,35 +11,26 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { IconDotsVertical, IconTrash, IconUnlink } from '@/ui/display/icon'; -import { CardContent } from '@/ui/layout/card/components/CardContent'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; -const StyledCardContent = styled(CardContent)<{ +const StyledListItem = styled(RecordDetailRecordsListItem)<{ isDropdownOpen?: boolean; }>` - align-items: center; - justify-content: space-between; - gap: ${({ theme }) => theme.spacing(1)}; - display: flex; - height: ${({ theme }) => theme.spacing(10)}; - padding: 0; - border: 0; - ${({ isDropdownOpen, theme }) => - isDropdownOpen - ? '' - : css` - .displayOnHover { - opacity: 0; - pointer-events: none; - transition: opacity ${theme.animation.duration.instant}s ease; - } - `} + !isDropdownOpen && + css` + .displayOnHover { + opacity: 0; + pointer-events: none; + transition: opacity ${theme.animation.duration.instant}s ease; + } + `} &:hover { .displayOnHover { @@ -49,15 +40,13 @@ const StyledCardContent = styled(CardContent)<{ } `; -type RecordRelationFieldCardContentProps = { - divider?: boolean; +type RecordDetailRelationRecordsListItemProps = { relationRecord: ObjectRecord; }; -export const RecordRelationFieldCardContent = ({ - divider, +export const RecordDetailRelationRecordsListItem = ({ relationRecord, -}: RecordRelationFieldCardContentProps) => { +}: RecordDetailRelationRecordsListItemProps) => { const { fieldDefinition } = useContext(FieldContext); const { @@ -124,7 +113,7 @@ export const RecordRelationFieldCardContent = ({ CoreObjectNameSingular.WorkspaceMember; return ( - + )} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordRelationFieldCardSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx similarity index 75% rename from packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordRelationFieldCardSection.tsx rename to packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index ca54e981e5..c3c2133377 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordRelationFieldCardSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -1,5 +1,4 @@ import { useCallback, useContext } from 'react'; -import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import qs from 'qs'; import { useRecoilValue } from 'recoil'; @@ -9,8 +8,10 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList'; +import { RecordDetailRelationRecordsListEmptyState } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListEmptyState'; +import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection'; import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader'; -import { RecordRelationFieldCardContent } from '@/object-record/record-show/record-detail-section/components/RecordRelationFieldCardContent'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { SingleEntitySelectMenuItemsWithSearch } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch'; @@ -19,12 +20,10 @@ import { RelationPickerScope } from '@/object-record/relation-picker/scopes/Rela import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { IconForbid, IconPencil, IconPlus } from '@/ui/display/icon'; -import { useIcons } from '@/ui/display/icon/hooks/useIcons'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; -import { Section } from '@/ui/layout/section/components/Section'; import { FilterQueryParams } from '@/views/hooks/internal/useFiltersFromQueryParams'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; @@ -32,30 +31,7 @@ const StyledAddDropdown = styled(Dropdown)` margin-left: auto; `; -const StyledCardNoContent = styled.div` - color: ${({ theme }) => theme.font.color.light}; - align-items: center; - justify-content: center; - gap: ${({ theme }) => theme.spacing(2)}; - display: flex; - height: ${({ theme }) => theme.spacing(10)}; - text-transform: capitalize; -`; - -const StyledCard = styled.div` - color: ${({ theme }) => theme.font.color.secondary}; - overflow: hidden; -`; - -const StyledSection = styled(Section)` - padding: ${({ theme }) => theme.spacing(3)}; - border-top: 1px solid ${({ theme }) => theme.border.color.light}; - width: unset; -`; - -export const RecordRelationFieldCardSection = () => { - const theme = useTheme(); - +export const RecordDetailRelationSection = () => { const { entityId, fieldDefinition } = useContext(FieldContext); const { fieldName, @@ -65,12 +41,10 @@ export const RecordRelationFieldCardSection = () => { } = fieldDefinition.metadata as FieldRelationMetadata; const record = useRecoilValue(recordStoreFamilyState(entityId)); - const { - labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata, - objectMetadataItem: relationObjectMetadataItem, - } = useObjectMetadataItem({ - objectNameSingular: relationObjectMetadataNameSingular, - }); + const { objectMetadataItem: relationObjectMetadataItem } = + useObjectMetadataItem({ + objectNameSingular: relationObjectMetadataNameSingular, + }); const relationFieldMetadataItem = relationObjectMetadataItem.fields.find( ({ id }) => id === relationFieldMetadataId, @@ -137,11 +111,8 @@ export const RecordRelationFieldCardSection = () => { relationObjectMetadataItem.namePlural }?${qs.stringify(filterQueryParams)}`; - const { getIcon } = useIcons(); - const Icon = getIcon(relationObjectMetadataItem.icon); - return ( - + { } /> - {relationRecords.length === 0 && ( - - -
No {relationObjectMetadataItem.labelSingular}
-
+ {relationRecords.length ? ( + + ) : ( + )} - {!!relationRecords.length && ( - - {relationRecords.slice(0, 5).map((relationRecord, index) => ( - - ))} - - )} -
+ ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailSection.tsx new file mode 100644 index 0000000000..8a9218ae9a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailSection.tsx @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +import { Section } from '@/ui/layout/section/components/Section'; + +const StyledRecordDetailSection = styled(Section)` + border-top: 1px solid ${({ theme }) => theme.border.color.light}; + padding: ${({ theme }) => theme.spacing(3)}; + width: auto; +`; + +export { StyledRecordDetailSection as RecordDetailSection }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDuplicatesFieldCardSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDuplicatesFieldCardSection.tsx deleted file mode 100644 index e3beaafe23..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDuplicatesFieldCardSection.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import styled from '@emotion/styled'; - -import { RecordChip } from '@/object-record/components/RecordChip'; -import { useFindDuplicateRecords } from '@/object-record/hooks/useFindDuplicateRecords'; -import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader'; -import { Card } from '@/ui/layout/card/components/Card'; -import { CardContent } from '@/ui/layout/card/components/CardContent'; -import { Section } from '@/ui/layout/section/components/Section'; - -const StyledCardContent = styled(CardContent)` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(4)}; - padding: ${({ theme }) => theme.spacing(3)}; -`; - -export const RecordDuplicatesFieldCardSection = ({ - objectRecordId, - objectNameSingular, -}: { - objectRecordId: string; - objectNameSingular: string; -}) => { - const { records: duplicateRecords } = useFindDuplicateRecords({ - objectRecordId, - objectNameSingular, - }); - - if (duplicateRecords.length === 0) { - return null; - } - - return ( -
- - - {duplicateRecords.slice(0, 5).map((duplicateRecord, index) => ( - - - - ))} - -
- ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailDuplicatesSection.stories.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailDuplicatesSection.stories.tsx new file mode 100644 index 0000000000..b2d517f325 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailDuplicatesSection.stories.tsx @@ -0,0 +1,35 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; +import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; +import { graphqlMocks } from '~/testing/graphqlMocks'; +import { mockedCompaniesData } from '~/testing/mock-data/companies'; + +import { RecordDetailDuplicatesSection } from '../RecordDetailDuplicatesSection'; + +const meta: Meta = { + title: + 'Modules/ObjectRecord/RecordShow/RecordDetailSection/RecordDetailDuplicatesSection', + component: RecordDetailDuplicatesSection, + decorators: [ + ComponentDecorator, + ObjectMetadataItemsDecorator, + SnackBarDecorator, + MemoryRouterDecorator, + ], + args: { + objectRecordId: mockedCompaniesData[0].id, + objectNameSingular: CoreObjectNameSingular.Company, + }, + parameters: { + msw: graphqlMocks, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx new file mode 100644 index 0000000000..19ea41cac3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx @@ -0,0 +1,67 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; +import { mockedCompanyObjectMetadataItem } from '@/object-record/record-field/__mocks__/fieldDefinitions'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; +import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; +import { RecordStoreDecorator } from '~/testing/decorators/RecordStoreDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; +import { graphqlMocks } from '~/testing/graphqlMocks'; +import { mockedCompaniesData } from '~/testing/mock-data/companies'; +import { mockedPeopleData } from '~/testing/mock-data/people'; + +import { RecordDetailRelationSection } from '../RecordDetailRelationSection'; + +const meta: Meta = { + title: + 'Modules/ObjectRecord/RecordShow/RecordDetailSection/RecordDetailRelationSection', + component: RecordDetailRelationSection, + decorators: [ + (Story) => ( + name === 'people', + )!, + objectMetadataItem: mockedCompanyObjectMetadataItem, + }), + hotkeyScope: 'hotkey-scope', + }} + > + + + ), + ComponentDecorator, + ObjectMetadataItemsDecorator, + SnackBarDecorator, + MemoryRouterDecorator, + ], + parameters: { + msw: graphqlMocks, + records: mockedCompaniesData, + }, +}; + +export default meta; +type Story = StoryObj; + +export const EmptyState: Story = {}; + +export const WithRecords: Story = { + decorators: [RecordStoreDecorator], + parameters: { + records: [ + { + ...mockedCompaniesData[0], + people: { edges: mockedPeopleData.map((person) => ({ node: person })) }, + }, + ...mockedPeopleData, + ], + }, +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx index 4a942727d8..6fdc29ea87 100644 --- a/packages/twenty-front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx @@ -1,8 +1,8 @@ -import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; import { Field, FieldMetadataType } from '~/generated-metadata/graphql'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { RecordStoreDecorator } from '~/testing/decorators/RecordStoreDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; @@ -81,13 +81,7 @@ export const Date: Story = { }; export const Link: Story = { - decorators: [ - (Story) => ( - - - - ), - ], + decorators: [MemoryRouterDecorator], args: { fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find( ({ node }) => node.type === FieldMetadataType.Link, @@ -114,13 +108,7 @@ export const Rating: Story = { }; export const Relation: Story = { - decorators: [ - (Story) => ( - - - - ), - ], + decorators: [MemoryRouterDecorator], args: { fieldMetadata: mockedPeopleMetadata.node.fields.edges.find( ({ node }) => node.type === FieldMetadataType.Relation, diff --git a/packages/twenty-front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx index 568c23a83a..69a883f510 100644 --- a/packages/twenty-front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx @@ -1,4 +1,3 @@ -import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/test'; import { fn } from '@storybook/test'; @@ -8,6 +7,7 @@ import { RelationMetadataType, } from '~/generated-metadata/graphql'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; @@ -78,13 +78,7 @@ const relationFieldMetadata = mockedPeopleMetadata.node.fields.edges.find( )!.node; export const WithRelationForm: Story = { - decorators: [ - (Story) => ( - - - - ), - ], + decorators: [MemoryRouterDecorator], args: { fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find( ({ node }) => node.type === FieldMetadataType.Relation, diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx index 825122c533..24daaea855 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx @@ -1,9 +1,9 @@ -import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; import { useSetRecoilState } from 'recoil'; import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { actionBarOpenState } from '../../states/actionBarIsOpenState'; import { ActionBar } from '../ActionBar'; @@ -18,14 +18,13 @@ const meta: Meta = { title: 'UI/Navigation/ActionBar/ActionBar', component: FilledActionBar, decorators: [ + MemoryRouterDecorator, (Story) => ( {}} > - - - + ), ComponentDecorator, diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx index ea3f62ca44..ee9eddd2f8 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx @@ -1,9 +1,9 @@ -import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; import { useSetRecoilState } from 'recoil'; import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { contextMenuIsOpenState } from '../../states/contextMenuIsOpenState'; import { contextMenuPositionState } from '../../states/contextMenuPositionState'; @@ -24,14 +24,13 @@ const meta: Meta = { title: 'UI/Navigation/ContextMenu/ContextMenu', component: FilledContextMenu, decorators: [ + MemoryRouterDecorator, (Story) => ( {}} > - - - + ), ComponentDecorator, diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawerItem.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawerItem.stories.tsx index cf5a5cece5..c7f7957d89 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawerItem.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawerItem.stories.tsx @@ -1,10 +1,10 @@ -import { MemoryRouter } from 'react-router-dom'; import styled from '@emotion/styled'; import { Meta, StoryObj } from '@storybook/react'; import { IconSearch } from '@/ui/display/icon'; import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { CatalogStory } from '~/testing/types'; import { NavigationDrawerItem } from '../NavigationDrawerItem'; @@ -47,11 +47,7 @@ export const Catalog: CatalogStory = { ), CatalogDecorator, - (Story) => ( - - - - ), + MemoryRouterDecorator, ], parameters: { pseudo: { hover: ['.hover'] }, diff --git a/packages/twenty-front/src/testing/decorators/MemoryRouterDecorator.tsx b/packages/twenty-front/src/testing/decorators/MemoryRouterDecorator.tsx new file mode 100644 index 0000000000..909790ed34 --- /dev/null +++ b/packages/twenty-front/src/testing/decorators/MemoryRouterDecorator.tsx @@ -0,0 +1,8 @@ +import { MemoryRouter } from 'react-router-dom'; +import { Decorator } from '@storybook/react'; + +export const MemoryRouterDecorator: Decorator = (Story) => ( + + + +); diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index 50ab864e61..e7631da7e4 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -7,7 +7,10 @@ import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queri import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { mockedActivities } from '~/testing/mock-data/activities'; -import { mockedCompaniesData } from '~/testing/mock-data/companies'; +import { + mockedCompaniesData, + mockedDuplicateCompanyData, +} from '~/testing/mock-data/companies'; import { mockedClientConfig } from '~/testing/mock-data/config'; import { mockedPipelineSteps } from '~/testing/mock-data/pipeline-steps'; import { mockedUsersData } from '~/testing/mock-data/users'; @@ -140,6 +143,49 @@ export const graphqlMocks = { }, }); }), + graphql.query('FindDuplicateCompany', () => { + return HttpResponse.json({ + data: { + companyDuplicates: { + edges: [ + { + node: { + ...mockedDuplicateCompanyData, + favorites: { + edges: [], + __typename: 'FavoriteConnection', + }, + attachments: { + edges: [], + __typename: 'AttachmentConnection', + }, + people: { + edges: [], + __typename: 'PersonConnection', + }, + opportunities: { + edges: [], + __typename: 'OpportunityConnection', + }, + activityTargets: { + edges: [], + __typename: 'ActivityTargetConnection', + }, + }, + cursor: null, + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + totalCount: 1, + }, + }, + }); + }), graphql.query('FindManyPeople', () => { return HttpResponse.json({ data: { diff --git a/packages/twenty-front/src/testing/mock-data/companies.ts b/packages/twenty-front/src/testing/mock-data/companies.ts index 52dfaa4615..0e779ab3cd 100644 --- a/packages/twenty-front/src/testing/mock-data/companies.ts +++ b/packages/twenty-front/src/testing/mock-data/companies.ts @@ -179,6 +179,11 @@ export const mockedCompaniesData: Array = [ }, ]; +export const mockedDuplicateCompanyData: MockedCompany = { + ...mockedCompaniesData[0], + id: '8b40856a-2ec9-4c03-8bc0-c032c89e1824', +}; + export const mockedEmptyCompanyData = { id: '9231e6ee-4cc2-4c7b-8c55-dff16f4d968a', name: '',