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", "lodash.upperfirst": "^4.3.1",
"luxon": "^3.3.0", "luxon": "^3.3.0",
"microdiff": "^1.3.2", "microdiff": "^1.3.2",
"moize": "^6.1.6",
"nest-commander": "^3.12.0", "nest-commander": "^3.12.0",
"next": "14.0.4", "next": "14.0.4",
"next-mdx-remote": "^4.4.1", "next-mdx-remote": "^4.4.1",

View File

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

View File

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

View File

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

View File

@ -13,6 +13,8 @@ import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWith
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { getPeopleMock } from '~/testing/mock-data/people';
import { import {
mockDefaultWorkspace, mockDefaultWorkspace,
mockedWorkspaceMemberData, mockedWorkspaceMemberData,
@ -21,6 +23,9 @@ import { sleep } from '~/testing/sleep';
import { CommandMenu } from '../CommandMenu'; import { CommandMenu } from '../CommandMenu';
const peopleMock = getPeopleMock();
const companiesMock = getCompaniesMock();
const openTimeout = 50; const openTimeout = 50;
const meta: Meta<typeof CommandMenu> = { const meta: Meta<typeof CommandMenu> = {
@ -94,8 +99,12 @@ export const MatchingPersonCompanyActivityCreateNavigate: Story = {
const searchInput = await canvas.findByPlaceholderText('Search'); const searchInput = await canvas.findByPlaceholderText('Search');
await sleep(openTimeout); await sleep(openTimeout);
await userEvent.type(searchInput, 'n'); await userEvent.type(searchInput, 'n');
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument(); expect(
expect(await canvas.findByText('Airbnb')).toBeInTheDocument(); 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('My very first note')).toBeInTheDocument();
expect(await canvas.findByText('Create Note')).toBeInTheDocument(); expect(await canvas.findByText('Create Note')).toBeInTheDocument();
expect(await canvas.findByText('Go to Companies')).toBeInTheDocument(); expect(await canvas.findByText('Go to Companies')).toBeInTheDocument();
@ -119,7 +128,11 @@ export const AtleastMatchingOnePerson: Story = {
const searchInput = await canvas.findByPlaceholderText('Search'); const searchInput = await canvas.findByPlaceholderText('Search');
await sleep(openTimeout); await sleep(openTimeout);
await userEvent.type(searchInput, 'alex'); 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, mockedObjectMetadataItems,
mockedPersonObjectMetadataItem, mockedPersonObjectMetadataItem,
} from '~/testing/mock-data/metadata'; } from '~/testing/mock-data/metadata';
import { mockedPeopleData } from '~/testing/mock-data/people'; import { getPeopleMock } from '~/testing/mock-data/people';
import { getRecordNodeFromRecord } from '../getRecordNodeFromRecord'; import { getRecordNodeFromRecord } from '../getRecordNodeFromRecord';
const peopleMock = getPeopleMock();
describe('getRecordNodeFromRecord', () => { describe('getRecordNodeFromRecord', () => {
it('computes relation records cache references by default', () => { it('computes relation records cache references by default', () => {
// Given // Given
@ -19,7 +21,7 @@ describe('getRecordNodeFromRecord', () => {
name: true, name: true,
company: true, company: true,
}; };
const record = mockedPeopleData[0]; const record = peopleMock[0];
// When // When
const result = getRecordNodeFromRecord({ const result = getRecordNodeFromRecord({
@ -33,12 +35,12 @@ describe('getRecordNodeFromRecord', () => {
expect(result).toEqual({ expect(result).toEqual({
__typename: 'Person', __typename: 'Person',
company: { company: {
__ref: 'Company:5c21e19e-e049-4393-8c09-3e3f8fb09ecb', __ref: `Company:${record.company.id}`,
}, },
name: { name: {
__typename: 'FullName', __typename: 'FullName',
firstName: 'Alexandre', firstName: record.name.firstName,
lastName: 'Prot', lastName: record.name.lastName,
}, },
}); });
}); });
@ -54,7 +56,7 @@ describe('getRecordNodeFromRecord', () => {
name: true, name: true,
company: true, company: true,
}; };
const record = mockedPeopleData[0]; const record = peopleMock[0];
const computeReferences = false; const computeReferences = false;
// When // When
@ -72,8 +74,8 @@ describe('getRecordNodeFromRecord', () => {
company: record.company, company: record.company,
name: { name: {
__typename: 'FullName', __typename: 'FullName',
firstName: 'Alexandre', firstName: record.name.firstName,
lastName: 'Prot', lastName: record.name.lastName,
}, },
}); });
}); });

View File

@ -1,5 +1,7 @@
import { gql } from '@apollo/client'; 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` export const query = gql`
query FindDuplicatePerson($id: ID!) { query FindDuplicatePerson($id: ID!) {
@ -49,11 +51,11 @@ export const responseData = {
personDuplicates: { personDuplicates: {
edges: [ edges: [
{ {
node: { ...mockedPeopleData[0], updatedAt: '' }, node: { ...peopleMock[0], updatedAt: '' },
cursor: 'cursor1', cursor: 'cursor1',
}, },
{ {
node: { ...mockedPeopleData[1], updatedAt: '' }, node: { ...peopleMock[1], updatedAt: '' },
cursor: 'cursor2', 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'; import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
export const AddressFieldDisplay = () => { export const AddressFieldDisplay = () => {
const { fieldValue } = useAddressField(); const { fieldValue } = useAddressFieldDisplay();
const content = [ const content = [
fieldValue?.addressStreet1, fieldValue?.addressStreet1,
@ -10,7 +12,7 @@ export const AddressFieldDisplay = () => {
fieldValue?.addressCity, fieldValue?.addressCity,
fieldValue?.addressCountry, fieldValue?.addressCountry,
] ]
.filter(Boolean) .filter(isNonEmptyString)
.join(', '); .join(', ');
return <TextDisplay text={content} />; 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'; import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';
export const BooleanFieldDisplay = () => { export const BooleanFieldDisplay = () => {
const { fieldValue } = useBooleanField(); const { fieldValue } = useBooleanFieldDisplay();
return <BooleanDisplay value={fieldValue} />; 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 { CurrencyDisplay } from '@/ui/field/display/components/CurrencyDisplay';
import { useCurrencyField } from '../../hooks/useCurrencyField';
export const CurrencyFieldDisplay = () => { export const CurrencyFieldDisplay = () => {
const { fieldValue } = useCurrencyField(); const { fieldValue } = useCurrencyFieldDisplay();
return <CurrencyDisplay currencyValue={fieldValue} />; 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 { DateDisplay } from '@/ui/field/display/components/DateDisplay';
import { useDateField } from '../../hooks/useDateField';
export const DateFieldDisplay = () => { export const DateFieldDisplay = () => {
const { fieldValue } = useDateField(); const { fieldValue } = useDateFieldDisplay();
return <DateDisplay value={fieldValue} />; 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 { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay';
import { useDateTimeField } from '../../hooks/useDateTimeField';
export const DateTimeFieldDisplay = () => { export const DateTimeFieldDisplay = () => {
const { fieldValue } = useDateTimeField(); const { fieldValue } = useDateTimeFieldDisplay();
return <DateTimeDisplay value={fieldValue} />; 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 { EmailDisplay } from '@/ui/field/display/components/EmailDisplay';
import { useEmailField } from '../../hooks/useEmailField';
export const EmailFieldDisplay = () => { export const EmailFieldDisplay = () => {
const { fieldValue } = useEmailField(); const { fieldValue } = useEmailFieldDisplay();
return <EmailDisplay value={fieldValue} />; 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'; import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
export const FullNameFieldDisplay = () => { export const FullNameFieldDisplay = () => {
const { fieldValue } = useFullNameField(); const { fieldValue } = useFullNameFieldDisplay();
const content = [fieldValue.firstName, fieldValue.lastName] const content = [fieldValue?.firstName, fieldValue?.lastName]
.filter(Boolean) .filter(isNonEmptyString)
.join(' '); .join(' ');
return <TextDisplay text={content} />; return <TextDisplay text={content} />;

View File

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

View File

@ -1,9 +1,9 @@
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; 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'; import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
export const LinksFieldDisplay = () => { export const LinksFieldDisplay = () => {
const { fieldValue } = useLinksField(); const { fieldValue } = useLinksFieldDisplay();
const { isFocused } = useFieldFocus(); 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 { 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'; 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 = () => { export const MultiSelectFieldDisplay = () => {
const { fieldValues, fieldDefinition } = useMultiSelectField(); const { fieldValue, fieldDefinition } = useMultiSelectFieldDisplay();
const { isFocused } = useFieldFocus(); const { isFocused } = useFieldFocus();
const selectedOptions = fieldValues const selectedOptions = fieldValue
? fieldDefinition.metadata.options?.filter((option) => ? fieldDefinition.metadata.options?.filter((option) =>
fieldValues.includes(option.value), fieldValue.includes(option.value),
) )
: []; : [];
if (!selectedOptions) return null; if (!selectedOptions) return null;
return ( return isFocused ? (
<ExpandableList isChipCountDisplayed={isFocused}> <ExpandableList isChipCountDisplayed={isFocused}>
{selectedOptions.map((selectedOption, index) => ( {selectedOptions.map((selectedOption, index) => (
<Tag <Tag
@ -27,5 +43,16 @@ export const MultiSelectFieldDisplay = () => {
/> />
))} ))}
</ExpandableList> </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 { NumberDisplay } from '@/ui/field/display/components/NumberDisplay';
import { useNumberField } from '../../hooks/useNumberField';
export const NumberFieldDisplay = () => { export const NumberFieldDisplay = () => {
const { fieldValue } = useNumberField(); const { fieldValue } = useNumberFieldDisplay();
return <NumberDisplay value={fieldValue} />; return <NumberDisplay value={fieldValue} />;
}; };

View File

@ -1,17 +1,24 @@
import { Tag } from 'twenty-ui'; 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 = () => { export const SelectFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useSelectField(); const { fieldValue, fieldDefinition } = useSelectFieldDisplay();
const selectedOption = fieldDefinition.metadata.options?.find( const selectedOption = fieldDefinition.metadata.options?.find(
(option) => option.value === fieldValue, (option) => option.value === fieldValue,
); );
return selectedOption ? ( if (!isDefined(selectedOption)) {
<Tag color={selectedOption.color} text={selectedOption.label} /> 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 { TextDisplay } from '@/ui/field/display/components/TextDisplay';
import { useTextField } from '../../hooks/useTextField';
export const TextFieldDisplay = () => { export const TextFieldDisplay = () => {
const { fieldValue, maxWidth } = useTextField(); const { fieldValue, maxWidth } = useTextFieldDisplay();
return <TextDisplay text={fieldValue} maxWidth={maxWidth} />; 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 = { export const WrongNumber: Story = {
parameters: {
container: { width: 50 },
},
decorators: [getFieldDecorator('person', 'phone', 'sdklaskdj')], 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 { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui'; import { ComponentDecorator } from 'twenty-ui';
import { FieldMetadataType } from '~/generated/graphql'; import { TextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/TextFieldDisplay';
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
import { FieldContext } from '../../../../contexts/FieldContext'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { useTextField } from '../../../hooks/useTextField'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
import { TextFieldDisplay } from '../TextFieldDisplay';
const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useTextField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = { const meta: Meta = {
title: 'UI/Data/Field/Display/TextFieldDisplay', title: 'UI/Data/Field/Display/TextFieldDisplay',
decorators: [ decorators: [
(Story, { args }) => ( MemoryRouterDecorator,
<FieldContext.Provider getFieldDecorator('person', 'city'),
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>
),
ComponentDecorator, ComponentDecorator,
], ],
component: TextFieldDisplay, component: TextFieldDisplay,
args: { args: {},
value: 'Lorem ipsum', parameters: {
chromatic: { disableSnapshot: true },
}, },
}; };
@ -59,11 +27,21 @@ type Story = StoryObj<typeof TextFieldDisplay>;
export const Default: Story = {}; export const Default: Story = {};
export const Elipsis: 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: { parameters: {
container: { width: 100 }, 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 fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue(entityId, fieldName); const fieldValue = useRecordFieldValue<string | undefined>(
entityId,
fieldName,
);
return { return {
fieldDefinition, fieldDefinition,

View File

@ -3,6 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext'; import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; 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 { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -32,7 +33,10 @@ export const useRelationFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName; const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue(entityId, fieldName); const fieldValue = useRecordFieldValue<ObjectRecord | undefined>(
entityId,
fieldName,
);
const maxWidthForField = const maxWidthForField =
isDefined(button) && isDefined(maxWidth) 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 { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { mockedCompaniesData } from '~/testing/mock-data/companies'; import { getCompaniesMock } from '~/testing/mock-data/companies';
import { mockObjectMetadataItem } from '~/testing/mock-data/objectMetadataItems'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { isRecordMatchingFilter } from './isRecordMatchingFilter'; import { isRecordMatchingFilter } from './isRecordMatchingFilter';
const companiesMock = getCompaniesMock();
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
describe('isRecordMatchingFilter', () => { describe('isRecordMatchingFilter', () => {
describe('Empty Filters', () => { describe('Empty Filters', () => {
it('matches any record when no filter is provided', () => { it('matches any record when no filter is provided', () => {
const emptyFilter = {}; const emptyFilter = {};
mockedCompaniesData.forEach((company) => { companiesMock.forEach((company) => {
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: company, record: company,
filter: emptyFilter, filter: emptyFilter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
}); });
@ -26,12 +32,12 @@ describe('isRecordMatchingFilter', () => {
employees: {}, employees: {},
}; };
mockedCompaniesData.forEach((company) => { companiesMock.forEach((company) => {
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: company, record: company,
filter: filterWithEmptyFields, filter: filterWithEmptyFields,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
}); });
@ -40,12 +46,12 @@ describe('isRecordMatchingFilter', () => {
it('matches any record with an empty and filter', () => { it('matches any record with an empty and filter', () => {
const filter = { and: [] }; const filter = { and: [] };
mockedCompaniesData.forEach((company) => { companiesMock.forEach((company) => {
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: company, record: company,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
}); });
@ -54,12 +60,12 @@ describe('isRecordMatchingFilter', () => {
it('matches any record with an empty or filter', () => { it('matches any record with an empty or filter', () => {
const filter = { or: [] }; const filter = { or: [] };
mockedCompaniesData.forEach((company) => { companiesMock.forEach((company) => {
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: company, record: company,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
}); });
@ -68,12 +74,12 @@ describe('isRecordMatchingFilter', () => {
it('matches any record with an empty not filter', () => { it('matches any record with an empty not filter', () => {
const filter = { not: {} }; const filter = { not: {} };
mockedCompaniesData.forEach((company) => { companiesMock.forEach((company) => {
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: company, record: company,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
}); });
@ -82,92 +88,161 @@ describe('isRecordMatchingFilter', () => {
describe('Simple Filters', () => { describe('Simple Filters', () => {
it('matches a record with a simple equality filter on name', () => { 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( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[0], record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[1], record: companyMockNotInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); ).toBe(false);
}); });
it('matches a record with a simple equality filter on domainName', () => { 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( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[0], record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[1], record: companyMockNotInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); ).toBe(false);
}); });
it('matches a record with a greater than filter on employees', () => { 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( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[0], record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[1], record: companyMockNotInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); ).toBe(false);
}); });
it('matches a record with a boolean filter on idealCustomerProfile', () => { 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( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[0], record: companyIdealCustomerProfileTrue,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(companyIdealCustomerProfileTrue.idealCustomerProfile);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[4], // Assuming this record has idealCustomerProfile as false record: companyIdealCustomerProfileFalse,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); ).toBe(companyIdealCustomerProfileFalse.idealCustomerProfile);
}); });
}); });
describe('Complex And/Or/Not Nesting', () => { describe('Complex And/Or/Not Nesting', () => {
it('matches record with a combination of and + or filters', () => { 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 = { const filter: RecordGqlOperationFilter = {
and: [ and: [
{ domainName: { eq: 'airbnb.com' } }, {
domainName: {
eq: companyMockInFilter.domainName,
},
},
{ {
or: [ or: [
{ employees: { gt: 10 } }, {
{ idealCustomerProfile: { eq: true } }, employees: {
gt: companyMockInFilter.employees - 1,
},
},
{
idealCustomerProfile: {
eq: companyMockInFilter.idealCustomerProfile,
},
},
], ],
}, },
], ],
@ -175,118 +250,181 @@ describe('isRecordMatchingFilter', () => {
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[1], // Aircall record: companyMockNotInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); ).toBe(false);
}); });
it('matches record with nested not filter', () => { 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 = { const filter: RecordGqlOperationFilter = {
not: { not: {
and: [ and: [
{ name: { eq: 'Airbnb' } }, { name: { eq: companyMockInFilter.name } },
{ idealCustomerProfile: { eq: true } }, {
idealCustomerProfile: {
eq: companyMockInFilter.idealCustomerProfile,
},
},
], ],
}, },
}; };
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); // Should not match as it's Airbnb with idealCustomerProfile true ).toBe(false);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[3], // Apple record: companyMockNotInFilter,
filter, 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', () => { 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 = { const filter: RecordGqlOperationFilter = {
and: [ 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( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[3], // Apple record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[4], // Qonto record: companyMockNotInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); ).toBe(false);
}); });
it('matches record with and filter at root level', () => { 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 = { const filter: RecordGqlOperationFilter = {
and: [ and: [
{ name: { eq: 'Facebook' } }, { name: { eq: companyMockInFilter.name } },
{ idealCustomerProfile: { eq: true } }, {
idealCustomerProfile: {
eq: companyMockInFilter.idealCustomerProfile,
},
},
], ],
}; };
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[5], // Facebook record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb record: companyMockNotInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); ).toBe(false);
}); });
it('matches record with or filter at root level including a not condition', () => { 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 = { const filter: RecordGqlOperationFilter = {
or: [{ name: { eq: 'Sequoia' } }, { not: { employees: { eq: 1 } } }], or: [
{ name: { eq: companyMockInFilter.name } },
{ not: { employees: { eq: companyMockInFilter.employees - 1 } } },
],
}; };
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[6], // Sequoia record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[1], // Aircall record: companyMockNotInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); ).toBe(false);
}); });
@ -294,49 +432,75 @@ describe('isRecordMatchingFilter', () => {
describe('Implicit And Conditions', () => { describe('Implicit And Conditions', () => {
it('matches record with implicit and of multiple operators within the same field', () => { 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 = { const filter = {
employees: { gt: 10, lt: 100000 }, employees: {
name: { eq: 'Airbnb' }, gt: companyMockInFilter.employees - 10,
lt: companyMockInFilter.employees + 10,
},
name: { eq: companyMockInFilter.name },
}; };
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); // Matches as Airbnb's employee count is between 10 and 100000 ).toBe(true); // Matches as Airbnb's employee count is between 10 and 100000
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[1], // Aircall record: companyMockNotInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); // Does not match as Aircall's employee count is not within the range ).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', () => { 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 = { const filter = {
or: { or: {
name: { eq: 'Airbnb' }, name: { eq: companyMockInFilter.name },
domainName: { eq: 'airbnb.com' }, domainName: { eq: companyMockInFilter.domainName },
}, },
}; };
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[0], // Airbnb record: companyMockInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(true); ).toBe(true);
expect( expect(
isRecordMatchingFilter({ isRecordMatchingFilter({
record: mockedCompaniesData[2], // Algolia record: companyMockNotInFilter,
filter, filter,
objectMetadataItem: mockObjectMetadataItem, objectMetadataItem: companyMockObjectMetadataItem,
}), }),
).toBe(false); ).toBe(false);
}); });

View File

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

View File

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

View File

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

View File

@ -8,16 +8,18 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
import { RelationPickerDecorator } from '~/testing/decorators/RelationPickerDecorator'; import { RelationPickerDecorator } from '~/testing/decorators/RelationPickerDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; 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 { sleep } from '~/testing/sleep';
import { EntityForSelect } from '../../types/EntityForSelect'; import { EntityForSelect } from '../../types/EntityForSelect';
import { SingleEntitySelect } from '../SingleEntitySelect'; import { SingleEntitySelect } from '../SingleEntitySelect';
const entities = mockedPeopleData.map<EntityForSelect>((person) => ({ const peopleMock = getPeopleMock();
const entities = peopleMock.map<EntityForSelect>((person) => ({
id: person.id, id: person.id,
name: person.name.firstName + ' ' + person.name.lastName, name: person.name.firstName + ' ' + person.name.lastName,
avatarUrl: person.avatarUrl, avatarUrl: 'https://picsum.photos/200',
avatarType: 'rounded', avatarType: 'rounded',
record: { ...person, __typename: 'Person' }, 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 { IconButton } from '@/ui/input/button/components/IconButton';
import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
import { formatToHumanReadableDate } from '~/utils'; import { formatToHumanReadableDate } from '~/utils/date-utils';
type SettingsAccountsEmailsBlocklistTableRowProps = { type SettingsAccountsEmailsBlocklistTableRowProps = {
blocklistItem: BlocklistItem; blocklistItem: BlocklistItem;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,39 @@
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import { ContactLink } from '@/ui/navigation/link/components/ContactLink'; import { ContactLink } from '@/ui/navigation/link/components/ContactLink';
import { isDefined } from '~/utils/isDefined';
import { EllipsisDisplay } from './EllipsisDisplay'; import { EllipsisDisplay } from './EllipsisDisplay';
const validateEmail = (email: string) => { const validateEmail = (email: string) => {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email.trim()); // return emailPattern.test(email.trim());
// Record this without using regex
const emailParts = email.split('@');
if (emailParts.length !== 2) {
return false;
}
return true;
}; };
type EmailDisplayProps = { type EmailDisplayProps = {
value: string | null; value: string | null;
}; };
export const EmailDisplay = ({ value }: EmailDisplayProps) => ( export const EmailDisplay = ({ value }: EmailDisplayProps) => {
<EllipsisDisplay> if (!isDefined(value)) {
{value && validateEmail(value) ? ( return <ContactLink href="#">{value}</ContactLink>;
}
if (!validateEmail(value)) {
return <ContactLink href="#">{value}</ContactLink>;
}
return (
<EllipsisDisplay>
<ContactLink <ContactLink
href={`mailto:${value}`} href={`mailto:${value}`}
onClick={(event: MouseEvent<HTMLElement>) => { onClick={(event: MouseEvent<HTMLElement>) => {
@ -24,8 +42,6 @@ export const EmailDisplay = ({ value }: EmailDisplayProps) => (
> >
{value} {value}
</ContactLink> </ContactLink>
) : ( </EllipsisDisplay>
<ContactLink href="#">{value}</ContactLink> );
)} };
</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 { FieldLinkValue } from '@/object-record/record-field/types/FieldMetadata';
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink'; import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
@ -6,34 +6,31 @@ import {
LinkType, LinkType,
SocialLink, SocialLink,
} from '@/ui/navigation/link/components/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 = { type LinkDisplayProps = {
value?: FieldLinkValue; value?: FieldLinkValue;
}; };
export const LinkDisplay = ({ value }: LinkDisplayProps) => { export const LinkDisplay = ({ value }: LinkDisplayProps) => {
const handleClick = (event: MouseEvent<HTMLElement>) => { const url = value?.url;
event.stopPropagation();
};
const absoluteUrl = getAbsoluteUrl(value?.url || ''); if (!isNonEmptyString(url)) {
const displayedValue = value?.label || getUrlHostName(absoluteUrl); return <></>;
const type = checkUrlType(absoluteUrl);
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return (
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
{displayedValue}
</SocialLink>
);
} }
return ( const displayedValue = isNonEmptyString(value?.label)
<RoundedLink href={absoluteUrl} onClick={handleClick}> ? value?.label
{displayedValue} : url?.replace(/^http[s]?:\/\/(?:[w]+\.)?/gm, '').replace(/^[w]+\./gm, '');
</RoundedLink>
); 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 { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
@ -17,6 +19,21 @@ type LinksDisplayProps = {
isFocused?: boolean; 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) => { export const LinksDisplay = ({ value, isFocused }: LinksDisplayProps) => {
const links = useMemo( const links = useMemo(
() => () =>
@ -41,21 +58,25 @@ export const LinksDisplay = ({ value, isFocused }: LinksDisplayProps) => {
[value?.primaryLinkLabel, value?.primaryLinkUrl, value?.secondaryLinks], [value?.primaryLinkLabel, value?.primaryLinkUrl, value?.secondaryLinks],
); );
const handleClick: MouseEventHandler = (event) => event.stopPropagation(); return isFocused ? (
return (
<ExpandableList isChipCountDisplayed={isFocused}> <ExpandableList isChipCountDisplayed={isFocused}>
{links.map(({ url, label, type }, index) => {links.map(({ url, label, type }, index) =>
type === LinkType.LinkedIn || type === LinkType.Twitter ? ( type === LinkType.LinkedIn || type === LinkType.Twitter ? (
<SocialLink key={index} href={url} onClick={handleClick} type={type}> <SocialLink key={index} href={url} type={type} label={label} />
{label}
</SocialLink>
) : ( ) : (
<RoundedLink key={index} href={url} onClick={handleClick}> <RoundedLink key={index} href={url} label={label} />
{label}
</RoundedLink>
), ),
)} )}
</ExpandableList> </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) { if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return ( return (
<EllipsisDisplay> <EllipsisDisplay>
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}> <SocialLink
{displayedValue} href={absoluteUrl}
</SocialLink> onClick={handleClick}
type={type}
label={displayedValue}
/>
</EllipsisDisplay> </EllipsisDisplay>
); );
} }
return ( return (
<EllipsisDisplay> <EllipsisDisplay>
<StyledRawLink href={absoluteUrl} onClick={handleClick}> <StyledRawLink
{displayedValue} href={absoluteUrl}
</StyledRawLink> onClick={handleClick}
label={displayedValue}
/>
</EllipsisDisplay> </EllipsisDisplay>
); );
}; };

View File

@ -1,50 +1,71 @@
import * as React from 'react'; import { MouseEvent } from 'react';
import { Link as ReactLink } from 'react-router-dom'; import { styled } from '@linaria/react';
import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards';
import { Chip, ChipSize, ChipVariant } from 'twenty-ui'; import { FONT_COMMON, THEME_COMMON } from 'twenty-ui';
type RoundedLinkProps = { type RoundedLinkProps = {
href: string; href: string;
children?: React.ReactNode; label?: string;
className?: string;
onClick?: (event: React.MouseEvent<HTMLElement>) => void; onClick?: (event: React.MouseEvent<HTMLElement>) => void;
}; };
const StyledLink = styled(ReactLink)` const fontSizeMd = FONT_COMMON.size.md;
font-size: ${({ theme }) => theme.font.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%; 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)` export const RoundedLink = ({ label, href, onClick }: RoundedLinkProps) => {
border-color: ${({ theme }) => theme.border.color.strong}; if (!isNonEmptyString(label)) {
box-sizing: border-box; return <></>;
padding: ${({ theme }) => theme.spacing(0, 2)}; }
max-width: 100%;
height: ${({ theme }) => theme.spacing(5)};
min-width: 40px;
`;
export const RoundedLink = ({ const handleClick = (event: MouseEvent<HTMLElement>) => {
children, event.stopPropagation();
className,
href, onClick?.(event);
onClick, };
}: RoundedLinkProps) => {
if (!children) return null;
return ( return (
<StyledLink <StyledLink
className={className} href={href}
target="_blank" target="_blank"
to={href} rel="noreferrer"
onClick={onClick} onClick={handleClick}
> >
<StyledChip {label}
label={`${children}`}
variant={ChipVariant.Rounded}
size={ChipSize.Large}
/>
</StyledLink> </StyledLink>
); );
}; };

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { ThemeProvider } from '@emotion/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 { useColorScheme } from '../hooks/useColorScheme';
import { useSystemColorScheme } from '../hooks/useSystemColorScheme'; import { useSystemColorScheme } from '../hooks/useSystemColorScheme';
@ -24,5 +24,9 @@ export const AppThemeProvider = ({ children }: AppThemeProviderProps) => {
theme.name === 'dark' ? 'dark' : 'light'; theme.name === 'dark' ? 'dark' : 'light';
}, [theme]); }, [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.findByText('People');
await canvas.findAllByText('Companies'); await canvas.findAllByText('Companies');
await canvas.findByText('Opportunities'); await canvas.findByText('Opportunities');
await canvas.findByText('Listings');
await canvas.findByText('My Customs'); await canvas.findByText('My Customs');
}, },
}; };

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import { useEffect } from 'react';
import { Decorator } from '@storybook/react'; import { Decorator } from '@storybook/react';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { Company } from '@/companies/types/Company';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -12,18 +11,17 @@ import {
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { Person } from '@/people/types/Person'; import { getCompaniesMock } from '~/testing/mock-data/companies';
import { mockedCompaniesDataV2 } from '~/testing/mock-data/companiesV2';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; 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'; import { isDefined } from '~/utils/isDefined';
const RecordMockSetterEffect = ({ const RecordMockSetterEffect = ({
companies, companies,
people, people,
}: { }: {
companies: Company[]; companies: ObjectRecord[];
people: Person[]; people: ObjectRecord[];
}) => { }) => {
const setRecordValue = useSetRecordValue(); const setRecordValue = useSetRecordValue();
@ -56,21 +54,25 @@ export const getFieldDecorator =
fieldValue?: any, fieldValue?: any,
): Decorator => ): Decorator =>
(Story) => { (Story) => {
const companiesMock = getCompaniesMock();
const companies = const companies =
objectNameSingular === 'company' && isDefined(fieldValue) objectNameSingular === 'company' && isDefined(fieldValue)
? [ ? [
{ ...mockedCompaniesDataV2[0], [fieldName]: fieldValue }, { ...companiesMock[0], [fieldName]: fieldValue },
...mockedCompaniesDataV2.slice(1), ...companiesMock.slice(1),
] ]
: mockedCompaniesDataV2; : companiesMock;
const peopleMock = getPeopleMock();
const people = const people =
objectNameSingular === 'person' && isDefined(fieldValue) objectNameSingular === 'person' && isDefined(fieldValue)
? [ ? [
{ ...mockPeopleDataV2[0], [fieldName]: fieldValue }, { ...peopleMock[0], [fieldName]: fieldValue },
...mockPeopleDataV2.slice(1), ...peopleMock.slice(1),
] ]
: mockPeopleDataV2; : peopleMock;
const record = objectNameSingular === 'company' ? companies[0] : people[0]; 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 { REACT_APP_SERVER_BASE_URL } from '~/config';
import { mockedActivities } from '~/testing/mock-data/activities'; import { mockedActivities } from '~/testing/mock-data/activities';
import { import {
mockedCompaniesData, getCompaniesMock,
mockedDuplicateCompanyData, getCompanyDuplicateMock,
} from '~/testing/mock-data/companies'; } from '~/testing/mock-data/companies';
import { mockedClientConfig } from '~/testing/mock-data/config'; import { mockedClientConfig } from '~/testing/mock-data/config';
import { mockedObjectMetadataItemsQueryResult } from '~/testing/mock-data/metadata'; import { mockedObjectMetadataItemsQueryResult } from '~/testing/mock-data/metadata';
import { getPeopleMock } from '~/testing/mock-data/people';
import { mockedRemoteTables } from '~/testing/mock-data/remote-tables'; import { mockedRemoteTables } from '~/testing/mock-data/remote-tables';
import { mockedUsersData } from '~/testing/mock-data/users'; import { mockedUsersData } from '~/testing/mock-data/users';
import { mockedViewsData } from '~/testing/mock-data/views'; import { mockedViewsData } from '~/testing/mock-data/views';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
import { mockedPeopleData } from './mock-data/people';
import { mockedRemoteServers } from './mock-data/remote-servers'; import { mockedRemoteServers } from './mock-data/remote-servers';
import { mockedViewFieldsData } from './mock-data/view-fields'; import { mockedViewFieldsData } from './mock-data/view-fields';
const peopleMock = getPeopleMock();
const companiesMock = getCompaniesMock();
const duplicateCompanyMock = getCompanyDuplicateMock();
export const metadataGraphql = graphql.link( export const metadataGraphql = graphql.link(
`${REACT_APP_SERVER_BASE_URL}/metadata`, `${REACT_APP_SERVER_BASE_URL}/metadata`,
); );
@ -108,8 +112,8 @@ export const graphqlMocks = {
}), }),
graphql.query('FindManyCompanies', ({ variables }) => { graphql.query('FindManyCompanies', ({ variables }) => {
const mockedData = variables.limit const mockedData = variables.limit
? mockedCompaniesData.slice(0, variables.limit) ? companiesMock.slice(0, variables.limit)
: mockedCompaniesData; : companiesMock;
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
@ -157,7 +161,7 @@ export const graphqlMocks = {
edges: [ edges: [
{ {
node: { node: {
...mockedDuplicateCompanyData, ...duplicateCompanyMock,
favorites: { favorites: {
edges: [], edges: [],
__typename: 'FavoriteConnection', __typename: 'FavoriteConnection',
@ -197,7 +201,7 @@ export const graphqlMocks = {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
people: { people: {
edges: mockedPeopleData.map((person) => ({ edges: peopleMock.map((person) => ({
node: person, node: person,
cursor: null, cursor: null,
})), })),

View File

@ -1,210 +1,393 @@
import { Company } from '@/companies/types/Company'; export const getCompaniesMock = () => {
import { Favorite } from '@/favorites/types/Favorite'; return companiesQueryResult.companies.edges.map((edge) => edge.node);
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 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', __typename: 'Company',
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', };
domainName: 'airbnb.com', };
name: 'Airbnb',
createdAt: '2023-04-26T10:08:54.724515+00:00', export const companiesQueryResult = {
updatedAt: '2023-04-26T10:23:42.33625+00:00', companies: {
address: '17 rue de clignancourt', __typename: 'CompanyConnection',
employees: 12, totalCount: 13,
linkedinLink: { pageInfo: {
url: 'https://www.linkedin.com/company/airbnb/', __typename: 'PageInfo',
label: 'https://www.linkedin.com/company/airbnb/', hasNextPage: false,
startCursor:
'WzEsICIyMDIwMjAyMC0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
endCursor: 'WzEzLCAiMjAyMDIwMjAtMTQ1NS00YzU3LWFmYWYtZGQ1ZGMwODYzNjFkIl0=',
}, },
xLink: { edges: [
url: 'https://twitter.com/airbnb', {
label: 'https://twitter.com/airbnb', __typename: 'CompanyEdge',
}, cursor: 'WzEsICIyMDIwMjAyMC0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
annualRecurringRevenue: { amountMicros: 5000000, currencyCode: 'USD' }, node: {
idealCustomerProfile: true, __typename: 'Company',
Favorite: null, id: '20202020-3ec3-4fe3-8997-b76aa0bfa408',
accountOwnerId: mockedUsersData[0].id, employees: 100,
accountOwner: { createdAt: '2024-06-05T09:00:20.412Z',
__typename: 'WorkspaceMember', name: 'Linkedin',
name: { accountOwner: null,
firstName: 'Charles', domainName: 'linkedin.com',
lastName: 'Test', 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, __typename: 'CompanyEdge',
locale: 'en', cursor: 'WzIsICIyMDIwMjAyMC01ZDgxLTQ2ZDYtYmY4My1mN2ZkMzNlYTYxMDIiXQ==',
colorScheme: 'Light', node: {
updatedAt: '2023-04-26T10:23:42.33625+00:00', __typename: 'Company',
createdAt: '2023-04-26T10:23:42.33625+00:00', id: '20202020-5d81-46d6-bf83-f7fd33ea6102',
userId: mockedUsersData[0].id, employees: null,
userEmail: 'charles@test.com', 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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/standard-metadata-query-result'; import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/standard-metadata-query-result';
export const generatedMockObjectMetadataItems: ObjectMetadataItem[] = export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
@ -10,431 +6,3 @@ export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
...edge.node, ...edge.node,
fields: edge.node.fields.edges.map((edge) => 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 { isDate, isNumber, isString } from '@sniptt/guards';
import { differenceInCalendarDays, formatDistanceToNow } from 'date-fns'; import { differenceInCalendarDays, formatDistanceToNow } from 'date-fns';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import moize from 'moize';
import { isDefined } from '~/utils/isDefined';
import { logError } from './logError'; import { logError } from './logError';
@ -133,3 +136,75 @@ export const beautifyDateDiff = (
if (![0, 1].includes(dateDiff.days)) result = result + 's'; if (![0, 1].includes(dateDiff.days)) result = result + 's';
return result; 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) => { export const sanitizeURL = (link: string | null | undefined) => {
return link return link
? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '') ? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '')

View File

@ -49,8 +49,21 @@ export default defineConfig(({ command, mode }) => {
}), }),
svgr(), svgr(),
checker(checkers), 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({ 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: { babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'], presets: ['@babel/preset-typescript', '@babel/preset-react'],
}, },

View File

@ -3,7 +3,11 @@ import { ThemeProvider } from '@emotion/react';
import { Preview } from '@storybook/react'; import { Preview } from '@storybook/react';
import { useDarkMode } from 'storybook-dark-mode'; 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 = { const preview: Preview = {
decorators: [ decorators: [
@ -18,7 +22,9 @@ const preview: Preview = {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Story /> <ThemeContextProvider theme={theme}>
<Story />
</ThemeContextProvider>
</ThemeProvider> </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 { 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 { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
import styles from './Chip.module.css';
export enum ChipSize { export enum ChipSize {
Large = 'large', Large = 'large',
Small = 'small', Small = 'small',
@ -34,9 +33,86 @@ type ChipProps = {
rightComponent?: ReactNode; rightComponent?: ReactNode;
className?: string; className?: string;
onClick?: (event: MouseEvent<HTMLDivElement>) => void; 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 = ({ export const Chip = ({
size = ChipSize.Small, size = ChipSize.Small,
label, label,
@ -49,30 +125,21 @@ export const Chip = ({
onClick, onClick,
}: ChipProps) => { }: ChipProps) => {
return ( return (
<div <StyledContainer
data-testid="chip" data-testid="chip"
className={clsx({ accent={accent}
[styles.chip]: true, clickable={clickable}
[styles.clickable]: clickable, disabled={disabled}
[styles.disabled]: disabled, size={size}
[styles.accentTextPrimary]: accent === ChipAccent.TextPrimary, variant={variant}
[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,
})}
onClick={onClick} onClick={onClick}
> >
{leftComponent} {leftComponent}
<div className={styles.label}> <OverflowingTextWithTooltip
<OverflowingTextWithTooltip size={size === ChipSize.Large ? 'large' : 'small'}
size={size === ChipSize.Large ? 'large' : 'small'} text={label}
text={label} />
/>
</div>
{rightComponent} {rightComponent}
</div> </StyledContainer>
); );
}; };

View File

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

View File

@ -1,6 +1,7 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within } from '@storybook/test'; import { expect, fn, userEvent, within } from '@storybook/test';
import { IconUser } from '@ui/display/icon/components/TablerIcons';
import { import {
CatalogDecorator, CatalogDecorator,
CatalogStory, 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> = { export const Catalog: CatalogStory<Story, typeof Tag> = {
argTypes: { argTypes: {
color: { control: false }, color: { control: false },

View File

@ -1,11 +1,38 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import clsx from 'clsx'; import { styled } from '@linaria/react';
import { v4 as uuidV4 } from 'uuid';
import { THEME_COMMON } from '@ui/theme';
import { AppTooltip } from './AppTooltip'; 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 = ({ export const OverflowingTextWithTooltip = ({
size = 'small', size = 'small',
@ -16,7 +43,7 @@ export const OverflowingTextWithTooltip = ({
text: string | null | undefined; text: string | null | undefined;
mutliline?: boolean; mutliline?: boolean;
}) => { }) => {
const textElementId = `title-id-${uuidV4()}`; const textElementId = `title-id-${+new Date()}`;
const textRef = useRef<HTMLDivElement>(null); const textRef = useRef<HTMLDivElement>(null);
@ -43,20 +70,17 @@ export const OverflowingTextWithTooltip = ({
return ( return (
<> <>
<div <StyledOverflowingText
data-testid="tooltip" data-testid="tooltip"
className={clsx({ cursorPointer={isTitleOverflowing}
[styles.main]: true, size={size}
[styles.cursor]: isTitleOverflowing,
[styles.large]: size === 'large',
})}
ref={textRef} ref={textRef}
id={textElementId} id={textElementId}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
{text} {text}
</div> </StyledOverflowingText>
{isTitleOverflowing && {isTitleOverflowing &&
createPortal( createPortal(
<div onClick={handleTooltipClick}> <div onClick={handleTooltipClick}>

View File

@ -34,6 +34,7 @@ export * from './constants/TextInputStyle';
export * from './constants/ThemeCommon'; export * from './constants/ThemeCommon';
export * from './constants/ThemeDark'; export * from './constants/ThemeDark';
export * from './constants/ThemeLight'; export * from './constants/ThemeLight';
export * from './provider/ThemeContextProvider';
export * from './provider/ThemeProvider'; export * from './provider/ThemeProvider';
export * from './types/ThemeType'; export * from './types/ThemeType';
export * from './utils/getNextThemeColor'; 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 { ReactNode, useEffect } from 'react';
import { ThemeProvider as EmotionThemeProvider } from '@emotion/react'; import { ThemeProvider as EmotionThemeProvider } from '@emotion/react';
import { ThemeContextProvider } from '@ui/theme/provider/ThemeContextProvider';
import { ThemeType } from '..'; import { ThemeType } from '..';
import './theme.css'; import './theme.css';
@ -16,7 +18,11 @@ const ThemeProvider = ({ theme, children }: ThemeProviderProps) => {
theme.name === 'dark' ? 'dark' : 'light'; theme.name === 'dark' ? 'dark' : 'light';
}, [theme]); }, [theme]);
return <EmotionThemeProvider theme={theme}>{children}</EmotionThemeProvider>; return (
<EmotionThemeProvider theme={theme}>
<ThemeContextProvider theme={theme}>{children}</ThemeContextProvider>
</EmotionThemeProvider>
);
}; };
export default ThemeProvider; export default ThemeProvider;

View File

@ -1,5 +1,6 @@
/// <reference types='vitest' /> /// <reference types='vitest' />
import react from '@vitejs/plugin-react-swc'; import react from '@vitejs/plugin-react-swc';
import wyw from '@wyw-in-js/vite';
import * as path from 'path'; import * as path from 'path';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import checker from 'vite-plugin-checker'; import checker from 'vite-plugin-checker';
@ -27,6 +28,16 @@ export default defineConfig({
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'), 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. // Configuration for building your library.

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