feat: apply RecordDetailSection style on RecordDuplicatesSection and … (#4241)

feat: apply RecordDetailSection style on RecordDuplicatesSection and add stories

Closes #3963, Closes #4240
This commit is contained in:
Thaïs 2024-02-29 10:10:07 -03:00 committed by GitHub
parent 68a8502920
commit 6ad3880696
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 395 additions and 240 deletions

View File

@ -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<typeof App> = {
title: 'App/App',
component: App,
decorators: [
MemoryRouterDecorator,
(Story) => (
<ClientConfigProvider>
<UserProvider>
<MemoryRouter>
<FullHeightStorybookLayout>
<HelmetProvider>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<ObjectMetadataItemsProvider>
<Story />
</ObjectMetadataItemsProvider>
</SnackBarProviderScope>
</HelmetProvider>
</FullHeightStorybookLayout>
</MemoryRouter>
<FullHeightStorybookLayout>
<HelmetProvider>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<ObjectMetadataItemsProvider>
<Story />
</ObjectMetadataItemsProvider>
</SnackBarProviderScope>
</HelmetProvider>
</FullHeightStorybookLayout>
</UserProvider>
</ClientConfigProvider>
),

View File

@ -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<typeof TaskList> = {
title: 'Modules/Activity/TaskList',
component: TaskList,
decorators: [
(Story) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
ComponentDecorator,
SnackBarDecorator,
],
decorators: [MemoryRouterDecorator, ComponentDecorator, SnackBarDecorator],
args: {
title: 'Tasks',
tasks: mockedActivities,

View File

@ -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 = <T extends ObjectRecord = ObjectRecord>({
const { enqueueSnackBar } = useSnackBar();
const queryResponseField = getFindDuplicateRecordsQueryResponseField(
objectMetadataItem.nameSingular,
);
const { data, loading, error } = useQuery<ObjectRecordQueryResult<T>>(
findDuplicateRecordsQuery,
{
@ -36,7 +41,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
id: objectRecordId,
},
onCompleted: (data) => {
onCompleted?.(data[objectMetadataItem.nameSingular]);
onCompleted?.(data[queryResponseField]);
},
onError: (error) => {
logError(
@ -53,8 +58,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
},
);
const objectRecordConnection =
data?.[`${objectMetadataItem.nameSingular}Duplicates`];
const objectRecordConnection = data?.[queryResponseField];
const mapConnectionToRecords = useMapConnectionToRecords();

View File

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

View File

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

View File

@ -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) => (
<MemoryRouter>
<FieldContext.Provider
value={{
entityId: '123',
basePathToShowPage: '/object-record/',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'full name',
label: 'Henry Cavill',
type: 'FULL_NAME',
iconName: 'IconCalendarEvent',
metadata: {
fieldName: 'full name',
objectMetadataNameSingular: 'person',
},
<FieldContext.Provider
value={{
entityId: '123',
basePathToShowPage: '/object-record/',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'full name',
label: 'Henry Cavill',
type: 'FULL_NAME',
iconName: 'IconCalendarEvent',
metadata: {
fieldName: 'full name',
objectMetadataNameSingular: 'person',
},
hotkeyScope: 'hotkey-scope',
}}
>
<ChipFieldValueSetterEffect />
<Story />
</FieldContext.Provider>
</MemoryRouter>
},
hotkeyScope: 'hotkey-scope',
}}
>
<ChipFieldValueSetterEffect />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],

View File

@ -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 }) => (
<FieldContext.Provider
value={{
@ -39,10 +40,8 @@ const meta: Meta = {
hotkeyScope: 'hotkey-scope',
}}
>
<MemoryRouter>
<EmailFieldValueSetterEffect value={args.value} />
<Story />
</MemoryRouter>
<EmailFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,

View File

@ -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 }) => (
<FieldContext.Provider
value={{
@ -41,10 +42,8 @@ const meta: Meta = {
useUpdateRecord: () => [() => undefined, {}],
}}
>
<MemoryRouter>
<PhoneFieldValueSetterEffect value={args.value} />
<Story />
</MemoryRouter>
<PhoneFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,

View File

@ -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 = ({
</FieldContext.Provider>
))}
</PropertyBox>
<RecordDuplicatesFieldCardSection
<RecordDetailDuplicatesSection
objectRecordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
@ -229,7 +229,7 @@ export const RecordShowContainer = ({
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordRelationFieldCardSection />
<RecordDetailRelationSection />
</FieldContext.Provider>
))}
</>

View File

@ -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 (
<RecordDetailSection>
<RecordDetailSectionHeader title="Duplicates" />
<RecordDetailRecordsList>
{duplicateRecords.slice(0, 5).map((duplicateRecord) => (
<RecordDetailRecordsListItem key={duplicateRecord.id}>
<RecordChip
record={duplicateRecord}
objectNameSingular={objectNameSingular}
/>
</RecordDetailRecordsListItem>
))}
</RecordDetailRecordsList>
</RecordDetailSection>
);
};

View File

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

View File

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

View File

@ -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) => (
<RecordDetailRecordsList>
{relationRecords.slice(0, 5).map((relationRecord) => (
<RecordDetailRelationRecordsListItem
key={relationRecord.id}
relationRecord={relationRecord}
/>
))}
</RecordDetailRecordsList>
);

View File

@ -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 (
<StyledRelationRecordsListEmptyState>
<Icon size={theme.icon.size.sm} />
<div>No {relationObjectMetadataItem.labelSingular}</div>
</StyledRelationRecordsListEmptyState>
);
};

View File

@ -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 (
<StyledCardContent isDropdownOpen={isDropdownOpen} divider={divider}>
<StyledListItem isDropdownOpen={isDropdownOpen}>
<RecordChip
record={relationRecord}
objectNameSingular={relationObjectMetadataItem.nameSingular}
@ -165,6 +154,6 @@ export const RecordRelationFieldCardContent = ({
/>
</DropdownScope>
)}
</StyledCardContent>
</StyledListItem>
);
};

