Added Linaria for performance optimization (#5693)
- Added Linaria to have compiled CSS on our optimized field displays - Refactored mocks for performance stories on fields - Refactored generateRecordChipData into a global context, computed only when we fetch object metadata items. - Refactored ChipFieldDisplay - Refactored PhoneFieldDisplay
This commit is contained in:
@ -2,15 +2,20 @@ export type Company = {
|
||||
__typename: 'Company';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
updatedAt?: string;
|
||||
deletedAt?: string | null;
|
||||
name: string;
|
||||
domainName: string;
|
||||
address: string;
|
||||
accountOwnerId: string | null;
|
||||
linkedinLink: { url: string; label: string };
|
||||
xLink: { url: string; label: string };
|
||||
annualRecurringRevenue: { amountMicros: number | null; currencyCode: string };
|
||||
accountOwnerId?: string | null;
|
||||
position?: number;
|
||||
linkedinLink: { __typename?: 'Link'; url: string; label: string };
|
||||
xLink?: { __typename?: 'Link'; url: string; label: string };
|
||||
annualRecurringRevenue: {
|
||||
__typename?: 'Currency';
|
||||
amountMicros: number | null;
|
||||
currencyCode: string;
|
||||
};
|
||||
employees: number | null;
|
||||
idealCustomerProfile: boolean;
|
||||
idealCustomerProfile?: boolean;
|
||||
};
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||
import { getRecordChipGeneratorPerObjectPerField } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
|
||||
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
||||
|
||||
export const ObjectMetadataItemsProvider = ({
|
||||
@ -13,13 +15,23 @@ export const ObjectMetadataItemsProvider = ({
|
||||
|
||||
const shouldDisplayChildren = objectMetadataItems.length > 0;
|
||||
|
||||
const chipGeneratorPerObjectPerField = useMemo(() => {
|
||||
return getRecordChipGeneratorPerObjectPerField(objectMetadataItems);
|
||||
}, [objectMetadataItems]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ObjectMetadataItemsLoadEffect />
|
||||
{shouldDisplayChildren ? (
|
||||
<RelationPickerScope relationPickerScopeId="relation-picker">
|
||||
{children}
|
||||
</RelationPickerScope>
|
||||
<PreComputedChipGeneratorsContext.Provider
|
||||
value={{
|
||||
chipGeneratorPerObjectPerField,
|
||||
}}
|
||||
>
|
||||
<RelationPickerScope relationPickerScopeId="relation-picker">
|
||||
{children}
|
||||
</RelationPickerScope>
|
||||
</PreComputedChipGeneratorsContext.Provider>
|
||||
) : (
|
||||
<UserOrMetadataLoader />
|
||||
)}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export type ChipGeneratorPerObjectPerField = Record<
|
||||
string,
|
||||
Record<string, (record: ObjectRecord) => RecordChipData>
|
||||
>;
|
||||
|
||||
export type PreComputedChipGeneratorsContextProps = {
|
||||
chipGeneratorPerObjectPerField: ChipGeneratorPerObjectPerField;
|
||||
};
|
||||
|
||||
export const PreComputedChipGeneratorsContext =
|
||||
createContext<PreComputedChipGeneratorsContextProps>(
|
||||
{} as PreComputedChipGeneratorsContextProps,
|
||||
);
|
||||
@ -5,6 +5,7 @@ import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/displ
|
||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||
import { isFieldChipDisplay } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
||||
@ -42,11 +43,7 @@ import { isFieldUuid } from '../types/guards/isFieldUuid';
|
||||
export const FieldDisplay = () => {
|
||||
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
|
||||
|
||||
const isChipDisplay =
|
||||
isLabelIdentifier &&
|
||||
(isFieldText(fieldDefinition) ||
|
||||
isFieldFullName(fieldDefinition) ||
|
||||
isFieldNumber(fieldDefinition));
|
||||
const isChipDisplay = isFieldChipDisplay(fieldDefinition, isLabelIdentifier);
|
||||
|
||||
return isChipDisplay ? (
|
||||
<ChipFieldDisplay />
|
||||
|
||||
@ -1,12 +1,24 @@
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { useChipField } from '@/object-record/record-field/meta-types/hooks/useChipField';
|
||||
import { EntityChip } from 'twenty-ui';
|
||||
|
||||
import { useChipFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useChipFieldDisplay';
|
||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||
|
||||
export const ChipFieldDisplay = () => {
|
||||
const { objectNameSingular, record } = useChipField();
|
||||
const { recordValue, generateRecordChipData } = useChipFieldDisplay();
|
||||
|
||||
if (!record) return null;
|
||||
if (!recordValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recordChipData = generateRecordChipData(recordValue);
|
||||
|
||||
return (
|
||||
<RecordChip objectNameSingular={objectNameSingular || ''} record={record} />
|
||||
<EntityChip
|
||||
entityId={recordValue.id}
|
||||
name={recordChipData.name as any}
|
||||
avatarType={recordChipData.avatarType}
|
||||
avatarUrl={getImageAbsoluteURIOrBase64(recordChipData.avatarUrl) ?? ''}
|
||||
linkToEntity={recordChipData.linkToShowPage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { usePhoneFieldDisplay } from '@/object-record/record-field/meta-types/hooks/usePhoneFieldDisplay';
|
||||
import { PhoneDisplay } from '@/ui/field/display/components/PhoneDisplay';
|
||||
|
||||
import { usePhoneField } from '../../hooks/usePhoneField';
|
||||
|
||||
export const PhoneFieldDisplay = () => {
|
||||
const { fieldValue } = usePhoneField();
|
||||
const { fieldValue } = usePhoneFieldDisplay();
|
||||
|
||||
return <PhoneDisplay value={fieldValue} />;
|
||||
};
|
||||
|
||||
@ -23,8 +23,8 @@ export const RelationFromManyFieldDisplay = ({
|
||||
{recordChipsData.map((record) => {
|
||||
return (
|
||||
<EntityChip
|
||||
key={record.id}
|
||||
entityId={record.id}
|
||||
key={record.recordId}
|
||||
entityId={record.recordId}
|
||||
name={record.name as any}
|
||||
avatarType={record.avatarType}
|
||||
avatarUrl={getImageAbsoluteURIOrBase64(record.avatarUrl) || ''}
|
||||
|
||||
@ -1,67 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
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 { FieldMetadataType } from '~/generated/graphql';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
|
||||
const ChipFieldValueSetterEffect = () => {
|
||||
const setEntityFields = useSetRecoilState(recordStoreFamilyState('123'));
|
||||
|
||||
useEffect(() => {
|
||||
setEntityFields({
|
||||
id: 'henry',
|
||||
name: {
|
||||
firstName: 'Henry',
|
||||
lastName: 'Cavill',
|
||||
},
|
||||
__typename: 'Person',
|
||||
});
|
||||
}, [setEntityFields]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/ChipFieldDisplay',
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
(Story) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: '123',
|
||||
basePathToShowPage: '/object-record/',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'full name',
|
||||
label: 'Henry Cavill',
|
||||
type: FieldMetadataType.FullName,
|
||||
iconName: 'IconCalendarEvent',
|
||||
metadata: {
|
||||
fieldName: 'full name',
|
||||
objectMetadataNameSingular: 'person',
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
}}
|
||||
>
|
||||
<ChipFieldValueSetterEffect />
|
||||
<Story />
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: ChipFieldDisplay,
|
||||
argTypes: { value: { control: 'date' } },
|
||||
args: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ChipFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -1,68 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePhoneField } from '@/object-record/record-field/meta-types/hooks/usePhoneField';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
|
||||
import { PhoneFieldDisplay } from '../PhoneFieldDisplay';
|
||||
|
||||
const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => {
|
||||
const { setFieldValue } = usePhoneField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/PhoneFieldDisplay',
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
(Story, { args }) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: '',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'phone',
|
||||
label: 'Phone',
|
||||
type: FieldMetadataType.Phone,
|
||||
iconName: 'IconPhone',
|
||||
metadata: {
|
||||
fieldName: 'phone',
|
||||
placeHolder: 'Phone',
|
||||
objectMetadataNameSingular: 'person',
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
useUpdateRecord: () => [() => undefined, {}],
|
||||
}}
|
||||
>
|
||||
<PhoneFieldValueSetterEffect value={args.value} />
|
||||
<Story />
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: PhoneFieldDisplay,
|
||||
args: {
|
||||
value: '362763872687362',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof PhoneFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Elipsis: Story = {
|
||||
parameters: {
|
||||
container: { width: 50 },
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { ChipFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ChipFieldDisplay';
|
||||
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/ChipFieldDisplay',
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
ChipGeneratorsDecorator,
|
||||
getFieldDecorator('person', 'name'),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: ChipFieldDisplay,
|
||||
args: {},
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ChipFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'ChipFieldDisplay',
|
||||
averageThresholdInMs: 0.2,
|
||||
numberOfRuns: 20,
|
||||
numberOfTestsPerRun: 100,
|
||||
});
|
||||
@ -0,0 +1,47 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { PhoneFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhoneFieldDisplay';
|
||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/PhoneFieldDisplay',
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
getFieldDecorator('person', 'phone'),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: PhoneFieldDisplay,
|
||||
args: {},
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof PhoneFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Elipsis: Story = {
|
||||
parameters: {
|
||||
container: { width: 50 },
|
||||
},
|
||||
};
|
||||
|
||||
export const WrongNumber: Story = {
|
||||
parameters: {
|
||||
container: { width: 50 },
|
||||
},
|
||||
decorators: [getFieldDecorator('person', 'phone', 'sdklaskdj')],
|
||||
};
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'PhoneFieldDisplay',
|
||||
averageThresholdInMs: 0.5,
|
||||
numberOfRuns: 20,
|
||||
numberOfTestsPerRun: 100,
|
||||
});
|
||||
@ -1,74 +1,21 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RelationFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFieldDisplay';
|
||||
import {
|
||||
RecordFieldValueSelectorContextProvider,
|
||||
useSetRecordValue,
|
||||
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
import { relationFieldDisplayMock } from './mock';
|
||||
|
||||
const RelationFieldValueSetterEffect = () => {
|
||||
const setEntity = useSetRecoilState(
|
||||
recordStoreFamilyState(relationFieldDisplayMock.entityId),
|
||||
);
|
||||
|
||||
const setRelationEntity = useSetRecoilState(
|
||||
recordStoreFamilyState(relationFieldDisplayMock.relationEntityId),
|
||||
);
|
||||
|
||||
const setRecordValue = useSetRecordValue();
|
||||
|
||||
useEffect(() => {
|
||||
setEntity(relationFieldDisplayMock.entityValue);
|
||||
setRelationEntity(relationFieldDisplayMock.relationFieldValue);
|
||||
|
||||
setRecordValue(
|
||||
relationFieldDisplayMock.entityValue.id,
|
||||
relationFieldDisplayMock.entityValue,
|
||||
);
|
||||
setRecordValue(
|
||||
relationFieldDisplayMock.relationFieldValue.id,
|
||||
relationFieldDisplayMock.relationFieldValue,
|
||||
);
|
||||
}, [setEntity, setRelationEntity, setRecordValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/RelationFieldDisplay',
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
(Story) => (
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: relationFieldDisplayMock.entityId,
|
||||
basePathToShowPage: '/object-record/',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
...relationFieldDisplayMock.fieldDefinition,
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
}}
|
||||
>
|
||||
<RelationFieldValueSetterEffect />
|
||||
<Story />
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
),
|
||||
ChipGeneratorsDecorator,
|
||||
getFieldDecorator('person', 'company'),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: RelationFieldDisplay,
|
||||
argTypes: { value: { control: 'date' } },
|
||||
args: {},
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
@ -83,7 +30,7 @@ export const Default: Story = {};
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'RelationFieldDisplay',
|
||||
averageThresholdInMs: 0.4,
|
||||
averageThresholdInMs: 0.2,
|
||||
numberOfRuns: 20,
|
||||
numberOfTestsPerRun: 100,
|
||||
});
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
useSetRecordValue,
|
||||
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
@ -52,6 +53,7 @@ const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/RelationFromManyFieldDisplay',
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
ChipGeneratorsDecorator,
|
||||
(Story) => (
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<FieldContext.Provider
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const relationFieldDisplayMock = {
|
||||
entityId: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||
relationEntityId: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
entityValue: {
|
||||
__typename: 'Person',
|
||||
asd: '',
|
||||
city: 'Seattle',
|
||||
jobTitle: '',
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'Lorie',
|
||||
lastName: 'Vladim',
|
||||
},
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
company: {
|
||||
__typename: 'Company',
|
||||
domainName: 'google.com',
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
name: 'Google',
|
||||
annualRecurringRevenue: {
|
||||
__typename: 'Currency',
|
||||
amountMicros: null,
|
||||
currencyCode: '',
|
||||
},
|
||||
employees: null,
|
||||
accountOwnerId: null,
|
||||
address: '',
|
||||
idealCustomerProfile: false,
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
id: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
position: 6,
|
||||
updatedAt: '2024-05-01T13:16:29.046Z',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
id: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||
email: 'lorie.vladim@google.com',
|
||||
phone: '+33788901235',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
tEst: '',
|
||||
position: 15,
|
||||
},
|
||||
relationFieldValue: {
|
||||
__typename: 'Company',
|
||||
domainName: 'microsoft.com',
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
name: 'Microsoft',
|
||||
annualRecurringRevenue: {
|
||||
__typename: 'Currency',
|
||||
amountMicros: null,
|
||||
currencyCode: '',
|
||||
},
|
||||
employees: null,
|
||||
accountOwnerId: null,
|
||||
address: '',
|
||||
idealCustomerProfile: false,
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
id: '20202020-ed89-413a-b31a-962986e67bb4',
|
||||
position: 4,
|
||||
updatedAt: '2024-05-01T13:16:29.046Z',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: '4e79f0b7-d100-4e89-a07b-315a710b8059',
|
||||
label: 'Company',
|
||||
metadata: {
|
||||
fieldName: 'company',
|
||||
placeHolder: 'Company',
|
||||
relationType: 'TO_ONE_OBJECT',
|
||||
relationFieldMetadataId: '01fa2247-7937-4493-b7e2-3d72f05d6d25',
|
||||
relationObjectMetadataNameSingular: 'company',
|
||||
relationObjectMetadataNamePlural: 'companies',
|
||||
objectMetadataNameSingular: 'person',
|
||||
options: null,
|
||||
},
|
||||
iconName: 'IconBuildingSkyscraper',
|
||||
type: FieldMetadataType.Relation,
|
||||
position: 2,
|
||||
size: 150,
|
||||
isLabelIdentifier: false,
|
||||
isVisible: true,
|
||||
viewFieldId: '924f4c94-cbcd-4de5-b7a2-ebae2f0b2c3b',
|
||||
isSortable: false,
|
||||
isFilterable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import { useContext } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||
import { useRecordValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
|
||||
export const useChipFieldDisplay = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const { chipGeneratorPerObjectPerField } = useContext(
|
||||
PreComputedChipGeneratorsContext,
|
||||
);
|
||||
|
||||
if (!isDefined(chipGeneratorPerObjectPerField)) {
|
||||
throw new Error('Chip generator per object per field is not defined');
|
||||
}
|
||||
|
||||
const objectNameSingular =
|
||||
isFieldText(fieldDefinition) ||
|
||||
isFieldFullName(fieldDefinition) ||
|
||||
isFieldNumber(fieldDefinition)
|
||||
? fieldDefinition.metadata.objectMetadataNameSingular
|
||||
: undefined;
|
||||
|
||||
const recordValue = useRecordValue(entityId);
|
||||
|
||||
if (!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)) {
|
||||
throw new Error('Object metadata name singular is not a non-empty string');
|
||||
}
|
||||
|
||||
const generateRecordChipData =
|
||||
chipGeneratorPerObjectPerField[
|
||||
fieldDefinition.metadata.objectMetadataNameSingular
|
||||
][fieldDefinition.metadata.fieldName];
|
||||
|
||||
return {
|
||||
objectNameSingular,
|
||||
recordValue,
|
||||
generateRecordChipData,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
|
||||
export const usePhoneFieldDisplay = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const fieldValue = useRecordFieldValue(entityId, fieldName);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -1,9 +1,8 @@
|
||||
import { useContext } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -15,6 +14,14 @@ import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||
export const useRelationFieldDisplay = () => {
|
||||
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||
|
||||
const { chipGeneratorPerObjectPerField } = useContext(
|
||||
PreComputedChipGeneratorsContext,
|
||||
);
|
||||
|
||||
if (!isDefined(chipGeneratorPerObjectPerField)) {
|
||||
throw new Error('Chip generator per object per field is not defined');
|
||||
}
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.Relation,
|
||||
isFieldRelation,
|
||||
@ -32,18 +39,14 @@ export const useRelationFieldDisplay = () => {
|
||||
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
|
||||
: maxWidth;
|
||||
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
if (!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)) {
|
||||
throw new Error('Object metadata name singular is not a non-empty string');
|
||||
}
|
||||
|
||||
const generateRecordChipData = (record: ObjectRecord) => {
|
||||
return getObjectRecordIdentifier({
|
||||
objectMetadataItem: relationObjectMetadataItem,
|
||||
record,
|
||||
});
|
||||
};
|
||||
const generateRecordChipData =
|
||||
chipGeneratorPerObjectPerField[
|
||||
fieldDefinition.metadata.objectMetadataNameSingular
|
||||
][fieldDefinition.metadata.fieldName];
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { AvatarType } from 'twenty-ui';
|
||||
|
||||
export type RecordChipData = {
|
||||
recordId: string;
|
||||
name: string | number;
|
||||
avatarType: AvatarType;
|
||||
avatarUrl: string;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export type RecordFieldValue = {
|
||||
[recordId: string]: {
|
||||
[fieldName: string]: any;
|
||||
@ -31,7 +33,7 @@ export const useRecordValue = (recordId: string) => {
|
||||
(value) => value[0],
|
||||
);
|
||||
|
||||
return tableValue?.[recordId];
|
||||
return tableValue?.[recordId] as ObjectRecord | undefined;
|
||||
};
|
||||
|
||||
export const useRecordFieldValue = (recordId: string, fieldName: string) => {
|
||||
|
||||
@ -17,6 +17,7 @@ import { RecordTableCellContext } from '@/object-record/record-table/contexts/Re
|
||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
|
||||
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
@ -57,6 +58,7 @@ const meta: Meta = {
|
||||
title: 'RecordIndex/Table/RecordTableCell',
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
ChipGeneratorsDecorator,
|
||||
(Story) => {
|
||||
return (
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
@ -143,7 +145,7 @@ export const Default: Story = {};
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'RecordTableCell',
|
||||
averageThresholdInMs: 0.6,
|
||||
averageThresholdInMs: 0.3,
|
||||
numberOfRuns: 50,
|
||||
numberOfTestsPerRun: 200,
|
||||
warmUpRounds: 20,
|
||||
|
||||
@ -1,86 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
|
||||
import { getAvatarUrl } from '@/object-metadata/utils/getAvatarUrl';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { getLabelIdentifierFieldValue } from '@/object-metadata/utils/getLabelIdentifierFieldValue';
|
||||
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useRecordChipDataGenerator = ({
|
||||
objectNameSingular,
|
||||
visibleTableColumns,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
visibleTableColumns: ColumnDefinition<FieldMetadata>[];
|
||||
}) => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
return useMemo(() => {
|
||||
return Object.fromEntries<(record: ObjectRecord) => RecordChipData>(
|
||||
visibleTableColumns
|
||||
.filter(
|
||||
(tableColumn) =>
|
||||
tableColumn.isLabelIdentifier ||
|
||||
tableColumn.type === FieldMetadataType.Relation,
|
||||
)
|
||||
.map((tableColumn) => {
|
||||
const objectNameSingularToFind = tableColumn.isLabelIdentifier
|
||||
? objectNameSingular
|
||||
: isFieldRelation(tableColumn)
|
||||
? tableColumn.metadata.relationObjectMetadataNameSingular
|
||||
: undefined;
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === objectNameSingularToFind,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(objectMetadataItem) ||
|
||||
!isDefined(objectNameSingularToFind)
|
||||
) {
|
||||
return ['', () => ({}) as any];
|
||||
}
|
||||
|
||||
const labelIdentifierFieldMetadataItem =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
|
||||
const imageIdentifierFieldMetadata = objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.id === objectMetadataItem.imageIdentifierFieldMetadataId,
|
||||
);
|
||||
|
||||
const avatarType = getAvatarType(objectNameSingularToFind);
|
||||
|
||||
return [
|
||||
tableColumn.metadata.fieldName,
|
||||
(record: ObjectRecord) => ({
|
||||
name: getLabelIdentifierFieldValue(
|
||||
record,
|
||||
labelIdentifierFieldMetadataItem,
|
||||
objectMetadataItem.nameSingular,
|
||||
),
|
||||
avatarUrl: getAvatarUrl(
|
||||
objectMetadataItem.nameSingular,
|
||||
record,
|
||||
imageIdentifierFieldMetadata,
|
||||
),
|
||||
avatarType,
|
||||
linkToShowPage: getLinkToShowPage(
|
||||
objectMetadataItem.nameSingular,
|
||||
record,
|
||||
),
|
||||
}),
|
||||
];
|
||||
}),
|
||||
);
|
||||
}, [objectNameSingular, visibleTableColumns, objectMetadataItems]);
|
||||
};
|
||||
@ -0,0 +1,116 @@
|
||||
import { ChipGeneratorPerObjectPerField } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
|
||||
import { getAvatarUrl } from '@/object-metadata/utils/getAvatarUrl';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { getLabelIdentifierFieldValue } from '@/object-metadata/utils/getLabelIdentifierFieldValue';
|
||||
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const isFieldChipDisplay = (
|
||||
field: Pick<FieldMetadataItem, 'type'>,
|
||||
isLabelIdentifier: boolean,
|
||||
) =>
|
||||
isLabelIdentifier &&
|
||||
(isFieldText(field) || isFieldFullName(field) || isFieldNumber(field));
|
||||
|
||||
export const getRecordChipGeneratorPerObjectPerField = (
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
) => {
|
||||
const recordChipGeneratorPerObjectPerField: ChipGeneratorPerObjectPerField =
|
||||
{};
|
||||
|
||||
for (const objectMetadataItem of objectMetadataItems) {
|
||||
const generatorPerField = Object.fromEntries<
|
||||
(record: ObjectRecord) => RecordChipData
|
||||
>(
|
||||
objectMetadataItem.fields
|
||||
.filter(
|
||||
(fieldMetadataItem) =>
|
||||
isLabelIdentifierField({
|
||||
fieldMetadataItem: fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
}) ||
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation ||
|
||||
isFieldChipDisplay(
|
||||
fieldMetadataItem,
|
||||
isLabelIdentifierField({
|
||||
fieldMetadataItem: fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.map((fieldMetadataItem) => {
|
||||
const objectNameSingularToFind = isLabelIdentifierField({
|
||||
fieldMetadataItem: fieldMetadataItem,
|
||||
objectMetadataItem: objectMetadataItem,
|
||||
})
|
||||
? objectMetadataItem.nameSingular
|
||||
: isFieldRelation(fieldMetadataItem)
|
||||
? fieldMetadataItem.relationDefinition?.targetObjectMetadata
|
||||
.nameSingular ?? undefined
|
||||
: undefined;
|
||||
|
||||
const objectMetadataItemToUse = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === objectNameSingularToFind,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(objectMetadataItemToUse) ||
|
||||
!isDefined(objectNameSingularToFind)
|
||||
) {
|
||||
return ['', () => ({}) as any];
|
||||
}
|
||||
|
||||
const labelIdentifierFieldMetadataItem =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItemToUse);
|
||||
|
||||
const imageIdentifierFieldMetadata =
|
||||
objectMetadataItemToUse.fields.find(
|
||||
(field) =>
|
||||
field.id ===
|
||||
objectMetadataItemToUse.imageIdentifierFieldMetadataId,
|
||||
);
|
||||
|
||||
const avatarType = getAvatarType(objectNameSingularToFind);
|
||||
|
||||
return [
|
||||
fieldMetadataItem.name,
|
||||
(record: ObjectRecord) => ({
|
||||
recordId: record.id,
|
||||
name: getLabelIdentifierFieldValue(
|
||||
record,
|
||||
labelIdentifierFieldMetadataItem,
|
||||
objectMetadataItemToUse.nameSingular,
|
||||
),
|
||||
avatarUrl: getAvatarUrl(
|
||||
objectMetadataItemToUse.nameSingular,
|
||||
record,
|
||||
imageIdentifierFieldMetadata,
|
||||
),
|
||||
avatarType,
|
||||
linkToShowPage: getLinkToShowPage(
|
||||
objectMetadataItemToUse.nameSingular,
|
||||
record,
|
||||
),
|
||||
}),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
recordChipGeneratorPerObjectPerField[objectMetadataItem.nameSingular] =
|
||||
generatorPerField;
|
||||
}
|
||||
|
||||
return recordChipGeneratorPerObjectPerField;
|
||||
};
|
||||
@ -2,24 +2,39 @@ export type Person = {
|
||||
__typename: 'Person';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
updatedAt?: string;
|
||||
deletedAt?: string | null;
|
||||
name: {
|
||||
__typename?: 'FullName';
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
avatarUrl: string;
|
||||
avatarUrl?: string;
|
||||
jobTitle: string;
|
||||
linkedinLink: {
|
||||
__typename?: 'Link';
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
xLink: {
|
||||
__typename?: 'Link';
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
city: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
companyId: string;
|
||||
companyId?: string;
|
||||
position?: number;
|
||||
links?: {
|
||||
__typename: 'Links';
|
||||
primaryLinkUrl: string;
|
||||
primaryLinkLabel: '';
|
||||
secondaryLinks?:
|
||||
| {
|
||||
url: string;
|
||||
label: string;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledEllipsisDisplay = styled.div<{ maxWidth?: number }>`
|
||||
max-width: ${({ maxWidth }) => maxWidth ?? '100%'};
|
||||
@ -19,7 +19,7 @@ export const EllipsisDisplay = ({
|
||||
maxWidth,
|
||||
className,
|
||||
}: EllipsisDisplayProps) => (
|
||||
<StyledEllipsisDisplay style={{ maxWidth }} className={className}>
|
||||
<StyledEllipsisDisplay maxWidth={maxWidth} className={className}>
|
||||
{children}
|
||||
</StyledEllipsisDisplay>
|
||||
);
|
||||
|
||||
@ -1,27 +1,39 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
|
||||
import { parsePhoneNumber, PhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
import { ContactLink } from '@/ui/navigation/link/components/ContactLink';
|
||||
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type PhoneDisplayProps = {
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
export const PhoneDisplay = ({ value }: PhoneDisplayProps) => (
|
||||
<EllipsisDisplay>
|
||||
{value && isValidPhoneNumber(value) ? (
|
||||
<ContactLink
|
||||
href={parsePhoneNumber(value, 'FR')?.getURI()}
|
||||
onClick={(event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{parsePhoneNumber(value, 'FR')?.formatNational() || value}
|
||||
</ContactLink>
|
||||
) : (
|
||||
<ContactLink href="#">{value}</ContactLink>
|
||||
)}
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
// TODO: see if we can find a faster way to format the phone number
|
||||
export const PhoneDisplay = ({ value }: PhoneDisplayProps) => {
|
||||
if (!isDefined(value)) {
|
||||
return <ContactLink href="#">{value}</ContactLink>;
|
||||
}
|
||||
|
||||
let parsedPhoneNumber: PhoneNumber | null = null;
|
||||
|
||||
try {
|
||||
// TODO: parse according to locale not hard coded FR
|
||||
parsedPhoneNumber = parsePhoneNumber(value, 'FR');
|
||||
} catch (error) {
|
||||
return <ContactLink href="#">{value}</ContactLink>;
|
||||
}
|
||||
|
||||
const URI = parsedPhoneNumber.getURI();
|
||||
const formattedNational = parsedPhoneNumber?.formatNational();
|
||||
|
||||
return (
|
||||
<ContactLink
|
||||
href={URI}
|
||||
onClick={(event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{formattedNational || value}
|
||||
</ContactLink>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Input/EllipsisDisplay/EllipsisDisplay',
|
||||
component: EllipsisDisplay,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
maxWidth: 100,
|
||||
children: 'This is a long text that should be truncated',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof EllipsisDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -1,4 +1,4 @@
|
||||
import { Meta } from '@storybook/react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||
@ -19,6 +19,10 @@ const meta: Meta = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof EllipsisDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'EllipsisDisplay',
|
||||
averageThresholdInMs: 0.1,
|
||||
|
||||
@ -1,43 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import { Link as ReactLink } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { Theme, withTheme } from '@emotion/react';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledClickableLink = withTheme(styled.a<{
|
||||
theme: Theme;
|
||||
maxWidth?: number;
|
||||
}>`
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: ${({ theme }) => theme.border.color.strong};
|
||||
text-overflow: ellipsis;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
|
||||
max-width: ${({ maxWidth }) => maxWidth ?? '100%'};
|
||||
|
||||
&:hover {
|
||||
text-decoration-color: ${({ theme }) => theme.font.color.primary};
|
||||
}
|
||||
`);
|
||||
|
||||
type ContactLinkProps = {
|
||||
className?: string;
|
||||
href: string;
|
||||
children?: React.ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
maxWidth?: number;
|
||||
};
|
||||
|
||||
const StyledClickable = styled.div`
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: ${({ theme }) => theme.border.color.strong};
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
text-decoration-color: ${({ theme }) => theme.font.color.primary};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ContactLink = ({
|
||||
className,
|
||||
href,
|
||||
children,
|
||||
onClick,
|
||||
maxWidth,
|
||||
}: ContactLinkProps) => (
|
||||
<div>
|
||||
<StyledClickable className={className}>
|
||||
<ReactLink target="_blank" onClick={onClick} to={href}>
|
||||
{children}
|
||||
</ReactLink>
|
||||
</StyledClickable>
|
||||
</div>
|
||||
<StyledClickableLink
|
||||
maxWidth={maxWidth}
|
||||
target="_blank"
|
||||
onClick={onClick}
|
||||
href={href}
|
||||
>
|
||||
{children}
|
||||
</StyledClickableLink>
|
||||
);
|
||||
|
||||
@ -10,7 +10,6 @@ const meta: Meta<typeof ContactLink> = {
|
||||
component: ContactLink,
|
||||
decorators: [ComponentWithRouterDecorator],
|
||||
args: {
|
||||
className: 'ContactLink',
|
||||
href: '/test',
|
||||
children: 'Contact Link',
|
||||
},
|
||||
|
||||
@ -4,6 +4,7 @@ export type WorkspaceMember = {
|
||||
__typename: 'WorkspaceMember';
|
||||
id: string;
|
||||
name: {
|
||||
__typename?: 'FullName';
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user