mirror of
https://github.com/twentyhq/twenty.git
synced 2024-10-27 03:33: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",
|
"tsup": "^8.0.1",
|
||||||
"type-fest": "4.10.1",
|
"type-fest": "4.10.1",
|
||||||
"typeorm": "^0.3.17",
|
"typeorm": "^0.3.17",
|
||||||
|
"use-context-selector": "^2.0.0",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite-tsconfig-paths": "^4.2.1",
|
"vite-tsconfig-paths": "^4.2.1",
|
||||||
|
@ -53,12 +53,12 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Relations"
|
label="Relations"
|
||||||
displayModeContent={() => (
|
displayModeContent={
|
||||||
<ActivityTargetChips
|
<ActivityTargetChips
|
||||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
||||||
/>
|
/>
|
||||||
</RecordFieldInputScope>
|
</RecordFieldInputScope>
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
|
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';
|
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||||
|
|
||||||
@ -15,7 +18,7 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
showLabel,
|
showLabel,
|
||||||
labelWidth,
|
labelWidth,
|
||||||
}: FieldMetadataItemAsFieldDefinitionProps) => {
|
}: FieldMetadataItemAsFieldDefinitionProps): FieldDefinition<FieldMetadata> => {
|
||||||
const relationObjectMetadataItem =
|
const relationObjectMetadataItem =
|
||||||
field.toRelationMetadata?.fromObjectMetadata ||
|
field.toRelationMetadata?.fromObjectMetadata ||
|
||||||
field.fromRelationMetadata?.toObjectMetadata;
|
field.fromRelationMetadata?.toObjectMetadata;
|
||||||
@ -24,25 +27,31 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
|||||||
field.toRelationMetadata?.fromFieldMetadataId ||
|
field.toRelationMetadata?.fromFieldMetadataId ||
|
||||||
field.fromRelationMetadata?.toFieldMetadataId;
|
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 {
|
return {
|
||||||
fieldMetadataId: field.id,
|
fieldMetadataId: field.id,
|
||||||
label: field.label,
|
label: field.label,
|
||||||
showLabel,
|
showLabel,
|
||||||
labelWidth,
|
labelWidth,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
metadata: {
|
metadata: fieldDefintionMetadata,
|
||||||
fieldName: field.name,
|
|
||||||
placeHolder: field.label,
|
|
||||||
relationType: parseFieldRelationType(field),
|
|
||||||
relationFieldMetadataId,
|
|
||||||
relationObjectMetadataNameSingular:
|
|
||||||
relationObjectMetadataItem?.nameSingular ?? '',
|
|
||||||
relationObjectMetadataNamePlural:
|
|
||||||
relationObjectMetadataItem?.namePlural ?? '',
|
|
||||||
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
|
|
||||||
options: field.options,
|
|
||||||
},
|
|
||||||
iconName: field.icon ?? 'Icon123',
|
iconName: field.icon ?? 'Icon123',
|
||||||
defaultValue: field.defaultValue,
|
defaultValue: field.defaultValue,
|
||||||
|
editButtonIcon: getFieldButtonIcon({
|
||||||
|
metadata: fieldDefintionMetadata,
|
||||||
|
type: field.type,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -13,8 +13,10 @@ import {
|
|||||||
RecordUpdateHook,
|
RecordUpdateHook,
|
||||||
RecordUpdateHookParams,
|
RecordUpdateHookParams,
|
||||||
} from '@/object-record/record-field/contexts/FieldContext';
|
} 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 { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
||||||
@ -209,6 +211,7 @@ export const RecordBoardCard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBoardCardWrapper onContextMenu={handleContextMenu}>
|
<StyledBoardCardWrapper onContextMenu={handleContextMenu}>
|
||||||
|
<RecordValueSetterEffect recordId={recordId} />
|
||||||
<StyledBoardCard
|
<StyledBoardCard
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
selected={isCurrentCardSelected}
|
selected={isCurrentCardSelected}
|
||||||
@ -266,6 +269,10 @@ export const RecordBoardCard = () => {
|
|||||||
type: fieldDefinition.type,
|
type: fieldDefinition.type,
|
||||||
metadata: fieldDefinition.metadata,
|
metadata: fieldDefinition.metadata,
|
||||||
defaultValue: fieldDefinition.defaultValue,
|
defaultValue: fieldDefinition.defaultValue,
|
||||||
|
editButtonIcon: getFieldButtonIcon({
|
||||||
|
metadata: fieldDefinition.metadata,
|
||||||
|
type: fieldDefinition.type,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
useUpdateRecord: useUpdateOneRecordHook,
|
useUpdateRecord: useUpdateOneRecordHook,
|
||||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||||
|
@ -37,17 +37,7 @@ import { isFieldSelect } from '../types/guards/isFieldSelect';
|
|||||||
import { isFieldText } from '../types/guards/isFieldText';
|
import { isFieldText } from '../types/guards/isFieldText';
|
||||||
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
||||||
|
|
||||||
type FieldDisplayProps = {
|
export const FieldDisplay = () => {
|
||||||
isCellSoftFocused?: boolean;
|
|
||||||
cellElement?: HTMLElement;
|
|
||||||
fromTableCell?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FieldDisplay = ({
|
|
||||||
isCellSoftFocused,
|
|
||||||
cellElement,
|
|
||||||
fromTableCell,
|
|
||||||
}: FieldDisplayProps) => {
|
|
||||||
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
|
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
|
||||||
|
|
||||||
const isChipDisplay =
|
const isChipDisplay =
|
||||||
@ -78,10 +68,7 @@ export const FieldDisplay = ({
|
|||||||
) : isFieldLink(fieldDefinition) ? (
|
) : isFieldLink(fieldDefinition) ? (
|
||||||
<LinkFieldDisplay />
|
<LinkFieldDisplay />
|
||||||
) : isFieldLinks(fieldDefinition) ? (
|
) : isFieldLinks(fieldDefinition) ? (
|
||||||
<LinksFieldDisplay
|
<LinksFieldDisplay />
|
||||||
isCellSoftFocused={isCellSoftFocused}
|
|
||||||
fromTableCell={fromTableCell}
|
|
||||||
/>
|
|
||||||
) : isFieldCurrency(fieldDefinition) ? (
|
) : isFieldCurrency(fieldDefinition) ? (
|
||||||
<CurrencyFieldDisplay />
|
<CurrencyFieldDisplay />
|
||||||
) : isFieldFullName(fieldDefinition) ? (
|
) : isFieldFullName(fieldDefinition) ? (
|
||||||
@ -89,11 +76,7 @@ export const FieldDisplay = ({
|
|||||||
) : isFieldSelect(fieldDefinition) ? (
|
) : isFieldSelect(fieldDefinition) ? (
|
||||||
<SelectFieldDisplay />
|
<SelectFieldDisplay />
|
||||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||||
<MultiSelectFieldDisplay
|
<MultiSelectFieldDisplay />
|
||||||
isCellSoftFocused={isCellSoftFocused}
|
|
||||||
cellElement={cellElement}
|
|
||||||
fromTableCell={fromTableCell}
|
|
||||||
/>
|
|
||||||
) : isFieldAddress(fieldDefinition) ? (
|
) : isFieldAddress(fieldDefinition) ? (
|
||||||
<AddressFieldDisplay />
|
<AddressFieldDisplay />
|
||||||
) : isFieldRawJson(fieldDefinition) ? (
|
) : 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 { 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 { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||||
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 { FieldContext } from '../contexts/FieldContext';
|
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 => {
|
export const useGetButtonIcon = (): IconComponent | undefined => {
|
||||||
const { fieldDefinition } = useContext(FieldContext);
|
const { fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
if (isUndefinedOrNull(fieldDefinition)) return undefined;
|
return getFieldButtonIcon(fieldDefinition);
|
||||||
|
|
||||||
if (
|
|
||||||
isFieldLink(fieldDefinition) ||
|
|
||||||
isFieldEmail(fieldDefinition) ||
|
|
||||||
isFieldPhone(fieldDefinition) ||
|
|
||||||
isFieldDisplayedAsPhone(fieldDefinition) ||
|
|
||||||
isFieldMultiSelect(fieldDefinition) ||
|
|
||||||
(isFieldRelation(fieldDefinition) &&
|
|
||||||
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
|
||||||
'workspaceMember') ||
|
|
||||||
isFieldLinks(fieldDefinition)
|
|
||||||
) {
|
|
||||||
return IconPencil;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
|
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';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
|
|
||||||
export const useIsFieldEmpty = () => {
|
export const useIsFieldEmpty = () => {
|
||||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||||
const fieldValue = useRecoilValue(
|
|
||||||
recordStoreFamilySelector({
|
const fieldValue = useRecordFieldValue(
|
||||||
fieldName: fieldDefinition.metadata.fieldName,
|
entityId,
|
||||||
recordId: entityId,
|
fieldDefinition.metadata.fieldName,
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return isFieldValueEmpty({
|
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 { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
||||||
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
|
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
|
||||||
|
|
||||||
type LinksFieldDisplayProps = {
|
export const LinksFieldDisplay = () => {
|
||||||
isCellSoftFocused?: boolean;
|
|
||||||
fromTableCell?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LinksFieldDisplay = ({
|
|
||||||
isCellSoftFocused,
|
|
||||||
fromTableCell,
|
|
||||||
}: LinksFieldDisplayProps) => {
|
|
||||||
const { fieldValue } = useLinksField();
|
const { fieldValue } = useLinksField();
|
||||||
|
|
||||||
return (
|
const { isFocused } = useFieldFocus();
|
||||||
<LinksDisplay
|
|
||||||
value={fieldValue}
|
return <LinksDisplay value={fieldValue} isFocused={isFocused} />;
|
||||||
isChipCountDisplayed={isCellSoftFocused}
|
|
||||||
withExpandedListBorder={fromTableCell}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
import { Tag } from 'twenty-ui';
|
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 { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
|
||||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
|
|
||||||
type MultiSelectFieldDisplayProps = {
|
export const MultiSelectFieldDisplay = () => {
|
||||||
isCellSoftFocused?: boolean;
|
|
||||||
cellElement?: HTMLElement;
|
|
||||||
fromTableCell?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MultiSelectFieldDisplay = ({
|
|
||||||
isCellSoftFocused,
|
|
||||||
fromTableCell,
|
|
||||||
}: MultiSelectFieldDisplayProps) => {
|
|
||||||
const { fieldValues, fieldDefinition } = useMultiSelectField();
|
const { fieldValues, fieldDefinition } = useMultiSelectField();
|
||||||
|
|
||||||
|
const { isFocused } = useFieldFocus();
|
||||||
|
|
||||||
const selectedOptions = fieldValues
|
const selectedOptions = fieldValues
|
||||||
? fieldDefinition.metadata.options?.filter((option) =>
|
? fieldDefinition.metadata.options?.filter((option) =>
|
||||||
fieldValues.includes(option.value),
|
fieldValues.includes(option.value),
|
||||||
@ -24,10 +18,7 @@ export const MultiSelectFieldDisplay = ({
|
|||||||
if (!selectedOptions) return null;
|
if (!selectedOptions) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableList
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
isChipCountDisplayed={isCellSoftFocused}
|
|
||||||
withExpandedListBorder={fromTableCell}
|
|
||||||
>
|
|
||||||
{selectedOptions.map((selectedOption, index) => (
|
{selectedOptions.map((selectedOption, index) => (
|
||||||
<Tag
|
<Tag
|
||||||
key={index}
|
key={index}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
import { useRelationField } from '../../hooks/useRelationField';
|
|
||||||
|
|
||||||
export const RelationFieldDisplay = () => {
|
export const RelationFieldDisplay = () => {
|
||||||
const { fieldValue, fieldDefinition, maxWidth } = useRelationField();
|
const { fieldValue, fieldDefinition, maxWidth } = useRelationFieldDisplay();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!fieldValue ||
|
!fieldValue ||
|
||||||
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
||||||
)
|
) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordChip
|
<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 { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
import { FieldMetadata } from './FieldMetadata';
|
import { FieldMetadata } from './FieldMetadata';
|
||||||
@ -24,4 +26,5 @@ export type FieldDefinition<T extends FieldMetadata> = {
|
|||||||
metadata: T;
|
metadata: T;
|
||||||
infoTooltipContent?: string;
|
infoTooltipContent?: string;
|
||||||
defaultValue?: any;
|
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 { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
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 { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
|
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
|
||||||
import { ViewBar } from '@/views/components/ViewBar';
|
import { ViewBar } from '@/views/components/ViewBar';
|
||||||
@ -105,74 +106,78 @@ export const RecordIndexContainer = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<SpreadsheetImportProvider>
|
<RecordFieldValueSelectorContextProvider>
|
||||||
<ViewBar
|
<SpreadsheetImportProvider>
|
||||||
viewBarId={recordIndexId}
|
<ViewBar
|
||||||
optionsDropdownButton={
|
viewBarId={recordIndexId}
|
||||||
<RecordIndexOptionsDropdown
|
optionsDropdownButton={
|
||||||
recordIndexId={recordIndexId}
|
<RecordIndexOptionsDropdown
|
||||||
objectNameSingular={objectNameSingular}
|
recordIndexId={recordIndexId}
|
||||||
viewType={recordIndexViewType ?? ViewType.Table}
|
objectNameSingular={objectNameSingular}
|
||||||
/>
|
viewType={recordIndexViewType ?? ViewType.Table}
|
||||||
}
|
/>
|
||||||
onCurrentViewChange={(view) => {
|
|
||||||
if (!view) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
onCurrentViewChange={(view) => {
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onViewFieldsChange(view.viewFields);
|
onViewFieldsChange(view.viewFields);
|
||||||
setTableFilters(
|
setTableFilters(
|
||||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||||
);
|
);
|
||||||
setRecordIndexFilters(
|
setRecordIndexFilters(
|
||||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||||
);
|
);
|
||||||
setTableSorts(mapViewSortsToSorts(view.viewSorts, sortDefinitions));
|
setTableSorts(
|
||||||
setRecordIndexSorts(
|
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
);
|
||||||
);
|
setRecordIndexSorts(
|
||||||
setRecordIndexViewType(view.type);
|
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
);
|
||||||
view.kanbanFieldMetadataId,
|
setRecordIndexViewType(view.type);
|
||||||
);
|
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||||
setRecordIndexIsCompactModeActive(view.isCompact);
|
view.kanbanFieldMetadataId,
|
||||||
}}
|
);
|
||||||
/>
|
setRecordIndexIsCompactModeActive(view.isCompact);
|
||||||
<RecordIndexViewBarEffect
|
}}
|
||||||
objectNamePlural={objectNamePlural}
|
|
||||||
viewBarId={recordIndexId}
|
|
||||||
/>
|
|
||||||
</SpreadsheetImportProvider>
|
|
||||||
{recordIndexViewType === ViewType.Table && (
|
|
||||||
<>
|
|
||||||
<RecordIndexTableContainer
|
|
||||||
recordTableId={recordIndexId}
|
|
||||||
viewBarId={recordIndexId}
|
|
||||||
objectNameSingular={objectNameSingular}
|
|
||||||
createRecord={createRecord}
|
|
||||||
/>
|
/>
|
||||||
<RecordIndexTableContainerEffect
|
<RecordIndexViewBarEffect
|
||||||
objectNameSingular={objectNameSingular}
|
objectNamePlural={objectNamePlural}
|
||||||
recordTableId={recordIndexId}
|
|
||||||
viewBarId={recordIndexId}
|
viewBarId={recordIndexId}
|
||||||
/>
|
/>
|
||||||
</>
|
</SpreadsheetImportProvider>
|
||||||
)}
|
{recordIndexViewType === ViewType.Table && (
|
||||||
{recordIndexViewType === ViewType.Kanban && (
|
<>
|
||||||
<>
|
<RecordIndexTableContainer
|
||||||
<RecordIndexBoardContainer
|
recordTableId={recordIndexId}
|
||||||
recordBoardId={recordIndexId}
|
viewBarId={recordIndexId}
|
||||||
viewBarId={recordIndexId}
|
objectNameSingular={objectNameSingular}
|
||||||
objectNameSingular={objectNameSingular}
|
createRecord={createRecord}
|
||||||
createRecord={createRecord}
|
/>
|
||||||
/>
|
<RecordIndexTableContainerEffect
|
||||||
<RecordIndexBoardContainerEffect
|
objectNameSingular={objectNameSingular}
|
||||||
objectNameSingular={objectNameSingular}
|
recordTableId={recordIndexId}
|
||||||
recordBoardId={recordIndexId}
|
viewBarId={recordIndexId}
|
||||||
viewBarId={recordIndexId}
|
/>
|
||||||
/>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
{recordIndexViewType === ViewType.Kanban && (
|
||||||
|
<>
|
||||||
|
<RecordIndexBoardContainer
|
||||||
|
recordBoardId={recordIndexId}
|
||||||
|
viewBarId={recordIndexId}
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
createRecord={createRecord}
|
||||||
|
/>
|
||||||
|
<RecordIndexBoardContainerEffect
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
recordBoardId={recordIndexId}
|
||||||
|
viewBarId={recordIndexId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</RecordFieldValueSelectorContextProvider>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ import { useIcons } from 'twenty-ui';
|
|||||||
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
||||||
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
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 { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
||||||
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
|
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
|
||||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||||
@ -70,45 +71,44 @@ export const RecordInlineCell = ({
|
|||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordInlineCellContainer
|
<FieldFocusContextProvider>
|
||||||
readonly={readonly}
|
<RecordInlineCellContainer
|
||||||
buttonIcon={buttonIcon}
|
readonly={readonly}
|
||||||
customEditHotkeyScope={
|
buttonIcon={buttonIcon}
|
||||||
isFieldRelation(fieldDefinition)
|
customEditHotkeyScope={
|
||||||
? {
|
isFieldRelation(fieldDefinition)
|
||||||
scope: RelationPickerHotkeyScope.RelationPicker,
|
? {
|
||||||
}
|
scope: RelationPickerHotkeyScope.RelationPicker,
|
||||||
: undefined
|
}
|
||||||
}
|
: undefined
|
||||||
IconLabel={
|
}
|
||||||
fieldDefinition.iconName ? getIcon(fieldDefinition.iconName) : undefined
|
IconLabel={
|
||||||
}
|
fieldDefinition.iconName
|
||||||
label={fieldDefinition.label}
|
? getIcon(fieldDefinition.iconName)
|
||||||
labelWidth={fieldDefinition.labelWidth}
|
: undefined
|
||||||
showLabel={fieldDefinition.showLabel}
|
}
|
||||||
editModeContent={
|
label={fieldDefinition.label}
|
||||||
<FieldInput
|
labelWidth={fieldDefinition.labelWidth}
|
||||||
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
|
showLabel={fieldDefinition.showLabel}
|
||||||
onEnter={handleEnter}
|
editModeContent={
|
||||||
onCancel={handleCancel}
|
<FieldInput
|
||||||
onEscape={handleEscape}
|
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
|
||||||
onSubmit={handleSubmit}
|
onEnter={handleEnter}
|
||||||
onTab={handleTab}
|
onCancel={handleCancel}
|
||||||
onShiftTab={handleShiftTab}
|
onEscape={handleEscape}
|
||||||
onClickOutside={handleClickOutside}
|
onSubmit={handleSubmit}
|
||||||
isReadOnly={readonly}
|
onTab={handleTab}
|
||||||
/>
|
onShiftTab={handleShiftTab}
|
||||||
}
|
onClickOutside={handleClickOutside}
|
||||||
displayModeContent={({ cellElement, isCellSoftFocused }) => (
|
isReadOnly={readonly}
|
||||||
<FieldDisplay
|
/>
|
||||||
cellElement={cellElement}
|
}
|
||||||
isCellSoftFocused={isCellSoftFocused}
|
displayModeContent={<FieldDisplay />}
|
||||||
/>
|
isDisplayModeContentEmpty={isFieldEmpty}
|
||||||
)}
|
isDisplayModeFixHeight
|
||||||
isDisplayModeContentEmpty={isFieldEmpty}
|
editModeContentOnly={isFieldInputOnly}
|
||||||
isDisplayModeFixHeight
|
loading={loading}
|
||||||
editModeContentOnly={isFieldInputOnly}
|
/>
|
||||||
loading={loading}
|
</FieldFocusContextProvider>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
import React, { useContext, useState } from 'react';
|
import React, { ReactElement, useContext } from 'react';
|
||||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
|
||||||
import { Tooltip } from 'react-tooltip';
|
import { Tooltip } from 'react-tooltip';
|
||||||
import { css, useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
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 { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
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`
|
const StyledIconContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
@ -48,18 +43,6 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
|
|||||||
width: ${({ width }) => width}px;
|
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`
|
const StyledInlineCellBaseContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -82,41 +65,20 @@ const StyledTooltip = styled(Tooltip)`
|
|||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSkeletonDiv = styled.div`
|
export const StyledSkeletonDiv = styled.div`
|
||||||
height: 24px;
|
height: 24px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInlineCellSkeletonLoader = () => {
|
export type RecordInlineCellContainerProps = {
|
||||||
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 = {
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
IconLabel?: IconComponent;
|
IconLabel?: IconComponent;
|
||||||
label?: string;
|
label?: string;
|
||||||
labelWidth?: number;
|
labelWidth?: number;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
buttonIcon?: IconComponent;
|
buttonIcon?: IconComponent;
|
||||||
editModeContent?: React.ReactNode;
|
editModeContent?: ReactElement;
|
||||||
editModeContentOnly?: boolean;
|
editModeContentOnly?: boolean;
|
||||||
displayModeContent: ({
|
displayModeContent: ReactElement;
|
||||||
isCellSoftFocused,
|
|
||||||
cellElement,
|
|
||||||
}: {
|
|
||||||
isCellSoftFocused: boolean;
|
|
||||||
cellElement?: HTMLDivElement;
|
|
||||||
}) => React.ReactNode;
|
|
||||||
customEditHotkeyScope?: HotkeyScope;
|
customEditHotkeyScope?: HotkeyScope;
|
||||||
isDisplayModeContentEmpty?: boolean;
|
isDisplayModeContentEmpty?: boolean;
|
||||||
isDisplayModeFixHeight?: boolean;
|
isDisplayModeFixHeight?: boolean;
|
||||||
@ -141,85 +103,24 @@ export const RecordInlineCellContainer = ({
|
|||||||
loading = false,
|
loading = false,
|
||||||
}: RecordInlineCellContainerProps) => {
|
}: RecordInlineCellContainerProps) => {
|
||||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
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`,
|
const { setIsFocused } = useFieldFocus();
|
||||||
// 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 handleContainerMouseEnter = () => {
|
const handleContainerMouseEnter = () => {
|
||||||
if (!readonly) {
|
if (!readonly) {
|
||||||
setIsHovered(true);
|
setIsFocused(true);
|
||||||
}
|
}
|
||||||
setIsCellSoftFocused(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContainerMouseLeave = () => {
|
const handleContainerMouseLeave = () => {
|
||||||
if (!readonly) {
|
if (!readonly) {
|
||||||
setIsHovered(false);
|
setIsFocused(false);
|
||||||
}
|
|
||||||
setIsCellSoftFocused(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
|
|
||||||
|
|
||||||
const handleDisplayModeClick = () => {
|
|
||||||
if (!readonly && !editModeContentOnly) {
|
|
||||||
openInlineCell(customEditHotkeyScope);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEditButton =
|
|
||||||
buttonIcon &&
|
|
||||||
!isInlineCellInEditMode &&
|
|
||||||
isHovered &&
|
|
||||||
!editModeContentOnly &&
|
|
||||||
!isDisplayModeContentEmpty;
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`;
|
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 (
|
return (
|
||||||
<StyledInlineCellBaseContainer
|
<StyledInlineCellBaseContainer
|
||||||
onMouseEnter={handleContainerMouseEnter}
|
onMouseEnter={handleContainerMouseEnter}
|
||||||
@ -250,8 +151,23 @@ export const RecordInlineCellContainer = ({
|
|||||||
)}
|
)}
|
||||||
</StyledLabelAndIconContainer>
|
</StyledLabelAndIconContainer>
|
||||||
)}
|
)}
|
||||||
<StyledValueContainer ref={setCellElement}>
|
<StyledValueContainer>
|
||||||
{showContent()}
|
<RecordInlineCellValue
|
||||||
|
{...{
|
||||||
|
displayModeContent,
|
||||||
|
customEditHotkeyScope,
|
||||||
|
disableHoverEffect,
|
||||||
|
editModeContent,
|
||||||
|
editModeContentOnly,
|
||||||
|
isDisplayModeContentEmpty,
|
||||||
|
isDisplayModeFixHeight,
|
||||||
|
buttonIcon,
|
||||||
|
label,
|
||||||
|
loading,
|
||||||
|
readonly,
|
||||||
|
showLabel,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</StyledValueContainer>
|
</StyledValueContainer>
|
||||||
</StyledInlineCellBaseContainer>
|
</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 { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||||
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
|
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 { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
|
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
|
||||||
@ -187,6 +188,7 @@ export const RecordDetailRelationRecordsListItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<RecordValueSetterEffect recordId={relationRecord.id} />
|
||||||
<StyledListItem isDropdownOpen={isDropdownOpen}>
|
<StyledListItem isDropdownOpen={isDropdownOpen}>
|
||||||
<RecordChip
|
<RecordChip
|
||||||
record={relationRecord}
|
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 { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
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 { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/components/RecordTableCellFieldContextWrapper';
|
||||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
@ -52,6 +53,7 @@ export const RecordTableRow = ({ recordId, rowIndex }: RecordTableRowProps) => {
|
|||||||
isReadOnly: objectMetadataItem.isRemote ?? false,
|
isReadOnly: objectMetadataItem.isRemote ?? false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<RecordValueSetterEffect recordId={recordId} />
|
||||||
<tr
|
<tr
|
||||||
ref={elementRef}
|
ref={elementRef}
|
||||||
data-testid={`row-id-${recordId}`}
|
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),
|
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(
|
||||||
|
`edit-mode-change-${currentTableCellInEditModePosition.row}:${currentTableCellInEditModePosition.column}`,
|
||||||
|
{ detail: false },
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState],
|
[currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState],
|
||||||
|
@ -24,9 +24,23 @@ export const useMoveEditModeToTableCellPosition = (recordTableId?: string) => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(
|
||||||
|
`edit-mode-change-${currentTableCellInEditModePosition.row}:${currentTableCellInEditModePosition.column}`,
|
||||||
|
{ detail: false },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
set(currentTableCellInEditModePositionState, newPosition);
|
set(currentTableCellInEditModePositionState, newPosition);
|
||||||
|
|
||||||
set(isTableCellInEditModeFamilyState(newPosition), true);
|
set(isTableCellInEditModeFamilyState(newPosition), true);
|
||||||
|
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(
|
||||||
|
`edit-mode-change-${newPosition.row}:${newPosition.column}`,
|
||||||
|
{ detail: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState],
|
[currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState],
|
||||||
|
@ -24,9 +24,23 @@ export const useSetSoftFocusPosition = (recordTableId?: string) => {
|
|||||||
|
|
||||||
set(isSoftFocusOnTableCellFamilyState(currentPosition), false);
|
set(isSoftFocusOnTableCellFamilyState(currentPosition), false);
|
||||||
|
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(
|
||||||
|
`soft-focus-move-${currentPosition.row}:${currentPosition.column}`,
|
||||||
|
{ detail: false },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
set(softFocusPositionState, newPosition);
|
set(softFocusPositionState, newPosition);
|
||||||
|
|
||||||
set(isSoftFocusOnTableCellFamilyState(newPosition), true);
|
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 { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
||||||
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
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 { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
@ -87,28 +88,24 @@ export const RecordTableCell = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordTableCellContainer
|
<FieldFocusContextProvider>
|
||||||
editHotkeyScope={customHotkeyScope}
|
<RecordTableCellContainer
|
||||||
editModeContent={
|
editHotkeyScope={customHotkeyScope}
|
||||||
<FieldInput
|
editModeContent={
|
||||||
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
|
<FieldInput
|
||||||
onCancel={handleCancel}
|
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
|
||||||
onClickOutside={handleClickOutside}
|
onCancel={handleCancel}
|
||||||
onEnter={handleEnter}
|
onClickOutside={handleClickOutside}
|
||||||
onEscape={handleEscape}
|
onEnter={handleEnter}
|
||||||
onShiftTab={handleShiftTab}
|
onEscape={handleEscape}
|
||||||
onSubmit={handleSubmit}
|
onShiftTab={handleShiftTab}
|
||||||
onTab={handleTab}
|
onSubmit={handleSubmit}
|
||||||
isReadOnly={isReadOnly}
|
onTab={handleTab}
|
||||||
/>
|
isReadOnly={isReadOnly}
|
||||||
}
|
/>
|
||||||
nonEditModeContent={({ isCellSoftFocused, cellElement }) => (
|
}
|
||||||
<FieldDisplay
|
nonEditModeContent={<FieldDisplay />}
|
||||||
isCellSoftFocused={isCellSoftFocused}
|
/>
|
||||||
cellElement={cellElement}
|
</FieldFocusContextProvider>
|
||||||
fromTableCell
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { IconArrowUpRight } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
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 { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
|
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 { 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 { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
|
||||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||||
|
|
||||||
import { RecordTableCellButton } from './RecordTableCellButton';
|
|
||||||
import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode';
|
import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode';
|
||||||
import { RecordTableCellEditMode } from './RecordTableCellEditMode';
|
import { RecordTableCellEditMode } from './RecordTableCellEditMode';
|
||||||
import { RecordTableCellSoftFocusMode } from './RecordTableCellSoftFocusMode';
|
import { RecordTableCellSoftFocusMode } from './RecordTableCellSoftFocusMode';
|
||||||
@ -51,13 +38,7 @@ const StyledCellBaseContainer = styled.div<{ softFocus: boolean }>`
|
|||||||
|
|
||||||
export type RecordTableCellContainerProps = {
|
export type RecordTableCellContainerProps = {
|
||||||
editModeContent: ReactElement;
|
editModeContent: ReactElement;
|
||||||
nonEditModeContent?: ({
|
nonEditModeContent: ReactElement;
|
||||||
isCellSoftFocused,
|
|
||||||
cellElement,
|
|
||||||
}: {
|
|
||||||
isCellSoftFocused: boolean;
|
|
||||||
cellElement?: HTMLTableCellElement;
|
|
||||||
}) => ReactElement;
|
|
||||||
editHotkeyScope?: HotkeyScope;
|
editHotkeyScope?: HotkeyScope;
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
maxContentWidth?: number;
|
maxContentWidth?: number;
|
||||||
@ -74,91 +55,85 @@ export const RecordTableCellContainer = ({
|
|||||||
nonEditModeContent,
|
nonEditModeContent,
|
||||||
editHotkeyScope,
|
editHotkeyScope,
|
||||||
}: RecordTableCellContainerProps) => {
|
}: RecordTableCellContainerProps) => {
|
||||||
const { columnIndex } = useContext(RecordTableCellContext);
|
const { setIsFocused } = useFieldFocus();
|
||||||
// Used by some fields in ExpandableList as an anchor for the floating element.
|
|
||||||
// floating-ui mentions that `useState` must be used instead of `useRef`,
|
const { isSelected, recordId } = useContext(RecordTableRowContext);
|
||||||
// see https://floating-ui.com/docs/useFloating#elements
|
const { onContextMenu, onCellMouseEnter } = useContext(RecordTableContext);
|
||||||
const [cellElement, setCellElement] = useState<HTMLTableCellElement | null>(
|
|
||||||
null,
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
);
|
const [hasSoftFocus, setHasSoftFocus] = useState(false);
|
||||||
const [isCellBaseContainerHovered, setIsCellBaseContainerHovered] =
|
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||||
useState(false);
|
|
||||||
const { isReadOnly, isSelected, recordId } = useContext(
|
|
||||||
RecordTableRowContext,
|
|
||||||
);
|
|
||||||
const { onMoveSoftFocusToCell, onContextMenu, onCellMouseEnter } =
|
|
||||||
useContext(RecordTableContext);
|
|
||||||
|
|
||||||
const cellPosition = useCurrentTableCellPosition();
|
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) => {
|
const handleContextMenu = (event: React.MouseEvent) => {
|
||||||
onContextMenu(event, recordId);
|
onContextMenu(event, recordId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContainerMouseEnter = () => {
|
const handleContainerMouseEnter = () => {
|
||||||
onCellMouseEnter({
|
if (!hasSoftFocus) {
|
||||||
cellPosition,
|
onCellMouseEnter({
|
||||||
isHovered: isCellBaseContainerHovered,
|
cellPosition,
|
||||||
setIsHovered: setIsCellBaseContainerHovered,
|
isHovered,
|
||||||
});
|
setIsHovered,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContainerMouseLeave = () => {
|
const handleContainerMouseLeave = () => {
|
||||||
setIsCellBaseContainerHovered(false);
|
setIsHovered(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const editModeContentOnly = useIsFieldInputOnly();
|
const handleContainerMouseMove = () => {
|
||||||
|
handleContainerMouseEnter();
|
||||||
|
};
|
||||||
|
|
||||||
const isFirstColumn = columnIndex === 0;
|
useEffect(() => {
|
||||||
const customButtonIcon = useGetButtonIcon();
|
const customEventListener = (event: any) => {
|
||||||
const buttonIcon = isFirstColumn ? IconArrowUpRight : customButtonIcon;
|
const newHasSoftFocus = event.detail;
|
||||||
|
|
||||||
const showButton =
|
setHasSoftFocus(newHasSoftFocus);
|
||||||
!!buttonIcon &&
|
setIsFocused(newHasSoftFocus);
|
||||||
hasSoftFocus &&
|
};
|
||||||
!isCurrentTableCellInEditMode &&
|
|
||||||
!editModeContentOnly &&
|
document.addEventListener(
|
||||||
(!isFirstColumn || !isEmpty) &&
|
`soft-focus-move-${cellPosition.row}:${cellPosition.column}`,
|
||||||
!isReadOnly;
|
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 (
|
return (
|
||||||
<StyledTd
|
<StyledTd
|
||||||
ref={setCellElement}
|
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
isInEditMode={isCurrentTableCellInEditMode}
|
isInEditMode={isInEditMode}
|
||||||
>
|
>
|
||||||
<CellHotkeyScopeContext.Provider
|
<CellHotkeyScopeContext.Provider
|
||||||
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
|
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
|
||||||
@ -166,46 +141,22 @@ export const RecordTableCellContainer = ({
|
|||||||
<StyledCellBaseContainer
|
<StyledCellBaseContainer
|
||||||
onMouseEnter={handleContainerMouseEnter}
|
onMouseEnter={handleContainerMouseEnter}
|
||||||
onMouseLeave={handleContainerMouseLeave}
|
onMouseLeave={handleContainerMouseLeave}
|
||||||
|
onMouseMove={handleContainerMouseMove}
|
||||||
softFocus={hasSoftFocus}
|
softFocus={hasSoftFocus}
|
||||||
>
|
>
|
||||||
{isCurrentTableCellInEditMode ? (
|
{isInEditMode ? (
|
||||||
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
|
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
|
||||||
) : hasSoftFocus ? (
|
) : hasSoftFocus ? (
|
||||||
<>
|
<>
|
||||||
<RecordTableCellSoftFocusMode>
|
<RecordTableCellSoftFocusMode
|
||||||
{editModeContentOnly
|
editModeContent={editModeContent}
|
||||||
? editModeContent
|
nonEditModeContent={nonEditModeContent}
|
||||||
: nonEditModeContent?.({
|
/>
|
||||||
isCellSoftFocused: true,
|
|
||||||
cellElement: cellElement ?? undefined,
|
|
||||||
})}
|
|
||||||
</RecordTableCellSoftFocusMode>
|
|
||||||
{showButton && (
|
|
||||||
<RecordTableCellButton
|
|
||||||
onClick={handleButtonClick}
|
|
||||||
Icon={buttonIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<RecordTableCellDisplayMode>
|
||||||
{!isEmpty && (
|
{nonEditModeContent}
|
||||||
<RecordTableCellDisplayMode>
|
</RecordTableCellDisplayMode>
|
||||||
{editModeContentOnly
|
|
||||||
? editModeContent
|
|
||||||
: nonEditModeContent?.({
|
|
||||||
isCellSoftFocused: false,
|
|
||||||
cellElement: cellElement ?? undefined,
|
|
||||||
})}
|
|
||||||
</RecordTableCellDisplayMode>
|
|
||||||
)}
|
|
||||||
{showButton && (
|
|
||||||
<RecordTableCellButton
|
|
||||||
onClick={handleButtonClick}
|
|
||||||
Icon={buttonIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</StyledCellBaseContainer>
|
</StyledCellBaseContainer>
|
||||||
</CellHotkeyScopeContext.Provider>
|
</CellHotkeyScopeContext.Provider>
|
||||||
|
@ -1,31 +1,18 @@
|
|||||||
import { useContext } from 'react';
|
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
|
||||||
|
|
||||||
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 { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
|
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
|
||||||
|
|
||||||
export const RecordTableCellDisplayMode = ({
|
export const RecordTableCellDisplayMode = ({
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<unknown>) => {
|
}: React.PropsWithChildren<unknown>) => {
|
||||||
const cellPosition = useCurrentTableCellPosition();
|
const isEmpty = useIsFieldEmpty();
|
||||||
const { onMoveSoftFocusToCell } = useContext(RecordTableContext);
|
|
||||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
|
||||||
|
|
||||||
const isFieldInputOnly = useIsFieldInputOnly();
|
if (isEmpty) {
|
||||||
|
return <></>;
|
||||||
const handleClick = () => {
|
}
|
||||||
onMoveSoftFocusToCell(cellPosition);
|
|
||||||
|
|
||||||
if (!isFieldInputOnly) {
|
|
||||||
openTableCell();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordTableCellDisplayContainer onClick={handleClick}>
|
<RecordTableCellDisplayContainer>
|
||||||
{children}
|
{children}
|
||||||
</RecordTableCellDisplayContainer>
|
</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 { useRecoilValue } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
import { IconArrowUpRight } from 'twenty-ui';
|
||||||
|
|
||||||
import { useClearField } from '@/object-record/record-field/hooks/useClearField';
|
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 { useIsFieldClearable } from '@/object-record/record-field/hooks/useIsFieldClearable';
|
||||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||||
import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput';
|
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 { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
|
||||||
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
|
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
@ -15,13 +21,23 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
|||||||
|
|
||||||
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
|
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
|
||||||
|
|
||||||
type RecordTableCellSoftFocusModeProps = PropsWithChildren<unknown>;
|
type RecordTableCellSoftFocusModeProps = {
|
||||||
|
editModeContent: ReactElement;
|
||||||
|
nonEditModeContent: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
export const RecordTableCellSoftFocusMode = ({
|
export const RecordTableCellSoftFocusMode = ({
|
||||||
children,
|
editModeContent,
|
||||||
|
nonEditModeContent,
|
||||||
}: RecordTableCellSoftFocusModeProps) => {
|
}: RecordTableCellSoftFocusModeProps) => {
|
||||||
|
const { columnIndex } = useContext(RecordTableCellContext);
|
||||||
|
|
||||||
|
const { isReadOnly } = useContext(RecordTableRowContext);
|
||||||
|
|
||||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||||
|
|
||||||
|
const editModeContentOnly = useIsFieldInputOnly();
|
||||||
|
|
||||||
const isFieldInputOnly = useIsFieldInputOnly();
|
const isFieldInputOnly = useIsFieldInputOnly();
|
||||||
|
|
||||||
const isFieldClearable = useIsFieldClearable();
|
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 (
|
return (
|
||||||
<RecordTableCellDisplayContainer
|
<>
|
||||||
onClick={handleClick}
|
<RecordTableCellDisplayContainer
|
||||||
scrollRef={scrollRef}
|
onClick={handleClick}
|
||||||
>
|
scrollRef={scrollRef}
|
||||||
{children}
|
>
|
||||||
</RecordTableCellDisplayContainer>
|
{editModeContentOnly ? editModeContent : nonEditModeContent}
|
||||||
|
</RecordTableCellDisplayContainer>
|
||||||
|
{showButton && (
|
||||||
|
<RecordTableCellButton onClick={handleClick} Icon={buttonIcon} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { MouseEventHandler, useMemo } from 'react';
|
import { MouseEventHandler, useMemo } from 'react';
|
||||||
|
|
||||||
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import {
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
ExpandableList,
|
|
||||||
ExpandableListProps,
|
|
||||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
|
||||||
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
|
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
|
||||||
import {
|
import {
|
||||||
LinkType,
|
LinkType,
|
||||||
@ -15,18 +12,12 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
||||||
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
||||||
|
|
||||||
type LinksDisplayProps = Pick<
|
type LinksDisplayProps = {
|
||||||
ExpandableListProps,
|
|
||||||
'isChipCountDisplayed' | 'withExpandedListBorder'
|
|
||||||
> & {
|
|
||||||
value?: FieldLinksValue;
|
value?: FieldLinksValue;
|
||||||
|
isFocused?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LinksDisplay = ({
|
export const LinksDisplay = ({ value, isFocused }: LinksDisplayProps) => {
|
||||||
isChipCountDisplayed,
|
|
||||||
withExpandedListBorder,
|
|
||||||
value,
|
|
||||||
}: LinksDisplayProps) => {
|
|
||||||
const links = useMemo(
|
const links = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
[
|
||||||
@ -53,10 +44,7 @@ export const LinksDisplay = ({
|
|||||||
const handleClick: MouseEventHandler = (event) => event.stopPropagation();
|
const handleClick: MouseEventHandler = (event) => event.stopPropagation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableList
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
isChipCountDisplayed={isChipCountDisplayed}
|
|
||||||
withExpandedListBorder={withExpandedListBorder}
|
|
||||||
>
|
|
||||||
{links.map(({ url, label, type }, index) =>
|
{links.map(({ url, label, type }, index) =>
|
||||||
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
|
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
|
||||||
<SocialLink key={index} href={url} onClick={handleClick} type={type}>
|
<SocialLink key={index} href={url} 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 { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
||||||
import { findOneRecordForShowPageOperationSignatureFactory } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory';
|
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { PageBody } from '@/ui/layout/page/PageBody';
|
import { PageBody } from '@/ui/layout/page/PageBody';
|
||||||
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||||
@ -64,7 +66,10 @@ export const RecordShowPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!record) return;
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setEntityFields(record);
|
setEntityFields(record);
|
||||||
}, [record, setEntityFields]);
|
}, [record, setEntityFields]);
|
||||||
|
|
||||||
@ -102,40 +107,43 @@ export const RecordShowPage = () => {
|
|||||||
: capitalize(objectNameSingular);
|
: capitalize(objectNameSingular);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<RecordFieldValueSelectorContextProvider>
|
||||||
<PageTitle title={pageTitle} />
|
<RecordValueSetterEffect recordId={objectRecordId} />
|
||||||
<PageHeader
|
<PageContainer>
|
||||||
title={pageName ?? ''}
|
<PageTitle title={pageTitle} />
|
||||||
hasBackButton
|
<PageHeader
|
||||||
Icon={headerIcon}
|
title={pageName ?? ''}
|
||||||
loading={loading}
|
hasBackButton
|
||||||
>
|
Icon={headerIcon}
|
||||||
<>
|
|
||||||
<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}
|
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 { ProfilerWrapper } from '~/testing/profiling/components/ProfilerWrapper';
|
||||||
import { ProfilingQueueEffect } from '~/testing/profiling/components/ProfilingQueueEffect';
|
import { ProfilingQueueEffect } from '~/testing/profiling/components/ProfilingQueueEffect';
|
||||||
import { ProfilingReporter } from '~/testing/profiling/components/ProfilingReporter';
|
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 { profilingSessionRunsState } from '~/testing/profiling/states/profilingSessionRunsState';
|
||||||
import { profilingSessionStatusState } from '~/testing/profiling/states/profilingSessionStatusState';
|
import { profilingSessionStatusState } from '~/testing/profiling/states/profilingSessionStatusState';
|
||||||
import { getTestArray } from '~/testing/profiling/utils/getTestArray';
|
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 }) => {
|
export const ProfilerDecorator: Decorator = (Story, { id, parameters }) => {
|
||||||
const numberOfTests = parameters.numberOfTests ?? 2;
|
const numberOfTests = parameters.numberOfTests ?? 2;
|
||||||
const numberOfRuns = parameters.numberOfRuns ?? 2;
|
const numberOfRuns = parameters.numberOfRuns ?? 2;
|
||||||
|
const warmUpRounds = parameters.warmUpRounds ?? 5;
|
||||||
|
|
||||||
const [currentProfilingRunIndex] = useRecoilState(
|
const [currentProfilingRunIndex] = useRecoilState(
|
||||||
currentProfilingRunIndexState,
|
currentProfilingRunIndexState,
|
||||||
@ -31,6 +32,7 @@ export const ProfilerDecorator: Decorator = (Story, { id, parameters }) => {
|
|||||||
<ProfilingQueueEffect
|
<ProfilingQueueEffect
|
||||||
numberOfRuns={numberOfRuns}
|
numberOfRuns={numberOfRuns}
|
||||||
numberOfTestsPerRun={numberOfTests}
|
numberOfTestsPerRun={numberOfTests}
|
||||||
|
warmUpRounds={warmUpRounds}
|
||||||
profilingId={id}
|
profilingId={id}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
|||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { TIME_BETWEEN_TEST_RUNS_IN_MS } from '~/testing/profiling/constants/TimeBetweenTestRunsInMs';
|
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 { profilingQueueState } from '~/testing/profiling/states/profilingQueueState';
|
||||||
import { profilingSessionRunsState } from '~/testing/profiling/states/profilingSessionRunsState';
|
import { profilingSessionRunsState } from '~/testing/profiling/states/profilingSessionRunsState';
|
||||||
import { profilingSessionStatusState } from '~/testing/profiling/states/profilingSessionStatusState';
|
import { profilingSessionStatusState } from '~/testing/profiling/states/profilingSessionStatusState';
|
||||||
@ -12,10 +12,12 @@ export const ProfilingQueueEffect = ({
|
|||||||
profilingId,
|
profilingId,
|
||||||
numberOfTestsPerRun,
|
numberOfTestsPerRun,
|
||||||
numberOfRuns,
|
numberOfRuns,
|
||||||
|
warmUpRounds,
|
||||||
}: {
|
}: {
|
||||||
profilingId: string;
|
profilingId: string;
|
||||||
numberOfTestsPerRun: number;
|
numberOfTestsPerRun: number;
|
||||||
numberOfRuns: number;
|
numberOfRuns: number;
|
||||||
|
warmUpRounds: number;
|
||||||
}) => {
|
}) => {
|
||||||
const [currentProfilingRunIndex, setCurrentProfilingRunIndex] =
|
const [currentProfilingRunIndex, setCurrentProfilingRunIndex] =
|
||||||
useRecoilState(currentProfilingRunIndexState);
|
useRecoilState(currentProfilingRunIndexState);
|
||||||
@ -38,9 +40,9 @@ export const ProfilingQueueEffect = ({
|
|||||||
setCurrentProfilingRunIndex(0);
|
setCurrentProfilingRunIndex(0);
|
||||||
|
|
||||||
const newTestRuns = [
|
const newTestRuns = [
|
||||||
'warm-up-1',
|
...[
|
||||||
'warm-up-2',
|
...Array.from({ length: warmUpRounds }, (_, i) => `warm-up-${i}`),
|
||||||
'warm-up-3',
|
],
|
||||||
...[
|
...[
|
||||||
...Array.from({ length: numberOfRuns }, (_, i) => `real-run-${i}`),
|
...Array.from({ length: numberOfRuns }, (_, i) => `real-run-${i}`),
|
||||||
],
|
],
|
||||||
@ -76,9 +78,13 @@ export const ProfilingQueueEffect = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise((resolve) =>
|
const timeInMs = profilingSessionRuns[
|
||||||
setTimeout(resolve, TIME_BETWEEN_TEST_RUNS_IN_MS),
|
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;
|
const nextIndex = currentProfilingRunIndex + 1;
|
||||||
|
|
||||||
@ -109,6 +115,7 @@ export const ProfilingQueueEffect = ({
|
|||||||
profilingSessionRuns,
|
profilingSessionRuns,
|
||||||
setProfilingSessionRuns,
|
setProfilingSessionRuns,
|
||||||
numberOfRuns,
|
numberOfRuns,
|
||||||
|
warmUpRounds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
@ -3,7 +3,9 @@ import styled from '@emotion/styled';
|
|||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { PROFILING_REPORTER_DIV_ID } from '~/testing/profiling/constants/ProfilingReporterDivId';
|
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 { profilingSessionDataPointsState } from '~/testing/profiling/states/profilingSessionDataPointsState';
|
||||||
|
import { profilingSessionStatusState } from '~/testing/profiling/states/profilingSessionStatusState';
|
||||||
import { computeProfilingReport } from '~/testing/profiling/utils/computeProfilingReport';
|
import { computeProfilingReport } from '~/testing/profiling/utils/computeProfilingReport';
|
||||||
|
|
||||||
const StyledTable = styled.table`
|
const StyledTable = styled.table`
|
||||||
@ -24,6 +26,12 @@ export const ProfilingReporter = () => {
|
|||||||
profilingSessionDataPointsState,
|
profilingSessionDataPointsState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [currentProfilingRunIndex] = useRecoilState(
|
||||||
|
currentProfilingRunIndexState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [profilingSessionStatus] = useRecoilState(profilingSessionStatusState);
|
||||||
|
|
||||||
const profilingReport = useMemo(
|
const profilingReport = useMemo(
|
||||||
() => computeProfilingReport(profilingSessionDataPoints),
|
() => computeProfilingReport(profilingSessionDataPoints),
|
||||||
[profilingSessionDataPoints],
|
[profilingSessionDataPoints],
|
||||||
@ -34,6 +42,10 @@ export const ProfilingReporter = () => {
|
|||||||
data-profiling-report={JSON.stringify(profilingReport)}
|
data-profiling-report={JSON.stringify(profilingReport)}
|
||||||
id={PROFILING_REPORTER_DIV_ID}
|
id={PROFILING_REPORTER_DIV_ID}
|
||||||
>
|
>
|
||||||
|
<h2>Profiling report</h2>
|
||||||
|
<div>
|
||||||
|
Run #{currentProfilingRunIndex} - Status {profilingSessionStatus}
|
||||||
|
</div>
|
||||||
<StyledTable>
|
<StyledTable>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -46,6 +58,7 @@ export const ProfilingReporter = () => {
|
|||||||
<th>P95</th>
|
<th>P95</th>
|
||||||
<th>P99</th>
|
<th>P99</th>
|
||||||
<th>Max</th>
|
<th>Max</th>
|
||||||
|
<th>Variance</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -59,6 +72,9 @@ export const ProfilingReporter = () => {
|
|||||||
<td>{Math.round(profilingReport.total.p95 * 1000) / 1000}ms</td>
|
<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.p99 * 1000) / 1000}ms</td>
|
||||||
<td>{Math.round(profilingReport.total.max * 1000) / 1000}ms</td>
|
<td>{Math.round(profilingReport.total.max * 1000) / 1000}ms</td>
|
||||||
|
<td>
|
||||||
|
{Math.round(profilingReport.total.variance * 1000000) / 1000000}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{Object.entries(profilingReport.runs).map(([runName, report]) => (
|
{Object.entries(profilingReport.runs).map(([runName, report]) => (
|
||||||
<tr key={runName}>
|
<tr key={runName}>
|
||||||
@ -71,6 +87,7 @@ export const ProfilingReporter = () => {
|
|||||||
<td>{Math.round(report.p95 * 1000) / 1000}ms</td>
|
<td>{Math.round(report.p95 * 1000) / 1000}ms</td>
|
||||||
<td>{Math.round(report.p99 * 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.max * 1000) / 1000}ms</td>
|
||||||
|
<td>{Math.round(report.variance * 1000000) / 1000000}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</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;
|
p99: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
|
variance: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProfilingReport = {
|
export type ProfilingReport = {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { ProfilingDataPoint } from '~/testing/profiling/types/ProfilingDataPoint';
|
import { ProfilingDataPoint } from '~/testing/profiling/types/ProfilingDataPoint';
|
||||||
import { ProfilingReport } from '~/testing/profiling/types/ProfilingReportByRun';
|
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;
|
const profilingReport = { total: {}, runs: {} } as ProfilingReport;
|
||||||
|
|
||||||
for (const dataPoint of dataPoints) {
|
for (const dataPoint of dataPoints) {
|
||||||
@ -27,8 +30,9 @@ export const computeProfilingReport = (dataPoints: ProfilingDataPoint[]) => {
|
|||||||
|
|
||||||
const numberOfIds = ids.length;
|
const numberOfIds = ids.length;
|
||||||
|
|
||||||
profilingReport.runs[runName].average =
|
const mean = profilingReport.runs[runName].sum / numberOfIds;
|
||||||
profilingReport.runs[runName].sum / numberOfIds;
|
|
||||||
|
profilingReport.runs[runName].average = mean;
|
||||||
|
|
||||||
profilingReport.runs[runName].min = Math.min(
|
profilingReport.runs[runName].min = Math.min(
|
||||||
...Object.values(profilingReport.runs[runName].sumById),
|
...Object.values(profilingReport.runs[runName].sumById),
|
||||||
@ -38,6 +42,14 @@ export const computeProfilingReport = (dataPoints: ProfilingDataPoint[]) => {
|
|||||||
...Object.values(profilingReport.runs[runName].sumById),
|
...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 p50Index = Math.floor(numberOfIds * 0.5);
|
||||||
const p80Index = Math.floor(numberOfIds * 0.8);
|
const p80Index = Math.floor(numberOfIds * 0.8);
|
||||||
const p90Index = Math.floor(numberOfIds * 0.9);
|
const p90Index = Math.floor(numberOfIds * 0.9);
|
||||||
@ -55,9 +67,9 @@ export const computeProfilingReport = (dataPoints: ProfilingDataPoint[]) => {
|
|||||||
runName.startsWith('real-run'),
|
runName.startsWith('real-run'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const runsForTotal = runNamesForTotal.map(
|
const runsForTotal = runNamesForTotal
|
||||||
(runName) => profilingReport.runs[runName],
|
.map((runName) => profilingReport.runs[runName])
|
||||||
);
|
.filter((run) => run.variance < (varianceThreshold ?? 0.2));
|
||||||
|
|
||||||
profilingReport.total = {
|
profilingReport.total = {
|
||||||
sum: Object.values(runsForTotal).reduce((acc, run) => acc + run.sum, 0),
|
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.values(runsForTotal).reduce((acc, run) => acc + run.p99, 0) /
|
||||||
Object.keys(runsForTotal).length,
|
Object.keys(runsForTotal).length,
|
||||||
dataPointCount: dataPoints.length,
|
dataPointCount: dataPoints.length,
|
||||||
|
variance:
|
||||||
|
runsForTotal.reduce((acc, run) => acc + run.variance, 0) /
|
||||||
|
runsForTotal.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
return profilingReport;
|
return profilingReport;
|
||||||
|
@ -11,19 +11,21 @@ export const getProfilingStory = ({
|
|||||||
averageThresholdInMs,
|
averageThresholdInMs,
|
||||||
numberOfRuns,
|
numberOfRuns,
|
||||||
numberOfTestsPerRun,
|
numberOfTestsPerRun,
|
||||||
|
warmUpRounds,
|
||||||
}: {
|
}: {
|
||||||
componentName: string;
|
componentName: string;
|
||||||
p95ThresholdInMs?: number;
|
p95ThresholdInMs?: number;
|
||||||
averageThresholdInMs: number;
|
averageThresholdInMs: number;
|
||||||
numberOfRuns: number;
|
numberOfRuns: number;
|
||||||
numberOfTestsPerRun: number;
|
numberOfTestsPerRun: number;
|
||||||
|
warmUpRounds?: number;
|
||||||
}): StoryObj<any> => ({
|
}): StoryObj<any> => ({
|
||||||
decorators: [ProfilerDecorator],
|
decorators: [ProfilerDecorator],
|
||||||
parameters: {
|
parameters: {
|
||||||
numberOfRuns,
|
numberOfRuns,
|
||||||
numberOfTests: numberOfTestsPerRun,
|
numberOfTests: numberOfTestsPerRun,
|
||||||
componentName,
|
componentName,
|
||||||
chromatic: { disableSnapshot: true },
|
warmUpRounds,
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
await findByTestId(
|
await findByTestId(
|
||||||
|
11
yarn.lock
11
yarn.lock
@ -46630,6 +46630,7 @@ __metadata:
|
|||||||
type-fest: "npm:4.10.1"
|
type-fest: "npm:4.10.1"
|
||||||
typeorm: "npm:^0.3.17"
|
typeorm: "npm:^0.3.17"
|
||||||
typescript: "npm:5.3.3"
|
typescript: "npm:5.3.3"
|
||||||
|
use-context-selector: "npm:^2.0.0"
|
||||||
use-debounce: "npm:^10.0.0"
|
use-debounce: "npm:^10.0.0"
|
||||||
uuid: "npm:^9.0.0"
|
uuid: "npm:^9.0.0"
|
||||||
vite: "npm:^5.0.0"
|
vite: "npm:^5.0.0"
|
||||||
@ -47659,6 +47660,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"use-debounce@npm:^10.0.0":
|
||||||
version: 10.0.0
|
version: 10.0.0
|
||||||
resolution: "use-debounce@npm:10.0.0"
|
resolution: "use-debounce@npm:10.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user