Refactored all FieldDisplay types for performance optimization (#5768)

This PR is the second part of
https://github.com/twentyhq/twenty/pull/5693.

It optimizes all remaining field types.

The observed improvements are :
- x2 loading time improvement on table rows
- more consistent render time

Here's a summary of measured improvements, what's given here is the
average of hundreds of renders with a React Profiler component. (in our
Storybook performance stories)

| Component | Before (µs) | After (µs) |
| ----- | ------------- | --- |
| TextFieldDisplay | 127 | 83 |
| EmailFieldDisplay | 117 | 83 |
| NumberFieldDisplay | 97 | 56 |
| DateFieldDisplay | 240 | 52 |
| CurrencyFieldDisplay | 236 | 110 |
| FullNameFieldDisplay | 131 | 85 |
| AddressFieldDisplay | 118 | 81 |
| BooleanFieldDisplay | 130 | 100 |
| JSONFieldDisplay | 248 | 49 |
| LinksFieldDisplay | 1180 | 140 |
| LinkFieldDisplay | 140 | 78 |
| MultiSelectFieldDisplay | 770 | 130 |
| SelectFieldDisplay | 230 | 87 |
This commit is contained in:
Lucas Bordeau 2024-06-12 18:36:25 +02:00 committed by GitHub
parent 007e0e8b0e
commit 03b3c8a67a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 17167 additions and 15795 deletions

View File

@ -131,6 +131,7 @@
"lodash.upperfirst": "^4.3.1",
"luxon": "^3.3.0",
"microdiff": "^1.3.2",
"moize": "^6.1.6",
"nest-commander": "^3.12.0",
"next": "14.0.4",
"next-mdx-remote": "^4.4.1",

View File

@ -3,7 +3,7 @@ import { ThemeProvider } from '@emotion/react';
import { Preview } from '@storybook/react';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { useDarkMode } from 'storybook-dark-mode';
import { THEME_DARK, THEME_LIGHT } from 'twenty-ui';
import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui';
import { RootDecorator } from '../src/testing/decorators/RootDecorator';
import { mockedUserJWT } from '../src/testing/mock-data/jwt';
@ -39,7 +39,9 @@ const preview: Preview = {
return (
<ThemeProvider theme={theme}>
<Story />
<ThemeContextProvider theme={theme}>
<Story />
</ThemeContextProvider>
</ThemeProvider>
);
},

View File

@ -9,7 +9,7 @@ import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql';
import { formatToHumanReadableDate } from '~/utils';
import { formatToHumanReadableDate } from '~/utils/date-utils';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
const StyledCardContent = styled(CardContent)<{

View File

@ -14,7 +14,7 @@ import {
GenericFieldContextType,
} from '@/object-record/record-field/contexts/FieldContext';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { formatToHumanReadableDate } from '~/utils';
import { formatToHumanReadableDate } from '~/utils/date-utils';
const StyledRow = styled.div`
align-items: center;

View File

@ -13,6 +13,8 @@ import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWith
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { getPeopleMock } from '~/testing/mock-data/people';
import {
mockDefaultWorkspace,
mockedWorkspaceMemberData,
@ -21,6 +23,9 @@ import { sleep } from '~/testing/sleep';
import { CommandMenu } from '../CommandMenu';
const peopleMock = getPeopleMock();
const companiesMock = getCompaniesMock();
const openTimeout = 50;
const meta: Meta<typeof CommandMenu> = {
@ -94,8 +99,12 @@ export const MatchingPersonCompanyActivityCreateNavigate: Story = {
const searchInput = await canvas.findByPlaceholderText('Search');
await sleep(openTimeout);
await userEvent.type(searchInput, 'n');
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
expect(await canvas.findByText('Airbnb')).toBeInTheDocument();
expect(
await canvas.findByText(
peopleMock[0].name.firstName + ' ' + peopleMock[0].name.lastName,
),
).toBeInTheDocument();
expect(await canvas.findByText(companiesMock[0].name)).toBeInTheDocument();
expect(await canvas.findByText('My very first note')).toBeInTheDocument();
expect(await canvas.findByText('Create Note')).toBeInTheDocument();
expect(await canvas.findByText('Go to Companies')).toBeInTheDocument();
@ -119,7 +128,11 @@ export const AtleastMatchingOnePerson: Story = {
const searchInput = await canvas.findByPlaceholderText('Search');
await sleep(openTimeout);
await userEvent.type(searchInput, 'alex');
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
expect(
await canvas.findByText(
peopleMock[0].name.firstName + ' ' + peopleMock[0].name.lastName,
),
).toBeInTheDocument();
},
};

View File

@ -3,10 +3,12 @@ import {
mockedObjectMetadataItems,
mockedPersonObjectMetadataItem,
} from '~/testing/mock-data/metadata';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { getPeopleMock } from '~/testing/mock-data/people';
import { getRecordNodeFromRecord } from '../getRecordNodeFromRecord';
const peopleMock = getPeopleMock();
describe('getRecordNodeFromRecord', () => {
it('computes relation records cache references by default', () => {
// Given
@ -19,7 +21,7 @@ describe('getRecordNodeFromRecord', () => {
name: true,
company: true,
};
const record = mockedPeopleData[0];
const record = peopleMock[0];
// When
const result = getRecordNodeFromRecord({
@ -33,12 +35,12 @@ describe('getRecordNodeFromRecord', () => {
expect(result).toEqual({
__typename: 'Person',
company: {
__ref: 'Company:5c21e19e-e049-4393-8c09-3e3f8fb09ecb',
__ref: `Company:${record.company.id}`,
},
name: {
__typename: 'FullName',
firstName: 'Alexandre',
lastName: 'Prot',
firstName: record.name.firstName,
lastName: record.name.lastName,
},
});
});
@ -54,7 +56,7 @@ describe('getRecordNodeFromRecord', () => {
name: true,
company: true,
};
const record = mockedPeopleData[0];
const record = peopleMock[0];
const computeReferences = false;
// When
@ -72,8 +74,8 @@ describe('getRecordNodeFromRecord', () => {
company: record.company,
name: {
__typename: 'FullName',
firstName: 'Alexandre',
lastName: 'Prot',
firstName: record.name.firstName,
lastName: record.name.lastName,
},
});
});

View File

@ -1,5 +1,7 @@
import { gql } from '@apollo/client';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { getPeopleMock } from '~/testing/mock-data/people';
const peopleMock = getPeopleMock();
export const query = gql`
query FindDuplicatePerson($id: ID!) {
@ -49,11 +51,11 @@ export const responseData = {
personDuplicates: {
edges: [
{
node: { ...mockedPeopleData[0], updatedAt: '' },
node: { ...peopleMock[0], updatedAt: '' },
cursor: 'cursor1',
},
{
node: { ...mockedPeopleData[1], updatedAt: '' },
node: { ...peopleMock[1], updatedAt: '' },
cursor: 'cursor2',
},
],

View File

@ -1,8 +1,10 @@
import { useAddressField } from '@/object-record/record-field/meta-types/hooks/useAddressField';
import { isNonEmptyString } from '@sniptt/guards';
import { useAddressFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useAddressFieldDisplay';
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
export const AddressFieldDisplay = () => {
const { fieldValue } = useAddressField();
const { fieldValue } = useAddressFieldDisplay();
const content = [
fieldValue?.addressStreet1,
@ -10,7 +12,7 @@ export const AddressFieldDisplay = () => {
fieldValue?.addressCity,
fieldValue?.addressCountry,
]
.filter(Boolean)
.filter(isNonEmptyString)
.join(', ');
return <TextDisplay text={content} />;

View File

@ -1,8 +1,8 @@
import { useBooleanField } from '@/object-record/record-field/meta-types/hooks/useBooleanField';
import { useBooleanFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useBooleanFieldDisplay';
import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';
export const BooleanFieldDisplay = () => {
const { fieldValue } = useBooleanField();
const { fieldValue } = useBooleanFieldDisplay();
return <BooleanDisplay value={fieldValue} />;
};

View File

@ -1,9 +1,8 @@
import { useCurrencyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useCurrencyFieldDisplay';
import { CurrencyDisplay } from '@/ui/field/display/components/CurrencyDisplay';
import { useCurrencyField } from '../../hooks/useCurrencyField';
export const CurrencyFieldDisplay = () => {
const { fieldValue } = useCurrencyField();
const { fieldValue } = useCurrencyFieldDisplay();
return <CurrencyDisplay currencyValue={fieldValue} />;
};

View File

@ -1,9 +1,8 @@
import { useDateFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useDateFieldDisplay';
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
import { useDateField } from '../../hooks/useDateField';
export const DateFieldDisplay = () => {
const { fieldValue } = useDateField();
const { fieldValue } = useDateFieldDisplay();
return <DateDisplay value={fieldValue} />;
};

View File

@ -1,9 +1,8 @@
import { useDateTimeFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useDateTimeFieldDisplay';
import { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay';
import { useDateTimeField } from '../../hooks/useDateTimeField';
export const DateTimeFieldDisplay = () => {
const { fieldValue } = useDateTimeField();
const { fieldValue } = useDateTimeFieldDisplay();
return <DateTimeDisplay value={fieldValue} />;
};

View File

@ -1,9 +1,8 @@
import { useEmailFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useEmailFieldDisplay';
import { EmailDisplay } from '@/ui/field/display/components/EmailDisplay';
import { useEmailField } from '../../hooks/useEmailField';
export const EmailFieldDisplay = () => {
const { fieldValue } = useEmailField();
const { fieldValue } = useEmailFieldDisplay();
return <EmailDisplay value={fieldValue} />;
};

View File

@ -1,11 +1,13 @@
import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField';
import { isNonEmptyString } from '@sniptt/guards';
import { useFullNameFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useFullNameFieldDisplay';
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
export const FullNameFieldDisplay = () => {
const { fieldValue } = useFullNameField();
const { fieldValue } = useFullNameFieldDisplay();
const content = [fieldValue.firstName, fieldValue.lastName]
.filter(Boolean)
const content = [fieldValue?.firstName, fieldValue?.lastName]
.filter(isNonEmptyString)
.join(' ');
return <TextDisplay text={content} />;

View File

@ -1,19 +1,15 @@
import { useJsonField } from '@/object-record/record-field/meta-types/hooks/useJsonField';
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
import { useJsonFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useJsonFieldDisplay';
import { JsonDisplay } from '@/ui/field/display/components/JsonDisplay';
import { isDefined } from '~/utils/isDefined';
export const JsonFieldDisplay = () => {
const { fieldValue, maxWidth } = useJsonField();
const { fieldValue, maxWidth } = useJsonFieldDisplay();
return (
<JsonDisplay
text={
isFieldRawJsonValue(fieldValue) && isDefined(fieldValue)
? JSON.stringify(fieldValue)
: ''
}
maxWidth={maxWidth}
/>
);
if (!isDefined(fieldValue)) {
return <></>;
}
const value = JSON.stringify(fieldValue);
return <JsonDisplay text={value} maxWidth={maxWidth} />;
};

View File

@ -1,9 +1,8 @@
import { useLinkFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useLinkFieldDisplay';
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
import { useLinkField } from '../../hooks/useLinkField';
export const LinkFieldDisplay = () => {
const { fieldValue } = useLinkField();
const { fieldValue } = useLinkFieldDisplay();
return <LinkDisplay value={fieldValue} />;
};

View File

@ -1,9 +1,9 @@
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
import { useLinksFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useLinksFieldDisplay';
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
export const LinksFieldDisplay = () => {
const { fieldValue } = useLinksField();
const { fieldValue } = useLinksFieldDisplay();
const { isFocused } = useFieldFocus();

View File

@ -1,23 +1,39 @@
import { Tag } from 'twenty-ui';
import { styled } from '@linaria/react';
import { Tag, THEME_COMMON } from 'twenty-ui';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
import { useMultiSelectFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useMultiSelectFieldDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
const spacing1 = THEME_COMMON.spacing(1);
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${spacing1};
justify-content: flex-start;
max-width: 100%;
overflow: hidden;
width: 100%;
`;
export const MultiSelectFieldDisplay = () => {
const { fieldValues, fieldDefinition } = useMultiSelectField();
const { fieldValue, fieldDefinition } = useMultiSelectFieldDisplay();
const { isFocused } = useFieldFocus();
const selectedOptions = fieldValues
const selectedOptions = fieldValue
? fieldDefinition.metadata.options?.filter((option) =>
fieldValues.includes(option.value),
fieldValue.includes(option.value),
)
: [];
if (!selectedOptions) return null;
return (
return isFocused ? (
<ExpandableList isChipCountDisplayed={isFocused}>
{selectedOptions.map((selectedOption, index) => (
<Tag
@ -27,5 +43,16 @@ export const MultiSelectFieldDisplay = () => {
/>
))}
</ExpandableList>
) : (
<StyledContainer>
{selectedOptions.map((selectedOption, index) => (
<Tag
preventShrink
key={index}
color={selectedOption.color}
text={selectedOption.label}
/>
))}
</StyledContainer>
);
};

View File

@ -1,9 +1,8 @@
import { useNumberFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useNumberFieldDisplay';
import { NumberDisplay } from '@/ui/field/display/components/NumberDisplay';
import { useNumberField } from '../../hooks/useNumberField';
export const NumberFieldDisplay = () => {
const { fieldValue } = useNumberField();
const { fieldValue } = useNumberFieldDisplay();
return <NumberDisplay value={fieldValue} />;
};

View File

@ -1,17 +1,24 @@
import { Tag } from 'twenty-ui';
import { useSelectField } from '../../hooks/useSelectField';
import { useSelectFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useSelectFieldDisplay';
import { isDefined } from '~/utils/isDefined';
export const SelectFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useSelectField();
const { fieldValue, fieldDefinition } = useSelectFieldDisplay();
const selectedOption = fieldDefinition.metadata.options?.find(
(option) => option.value === fieldValue,
);
return selectedOption ? (
<Tag color={selectedOption.color} text={selectedOption.label} />
) : (
<></>
if (!isDefined(selectedOption)) {
return <></>;
}
return (
<Tag
preventShrink
color={selectedOption.color}
text={selectedOption.label}
/>
);
};

View File

@ -1,9 +1,8 @@
import { useTextFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useTextFieldDisplay';
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
import { useTextField } from '../../hooks/useTextField';
export const TextFieldDisplay = () => {
const { fieldValue, maxWidth } = useTextField();
const { fieldValue, maxWidth } = useTextFieldDisplay();
return <TextDisplay text={fieldValue} maxWidth={maxWidth} />;
};

View File

@ -1,67 +0,0 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { FieldMetadataType } from '~/generated/graphql';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useDateTimeField } from '../../../hooks/useDateTimeField';
import { DateTimeFieldDisplay } from '../DateTimeFieldDisplay';
const formattedDate = new Date('2023-04-01');
const DateFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useDateTimeField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/DateFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'date',
label: 'Date',
type: FieldMetadataType.DateTime,
iconName: 'IconCalendarEvent',
metadata: {
fieldName: 'Date',
objectMetadataNameSingular: 'person',
},
},
hotkeyScope: 'hotkey-scope',
}}
>
<DateFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],
component: DateTimeFieldDisplay,
argTypes: { value: { control: 'date' } },
args: {
value: formattedDate,
},
};
export default meta;
type Story = StoryObj<typeof DateTimeFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};

View File

@ -1,67 +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 { useEmailField } from '@/object-record/record-field/meta-types/hooks/useEmailField';
import { FieldMetadataType } from '~/generated/graphql';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { EmailFieldDisplay } from '../EmailFieldDisplay';
const EmailFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useEmailField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/EmailFieldDisplay',
decorators: [
MemoryRouterDecorator,
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'email',
label: 'Email',
type: FieldMetadataType.Email,
iconName: 'IconLink',
metadata: {
fieldName: 'Email',
placeHolder: 'Email',
objectMetadataNameSingular: 'person',
},
},
hotkeyScope: 'hotkey-scope',
}}
>
<EmailFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],
component: EmailFieldDisplay,
args: {
value: 'Test@Test.test',
},
};
export default meta;
type Story = StoryObj<typeof EmailFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};

View File

@ -1,83 +0,0 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { FieldMetadataType } from '~/generated/graphql';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useNumberField } from '../../../hooks/useNumberField';
import { NumberFieldDisplay } from '../NumberFieldDisplay';
const NumberFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useNumberField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/NumberFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'number',
label: 'Number',
type: FieldMetadataType.Number,
iconName: 'Icon123',
metadata: {
fieldName: 'Number',
placeHolder: 'Number',
isPositive: true,
objectMetadataNameSingular: 'person',
},
},
hotkeyScope: 'hotkey-scope',
useUpdateRecord: () => [() => undefined, {}],
}}
>
<NumberFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],
component: NumberFieldDisplay,
};
export default meta;
type Story = StoryObj<typeof NumberFieldDisplay>;
export const Default: Story = {
args: {
value: 100,
},
};
export const Elipsis: Story = {
args: {
value: 1e100,
},
parameters: {
container: { width: 100 },
},
};
export const Negative: Story = {
args: {
value: -1000,
},
};
export const Float: Story = {
args: {
value: 1.357802,
},
};

View File

@ -0,0 +1,54 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { AddressFieldDisplay } from '@/object-record/record-field/meta-types/display/components/AddressFieldDisplay';
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
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/AddressFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'testAddress'),
ComponentDecorator,
],
component: AddressFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof AddressFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 100 },
},
decorators: [
getFieldDecorator('person', 'testAddress', {
addressCity:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam',
addressCountry: 'United States',
addressStreet1: '1234 Elm Street',
addressStreet2: 'Apt 1234',
addressLat: 0,
addressLng: 0,
addressPostcode: '12345',
addressState: 'CA',
} as FieldAddressValue),
],
};
export const Performance = getProfilingStory({
componentName: 'AddressFieldDisplay',
averageThresholdInMs: 0.15,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,34 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
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/BooleanFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'testBoolean'),
ComponentDecorator,
],
component: BooleanFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof BooleanFieldDisplay>;
export const Default: Story = {};
export const Performance = getProfilingStory({
componentName: 'BooleanFieldDisplay',
averageThresholdInMs: 0.15,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,64 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { CurrencyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/CurrencyFieldDisplay';
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/CurrencyFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('company', 'annualRecurringRevenue'),
ComponentDecorator,
],
component: CurrencyFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof CurrencyFieldDisplay>;
export const Default: Story = {};
export const Millions: Story = {
decorators: [
getFieldDecorator('company', 'annualRecurringRevenue', {
__typename: 'Currency',
amountMicros: 18200000 * 1000000,
currencyCode: 'EUR',
}),
],
};
export const Billions: Story = {
decorators: [
getFieldDecorator('company', 'annualRecurringRevenue', {
__typename: 'Currency',
amountMicros: 3230000000 * 1000000,
currencyCode: 'USD',
}),
],
};
export const Bazillions: Story = {
decorators: [
getFieldDecorator('company', 'annualRecurringRevenue', {
__typename: 'Currency',
amountMicros: 1e100,
currencyCode: 'USD',
}),
],
};
export const Performance = getProfilingStory({
componentName: 'CurrencyFieldDisplay',
averageThresholdInMs: 0.2,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,40 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { DateFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateFieldDisplay';
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/DateFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'createdAt'),
ComponentDecorator,
],
component: DateFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof DateFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
export const Performance = getProfilingStory({
componentName: 'DateFieldDisplay',
averageThresholdInMs: 0.1,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,40 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { DateTimeFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateTimeFieldDisplay';
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/DateTimeFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'createdAt'),
ComponentDecorator,
],
component: DateTimeFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof DateTimeFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
export const Performance = getProfilingStory({
componentName: 'DateTimeFieldDisplay',
averageThresholdInMs: 0.1,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,47 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { EmailFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailFieldDisplay';
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/EmailFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'email'),
ComponentDecorator,
],
component: EmailFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof EmailFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
decorators: [
getFieldDecorator(
'person',
'email',
'asdasdasdaksjdhkajshdkajhasmdkamskdsd@asdkjhaksjdhaksjd.com',
),
],
};
export const Performance = getProfilingStory({
componentName: 'EmailFieldDisplay',
averageThresholdInMs: 0.5,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,40 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { FullNameFieldDisplay } from '@/object-record/record-field/meta-types/display/components/FullNameFieldDisplay';
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/FullNameFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'name'),
ComponentDecorator,
],
component: FullNameFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof FullNameFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
export const Performance = getProfilingStory({
componentName: 'FullNameFieldDisplay',
averageThresholdInMs: 0.5,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,40 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { JsonFieldDisplay } from '@/object-record/record-field/meta-types/display/components/JsonFieldDisplay';
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/JsonFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'testJson'),
ComponentDecorator,
],
component: JsonFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof JsonFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
export const Performance = getProfilingStory({
componentName: 'JsonFieldDisplay',
averageThresholdInMs: 0.1,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,34 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { LinkFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinkFieldDisplay';
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/LinkFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'testLink'),
ComponentDecorator,
],
component: LinkFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof LinkFieldDisplay>;
export const Default: Story = {};
export const Performance = getProfilingStory({
componentName: 'LinkFieldDisplay',
averageThresholdInMs: 0.5,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,69 @@
import { useContext, useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { FieldFocusContext } from '@/object-record/record-field/contexts/FieldFocusContext';
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
const FieldFocusEffect = () => {
const { setIsFocused } = useContext(FieldFocusContext);
useEffect(() => {
setIsFocused(true);
}, [setIsFocused]);
return <></>;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/LinksFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'testLinks'),
ComponentDecorator,
],
component: LinksFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof LinksFieldDisplay>;
export const Default: Story = {};
export const ExpandableList: Story = {
decorators: [
(Story) => {
return (
<FieldFocusContextProvider>
<FieldFocusEffect />
<Story />
</FieldFocusContextProvider>
);
},
],
parameters: {
container: { width: 100 },
},
};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
export const Performance = getProfilingStory({
componentName: 'LinksFieldDisplay',
averageThresholdInMs: 0.5,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,69 @@
import { useContext, useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { FieldFocusContext } from '@/object-record/record-field/contexts/FieldFocusContext';
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
import { MultiSelectFieldDisplay } from '@/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay';
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
const FieldFocusEffect = () => {
const { setIsFocused } = useContext(FieldFocusContext);
useEffect(() => {
setIsFocused(true);
}, [setIsFocused]);
return <></>;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/MultiSelectFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'testMultiSelect'),
ComponentDecorator,
],
component: MultiSelectFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof MultiSelectFieldDisplay>;
export const Default: Story = {};
export const ExpandableList: Story = {
decorators: [
(Story) => {
return (
<FieldFocusContextProvider>
<FieldFocusEffect />
<Story />
</FieldFocusContextProvider>
);
},
],
parameters: {
container: { width: 130 },
},
};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
export const Performance = getProfilingStory({
componentName: 'MultiSelectFieldDisplay',
averageThresholdInMs: 0.2,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,53 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { NumberFieldDisplay } from '@/object-record/record-field/meta-types/display/components/NumberFieldDisplay';
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/NumberFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('company', 'employees'),
ComponentDecorator,
],
component: NumberFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof NumberFieldDisplay>;
export const Default: Story = {
args: {
value: 100,
},
};
export const Elipsis: Story = {
decorators: [getFieldDecorator('company', 'employees', 1e100)],
parameters: {
container: { width: 100 },
},
};
export const Negative: Story = {
decorators: [getFieldDecorator('company', 'employees', -1000)],
};
export const Float: Story = {
decorators: [getFieldDecorator('company', 'employees', 3.14159)],
};
export const Performance = getProfilingStory({
componentName: 'NumberFieldDisplay',
averageThresholdInMs: 0.5,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -33,9 +33,6 @@ export const Elipsis: Story = {
};
export const WrongNumber: Story = {
parameters: {
container: { width: 50 },
},
decorators: [getFieldDecorator('person', 'phone', 'sdklaskdj')],
};

View File

@ -0,0 +1,40 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { SelectFieldDisplay } from '@/object-record/record-field/meta-types/display/components/SelectFieldDisplay';
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/SelectFieldDisplay',
decorators: [
MemoryRouterDecorator,
getFieldDecorator('person', 'testSelect'),
ComponentDecorator,
],
component: SelectFieldDisplay,
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof SelectFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
export const Performance = getProfilingStory({
componentName: 'SelectFieldDisplay',
averageThresholdInMs: 0.2,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -1,54 +1,22 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { FieldMetadataType } from '~/generated/graphql';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useTextField } from '../../../hooks/useTextField';
import { TextFieldDisplay } from '../TextFieldDisplay';
const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useTextField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
import { TextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/TextFieldDisplay';
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/TextFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'text',
label: 'Text',
type: FieldMetadataType.Text,
iconName: 'IconLink',
metadata: {
fieldName: 'Text',
placeHolder: 'Text',
objectMetadataNameSingular: 'person',
},
},
hotkeyScope: 'hotkey-scope',
}}
>
<TextFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
MemoryRouterDecorator,
getFieldDecorator('person', 'city'),
ComponentDecorator,
],
component: TextFieldDisplay,
args: {
value: 'Lorem ipsum',
args: {},
parameters: {
chromatic: { disableSnapshot: true },
},
};
@ -59,11 +27,21 @@ type Story = StoryObj<typeof TextFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
args: {
value:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae rerum fugiat veniam illum accusantium saepe, voluptate inventore libero doloribus doloremque distinctio blanditiis amet quis dolor a nulla? Placeat nam itaque rerum esse quidem animi, temporibus saepe debitis commodi quia eius eos minus inventore. Voluptates fugit optio sit ab consectetur ipsum, neque eius atque blanditiis. Ullam provident at porro minima, nobis vero dicta consequatur maxime laboriosam fugit repudiandae repellat tempore voluptas non voluptatibus neque aliquam ducimus doloribus ipsa? Sapiente suscipit unde modi commodi possimus doloribus eum voluptatibus, architecto laudantium, magnam, eos numquam exercitationem est maxime explicabo odio nemo qui distinctio temporibus.',
},
parameters: {
container: { width: 100 },
},
decorators: [
getFieldDecorator(
'person',
'city',
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae rerum fugiat veniam illum accusantium saepe, voluptate inventore libero doloribus doloremque distinctio blanditiis amet quis dolor a nulla? Placeat nam itaque rerum esse quidem animi, temporibus saepe debitis commodi quia eius eos minus inventore. Voluptates fugit optio sit ab consectetur ipsum, neque eius atque blanditiis. Ullam provident at porro minima, nobis vero dicta consequatur maxime laboriosam fugit repudiandae repellat tempore voluptas non voluptatibus neque aliquam ducimus doloribus ipsa? Sapiente suscipit unde modi commodi possimus doloribus eum voluptatibus, architecto laudantium, magnam, eos numquam exercitationem est maxime explicabo odio nemo qui distinctio temporibus.',
),
],
};
export const Performance = getProfilingStory({
componentName: 'TextFieldDisplay',
averageThresholdInMs: 0.5,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});

View File

@ -0,0 +1,22 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
import { FieldAddressValue } from '../../types/FieldMetadata';
export const useAddressFieldDisplay = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldAddressValue | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
};
};

View File

@ -0,0 +1,21 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
export const useBooleanFieldDisplay = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<boolean | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
};
};

View File

@ -0,0 +1,22 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
import { FieldCurrencyValue } from '../../types/FieldMetadata';
export const useCurrencyFieldDisplay = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldCurrencyValue | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
};
};

View File

@ -0,0 +1,24 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
export const useDateFieldDisplay = () => {
const { entityId, fieldDefinition, hotkeyScope, clearable } =
useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<string | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
hotkeyScope,
clearable,
};
};

View File

@ -0,0 +1,24 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
export const useDateTimeFieldDisplay = () => {
const { entityId, fieldDefinition, hotkeyScope, clearable } =
useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<string | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
hotkeyScope,
clearable,
};
};

View File

@ -0,0 +1,22 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
export const useEmailFieldDisplay = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<string | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
hotkeyScope,
};
};

View File

@ -0,0 +1,22 @@
import { useContext } from 'react';
import { FieldFullNameValue } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
export const useFullNameFieldDisplay = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldFullNameValue | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
};
};

View File

@ -0,0 +1,23 @@
import { useContext } from 'react';
import { FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
export const useJsonFieldDisplay = () => {
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldJsonValue | undefined>(
entityId,
fieldName,
);
return {
maxWidth,
fieldDefinition,
fieldValue,
};
};

View File

@ -0,0 +1,21 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
import { FieldLinkValue } from '../../types/FieldMetadata';
export const useLinkFieldDisplay = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldLinkValue | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
};
};

View File

@ -0,0 +1,22 @@
import { useContext } from 'react';
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
export const useLinksFieldDisplay = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldLinksValue | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
};
};

View File

@ -0,0 +1,26 @@
import { useContext } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import {
FieldMultiSelectMetadata,
FieldMultiSelectValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
export const useMultiSelectFieldDisplay = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const { fieldName } = fieldDefinition.metadata;
const fieldValue = useRecordFieldValue<FieldMultiSelectValue | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition:
fieldDefinition as FieldDefinition<FieldMultiSelectMetadata>,
fieldValue,
};
};

View File

@ -0,0 +1,26 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldNumber } from '../../types/guards/isFieldNumber';
export const useNumberFieldDisplay = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata(FieldMetadataType.Number, isFieldNumber, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<number | null | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,
fieldValue,
hotkeyScope,
};
};

View File

@ -9,7 +9,10 @@ export const usePhoneFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue(entityId, fieldName);
const fieldValue = useRecordFieldValue<string | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition,

View File

@ -3,6 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards';
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';
@ -32,7 +33,10 @@ export const useRelationFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue(entityId, fieldName);
const fieldValue = useRecordFieldValue<ObjectRecord | undefined>(
entityId,
fieldName,
);
const maxWidthForField =
isDefined(button) && isDefined(maxWidth)

View File

@ -0,0 +1,26 @@
import { useContext } from 'react';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
import {
FieldSelectMetadata,
FieldSelectValue,
} from '../../types/FieldMetadata';
export const useSelectFieldDisplay = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const { fieldName } = fieldDefinition.metadata;
const fieldValue = useRecordFieldValue<FieldSelectValue | undefined>(
entityId,
fieldName,
);
return {
fieldDefinition: fieldDefinition as FieldDefinition<FieldSelectMetadata>,
fieldValue,
};
};

View File

@ -0,0 +1,22 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldContext } from '../../contexts/FieldContext';
export const useTextFieldDisplay = () => {
const { entityId, fieldDefinition, hotkeyScope, maxWidth } =
useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue =
useRecordFieldValue<string | undefined>(entityId, fieldName) ?? '';
return {
maxWidth,
fieldDefinition,
fieldValue,
hotkeyScope,
};
};

View File

@ -1,20 +1,26 @@
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { mockedCompaniesData } from '~/testing/mock-data/companies';
import { mockObjectMetadataItem } from '~/testing/mock-data/objectMetadataItems';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { isRecordMatchingFilter } from './isRecordMatchingFilter';
const companiesMock = getCompaniesMock();
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
describe('isRecordMatchingFilter', () => {
describe('Empty Filters', () => {
it('matches any record when no filter is provided', () => {
const emptyFilter = {};
mockedCompaniesData.forEach((company) => {
companiesMock.forEach((company) => {
expect(
isRecordMatchingFilter({
record: company,
filter: emptyFilter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
});
@ -26,12 +32,12 @@ describe('isRecordMatchingFilter', () => {
employees: {},
};
mockedCompaniesData.forEach((company) => {
companiesMock.forEach((company) => {
expect(
isRecordMatchingFilter({
record: company,
filter: filterWithEmptyFields,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
});
@ -40,12 +46,12 @@ describe('isRecordMatchingFilter', () => {
it('matches any record with an empty and filter', () => {
const filter = { and: [] };
mockedCompaniesData.forEach((company) => {
companiesMock.forEach((company) => {
expect(
isRecordMatchingFilter({
record: company,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
});
@ -54,12 +60,12 @@ describe('isRecordMatchingFilter', () => {
it('matches any record with an empty or filter', () => {
const filter = { or: [] };
mockedCompaniesData.forEach((company) => {
companiesMock.forEach((company) => {
expect(
isRecordMatchingFilter({
record: company,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
});
@ -68,12 +74,12 @@ describe('isRecordMatchingFilter', () => {
it('matches any record with an empty not filter', () => {
const filter = { not: {} };
mockedCompaniesData.forEach((company) => {
companiesMock.forEach((company) => {
expect(
isRecordMatchingFilter({
record: company,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
});
@ -82,92 +88,161 @@ describe('isRecordMatchingFilter', () => {
describe('Simple Filters', () => {
it('matches a record with a simple equality filter on name', () => {
const filter = { name: { eq: 'Airbnb' } };
const companyMockInFilter = {
...companiesMock[0],
};
const companyMockNotInFilter = {
...companiesMock[0],
name: companyMockInFilter.name + 'Different',
};
const filter = { name: { eq: companyMockInFilter.name } };
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[0],
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[1],
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false);
});
it('matches a record with a simple equality filter on domainName', () => {
const filter = { domainName: { eq: 'airbnb.com' } };
const companyMockInFilter = {
...companiesMock[0],
};
const companyMockNotInFilter = {
...companiesMock[0],
domainName: companyMockInFilter.domainName + 'Different',
};
const filter = { domainName: { eq: companyMockInFilter.domainName } };
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[0],
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[1],
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false);
});
it('matches a record with a greater than filter on employees', () => {
const filter = { employees: { gt: 10 } };
const companyMockInFilter = {
...companiesMock[0],
employees: 100,
};
const companyMockNotInFilter = {
...companiesMock[0],
employees: companyMockInFilter.employees - 50,
};
const filter = {
employees: { gt: companyMockInFilter.employees - 1 },
};
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[0],
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[1],
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false);
});
it('matches a record with a boolean filter on idealCustomerProfile', () => {
const filter = { idealCustomerProfile: { eq: true } };
const companyIdealCustomerProfileTrue = {
...companiesMock[0],
idealCustomerProfile: true,
};
const companyIdealCustomerProfileFalse = {
...companiesMock[0],
idealCustomerProfile: false,
};
const filter = {
idealCustomerProfile: {
eq: companyIdealCustomerProfileTrue.idealCustomerProfile,
},
};
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[0],
record: companyIdealCustomerProfileTrue,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
).toBe(companyIdealCustomerProfileTrue.idealCustomerProfile);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[4], // Assuming this record has idealCustomerProfile as false
record: companyIdealCustomerProfileFalse,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false);
).toBe(companyIdealCustomerProfileFalse.idealCustomerProfile);
});
});
describe('Complex And/Or/Not Nesting', () => {
it('matches record with a combination of and + or filters', () => {
const companyMockInFilter = {
...companiesMock[0],
idealCustomerProfile: true,
employees: 100,
};
const companyMockNotInFilter = {
...companiesMock[0],
idealCustomerProfile: false,
employees: 0,
};
const filter: RecordGqlOperationFilter = {
and: [
{ domainName: { eq: 'airbnb.com' } },
{
domainName: {
eq: companyMockInFilter.domainName,
},
},
{
or: [
{ employees: { gt: 10 } },
{ idealCustomerProfile: { eq: true } },
{
employees: {
gt: companyMockInFilter.employees - 1,
},
},
{
idealCustomerProfile: {
eq: companyMockInFilter.idealCustomerProfile,
},
},
],
},
],
@ -175,118 +250,181 @@ describe('isRecordMatchingFilter', () => {
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[1], // Aircall
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false);
});
it('matches record with nested not filter', () => {
const companyMockInFilter = {
...companiesMock[0],
idealCustomerProfile: true,
employees: 100,
};
const companyMockNotInFilter = {
...companiesMock[0],
idealCustomerProfile: false,
name: companyMockInFilter.name + 'Different',
};
const filter: RecordGqlOperationFilter = {
not: {
and: [
{ name: { eq: 'Airbnb' } },
{ idealCustomerProfile: { eq: true } },
{ name: { eq: companyMockInFilter.name } },
{
idealCustomerProfile: {
eq: companyMockInFilter.idealCustomerProfile,
},
},
],
},
};
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false); // Should not match as it's Airbnb with idealCustomerProfile true
).toBe(false);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[3], // Apple
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true); // Should match as it's not Airbnb
).toBe(true);
});
it('matches record with deep nesting of and, or, and not filters', () => {
const companyMockInFilter = {
...companiesMock[0],
idealCustomerProfile: true,
employees: 100,
};
const companyMockNotInFilter = {
...companiesMock[0],
domainName: companyMockInFilter.domainName + 'Different',
employees: 5,
name: companyMockInFilter.name + 'Different',
};
const filter: RecordGqlOperationFilter = {
and: [
{ domainName: { eq: 'apple.com' } },
{ domainName: { eq: companyMockInFilter.domainName } },
{
or: [{ employees: { eq: 10 } }, { not: { name: { eq: 'Apple' } } }],
or: [
{ employees: { eq: companyMockInFilter.employees } },
{ not: { name: { eq: companyMockInFilter.name } } },
],
},
],
};
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[3], // Apple
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[4], // Qonto
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false);
});
it('matches record with and filter at root level', () => {
const companyMockInFilter = {
...companiesMock[0],
idealCustomerProfile: true,
};
const companyMockNotInFilter = {
...companiesMock[0],
idealCustomerProfile: false,
name: companyMockInFilter.name + 'Different',
};
const filter: RecordGqlOperationFilter = {
and: [
{ name: { eq: 'Facebook' } },
{ idealCustomerProfile: { eq: true } },
{ name: { eq: companyMockInFilter.name } },
{
idealCustomerProfile: {
eq: companyMockInFilter.idealCustomerProfile,
},
},
],
};
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[5], // Facebook
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false);
});
it('matches record with or filter at root level including a not condition', () => {
const companyMockInFilter = {
...companiesMock[0],
idealCustomerProfile: true,
employees: 100,
};
const companyMockNotInFilter = {
...companiesMock[0],
idealCustomerProfile: false,
name: companyMockInFilter.name + 'Different',
employees: companyMockInFilter.employees - 1,
};
const filter: RecordGqlOperationFilter = {
or: [{ name: { eq: 'Sequoia' } }, { not: { employees: { eq: 1 } } }],
or: [
{ name: { eq: companyMockInFilter.name } },
{ not: { employees: { eq: companyMockInFilter.employees - 1 } } },
],
};
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[6], // Sequoia
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[1], // Aircall
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false);
});
@ -294,49 +432,75 @@ describe('isRecordMatchingFilter', () => {
describe('Implicit And Conditions', () => {
it('matches record with implicit and of multiple operators within the same field', () => {
const companyMockInFilter = {
...companiesMock[0],
idealCustomerProfile: true,
employees: 100,
};
const companyMockNotInFilter = {
...companiesMock[0],
idealCustomerProfile: false,
name: companyMockInFilter.name + 'Different',
employees: companyMockInFilter.employees + 100,
};
const filter = {
employees: { gt: 10, lt: 100000 },
name: { eq: 'Airbnb' },
employees: {
gt: companyMockInFilter.employees - 10,
lt: companyMockInFilter.employees + 10,
},
name: { eq: companyMockInFilter.name },
};
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true); // Matches as Airbnb's employee count is between 10 and 100000
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[1], // Aircall
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false); // Does not match as Aircall's employee count is not within the range
});
it('matches record with implicit and within an object passed to or', () => {
const companyMockInFilter = {
...companiesMock[0],
};
const companyMockNotInFilter = {
...companiesMock[0],
name: companyMockInFilter.name + 'Different',
domainName: companyMockInFilter.name + 'Different',
};
const filter = {
or: {
name: { eq: 'Airbnb' },
domainName: { eq: 'airbnb.com' },
name: { eq: companyMockInFilter.name },
domainName: { eq: companyMockInFilter.domainName },
},
};
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb
record: companyMockInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(true);
expect(
isRecordMatchingFilter({
record: mockedCompaniesData[2], // Algolia
record: companyMockNotInFilter,
filter,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItem: companyMockObjectMetadataItem,
}),
).toBe(false);
});

View File

@ -6,10 +6,12 @@ import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorato
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 { getCompaniesMock } from '~/testing/mock-data/companies';
import { RecordDetailDuplicatesSection } from '../RecordDetailDuplicatesSection';
const companiesMock = getCompaniesMock();
const meta: Meta<typeof RecordDetailDuplicatesSection> = {
title:
'Modules/ObjectRecord/RecordShow/RecordDetailSection/RecordDetailDuplicatesSection',
@ -21,7 +23,7 @@ const meta: Meta<typeof RecordDetailDuplicatesSection> = {
MemoryRouterDecorator,
],
args: {
objectRecordId: mockedCompaniesData[0].id,
objectRecordId: companiesMock[0].id,
objectNameSingular: CoreObjectNameSingular.Company,
},
parameters: {

View File

@ -8,12 +8,16 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
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 { getCompaniesMock } from '~/testing/mock-data/companies';
import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { getPeopleMock } from '~/testing/mock-data/people';
import { RecordDetailRelationSection } from '../RecordDetailRelationSection';
const companiesMock = getCompaniesMock();
const peopleMock = getPeopleMock();
const meta: Meta<typeof RecordDetailRelationSection> = {
title:
'Modules/ObjectRecord/RecordShow/RecordDetailSection/RecordDetailRelationSection',
@ -22,7 +26,7 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
(Story) => (
<FieldContext.Provider
value={{
entityId: mockedCompaniesData[0].id,
entityId: companiesMock[0].id,
basePathToShowPage: '/object-record/',
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
@ -44,7 +48,7 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
],
parameters: {
msw: graphqlMocks,
records: mockedCompaniesData,
records: companiesMock,
},
};
@ -58,10 +62,10 @@ export const WithRecords: Story = {
parameters: {
records: [
{
...mockedCompaniesData[0],
people: mockedPeopleData,
...companiesMock[0],
people: peopleMock,
},
...mockedPeopleData,
...peopleMock,
],
},
};

View File

@ -36,13 +36,16 @@ export const useRecordValue = (recordId: string) => {
return tableValue?.[recordId] as ObjectRecord | undefined;
};
export const useRecordFieldValue = (recordId: string, fieldName: string) => {
export const useRecordFieldValue = <T,>(
recordId: string,
fieldName: string,
) => {
const recordFieldValues = useContextSelector(
RecordFieldValueSelectorContext,
(value) => value[0],
);
return recordFieldValues?.[recordId]?.[fieldName];
return recordFieldValues?.[recordId]?.[fieldName] as T;
};
export const useSetRecordFieldValue = () => {

View File

@ -8,16 +8,18 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
import { RelationPickerDecorator } from '~/testing/decorators/RelationPickerDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { getPeopleMock } from '~/testing/mock-data/people';
import { sleep } from '~/testing/sleep';
import { EntityForSelect } from '../../types/EntityForSelect';
import { SingleEntitySelect } from '../SingleEntitySelect';
const entities = mockedPeopleData.map<EntityForSelect>((person) => ({
const peopleMock = getPeopleMock();
const entities = peopleMock.map<EntityForSelect>((person) => ({
id: person.id,
name: person.name.firstName + ' ' + person.name.lastName,
avatarUrl: person.avatarUrl,
avatarUrl: 'https://picsum.photos/200',
avatarType: 'rounded',
record: { ...person, __typename: 'Person' },
}));

View File

@ -4,7 +4,7 @@ import { BlocklistItem } from '@/accounts/types/BlocklistItem';
import { IconButton } from '@/ui/input/button/components/IconButton';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { formatToHumanReadableDate } from '~/utils';
import { formatToHumanReadableDate } from '~/utils/date-utils';
type SettingsAccountsEmailsBlocklistTableRowProps = {
blocklistItem: BlocklistItem;

View File

@ -4,7 +4,7 @@ import { ComponentDecorator } from 'twenty-ui';
import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist';
import { SettingsAccountsEmailsBlocklistTable } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistTable';
import { formatToHumanReadableDate } from '~/utils';
import { formatToHumanReadableDate } from '~/utils/date-utils';
const handleBlockedEmailRemoveJestFn = fn();

View File

@ -4,7 +4,7 @@ import { ComponentDecorator } from 'twenty-ui';
import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist';
import { SettingsAccountsEmailsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistTableRow';
import { formatToHumanReadableDate } from '~/utils';
import { formatToHumanReadableDate } from '~/utils/date-utils';
const onRemoveJestFn = fn();

View File

@ -1,36 +1,32 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck, IconX } from 'twenty-ui';
import { styled } from '@linaria/react';
import { IconCheck, IconX, THEME_COMMON } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
const spacing = THEME_COMMON.spacingMultiplicator * 1;
const iconSizeSm = THEME_COMMON.icon.size.sm;
const StyledBooleanFieldValue = styled.div`
margin-left: ${({ theme }) => theme.spacing(1)};
margin-left: ${spacing}px;
`;
type BooleanDisplayProps = {
value: boolean | null;
value: boolean | null | undefined;
};
export const BooleanDisplay = ({ value }: BooleanDisplayProps) => {
const theme = useTheme();
if (!isDefined(value)) {
return <></>;
}
const isTrue = value === true;
return (
<>
{isDefined(value) ? (
<>
{value ? (
<IconCheck size={theme.icon.size.sm} />
) : (
<IconX size={theme.icon.size.sm} />
)}
<StyledBooleanFieldValue>
{value ? 'True' : 'False'}
</StyledBooleanFieldValue>
</>
) : (
<></>
)}
{isTrue ? <IconCheck size={iconSizeSm} /> : <IconX size={iconSizeSm} />}
<StyledBooleanFieldValue>
{isTrue ? 'True' : 'False'}
</StyledBooleanFieldValue>
</>
);
};

View File

@ -1,20 +1,23 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { formatAmount } from '~/utils/format/formatAmount';
import { isDefined } from '~/utils/isDefined';
import { EllipsisDisplay } from './EllipsisDisplay';
type CurrencyDisplayProps = {
currencyValue: FieldCurrencyValue | null | undefined;
};
const StyledEllipsisDisplay = styled(EllipsisDisplay)`
const StyledEllipsisDisplay = styled.div`
align-items: center;
display: flex;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
`;
export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => {
@ -26,9 +29,7 @@ export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => {
? SETTINGS_FIELD_CURRENCY_CODES[currencyValue?.currencyCode]?.Icon
: null;
const amountToDisplay = isDefined(currencyValue?.amountMicros)
? currencyValue.amountMicros / 1000000
: 0;
const amountToDisplay = (currencyValue?.amountMicros ?? 0) / 1000000;
if (!shouldDisplayCurrency) {
return <StyledEllipsisDisplay>{0}</StyledEllipsisDisplay>;

View File

@ -1,11 +1,13 @@
import { formatToHumanReadableDate } from '~/utils';
import { formatISOStringToHumanReadableDate } from '~/utils/date-utils';
import { EllipsisDisplay } from './EllipsisDisplay';
type DateDisplayProps = {
value: Date | string | null | undefined;
value: string | null | undefined;
};
export const DateDisplay = ({ value }: DateDisplayProps) => (
<EllipsisDisplay>{value && formatToHumanReadableDate(value)}</EllipsisDisplay>
<EllipsisDisplay>
{value ? formatISOStringToHumanReadableDate(value) : ''}
</EllipsisDisplay>
);

View File

@ -1,13 +1,13 @@
import { formatToHumanReadableDateTime } from '~/utils';
import { formatISOStringToHumanReadableDateTime } from '~/utils/date-utils';
import { EllipsisDisplay } from './EllipsisDisplay';
type DateTimeDisplayProps = {
value: Date | string | null | undefined;
value: string | null | undefined;
};
export const DateTimeDisplay = ({ value }: DateTimeDisplayProps) => (
<EllipsisDisplay>
{value && formatToHumanReadableDateTime(value)}
{value ? formatISOStringToHumanReadableDateTime(value) : ''}
</EllipsisDisplay>
);

View File

@ -1,21 +1,39 @@
import { MouseEvent } from 'react';
import { ContactLink } from '@/ui/navigation/link/components/ContactLink';
import { isDefined } from '~/utils/isDefined';
import { EllipsisDisplay } from './EllipsisDisplay';
const validateEmail = (email: string) => {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email.trim());
// const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// return emailPattern.test(email.trim());
// Record this without using regex
const emailParts = email.split('@');
if (emailParts.length !== 2) {
return false;
}
return true;
};
type EmailDisplayProps = {
value: string | null;
};
export const EmailDisplay = ({ value }: EmailDisplayProps) => (
<EllipsisDisplay>
{value && validateEmail(value) ? (
export const EmailDisplay = ({ value }: EmailDisplayProps) => {
if (!isDefined(value)) {
return <ContactLink href="#">{value}</ContactLink>;
}
if (!validateEmail(value)) {
return <ContactLink href="#">{value}</ContactLink>;
}
return (
<EllipsisDisplay>
<ContactLink
href={`mailto:${value}`}
onClick={(event: MouseEvent<HTMLElement>) => {
@ -24,8 +42,6 @@ export const EmailDisplay = ({ value }: EmailDisplayProps) => (
>
{value}
</ContactLink>
) : (
<ContactLink href="#">{value}</ContactLink>
)}
</EllipsisDisplay>
);
</EllipsisDisplay>
);
};

View File

@ -1,4 +1,4 @@
import { MouseEvent } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldLinkValue } from '@/object-record/record-field/types/FieldMetadata';
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
@ -6,34 +6,31 @@ import {
LinkType,
SocialLink,
} from '@/ui/navigation/link/components/SocialLink';
import { checkUrlType } from '~/utils/checkUrlType';
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
import { getUrlHostName } from '~/utils/url/getUrlHostName';
type LinkDisplayProps = {
value?: FieldLinkValue;
};
export const LinkDisplay = ({ value }: LinkDisplayProps) => {
const handleClick = (event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
};
const url = value?.url;
const absoluteUrl = getAbsoluteUrl(value?.url || '');
const displayedValue = value?.label || getUrlHostName(absoluteUrl);
const type = checkUrlType(absoluteUrl);
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return (
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
{displayedValue}
</SocialLink>
);
if (!isNonEmptyString(url)) {
return <></>;
}
return (
<RoundedLink href={absoluteUrl} onClick={handleClick}>
{displayedValue}
</RoundedLink>
);
const displayedValue = isNonEmptyString(value?.label)
? value?.label
: url?.replace(/^http[s]?:\/\/(?:[w]+\.)?/gm, '').replace(/^[w]+\./gm, '');
const type = displayedValue.startsWith('linkedin.')
? LinkType.LinkedIn
: displayedValue.startsWith('twitter.')
? LinkType.Twitter
: LinkType.Url;
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return <SocialLink href={url} type={type} label={displayedValue} />;
}
return <RoundedLink href={url} label={displayedValue} />;
};

View File

@ -1,4 +1,6 @@
import { MouseEventHandler, useMemo } from 'react';
import { useMemo } from 'react';
import { styled } from '@linaria/react';
import { THEME_COMMON } from 'twenty-ui';
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
@ -17,6 +19,21 @@ type LinksDisplayProps = {
isFocused?: boolean;
};
const themeSpacing = THEME_COMMON.spacingMultiplicator;
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${themeSpacing * 1}px;
justify-content: flex-start;
max-width: 100%;
overflow: hidden;
width: 100%;
`;
export const LinksDisplay = ({ value, isFocused }: LinksDisplayProps) => {
const links = useMemo(
() =>
@ -41,21 +58,25 @@ export const LinksDisplay = ({ value, isFocused }: LinksDisplayProps) => {
[value?.primaryLinkLabel, value?.primaryLinkUrl, value?.secondaryLinks],
);
const handleClick: MouseEventHandler = (event) => event.stopPropagation();
return (
return isFocused ? (
<ExpandableList isChipCountDisplayed={isFocused}>
{links.map(({ url, label, type }, index) =>
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
<SocialLink key={index} href={url} onClick={handleClick} type={type}>
{label}
</SocialLink>
<SocialLink key={index} href={url} type={type} label={label} />
) : (
<RoundedLink key={index} href={url} onClick={handleClick}>
{label}
</RoundedLink>
<RoundedLink key={index} href={url} label={label} />
),
)}
</ExpandableList>
) : (
<StyledContainer>
{links.map(({ url, label, type }, index) =>
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
<SocialLink key={index} href={url} type={type} label={label} />
) : (
<RoundedLink key={index} href={url} label={label} />
),
)}
</StyledContainer>
);
};

View File

@ -42,17 +42,22 @@ export const URLDisplay = ({ value }: URLDisplayProps) => {
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return (
<EllipsisDisplay>
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
{displayedValue}
</SocialLink>
<SocialLink
href={absoluteUrl}
onClick={handleClick}
type={type}
label={displayedValue}
/>
</EllipsisDisplay>
);
}
return (
<EllipsisDisplay>
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
{displayedValue}
</StyledRawLink>
<StyledRawLink
href={absoluteUrl}
onClick={handleClick}
label={displayedValue}
/>
</EllipsisDisplay>
);
};

View File

@ -1,50 +1,71 @@
import * as React from 'react';
import { Link as ReactLink } from 'react-router-dom';
import styled from '@emotion/styled';
import { Chip, ChipSize, ChipVariant } from 'twenty-ui';
import { MouseEvent } from 'react';
import { styled } from '@linaria/react';
import { isNonEmptyString } from '@sniptt/guards';
import { FONT_COMMON, THEME_COMMON } from 'twenty-ui';
type RoundedLinkProps = {
href: string;
children?: React.ReactNode;
className?: string;
label?: string;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
};
const StyledLink = styled(ReactLink)`
font-size: ${({ theme }) => theme.font.size.md};
const fontSizeMd = FONT_COMMON.size.md;
const spacing1 = THEME_COMMON.spacing(1);
const spacing3 = THEME_COMMON.spacing(3);
const spacingMultiplicator = THEME_COMMON.spacingMultiplicator;
const StyledLink = styled.a`
align-items: center;
background-color: var(--twentycrm-background-transparent-light);
border: 1px solid var(--twentycrm-border-color-medium);
border-radius: 50px;
color: var(--twentycrm-font-color-primary);
cursor: pointer;
display: inline-flex;
font-weight: ${fontSizeMd};
gap: ${spacing1};
height: ${spacing3};
justify-content: center;
max-width: calc(100% - ${spacingMultiplicator} * 2px);
max-width: 100%;
height: ${({ theme }) => theme.spacing(5)};
min-width: fit-content;
overflow: hidden;
padding: ${spacing1} ${spacing1};
text-decoration: none;
text-overflow: ellipsis;
user-select: none;
white-space: nowrap;
`;
const StyledChip = styled(Chip)`
border-color: ${({ theme }) => theme.border.color.strong};
box-sizing: border-box;
padding: ${({ theme }) => theme.spacing(0, 2)};
max-width: 100%;
height: ${({ theme }) => theme.spacing(5)};
min-width: 40px;
`;
export const RoundedLink = ({ label, href, onClick }: RoundedLinkProps) => {
if (!isNonEmptyString(label)) {
return <></>;
}
export const RoundedLink = ({
children,
className,
href,
onClick,
}: RoundedLinkProps) => {
if (!children) return null;
const handleClick = (event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
onClick?.(event);
};
return (
<StyledLink
className={className}
href={href}
target="_blank"
to={href}
onClick={onClick}
rel="noreferrer"
onClick={handleClick}
>
<StyledChip
label={`${children}`}
variant={ChipVariant.Rounded}
size={ChipSize.Large}
/>
{label}
</StyledLink>
);
};

View File

@ -11,24 +11,15 @@ export enum LinkType {
}
type SocialLinkProps = {
label: string;
href: string;
children?: React.ReactNode;
type: LinkType;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
};
export const SocialLink = ({
children,
href,
onClick,
type,
}: SocialLinkProps) => {
export const SocialLink = ({ label, href, onClick, type }: SocialLinkProps) => {
const displayValue =
getDisplayValueByUrlType({ type: type, href: href }) ?? children;
getDisplayValueByUrlType({ type: type, href: href }) ?? label;
return (
<RoundedLink href={href} onClick={onClick}>
{displayValue}
</RoundedLink>
);
return <RoundedLink href={href} onClick={onClick} label={displayValue} />;
};

View File

@ -11,7 +11,7 @@ const meta: Meta<typeof RoundedLink> = {
decorators: [ComponentWithRouterDecorator],
args: {
href: '/test',
children: 'Rounded chip',
label: 'Rounded chip',
},
};

View File

@ -11,7 +11,7 @@ const meta: Meta<typeof SocialLink> = {
decorators: [ComponentWithRouterDecorator],
args: {
href: '/test',
children: 'Social Link',
label: 'Social Link',
},
};
@ -25,7 +25,7 @@ const twitter: LinkType = LinkType.Twitter;
export const LinkedIn: Story = {
args: {
href: '/LinkedIn',
children: 'LinkedIn',
label: 'LinkedIn',
onClick: clickJestFn,
type: linkedin,
},
@ -34,7 +34,7 @@ export const LinkedIn: Story = {
export const Twitter: Story = {
args: {
href: '/Twitter',
children: 'Twitter',
label: 'Twitter',
onClick: clickJestFn,
type: twitter,
},

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { ThemeProvider } from '@emotion/react';
import { THEME_DARK, THEME_LIGHT } from 'twenty-ui';
import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui';
import { useColorScheme } from '../hooks/useColorScheme';
import { useSystemColorScheme } from '../hooks/useSystemColorScheme';
@ -24,5 +24,9 @@ export const AppThemeProvider = ({ children }: AppThemeProviderProps) => {
theme.name === 'dark' ? 'dark' : 'light';
}, [theme]);
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
return (
<ThemeProvider theme={theme}>
<ThemeContextProvider theme={theme}>{children}</ThemeContextProvider>
</ThemeProvider>
);
};

View File

@ -35,7 +35,6 @@ export const Default: Story = {
await canvas.findByText('People');
await canvas.findAllByText('Companies');
await canvas.findByText('Opportunities');
await canvas.findByText('Listings');
await canvas.findByText('My Customs');
},
};

View File

@ -8,11 +8,13 @@ import {
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { getPeopleMock } from '~/testing/mock-data/people';
import { mockedWorkspaceMemberData } from '~/testing/mock-data/users';
import { RecordShowPage } from '../RecordShowPage';
const peopleMock = getPeopleMock();
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/ObjectRecord/RecordShowPage',
component: RecordShowPage,
@ -29,7 +31,7 @@ const meta: Meta<PageDecoratorArgs> = {
graphql.query('FindOnePerson', () => {
return HttpResponse.json({
data: {
person: mockedPeopleData[0],
person: peopleMock[0],
},
});
}),
@ -87,7 +89,9 @@ export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findAllByText('Alexandre Prot');
await canvas.findAllByText(
peopleMock[0].name.firstName + ' ' + peopleMock[0].name.lastName,
);
await canvas.findByText('Add your first Activity');
},
};
@ -99,7 +103,11 @@ export const Loading: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.queryByText('Alexandre Prot')).toBeNull();
expect(
canvas.queryByText(
peopleMock[0].name.firstName + ' ' + peopleMock[0].name.lastName,
),
).toBeNull();
expect(canvas.queryByText('Add your first Activity')).toBeNull();
},
};

View File

@ -161,13 +161,13 @@ export const SettingsObjectNewFieldStep2 = () => {
const excludedFieldTypes: SettingsSupportedFieldType[] = (
[
FieldMetadataType.Email,
FieldMetadataType.FullName,
FieldMetadataType.Link,
// FieldMetadataType.Email,
// FieldMetadataType.FullName,
// FieldMetadataType.Link,
FieldMetadataType.Numeric,
FieldMetadataType.Probability,
FieldMetadataType.Uuid,
FieldMetadataType.Phone,
// FieldMetadataType.Uuid,
// FieldMetadataType.Phone,
] as const
).filter(isDefined);

View File

@ -2,7 +2,6 @@ import { useEffect } from 'react';
import { Decorator } from '@storybook/react';
import { useRecoilCallback } from 'recoil';
import { Company } from '@/companies/types/Company';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -12,18 +11,17 @@ import {
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { Person } from '@/people/types/Person';
import { mockedCompaniesDataV2 } from '~/testing/mock-data/companiesV2';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { mockPeopleDataV2 } from '~/testing/mock-data/peopleV2';
import { getPeopleMock } from '~/testing/mock-data/people';
import { isDefined } from '~/utils/isDefined';
const RecordMockSetterEffect = ({
companies,
people,
}: {
companies: Company[];
people: Person[];
companies: ObjectRecord[];
people: ObjectRecord[];
}) => {
const setRecordValue = useSetRecordValue();
@ -56,21 +54,25 @@ export const getFieldDecorator =
fieldValue?: any,
): Decorator =>
(Story) => {
const companiesMock = getCompaniesMock();
const companies =
objectNameSingular === 'company' && isDefined(fieldValue)
? [
{ ...mockedCompaniesDataV2[0], [fieldName]: fieldValue },
...mockedCompaniesDataV2.slice(1),
{ ...companiesMock[0], [fieldName]: fieldValue },
...companiesMock.slice(1),
]
: mockedCompaniesDataV2;
: companiesMock;
const peopleMock = getPeopleMock();
const people =
objectNameSingular === 'person' && isDefined(fieldValue)
? [
{ ...mockPeopleDataV2[0], [fieldName]: fieldValue },
...mockPeopleDataV2.slice(1),
{ ...peopleMock[0], [fieldName]: fieldValue },
...peopleMock.slice(1),
]
: mockPeopleDataV2;
: peopleMock;
const record = objectNameSingular === 'company' ? companies[0] : people[0];

View File

@ -8,20 +8,24 @@ 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,
mockedDuplicateCompanyData,
getCompaniesMock,
getCompanyDuplicateMock,
} from '~/testing/mock-data/companies';
import { mockedClientConfig } from '~/testing/mock-data/config';
import { mockedObjectMetadataItemsQueryResult } from '~/testing/mock-data/metadata';
import { getPeopleMock } from '~/testing/mock-data/people';
import { mockedRemoteTables } from '~/testing/mock-data/remote-tables';
import { mockedUsersData } from '~/testing/mock-data/users';
import { mockedViewsData } from '~/testing/mock-data/views';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
import { mockedPeopleData } from './mock-data/people';
import { mockedRemoteServers } from './mock-data/remote-servers';
import { mockedViewFieldsData } from './mock-data/view-fields';
const peopleMock = getPeopleMock();
const companiesMock = getCompaniesMock();
const duplicateCompanyMock = getCompanyDuplicateMock();
export const metadataGraphql = graphql.link(
`${REACT_APP_SERVER_BASE_URL}/metadata`,
);
@ -108,8 +112,8 @@ export const graphqlMocks = {
}),
graphql.query('FindManyCompanies', ({ variables }) => {
const mockedData = variables.limit
? mockedCompaniesData.slice(0, variables.limit)
: mockedCompaniesData;
? companiesMock.slice(0, variables.limit)
: companiesMock;
return HttpResponse.json({
data: {
@ -157,7 +161,7 @@ export const graphqlMocks = {
edges: [
{
node: {
...mockedDuplicateCompanyData,
...duplicateCompanyMock,
favorites: {
edges: [],
__typename: 'FavoriteConnection',
@ -197,7 +201,7 @@ export const graphqlMocks = {
return HttpResponse.json({
data: {
people: {
edges: mockedPeopleData.map((person) => ({
edges: peopleMock.map((person) => ({
node: person,
cursor: null,
})),

View File

@ -1,210 +1,393 @@
import { Company } from '@/companies/types/Company';
import { Favorite } from '@/favorites/types/Favorite';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { mockedUsersData } from './users';
type MockedCompany = Omit<Company, 'deletedAt'> & {
accountOwner: WorkspaceMember | null;
Favorite: Pick<Favorite, 'id'> | null;
export const getCompaniesMock = () => {
return companiesQueryResult.companies.edges.map((edge) => edge.node);
};
export const mockedCompaniesData: Array<MockedCompany> = [
{
export const getCompanyDuplicateMock = () => {
return {
...companiesQueryResult.companies.edges[0].node,
id: '8b40856a-2ec9-4c03-8bc0-c032c89e1824',
};
};
export const getEmptyCompanyMock = () => {
return {
id: '9231e6ee-4cc2-4c7b-8c55-dff16f4d968a',
name: '',
domainName: '',
address: '',
accountOwner: null,
createdAt: null,
updatedAt: null,
employees: null,
idealCustomerProfile: null,
linkedinLink: null,
xLink: null,
_activityCount: null,
__typename: 'Company',
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
domainName: 'airbnb.com',
name: 'Airbnb',
createdAt: '2023-04-26T10:08:54.724515+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
address: '17 rue de clignancourt',
employees: 12,
linkedinLink: {
url: 'https://www.linkedin.com/company/airbnb/',
label: 'https://www.linkedin.com/company/airbnb/',
};
};
export const companiesQueryResult = {
companies: {
__typename: 'CompanyConnection',
totalCount: 13,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
startCursor:
'WzEsICIyMDIwMjAyMC0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
endCursor: 'WzEzLCAiMjAyMDIwMjAtMTQ1NS00YzU3LWFmYWYtZGQ1ZGMwODYzNjFkIl0=',
},
xLink: {
url: 'https://twitter.com/airbnb',
label: 'https://twitter.com/airbnb',
},
annualRecurringRevenue: { amountMicros: 5000000, currencyCode: 'USD' },
idealCustomerProfile: true,
Favorite: null,
accountOwnerId: mockedUsersData[0].id,
accountOwner: {
__typename: 'WorkspaceMember',
name: {
firstName: 'Charles',
lastName: 'Test',
edges: [
{
__typename: 'CompanyEdge',
cursor: 'WzEsICIyMDIwMjAyMC0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
node: {
__typename: 'Company',
id: '20202020-3ec3-4fe3-8997-b76aa0bfa408',
employees: 100,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Linkedin',
accountOwner: null,
domainName: 'linkedin.com',
address: '',
position: 1,
idealCustomerProfile: true,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
previousEmployees: {
__typename: 'Person',
id: '20202020-2d40-4e49-8df4-9c6a049191de',
email: 'louis.duss@google.com',
position: 14,
testJson: null,
testRating: null,
companyId: '20202020-c21e-4ec2-873b-de4264d89025',
avatarUrl: '',
updatedAt: '2024-06-05T09:36:42.400Z',
testMultiSelect: null,
testBoolean: true,
testSelect: 'OPTION_1',
testDateOnly: null,
bestCompanyId: null,
testUuid: null,
phone: '+33788901234',
createdAt: '2024-06-05T09:00:20.412Z',
city: 'Seattle',
testPhone: '',
jobTitle: 'CTO',
testCurrency: {
__typename: 'Currency',
amountMicros: null,
currencyCode: 'USD',
},
xLink: {
__typename: 'Link',
label: '',
url: 'twitter.com',
},
testLinks: {
__typename: 'Links',
primaryLinkUrl: '',
primaryLinkLabel: '',
secondaryLinks: null,
},
name: {
__typename: 'FullName',
firstName: 'Louis',
lastName: 'Duss',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: 'linkedin.com',
},
testAddress: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
testLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
},
avatarUrl: null,
id: mockedUsersData[0].id,
locale: 'en',
colorScheme: 'Light',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
createdAt: '2023-04-26T10:23:42.33625+00:00',
userId: mockedUsersData[0].id,
userEmail: 'charles@test.com',
},
{
__typename: 'CompanyEdge',
cursor: 'WzIsICIyMDIwMjAyMC01ZDgxLTQ2ZDYtYmY4My1mN2ZkMzNlYTYxMDIiXQ==',
node: {
__typename: 'Company',
id: '20202020-5d81-46d6-bf83-f7fd33ea6102',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Facebook',
idealCustomerProfile: false,
accountOwner: null,
domainName: 'facebook.com',
address: '',
previousEmployees: null,
position: 2,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzMsICIyMDIwMjAyMC0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
node: {
__typename: 'Company',
id: '20202020-0713-40a5-8216-82802401d33e',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Qonto',
idealCustomerProfile: false,
accountOwner: null,
domainName: 'qonto.com',
address: '',
previousEmployees: null,
position: 3,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzQsICIyMDIwMjAyMC1lZDg5LTQxM2EtYjMxYS05NjI5ODZlNjdiYjQiXQ==',
node: {
__typename: 'Company',
id: '20202020-ed89-413a-b31a-962986e67bb4',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Microsoft',
idealCustomerProfile: true,
accountOwner: null,
domainName: 'microsoft.com',
address: '',
previousEmployees: null,
position: 4,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzUsICIyMDIwMjAyMC0xNzFlLTRiY2MtOWNmNy00MzQ0OGQ2ZmIyNzgiXQ==',
node: {
__typename: 'Company',
id: '20202020-171e-4bcc-9cf7-43448d6fb278',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Airbnb',
idealCustomerProfile: true,
accountOwner: null,
domainName: 'airbnb.com',
address: '',
previousEmployees: null,
position: 5,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzYsICIyMDIwMjAyMC1jMjFlLTRlYzItODczYi1kZTQyNjRkODkwMjUiXQ==',
node: {
__typename: 'Company',
id: '20202020-c21e-4ec2-873b-de4264d89025',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Google',
idealCustomerProfile: false,
accountOwner: null,
domainName: 'google.com',
address: '',
previousEmployees: null,
position: 6,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzcsICIyMDIwMjAyMC03MDdlLTQ0ZGMtYTFkMi0zMDAzMGJmMWE5NDQiXQ==',
node: {
__typename: 'Company',
id: '20202020-707e-44dc-a1d2-30030bf1a944',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Netflix',
idealCustomerProfile: true,
accountOwner: null,
domainName: 'netflix.com',
address: '',
previousEmployees: null,
position: 7,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzgsICIyMDIwMjAyMC0zZjc0LTQ5MmQtYTEwMS0yYTcwZjUwYTE2NDUiXQ==',
node: {
__typename: 'Company',
id: '20202020-3f74-492d-a101-2a70f50a1645',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Libeo',
idealCustomerProfile: false,
accountOwner: null,
domainName: 'libeo.io',
address: '',
previousEmployees: null,
position: 8,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzksICIyMDIwMjAyMC1jZmJmLTQxNTYtYTc5MC1lMzk4NTRkY2Q0ZWIiXQ==',
node: {
__typename: 'Company',
id: '20202020-cfbf-4156-a790-e39854dcd4eb',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Claap',
idealCustomerProfile: false,
accountOwner: null,
domainName: 'claap.io',
address: '',
previousEmployees: null,
position: 9,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzEwLCAiMjAyMDIwMjAtZjg2Yi00MTlmLWI3OTQtMDIzMTlhYmU4NjM3Il0=',
node: {
__typename: 'Company',
id: '20202020-f86b-419f-b794-02319abe8637',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Hasura',
idealCustomerProfile: false,
accountOwner: null,
domainName: 'hasura.io',
address: '',
previousEmployees: null,
position: 10,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzExLCAiMjAyMDIwMjAtNTUxOC00NTUzLTk0MzMtNDJkOGViODI4MzRiIl0=',
node: {
__typename: 'Company',
id: '20202020-5518-4553-9433-42d8eb82834b',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Wework',
idealCustomerProfile: false,
accountOwner: null,
domainName: 'wework.com',
address: '',
previousEmployees: null,
position: 11,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzEyLCAiMjAyMDIwMjAtZjc5ZS00MGRkLWJkMDYtYzM2ZTZhYmI0Njc4Il0=',
node: {
__typename: 'Company',
id: '20202020-f79e-40dd-bd06-c36e6abb4678',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Samsung',
idealCustomerProfile: false,
accountOwner: null,
domainName: 'samsung.com',
address: '',
previousEmployees: null,
position: 12,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzEzLCAiMjAyMDIwMjAtMTQ1NS00YzU3LWFmYWYtZGQ1ZGMwODYzNjFkIl0=',
node: {
__typename: 'Company',
id: '20202020-1455-4c57-afaf-dd5dc086361d',
employees: null,
createdAt: '2024-06-05T09:00:20.412Z',
name: 'Algolia',
idealCustomerProfile: false,
accountOwner: null,
domainName: 'algolia.com',
address: '',
previousEmployees: null,
position: 13,
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
},
],
},
{
__typename: 'Company',
id: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
domainName: 'aircall.io',
name: 'Aircall',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
address: '',
employees: 1,
accountOwnerId: null,
linkedinLink: {
url: 'https://www.linkedin.com/company/aircall/',
label: 'https://www.linkedin.com/company/aircall/',
},
xLink: {
url: 'https://twitter.com/aircall',
label: 'https://twitter.com/aircall',
},
annualRecurringRevenue: { amountMicros: 500000, currencyCode: 'USD' },
idealCustomerProfile: false,
accountOwner: null,
Favorite: null,
},
{
__typename: 'Company',
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
domainName: 'algolia.com',
name: 'Algolia',
createdAt: '2023-04-26T10:10:32.530184+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
address: '',
employees: 1,
linkedinLink: {
url: 'https://www.linkedin.com/company/algolia/',
label: 'https://www.linkedin.com/company/algolia/',
},
xLink: {
url: 'https://twitter.com/algolia',
label: 'https://twitter.com/algolia',
},
annualRecurringRevenue: { amountMicros: 5000000, currencyCode: 'USD' },
idealCustomerProfile: true,
accountOwner: null,
Favorite: null,
accountOwnerId: null,
},
{
__typename: 'Company',
id: 'b1cfd51b-a831-455f-ba07-4e30671e1dc3',
domainName: 'apple.com',
name: 'Apple',
createdAt: '2023-03-21T06:30:25.39474+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
address: '',
employees: 10,
linkedinLink: {
url: 'https://www.linkedin.com/company/apple/',
label: 'https://www.linkedin.com/company/apple/',
},
xLink: {
url: 'https://twitter.com/apple',
label: 'https://twitter.com/apple',
},
annualRecurringRevenue: { amountMicros: 1000000, currencyCode: 'USD' },
idealCustomerProfile: false,
accountOwner: null,
Favorite: null,
accountOwnerId: null,
},
{
__typename: 'Company',
id: '5c21e19e-e049-4393-8c09-3e3f8fb09ecb',
domainName: 'qonto.com',
name: 'Qonto',
createdAt: '2023-04-26T10:13:29.712485+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
address: '10 rue de la Paix',
employees: 1,
linkedinLink: {
url: 'https://www.linkedin.com/company/qonto/',
label: 'https://www.linkedin.com/company/qonto/',
},
xLink: {
url: 'https://twitter.com/qonto',
label: 'https://twitter.com/qonto',
},
annualRecurringRevenue: { amountMicros: 5000000, currencyCode: 'USD' },
idealCustomerProfile: false,
accountOwner: null,
Favorite: null,
accountOwnerId: null,
},
{
__typename: 'Company',
id: '9d162de6-cfbf-4156-a790-e39854dcd4eb',
domainName: 'facebook.com',
name: 'Facebook',
createdAt: '2023-04-26T10:09:25.656555+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
address: '',
employees: 1,
linkedinLink: {
url: 'https://www.linkedin.com/company/facebook/',
label: 'https://www.linkedin.com/company/facebook/',
},
xLink: {
url: 'https://twitter.com/facebook',
label: 'https://twitter.com/facebook',
},
annualRecurringRevenue: { amountMicros: 5000000, currencyCode: 'USD' },
idealCustomerProfile: true,
accountOwner: null,
Favorite: null,
accountOwnerId: null,
},
{
__typename: 'Company',
id: '9d162de6-cfbf-4156-a790-e39854dcd4ef',
domainName: 'sequoia.com',
name: 'Sequoia',
createdAt: '2023-04-26T10:09:25.656555+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
address: '',
employees: 1,
linkedinLink: {
url: 'https://www.linkedin.com/company/sequoia/',
label: 'https://www.linkedin.com/company/sequoia/',
},
xLink: {
url: 'https://twitter.com/sequoia',
label: 'https://twitter.com/sequoia',
},
annualRecurringRevenue: { amountMicros: 5000000, currencyCode: 'USD' },
idealCustomerProfile: true,
accountOwner: null,
Favorite: null,
accountOwnerId: null,
},
];
export const mockedDuplicateCompanyData: MockedCompany = {
...mockedCompaniesData[0],
id: '8b40856a-2ec9-4c03-8bc0-c032c89e1824',
};
export const mockedEmptyCompanyData = {
id: '9231e6ee-4cc2-4c7b-8c55-dff16f4d968a',
name: '',
domainName: '',
address: '',
accountOwner: null,
annualRecurringRevenue: null,
createdAt: null,
updatedAt: null,
employees: null,
idealCustomerProfile: null,
linkedinLink: null,
xLink: null,
_activityCount: null,
__typename: 'Company',
};

View File

@ -1,493 +0,0 @@
import { Company } from '@/companies/types/Company';
import { Favorite } from '@/favorites/types/Favorite';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { mockedCompaniesData } from '~/testing/mock-data/companies';
type MockedCompanyV2 = Omit<Company, 'deletedAt'> & {
accountOwner: WorkspaceMember | null;
Favorite?: Pick<Favorite, 'id'> | null;
};
export const mockedCompaniesDataV2: Array<MockedCompanyV2> = [
{
__typename: 'Company',
domainName: 'paris.com',
name: 'Test',
employees: null,
address: 'Paris France',
createdAt: '2024-05-27T11:23:05.954Z',
id: 'd55c240e-e4e0-4248-b56d-8004d1218a9c',
position: 6.109375,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 1000000000,
currencyCode: 'USD',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: 'paris.com',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-1553-45c6-a028-5a9064cce07f',
colorScheme: 'Light',
updatedAt: '2024-05-01T13:16:29.046Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-7169-42cf-bc47-1cfef15264b8',
userEmail: 'phil.schiler@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Phil',
lastName: 'Shiler',
},
},
},
{
__typename: 'Company',
domainName: 'google.com',
name: 'Google',
employees: 10202,
address: 'Paris France',
createdAt: '2024-05-21T13:16:29.000Z',
id: '20202020-c21e-4ec2-873b-de4264d89025',
position: 7.5,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 1001000000,
currencyCode: 'USD',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-0687-4c41-b707-ed1bfca972a7',
colorScheme: 'Light',
updatedAt: '2024-05-30T09:00:31.127Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
userEmail: 'tim@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Tim',
lastName: 'Apple',
},
},
},
{
__typename: 'Company',
domainName: 'hasura.io',
name: 'Hasura',
employees: 102938102938,
address: '',
createdAt: '2024-05-16T13:16:29.000Z',
id: '20202020-f86b-419f-b794-02319abe8637',
position: 10,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-77d5-4cb6-b60a-f4a835a85d61',
colorScheme: 'Light',
updatedAt: '2024-05-01T13:16:29.046Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-3957-4908-9c36-2929a23f8357',
userEmail: 'jony.ive@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Jony',
lastName: 'Ive',
},
},
},
{
__typename: 'Company',
domainName: 'netflix.com',
name: 'Netflix',
employees: null,
address: '',
createdAt: '2024-05-15T13:16:29.000Z',
id: '20202020-707e-44dc-a1d2-30030bf1a944',
position: 7,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 2000000000,
currencyCode: 'USD',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-1553-45c6-a028-5a9064cce07f',
colorScheme: 'Light',
updatedAt: '2024-05-01T13:16:29.046Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-7169-42cf-bc47-1cfef15264b8',
userEmail: 'phil.schiler@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Phil',
lastName: 'Shiler',
},
},
},
{
__typename: 'Company',
domainName: 'claap.io',
name: 'Claap',
employees: 2131920,
address: 'asdasd',
createdAt: '2024-05-10T13:16:29.000Z',
id: '20202020-cfbf-4156-a790-e39854dcd4eb',
position: 9,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: 'asmdlkasd',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-0687-4c41-b707-ed1bfca972a7',
colorScheme: 'Light',
updatedAt: '2024-05-30T09:00:31.127Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
userEmail: 'tim@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Tim',
lastName: 'Apple',
},
},
},
{
__typename: 'Company',
domainName: 'libeo.io',
name: 'Libeo',
employees: 1239819238,
address: '',
createdAt: '2024-05-09T13:16:29.000Z',
id: '20202020-3f74-492d-a101-2a70f50a1645',
position: 8,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-1553-45c6-a028-5a9064cce07f',
colorScheme: 'Light',
updatedAt: '2024-05-01T13:16:29.046Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-7169-42cf-bc47-1cfef15264b8',
userEmail: 'phil.schiler@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Phil',
lastName: 'Shiler',
},
},
},
{
__typename: 'Company',
domainName: 'qonto.com',
name: 'Qonto',
employees: 123123123,
address: '',
createdAt: '2024-05-08T13:16:29.000Z',
id: '20202020-0713-40a5-8216-82802401d33e',
position: 9.5,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-1553-45c6-a028-5a9064cce07f',
colorScheme: 'Light',
updatedAt: '2024-05-01T13:16:29.046Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-7169-42cf-bc47-1cfef15264b8',
userEmail: 'phil.schiler@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Phil',
lastName: 'Shiler',
},
},
},
{
__typename: 'Company',
domainName: 'wework.com',
name: 'Wework',
employees: 123123123,
address: '',
createdAt: '2024-05-08T13:16:29.000Z',
id: '20202020-5518-4553-9433-42d8eb82834b',
position: 11,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-0687-4c41-b707-ed1bfca972a7',
colorScheme: 'Light',
updatedAt: '2024-05-30T09:00:31.127Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
userEmail: 'tim@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Tim',
lastName: 'Apple',
},
},
},
{
__typename: 'Company',
domainName: 'linkedin.com',
name: 'Linkedin',
employees: 10102,
accountOwner: null,
address: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-3ec3-4fe3-8997-b76aa0bfa408',
position: 1,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: 'adasd',
url: 'adasd',
},
},
{
__typename: 'Company',
domainName: 'airbnb.com',
name: 'Airbnb',
employees: 123333,
accountOwner: null,
address: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-171e-4bcc-9cf7-43448d6fb278',
position: 5,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
},
{
__typename: 'Company',
domainName: 'samsung.com',
name: 'Samsung',
employees: 10000,
address: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-f79e-40dd-bd06-c36e6abb4678',
position: 12,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-1553-45c6-a028-5a9064cce07f',
colorScheme: 'Light',
updatedAt: '2024-05-01T13:16:29.046Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-7169-42cf-bc47-1cfef15264b8',
userEmail: 'phil.schiler@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Phil',
lastName: 'Shiler',
},
},
},
{
__typename: 'Company',
domainName: 'algolia.com',
name: 'Algolia',
employees: 10000,
address: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-1455-4c57-afaf-dd5dc086361d',
position: 13,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-77d5-4cb6-b60a-f4a835a85d61',
colorScheme: 'Light',
updatedAt: '2024-05-01T13:16:29.046Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-3957-4908-9c36-2929a23f8357',
userEmail: 'jony.ive@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Jony',
lastName: 'Ive',
},
},
},
{
__typename: 'Company',
domainName: 'facebook.com',
name: 'Facebook',
employees: 220323,
accountOwner: null,
address: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-5d81-46d6-bf83-f7fd33ea6102',
position: 6.0625,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: 'asdasd',
},
},
{
__typename: 'Company',
domainName: 'microsoft.com',
name: 'Microsoft',
employees: 10000,
address: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-ed89-413a-b31a-962986e67bb4',
position: 6.09375,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 10000000000,
currencyCode: 'USD',
},
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
},
accountOwner: {
__typename: 'WorkspaceMember',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-0687-4c41-b707-ed1bfca972a7',
colorScheme: 'Light',
updatedAt: '2024-05-30T09:00:31.127Z',
locale: 'en',
avatarUrl: '',
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
userEmail: 'tim@apple.dev',
name: {
__typename: 'FullName',
firstName: 'Tim',
lastName: 'Apple',
},
},
},
];
export const mockedDuplicateCompanyData: MockedCompanyV2 = {
...mockedCompaniesData[0],
id: '8b40856a-2ec9-4c03-8bc0-c032c89e1824',
};
export const mockedEmptyCompanyData = {
id: '9231e6ee-4cc2-4c7b-8c55-dff16f4d968a',
name: '',
domainName: '',
address: '',
accountOwner: null,
annualRecurringRevenue: null,
createdAt: null,
updatedAt: null,
employees: null,
idealCustomerProfile: null,
linkedinLink: null,
xLink: null,
_activityCount: null,
__typename: 'Company',
};

View File

@ -1,8 +1,4 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/standard-metadata-query-result';
export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
@ -10,431 +6,3 @@ export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
...edge.node,
fields: edge.node.fields.edges.map((edge) => edge.node),
}));
export const mockObjectMetadataItem: ObjectMetadataItem = {
__typename: 'object',
id: 'b79a038c-b06b-4a5a-b7ee-f8ba412aa1c0',
nameSingular: 'company',
namePlural: 'companies',
labelSingular: 'Company',
labelPlural: 'Companies',
description: 'A company',
icon: 'IconBuildingSkyscraper',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
fields: [
{
__typename: 'field',
id: '390eb5e5-d8d1-4064-bf75-3461251eb142',
type: FieldMetadataType.Boolean,
name: 'idealCustomerProfile',
label: 'ICP',
description:
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
icon: 'IconTarget',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: '72a43010-f236-4fa2-8ac4-a31e6b37d692',
type: FieldMetadataType.Relation,
name: 'people',
label: 'People',
description: 'People linked to the company.',
icon: 'IconUsers',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: {
id: 'f08943fe-e8a0-4747-951c-c3b391842453',
relationType: RelationMetadataType.OneToMany,
toObjectMetadata: {
id: 'fcccc985-5edf-405c-aa2b-80c82b230f35',
nameSingular: 'person',
namePlural: 'people',
isSystem: false,
isRemote: false,
},
toFieldMetadataId: 'c756f6ff-8c00-4fe5-a923-c6cfc7b1ac4a',
},
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: '51636fba-1bd9-4344-bba8-9639cbc8e134',
type: FieldMetadataType.Relation,
name: 'opportunities',
label: 'Opportunities',
description: 'Opportunities linked to the company.',
icon: 'IconTargetArrow',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: {
id: '7ffae8bb-b12b-4ad9-8922-da0d517b5612',
relationType: RelationMetadataType.OneToMany,
toObjectMetadata: {
id: '169e5b21-dc95-44a8-acd0-5e9447dd0784',
nameSingular: 'opportunity',
namePlural: 'opportunities',
isSystem: false,
isRemote: false,
},
toFieldMetadataId: '00468e2a-a601-4635-ae9c-a9bb826cc860',
},
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: 'd541f76b-d327-4dda-8ef8-81b60e5ad01e',
type: FieldMetadataType.Relation,
name: 'activityTargets',
label: 'Activities',
description: 'Activities tied to the company',
icon: 'IconCheckbox',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: {
id: 'bc42672b-350f-45c3-bd1f-4debb536ccd1',
relationType: RelationMetadataType.OneToMany,
toObjectMetadata: {
id: 'b87c6cac-a8e7-4156-a525-30ec536acd75',
nameSingular: 'activityTarget',
namePlural: 'activityTargets',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: 'bba19feb-c248-487b-92d7-98df54c51e44',
},
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: 'dacb7562-497e-4080-8ef5-746d6786ed49',
type: FieldMetadataType.DateTime,
name: 'createdAt',
label: 'Creation date',
description: null,
icon: 'IconCalendar',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: false,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
type: 'now',
},
},
{
__typename: 'field',
id: 'f3b4ff22-800b-4f13-8262-8003da8eed5b',
type: FieldMetadataType.Number,
name: 'employees',
label: 'Employees',
description: 'Number of employees in the company',
icon: 'IconUsers',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: 'c3e64012-32cc-43f1-af2f-33b37cc4e59d',
type: FieldMetadataType.Link,
name: 'linkedinLink',
label: 'Linkedin',
description: 'The company Linkedin account',
icon: 'IconBrandLinkedin',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: 'fced9acc-0374-487d-9da4-579a17435df0',
type: FieldMetadataType.Link,
name: 'xLink',
label: 'X',
description: 'The company Twitter/X account',
icon: 'IconBrandX',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: '63db0a2f-ffb4-4ea1-98c7-f7e13ce75c38',
type: FieldMetadataType.Relation,
name: 'attachments',
label: 'Attachments',
description: 'Attachments linked to the company.',
icon: 'IconFileImport',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: {
id: '901fd405-c6bf-4559-9d1f-d0937b6f16d9',
relationType: RelationMetadataType.OneToMany,
toObjectMetadata: {
id: '77240b4b-6bcf-454d-a102-19bbba181716',
nameSingular: 'attachment',
namePlural: 'attachments',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '0880dac5-37d2-43a6-b143-722126d4923f',
},
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: 'e775ce12-87c0-4feb-bcfe-9af3d8ca117b',
type: FieldMetadataType.Uuid,
name: 'id',
label: 'Id',
description: null,
icon: null,
isCustom: false,
isActive: true,
isSystem: true,
isNullable: false,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
type: 'uuid',
},
},
{
__typename: 'field',
id: '2278ef91-3d6a-45cf-86f5-76b7bfa2bf32',
type: FieldMetadataType.Text,
name: 'domainName',
label: 'Domain Name',
description:
'The company website URL. We use this url to fetch the company icon',
icon: 'IconLink',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
value: '',
},
},
{
__typename: 'field',
id: '438291d7-18f4-48cf-8dca-05e96c5a0765',
type: FieldMetadataType.Currency,
name: 'annualRecurringRevenue',
label: 'ARR',
description:
'Annual Recurring Revenue: The actual or estimated annual revenue of the company',
icon: 'IconMoneybag',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: 'edb8475f-03fc-4ac1-9305-e9d4e2dacd11',
type: FieldMetadataType.DateTime,
name: 'updatedAt',
label: 'Update date',
description: null,
icon: 'IconCalendar',
isCustom: false,
isActive: true,
isSystem: true,
isNullable: false,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
type: 'now',
},
},
{
__typename: 'field',
id: 'e3c9ba7f-cecf-4ac6-a7b9-7a9987be0253',
type: FieldMetadataType.Relation,
name: 'accountOwner',
label: 'Account Owner',
description:
'Your team member responsible for managing the company account',
icon: 'IconUserCircle',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: {
id: '0317d74c-5187-491f-9e1d-d22f06ca2a38',
relationType: RelationMetadataType.OneToMany,
fromObjectMetadata: {
id: '92c306ce-ad06-4712-99d2-5d0daf13c95f',
nameSingular: 'workspaceMember',
namePlural: 'workspaceMembers',
isSystem: true,
isRemote: false,
},
fromFieldMetadataId: '0f3e456f-3bb4-4261-a436-95246dc0e159',
},
defaultValue: null,
},
{
__typename: 'field',
id: 'a34bd3b3-6949-4793-bac6-d2c054639c7f',
type: FieldMetadataType.Text,
name: 'address',
label: 'Address',
description: 'The company address',
icon: 'IconMap',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
value: '',
},
},
{
__typename: 'field',
id: '4b204845-f1fc-4fd8-8fdd-f4caeaab749f',
type: FieldMetadataType.Relation,
name: 'favorites',
label: 'Favorites',
description: 'Favorites linked to the company',
icon: 'IconHeart',
isCustom: false,
isActive: true,
isSystem: true,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: {
id: '8e0d3aa1-6135-4d65-aa28-15a5b6d1619c',
relationType: RelationMetadataType.OneToMany,
toObjectMetadata: {
id: '1415392e-0ecb-462e-aa67-001e424e6a37',
nameSingular: 'favorite',
namePlural: 'favorites',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '8fd8965b-bd4e-4a9b-90e9-c75652dadda1',
},
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: 'a795e81e-0bcf-4fd6-8f2f-b3764b990d2d',
type: FieldMetadataType.Uuid,
name: 'accountOwnerId',
label: 'Account Owner id (foreign key)',
description:
'Your team member responsible for managing the company account id foreign key',
icon: 'IconUserCircle',
isCustom: false,
isActive: true,
isSystem: true,
isNullable: true,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
},
{
__typename: 'field',
id: '87887d23-f632-4d3e-840a-02fcee960660',
type: FieldMetadataType.Text,
name: 'name',
label: 'Name',
description: 'The company name',
icon: 'IconBuildingSkyscraper',
isCustom: false,
isActive: true,
isSystem: false,
isNullable: false,
createdAt: '2023-12-19T12:15:28.459Z',
updatedAt: '2023-12-19T12:15:28.459Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
value: '',
},
},
],
};

File diff suppressed because it is too large Load Diff

View File

@ -1,533 +0,0 @@
import { Company } from '@/companies/types/Company';
import { Person } from '@/people/types/Person';
export type MockedPersonV2 = Pick<
Person,
| '__typename'
| 'id'
| 'name'
| 'linkedinLink'
| 'xLink'
| 'links'
| 'jobTitle'
| 'email'
| 'phone'
| 'city'
| 'avatarUrl'
| 'createdAt'
| 'updatedAt'
| 'companyId'
| 'position'
> & {
company?: Company;
};
export const mockPeopleDataV2: MockedPersonV2[] = [
{
__typename: 'Person',
city: 'Seattle',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-1c0e-494c-a1b6-85b1c6fefaa5',
email: 'christoph.calisto@linkedin.com',
phone: '+33789012345',
position: 1,
name: {
__typename: 'FullName',
firstName: 'Christoph',
lastName: 'Callisto',
},
linkedinLink: { __typename: 'Link', label: '', url: 'asd' },
xLink: { __typename: 'Link', label: '', url: 'asd' },
company: {
__typename: 'Company',
domainName: 'linkedin.com',
name: 'Linkedin',
employees: 10102,
accountOwnerId: null,
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-3ec3-4fe3-8997-b76aa0bfa408',
position: 1,
updatedAt: '2024-05-23T13:21:41.159Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: { __typename: 'Link', label: 'adasd', url: 'adasd' },
},
},
{
__typename: 'Person',
city: 'Los Angeles',
jobTitle: '@',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-ac73-4797-824e-87a1f5aea9e0',
email: 'sylvie.palmer@linkedin.com',
phone: '+33780123456',
position: 2,
name: { __typename: 'FullName', firstName: 'Sylvie', lastName: 'Palmer' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'algolia.com',
name: 'Algolia',
employees: 10000,
accountOwnerId: '20202020-77d5-4cb6-b60a-f4a835a85d61',
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-1455-4c57-afaf-dd5dc086361d',
position: 13,
updatedAt: '2024-05-28T15:52:31.839Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'Seattle',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-f517-42fd-80ae-14173b3b70ae',
email: 'christopher.gonzalez@qonto.com',
phone: '+33789012345',
position: 3,
name: {
__typename: 'FullName',
firstName: 'Christopher',
lastName: 'Gonzalez',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'qonto.com',
name: 'Qonto',
employees: 123123123,
accountOwnerId: '20202020-1553-45c6-a028-5a9064cce07f',
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-08T13:16:29.000Z',
id: '20202020-0713-40a5-8216-82802401d33e',
position: 9.5,
updatedAt: '2024-05-28T15:52:46.961Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'Los Angeles',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-eee1-4690-ad2c-8619e5b56a2e',
email: 'ashley.parker@qonto.com',
phone: '+33780123456',
position: 4,
name: { __typename: 'FullName', firstName: 'Ashley', lastName: 'Parker' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'qonto.com',
name: 'Qonto',
employees: 123123123,
accountOwnerId: '20202020-1553-45c6-a028-5a9064cce07f',
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-08T13:16:29.000Z',
id: '20202020-0713-40a5-8216-82802401d33e',
position: 9.5,
updatedAt: '2024-05-28T15:52:46.961Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'Seattle',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-6784-4449-afdf-dc62cb8702f2',
email: 'nicholas.wright@microsoft.com',
phone: '+33781234567',
position: 5,
name: { __typename: 'FullName', firstName: 'Nicholas', lastName: 'Wright' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'microsoft.com',
name: 'Microsoft',
employees: 10000,
accountOwnerId: '20202020-0687-4c41-b707-ed1bfca972a7',
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-ed89-413a-b31a-962986e67bb4',
position: 6.09375,
updatedAt: '2024-05-28T15:52:35.621Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 10000000000,
currencyCode: 'USD',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'New York',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-490f-4466-8391-733cfd66a0c8',
email: 'isabella.scott@microsoft.com',
phone: '+33782345678',
position: 6,
name: { __typename: 'FullName', firstName: 'Isabella', lastName: 'Scott' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'microsoft.com',
name: 'Microsoft',
employees: 10000,
accountOwnerId: '20202020-0687-4c41-b707-ed1bfca972a7',
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-ed89-413a-b31a-962986e67bb4',
position: 6.09375,
updatedAt: '2024-05-28T15:52:35.621Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 10000000000,
currencyCode: 'USD',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'Seattle',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-80f1-4dff-b570-a74942528de3',
email: 'matthew.green@microsoft.com',
phone: '+33783456789',
position: 7,
name: { __typename: 'FullName', firstName: 'Matthew', lastName: 'Green' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'microsoft.com',
name: 'Microsoft',
employees: 10000,
accountOwnerId: '20202020-0687-4c41-b707-ed1bfca972a7',
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-ed89-413a-b31a-962986e67bb4',
position: 6.09375,
updatedAt: '2024-05-28T15:52:35.621Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 10000000000,
currencyCode: 'USD',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'New York',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-338b-46df-8811-fa08c7d19d35',
email: 'elizabeth.baker@airbnb.com',
phone: '+33784567890',
position: 8,
name: { __typename: 'FullName', firstName: 'Elizabeth', lastName: 'Baker' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'airbnb.com',
name: 'Airbnb',
employees: 123333,
accountOwnerId: null,
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-171e-4bcc-9cf7-43448d6fb278',
position: 5,
updatedAt: '2024-05-28T15:52:27.902Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'San Francisco',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-64ad-4b0e-bbfd-e9fd795b7016',
email: 'christopher.nelson@airbnb.com',
phone: '+33785678901',
position: 9,
name: {
__typename: 'FullName',
firstName: 'Christopher',
lastName: 'Nelson',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'airbnb.com',
name: 'Airbnb',
employees: 123333,
accountOwnerId: null,
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-171e-4bcc-9cf7-43448d6fb278',
position: 5,
updatedAt: '2024-05-28T15:52:27.902Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'New York',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-5d54-41b7-ba36-f0d20e1417ae',
email: 'avery.carter@airbnb.com',
phone: '+33786789012',
position: 10,
name: { __typename: 'FullName', firstName: 'Avery', lastName: 'Carter' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'airbnb.com',
name: 'Airbnb',
employees: 123333,
accountOwnerId: null,
address: '',
idealCustomerProfile: false,
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-171e-4bcc-9cf7-43448d6fb278',
position: 5,
updatedAt: '2024-05-28T15:52:27.902Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'Los Angeles',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-623d-41fe-92e7-dd45b7c568e1',
email: 'ethan.mitchell@google.com',
phone: '+33787890123',
position: 11,
name: { __typename: 'FullName', firstName: 'Ethan', lastName: 'Mitchell' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'google.com',
name: 'Google',
employees: 10202,
accountOwnerId: '20202020-0687-4c41-b707-ed1bfca972a7',
address: 'Paris France',
idealCustomerProfile: false,
createdAt: '2024-05-21T13:16:29.000Z',
id: '20202020-c21e-4ec2-873b-de4264d89025',
position: 7.5,
updatedAt: '2024-05-28T15:53:28.838Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 1001000000,
currencyCode: 'USD',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'Seattle',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-2d40-4e49-8df4-9c6a049190ef',
email: 'madison.perez@google.com',
phone: '+33788901234',
position: 12,
name: { __typename: 'FullName', firstName: 'Madison', lastName: 'Perez' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'google.com',
name: 'Google',
employees: 10202,
accountOwnerId: '20202020-0687-4c41-b707-ed1bfca972a7',
address: 'Paris France',
idealCustomerProfile: false,
createdAt: '2024-05-21T13:16:29.000Z',
id: '20202020-c21e-4ec2-873b-de4264d89025',
position: 7.5,
updatedAt: '2024-05-28T15:53:28.838Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 1001000000,
currencyCode: 'USD',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'Seattle',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-2d40-4e49-8df4-9c6a049190df',
email: 'bertrand.voulzy@google.com',
phone: '+33788901234',
position: 13,
name: { __typename: 'FullName', firstName: 'Bertrand', lastName: 'Voulzy' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'google.com',
name: 'Google',
employees: 10202,
accountOwnerId: '20202020-0687-4c41-b707-ed1bfca972a7',
address: 'Paris France',
idealCustomerProfile: false,
createdAt: '2024-05-21T13:16:29.000Z',
id: '20202020-c21e-4ec2-873b-de4264d89025',
position: 7.5,
updatedAt: '2024-05-28T15:53:28.838Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 1001000000,
currencyCode: 'USD',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'Seattle',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-2d40-4e49-8df4-9c6a049191de',
email: 'louis.duss@google.com',
phone: '+33788901234',
position: 14,
name: { __typename: 'FullName', firstName: 'Louis', lastName: 'Duss' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'google.com',
name: 'Google',
employees: 10202,
accountOwnerId: '20202020-0687-4c41-b707-ed1bfca972a7',
address: 'Paris France',
idealCustomerProfile: false,
createdAt: '2024-05-21T13:16:29.000Z',
id: '20202020-c21e-4ec2-873b-de4264d89025',
position: 7.5,
updatedAt: '2024-05-28T15:53:28.838Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 1001000000,
currencyCode: 'USD',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
{
__typename: 'Person',
city: 'Seattle',
jobTitle: '',
createdAt: '2024-05-01T13:16:29.046Z',
id: '20202020-2d40-4e49-8df4-9c6a049191df',
email: 'lorie.vladim@google.com',
phone: '+33788901235',
position: 15,
name: { __typename: 'FullName', firstName: 'Lorie', lastName: 'Vladim' },
linkedinLink: { __typename: 'Link', label: '', url: '' },
xLink: { __typename: 'Link', label: '', url: '' },
company: {
__typename: 'Company',
domainName: 'google.com',
name: 'Google',
employees: 10202,
accountOwnerId: '20202020-0687-4c41-b707-ed1bfca972a7',
address: 'Paris France',
idealCustomerProfile: false,
createdAt: '2024-05-21T13:16:29.000Z',
id: '20202020-c21e-4ec2-873b-de4264d89025',
position: 7.5,
updatedAt: '2024-05-28T15:53:28.838Z',
xLink: { __typename: 'Link', label: '', url: '' },
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 1001000000,
currencyCode: 'USD',
},
linkedinLink: { __typename: 'Link', label: '', url: '' },
},
},
];

View File

@ -2,6 +2,9 @@
import { isDate, isNumber, isString } from '@sniptt/guards';
import { differenceInCalendarDays, formatDistanceToNow } from 'date-fns';
import { DateTime } from 'luxon';
import moize from 'moize';
import { isDefined } from '~/utils/isDefined';
import { logError } from './logError';
@ -133,3 +136,75 @@ export const beautifyDateDiff = (
if (![0, 1].includes(dateDiff.days)) result = result + 's';
return result;
};
const getMonthLabels = () => {
const formatter = new Intl.DateTimeFormat(undefined, {
month: 'short',
timeZone: 'UTC',
});
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
.map((month) => {
const monthZeroFilled = month < 10 ? `0${month}` : month;
return new Date(`2017-${monthZeroFilled}-01T00:00:00+00:00`);
})
.map((date) => formatter.format(date));
};
const getMonthLabelsMemoized = moize(getMonthLabels);
export const formatISOStringToHumanReadableDateTime = (date: string) => {
const monthLabels = getMonthLabelsMemoized();
if (!isDefined(monthLabels)) {
return formatToHumanReadableDateTime(date);
}
const year = date.slice(0, 4);
const month = date.slice(5, 7);
const day = date.slice(8, 10);
const monthLabel = monthLabels[parseInt(month, 10) - 1];
const jsDate = new Date(date);
return `${day} ${monthLabel} ${year} - ${jsDate.getHours()}:${jsDate.getMinutes()}`;
};
export const formatISOStringToHumanReadableDate = (date: string) => {
const monthLabels = getMonthLabelsMemoized();
if (!isDefined(monthLabels)) {
return formatToHumanReadableDate(date);
}
const year = date.slice(0, 4);
const month = date.slice(5, 7);
const day = date.slice(8, 10);
const monthLabel = monthLabels[parseInt(month, 10) - 1];
return `${day} ${monthLabel} ${year}`;
};
export const formatToHumanReadableDate = (date: Date | string) => {
const parsedJSDate = parseDate(date).toJSDate();
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(parsedJSDate);
};
export const formatToHumanReadableDateTime = (date: Date | string) => {
const parsedJSDate = parseDate(date).toJSDate();
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
}).format(parsedJSDate);
};

View File

@ -1,27 +1,3 @@
import { parseDate } from './date-utils';
export const formatToHumanReadableDate = (date: Date | string) => {
const parsedJSDate = parseDate(date).toJSDate();
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(parsedJSDate);
};
export const formatToHumanReadableDateTime = (date: Date | string) => {
const parsedJSDate = parseDate(date).toJSDate();
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
}).format(parsedJSDate);
};
export const sanitizeURL = (link: string | null | undefined) => {
return link
? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '')

View File

@ -49,8 +49,21 @@ export default defineConfig(({ command, mode }) => {
}),
svgr(),
checker(checkers),
// TODO: fix this, we have to restrict the include to only the components that are using linaria
// Otherwise the build will fail because wyw tries to include emotion styled components
wyw({
include: ['**/EllipsisDisplay.tsx', '**/ContactLink.tsx'],
include: [
'**/CurrencyDisplay.tsx',
'**/EllipsisDisplay.tsx',
'**/ContactLink.tsx',
'**/BooleanDisplay.tsx',
'**/LinksDisplay.tsx',
'**/RoundedLink.tsx',
'**/OverflowingTextWithTooltip.tsx',
'**/Chip.tsx',
'**/Tag.tsx',
'**/MultiSelectFieldDisplay.tsx',
],
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
},

View File

@ -3,7 +3,11 @@ import { ThemeProvider } from '@emotion/react';
import { Preview } from '@storybook/react';
import { useDarkMode } from 'storybook-dark-mode';
import { THEME_DARK, THEME_LIGHT } from '../src/theme/index';
import {
THEME_DARK,
THEME_LIGHT,
ThemeContextProvider,
} from '../src/theme/index';
const preview: Preview = {
decorators: [
@ -18,7 +22,9 @@ const preview: Preview = {
return (
<ThemeProvider theme={theme}>
<Story />
<ThemeContextProvider theme={theme}>
<Story />
</ThemeContextProvider>
</ThemeProvider>
);
},

View File

@ -1,84 +0,0 @@
.label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chip {
--chip-horizontal-padding: calc(var(--twentycrm-spacing-multiplicator) * 1px);
--chip-vertical-padding: calc(var(--twentycrm-spacing-multiplicator) * 1px);
align-items: center;
border-radius: var(--twentycrm-border-radius-sm);
color: var(--twentycrm-font-color-secondary);
display: inline-flex;
justify-content: center;
gap: calc(var(--twentycrm-spacing-multiplicator) * 1px);
height: calc(var(--twentycrm-spacing-multiplicator) * 3px);
max-width: calc(100% - var(--chip-horizontal-padding) * 2px);
overflow: hidden;
padding: var(--chip-vertical-padding) var(--chip-horizontal-padding);
user-select: none;
}
.disabled {
cursor: not-allowed;
color: var(--twentycrm-font-color-light);
}
.clickable {
cursor: pointer;
}
.accent-text-primary {
color: var(--twentycrm-font-color-primary);
}
.accent-text-secondary {
font-weight: var(--twentycrm-font-weight-medium);
}
.size-large {
height: calc(var(--twentycrm-spacing-multiplicator) * 4px);
}
.variant-regular:hover {
background-color: var(--twentycrm-background-transparent-light);
}
.variant-regular:active {
background-color: var(--twentycrm-background-transparent-medium);
}
.variant-highlighted {
background-color: var(--twentycrm-background-transparent-light);
}
.variant-highlighted:hover {
background-color: var(--twentycrm-background-transparent-medium);
}
.variant-highlighted:active {
background-color: var(--twentycrm-background-transparent-strong);
}
.variant-rounded {
--chip-horizontal-padding: calc(var(--twentycrm-spacing-multiplicator) * 2px);
--chip-vertical-padding: 1px;
background-color: var(--twentycrm-background-transparent-light);
border: 1px solid var(--twentycrm-border-color-medium);
border-radius: 50px;
}
.variant-transparent {
cursor: inherit;
}

View File

@ -1,10 +1,9 @@
import { MouseEvent, ReactNode } from 'react';
import { clsx } from 'clsx';
import { Theme, withTheme } from '@emotion/react';
import { styled } from '@linaria/react';
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
import styles from './Chip.module.css';
export enum ChipSize {
Large = 'large',
Small = 'small',
@ -34,9 +33,86 @@ type ChipProps = {
rightComponent?: ReactNode;
className?: string;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
to?: string;
};
const StyledContainer = withTheme(styled.div<
Pick<
ChipProps,
'accent' | 'clickable' | 'disabled' | 'maxWidth' | 'size' | 'variant'
> & { theme: Theme }
>`
--chip-horizontal-padding: ${({ theme }) => theme.spacing(1)};
--chip-vertical-padding: ${({ theme }) => theme.spacing(1)};
text-decoration: none;
align-items: center;
color: ${({ theme, accent, disabled }) =>
disabled
? theme.font.color.light
: accent === ChipAccent.TextPrimary
? theme.font.color.primary
: theme.font.color.secondary};
cursor: ${({ clickable, disabled, variant }) =>
variant === ChipVariant.Transparent
? 'inherit'
: clickable
? 'pointer'
: disabled
? 'not-allowed'
: 'inherit'};
display: inline-flex;
justify-content: center;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(3)};
max-width: ${({ maxWidth }) =>
maxWidth
? `calc(${maxWidth}px - 2 * var(--chip-horizontal-padding))`
: '200px'};
overflow: hidden;
padding: var(--chip-vertical-padding) var(--chip-horizontal-padding);
user-select: none;
font-weight: ${({ theme, accent }) =>
accent === ChipAccent.TextSecondary ? theme.font.weight.medium : 'inherit'};
&:hover {
background-color: ${({ theme, variant, disabled }) =>
variant === ChipVariant.Regular && !disabled
? theme.background.transparent.light
: variant === ChipVariant.Highlighted
? theme.background.transparent.medium
: 'inherit'};
}
&:active {
background-color: ${({ theme, disabled, variant }) =>
variant === ChipVariant.Regular && !disabled
? theme.background.transparent.medium
: variant === ChipVariant.Highlighted
? theme.background.transparent.strong
: 'inherit'};
}
background-color: ${({ theme, variant }) =>
variant === ChipVariant.Highlighted
? theme.background.transparent.light
: variant === ChipVariant.Rounded
? theme.background.transparent.lighter
: 'inherit'};
border: ${({ theme, variant }) =>
variant === ChipVariant.Rounded
? `1px solid ${theme.border.color.medium}`
: 'none'};
border-radius: ${({ theme, variant }) =>
variant === ChipVariant.Rounded ? '50px' : theme.border.radius.sm};
`);
export const Chip = ({
size = ChipSize.Small,
label,
@ -49,30 +125,21 @@ export const Chip = ({
onClick,
}: ChipProps) => {
return (
<div
<StyledContainer
data-testid="chip"
className={clsx({
[styles.chip]: true,
[styles.clickable]: clickable,
[styles.disabled]: disabled,
[styles.accentTextPrimary]: accent === ChipAccent.TextPrimary,
[styles.accentTextSecondary]: accent === ChipAccent.TextSecondary,
[styles.sizeLarge]: size === ChipSize.Large,
[styles.variantRegular]: variant === ChipVariant.Regular,
[styles.variantHighlighted]: variant === ChipVariant.Highlighted,
[styles.variantRounded]: variant === ChipVariant.Rounded,
[styles.variantTransparent]: variant === ChipVariant.Transparent,
})}
accent={accent}
clickable={clickable}
disabled={disabled}
size={size}
variant={variant}
onClick={onClick}
>
{leftComponent}
<div className={styles.label}>
<OverflowingTextWithTooltip
size={size === ChipSize.Large ? 'large' : 'small'}
text={label}
/>
</div>
<OverflowingTextWithTooltip
size={size === ChipSize.Large ? 'large' : 'small'}
text={label}
/>
{rightComponent}
</div>
</StyledContainer>
);
};

View File

@ -1,16 +1,28 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { styled } from '@linaria/react';
import { IconComponent, OverflowingTextWithTooltip } from '@ui/display';
import { ThemeColor, themeColorSchema } from '@ui/theme';
import {
BORDER_COMMON,
THEME_COMMON,
ThemeColor,
ThemeContext,
ThemeType,
} from '@ui/theme';
const spacing5 = THEME_COMMON.spacing(5);
const spacing2 = THEME_COMMON.spacing(2);
const spacing1 = THEME_COMMON.spacing(1);
const StyledTag = styled.h3<{
theme: ThemeType;
color: ThemeColor;
weight: TagWeight;
preventShrink?: boolean;
}>`
align-items: center;
background: ${({ color, theme }) => theme.tag.background[color]};
border-radius: ${({ theme }) => theme.border.radius.sm};
border-radius: ${BORDER_COMMON.radius.sm};
color: ${({ color, theme }) => theme.tag.text[color]};
display: inline-flex;
font-size: ${({ theme }) => theme.font.size.md};
@ -19,10 +31,15 @@ const StyledTag = styled.h3<{
weight === 'regular'
? theme.font.weight.regular
: theme.font.weight.medium};
height: ${({ theme }) => theme.spacing(5)};
height: ${spacing5};
margin: 0;
overflow: hidden;
padding: 0 ${({ theme }) => theme.spacing(2)};
padding: 0 ${spacing2};
gap: ${spacing1};
min-width: ${({ preventShrink }) =>
preventShrink ? 'fit-content' : 'none;'};
`;
const StyledContent = styled.span`
@ -31,9 +48,13 @@ const StyledContent = styled.span`
white-space: nowrap;
`;
const StyledNonShrinkableText = styled.span`
white-space: nowrap;
width: fit-content;
`;
const StyledIconContainer = styled.div`
display: flex;
margin-right: ${({ theme }) => theme.spacing(1)};
`;
type TagWeight = 'regular' | 'medium';
@ -45,8 +66,10 @@ type TagProps = {
Icon?: IconComponent;
onClick?: () => void;
weight?: TagWeight;
preventShrink?: boolean;
};
// TODO: Find a way to have ellipsis and shrinkable tag in tag list while keeping good perf for table cells
export const Tag = ({
className,
color,
@ -54,23 +77,31 @@ export const Tag = ({
Icon,
onClick,
weight = 'regular',
preventShrink,
}: TagProps) => {
const theme = useTheme();
const { theme } = useContext(ThemeContext);
return (
<StyledTag
theme={theme}
className={className}
color={themeColorSchema.catch('gray').parse(color)}
color={color}
onClick={onClick}
weight={weight}
preventShrink={preventShrink}
>
{!!Icon && (
<StyledIconContainer>
<Icon size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
</StyledIconContainer>
)}
<StyledContent>
<OverflowingTextWithTooltip text={text} />
</StyledContent>
{preventShrink ? (
<StyledNonShrinkableText>{text}</StyledNonShrinkableText>
) : (
<StyledContent>
<OverflowingTextWithTooltip text={text} />
</StyledContent>
)}
</StyledTag>
);
};

View File

@ -1,6 +1,7 @@
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within } from '@storybook/test';
import { IconUser } from '@ui/display/icon/components/TablerIcons';
import {
CatalogDecorator,
CatalogStory,
@ -48,6 +49,30 @@ export const WithLongText: Story = {
},
};
export const WithIcon: Story = {
decorators: [ComponentDecorator],
args: {
color: 'green',
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
Icon: IconUser,
},
parameters: {
container: { width: 100 },
},
};
export const DontShrink: Story = {
decorators: [ComponentDecorator],
args: {
color: 'green',
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
preventShrink: true,
},
parameters: {
container: { width: 100 },
},
};
export const Catalog: CatalogStory<Story, typeof Tag> = {
argTypes: {
color: { control: false },

View File

@ -1,11 +1,38 @@
import { useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import clsx from 'clsx';
import { v4 as uuidV4 } from 'uuid';
import { styled } from '@linaria/react';
import { THEME_COMMON } from '@ui/theme';
import { AppTooltip } from './AppTooltip';
import styles from './OverflowingTextWithTooltip.module.css';
const spacing4 = THEME_COMMON.spacing(4);
const StyledOverflowingText = styled.div<{
cursorPointer: boolean;
size: 'large' | 'small';
}>`
cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'inherit')};
font-family: inherit;
font-size: inherit;
font-weight: inherit;
max-width: 100%;
overflow: hidden;
text-decoration: inherit;
text-overflow: ellipsis;
white-space: nowrap;
height: ${({ size }) => (size === 'large' ? spacing4 : 'auto')};
& :hover {
text-overflow: ${({ cursorPointer }) =>
cursorPointer ? 'clip' : 'ellipsis'};
white-space: ${({ cursorPointer }) =>
cursorPointer ? 'normal' : 'nowrap'};
}
`;
export const OverflowingTextWithTooltip = ({
size = 'small',
@ -16,7 +43,7 @@ export const OverflowingTextWithTooltip = ({
text: string | null | undefined;
mutliline?: boolean;
}) => {
const textElementId = `title-id-${uuidV4()}`;
const textElementId = `title-id-${+new Date()}`;
const textRef = useRef<HTMLDivElement>(null);
@ -43,20 +70,17 @@ export const OverflowingTextWithTooltip = ({
return (
<>
<div
<StyledOverflowingText
data-testid="tooltip"
className={clsx({
[styles.main]: true,
[styles.cursor]: isTitleOverflowing,
[styles.large]: size === 'large',
})}
cursorPointer={isTitleOverflowing}
size={size}
ref={textRef}
id={textElementId}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{text}
</div>
</StyledOverflowingText>
{isTitleOverflowing &&
createPortal(
<div onClick={handleTooltipClick}>

View File

@ -34,6 +34,7 @@ export * from './constants/TextInputStyle';
export * from './constants/ThemeCommon';
export * from './constants/ThemeDark';
export * from './constants/ThemeLight';
export * from './provider/ThemeContextProvider';
export * from './provider/ThemeProvider';
export * from './types/ThemeType';
export * from './utils/getNextThemeColor';

View File

@ -0,0 +1,23 @@
import { createContext } from 'react';
import { ThemeType } from '@ui/theme/types/ThemeType';
export type ThemeContextType = {
theme: ThemeType;
};
export const ThemeContext = createContext<ThemeContextType>(
{} as ThemeContextType,
);
export const ThemeContextProvider = ({
children,
theme,
}: {
children: React.ReactNode;
theme: ThemeType;
}) => {
return (
<ThemeContext.Provider value={{ theme }}>{children}</ThemeContext.Provider>
);
};

View File

@ -1,6 +1,8 @@
import { ReactNode, useEffect } from 'react';
import { ThemeProvider as EmotionThemeProvider } from '@emotion/react';
import { ThemeContextProvider } from '@ui/theme/provider/ThemeContextProvider';
import { ThemeType } from '..';
import './theme.css';
@ -16,7 +18,11 @@ const ThemeProvider = ({ theme, children }: ThemeProviderProps) => {
theme.name === 'dark' ? 'dark' : 'light';
}, [theme]);
return <EmotionThemeProvider theme={theme}>{children}</EmotionThemeProvider>;
return (
<EmotionThemeProvider theme={theme}>
<ThemeContextProvider theme={theme}>{children}</ThemeContextProvider>
</EmotionThemeProvider>
);
};
export default ThemeProvider;

View File

@ -1,5 +1,6 @@
/// <reference types='vitest' />
import react from '@vitejs/plugin-react-swc';
import wyw from '@wyw-in-js/vite';
import * as path from 'path';
import { defineConfig } from 'vite';
import checker from 'vite-plugin-checker';
@ -27,6 +28,16 @@ export default defineConfig({
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
},
}),
wyw({
include: [
'**/OverflowingTextWithTooltip.tsx',
'**/Chip.tsx',
'**/Tag.tsx',
],
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
},
}),
],
// Configuration for building your library.

Some files were not shown because too many files have changed in this diff Show More