mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 23:37:21 +03:00
Fixed sync between record value context selector and record store (#5517)
This PR introduces many improvements over the new profiling story feature, with new tests and some refactor with main : - Added use-context-selector for getting value faster in display fields and created useRecordFieldValue() hook and RecordValueSetterEffect to synchronize states - Added performance test command in CI - Refactored ExpandableList drill-downs with FieldFocusContext - Refactored field button icon logic into getFieldButtonIcon util - Added RelationFieldDisplay perf story - Added RecordTableCell perf story - First split test of useField.. hook with useRelationFieldDisplay() - Fixed problem with set cell soft focus - Isolated logic between display / soft focus and edit mode in the related components to optimize performances for display mode. - Added warmupRound config for performance story decorator - Added variance in test reporting
This commit is contained in:
parent
82ec30c957
commit
de9321dcd9
@ -181,6 +181,7 @@
|
||||
"tsup": "^8.0.1",
|
||||
"type-fest": "4.10.1",
|
||||
"typeorm": "^0.3.17",
|
||||
"use-context-selector": "^2.0.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
|
@ -53,12 +53,12 @@ export const ActivityTargetsInlineCell = ({
|
||||
/>
|
||||
}
|
||||
label="Relations"
|
||||
displayModeContent={() => (
|
||||
displayModeContent={
|
||||
<ActivityTargetChips
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
||||
/>
|
||||
</RecordFieldInputScope>
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
@ -15,7 +18,7 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
||||
objectMetadataItem,
|
||||
showLabel,
|
||||
labelWidth,
|
||||
}: FieldMetadataItemAsFieldDefinitionProps) => {
|
||||
}: FieldMetadataItemAsFieldDefinitionProps): FieldDefinition<FieldMetadata> => {
|
||||
const relationObjectMetadataItem =
|
||||
field.toRelationMetadata?.fromObjectMetadata ||
|
||||
field.fromRelationMetadata?.toObjectMetadata;
|
||||
@ -24,25 +27,31 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
||||
field.toRelationMetadata?.fromFieldMetadataId ||
|
||||
field.fromRelationMetadata?.toFieldMetadataId;
|
||||
|
||||
const fieldDefintionMetadata = {
|
||||
fieldName: field.name,
|
||||
placeHolder: field.label,
|
||||
relationType: parseFieldRelationType(field),
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular ?? '',
|
||||
relationObjectMetadataNamePlural:
|
||||
relationObjectMetadataItem?.namePlural ?? '',
|
||||
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
|
||||
options: field.options,
|
||||
};
|
||||
|
||||
return {
|
||||
fieldMetadataId: field.id,
|
||||
label: field.label,
|
||||
showLabel,
|
||||
labelWidth,
|
||||
type: field.type,
|
||||
metadata: {
|
||||
fieldName: field.name,
|
||||
placeHolder: field.label,
|
||||
relationType: parseFieldRelationType(field),
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular ?? '',
|
||||
relationObjectMetadataNamePlural:
|
||||
relationObjectMetadataItem?.namePlural ?? '',
|
||||
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
|
||||
options: field.options,
|
||||
},
|
||||
metadata: fieldDefintionMetadata,
|
||||
iconName: field.icon ?? 'Icon123',
|
||||
defaultValue: field.defaultValue,
|
||||
editButtonIcon: getFieldButtonIcon({
|
||||
metadata: fieldDefintionMetadata,
|
||||
type: field.type,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -13,8 +13,10 @@ import {
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
||||
@ -209,6 +211,7 @@ export const RecordBoardCard = () => {
|
||||
|
||||
return (
|
||||
<StyledBoardCardWrapper onContextMenu={handleContextMenu}>
|
||||
<RecordValueSetterEffect recordId={recordId} />
|
||||
<StyledBoardCard
|
||||
ref={cardRef}
|
||||
selected={isCurrentCardSelected}
|
||||
@ -266,6 +269,10 @@ export const RecordBoardCard = () => {
|
||||
type: fieldDefinition.type,
|
||||
metadata: fieldDefinition.metadata,
|
||||
defaultValue: fieldDefinition.defaultValue,
|
||||
editButtonIcon: getFieldButtonIcon({
|
||||
metadata: fieldDefinition.metadata,
|
||||
type: fieldDefinition.type,
|
||||
}),
|
||||
},
|
||||
useUpdateRecord: useUpdateOneRecordHook,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
|
@ -37,17 +37,7 @@ import { isFieldSelect } from '../types/guards/isFieldSelect';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
||||
|
||||
type FieldDisplayProps = {
|
||||
isCellSoftFocused?: boolean;
|
||||
cellElement?: HTMLElement;
|
||||
fromTableCell?: boolean;
|
||||
};
|
||||
|
||||
export const FieldDisplay = ({
|
||||
isCellSoftFocused,
|
||||
cellElement,
|
||||
fromTableCell,
|
||||
}: FieldDisplayProps) => {
|
||||
export const FieldDisplay = () => {
|
||||
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
|
||||
|
||||
const isChipDisplay =
|
||||
@ -78,10 +68,7 @@ export const FieldDisplay = ({
|
||||
) : isFieldLink(fieldDefinition) ? (
|
||||
<LinkFieldDisplay />
|
||||
) : isFieldLinks(fieldDefinition) ? (
|
||||
<LinksFieldDisplay
|
||||
isCellSoftFocused={isCellSoftFocused}
|
||||
fromTableCell={fromTableCell}
|
||||
/>
|
||||
<LinksFieldDisplay />
|
||||
) : isFieldCurrency(fieldDefinition) ? (
|
||||
<CurrencyFieldDisplay />
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
@ -89,11 +76,7 @@ export const FieldDisplay = ({
|
||||
) : isFieldSelect(fieldDefinition) ? (
|
||||
<SelectFieldDisplay />
|
||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||
<MultiSelectFieldDisplay
|
||||
isCellSoftFocused={isCellSoftFocused}
|
||||
cellElement={cellElement}
|
||||
fromTableCell={fromTableCell}
|
||||
/>
|
||||
<MultiSelectFieldDisplay />
|
||||
) : isFieldAddress(fieldDefinition) ? (
|
||||
<AddressFieldDisplay />
|
||||
) : isFieldRawJson(fieldDefinition) ? (
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type FieldFocusContextType = {
|
||||
isFocused: boolean;
|
||||
setIsFocused: (isFocused: boolean) => void;
|
||||
};
|
||||
|
||||
export const FieldFocusContext = createContext<FieldFocusContextType>(
|
||||
{} as FieldFocusContextType,
|
||||
);
|
@ -0,0 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { FieldFocusContext } from '@/object-record/record-field/contexts/FieldFocusContext';
|
||||
|
||||
export const FieldFocusContextProvider = ({ children }: any) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
return (
|
||||
<FieldFocusContext.Provider
|
||||
value={{
|
||||
isFocused,
|
||||
setIsFocused,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FieldFocusContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldFocusContext } from '@/object-record/record-field/contexts/FieldFocusContext';
|
||||
|
||||
export const useFieldFocus = () => {
|
||||
const { isFocused, setIsFocused } = useContext(FieldFocusContext);
|
||||
|
||||
return {
|
||||
isFocused,
|
||||
setIsFocused,
|
||||
};
|
||||
};
|
@ -1,33 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { IconComponent, IconPencil } from 'twenty-ui';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||
import { isFieldLink } from '../types/guards/isFieldLink';
|
||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||
|
||||
export const useGetButtonIcon = (): IconComponent | undefined => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
if (isUndefinedOrNull(fieldDefinition)) return undefined;
|
||||
|
||||
if (
|
||||
isFieldLink(fieldDefinition) ||
|
||||
isFieldEmail(fieldDefinition) ||
|
||||
isFieldPhone(fieldDefinition) ||
|
||||
isFieldDisplayedAsPhone(fieldDefinition) ||
|
||||
isFieldMultiSelect(fieldDefinition) ||
|
||||
(isFieldRelation(fieldDefinition) &&
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
||||
'workspaceMember') ||
|
||||
isFieldLinks(fieldDefinition)
|
||||
) {
|
||||
return IconPencil;
|
||||
}
|
||||
return getFieldButtonIcon(fieldDefinition);
|
||||
};
|
||||
|
@ -1,18 +1,16 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
|
||||
export const useIsFieldEmpty = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
const fieldValue = useRecoilValue(
|
||||
recordStoreFamilySelector({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
recordId: entityId,
|
||||
}),
|
||||
|
||||
const fieldValue = useRecordFieldValue(
|
||||
entityId,
|
||||
fieldDefinition.metadata.fieldName,
|
||||
);
|
||||
|
||||
return isFieldValueEmpty({
|
||||
|
@ -1,22 +1,11 @@
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
||||
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
|
||||
|
||||
type LinksFieldDisplayProps = {
|
||||
isCellSoftFocused?: boolean;
|
||||
fromTableCell?: boolean;
|
||||
};
|
||||
|
||||
export const LinksFieldDisplay = ({
|
||||
isCellSoftFocused,
|
||||
fromTableCell,
|
||||
}: LinksFieldDisplayProps) => {
|
||||
export const LinksFieldDisplay = () => {
|
||||
const { fieldValue } = useLinksField();
|
||||
|
||||
return (
|
||||
<LinksDisplay
|
||||
value={fieldValue}
|
||||
isChipCountDisplayed={isCellSoftFocused}
|
||||
withExpandedListBorder={fromTableCell}
|
||||
/>
|
||||
);
|
||||
const { isFocused } = useFieldFocus();
|
||||
|
||||
return <LinksDisplay value={fieldValue} isFocused={isFocused} />;
|
||||
};
|
||||
|
@ -1,20 +1,14 @@
|
||||
import { Tag } from 'twenty-ui';
|
||||
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
|
||||
type MultiSelectFieldDisplayProps = {
|
||||
isCellSoftFocused?: boolean;
|
||||
cellElement?: HTMLElement;
|
||||
fromTableCell?: boolean;
|
||||
};
|
||||
|
||||
export const MultiSelectFieldDisplay = ({
|
||||
isCellSoftFocused,
|
||||
fromTableCell,
|
||||
}: MultiSelectFieldDisplayProps) => {
|
||||
export const MultiSelectFieldDisplay = () => {
|
||||
const { fieldValues, fieldDefinition } = useMultiSelectField();
|
||||
|
||||
const { isFocused } = useFieldFocus();
|
||||
|
||||
const selectedOptions = fieldValues
|
||||
? fieldDefinition.metadata.options?.filter((option) =>
|
||||
fieldValues.includes(option.value),
|
||||
@ -24,10 +18,7 @@ export const MultiSelectFieldDisplay = ({
|
||||
if (!selectedOptions) return null;
|
||||
|
||||
return (
|
||||
<ExpandableList
|
||||
isChipCountDisplayed={isCellSoftFocused}
|
||||
withExpandedListBorder={fromTableCell}
|
||||
>
|
||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||
{selectedOptions.map((selectedOption, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
import { useRelationField } from '../../hooks/useRelationField';
|
||||
|
||||
export const RelationFieldDisplay = () => {
|
||||
const { fieldValue, fieldDefinition, maxWidth } = useRelationField();
|
||||
const { fieldValue, fieldDefinition, maxWidth } = useRelationFieldDisplay();
|
||||
|
||||
if (
|
||||
!fieldValue ||
|
||||
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordChip
|
||||
|
@ -0,0 +1,86 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RelationFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFieldDisplay';
|
||||
import {
|
||||
RecordFieldValueSelectorContextProvider,
|
||||
useSetRecordValue,
|
||||
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
import { relationFieldDisplayMock } from './mock';
|
||||
|
||||
const RelationFieldValueSetterEffect = () => {
|
||||
const setEntity = useSetRecoilState(
|
||||
recordStoreFamilyState(relationFieldDisplayMock.entityId),
|
||||
);
|
||||
|
||||
const setRelationEntity = useSetRecoilState(
|
||||
recordStoreFamilyState(relationFieldDisplayMock.relationEntityId),
|
||||
);
|
||||
|
||||
const setRecordValue = useSetRecordValue();
|
||||
|
||||
useEffect(() => {
|
||||
setEntity(relationFieldDisplayMock.entityValue);
|
||||
setRelationEntity(relationFieldDisplayMock.relationFieldValue);
|
||||
|
||||
setRecordValue(
|
||||
relationFieldDisplayMock.entityValue.id,
|
||||
relationFieldDisplayMock.entityValue,
|
||||
);
|
||||
setRecordValue(
|
||||
relationFieldDisplayMock.relationFieldValue.id,
|
||||
relationFieldDisplayMock.relationFieldValue,
|
||||
);
|
||||
}, [setEntity, setRelationEntity, setRecordValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/RelationFieldDisplay',
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
(Story) => (
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: relationFieldDisplayMock.entityId,
|
||||
basePathToShowPage: '/object-record/',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
...relationFieldDisplayMock.fieldDefinition,
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
}}
|
||||
>
|
||||
<RelationFieldValueSetterEffect />
|
||||
<Story />
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: RelationFieldDisplay,
|
||||
argTypes: { value: { control: 'date' } },
|
||||
args: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RelationFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'RelationFieldDisplay',
|
||||
averageThresholdInMs: 0.4,
|
||||
numberOfRuns: 20,
|
||||
numberOfTestsPerRun: 100,
|
||||
});
|
@ -0,0 +1,113 @@
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const relationFieldDisplayMock = {
|
||||
entityId: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||
relationEntityId: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
entityValue: {
|
||||
__typename: 'Person',
|
||||
asd: '',
|
||||
city: 'Seattle',
|
||||
jobTitle: '',
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'Lorie',
|
||||
lastName: 'Vladim',
|
||||
},
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
company: {
|
||||
__typename: 'Company',
|
||||
domainName: 'google.com',
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
name: 'Google',
|
||||
annualRecurringRevenue: {
|
||||
__typename: 'Currency',
|
||||
amountMicros: null,
|
||||
currencyCode: '',
|
||||
},
|
||||
employees: null,
|
||||
accountOwnerId: null,
|
||||
address: '',
|
||||
idealCustomerProfile: false,
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
id: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
position: 6,
|
||||
updatedAt: '2024-05-01T13:16:29.046Z',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
id: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||
email: 'lorie.vladim@google.com',
|
||||
phone: '+33788901235',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
tEst: '',
|
||||
position: 15,
|
||||
},
|
||||
relationFieldValue: {
|
||||
__typename: 'Company',
|
||||
domainName: 'microsoft.com',
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
name: 'Microsoft',
|
||||
annualRecurringRevenue: {
|
||||
__typename: 'Currency',
|
||||
amountMicros: null,
|
||||
currencyCode: '',
|
||||
},
|
||||
employees: null,
|
||||
accountOwnerId: null,
|
||||
address: '',
|
||||
idealCustomerProfile: false,
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
id: '20202020-ed89-413a-b31a-962986e67bb4',
|
||||
position: 4,
|
||||
updatedAt: '2024-05-01T13:16:29.046Z',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: '4e79f0b7-d100-4e89-a07b-315a710b8059',
|
||||
label: 'Company',
|
||||
metadata: {
|
||||
fieldName: 'company',
|
||||
placeHolder: 'Company',
|
||||
relationType: 'TO_ONE_OBJECT',
|
||||
relationFieldMetadataId: '01fa2247-7937-4493-b7e2-3d72f05d6d25',
|
||||
relationObjectMetadataNameSingular: 'company',
|
||||
relationObjectMetadataNamePlural: 'companies',
|
||||
objectMetadataNameSingular: 'person',
|
||||
options: null,
|
||||
},
|
||||
iconName: 'IconBuildingSkyscraper',
|
||||
type: FieldMetadataType.Relation,
|
||||
position: 2,
|
||||
size: 150,
|
||||
isLabelIdentifier: false,
|
||||
isVisible: true,
|
||||
viewFieldId: '924f4c94-cbcd-4de5-b7a2-ebae2f0b2c3b',
|
||||
isSortable: false,
|
||||
isFilterable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||
|
||||
export const useRelationFieldDisplay = () => {
|
||||
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.Relation,
|
||||
isFieldRelation,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const button = fieldDefinition.editButtonIcon;
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const fieldValue = useRecordFieldValue(entityId, fieldName);
|
||||
|
||||
const maxWidthForField =
|
||||
isDefined(button) && isDefined(maxWidth)
|
||||
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
|
||||
: maxWidth;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
maxWidth: maxWidthForField,
|
||||
};
|
||||
};
|
@ -1,3 +1,5 @@
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldMetadata } from './FieldMetadata';
|
||||
@ -24,4 +26,5 @@ export type FieldDefinition<T extends FieldMetadata> = {
|
||||
metadata: T;
|
||||
infoTooltipContent?: string;
|
||||
defaultValue?: any;
|
||||
editButtonIcon?: IconComponent;
|
||||
};
|
||||
|
@ -0,0 +1,36 @@
|
||||
import { IconComponent, IconPencil } from 'twenty-ui';
|
||||
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||
import { isFieldLink } from '../types/guards/isFieldLink';
|
||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||
|
||||
export const getFieldButtonIcon = (
|
||||
fieldDefinition:
|
||||
| Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>
|
||||
| undefined
|
||||
| null,
|
||||
): IconComponent | undefined => {
|
||||
if (isUndefinedOrNull(fieldDefinition)) return undefined;
|
||||
|
||||
if (
|
||||
isFieldLink(fieldDefinition) ||
|
||||
isFieldEmail(fieldDefinition) ||
|
||||
isFieldPhone(fieldDefinition) ||
|
||||
isFieldDisplayedAsPhone(fieldDefinition) ||
|
||||
isFieldMultiSelect(fieldDefinition) ||
|
||||
(isFieldRelation(fieldDefinition) &&
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
||||
'workspaceMember') ||
|
||||
isFieldLinks(fieldDefinition)
|
||||
) {
|
||||
return IconPencil;
|
||||
}
|
||||
};
|
@ -16,6 +16,7 @@ import { recordIndexIsCompactModeActiveState } from '@/object-record/record-inde
|
||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
|
||||
import { ViewBar } from '@/views/components/ViewBar';
|
||||
@ -105,74 +106,78 @@ export const RecordIndexContainer = ({
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<SpreadsheetImportProvider>
|
||||
<ViewBar
|
||||
viewBarId={recordIndexId}
|
||||
optionsDropdownButton={
|
||||
<RecordIndexOptionsDropdown
|
||||
recordIndexId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
viewType={recordIndexViewType ?? ViewType.Table}
|
||||
/>
|
||||
}
|
||||
onCurrentViewChange={(view) => {
|
||||
if (!view) {
|
||||
return;
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<SpreadsheetImportProvider>
|
||||
<ViewBar
|
||||
viewBarId={recordIndexId}
|
||||
optionsDropdownButton={
|
||||
<RecordIndexOptionsDropdown
|
||||
recordIndexId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
viewType={recordIndexViewType ?? ViewType.Table}
|
||||
/>
|
||||
}
|
||||
onCurrentViewChange={(view) => {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
onViewFieldsChange(view.viewFields);
|
||||
setTableFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setTableSorts(mapViewSortsToSorts(view.viewSorts, sortDefinitions));
|
||||
setRecordIndexSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexViewType(view.type);
|
||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||
view.kanbanFieldMetadataId,
|
||||
);
|
||||
setRecordIndexIsCompactModeActive(view.isCompact);
|
||||
}}
|
||||
/>
|
||||
<RecordIndexViewBarEffect
|
||||
objectNamePlural={objectNamePlural}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
</SpreadsheetImportProvider>
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
<>
|
||||
<RecordIndexTableContainer
|
||||
recordTableId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
createRecord={createRecord}
|
||||
onViewFieldsChange(view.viewFields);
|
||||
setTableFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setTableSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexViewType(view.type);
|
||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||
view.kanbanFieldMetadataId,
|
||||
);
|
||||
setRecordIndexIsCompactModeActive(view.isCompact);
|
||||
}}
|
||||
/>
|
||||
<RecordIndexTableContainerEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordTableId={recordIndexId}
|
||||
<RecordIndexViewBarEffect
|
||||
objectNamePlural={objectNamePlural}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{recordIndexViewType === ViewType.Kanban && (
|
||||
<>
|
||||
<RecordIndexBoardContainer
|
||||
recordBoardId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
createRecord={createRecord}
|
||||
/>
|
||||
<RecordIndexBoardContainerEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordBoardId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SpreadsheetImportProvider>
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
<>
|
||||
<RecordIndexTableContainer
|
||||
recordTableId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
createRecord={createRecord}
|
||||
/>
|
||||
<RecordIndexTableContainerEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordTableId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{recordIndexViewType === ViewType.Kanban && (
|
||||
<>
|
||||
<RecordIndexBoardContainer
|
||||
recordBoardId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
createRecord={createRecord}
|
||||
/>
|
||||
<RecordIndexBoardContainerEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordBoardId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import { useIcons } from 'twenty-ui';
|
||||
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
||||
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
||||
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
@ -70,45 +71,44 @@ export const RecordInlineCell = ({
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
return (
|
||||
<RecordInlineCellContainer
|
||||
readonly={readonly}
|
||||
buttonIcon={buttonIcon}
|
||||
customEditHotkeyScope={
|
||||
isFieldRelation(fieldDefinition)
|
||||
? {
|
||||
scope: RelationPickerHotkeyScope.RelationPicker,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
IconLabel={
|
||||
fieldDefinition.iconName ? getIcon(fieldDefinition.iconName) : undefined
|
||||
}
|
||||
label={fieldDefinition.label}
|
||||
labelWidth={fieldDefinition.labelWidth}
|
||||
showLabel={fieldDefinition.showLabel}
|
||||
editModeContent={
|
||||
<FieldInput
|
||||
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
|
||||
onEnter={handleEnter}
|
||||
onCancel={handleCancel}
|
||||
onEscape={handleEscape}
|
||||
onSubmit={handleSubmit}
|
||||
onTab={handleTab}
|
||||
onShiftTab={handleShiftTab}
|
||||
onClickOutside={handleClickOutside}
|
||||
isReadOnly={readonly}
|
||||
/>
|
||||
}
|
||||
displayModeContent={({ cellElement, isCellSoftFocused }) => (
|
||||
<FieldDisplay
|
||||
cellElement={cellElement}
|
||||
isCellSoftFocused={isCellSoftFocused}
|
||||
/>
|
||||
)}
|
||||
isDisplayModeContentEmpty={isFieldEmpty}
|
||||
isDisplayModeFixHeight
|
||||
editModeContentOnly={isFieldInputOnly}
|
||||
loading={loading}
|
||||
/>
|
||||
<FieldFocusContextProvider>
|
||||
<RecordInlineCellContainer
|
||||
readonly={readonly}
|
||||
buttonIcon={buttonIcon}
|
||||
customEditHotkeyScope={
|
||||
isFieldRelation(fieldDefinition)
|
||||
? {
|
||||
scope: RelationPickerHotkeyScope.RelationPicker,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
IconLabel={
|
||||
fieldDefinition.iconName
|
||||
? getIcon(fieldDefinition.iconName)
|
||||
: undefined
|
||||
}
|
||||
label={fieldDefinition.label}
|
||||
labelWidth={fieldDefinition.labelWidth}
|
||||
showLabel={fieldDefinition.showLabel}
|
||||
editModeContent={
|
||||
<FieldInput
|
||||
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
|
||||
onEnter={handleEnter}
|
||||
onCancel={handleCancel}
|
||||
onEscape={handleEscape}
|
||||
onSubmit={handleSubmit}
|
||||
onTab={handleTab}
|
||||
onShiftTab={handleShiftTab}
|
||||
onClickOutside={handleClickOutside}
|
||||
isReadOnly={readonly}
|
||||
/>
|
||||
}
|
||||
displayModeContent={<FieldDisplay />}
|
||||
isDisplayModeContentEmpty={isFieldEmpty}
|
||||
isDisplayModeFixHeight
|
||||
editModeContentOnly={isFieldInputOnly}
|
||||
loading={loading}
|
||||
/>
|
||||
</FieldFocusContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,20 +1,15 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import React, { ReactElement, useContext } from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { RecordInlineCellValue } from '@/object-record/record-inline-cell/components/RecordInlineCellValue';
|
||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useInlineCell } from '../hooks/useInlineCell';
|
||||
|
||||
import { RecordInlineCellDisplayMode } from './RecordInlineCellDisplayMode';
|
||||
import { RecordInlineCellButton } from './RecordInlineCellEditButton';
|
||||
import { RecordInlineCellEditMode } from './RecordInlineCellEditMode';
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
@ -48,18 +43,6 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
|
||||
width: ${({ width }) => width}px;
|
||||
`;
|
||||
|
||||
const StyledClickableContainer = styled.div<{ readonly?: boolean }>`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
|
||||
${({ readonly }) =>
|
||||
!readonly &&
|
||||
css`
|
||||
cursor: pointer;
|
||||
`};
|
||||
`;
|
||||
|
||||
const StyledInlineCellBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
@ -82,41 +65,20 @@ const StyledTooltip = styled(Tooltip)`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSkeletonDiv = styled.div`
|
||||
export const StyledSkeletonDiv = styled.div`
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const StyledInlineCellSkeletonLoader = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={4}
|
||||
>
|
||||
<StyledSkeletonDiv>
|
||||
<Skeleton width={154} height={16} />
|
||||
</StyledSkeletonDiv>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
|
||||
type RecordInlineCellContainerProps = {
|
||||
export type RecordInlineCellContainerProps = {
|
||||
readonly?: boolean;
|
||||
IconLabel?: IconComponent;
|
||||
label?: string;
|
||||
labelWidth?: number;
|
||||
showLabel?: boolean;
|
||||
buttonIcon?: IconComponent;
|
||||
editModeContent?: React.ReactNode;
|
||||
editModeContent?: ReactElement;
|
||||
editModeContentOnly?: boolean;
|
||||
displayModeContent: ({
|
||||
isCellSoftFocused,
|
||||
cellElement,
|
||||
}: {
|
||||
isCellSoftFocused: boolean;
|
||||
cellElement?: HTMLDivElement;
|
||||
}) => React.ReactNode;
|
||||
displayModeContent: ReactElement;
|
||||
customEditHotkeyScope?: HotkeyScope;
|
||||
isDisplayModeContentEmpty?: boolean;
|
||||
isDisplayModeFixHeight?: boolean;
|
||||
@ -141,85 +103,24 @@ export const RecordInlineCellContainer = ({
|
||||
loading = false,
|
||||
}: RecordInlineCellContainerProps) => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
// Used by some fields in ExpandableList as an anchor for the floating element.
|
||||
// floating-ui mentions that `useState` must be used instead of `useRef`,
|
||||
// see https://floating-ui.com/docs/useFloating#elements
|
||||
const [cellElement, setCellElement] = useState<HTMLDivElement | null>(null);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isCellSoftFocused, setIsCellSoftFocused] = useState(false);
|
||||
|
||||
const { setIsFocused } = useFieldFocus();
|
||||
|
||||
const handleContainerMouseEnter = () => {
|
||||
if (!readonly) {
|
||||
setIsHovered(true);
|
||||
setIsFocused(true);
|
||||
}
|
||||
setIsCellSoftFocused(true);
|
||||
};
|
||||
|
||||
const handleContainerMouseLeave = () => {
|
||||
if (!readonly) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
setIsCellSoftFocused(false);
|
||||
};
|
||||
|
||||
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
|
||||
|
||||
const handleDisplayModeClick = () => {
|
||||
if (!readonly && !editModeContentOnly) {
|
||||
openInlineCell(customEditHotkeyScope);
|
||||
setIsFocused(false);
|
||||
}
|
||||
};
|
||||
|
||||
const showEditButton =
|
||||
buttonIcon &&
|
||||
!isInlineCellInEditMode &&
|
||||
isHovered &&
|
||||
!editModeContentOnly &&
|
||||
!isDisplayModeContentEmpty;
|
||||
|
||||
const theme = useTheme();
|
||||
const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`;
|
||||
|
||||
const showContent = () => {
|
||||
if (loading) {
|
||||
return <StyledInlineCellSkeletonLoader />;
|
||||
}
|
||||
return !readonly && isInlineCellInEditMode ? (
|
||||
<RecordInlineCellEditMode>{editModeContent}</RecordInlineCellEditMode>
|
||||
) : editModeContentOnly ? (
|
||||
<StyledClickableContainer readonly={readonly}>
|
||||
<RecordInlineCellDisplayMode
|
||||
disableHoverEffect={disableHoverEffect}
|
||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||
isHovered={isHovered}
|
||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||
>
|
||||
{editModeContent}
|
||||
</RecordInlineCellDisplayMode>
|
||||
</StyledClickableContainer>
|
||||
) : (
|
||||
<StyledClickableContainer
|
||||
readonly={readonly}
|
||||
onClick={handleDisplayModeClick}
|
||||
>
|
||||
<RecordInlineCellDisplayMode
|
||||
disableHoverEffect={disableHoverEffect}
|
||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||
isHovered={isHovered}
|
||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||
>
|
||||
{displayModeContent({
|
||||
isCellSoftFocused,
|
||||
cellElement: cellElement ?? undefined,
|
||||
})}
|
||||
</RecordInlineCellDisplayMode>
|
||||
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
|
||||
</StyledClickableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledInlineCellBaseContainer
|
||||
onMouseEnter={handleContainerMouseEnter}
|
||||
@ -250,8 +151,23 @@ export const RecordInlineCellContainer = ({
|
||||
)}
|
||||
</StyledLabelAndIconContainer>
|
||||
)}
|
||||
<StyledValueContainer ref={setCellElement}>
|
||||
{showContent()}
|
||||
<StyledValueContainer>
|
||||
<RecordInlineCellValue
|
||||
{...{
|
||||
displayModeContent,
|
||||
customEditHotkeyScope,
|
||||
disableHoverEffect,
|
||||
editModeContent,
|
||||
editModeContentOnly,
|
||||
isDisplayModeContentEmpty,
|
||||
isDisplayModeFixHeight,
|
||||
buttonIcon,
|
||||
label,
|
||||
loading,
|
||||
readonly,
|
||||
showLabel,
|
||||
}}
|
||||
/>
|
||||
</StyledValueContainer>
|
||||
</StyledInlineCellBaseContainer>
|
||||
);
|
||||
|
@ -0,0 +1,20 @@
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { StyledSkeletonDiv } from './RecordInlineCellContainer';
|
||||
|
||||
export const RecordInlineCellSkeletonLoader = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={4}
|
||||
>
|
||||
<StyledSkeletonDiv>
|
||||
<Skeleton width={154} height={16} />
|
||||
</StyledSkeletonDiv>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
@ -0,0 +1,106 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { RecordInlineCellContainerProps } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
|
||||
import { RecordInlineCellDisplayMode } from '@/object-record/record-inline-cell/components/RecordInlineCellDisplayMode';
|
||||
import { RecordInlineCellButton } from '@/object-record/record-inline-cell/components/RecordInlineCellEditButton';
|
||||
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
||||
import { RecordInlineCellSkeletonLoader } from '@/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader';
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
|
||||
const StyledClickableContainer = styled.div<{ readonly?: boolean }>`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
|
||||
${({ readonly }) =>
|
||||
!readonly &&
|
||||
css`
|
||||
cursor: pointer;
|
||||
`};
|
||||
`;
|
||||
|
||||
type RecordInlineCellValueProps = Pick<
|
||||
RecordInlineCellContainerProps,
|
||||
| 'editModeContent'
|
||||
| 'displayModeContent'
|
||||
| 'customEditHotkeyScope'
|
||||
| 'isDisplayModeContentEmpty'
|
||||
| 'editModeContentOnly'
|
||||
| 'isDisplayModeFixHeight'
|
||||
| 'disableHoverEffect'
|
||||
| 'readonly'
|
||||
| 'buttonIcon'
|
||||
| 'loading'
|
||||
| 'showLabel'
|
||||
| 'label'
|
||||
>;
|
||||
|
||||
export const RecordInlineCellValue = ({
|
||||
displayModeContent,
|
||||
customEditHotkeyScope,
|
||||
disableHoverEffect,
|
||||
editModeContent,
|
||||
editModeContentOnly,
|
||||
isDisplayModeContentEmpty,
|
||||
isDisplayModeFixHeight,
|
||||
readonly,
|
||||
buttonIcon,
|
||||
loading,
|
||||
showLabel,
|
||||
label,
|
||||
}: RecordInlineCellValueProps) => {
|
||||
const { isFocused } = useFieldFocus();
|
||||
|
||||
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
|
||||
|
||||
const handleDisplayModeClick = () => {
|
||||
if (!readonly && !editModeContentOnly) {
|
||||
openInlineCell(customEditHotkeyScope);
|
||||
}
|
||||
};
|
||||
|
||||
const showEditButton =
|
||||
buttonIcon &&
|
||||
!isInlineCellInEditMode &&
|
||||
isFocused &&
|
||||
!editModeContentOnly &&
|
||||
!isDisplayModeContentEmpty;
|
||||
|
||||
if (loading === true) {
|
||||
return <RecordInlineCellSkeletonLoader />;
|
||||
}
|
||||
|
||||
return !readonly && isInlineCellInEditMode ? (
|
||||
<RecordInlineCellEditMode>{editModeContent}</RecordInlineCellEditMode>
|
||||
) : editModeContentOnly ? (
|
||||
<StyledClickableContainer readonly={readonly}>
|
||||
<RecordInlineCellDisplayMode
|
||||
disableHoverEffect={disableHoverEffect}
|
||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||
isHovered={isFocused}
|
||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||
>
|
||||
{editModeContent}
|
||||
</RecordInlineCellDisplayMode>
|
||||
</StyledClickableContainer>
|
||||
) : (
|
||||
<StyledClickableContainer
|
||||
readonly={readonly}
|
||||
onClick={handleDisplayModeClick}
|
||||
>
|
||||
<RecordInlineCellDisplayMode
|
||||
disableHoverEffect={disableHoverEffect}
|
||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||
isHovered={isFocused}
|
||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||
>
|
||||
{displayModeContent}
|
||||
</RecordInlineCellDisplayMode>
|
||||
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
|
||||
</StyledClickableContainer>
|
||||
);
|
||||
};
|
@ -28,6 +28,7 @@ import { RecordInlineCell } from '@/object-record/record-inline-cell/components/
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
|
||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
|
||||
@ -187,6 +188,7 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecordValueSetterEffect recordId={relationRecord.id} />
|
||||
<StyledListItem isDropdownOpen={isDropdownOpen}>
|
||||
<RecordChip
|
||||
record={relationRecord}
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useSetRecordValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
|
||||
export const RecordValueSetterEffect = ({ recordId }: { recordId: string }) => {
|
||||
const setRecordValue = useSetRecordValue();
|
||||
|
||||
const recordValue = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
useEffect(() => {
|
||||
setRecordValue(recordId, recordValue);
|
||||
}, [setRecordValue, recordValue, recordId]);
|
||||
|
||||
return null;
|
||||
};
|
@ -0,0 +1,73 @@
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
|
||||
export type RecordFieldValue = {
|
||||
[recordId: string]: {
|
||||
[fieldName: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
export const RecordFieldValueSelectorContext = createContext<
|
||||
[RecordFieldValue, Dispatch<SetStateAction<RecordFieldValue>>]
|
||||
>([{}, () => {}]);
|
||||
|
||||
export const useSetRecordValue = () => {
|
||||
const setTableValue = useContextSelector(
|
||||
RecordFieldValueSelectorContext,
|
||||
(value) => value[1],
|
||||
);
|
||||
|
||||
return (recordId: string, newRecord: any) => {
|
||||
setTableValue((currentTable) => ({
|
||||
...currentTable,
|
||||
[recordId]: newRecord,
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
export const useRecordValue = (recordId: string) => {
|
||||
const tableValue = useContextSelector(
|
||||
RecordFieldValueSelectorContext,
|
||||
(value) => value[0],
|
||||
);
|
||||
|
||||
return tableValue?.[recordId];
|
||||
};
|
||||
|
||||
export const useRecordFieldValue = (recordId: string, fieldName: string) => {
|
||||
const tableValue = useContextSelector(
|
||||
RecordFieldValueSelectorContext,
|
||||
(value) => value[0],
|
||||
);
|
||||
|
||||
return tableValue?.[recordId]?.[fieldName];
|
||||
};
|
||||
|
||||
export const useSetRecordFieldValue = () => {
|
||||
const setTableValue = useContextSelector(
|
||||
RecordFieldValueSelectorContext,
|
||||
(value) => value[1],
|
||||
);
|
||||
|
||||
return (recordId: string, fieldName: string, newValue: any) => {
|
||||
setTableValue((currentTable) => ({
|
||||
...currentTable,
|
||||
[recordId]: {
|
||||
...currentTable[recordId],
|
||||
[fieldName]: newValue,
|
||||
},
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
export const RecordFieldValueSelectorContextProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: any;
|
||||
}) => (
|
||||
<RecordFieldValueSelectorContext.Provider
|
||||
value={useState<RecordFieldValue>({})}
|
||||
>
|
||||
{children}
|
||||
</RecordFieldValueSelectorContext.Provider>
|
||||
);
|
@ -4,6 +4,7 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/components/RecordTableCellFieldContextWrapper';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
@ -52,6 +53,7 @@ export const RecordTableRow = ({ recordId, rowIndex }: RecordTableRowProps) => {
|
||||
isReadOnly: objectMetadataItem.isRemote ?? false,
|
||||
}}
|
||||
>
|
||||
<RecordValueSetterEffect recordId={recordId} />
|
||||
<tr
|
||||
ref={elementRef}
|
||||
data-testid={`row-id-${recordId}`}
|
||||
|
@ -0,0 +1,134 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import {
|
||||
RecordFieldValueSelectorContextProvider,
|
||||
useSetRecordValue,
|
||||
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/components/RecordTableCellFieldContextWrapper';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
import { recordTableCellMock } from './mock';
|
||||
|
||||
const RelationFieldValueSetterEffect = () => {
|
||||
const setEntity = useSetRecoilState(
|
||||
recordStoreFamilyState(recordTableCellMock.entityId),
|
||||
);
|
||||
|
||||
const setRelationEntity = useSetRecoilState(
|
||||
recordStoreFamilyState(recordTableCellMock.relationEntityId),
|
||||
);
|
||||
|
||||
const setRecordValue = useSetRecordValue();
|
||||
|
||||
useEffect(() => {
|
||||
setEntity(recordTableCellMock.entityValue);
|
||||
setRelationEntity(recordTableCellMock.relationFieldValue);
|
||||
|
||||
setRecordValue(
|
||||
recordTableCellMock.entityValue.id,
|
||||
recordTableCellMock.entityValue,
|
||||
);
|
||||
setRecordValue(
|
||||
recordTableCellMock.relationFieldValue.id,
|
||||
recordTableCellMock.relationFieldValue,
|
||||
);
|
||||
}, [setEntity, setRelationEntity, setRecordValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'RecordIndex/Table/RecordTableCell',
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
(Story) => (
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<RecordTableContext.Provider
|
||||
value={{
|
||||
objectMetadataItem: recordTableCellMock.objectMetadataItem as any,
|
||||
onUpsertRecord: () => {},
|
||||
onOpenTableCell: () => {},
|
||||
onMoveFocus: () => {},
|
||||
onCloseTableCell: () => {},
|
||||
onMoveSoftFocusToCell: () => {},
|
||||
onContextMenu: () => {},
|
||||
onCellMouseEnter: () => {},
|
||||
}}
|
||||
>
|
||||
<RecordTableScope recordTableScopeId="asd" onColumnsChange={() => {}}>
|
||||
<RecordTableRowContext.Provider
|
||||
value={{
|
||||
recordId: recordTableCellMock.entityId,
|
||||
rowIndex: 0,
|
||||
pathToShowPage:
|
||||
getBasePathToShowPage({
|
||||
objectNameSingular:
|
||||
recordTableCellMock.entityValue.__typename.toLocaleLowerCase(),
|
||||
}) + recordTableCellMock.entityId,
|
||||
isSelected: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordTableCellContext.Provider
|
||||
value={{
|
||||
columnDefinition: recordTableCellMock.fieldDefinition,
|
||||
columnIndex: 0,
|
||||
}}
|
||||
>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: recordTableCellMock.entityId,
|
||||
basePathToShowPage: '/object-record/',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
...recordTableCellMock.fieldDefinition,
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
}}
|
||||
>
|
||||
<RelationFieldValueSetterEffect />
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<Story />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</FieldContext.Provider>
|
||||
</RecordTableCellContext.Provider>
|
||||
</RecordTableRowContext.Provider>
|
||||
</RecordTableScope>
|
||||
</RecordTableContext.Provider>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: RecordTableCellFieldContextWrapper,
|
||||
argTypes: { value: { control: 'date' } },
|
||||
args: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RecordTableCellFieldContextWrapper>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'RecordTableCell',
|
||||
averageThresholdInMs: 0.6,
|
||||
numberOfRuns: 50,
|
||||
numberOfTestsPerRun: 200,
|
||||
warmUpRounds: 20,
|
||||
});
|
@ -0,0 +1,883 @@
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const recordTableCellMock = {
|
||||
objectMetadataItem: {
|
||||
__typename: 'object',
|
||||
id: '4916628e-8570-4242-8970-f58c509e5a93',
|
||||
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
labelSingular: 'Person',
|
||||
labelPlural: 'People',
|
||||
description: 'A person',
|
||||
icon: 'IconUser',
|
||||
isCustom: false,
|
||||
isRemote: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
labelIdentifierFieldMetadataId: null,
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
fields: [
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '9058056e-36b3-4a3f-9037-f0bca9744296',
|
||||
type: 'RELATION',
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
description: 'Contact’s company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: {
|
||||
__typename: 'relation',
|
||||
id: '0cf72416-3d94-4d94-abf3-7dc9d734435b',
|
||||
relationType: 'ONE_TO_MANY',
|
||||
fromObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '79c2d29c-76f6-432f-91c9-df1259b73d95',
|
||||
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
isSystem: false,
|
||||
isRemote: false,
|
||||
},
|
||||
fromFieldMetadataId: '7b281010-5f47-4771-b3f5-f4bcd24ed1b5',
|
||||
},
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: {
|
||||
__typename: 'RelationDefinition',
|
||||
relationId: '0cf72416-3d94-4d94-abf3-7dc9d734435b',
|
||||
direction: 'MANY_TO_ONE',
|
||||
sourceObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '4916628e-8570-4242-8970-f58c509e5a93',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: '9058056e-36b3-4a3f-9037-f0bca9744296',
|
||||
name: 'company',
|
||||
},
|
||||
targetObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '79c2d29c-76f6-432f-91c9-df1259b73d95',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: '7b281010-5f47-4771-b3f5-f4bcd24ed1b5',
|
||||
name: 'people',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'bd504d22-ecae-4228-8729-5c770a174336',
|
||||
type: 'TEXT',
|
||||
name: 'avatarUrl',
|
||||
label: 'Avatar',
|
||||
description: 'Contact’s avatar',
|
||||
icon: 'IconFileUpload',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: "''",
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '21238919-5d92-402e-8124-367948ef86e6',
|
||||
type: 'TEXT',
|
||||
name: 'city',
|
||||
label: 'City',
|
||||
description: 'Contact’s city',
|
||||
icon: 'IconMap',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: "''",
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '78edf4bb-c6a6-449e-b9db-20a575b97d5e',
|
||||
type: 'RELATION',
|
||||
name: 'activityTargets',
|
||||
label: 'Activities',
|
||||
description: 'Activities tied to the contact',
|
||||
icon: 'IconCheckbox',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: {
|
||||
__typename: 'relation',
|
||||
id: 'd76f949d-023d-4b45-a71e-f39e3b1562ba',
|
||||
relationType: 'ONE_TO_MANY',
|
||||
toObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '82222ca2-dd40-44ec-b8c5-eb0eca9ec625',
|
||||
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
|
||||
nameSingular: 'activityTarget',
|
||||
namePlural: 'activityTargets',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: 'f5f515cc-6d8a-44c3-b2d4-f04b9868a9c5',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: {
|
||||
__typename: 'RelationDefinition',
|
||||
relationId: 'd76f949d-023d-4b45-a71e-f39e3b1562ba',
|
||||
direction: 'ONE_TO_MANY',
|
||||
sourceObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '4916628e-8570-4242-8970-f58c509e5a93',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: '78edf4bb-c6a6-449e-b9db-20a575b97d5e',
|
||||
name: 'activityTargets',
|
||||
},
|
||||
targetObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '82222ca2-dd40-44ec-b8c5-eb0eca9ec625',
|
||||
nameSingular: 'activityTarget',
|
||||
namePlural: 'activityTargets',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: 'f5f515cc-6d8a-44c3-b2d4-f04b9868a9c5',
|
||||
name: 'person',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '4128b168-1439-441e-bb6a-223fa1276642',
|
||||
type: 'RELATION',
|
||||
name: 'pointOfContactForOpportunities',
|
||||
label: 'POC for Opportunities',
|
||||
description: 'Point of Contact for Opportunities',
|
||||
icon: 'IconTargetArrow',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: {
|
||||
__typename: 'relation',
|
||||
id: 'a5a61d23-8ac9-4014-9441-ec3a1781a661',
|
||||
relationType: 'ONE_TO_MANY',
|
||||
toObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '494b9b7c-a44e-4d52-b274-cdfb0e322165',
|
||||
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
isSystem: false,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: '86559a6f-6afc-4d5c-9bed-fc74d063791b',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: {
|
||||
__typename: 'RelationDefinition',
|
||||
relationId: 'a5a61d23-8ac9-4014-9441-ec3a1781a661',
|
||||
direction: 'ONE_TO_MANY',
|
||||
sourceObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '4916628e-8570-4242-8970-f58c509e5a93',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: '4128b168-1439-441e-bb6a-223fa1276642',
|
||||
name: 'pointOfContactForOpportunities',
|
||||
},
|
||||
targetObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '494b9b7c-a44e-4d52-b274-cdfb0e322165',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: '86559a6f-6afc-4d5c-9bed-fc74d063791b',
|
||||
name: 'pointOfContact',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '3db3a6ac-a960-42bd-8375-59ab6c4837d6',
|
||||
type: 'RELATION',
|
||||
name: 'calendarEventParticipants',
|
||||
label: 'Calendar Event Participants',
|
||||
description: 'Calendar Event Participants',
|
||||
icon: 'IconCalendar',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: {
|
||||
__typename: 'relation',
|
||||
id: '456f7875-b48c-4795-a0c7-a69d7339afee',
|
||||
relationType: 'ONE_TO_MANY',
|
||||
toObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: 'eba13fca-57b7-470c-8c23-a0e640e04ffb',
|
||||
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
|
||||
nameSingular: 'calendarEventParticipant',
|
||||
namePlural: 'calendarEventParticipants',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: 'c1cdebda-b514-4487-9b9c-aa59d8fca8eb',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: {
|
||||
__typename: 'RelationDefinition',
|
||||
relationId: '456f7875-b48c-4795-a0c7-a69d7339afee',
|
||||
direction: 'ONE_TO_MANY',
|
||||
sourceObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '4916628e-8570-4242-8970-f58c509e5a93',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: '3db3a6ac-a960-42bd-8375-59ab6c4837d6',
|
||||
name: 'calendarEventParticipants',
|
||||
},
|
||||
targetObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: 'eba13fca-57b7-470c-8c23-a0e640e04ffb',
|
||||
nameSingular: 'calendarEventParticipant',
|
||||
namePlural: 'calendarEventParticipants',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: 'c1cdebda-b514-4487-9b9c-aa59d8fca8eb',
|
||||
name: 'person',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'f0a290ac-fa74-48da-a77f-db221cb0206a',
|
||||
type: 'DATE_TIME',
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: 'now',
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'b96e0e45-278c-44b6-a601-30ba24592dd6',
|
||||
type: 'RELATION',
|
||||
name: 'favorites',
|
||||
label: 'Favorites',
|
||||
description: 'Favorites linked to the contact',
|
||||
icon: 'IconHeart',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: {
|
||||
__typename: 'relation',
|
||||
id: '31542774-fb15-4d01-b00b-8fc94887f458',
|
||||
relationType: 'ONE_TO_MANY',
|
||||
toObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: 'f08422e2-14cd-4966-9cd3-bce0302cc56f',
|
||||
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
|
||||
nameSingular: 'favorite',
|
||||
namePlural: 'favorites',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: '67d28b17-ff3c-49b4-a6da-1354be9634b0',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: {
|
||||
__typename: 'RelationDefinition',
|
||||
relationId: '31542774-fb15-4d01-b00b-8fc94887f458',
|
||||
direction: 'ONE_TO_MANY',
|
||||
sourceObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '4916628e-8570-4242-8970-f58c509e5a93',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: 'b96e0e45-278c-44b6-a601-30ba24592dd6',
|
||||
name: 'favorites',
|
||||
},
|
||||
targetObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: 'f08422e2-14cd-4966-9cd3-bce0302cc56f',
|
||||
nameSingular: 'favorite',
|
||||
namePlural: 'favorites',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: '67d28b17-ff3c-49b4-a6da-1354be9634b0',
|
||||
name: 'person',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '430af81e-2a8c-4ce2-9969-c0f0e91818bb',
|
||||
type: 'LINK',
|
||||
name: 'linkedinLink',
|
||||
label: 'Linkedin',
|
||||
description: 'Contact’s Linkedin account',
|
||||
icon: 'IconBrandLinkedin',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: {
|
||||
url: "''",
|
||||
label: "''",
|
||||
},
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'c885c3d9-63e2-4c0d-b7d6-ee9e867eb1f6',
|
||||
type: 'RELATION',
|
||||
name: 'attachments',
|
||||
label: 'Attachments',
|
||||
description: 'Attachments linked to the contact.',
|
||||
icon: 'IconFileImport',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: {
|
||||
__typename: 'relation',
|
||||
id: 'c0cc3456-afa4-46e0-820d-2db0b63a8273',
|
||||
relationType: 'ONE_TO_MANY',
|
||||
toObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '0e3c9a9d-8a60-4671-a466-7b840a422da2',
|
||||
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
|
||||
nameSingular: 'attachment',
|
||||
namePlural: 'attachments',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: 'a920a0d6-8e71-4ab8-90b9-ab540e04732a',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: {
|
||||
__typename: 'RelationDefinition',
|
||||
relationId: 'c0cc3456-afa4-46e0-820d-2db0b63a8273',
|
||||
direction: 'ONE_TO_MANY',
|
||||
sourceObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '4916628e-8570-4242-8970-f58c509e5a93',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: 'c885c3d9-63e2-4c0d-b7d6-ee9e867eb1f6',
|
||||
name: 'attachments',
|
||||
},
|
||||
targetObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '0e3c9a9d-8a60-4671-a466-7b840a422da2',
|
||||
nameSingular: 'attachment',
|
||||
namePlural: 'attachments',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: 'a920a0d6-8e71-4ab8-90b9-ab540e04732a',
|
||||
name: 'person',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'cc63e38f-56d6-495e-a545-edf101e400cf',
|
||||
type: 'TEXT',
|
||||
name: 'phone',
|
||||
label: 'Phone',
|
||||
description: 'Contact’s phone number',
|
||||
icon: 'IconPhone',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: "''",
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '0084a5f7-cb57-4cd5-8b14-93ab51c21f45',
|
||||
type: 'POSITION',
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
description: 'Person record Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'ca54aa1d-1ecb-486c-99ea-b8240871a0da',
|
||||
type: 'EMAIL',
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
description: 'Contact’s Email',
|
||||
icon: 'IconMail',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: "''",
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '54561a8e-b918-471b-a363-5a77f49cd348',
|
||||
type: 'TEXT',
|
||||
name: 'jobTitle',
|
||||
label: 'Job Title',
|
||||
description: 'Contact’s job title',
|
||||
icon: 'IconBriefcase',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: "''",
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '4e844d31-f117-443c-8754-8cb63e963ecc',
|
||||
type: 'DATE_TIME',
|
||||
name: 'updatedAt',
|
||||
label: 'Update date',
|
||||
description: 'Update date',
|
||||
icon: 'IconCalendar',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: 'now',
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '4ddd38df-d9a3-4889-a39f-1e336cd8113c',
|
||||
type: 'UUID',
|
||||
name: 'companyId',
|
||||
label: 'Company id (foreign key)',
|
||||
description: 'Contact’s company id foreign key',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'e6922ecb-7a3a-4520-b001-bbf95fc33197',
|
||||
type: 'RELATION',
|
||||
name: 'timelineActivities',
|
||||
label: 'Events',
|
||||
description: 'Events linked to the company',
|
||||
icon: 'IconTimelineEvent',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: {
|
||||
__typename: 'relation',
|
||||
id: '25150feb-fcd7-407e-b5fa-ffe58a0450ac',
|
||||
relationType: 'ONE_TO_MANY',
|
||||
toObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '83b5ff3e-975e-4dc9-ba4d-c645a0d8afb2',
|
||||
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
|
||||
nameSingular: 'timelineActivity',
|
||||
namePlural: 'timelineActivities',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: '556a12d4-ef0a-4232-963f-0f317f4c5ef5',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: {
|
||||
__typename: 'RelationDefinition',
|
||||
relationId: '25150feb-fcd7-407e-b5fa-ffe58a0450ac',
|
||||
direction: 'ONE_TO_MANY',
|
||||
sourceObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '4916628e-8570-4242-8970-f58c509e5a93',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: 'e6922ecb-7a3a-4520-b001-bbf95fc33197',
|
||||
name: 'timelineActivities',
|
||||
},
|
||||
targetObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '83b5ff3e-975e-4dc9-ba4d-c645a0d8afb2',
|
||||
nameSingular: 'timelineActivity',
|
||||
namePlural: 'timelineActivities',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: '556a12d4-ef0a-4232-963f-0f317f4c5ef5',
|
||||
name: 'person',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '07a8a574-ed28-4015-b456-c01ff3050e2b',
|
||||
type: 'FULL_NAME',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Contact’s name',
|
||||
icon: 'IconUser',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: {
|
||||
lastName: "''",
|
||||
firstName: "''",
|
||||
},
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'c470144b-6692-47cb-a28f-04610d9d641c',
|
||||
type: 'LINK',
|
||||
name: 'xLink',
|
||||
label: 'X',
|
||||
description: 'Contact’s X/Twitter account',
|
||||
icon: 'IconBrandX',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: {
|
||||
url: "''",
|
||||
label: "''",
|
||||
},
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'c692aa2c-e88e-4aff-b77e-b9ebf26509e3',
|
||||
type: 'RELATION',
|
||||
name: 'messageParticipants',
|
||||
label: 'Message Participants',
|
||||
description: 'Message Participants',
|
||||
icon: 'IconUserCircle',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: {
|
||||
__typename: 'relation',
|
||||
id: 'e2eb7156-6e65-4bf8-922b-670179744f27',
|
||||
relationType: 'ONE_TO_MANY',
|
||||
toObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: 'ffd8e640-84b7-4ed6-99e9-14def0f9d82b',
|
||||
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
|
||||
nameSingular: 'messageParticipant',
|
||||
namePlural: 'messageParticipants',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: '8c4593a1-ad40-4681-92fe-43ad4fe60205',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
relationDefinition: {
|
||||
__typename: 'RelationDefinition',
|
||||
relationId: 'e2eb7156-6e65-4bf8-922b-670179744f27',
|
||||
direction: 'ONE_TO_MANY',
|
||||
sourceObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: '4916628e-8570-4242-8970-f58c509e5a93',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: 'c692aa2c-e88e-4aff-b77e-b9ebf26509e3',
|
||||
name: 'messageParticipants',
|
||||
},
|
||||
targetObjectMetadata: {
|
||||
__typename: 'object',
|
||||
id: 'ffd8e640-84b7-4ed6-99e9-14def0f9d82b',
|
||||
nameSingular: 'messageParticipant',
|
||||
namePlural: 'messageParticipants',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
__typename: 'field',
|
||||
id: '8c4593a1-ad40-4681-92fe-43ad4fe60205',
|
||||
name: 'person',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '66d33eae-71be-49fa-ad7a-3e10ac53dfba',
|
||||
type: 'UUID',
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
createdAt: '2024-05-16T10:54:27.788Z',
|
||||
updatedAt: '2024-05-16T10:54:27.788Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: 'uuid',
|
||||
options: null,
|
||||
relationDefinition: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
entityId: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||
relationEntityId: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
entityValue: {
|
||||
__typename: 'Person',
|
||||
asd: '',
|
||||
city: 'Seattle',
|
||||
jobTitle: '',
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'Lorie',
|
||||
lastName: 'Vladim',
|
||||
},
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
company: {
|
||||
__typename: 'Company',
|
||||
domainName: 'google.com',
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
name: 'Google',
|
||||
annualRecurringRevenue: {
|
||||
__typename: 'Currency',
|
||||
amountMicros: null,
|
||||
currencyCode: '',
|
||||
},
|
||||
employees: null,
|
||||
accountOwnerId: null,
|
||||
address: '',
|
||||
idealCustomerProfile: false,
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
id: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
position: 6,
|
||||
updatedAt: '2024-05-01T13:16:29.046Z',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
id: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||
email: 'lorie.vladim@google.com',
|
||||
phone: '+33788901235',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
tEst: '',
|
||||
position: 15,
|
||||
},
|
||||
relationFieldValue: {
|
||||
__typename: 'Company',
|
||||
domainName: 'microsoft.com',
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
name: 'Microsoft',
|
||||
annualRecurringRevenue: {
|
||||
__typename: 'Currency',
|
||||
amountMicros: null,
|
||||
currencyCode: '',
|
||||
},
|
||||
employees: null,
|
||||
accountOwnerId: null,
|
||||
address: '',
|
||||
idealCustomerProfile: false,
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
id: '20202020-ed89-413a-b31a-962986e67bb4',
|
||||
position: 4,
|
||||
updatedAt: '2024-05-01T13:16:29.046Z',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: '4e79f0b7-d100-4e89-a07b-315a710b8059',
|
||||
label: 'Company',
|
||||
metadata: {
|
||||
fieldName: 'company',
|
||||
placeHolder: 'Company',
|
||||
relationType: 'TO_ONE_OBJECT',
|
||||
relationFieldMetadataId: '01fa2247-7937-4493-b7e2-3d72f05d6d25',
|
||||
relationObjectMetadataNameSingular: 'company',
|
||||
relationObjectMetadataNamePlural: 'companies',
|
||||
objectMetadataNameSingular: 'person',
|
||||
options: null,
|
||||
},
|
||||
iconName: 'IconBuildingSkyscraper',
|
||||
type: FieldMetadataType.Relation,
|
||||
position: 2,
|
||||
size: 150,
|
||||
isLabelIdentifier: false,
|
||||
isVisible: true,
|
||||
viewFieldId: '924f4c94-cbcd-4de5-b7a2-ebae2f0b2c3b',
|
||||
isSortable: false,
|
||||
isFilterable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
};
|
@ -21,6 +21,13 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => {
|
||||
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
|
||||
false,
|
||||
);
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(
|
||||
`edit-mode-change-${currentTableCellInEditModePosition.row}:${currentTableCellInEditModePosition.column}`,
|
||||
{ detail: false },
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
[currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState],
|
||||
|
@ -24,9 +24,23 @@ export const useMoveEditModeToTableCellPosition = (recordTableId?: string) => {
|
||||
false,
|
||||
);
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(
|
||||
`edit-mode-change-${currentTableCellInEditModePosition.row}:${currentTableCellInEditModePosition.column}`,
|
||||
{ detail: false },
|
||||
),
|
||||
);
|
||||
|
||||
set(currentTableCellInEditModePositionState, newPosition);
|
||||
|
||||
set(isTableCellInEditModeFamilyState(newPosition), true);
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(
|
||||
`edit-mode-change-${newPosition.row}:${newPosition.column}`,
|
||||
{ detail: true },
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
[currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState],
|
||||
|
@ -24,9 +24,23 @@ export const useSetSoftFocusPosition = (recordTableId?: string) => {
|
||||
|
||||
set(isSoftFocusOnTableCellFamilyState(currentPosition), false);
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(
|
||||
`soft-focus-move-${currentPosition.row}:${currentPosition.column}`,
|
||||
{ detail: false },
|
||||
),
|
||||
);
|
||||
|
||||
set(softFocusPositionState, newPosition);
|
||||
|
||||
set(isSoftFocusOnTableCellFamilyState(newPosition), true);
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(
|
||||
`soft-focus-move-${newPosition.row}:${newPosition.column}`,
|
||||
{ detail: true },
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
[
|
||||
|
@ -3,6 +3,7 @@ import { useContext } from 'react';
|
||||
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
||||
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
@ -87,28 +88,24 @@ export const RecordTableCell = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<RecordTableCellContainer
|
||||
editHotkeyScope={customHotkeyScope}
|
||||
editModeContent={
|
||||
<FieldInput
|
||||
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
|
||||
onCancel={handleCancel}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onSubmit={handleSubmit}
|
||||
onTab={handleTab}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={({ isCellSoftFocused, cellElement }) => (
|
||||
<FieldDisplay
|
||||
isCellSoftFocused={isCellSoftFocused}
|
||||
cellElement={cellElement}
|
||||
fromTableCell
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FieldFocusContextProvider>
|
||||
<RecordTableCellContainer
|
||||
editHotkeyScope={customHotkeyScope}
|
||||
editModeContent={
|
||||
<FieldInput
|
||||
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
|
||||
onCancel={handleCancel}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onSubmit={handleSubmit}
|
||||
onTab={handleTab}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<FieldDisplay />}
|
||||
/>
|
||||
</FieldFocusContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,28 +1,15 @@
|
||||
import React, { ReactElement, useContext, useState } from 'react';
|
||||
import React, { ReactElement, useContext, useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconArrowUpRight } from 'twenty-ui';
|
||||
|
||||
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
||||
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
|
||||
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
|
||||
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
|
||||
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
|
||||
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
||||
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
|
||||
|
||||
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
import { RecordTableCellButton } from './RecordTableCellButton';
|
||||
import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode';
|
||||
import { RecordTableCellEditMode } from './RecordTableCellEditMode';
|
||||
import { RecordTableCellSoftFocusMode } from './RecordTableCellSoftFocusMode';
|
||||
@ -51,13 +38,7 @@ const StyledCellBaseContainer = styled.div<{ softFocus: boolean }>`
|
||||
|
||||
export type RecordTableCellContainerProps = {
|
||||
editModeContent: ReactElement;
|
||||
nonEditModeContent?: ({
|
||||
isCellSoftFocused,
|
||||
cellElement,
|
||||
}: {
|
||||
isCellSoftFocused: boolean;
|
||||
cellElement?: HTMLTableCellElement;
|
||||
}) => ReactElement;
|
||||
nonEditModeContent: ReactElement;
|
||||
editHotkeyScope?: HotkeyScope;
|
||||
transparent?: boolean;
|
||||
maxContentWidth?: number;
|
||||
@ -74,91 +55,85 @@ export const RecordTableCellContainer = ({
|
||||
nonEditModeContent,
|
||||
editHotkeyScope,
|
||||
}: RecordTableCellContainerProps) => {
|
||||
const { columnIndex } = useContext(RecordTableCellContext);
|
||||
// Used by some fields in ExpandableList as an anchor for the floating element.
|
||||
// floating-ui mentions that `useState` must be used instead of `useRef`,
|
||||
// see https://floating-ui.com/docs/useFloating#elements
|
||||
const [cellElement, setCellElement] = useState<HTMLTableCellElement | null>(
|
||||
null,
|
||||
);
|
||||
const [isCellBaseContainerHovered, setIsCellBaseContainerHovered] =
|
||||
useState(false);
|
||||
const { isReadOnly, isSelected, recordId } = useContext(
|
||||
RecordTableRowContext,
|
||||
);
|
||||
const { onMoveSoftFocusToCell, onContextMenu, onCellMouseEnter } =
|
||||
useContext(RecordTableContext);
|
||||
const { setIsFocused } = useFieldFocus();
|
||||
|
||||
const { isSelected, recordId } = useContext(RecordTableRowContext);
|
||||
const { onContextMenu, onCellMouseEnter } = useContext(RecordTableContext);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [hasSoftFocus, setHasSoftFocus] = useState(false);
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
|
||||
const cellPosition = useCurrentTableCellPosition();
|
||||
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
|
||||
const tableScopeId = useAvailableScopeIdOrThrow(
|
||||
RecordTableScopeInternalContext,
|
||||
getScopeIdOrUndefinedFromComponentId(),
|
||||
);
|
||||
|
||||
const isTableCellInEditModeFamilyState = extractComponentFamilyState(
|
||||
isTableCellInEditModeComponentFamilyState,
|
||||
tableScopeId,
|
||||
);
|
||||
|
||||
const isSoftFocusOnTableCellFamilyState = extractComponentFamilyState(
|
||||
isSoftFocusOnTableCellComponentFamilyState,
|
||||
tableScopeId,
|
||||
);
|
||||
|
||||
const isCurrentTableCellInEditMode = useRecoilValue(
|
||||
isTableCellInEditModeFamilyState(cellPosition),
|
||||
);
|
||||
|
||||
const hasSoftFocus = useRecoilValue(
|
||||
isSoftFocusOnTableCellFamilyState(cellPosition),
|
||||
);
|
||||
|
||||
const isEmpty = useIsFieldEmpty();
|
||||
|
||||
const handleButtonClick = () => {
|
||||
onMoveSoftFocusToCell(cellPosition);
|
||||
openTableCell();
|
||||
};
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
onContextMenu(event, recordId);
|
||||
};
|
||||
|
||||
const handleContainerMouseEnter = () => {
|
||||
onCellMouseEnter({
|
||||
cellPosition,
|
||||
isHovered: isCellBaseContainerHovered,
|
||||
setIsHovered: setIsCellBaseContainerHovered,
|
||||
});
|
||||
if (!hasSoftFocus) {
|
||||
onCellMouseEnter({
|
||||
cellPosition,
|
||||
isHovered,
|
||||
setIsHovered,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleContainerMouseLeave = () => {
|
||||
setIsCellBaseContainerHovered(false);
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const editModeContentOnly = useIsFieldInputOnly();
|
||||
const handleContainerMouseMove = () => {
|
||||
handleContainerMouseEnter();
|
||||
};
|
||||
|
||||
const isFirstColumn = columnIndex === 0;
|
||||
const customButtonIcon = useGetButtonIcon();
|
||||
const buttonIcon = isFirstColumn ? IconArrowUpRight : customButtonIcon;
|
||||
useEffect(() => {
|
||||
const customEventListener = (event: any) => {
|
||||
const newHasSoftFocus = event.detail;
|
||||
|
||||
const showButton =
|
||||
!!buttonIcon &&
|
||||
hasSoftFocus &&
|
||||
!isCurrentTableCellInEditMode &&
|
||||
!editModeContentOnly &&
|
||||
(!isFirstColumn || !isEmpty) &&
|
||||
!isReadOnly;
|
||||
setHasSoftFocus(newHasSoftFocus);
|
||||
setIsFocused(newHasSoftFocus);
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
`soft-focus-move-${cellPosition.row}:${cellPosition.column}`,
|
||||
customEventListener,
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
`soft-focus-move-${cellPosition.row}:${cellPosition.column}`,
|
||||
customEventListener,
|
||||
);
|
||||
};
|
||||
}, [cellPosition, setIsFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
const customEventListener = (event: any) => {
|
||||
const newIsInEditMode = event.detail;
|
||||
|
||||
setIsInEditMode(newIsInEditMode);
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
`edit-mode-change-${cellPosition.row}:${cellPosition.column}`,
|
||||
customEventListener,
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
`edit-mode-change-${cellPosition.row}:${cellPosition.column}`,
|
||||
customEventListener,
|
||||
);
|
||||
};
|
||||
}, [cellPosition]);
|
||||
|
||||
return (
|
||||
<StyledTd
|
||||
ref={setCellElement}
|
||||
isSelected={isSelected}
|
||||
onContextMenu={handleContextMenu}
|
||||
isInEditMode={isCurrentTableCellInEditMode}
|
||||
isInEditMode={isInEditMode}
|
||||
>
|
||||
<CellHotkeyScopeContext.Provider
|
||||
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
|
||||
@ -166,46 +141,22 @@ export const RecordTableCellContainer = ({
|
||||
<StyledCellBaseContainer
|
||||
onMouseEnter={handleContainerMouseEnter}
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
onMouseMove={handleContainerMouseMove}
|
||||
softFocus={hasSoftFocus}
|
||||
>
|
||||
{isCurrentTableCellInEditMode ? (
|
||||
{isInEditMode ? (
|
||||
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
|
||||
) : hasSoftFocus ? (
|
||||
<>
|
||||
<RecordTableCellSoftFocusMode>
|
||||
{editModeContentOnly
|
||||
? editModeContent
|
||||
: nonEditModeContent?.({
|
||||
isCellSoftFocused: true,
|
||||
cellElement: cellElement ?? undefined,
|
||||
})}
|
||||
</RecordTableCellSoftFocusMode>
|
||||
{showButton && (
|
||||
<RecordTableCellButton
|
||||
onClick={handleButtonClick}
|
||||
Icon={buttonIcon}
|
||||
/>
|
||||
)}
|
||||
<RecordTableCellSoftFocusMode
|
||||
editModeContent={editModeContent}
|
||||
nonEditModeContent={nonEditModeContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!isEmpty && (
|
||||
<RecordTableCellDisplayMode>
|
||||
{editModeContentOnly
|
||||
? editModeContent
|
||||
: nonEditModeContent?.({
|
||||
isCellSoftFocused: false,
|
||||
cellElement: cellElement ?? undefined,
|
||||
})}
|
||||
</RecordTableCellDisplayMode>
|
||||
)}
|
||||
{showButton && (
|
||||
<RecordTableCellButton
|
||||
onClick={handleButtonClick}
|
||||
Icon={buttonIcon}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<RecordTableCellDisplayMode>
|
||||
{nonEditModeContent}
|
||||
</RecordTableCellDisplayMode>
|
||||
)}
|
||||
</StyledCellBaseContainer>
|
||||
</CellHotkeyScopeContext.Provider>
|
||||
|
@ -1,31 +1,18 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
|
||||
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
|
||||
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
|
||||
|
||||
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
|
||||
|
||||
export const RecordTableCellDisplayMode = ({
|
||||
children,
|
||||
}: React.PropsWithChildren<unknown>) => {
|
||||
const cellPosition = useCurrentTableCellPosition();
|
||||
const { onMoveSoftFocusToCell } = useContext(RecordTableContext);
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
const isEmpty = useIsFieldEmpty();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const handleClick = () => {
|
||||
onMoveSoftFocusToCell(cellPosition);
|
||||
|
||||
if (!isFieldInputOnly) {
|
||||
openTableCell();
|
||||
}
|
||||
};
|
||||
if (isEmpty) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableCellDisplayContainer onClick={handleClick}>
|
||||
<RecordTableCellDisplayContainer>
|
||||
{children}
|
||||
</RecordTableCellDisplayContainer>
|
||||
);
|
||||
|
@ -1,11 +1,17 @@
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import { ReactElement, useContext, useEffect, useRef } from 'react';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { IconArrowUpRight } from 'twenty-ui';
|
||||
|
||||
import { useClearField } from '@/object-record/record-field/hooks/useClearField';
|
||||
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
||||
import { useIsFieldClearable } from '@/object-record/record-field/hooks/useIsFieldClearable';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableCellButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButton';
|
||||
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
|
||||
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
@ -15,13 +21,23 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
|
||||
|
||||
type RecordTableCellSoftFocusModeProps = PropsWithChildren<unknown>;
|
||||
type RecordTableCellSoftFocusModeProps = {
|
||||
editModeContent: ReactElement;
|
||||
nonEditModeContent: ReactElement;
|
||||
};
|
||||
|
||||
export const RecordTableCellSoftFocusMode = ({
|
||||
children,
|
||||
editModeContent,
|
||||
nonEditModeContent,
|
||||
}: RecordTableCellSoftFocusModeProps) => {
|
||||
const { columnIndex } = useContext(RecordTableCellContext);
|
||||
|
||||
const { isReadOnly } = useContext(RecordTableRowContext);
|
||||
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
|
||||
const editModeContentOnly = useIsFieldInputOnly();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const isFieldClearable = useIsFieldClearable();
|
||||
@ -98,12 +114,27 @@ export const RecordTableCellSoftFocusMode = ({
|
||||
}
|
||||
};
|
||||
|
||||
const isFirstColumn = columnIndex === 0;
|
||||
const customButtonIcon = useGetButtonIcon();
|
||||
const buttonIcon = isFirstColumn ? IconArrowUpRight : customButtonIcon;
|
||||
|
||||
const showButton =
|
||||
!!buttonIcon &&
|
||||
!editModeContentOnly &&
|
||||
(!isFirstColumn || !isEmpty) &&
|
||||
!isReadOnly;
|
||||
|
||||
return (
|
||||
<RecordTableCellDisplayContainer
|
||||
onClick={handleClick}
|
||||
scrollRef={scrollRef}
|
||||
>
|
||||
{children}
|
||||
</RecordTableCellDisplayContainer>
|
||||
<>
|
||||
<RecordTableCellDisplayContainer
|
||||
onClick={handleClick}
|
||||
scrollRef={scrollRef}
|
||||
>
|
||||
{editModeContentOnly ? editModeContent : nonEditModeContent}
|
||||
</RecordTableCellDisplayContainer>
|
||||
{showButton && (
|
||||
<RecordTableCellButton onClick={handleClick} Icon={buttonIcon} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { MouseEventHandler, useMemo } from 'react';
|
||||
|
||||
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import {
|
||||
ExpandableList,
|
||||
ExpandableListProps,
|
||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
|
||||
import {
|
||||
LinkType,
|
||||
@ -15,18 +12,12 @@ import { isDefined } from '~/utils/isDefined';
|
||||
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
||||
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
||||
|
||||
type LinksDisplayProps = Pick<
|
||||
ExpandableListProps,
|
||||
'isChipCountDisplayed' | 'withExpandedListBorder'
|
||||
> & {
|
||||
type LinksDisplayProps = {
|
||||
value?: FieldLinksValue;
|
||||
isFocused?: boolean;
|
||||
};
|
||||
|
||||
export const LinksDisplay = ({
|
||||
isChipCountDisplayed,
|
||||
withExpandedListBorder,
|
||||
value,
|
||||
}: LinksDisplayProps) => {
|
||||
export const LinksDisplay = ({ value, isFocused }: LinksDisplayProps) => {
|
||||
const links = useMemo(
|
||||
() =>
|
||||
[
|
||||
@ -53,10 +44,7 @@ export const LinksDisplay = ({
|
||||
const handleClick: MouseEventHandler = (event) => event.stopPropagation();
|
||||
|
||||
return (
|
||||
<ExpandableList
|
||||
isChipCountDisplayed={isChipCountDisplayed}
|
||||
withExpandedListBorder={withExpandedListBorder}
|
||||
>
|
||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||
{links.map(({ url, label, type }, index) =>
|
||||
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
|
||||
<SocialLink key={index} href={url} onClick={handleClick} type={type}>
|
||||
|
@ -0,0 +1 @@
|
||||
export const FIELD_EDIT_BUTTON_WIDTH = 28;
|
@ -9,6 +9,8 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
||||
import { findOneRecordForShowPageOperationSignatureFactory } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory';
|
||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { PageBody } from '@/ui/layout/page/PageBody';
|
||||
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||
@ -64,7 +66,10 @@ export const RecordShowPage = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!record) return;
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEntityFields(record);
|
||||
}, [record, setEntityFields]);
|
||||
|
||||
@ -102,40 +107,43 @@ export const RecordShowPage = () => {
|
||||
: capitalize(objectNameSingular);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitle title={pageTitle} />
|
||||
<PageHeader
|
||||
title={pageName ?? ''}
|
||||
hasBackButton
|
||||
Icon={headerIcon}
|
||||
loading={loading}
|
||||
>
|
||||
<>
|
||||
<PageFavoriteButton
|
||||
isFavorite={isFavorite}
|
||||
onClick={handleFavoriteButtonClick}
|
||||
/>
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
activityTargetObject={{
|
||||
id: record?.id ?? '0',
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
}}
|
||||
/>
|
||||
<ShowPageMoreButton
|
||||
key="more"
|
||||
recordId={record?.id ?? '0'}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
</>
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<RecordShowContainer
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectRecordId={objectRecordId}
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<RecordValueSetterEffect recordId={objectRecordId} />
|
||||
<PageContainer>
|
||||
<PageTitle title={pageTitle} />
|
||||
<PageHeader
|
||||
title={pageName ?? ''}
|
||||
hasBackButton
|
||||
Icon={headerIcon}
|
||||
loading={loading}
|
||||
/>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
>
|
||||
<>
|
||||
<PageFavoriteButton
|
||||
isFavorite={isFavorite}
|
||||
onClick={handleFavoriteButtonClick}
|
||||
/>
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
activityTargetObject={{
|
||||
id: record?.id ?? '0',
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
}}
|
||||
/>
|
||||
<ShowPageMoreButton
|
||||
key="more"
|
||||
recordId={record?.id ?? '0'}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
</>
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<RecordShowContainer
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectRecordId={objectRecordId}
|
||||
loading={loading}
|
||||
/>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import { useRecoilState } from 'recoil';
|
||||
import { ProfilerWrapper } from '~/testing/profiling/components/ProfilerWrapper';
|
||||
import { ProfilingQueueEffect } from '~/testing/profiling/components/ProfilingQueueEffect';
|
||||
import { ProfilingReporter } from '~/testing/profiling/components/ProfilingReporter';
|
||||
import { currentProfilingRunIndexState } from '~/testing/profiling/states/currentProfilingRunState';
|
||||
import { currentProfilingRunIndexState } from '~/testing/profiling/states/currentProfilingRunIndexState';
|
||||
import { profilingSessionRunsState } from '~/testing/profiling/states/profilingSessionRunsState';
|
||||
import { profilingSessionStatusState } from '~/testing/profiling/states/profilingSessionStatusState';
|
||||
import { getTestArray } from '~/testing/profiling/utils/getTestArray';
|
||||
@ -12,6 +12,7 @@ import { getTestArray } from '~/testing/profiling/utils/getTestArray';
|
||||
export const ProfilerDecorator: Decorator = (Story, { id, parameters }) => {
|
||||
const numberOfTests = parameters.numberOfTests ?? 2;
|
||||
const numberOfRuns = parameters.numberOfRuns ?? 2;
|
||||
const warmUpRounds = parameters.warmUpRounds ?? 5;
|
||||
|
||||
const [currentProfilingRunIndex] = useRecoilState(
|
||||
currentProfilingRunIndexState,
|
||||
@ -31,6 +32,7 @@ export const ProfilerDecorator: Decorator = (Story, { id, parameters }) => {
|
||||
<ProfilingQueueEffect
|
||||
numberOfRuns={numberOfRuns}
|
||||
numberOfTestsPerRun={numberOfTests}
|
||||
warmUpRounds={warmUpRounds}
|
||||
profilingId={id}
|
||||
/>
|
||||
<div>
|
||||
|
@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { TIME_BETWEEN_TEST_RUNS_IN_MS } from '~/testing/profiling/constants/TimeBetweenTestRunsInMs';
|
||||
import { currentProfilingRunIndexState } from '~/testing/profiling/states/currentProfilingRunState';
|
||||
import { currentProfilingRunIndexState } from '~/testing/profiling/states/currentProfilingRunIndexState';
|
||||
import { profilingQueueState } from '~/testing/profiling/states/profilingQueueState';
|
||||
import { profilingSessionRunsState } from '~/testing/profiling/states/profilingSessionRunsState';
|
||||
import { profilingSessionStatusState } from '~/testing/profiling/states/profilingSessionStatusState';
|
||||
@ -12,10 +12,12 @@ export const ProfilingQueueEffect = ({
|
||||
profilingId,
|
||||
numberOfTestsPerRun,
|
||||
numberOfRuns,
|
||||
warmUpRounds,
|
||||
}: {
|
||||
profilingId: string;
|
||||
numberOfTestsPerRun: number;
|
||||
numberOfRuns: number;
|
||||
warmUpRounds: number;
|
||||
}) => {
|
||||
const [currentProfilingRunIndex, setCurrentProfilingRunIndex] =
|
||||
useRecoilState(currentProfilingRunIndexState);
|
||||
@ -38,9 +40,9 @@ export const ProfilingQueueEffect = ({
|
||||
setCurrentProfilingRunIndex(0);
|
||||
|
||||
const newTestRuns = [
|
||||
'warm-up-1',
|
||||
'warm-up-2',
|
||||
'warm-up-3',
|
||||
...[
|
||||
...Array.from({ length: warmUpRounds }, (_, i) => `warm-up-${i}`),
|
||||
],
|
||||
...[
|
||||
...Array.from({ length: numberOfRuns }, (_, i) => `real-run-${i}`),
|
||||
],
|
||||
@ -76,9 +78,13 @@ export const ProfilingQueueEffect = ({
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, TIME_BETWEEN_TEST_RUNS_IN_MS),
|
||||
);
|
||||
const timeInMs = profilingSessionRuns[
|
||||
currentProfilingRunIndex
|
||||
].startsWith('warm-up')
|
||||
? TIME_BETWEEN_TEST_RUNS_IN_MS * 2
|
||||
: TIME_BETWEEN_TEST_RUNS_IN_MS;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, timeInMs));
|
||||
|
||||
const nextIndex = currentProfilingRunIndex + 1;
|
||||
|
||||
@ -109,6 +115,7 @@ export const ProfilingQueueEffect = ({
|
||||
profilingSessionRuns,
|
||||
setProfilingSessionRuns,
|
||||
numberOfRuns,
|
||||
warmUpRounds,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
@ -3,7 +3,9 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { PROFILING_REPORTER_DIV_ID } from '~/testing/profiling/constants/ProfilingReporterDivId';
|
||||
import { currentProfilingRunIndexState } from '~/testing/profiling/states/currentProfilingRunIndexState';
|
||||
import { profilingSessionDataPointsState } from '~/testing/profiling/states/profilingSessionDataPointsState';
|
||||
import { profilingSessionStatusState } from '~/testing/profiling/states/profilingSessionStatusState';
|
||||
import { computeProfilingReport } from '~/testing/profiling/utils/computeProfilingReport';
|
||||
|
||||
const StyledTable = styled.table`
|
||||
@ -24,6 +26,12 @@ export const ProfilingReporter = () => {
|
||||
profilingSessionDataPointsState,
|
||||
);
|
||||
|
||||
const [currentProfilingRunIndex] = useRecoilState(
|
||||
currentProfilingRunIndexState,
|
||||
);
|
||||
|
||||
const [profilingSessionStatus] = useRecoilState(profilingSessionStatusState);
|
||||
|
||||
const profilingReport = useMemo(
|
||||
() => computeProfilingReport(profilingSessionDataPoints),
|
||||
[profilingSessionDataPoints],
|
||||
@ -34,6 +42,10 @@ export const ProfilingReporter = () => {
|
||||
data-profiling-report={JSON.stringify(profilingReport)}
|
||||
id={PROFILING_REPORTER_DIV_ID}
|
||||
>
|
||||
<h2>Profiling report</h2>
|
||||
<div>
|
||||
Run #{currentProfilingRunIndex} - Status {profilingSessionStatus}
|
||||
</div>
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -46,6 +58,7 @@ export const ProfilingReporter = () => {
|
||||
<th>P95</th>
|
||||
<th>P99</th>
|
||||
<th>Max</th>
|
||||
<th>Variance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -59,6 +72,9 @@ export const ProfilingReporter = () => {
|
||||
<td>{Math.round(profilingReport.total.p95 * 1000) / 1000}ms</td>
|
||||
<td>{Math.round(profilingReport.total.p99 * 1000) / 1000}ms</td>
|
||||
<td>{Math.round(profilingReport.total.max * 1000) / 1000}ms</td>
|
||||
<td>
|
||||
{Math.round(profilingReport.total.variance * 1000000) / 1000000}
|
||||
</td>
|
||||
</tr>
|
||||
{Object.entries(profilingReport.runs).map(([runName, report]) => (
|
||||
<tr key={runName}>
|
||||
@ -71,6 +87,7 @@ export const ProfilingReporter = () => {
|
||||
<td>{Math.round(report.p95 * 1000) / 1000}ms</td>
|
||||
<td>{Math.round(report.p99 * 1000) / 1000}ms</td>
|
||||
<td>{Math.round(report.max * 1000) / 1000}ms</td>
|
||||
<td>{Math.round(report.variance * 1000000) / 1000000}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const currentProfilingRunIndexState = atom<number>({
|
||||
key: 'currentProfilingRunIndexState',
|
||||
default: 0,
|
||||
});
|
@ -10,6 +10,7 @@ export type ProfilingReportItem = {
|
||||
p99: number;
|
||||
min: number;
|
||||
max: number;
|
||||
variance: number;
|
||||
};
|
||||
|
||||
export type ProfilingReport = {
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { ProfilingDataPoint } from '~/testing/profiling/types/ProfilingDataPoint';
|
||||
import { ProfilingReport } from '~/testing/profiling/types/ProfilingReportByRun';
|
||||
|
||||
export const computeProfilingReport = (dataPoints: ProfilingDataPoint[]) => {
|
||||
export const computeProfilingReport = (
|
||||
dataPoints: ProfilingDataPoint[],
|
||||
varianceThreshold?: number,
|
||||
) => {
|
||||
const profilingReport = { total: {}, runs: {} } as ProfilingReport;
|
||||
|
||||
for (const dataPoint of dataPoints) {
|
||||
@ -27,8 +30,9 @@ export const computeProfilingReport = (dataPoints: ProfilingDataPoint[]) => {
|
||||
|
||||
const numberOfIds = ids.length;
|
||||
|
||||
profilingReport.runs[runName].average =
|
||||
profilingReport.runs[runName].sum / numberOfIds;
|
||||
const mean = profilingReport.runs[runName].sum / numberOfIds;
|
||||
|
||||
profilingReport.runs[runName].average = mean;
|
||||
|
||||
profilingReport.runs[runName].min = Math.min(
|
||||
...Object.values(profilingReport.runs[runName].sumById),
|
||||
@ -38,6 +42,14 @@ export const computeProfilingReport = (dataPoints: ProfilingDataPoint[]) => {
|
||||
...Object.values(profilingReport.runs[runName].sumById),
|
||||
);
|
||||
|
||||
const intermediaryValuesForVariance = valuesUnsorted.map((value) =>
|
||||
Math.pow(value - mean, 2),
|
||||
);
|
||||
|
||||
profilingReport.runs[runName].variance =
|
||||
intermediaryValuesForVariance.reduce((acc, curr) => acc + curr) /
|
||||
numberOfIds;
|
||||
|
||||
const p50Index = Math.floor(numberOfIds * 0.5);
|
||||
const p80Index = Math.floor(numberOfIds * 0.8);
|
||||
const p90Index = Math.floor(numberOfIds * 0.9);
|
||||
@ -55,9 +67,9 @@ export const computeProfilingReport = (dataPoints: ProfilingDataPoint[]) => {
|
||||
runName.startsWith('real-run'),
|
||||
);
|
||||
|
||||
const runsForTotal = runNamesForTotal.map(
|
||||
(runName) => profilingReport.runs[runName],
|
||||
);
|
||||
const runsForTotal = runNamesForTotal
|
||||
.map((runName) => profilingReport.runs[runName])
|
||||
.filter((run) => run.variance < (varianceThreshold ?? 0.2));
|
||||
|
||||
profilingReport.total = {
|
||||
sum: Object.values(runsForTotal).reduce((acc, run) => acc + run.sum, 0),
|
||||
@ -82,6 +94,9 @@ export const computeProfilingReport = (dataPoints: ProfilingDataPoint[]) => {
|
||||
Object.values(runsForTotal).reduce((acc, run) => acc + run.p99, 0) /
|
||||
Object.keys(runsForTotal).length,
|
||||
dataPointCount: dataPoints.length,
|
||||
variance:
|
||||
runsForTotal.reduce((acc, run) => acc + run.variance, 0) /
|
||||
runsForTotal.length,
|
||||
};
|
||||
|
||||
return profilingReport;
|
||||
|
@ -11,19 +11,21 @@ export const getProfilingStory = ({
|
||||
averageThresholdInMs,
|
||||
numberOfRuns,
|
||||
numberOfTestsPerRun,
|
||||
warmUpRounds,
|
||||
}: {
|
||||
componentName: string;
|
||||
p95ThresholdInMs?: number;
|
||||
averageThresholdInMs: number;
|
||||
numberOfRuns: number;
|
||||
numberOfTestsPerRun: number;
|
||||
warmUpRounds?: number;
|
||||
}): StoryObj<any> => ({
|
||||
decorators: [ProfilerDecorator],
|
||||
parameters: {
|
||||
numberOfRuns,
|
||||
numberOfTests: numberOfTestsPerRun,
|
||||
componentName,
|
||||
chromatic: { disableSnapshot: true },
|
||||
warmUpRounds,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await findByTestId(
|
||||
|
11
yarn.lock
11
yarn.lock
@ -46630,6 +46630,7 @@ __metadata:
|
||||
type-fest: "npm:4.10.1"
|
||||
typeorm: "npm:^0.3.17"
|
||||
typescript: "npm:5.3.3"
|
||||
use-context-selector: "npm:^2.0.0"
|
||||
use-debounce: "npm:^10.0.0"
|
||||
uuid: "npm:^9.0.0"
|
||||
vite: "npm:^5.0.0"
|
||||
@ -47659,6 +47660,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-context-selector@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "use-context-selector@npm:2.0.0"
|
||||
peerDependencies:
|
||||
react: ">=18.0.0"
|
||||
scheduler: ">=0.19.0"
|
||||
checksum: 4eb6054ab8996ae8b3f87f9d102e576066e5a8b9db5db2c891128ae920bd64bcdcb4e93a13bc99658ef16280929a8331fc8ac45177f4acd716c425b1bc31135a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-debounce@npm:^10.0.0":
|
||||
version: 10.0.0
|
||||
resolution: "use-debounce@npm:10.0.0"
|
||||
|
Loading…
Reference in New Issue
Block a user