View File

@ -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 (
<StyledSection>
<RecordDetailSection>
<RecordDetailSectionHeader
title={fieldDefinition.label}
link={
@ -186,23 +157,13 @@ export const RecordRelationFieldCardSection = () => {
</DropdownScope>
}
/>
{relationRecords.length === 0 && (
<StyledCardNoContent>
<Icon size={theme.icon.size.sm} />
<div>No {relationObjectMetadataItem.labelSingular}</div>
</StyledCardNoContent>
{relationRecords.length ? (
<RecordDetailRelationRecordsList relationRecords={relationRecords} />
) : (
<RecordDetailRelationRecordsListEmptyState
relationObjectMetadataItem={relationObjectMetadataItem}
/>
)}
{!!relationRecords.length && (
<StyledCard>
{relationRecords.slice(0, 5).map((relationRecord, index) => (
<RecordRelationFieldCardContent
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
divider={index < relationRecords.length - 1}
relationRecord={relationRecord}
/>
))}
</StyledCard>
)}
</StyledSection>
</RecordDetailSection>
);
};

View File

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

View File

@ -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 (
<Section>
<RecordDetailSectionHeader title="Duplicates" />
<Card>
{duplicateRecords.slice(0, 5).map((duplicateRecord, index) => (
<StyledCardContent
key={`${objectNameSingular}${duplicateRecord.id}`}
divider={index < duplicateRecords.length - 1}
>
<RecordChip
record={duplicateRecord}
objectNameSingular={objectNameSingular}
/>
</StyledCardContent>
))}
</Card>
</Section>
);
};

View File

@ -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<typeof RecordDetailDuplicatesSection> = {
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<typeof RecordDetailDuplicatesSection>;
export const Default: Story = {};

View File

@ -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<typeof RecordDetailRelationSection> = {
title:
'Modules/ObjectRecord/RecordShow/RecordDetailSection/RecordDetailRelationSection',
component: RecordDetailRelationSection,
decorators: [
(Story) => (
<FieldContext.Provider
value={{
entityId: mockedCompaniesData[0].id,
basePathToShowPage: '/object-record/',
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
field: mockedCompanyObjectMetadataItem.fields.find(
({ name }) => name === 'people',
)!,
objectMetadataItem: mockedCompanyObjectMetadataItem,
}),
hotkeyScope: 'hotkey-scope',
}}
>
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
MemoryRouterDecorator,
],
parameters: {
msw: graphqlMocks,
records: mockedCompaniesData,
},
};
export default meta;
type Story = StoryObj<typeof RecordDetailRelationSection>;
export const EmptyState: Story = {};
export const WithRecords: Story = {
decorators: [RecordStoreDecorator],
parameters: {
records: [
{
...mockedCompaniesData[0],
people: { edges: mockedPeopleData.map((person) => ({ node: person })) },
},
...mockedPeopleData,
],
},
};

View File

@ -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) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
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) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
decorators: [MemoryRouterDecorator],
args: {
fieldMetadata: mockedPeopleMetadata.node.fields.edges.find(
({ node }) => node.type === FieldMetadataType.Relation,

View File

@ -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) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
decorators: [MemoryRouterDecorator],
args: {
fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find(
({ node }) => node.type === FieldMetadataType.Relation,

View File

@ -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<typeof ActionBar> = {
title: 'UI/Navigation/ActionBar/ActionBar',
component: FilledActionBar,
decorators: [
MemoryRouterDecorator,
(Story) => (
<RecordTableScope
recordTableScopeId="companies"
onColumnsChange={() => {}}
>
<MemoryRouter>
<Story />
</MemoryRouter>
<Story />
</RecordTableScope>
),
ComponentDecorator,

View File

@ -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<typeof ContextMenu> = {
title: 'UI/Navigation/ContextMenu/ContextMenu',
component: FilledContextMenu,
decorators: [
MemoryRouterDecorator,
(Story) => (
<RecordTableScope
recordTableScopeId="companies"
onColumnsChange={() => {}}
>
<MemoryRouter>
<Story />
</MemoryRouter>
<Story />
</RecordTableScope>
),
ComponentDecorator,

View File

@ -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<Story, typeof NavigationDrawerItem> = {
</StyledContainer>
),
CatalogDecorator,
(Story) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
MemoryRouterDecorator,
],
parameters: {
pseudo: { hover: ['.hover'] },

View File

@ -0,0 +1,8 @@
import { MemoryRouter } from 'react-router-dom';
import { Decorator } from '@storybook/react';
export const MemoryRouterDecorator: Decorator = (Story) => (
<MemoryRouter>
<Story />
</MemoryRouter>
);

View File

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

View File

@ -179,6 +179,11 @@ export const mockedCompaniesData: Array<MockedCompany> = [
},
];
export const mockedDuplicateCompanyData: MockedCompany = {
...mockedCompaniesData[0],
id: '8b40856a-2ec9-4c03-8bc0-c032c89e1824',
};
export const mockedEmptyCompanyData = {
id: '9231e6ee-4cc2-4c7b-8c55-dff16f4d968a',
name: '',