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:
@ -9,7 +9,7 @@ import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql';
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||
|
||||
const StyledCardContent = styled(CardContent)<{
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
GenericFieldContextType,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
|
||||
const StyledRow = styled.div`
|
||||
align-items: center;
|
||||
|
||||
@ -13,6 +13,8 @@ import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWith
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||
import { getPeopleMock } from '~/testing/mock-data/people';
|
||||
import {
|
||||
mockDefaultWorkspace,
|
||||
mockedWorkspaceMemberData,
|
||||
@ -21,6 +23,9 @@ import { sleep } from '~/testing/sleep';
|
||||
|
||||
import { CommandMenu } from '../CommandMenu';
|
||||
|
||||
const peopleMock = getPeopleMock();
|
||||
const companiesMock = getCompaniesMock();
|
||||
|
||||
const openTimeout = 50;
|
||||
|
||||
const meta: Meta<typeof CommandMenu> = {
|
||||
@ -94,8 +99,12 @@ export const MatchingPersonCompanyActivityCreateNavigate: Story = {
|
||||
const searchInput = await canvas.findByPlaceholderText('Search');
|
||||
await sleep(openTimeout);
|
||||
await userEvent.type(searchInput, 'n');
|
||||
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Airbnb')).toBeInTheDocument();
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
peopleMock[0].name.firstName + ' ' + peopleMock[0].name.lastName,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(await canvas.findByText(companiesMock[0].name)).toBeInTheDocument();
|
||||
expect(await canvas.findByText('My very first note')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Create Note')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Go to Companies')).toBeInTheDocument();
|
||||
@ -119,7 +128,11 @@ export const AtleastMatchingOnePerson: Story = {
|
||||
const searchInput = await canvas.findByPlaceholderText('Search');
|
||||
await sleep(openTimeout);
|
||||
await userEvent.type(searchInput, 'alex');
|
||||
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
peopleMock[0].name.firstName + ' ' + peopleMock[0].name.lastName,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -3,10 +3,12 @@ import {
|
||||
mockedObjectMetadataItems,
|
||||
mockedPersonObjectMetadataItem,
|
||||
} from '~/testing/mock-data/metadata';
|
||||
import { mockedPeopleData } from '~/testing/mock-data/people';
|
||||
import { getPeopleMock } from '~/testing/mock-data/people';
|
||||
|
||||
import { getRecordNodeFromRecord } from '../getRecordNodeFromRecord';
|
||||
|
||||
const peopleMock = getPeopleMock();
|
||||
|
||||
describe('getRecordNodeFromRecord', () => {
|
||||
it('computes relation records cache references by default', () => {
|
||||
// Given
|
||||
@ -19,7 +21,7 @@ describe('getRecordNodeFromRecord', () => {
|
||||
name: true,
|
||||
company: true,
|
||||
};
|
||||
const record = mockedPeopleData[0];
|
||||
const record = peopleMock[0];
|
||||
|
||||
// When
|
||||
const result = getRecordNodeFromRecord({
|
||||
@ -33,12 +35,12 @@ describe('getRecordNodeFromRecord', () => {
|
||||
expect(result).toEqual({
|
||||
__typename: 'Person',
|
||||
company: {
|
||||
__ref: 'Company:5c21e19e-e049-4393-8c09-3e3f8fb09ecb',
|
||||
__ref: `Company:${record.company.id}`,
|
||||
},
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'Alexandre',
|
||||
lastName: 'Prot',
|
||||
firstName: record.name.firstName,
|
||||
lastName: record.name.lastName,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -54,7 +56,7 @@ describe('getRecordNodeFromRecord', () => {
|
||||
name: true,
|
||||
company: true,
|
||||
};
|
||||
const record = mockedPeopleData[0];
|
||||
const record = peopleMock[0];
|
||||
const computeReferences = false;
|
||||
|
||||
// When
|
||||
@ -72,8 +74,8 @@ describe('getRecordNodeFromRecord', () => {
|
||||
company: record.company,
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'Alexandre',
|
||||
lastName: 'Prot',
|
||||
firstName: record.name.firstName,
|
||||
lastName: record.name.lastName,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { mockedPeopleData } from '~/testing/mock-data/people';
|
||||
import { getPeopleMock } from '~/testing/mock-data/people';
|
||||
|
||||
const peopleMock = getPeopleMock();
|
||||
|
||||
export const query = gql`
|
||||
query FindDuplicatePerson($id: ID!) {
|
||||
@ -49,11 +51,11 @@ export const responseData = {
|
||||
personDuplicates: {
|
||||
edges: [
|
||||
{
|
||||
node: { ...mockedPeopleData[0], updatedAt: '' },
|
||||
node: { ...peopleMock[0], updatedAt: '' },
|
||||
cursor: 'cursor1',
|
||||
},
|
||||
{
|
||||
node: { ...mockedPeopleData[1], updatedAt: '' },
|
||||
node: { ...peopleMock[1], updatedAt: '' },
|
||||
cursor: 'cursor2',
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { useAddressField } from '@/object-record/record-field/meta-types/hooks/useAddressField';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { useAddressFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useAddressFieldDisplay';
|
||||
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
|
||||
|
||||
export const AddressFieldDisplay = () => {
|
||||
const { fieldValue } = useAddressField();
|
||||
const { fieldValue } = useAddressFieldDisplay();
|
||||
|
||||
const content = [
|
||||
fieldValue?.addressStreet1,
|
||||
@ -10,7 +12,7 @@ export const AddressFieldDisplay = () => {
|
||||
fieldValue?.addressCity,
|
||||
fieldValue?.addressCountry,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.filter(isNonEmptyString)
|
||||
.join(', ');
|
||||
|
||||
return <TextDisplay text={content} />;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useBooleanField } from '@/object-record/record-field/meta-types/hooks/useBooleanField';
|
||||
import { useBooleanFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useBooleanFieldDisplay';
|
||||
import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';
|
||||
|
||||
export const BooleanFieldDisplay = () => {
|
||||
const { fieldValue } = useBooleanField();
|
||||
const { fieldValue } = useBooleanFieldDisplay();
|
||||
|
||||
return <BooleanDisplay value={fieldValue} />;
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useCurrencyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useCurrencyFieldDisplay';
|
||||
import { CurrencyDisplay } from '@/ui/field/display/components/CurrencyDisplay';
|
||||
|
||||
import { useCurrencyField } from '../../hooks/useCurrencyField';
|
||||
|
||||
export const CurrencyFieldDisplay = () => {
|
||||
const { fieldValue } = useCurrencyField();
|
||||
const { fieldValue } = useCurrencyFieldDisplay();
|
||||
|
||||
return <CurrencyDisplay currencyValue={fieldValue} />;
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useDateFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useDateFieldDisplay';
|
||||
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
|
||||
|
||||
import { useDateField } from '../../hooks/useDateField';
|
||||
|
||||
export const DateFieldDisplay = () => {
|
||||
const { fieldValue } = useDateField();
|
||||
const { fieldValue } = useDateFieldDisplay();
|
||||
|
||||
return <DateDisplay value={fieldValue} />;
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useDateTimeFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useDateTimeFieldDisplay';
|
||||
import { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay';
|
||||
|
||||
import { useDateTimeField } from '../../hooks/useDateTimeField';
|
||||
|
||||
export const DateTimeFieldDisplay = () => {
|
||||
const { fieldValue } = useDateTimeField();
|
||||
const { fieldValue } = useDateTimeFieldDisplay();
|
||||
|
||||
return <DateTimeDisplay value={fieldValue} />;
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useEmailFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useEmailFieldDisplay';
|
||||
import { EmailDisplay } from '@/ui/field/display/components/EmailDisplay';
|
||||
|
||||
import { useEmailField } from '../../hooks/useEmailField';
|
||||
|
||||
export const EmailFieldDisplay = () => {
|
||||
const { fieldValue } = useEmailField();
|
||||
const { fieldValue } = useEmailFieldDisplay();
|
||||
|
||||
return <EmailDisplay value={fieldValue} />;
|
||||
};
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { useFullNameFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useFullNameFieldDisplay';
|
||||
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
|
||||
|
||||
export const FullNameFieldDisplay = () => {
|
||||
const { fieldValue } = useFullNameField();
|
||||
const { fieldValue } = useFullNameFieldDisplay();
|
||||
|
||||
const content = [fieldValue.firstName, fieldValue.lastName]
|
||||
.filter(Boolean)
|
||||
const content = [fieldValue?.firstName, fieldValue?.lastName]
|
||||
.filter(isNonEmptyString)
|
||||
.join(' ');
|
||||
|
||||
return <TextDisplay text={content} />;
|
||||
|
||||
@ -1,19 +1,15 @@
|
||||
import { useJsonField } from '@/object-record/record-field/meta-types/hooks/useJsonField';
|
||||
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
||||
import { useJsonFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useJsonFieldDisplay';
|
||||
import { JsonDisplay } from '@/ui/field/display/components/JsonDisplay';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const JsonFieldDisplay = () => {
|
||||
const { fieldValue, maxWidth } = useJsonField();
|
||||
const { fieldValue, maxWidth } = useJsonFieldDisplay();
|
||||
|
||||
return (
|
||||
<JsonDisplay
|
||||
text={
|
||||
isFieldRawJsonValue(fieldValue) && isDefined(fieldValue)
|
||||
? JSON.stringify(fieldValue)
|
||||
: ''
|
||||
}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
);
|
||||
if (!isDefined(fieldValue)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const value = JSON.stringify(fieldValue);
|
||||
|
||||
return <JsonDisplay text={value} maxWidth={maxWidth} />;
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useLinkFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useLinkFieldDisplay';
|
||||
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
|
||||
|
||||
import { useLinkField } from '../../hooks/useLinkField';
|
||||
|
||||
export const LinkFieldDisplay = () => {
|
||||
const { fieldValue } = useLinkField();
|
||||
const { fieldValue } = useLinkFieldDisplay();
|
||||
|
||||
return <LinkDisplay value={fieldValue} />;
|
||||
};
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
||||
import { useLinksFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useLinksFieldDisplay';
|
||||
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
|
||||
|
||||
export const LinksFieldDisplay = () => {
|
||||
const { fieldValue } = useLinksField();
|
||||
const { fieldValue } = useLinksFieldDisplay();
|
||||
|
||||
const { isFocused } = useFieldFocus();
|
||||
|
||||
|
||||
@ -1,23 +1,39 @@
|
||||
import { Tag } from 'twenty-ui';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Tag, THEME_COMMON } from 'twenty-ui';
|
||||
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
|
||||
import { useMultiSelectFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useMultiSelectFieldDisplay';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
|
||||
const spacing1 = THEME_COMMON.spacing(1);
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${spacing1};
|
||||
justify-content: flex-start;
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const MultiSelectFieldDisplay = () => {
|
||||
const { fieldValues, fieldDefinition } = useMultiSelectField();
|
||||
const { fieldValue, fieldDefinition } = useMultiSelectFieldDisplay();
|
||||
|
||||
const { isFocused } = useFieldFocus();
|
||||
|
||||
const selectedOptions = fieldValues
|
||||
const selectedOptions = fieldValue
|
||||
? fieldDefinition.metadata.options?.filter((option) =>
|
||||
fieldValues.includes(option.value),
|
||||
fieldValue.includes(option.value),
|
||||
)
|
||||
: [];
|
||||
|
||||
if (!selectedOptions) return null;
|
||||
|
||||
return (
|
||||
return isFocused ? (
|
||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||
{selectedOptions.map((selectedOption, index) => (
|
||||
<Tag
|
||||
@ -27,5 +43,16 @@ export const MultiSelectFieldDisplay = () => {
|
||||
/>
|
||||
))}
|
||||
</ExpandableList>
|
||||
) : (
|
||||
<StyledContainer>
|
||||
{selectedOptions.map((selectedOption, index) => (
|
||||
<Tag
|
||||
preventShrink
|
||||
key={index}
|
||||
color={selectedOption.color}
|
||||
text={selectedOption.label}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useNumberFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useNumberFieldDisplay';
|
||||
import { NumberDisplay } from '@/ui/field/display/components/NumberDisplay';
|
||||
|
||||
import { useNumberField } from '../../hooks/useNumberField';
|
||||
|
||||
export const NumberFieldDisplay = () => {
|
||||
const { fieldValue } = useNumberField();
|
||||
const { fieldValue } = useNumberFieldDisplay();
|
||||
|
||||
return <NumberDisplay value={fieldValue} />;
|
||||
};
|
||||
|
||||
@ -1,17 +1,24 @@
|
||||
import { Tag } from 'twenty-ui';
|
||||
|
||||
import { useSelectField } from '../../hooks/useSelectField';
|
||||
import { useSelectFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useSelectFieldDisplay';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const SelectFieldDisplay = () => {
|
||||
const { fieldValue, fieldDefinition } = useSelectField();
|
||||
const { fieldValue, fieldDefinition } = useSelectFieldDisplay();
|
||||
|
||||
const selectedOption = fieldDefinition.metadata.options?.find(
|
||||
(option) => option.value === fieldValue,
|
||||
);
|
||||
|
||||
return selectedOption ? (
|
||||
<Tag color={selectedOption.color} text={selectedOption.label} />
|
||||
) : (
|
||||
<></>
|
||||
if (!isDefined(selectedOption)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag
|
||||
preventShrink
|
||||
color={selectedOption.color}
|
||||
text={selectedOption.label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useTextFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useTextFieldDisplay';
|
||||
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
|
||||
|
||||
import { useTextField } from '../../hooks/useTextField';
|
||||
|
||||
export const TextFieldDisplay = () => {
|
||||
const { fieldValue, maxWidth } = useTextField();
|
||||
const { fieldValue, maxWidth } = useTextFieldDisplay();
|
||||
|
||||
return <TextDisplay text={fieldValue} maxWidth={maxWidth} />;
|
||||
};
|
||||
|
||||
@ -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 },
|
||||
},
|
||||
};
|
||||
@ -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 },
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -33,9 +33,6 @@ export const Elipsis: Story = {
|
||||
};
|
||||
|
||||
export const WrongNumber: Story = {
|
||||
parameters: {
|
||||
container: { width: 50 },
|
||||
},
|
||||
decorators: [getFieldDecorator('person', 'phone', 'sdklaskdj')],
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
@ -1,54 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
|
||||
import { FieldContext } from '../../../../contexts/FieldContext';
|
||||
import { useTextField } from '../../../hooks/useTextField';
|
||||
import { TextFieldDisplay } from '../TextFieldDisplay';
|
||||
|
||||
const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
|
||||
const { setFieldValue } = useTextField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
import { TextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/TextFieldDisplay';
|
||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/TextFieldDisplay',
|
||||
decorators: [
|
||||
(Story, { args }) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: '',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'text',
|
||||
label: 'Text',
|
||||
type: FieldMetadataType.Text,
|
||||
iconName: 'IconLink',
|
||||
metadata: {
|
||||
fieldName: 'Text',
|
||||
placeHolder: 'Text',
|
||||
objectMetadataNameSingular: 'person',
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
}}
|
||||
>
|
||||
<TextFieldValueSetterEffect value={args.value} />
|
||||
<Story />
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
MemoryRouterDecorator,
|
||||
getFieldDecorator('person', 'city'),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: TextFieldDisplay,
|
||||
args: {
|
||||
value: 'Lorem ipsum',
|
||||
args: {},
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
};
|
||||
|
||||
@ -59,11 +27,21 @@ type Story = StoryObj<typeof TextFieldDisplay>;
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Elipsis: Story = {
|
||||
args: {
|
||||
value:
|
||||
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae rerum fugiat veniam illum accusantium saepe, voluptate inventore libero doloribus doloremque distinctio blanditiis amet quis dolor a nulla? Placeat nam itaque rerum esse quidem animi, temporibus saepe debitis commodi quia eius eos minus inventore. Voluptates fugit optio sit ab consectetur ipsum, neque eius atque blanditiis. Ullam provident at porro minima, nobis vero dicta consequatur maxime laboriosam fugit repudiandae repellat tempore voluptas non voluptatibus neque aliquam ducimus doloribus ipsa? Sapiente suscipit unde modi commodi possimus doloribus eum voluptatibus, architecto laudantium, magnam, eos numquam exercitationem est maxime explicabo odio nemo qui distinctio temporibus.',
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 100 },
|
||||
},
|
||||
decorators: [
|
||||
getFieldDecorator(
|
||||
'person',
|
||||
'city',
|
||||
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae rerum fugiat veniam illum accusantium saepe, voluptate inventore libero doloribus doloremque distinctio blanditiis amet quis dolor a nulla? Placeat nam itaque rerum esse quidem animi, temporibus saepe debitis commodi quia eius eos minus inventore. Voluptates fugit optio sit ab consectetur ipsum, neque eius atque blanditiis. Ullam provident at porro minima, nobis vero dicta consequatur maxime laboriosam fugit repudiandae repellat tempore voluptas non voluptatibus neque aliquam ducimus doloribus ipsa? Sapiente suscipit unde modi commodi possimus doloribus eum voluptatibus, architecto laudantium, magnam, eos numquam exercitationem est maxime explicabo odio nemo qui distinctio temporibus.',
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'TextFieldDisplay',
|
||||
averageThresholdInMs: 0.5,
|
||||
numberOfRuns: 50,
|
||||
numberOfTestsPerRun: 100,
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -9,7 +9,10 @@ export const usePhoneFieldDisplay = () => {
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const fieldValue = useRecordFieldValue(entityId, fieldName);
|
||||
const fieldValue = useRecordFieldValue<string | undefined>(
|
||||
entityId,
|
||||
fieldName,
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
|
||||
@ -3,6 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -32,7 +33,10 @@ export const useRelationFieldDisplay = () => {
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const fieldValue = useRecordFieldValue(entityId, fieldName);
|
||||
const fieldValue = useRecordFieldValue<ObjectRecord | undefined>(
|
||||
entityId,
|
||||
fieldName,
|
||||
);
|
||||
|
||||
const maxWidthForField =
|
||||
isDefined(button) && isDefined(maxWidth)
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -1,20 +1,26 @@
|
||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
import { mockObjectMetadataItem } from '~/testing/mock-data/objectMetadataItems';
|
||||
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
|
||||
|
||||
import { isRecordMatchingFilter } from './isRecordMatchingFilter';
|
||||
|
||||
const companiesMock = getCompaniesMock();
|
||||
|
||||
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
)!;
|
||||
|
||||
describe('isRecordMatchingFilter', () => {
|
||||
describe('Empty Filters', () => {
|
||||
it('matches any record when no filter is provided', () => {
|
||||
const emptyFilter = {};
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
companiesMock.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter: emptyFilter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@ -26,12 +32,12 @@ describe('isRecordMatchingFilter', () => {
|
||||
employees: {},
|
||||
};
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
companiesMock.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter: filterWithEmptyFields,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@ -40,12 +46,12 @@ describe('isRecordMatchingFilter', () => {
|
||||
it('matches any record with an empty and filter', () => {
|
||||
const filter = { and: [] };
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
companiesMock.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@ -54,12 +60,12 @@ describe('isRecordMatchingFilter', () => {
|
||||
it('matches any record with an empty or filter', () => {
|
||||
const filter = { or: [] };
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
companiesMock.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@ -68,12 +74,12 @@ describe('isRecordMatchingFilter', () => {
|
||||
it('matches any record with an empty not filter', () => {
|
||||
const filter = { not: {} };
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
companiesMock.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@ -82,92 +88,161 @@ describe('isRecordMatchingFilter', () => {
|
||||
|
||||
describe('Simple Filters', () => {
|
||||
it('matches a record with a simple equality filter on name', () => {
|
||||
const filter = { name: { eq: 'Airbnb' } };
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
name: companyMockInFilter.name + 'Different',
|
||||
};
|
||||
|
||||
const filter = { name: { eq: companyMockInFilter.name } };
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0],
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1],
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches a record with a simple equality filter on domainName', () => {
|
||||
const filter = { domainName: { eq: 'airbnb.com' } };
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
domainName: companyMockInFilter.domainName + 'Different',
|
||||
};
|
||||
|
||||
const filter = { domainName: { eq: companyMockInFilter.domainName } };
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0],
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1],
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches a record with a greater than filter on employees', () => {
|
||||
const filter = { employees: { gt: 10 } };
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
employees: 100,
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
employees: companyMockInFilter.employees - 50,
|
||||
};
|
||||
|
||||
const filter = {
|
||||
employees: { gt: companyMockInFilter.employees - 1 },
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0],
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1],
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches a record with a boolean filter on idealCustomerProfile', () => {
|
||||
const filter = { idealCustomerProfile: { eq: true } };
|
||||
const companyIdealCustomerProfileTrue = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: true,
|
||||
};
|
||||
|
||||
const companyIdealCustomerProfileFalse = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: false,
|
||||
};
|
||||
|
||||
const filter = {
|
||||
idealCustomerProfile: {
|
||||
eq: companyIdealCustomerProfileTrue.idealCustomerProfile,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0],
|
||||
record: companyIdealCustomerProfileTrue,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
).toBe(companyIdealCustomerProfileTrue.idealCustomerProfile);
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[4], // Assuming this record has idealCustomerProfile as false
|
||||
record: companyIdealCustomerProfileFalse,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
).toBe(companyIdealCustomerProfileFalse.idealCustomerProfile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex And/Or/Not Nesting', () => {
|
||||
it('matches record with a combination of and + or filters', () => {
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: true,
|
||||
employees: 100,
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: false,
|
||||
employees: 0,
|
||||
};
|
||||
|
||||
const filter: RecordGqlOperationFilter = {
|
||||
and: [
|
||||
{ domainName: { eq: 'airbnb.com' } },
|
||||
{
|
||||
domainName: {
|
||||
eq: companyMockInFilter.domainName,
|
||||
},
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{ employees: { gt: 10 } },
|
||||
{ idealCustomerProfile: { eq: true } },
|
||||
{
|
||||
employees: {
|
||||
gt: companyMockInFilter.employees - 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
idealCustomerProfile: {
|
||||
eq: companyMockInFilter.idealCustomerProfile,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -175,118 +250,181 @@ describe('isRecordMatchingFilter', () => {
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1], // Aircall
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches record with nested not filter', () => {
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: true,
|
||||
employees: 100,
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: false,
|
||||
name: companyMockInFilter.name + 'Different',
|
||||
};
|
||||
|
||||
const filter: RecordGqlOperationFilter = {
|
||||
not: {
|
||||
and: [
|
||||
{ name: { eq: 'Airbnb' } },
|
||||
{ idealCustomerProfile: { eq: true } },
|
||||
{ name: { eq: companyMockInFilter.name } },
|
||||
{
|
||||
idealCustomerProfile: {
|
||||
eq: companyMockInFilter.idealCustomerProfile,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false); // Should not match as it's Airbnb with idealCustomerProfile true
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[3], // Apple
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true); // Should match as it's not Airbnb
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('matches record with deep nesting of and, or, and not filters', () => {
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: true,
|
||||
employees: 100,
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
domainName: companyMockInFilter.domainName + 'Different',
|
||||
employees: 5,
|
||||
name: companyMockInFilter.name + 'Different',
|
||||
};
|
||||
|
||||
const filter: RecordGqlOperationFilter = {
|
||||
and: [
|
||||
{ domainName: { eq: 'apple.com' } },
|
||||
{ domainName: { eq: companyMockInFilter.domainName } },
|
||||
{
|
||||
or: [{ employees: { eq: 10 } }, { not: { name: { eq: 'Apple' } } }],
|
||||
or: [
|
||||
{ employees: { eq: companyMockInFilter.employees } },
|
||||
{ not: { name: { eq: companyMockInFilter.name } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[3], // Apple
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[4], // Qonto
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches record with and filter at root level', () => {
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: true,
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: false,
|
||||
name: companyMockInFilter.name + 'Different',
|
||||
};
|
||||
|
||||
const filter: RecordGqlOperationFilter = {
|
||||
and: [
|
||||
{ name: { eq: 'Facebook' } },
|
||||
{ idealCustomerProfile: { eq: true } },
|
||||
{ name: { eq: companyMockInFilter.name } },
|
||||
{
|
||||
idealCustomerProfile: {
|
||||
eq: companyMockInFilter.idealCustomerProfile,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[5], // Facebook
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches record with or filter at root level including a not condition', () => {
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: true,
|
||||
employees: 100,
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: false,
|
||||
name: companyMockInFilter.name + 'Different',
|
||||
employees: companyMockInFilter.employees - 1,
|
||||
};
|
||||
|
||||
const filter: RecordGqlOperationFilter = {
|
||||
or: [{ name: { eq: 'Sequoia' } }, { not: { employees: { eq: 1 } } }],
|
||||
or: [
|
||||
{ name: { eq: companyMockInFilter.name } },
|
||||
{ not: { employees: { eq: companyMockInFilter.employees - 1 } } },
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[6], // Sequoia
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1], // Aircall
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
@ -294,49 +432,75 @@ describe('isRecordMatchingFilter', () => {
|
||||
|
||||
describe('Implicit And Conditions', () => {
|
||||
it('matches record with implicit and of multiple operators within the same field', () => {
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: true,
|
||||
employees: 100,
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
idealCustomerProfile: false,
|
||||
name: companyMockInFilter.name + 'Different',
|
||||
employees: companyMockInFilter.employees + 100,
|
||||
};
|
||||
|
||||
const filter = {
|
||||
employees: { gt: 10, lt: 100000 },
|
||||
name: { eq: 'Airbnb' },
|
||||
employees: {
|
||||
gt: companyMockInFilter.employees - 10,
|
||||
lt: companyMockInFilter.employees + 10,
|
||||
},
|
||||
name: { eq: companyMockInFilter.name },
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true); // Matches as Airbnb's employee count is between 10 and 100000
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1], // Aircall
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false); // Does not match as Aircall's employee count is not within the range
|
||||
});
|
||||
|
||||
it('matches record with implicit and within an object passed to or', () => {
|
||||
const companyMockInFilter = {
|
||||
...companiesMock[0],
|
||||
};
|
||||
|
||||
const companyMockNotInFilter = {
|
||||
...companiesMock[0],
|
||||
name: companyMockInFilter.name + 'Different',
|
||||
domainName: companyMockInFilter.name + 'Different',
|
||||
};
|
||||
|
||||
const filter = {
|
||||
or: {
|
||||
name: { eq: 'Airbnb' },
|
||||
domainName: { eq: 'airbnb.com' },
|
||||
name: { eq: companyMockInFilter.name },
|
||||
domainName: { eq: companyMockInFilter.domainName },
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
record: companyMockInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[2], // Algolia
|
||||
record: companyMockNotInFilter,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
@ -6,10 +6,12 @@ import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorato
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||
|
||||
import { RecordDetailDuplicatesSection } from '../RecordDetailDuplicatesSection';
|
||||
|
||||
const companiesMock = getCompaniesMock();
|
||||
|
||||
const meta: Meta<typeof RecordDetailDuplicatesSection> = {
|
||||
title:
|
||||
'Modules/ObjectRecord/RecordShow/RecordDetailSection/RecordDetailDuplicatesSection',
|
||||
@ -21,7 +23,7 @@ const meta: Meta<typeof RecordDetailDuplicatesSection> = {
|
||||
MemoryRouterDecorator,
|
||||
],
|
||||
args: {
|
||||
objectRecordId: mockedCompaniesData[0].id,
|
||||
objectRecordId: companiesMock[0].id,
|
||||
objectNameSingular: CoreObjectNameSingular.Company,
|
||||
},
|
||||
parameters: {
|
||||
|
||||
@ -8,12 +8,16 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
|
||||
import { RecordStoreDecorator } from '~/testing/decorators/RecordStoreDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||
import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||
import { mockedPeopleData } from '~/testing/mock-data/people';
|
||||
import { getPeopleMock } from '~/testing/mock-data/people';
|
||||
|
||||
import { RecordDetailRelationSection } from '../RecordDetailRelationSection';
|
||||
|
||||
const companiesMock = getCompaniesMock();
|
||||
|
||||
const peopleMock = getPeopleMock();
|
||||
|
||||
const meta: Meta<typeof RecordDetailRelationSection> = {
|
||||
title:
|
||||
'Modules/ObjectRecord/RecordShow/RecordDetailSection/RecordDetailRelationSection',
|
||||
@ -22,7 +26,7 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
|
||||
(Story) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: mockedCompaniesData[0].id,
|
||||
entityId: companiesMock[0].id,
|
||||
basePathToShowPage: '/object-record/',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
|
||||
@ -44,7 +48,7 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
|
||||
],
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
records: mockedCompaniesData,
|
||||
records: companiesMock,
|
||||
},
|
||||
};
|
||||
|
||||
@ -58,10 +62,10 @@ export const WithRecords: Story = {
|
||||
parameters: {
|
||||
records: [
|
||||
{
|
||||
...mockedCompaniesData[0],
|
||||
people: mockedPeopleData,
|
||||
...companiesMock[0],
|
||||
people: peopleMock,
|
||||
},
|
||||
...mockedPeopleData,
|
||||
...peopleMock,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@ -36,13 +36,16 @@ export const useRecordValue = (recordId: string) => {
|
||||
return tableValue?.[recordId] as ObjectRecord | undefined;
|
||||
};
|
||||
|
||||
export const useRecordFieldValue = (recordId: string, fieldName: string) => {
|
||||
export const useRecordFieldValue = <T,>(
|
||||
recordId: string,
|
||||
fieldName: string,
|
||||
) => {
|
||||
const recordFieldValues = useContextSelector(
|
||||
RecordFieldValueSelectorContext,
|
||||
(value) => value[0],
|
||||
);
|
||||
|
||||
return recordFieldValues?.[recordId]?.[fieldName];
|
||||
return recordFieldValues?.[recordId]?.[fieldName] as T;
|
||||
};
|
||||
|
||||
export const useSetRecordFieldValue = () => {
|
||||
|
||||
@ -8,16 +8,18 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
|
||||
import { RelationPickerDecorator } from '~/testing/decorators/RelationPickerDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedPeopleData } from '~/testing/mock-data/people';
|
||||
import { getPeopleMock } from '~/testing/mock-data/people';
|
||||
import { sleep } from '~/testing/sleep';
|
||||
|
||||
import { EntityForSelect } from '../../types/EntityForSelect';
|
||||
import { SingleEntitySelect } from '../SingleEntitySelect';
|
||||
|
||||
const entities = mockedPeopleData.map<EntityForSelect>((person) => ({
|
||||
const peopleMock = getPeopleMock();
|
||||
|
||||
const entities = peopleMock.map<EntityForSelect>((person) => ({
|
||||
id: person.id,
|
||||
name: person.name.firstName + ' ' + person.name.lastName,
|
||||
avatarUrl: person.avatarUrl,
|
||||
avatarUrl: 'https://picsum.photos/200',
|
||||
avatarType: 'rounded',
|
||||
record: { ...person, __typename: 'Person' },
|
||||
}));
|
||||
|
||||
@ -4,7 +4,7 @@ import { BlocklistItem } from '@/accounts/types/BlocklistItem';
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
|
||||
type SettingsAccountsEmailsBlocklistTableRowProps = {
|
||||
blocklistItem: BlocklistItem;
|
||||
|
||||
@ -4,7 +4,7 @@ import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist';
|
||||
import { SettingsAccountsEmailsBlocklistTable } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistTable';
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
|
||||
const handleBlockedEmailRemoveJestFn = fn();
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist';
|
||||
import { SettingsAccountsEmailsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistTableRow';
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
|
||||
const onRemoveJestFn = fn();
|
||||
|
||||
|
||||
@ -1,36 +1,32 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCheck, IconX } from 'twenty-ui';
|
||||
import { styled } from '@linaria/react';
|
||||
import { IconCheck, IconX, THEME_COMMON } from 'twenty-ui';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const spacing = THEME_COMMON.spacingMultiplicator * 1;
|
||||
const iconSizeSm = THEME_COMMON.icon.size.sm;
|
||||
|
||||
const StyledBooleanFieldValue = styled.div`
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-left: ${spacing}px;
|
||||
`;
|
||||
|
||||
type BooleanDisplayProps = {
|
||||
value: boolean | null;
|
||||
value: boolean | null | undefined;
|
||||
};
|
||||
|
||||
export const BooleanDisplay = ({ value }: BooleanDisplayProps) => {
|
||||
const theme = useTheme();
|
||||
if (!isDefined(value)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const isTrue = value === true;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDefined(value) ? (
|
||||
<>
|
||||
{value ? (
|
||||
<IconCheck size={theme.icon.size.sm} />
|
||||
) : (
|
||||
<IconX size={theme.icon.size.sm} />
|
||||
)}
|
||||
<StyledBooleanFieldValue>
|
||||
{value ? 'True' : 'False'}
|
||||
</StyledBooleanFieldValue>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{isTrue ? <IconCheck size={iconSizeSm} /> : <IconX size={iconSizeSm} />}
|
||||
<StyledBooleanFieldValue>
|
||||
{isTrue ? 'True' : 'False'}
|
||||
</StyledBooleanFieldValue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
|
||||
import { formatAmount } from '~/utils/format/formatAmount';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type CurrencyDisplayProps = {
|
||||
currencyValue: FieldCurrencyValue | null | undefined;
|
||||
};
|
||||
|
||||
const StyledEllipsisDisplay = styled(EllipsisDisplay)`
|
||||
const StyledEllipsisDisplay = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => {
|
||||
@ -26,9 +29,7 @@ export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => {
|
||||
? SETTINGS_FIELD_CURRENCY_CODES[currencyValue?.currencyCode]?.Icon
|
||||
: null;
|
||||
|
||||
const amountToDisplay = isDefined(currencyValue?.amountMicros)
|
||||
? currencyValue.amountMicros / 1000000
|
||||
: 0;
|
||||
const amountToDisplay = (currencyValue?.amountMicros ?? 0) / 1000000;
|
||||
|
||||
if (!shouldDisplayCurrency) {
|
||||
return <StyledEllipsisDisplay>{0}</StyledEllipsisDisplay>;
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
import { formatISOStringToHumanReadableDate } from '~/utils/date-utils';
|
||||
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type DateDisplayProps = {
|
||||
value: Date | string | null | undefined;
|
||||
value: string | null | undefined;
|
||||
};
|
||||
|
||||
export const DateDisplay = ({ value }: DateDisplayProps) => (
|
||||
<EllipsisDisplay>{value && formatToHumanReadableDate(value)}</EllipsisDisplay>
|
||||
<EllipsisDisplay>
|
||||
{value ? formatISOStringToHumanReadableDate(value) : ''}
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { formatToHumanReadableDateTime } from '~/utils';
|
||||
import { formatISOStringToHumanReadableDateTime } from '~/utils/date-utils';
|
||||
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type DateTimeDisplayProps = {
|
||||
value: Date | string | null | undefined;
|
||||
value: string | null | undefined;
|
||||
};
|
||||
|
||||
export const DateTimeDisplay = ({ value }: DateTimeDisplayProps) => (
|
||||
<EllipsisDisplay>
|
||||
{value && formatToHumanReadableDateTime(value)}
|
||||
{value ? formatISOStringToHumanReadableDateTime(value) : ''}
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
|
||||
@ -1,21 +1,39 @@
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
import { ContactLink } from '@/ui/navigation/link/components/ContactLink';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailPattern.test(email.trim());
|
||||
// const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
// return emailPattern.test(email.trim());
|
||||
|
||||
// Record this without using regex
|
||||
const emailParts = email.split('@');
|
||||
|
||||
if (emailParts.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
type EmailDisplayProps = {
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
export const EmailDisplay = ({ value }: EmailDisplayProps) => (
|
||||
<EllipsisDisplay>
|
||||
{value && validateEmail(value) ? (
|
||||
export const EmailDisplay = ({ value }: EmailDisplayProps) => {
|
||||
if (!isDefined(value)) {
|
||||
return <ContactLink href="#">{value}</ContactLink>;
|
||||
}
|
||||
|
||||
if (!validateEmail(value)) {
|
||||
return <ContactLink href="#">{value}</ContactLink>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EllipsisDisplay>
|
||||
<ContactLink
|
||||
href={`mailto:${value}`}
|
||||
onClick={(event: MouseEvent<HTMLElement>) => {
|
||||
@ -24,8 +42,6 @@ export const EmailDisplay = ({ value }: EmailDisplayProps) => (
|
||||
>
|
||||
{value}
|
||||
</ContactLink>
|
||||
) : (
|
||||
<ContactLink href="#">{value}</ContactLink>
|
||||
)}
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { FieldLinkValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
|
||||
@ -6,34 +6,31 @@ import {
|
||||
LinkType,
|
||||
SocialLink,
|
||||
} from '@/ui/navigation/link/components/SocialLink';
|
||||
import { checkUrlType } from '~/utils/checkUrlType';
|
||||
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
||||
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
||||
|
||||
type LinkDisplayProps = {
|
||||
value?: FieldLinkValue;
|
||||
};
|
||||
|
||||
export const LinkDisplay = ({ value }: LinkDisplayProps) => {
|
||||
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
const url = value?.url;
|
||||
|
||||
const absoluteUrl = getAbsoluteUrl(value?.url || '');
|
||||
const displayedValue = value?.label || getUrlHostName(absoluteUrl);
|
||||
const type = checkUrlType(absoluteUrl);
|
||||
|
||||
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
|
||||
return (
|
||||
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
|
||||
{displayedValue}
|
||||
</SocialLink>
|
||||
);
|
||||
if (!isNonEmptyString(url)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RoundedLink href={absoluteUrl} onClick={handleClick}>
|
||||
{displayedValue}
|
||||
</RoundedLink>
|
||||
);
|
||||
const displayedValue = isNonEmptyString(value?.label)
|
||||
? value?.label
|
||||
: url?.replace(/^http[s]?:\/\/(?:[w]+\.)?/gm, '').replace(/^[w]+\./gm, '');
|
||||
|
||||
const type = displayedValue.startsWith('linkedin.')
|
||||
? LinkType.LinkedIn
|
||||
: displayedValue.startsWith('twitter.')
|
||||
? LinkType.Twitter
|
||||
: LinkType.Url;
|
||||
|
||||
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
|
||||
return <SocialLink href={url} type={type} label={displayedValue} />;
|
||||
}
|
||||
|
||||
return <RoundedLink href={url} label={displayedValue} />;
|
||||
};
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { MouseEventHandler, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { THEME_COMMON } from 'twenty-ui';
|
||||
|
||||
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
@ -17,6 +19,21 @@ type LinksDisplayProps = {
|
||||
isFocused?: boolean;
|
||||
};
|
||||
|
||||
const themeSpacing = THEME_COMMON.spacingMultiplicator;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeSpacing * 1}px;
|
||||
justify-content: flex-start;
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LinksDisplay = ({ value, isFocused }: LinksDisplayProps) => {
|
||||
const links = useMemo(
|
||||
() =>
|
||||
@ -41,21 +58,25 @@ export const LinksDisplay = ({ value, isFocused }: LinksDisplayProps) => {
|
||||
[value?.primaryLinkLabel, value?.primaryLinkUrl, value?.secondaryLinks],
|
||||
);
|
||||
|
||||
const handleClick: MouseEventHandler = (event) => event.stopPropagation();
|
||||
|
||||
return (
|
||||
return isFocused ? (
|
||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||
{links.map(({ url, label, type }, index) =>
|
||||
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
|
||||
<SocialLink key={index} href={url} onClick={handleClick} type={type}>
|
||||
{label}
|
||||
</SocialLink>
|
||||
<SocialLink key={index} href={url} type={type} label={label} />
|
||||
) : (
|
||||
<RoundedLink key={index} href={url} onClick={handleClick}>
|
||||
{label}
|
||||
</RoundedLink>
|
||||
<RoundedLink key={index} href={url} label={label} />
|
||||
),
|
||||
)}
|
||||
</ExpandableList>
|
||||
) : (
|
||||
<StyledContainer>
|
||||
{links.map(({ url, label, type }, index) =>
|
||||
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
|
||||
<SocialLink key={index} href={url} type={type} label={label} />
|
||||
) : (
|
||||
<RoundedLink key={index} href={url} label={label} />
|
||||
),
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -42,17 +42,22 @@ export const URLDisplay = ({ value }: URLDisplayProps) => {
|
||||
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
|
||||
return (
|
||||
<EllipsisDisplay>
|
||||
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
|
||||
{displayedValue}
|
||||
</SocialLink>
|
||||
<SocialLink
|
||||
href={absoluteUrl}
|
||||
onClick={handleClick}
|
||||
type={type}
|
||||
label={displayedValue}
|
||||
/>
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EllipsisDisplay>
|
||||
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
|
||||
{displayedValue}
|
||||
</StyledRawLink>
|
||||
<StyledRawLink
|
||||
href={absoluteUrl}
|
||||
onClick={handleClick}
|
||||
label={displayedValue}
|
||||
/>
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,50 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import { Link as ReactLink } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { Chip, ChipSize, ChipVariant } from 'twenty-ui';
|
||||
import { MouseEvent } from 'react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FONT_COMMON, THEME_COMMON } from 'twenty-ui';
|
||||
|
||||
type RoundedLinkProps = {
|
||||
href: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
label?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
const StyledLink = styled(ReactLink)`
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
const fontSizeMd = FONT_COMMON.size.md;
|
||||
const spacing1 = THEME_COMMON.spacing(1);
|
||||
const spacing3 = THEME_COMMON.spacing(3);
|
||||
|
||||
const spacingMultiplicator = THEME_COMMON.spacingMultiplicator;
|
||||
|
||||
const StyledLink = styled.a`
|
||||
align-items: center;
|
||||
background-color: var(--twentycrm-background-transparent-light);
|
||||
border: 1px solid var(--twentycrm-border-color-medium);
|
||||
|
||||
border-radius: 50px;
|
||||
color: var(--twentycrm-font-color-primary);
|
||||
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-weight: ${fontSizeMd};
|
||||
|
||||
gap: ${spacing1};
|
||||
|
||||
height: ${spacing3};
|
||||
justify-content: center;
|
||||
|
||||
max-width: calc(100% - ${spacingMultiplicator} * 2px);
|
||||
|
||||
max-width: 100%;
|
||||
height: ${({ theme }) => theme.spacing(5)};
|
||||
|
||||
min-width: fit-content;
|
||||
|
||||
overflow: hidden;
|
||||
padding: ${spacing1} ${spacing1};
|
||||
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledChip = styled(Chip)`
|
||||
border-color: ${({ theme }) => theme.border.color.strong};
|
||||
box-sizing: border-box;
|
||||
padding: ${({ theme }) => theme.spacing(0, 2)};
|
||||
max-width: 100%;
|
||||
height: ${({ theme }) => theme.spacing(5)};
|
||||
min-width: 40px;
|
||||
`;
|
||||
export const RoundedLink = ({ label, href, onClick }: RoundedLinkProps) => {
|
||||
if (!isNonEmptyString(label)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export const RoundedLink = ({
|
||||
children,
|
||||
className,
|
||||
href,
|
||||
onClick,
|
||||
}: RoundedLinkProps) => {
|
||||
if (!children) return null;
|
||||
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
onClick?.(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledLink
|
||||
className={className}
|
||||
href={href}
|
||||
target="_blank"
|
||||
to={href}
|
||||
onClick={onClick}
|
||||
rel="noreferrer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<StyledChip
|
||||
label={`${children}`}
|
||||
variant={ChipVariant.Rounded}
|
||||
size={ChipSize.Large}
|
||||
/>
|
||||
{label}
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
@ -11,24 +11,15 @@ export enum LinkType {
|
||||
}
|
||||
|
||||
type SocialLinkProps = {
|
||||
label: string;
|
||||
href: string;
|
||||
children?: React.ReactNode;
|
||||
type: LinkType;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
export const SocialLink = ({
|
||||
children,
|
||||
href,
|
||||
onClick,
|
||||
type,
|
||||
}: SocialLinkProps) => {
|
||||
export const SocialLink = ({ label, href, onClick, type }: SocialLinkProps) => {
|
||||
const displayValue =
|
||||
getDisplayValueByUrlType({ type: type, href: href }) ?? children;
|
||||
getDisplayValueByUrlType({ type: type, href: href }) ?? label;
|
||||
|
||||
return (
|
||||
<RoundedLink href={href} onClick={onClick}>
|
||||
{displayValue}
|
||||
</RoundedLink>
|
||||
);
|
||||
return <RoundedLink href={href} onClick={onClick} label={displayValue} />;
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ const meta: Meta<typeof RoundedLink> = {
|
||||
decorators: [ComponentWithRouterDecorator],
|
||||
args: {
|
||||
href: '/test',
|
||||
children: 'Rounded chip',
|
||||
label: 'Rounded chip',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ const meta: Meta<typeof SocialLink> = {
|
||||
decorators: [ComponentWithRouterDecorator],
|
||||
args: {
|
||||
href: '/test',
|
||||
children: 'Social Link',
|
||||
label: 'Social Link',
|
||||
},
|
||||
};
|
||||
|
||||
@ -25,7 +25,7 @@ const twitter: LinkType = LinkType.Twitter;
|
||||
export const LinkedIn: Story = {
|
||||
args: {
|
||||
href: '/LinkedIn',
|
||||
children: 'LinkedIn',
|
||||
label: 'LinkedIn',
|
||||
onClick: clickJestFn,
|
||||
type: linkedin,
|
||||
},
|
||||
@ -34,7 +34,7 @@ export const LinkedIn: Story = {
|
||||
export const Twitter: Story = {
|
||||
args: {
|
||||
href: '/Twitter',
|
||||
children: 'Twitter',
|
||||
label: 'Twitter',
|
||||
onClick: clickJestFn,
|
||||
type: twitter,
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { THEME_DARK, THEME_LIGHT } from 'twenty-ui';
|
||||
import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui';
|
||||
|
||||
import { useColorScheme } from '../hooks/useColorScheme';
|
||||
import { useSystemColorScheme } from '../hooks/useSystemColorScheme';
|
||||
@ -24,5 +24,9 @@ export const AppThemeProvider = ({ children }: AppThemeProviderProps) => {
|
||||
theme.name === 'dark' ? 'dark' : 'light';
|
||||
}, [theme]);
|
||||
|
||||
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeContextProvider theme={theme}>{children}</ThemeContextProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user