Display and update fields from fromManyObjects relations in Show card (#5801)
In this PR, we implement the display and update of fields from fromManyObjects (e.g update Employees for a Company). Product requirement - update should be triggered at each box check/uncheck, not at lose of focus Left to do in upcoming PRs - add the column in the table views (e.g. column "Employees" on "Companies" table view) - add "Add new" possibility when there is no records (as is currently exists for "one" side of relations:) <img width="374" alt="Capture d’écran 2024-06-10 à 17 38 02" src="https://github.com/twentyhq/twenty/assets/51697796/6f0cc494-e44f-4620-a762-d7b438951eec"> - update cache after an update affecting other records (e.g "Listings" have one "Person"; if listing A belonged to Person A but then we attribute listing A to Person B, Person A is no longer owner of Listing A. For the moment that would not be reflected immediatly leading, to potential false information if information is accessed from cache) - try to get rid of the glitch - we also have it on the task page example. (probably) due to the fact that we are using a recoil state to read, update then re-read https://github.com/twentyhq/twenty/assets/51697796/54f71674-237a-4946-866e-b8d96353c458
This commit is contained in:
@ -171,15 +171,10 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
closeEditableField();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSelectContainer>
|
<StyledSelectContainer>
|
||||||
<MultipleObjectRecordSelect
|
<MultipleObjectRecordSelect
|
||||||
selectedObjectRecordIds={selectedTargetObjectIds}
|
selectedObjectRecordIds={selectedTargetObjectIds}
|
||||||
onCancel={handleCancel}
|
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
</StyledSelectContainer>
|
</StyledSelectContainer>
|
||||||
|
|||||||
@ -37,6 +37,8 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
|||||||
relationObjectMetadataNamePlural:
|
relationObjectMetadataNamePlural:
|
||||||
relationObjectMetadataItem?.namePlural ?? '',
|
relationObjectMetadataItem?.namePlural ?? '',
|
||||||
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
|
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
|
||||||
|
targetFieldMetadataName:
|
||||||
|
field.relationDefinition?.targetFieldMetadata?.name ?? '',
|
||||||
options: field.options,
|
options: field.options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { FullNameFieldInput } from '@/object-record/record-field/meta-types/inpu
|
|||||||
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
|
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
|
||||||
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
|
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
|
||||||
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
|
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
|
||||||
|
import { RelationManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationManyFieldInput';
|
||||||
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
|
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
|
||||||
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
||||||
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
||||||
@ -14,6 +15,7 @@ import { isFieldFullName } from '@/object-record/record-field/types/guards/isFie
|
|||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
|
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
|
||||||
@ -71,7 +73,15 @@ export const FieldInput = ({
|
|||||||
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
||||||
>
|
>
|
||||||
{isFieldRelation(fieldDefinition) ? (
|
{isFieldRelation(fieldDefinition) ? (
|
||||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||||
|
<RelationManyFieldInput
|
||||||
|
relationPickerScopeId={getScopeIdFromComponentId(
|
||||||
|
`relation-picker-${fieldDefinition.fieldMetadataId}`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||||
|
)
|
||||||
) : isFieldPhone(fieldDefinition) ||
|
) : isFieldPhone(fieldDefinition) ||
|
||||||
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
||||||
<PhoneFieldInput
|
<PhoneFieldInput
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
|
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||||
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
||||||
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
||||||
@ -13,9 +15,11 @@ import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/is
|
|||||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
||||||
|
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||||
import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue';
|
import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue';
|
||||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||||
@ -55,6 +59,11 @@ export const usePersistField = () => {
|
|||||||
isFieldRelation(fieldDefinition) &&
|
isFieldRelation(fieldDefinition) &&
|
||||||
isFieldRelationValue(valueToPersist);
|
isFieldRelationValue(valueToPersist);
|
||||||
|
|
||||||
|
const fieldIsRelationFromManyObjects =
|
||||||
|
isFieldRelationFromManyObjects(
|
||||||
|
fieldDefinition as FieldDefinition<FieldRelationMetadata>,
|
||||||
|
) && isFieldRelationValue(valueToPersist);
|
||||||
|
|
||||||
const fieldIsText =
|
const fieldIsText =
|
||||||
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
|
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
|
||||||
|
|
||||||
@ -111,7 +120,7 @@ export const usePersistField = () => {
|
|||||||
isFieldRawJsonValue(valueToPersist);
|
isFieldRawJsonValue(valueToPersist);
|
||||||
|
|
||||||
const isValuePersistable =
|
const isValuePersistable =
|
||||||
fieldIsRelation ||
|
(fieldIsRelation && !fieldIsRelationFromManyObjects) ||
|
||||||
fieldIsText ||
|
fieldIsText ||
|
||||||
fieldIsBoolean ||
|
fieldIsBoolean ||
|
||||||
fieldIsEmail ||
|
fieldIsEmail ||
|
||||||
@ -136,13 +145,14 @@ export const usePersistField = () => {
|
|||||||
valueToPersist,
|
valueToPersist,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fieldIsRelation) {
|
if (fieldIsRelation && !fieldIsRelationFromManyObjects) {
|
||||||
|
const value = valueToPersist as EntityForSelect;
|
||||||
updateRecord?.({
|
updateRecord?.({
|
||||||
variables: {
|
variables: {
|
||||||
where: { id: entityId },
|
where: { id: entityId },
|
||||||
updateOneRecordInput: {
|
updateOneRecordInput: {
|
||||||
[fieldName]: valueToPersist,
|
[fieldName]: value,
|
||||||
[`${fieldName}Id`]: valueToPersist?.id ?? null,
|
[`${fieldName}Id`]: value?.id ?? null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
|
import { isArray } from '@sniptt/guards';
|
||||||
import { EntityChip } from 'twenty-ui';
|
import { EntityChip } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
|
||||||
import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
|
import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
|
||||||
|
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||||
|
|
||||||
export const RelationFieldDisplay = () => {
|
export const RelationFieldDisplay = () => {
|
||||||
@ -14,6 +18,12 @@ export const RelationFieldDisplay = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isArray(fieldValue) && isFieldRelationFromManyObjects(fieldDefinition)) {
|
||||||
|
return (
|
||||||
|
<RelationFromManyFieldDisplay fieldValue={fieldValue as ObjectRecord[]} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const recordChipData = generateRecordChipData(fieldValue);
|
const recordChipData = generateRecordChipData(fieldValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { EntityChip } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
|
import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
|
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||||
|
|
||||||
|
export const RelationFromManyFieldDisplay = ({
|
||||||
|
fieldValue,
|
||||||
|
}: {
|
||||||
|
fieldValue: ObjectRecord[];
|
||||||
|
}) => {
|
||||||
|
const { isFocused } = useFieldFocus();
|
||||||
|
const { generateRecordChipData } = useRelationFieldDisplay();
|
||||||
|
|
||||||
|
const recordChipsData = fieldValue.map((fieldValueItem) =>
|
||||||
|
generateRecordChipData(fieldValueItem),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
|
{recordChipsData.map((record) => {
|
||||||
|
return (
|
||||||
|
<EntityChip
|
||||||
|
key={record.id}
|
||||||
|
entityId={record.id}
|
||||||
|
name={record.name as any}
|
||||||
|
avatarType={record.avatarType}
|
||||||
|
avatarUrl={getImageAbsoluteURIOrBase64(record.avatarUrl) || ''}
|
||||||
|
linkToEntity={record.linkToShowPage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ExpandableList>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
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 { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
|
||||||
|
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
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 {
|
||||||
|
fieldValue,
|
||||||
|
relationFromManyFieldDisplayMock,
|
||||||
|
} from './relationFromManyFieldDisplayMock';
|
||||||
|
|
||||||
|
const RelationFieldValueSetterEffect = () => {
|
||||||
|
const setEntity = useSetRecoilState(
|
||||||
|
recordStoreFamilyState(relationFromManyFieldDisplayMock.entityId),
|
||||||
|
);
|
||||||
|
|
||||||
|
const setRelationEntity = useSetRecoilState(
|
||||||
|
recordStoreFamilyState(relationFromManyFieldDisplayMock.relationEntityId),
|
||||||
|
);
|
||||||
|
|
||||||
|
const setRecordValue = useSetRecordValue();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEntity(relationFromManyFieldDisplayMock.entityValue);
|
||||||
|
setRelationEntity(relationFromManyFieldDisplayMock.relationFieldValue);
|
||||||
|
|
||||||
|
setRecordValue(
|
||||||
|
relationFromManyFieldDisplayMock.entityValue.id,
|
||||||
|
relationFromManyFieldDisplayMock.entityValue,
|
||||||
|
);
|
||||||
|
setRecordValue(
|
||||||
|
relationFromManyFieldDisplayMock.relationFieldValue.id,
|
||||||
|
relationFromManyFieldDisplayMock.relationFieldValue,
|
||||||
|
);
|
||||||
|
}, [setEntity, setRelationEntity, setRecordValue]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'UI/Data/Field/Display/RelationFromManyFieldDisplay',
|
||||||
|
decorators: [
|
||||||
|
MemoryRouterDecorator,
|
||||||
|
(Story) => (
|
||||||
|
<RecordFieldValueSelectorContextProvider>
|
||||||
|
<FieldContext.Provider
|
||||||
|
value={{
|
||||||
|
entityId: relationFromManyFieldDisplayMock.entityId,
|
||||||
|
basePathToShowPage: '/object-record/',
|
||||||
|
isLabelIdentifier: false,
|
||||||
|
fieldDefinition: {
|
||||||
|
...relationFromManyFieldDisplayMock.fieldDefinition,
|
||||||
|
} as unknown as FieldDefinition<FieldMetadata>,
|
||||||
|
hotkeyScope: 'hotkey-scope',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RelationFieldValueSetterEffect />
|
||||||
|
<Story />
|
||||||
|
</FieldContext.Provider>
|
||||||
|
</RecordFieldValueSelectorContextProvider>
|
||||||
|
),
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
|
component: RelationFromManyFieldDisplay,
|
||||||
|
argTypes: { value: { control: 'date' } },
|
||||||
|
args: { fieldValue: fieldValue },
|
||||||
|
parameters: {
|
||||||
|
chromatic: { disableSnapshot: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof RelationFromManyFieldDisplay>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const Performance = getProfilingStory({
|
||||||
|
componentName: 'RelationFromManyFieldDisplay',
|
||||||
|
averageThresholdInMs: 0.5,
|
||||||
|
numberOfRuns: 20,
|
||||||
|
numberOfTestsPerRun: 100,
|
||||||
|
});
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const fieldValue = [
|
||||||
|
{
|
||||||
|
__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: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Company',
|
||||||
|
domainName: 'airbnb.com',
|
||||||
|
xLink: {
|
||||||
|
__typename: 'Link',
|
||||||
|
label: '',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
name: 'Airbnb',
|
||||||
|
annualRecurringRevenue: {
|
||||||
|
__typename: 'Currency',
|
||||||
|
amountMicros: null,
|
||||||
|
currencyCode: '',
|
||||||
|
},
|
||||||
|
employees: null,
|
||||||
|
accountOwnerId: null,
|
||||||
|
address: '',
|
||||||
|
idealCustomerProfile: false,
|
||||||
|
createdAt: '2024-05-01T13:16:29.046Z',
|
||||||
|
id: '20202020-171e-4bcc-9cf7-43448d6fb278',
|
||||||
|
position: 6,
|
||||||
|
updatedAt: '2024-05-01T13:16:29.046Z',
|
||||||
|
linkedinLink: {
|
||||||
|
__typename: 'Link',
|
||||||
|
label: '',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const relationFromManyFieldDisplayMock = {
|
||||||
|
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: 'FROM_MANY_OBJECTS',
|
||||||
|
relationFieldMetadataId: '01fa2247-7937-4493-b7e2-3d72f05d6d25',
|
||||||
|
relationObjectMetadataNameSingular: 'company',
|
||||||
|
relationObjectMetadataNamePlural: 'companies',
|
||||||
|
objectMetadataNameSingular: 'person',
|
||||||
|
options: null,
|
||||||
|
},
|
||||||
|
type: FieldMetadataType.Relation,
|
||||||
|
iconName: 'IconUsers',
|
||||||
|
defaultValue: null,
|
||||||
|
editButtonIcon: {
|
||||||
|
propTypes: {},
|
||||||
|
},
|
||||||
|
position: 3,
|
||||||
|
size: 100,
|
||||||
|
isLabelIdentifier: false,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -5,14 +5,16 @@ import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButto
|
|||||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||||
import { FieldRelationValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldRelationValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
import { FieldContext } from '../../contexts/FieldContext';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||||
|
|
||||||
// TODO: we will be able to type more precisely when we will have custom field and custom entities support
|
export const useRelationField = <
|
||||||
export const useRelationField = () => {
|
T extends EntityForSelect | EntityForSelect[],
|
||||||
|
>() => {
|
||||||
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||||
const button = useGetButtonIcon();
|
const button = useGetButtonIcon();
|
||||||
|
|
||||||
@ -24,11 +26,11 @@ export const useRelationField = () => {
|
|||||||
|
|
||||||
const fieldName = fieldDefinition.metadata.fieldName;
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
const [fieldValue, setFieldValue] = useRecoilState<FieldRelationValue>(
|
const [fieldValue, setFieldValue] = useRecoilState<FieldRelationValue<T>>(
|
||||||
recordStoreFamilySelector({ recordId: entityId, fieldName }),
|
recordStoreFamilySelector({ recordId: entityId, fieldName }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getDraftValueSelector } = useRecordFieldInput<FieldRelationValue>(
|
const { getDraftValueSelector } = useRecordFieldInput<FieldRelationValue<T>>(
|
||||||
`${entityId}-${fieldName}`,
|
`${entityId}-${fieldName}`,
|
||||||
);
|
);
|
||||||
const draftValue = useRecoilValue(getDraftValueSelector());
|
const draftValue = useRecoilValue(getDraftValueSelector());
|
||||||
@ -41,5 +43,6 @@ export const useRelationField = () => {
|
|||||||
initialSearchValue,
|
initialSearchValue,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
maxWidth: button && maxWidth ? maxWidth - 28 : maxWidth,
|
maxWidth: button && maxWidth ? maxWidth - 28 : maxWidth,
|
||||||
|
entityId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const RelationFieldInput = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
}: RelationFieldInputProps) => {
|
}: RelationFieldInputProps) => {
|
||||||
const { fieldDefinition, initialSearchValue, fieldValue } =
|
const { fieldDefinition, initialSearchValue, fieldValue } =
|
||||||
useRelationField();
|
useRelationField<EntityForSelect>();
|
||||||
|
|
||||||
const persistField = usePersistField();
|
const persistField = usePersistField();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { useUpdateRelationManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput';
|
||||||
|
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||||
|
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
||||||
|
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||||
|
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
|
||||||
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
|
||||||
|
import { useRelationField } from '../../hooks/useRelationField';
|
||||||
|
|
||||||
|
export const RelationManyFieldInput = ({
|
||||||
|
relationPickerScopeId = 'relation-picker',
|
||||||
|
}: {
|
||||||
|
relationPickerScopeId?: string;
|
||||||
|
}) => {
|
||||||
|
const { closeInlineCell: closeEditableField } = useInlineCell();
|
||||||
|
|
||||||
|
const { fieldDefinition, fieldValue } = useRelationField<EntityForSelect[]>();
|
||||||
|
const { entities, relationPickerSearchFilter } =
|
||||||
|
useRelationPickerEntitiesOptions({
|
||||||
|
relationObjectNameSingular:
|
||||||
|
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||||
|
relationPickerScopeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { setRelationPickerSearchFilter } = useRelationPicker({
|
||||||
|
relationPickerScopeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleChange } = useUpdateRelationManyFieldInput({ entities });
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular:
|
||||||
|
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||||
|
});
|
||||||
|
const allRecords = useMemo(
|
||||||
|
() => [
|
||||||
|
...entities.entitiesToSelect.map((entity) => {
|
||||||
|
const { record, ...recordIdentifier } = entity;
|
||||||
|
return {
|
||||||
|
objectMetadataItem: objectMetadataItem,
|
||||||
|
record: record,
|
||||||
|
recordIdentifier: recordIdentifier,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[entities.entitiesToSelect, objectMetadataItem],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedRecords = useMemo(
|
||||||
|
() =>
|
||||||
|
allRecords.filter(
|
||||||
|
(entity) =>
|
||||||
|
fieldValue?.some((f) => {
|
||||||
|
return f.id === entity.recordIdentifier.id;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[allRecords, fieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ObjectMetadataItemsRelationPickerEffect
|
||||||
|
relationPickerScopeId={relationPickerScopeId}
|
||||||
|
/>
|
||||||
|
<MultiRecordSelect
|
||||||
|
allRecords={allRecords}
|
||||||
|
selectedObjectRecords={selectedRecords}
|
||||||
|
loading={entities.loading}
|
||||||
|
searchFilter={relationPickerSearchFilter}
|
||||||
|
setSearchFilter={setRelationPickerSearchFilter}
|
||||||
|
onSubmit={() => {
|
||||||
|
closeEditableField();
|
||||||
|
}}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { RelationManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationManyFieldInput';
|
||||||
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import {
|
||||||
|
mockDefaultWorkspace,
|
||||||
|
mockedWorkspaceMemberData,
|
||||||
|
} from '~/testing/mock-data/users';
|
||||||
|
|
||||||
|
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
||||||
|
|
||||||
|
const RelationWorkspaceSetterEffect = () => {
|
||||||
|
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||||
|
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||||
|
currentWorkspaceMemberState,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentWorkspace(mockDefaultWorkspace);
|
||||||
|
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
|
||||||
|
}, [setCurrentWorkspace, setCurrentWorkspaceMember]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RelationManyFieldInputWithContext = () => {
|
||||||
|
const setHotKeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHotKeyScope('hotkey-scope');
|
||||||
|
}, [setHotKeyScope]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FieldContextProvider
|
||||||
|
fieldDefinition={{
|
||||||
|
fieldMetadataId: 'relation',
|
||||||
|
label: 'Relation',
|
||||||
|
type: FieldMetadataType.Relation,
|
||||||
|
iconName: 'IconLink',
|
||||||
|
metadata: {
|
||||||
|
fieldName: 'Relation',
|
||||||
|
relationObjectMetadataNamePlural: 'workspaceMembers',
|
||||||
|
relationObjectMetadataNameSingular:
|
||||||
|
CoreObjectNameSingular.WorkspaceMember,
|
||||||
|
objectMetadataNameSingular: 'person',
|
||||||
|
relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
entityId={'entityId'}
|
||||||
|
>
|
||||||
|
<RelationWorkspaceSetterEffect />
|
||||||
|
<RelationManyFieldInput />
|
||||||
|
</FieldContextProvider>
|
||||||
|
<div data-testid="data-field-input-click-outside-div" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'UI/Data/Field/Input/RelationManyFieldInput',
|
||||||
|
component: RelationManyFieldInputWithContext,
|
||||||
|
args: {},
|
||||||
|
decorators: [ObjectMetadataItemsDecorator, SnackBarDecorator],
|
||||||
|
parameters: {
|
||||||
|
clearMocks: true,
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof RelationManyFieldInputWithContext>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
decorators: [ComponentWithRecoilScopeDecorator],
|
||||||
|
};
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
|
||||||
|
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||||
|
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
|
||||||
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const useUpdateRelationManyFieldInput = ({
|
||||||
|
entities,
|
||||||
|
}: {
|
||||||
|
entities: EntitiesForMultipleEntitySelect<EntityForSelect>;
|
||||||
|
}) => {
|
||||||
|
const { fieldDefinition, fieldValue, setFieldValue, entityId } =
|
||||||
|
useRelationField<EntityForSelect[]>();
|
||||||
|
|
||||||
|
const { updateOneRecord } = useUpdateOneRecord({
|
||||||
|
objectNameSingular:
|
||||||
|
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldName = fieldDefinition.metadata.targetFieldMetadataName;
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
objectRecord: ObjectRecordForSelect | null,
|
||||||
|
isSelected: boolean,
|
||||||
|
) => {
|
||||||
|
const entityToAddOrRemove = entities.entitiesToSelect.find(
|
||||||
|
(entity) => entity.id === objectRecord?.recordIdentifier.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedFieldValue = isSelected
|
||||||
|
? [...(fieldValue ?? []), entityToAddOrRemove]
|
||||||
|
: (fieldValue ?? []).filter(
|
||||||
|
(value) => value.id !== objectRecord?.recordIdentifier.id,
|
||||||
|
);
|
||||||
|
setFieldValue(
|
||||||
|
updatedFieldValue.filter((value) =>
|
||||||
|
isDefined(value),
|
||||||
|
) as EntityForSelect[],
|
||||||
|
);
|
||||||
|
if (isDefined(objectRecord)) {
|
||||||
|
updateOneRecord({
|
||||||
|
idToUpdate: objectRecord.record?.id,
|
||||||
|
updateOneRecordInput: {
|
||||||
|
[`${fieldName}Id`]: isSelected ? entityId : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleChange };
|
||||||
|
};
|
||||||
@ -18,6 +18,7 @@ import {
|
|||||||
FieldTextValue,
|
FieldTextValue,
|
||||||
FieldUUidValue,
|
FieldUUidValue,
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
|
||||||
export type FieldTextDraftValue = string;
|
export type FieldTextDraftValue = string;
|
||||||
export type FieldNumberDraftValue = string;
|
export type FieldNumberDraftValue = string;
|
||||||
@ -78,7 +79,9 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
|||||||
? FieldSelectDraftValue
|
? FieldSelectDraftValue
|
||||||
: FieldValue extends FieldMultiSelectValue
|
: FieldValue extends FieldMultiSelectValue
|
||||||
? FieldMultiSelectDraftValue
|
? FieldMultiSelectDraftValue
|
||||||
: FieldValue extends FieldRelationValue
|
: FieldValue extends
|
||||||
|
| FieldRelationValue<EntityForSelect>
|
||||||
|
| FieldRelationValue<EntityForSelect[]>
|
||||||
? FieldRelationDraftValue
|
? FieldRelationDraftValue
|
||||||
: FieldValue extends FieldAddressValue
|
: FieldValue extends FieldAddressValue
|
||||||
? FieldAddressDraftValue
|
? FieldAddressDraftValue
|
||||||
|
|||||||
@ -106,6 +106,7 @@ export type FieldRelationMetadata = {
|
|||||||
relationObjectMetadataNamePlural: string;
|
relationObjectMetadataNamePlural: string;
|
||||||
relationObjectMetadataNameSingular: string;
|
relationObjectMetadataNameSingular: string;
|
||||||
relationType?: FieldDefinitionRelationType;
|
relationType?: FieldDefinitionRelationType;
|
||||||
|
targetFieldMetadataName?: string;
|
||||||
useEditButton?: boolean;
|
useEditButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -173,7 +174,8 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number];
|
|||||||
export type FieldSelectValue = string | null;
|
export type FieldSelectValue = string | null;
|
||||||
export type FieldMultiSelectValue = string[] | null;
|
export type FieldMultiSelectValue = string[] | null;
|
||||||
|
|
||||||
export type FieldRelationValue = EntityForSelect | null;
|
export type FieldRelationValue<T extends EntityForSelect | EntityForSelect[]> =
|
||||||
|
T | null;
|
||||||
|
|
||||||
// See https://zod.dev/?id=json-type
|
// See https://zod.dev/?id=json-type
|
||||||
type Literal = string | number | boolean | null;
|
type Literal = string | number | boolean | null;
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
|
import { FieldRelationMetadata } from '../FieldMetadata';
|
||||||
|
|
||||||
|
export const isFieldRelationFromManyObjects = (
|
||||||
|
field: Pick<FieldDefinition<FieldRelationMetadata>, 'type' | 'metadata'>,
|
||||||
|
): field is FieldDefinition<FieldRelationMetadata> =>
|
||||||
|
field.type === FieldMetadataType.Relation &&
|
||||||
|
field.metadata.relationType === 'FROM_MANY_OBJECTS';
|
||||||
@ -1,9 +1,13 @@
|
|||||||
import { isNull, isObject, isUndefined } from '@sniptt/guards';
|
import { isNull, isObject, isUndefined } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
|
||||||
import { FieldRelationValue } from '../FieldMetadata';
|
import { FieldRelationValue } from '../FieldMetadata';
|
||||||
|
|
||||||
// TODO: add zod
|
// TODO: add zod
|
||||||
export const isFieldRelationValue = (
|
export const isFieldRelationValue = <
|
||||||
|
T extends EntityForSelect | EntityForSelect[],
|
||||||
|
>(
|
||||||
fieldValue: unknown,
|
fieldValue: unknown,
|
||||||
): fieldValue is FieldRelationValue =>
|
): fieldValue is FieldRelationValue<T> =>
|
||||||
!isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue));
|
!isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue));
|
||||||
|
|||||||
@ -0,0 +1,163 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
|
||||||
|
import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
|
||||||
|
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
|
||||||
|
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||||
|
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||||
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
|
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
|
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||||
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const StyledSelectableItem = styled(SelectableItem)`
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
export const MultiRecordSelect = ({
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
selectedObjectRecords,
|
||||||
|
allRecords,
|
||||||
|
loading,
|
||||||
|
searchFilter,
|
||||||
|
setSearchFilter,
|
||||||
|
}: {
|
||||||
|
onChange?: (
|
||||||
|
changedRecordForSelect: ObjectRecordForSelect,
|
||||||
|
newSelectedValue: boolean,
|
||||||
|
) => void;
|
||||||
|
onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
|
||||||
|
selectedObjectRecords: ObjectRecordForSelect[];
|
||||||
|
allRecords: ObjectRecordForSelect[];
|
||||||
|
loading: boolean;
|
||||||
|
searchFilter: string;
|
||||||
|
setSearchFilter: (searchFilter: string) => void;
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [internalSelectedRecords, setInternalSelectedRecords] = useState<
|
||||||
|
ObjectRecordForSelect[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading) {
|
||||||
|
setInternalSelectedRecords(selectedObjectRecords);
|
||||||
|
}
|
||||||
|
}, [selectedObjectRecords, loading]);
|
||||||
|
|
||||||
|
const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
|
||||||
|
leading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
debouncedSetSearchFilter(event.currentTarget.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (
|
||||||
|
changedRecordForSelect: ObjectRecordForSelect,
|
||||||
|
newSelectedValue: boolean,
|
||||||
|
) => {
|
||||||
|
const newSelectedRecords = newSelectedValue
|
||||||
|
? [...internalSelectedRecords, changedRecordForSelect]
|
||||||
|
: internalSelectedRecords.filter(
|
||||||
|
(selectedRecord) =>
|
||||||
|
selectedRecord.record.id !== changedRecordForSelect.record.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
setInternalSelectedRecords(newSelectedRecords);
|
||||||
|
|
||||||
|
onChange?.(changedRecordForSelect, newSelectedValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const entitiesInDropdown = useMemo(
|
||||||
|
() =>
|
||||||
|
[...(allRecords ?? [])].filter((entity) =>
|
||||||
|
isNonEmptyString(entity.recordIdentifier.id),
|
||||||
|
),
|
||||||
|
[allRecords],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectableItemIds = entitiesInDropdown.map(
|
||||||
|
(entity) => entity.record.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MultipleObjectRecordOnClickOutsideEffect
|
||||||
|
containerRef={containerRef}
|
||||||
|
onClickOutside={() => {
|
||||||
|
onSubmit?.(internalSelectedRecords);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DropdownMenu ref={containerRef} data-select-disable>
|
||||||
|
<DropdownMenuSearchInput
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
|
{loading ? (
|
||||||
|
<MenuItem text="Loading..." />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SelectableList
|
||||||
|
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
|
||||||
|
selectableItemIdArray={selectableItemIds}
|
||||||
|
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||||
|
onEnter={(recordId) => {
|
||||||
|
const recordIsSelected = internalSelectedRecords?.some(
|
||||||
|
(selectedRecord) => selectedRecord.record.id === recordId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const correspondingRecordForSelect = entitiesInDropdown?.find(
|
||||||
|
(entity) => entity.record.id === recordId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDefined(correspondingRecordForSelect)) {
|
||||||
|
handleSelectChange(
|
||||||
|
correspondingRecordForSelect,
|
||||||
|
!recordIsSelected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{entitiesInDropdown?.map((objectRecordForSelect) => (
|
||||||
|
<MultipleObjectRecordSelectItem
|
||||||
|
key={objectRecordForSelect.record.id}
|
||||||
|
objectRecordForSelect={objectRecordForSelect}
|
||||||
|
onSelectedChange={(newSelectedValue) =>
|
||||||
|
handleSelectChange(
|
||||||
|
objectRecordForSelect,
|
||||||
|
newSelectedValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
selected={internalSelectedRecords?.some(
|
||||||
|
(selectedRecord) => {
|
||||||
|
return (
|
||||||
|
selectedRecord.record.id ===
|
||||||
|
objectRecordForSelect.record.id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SelectableList>
|
||||||
|
{entitiesInDropdown?.length === 0 && (
|
||||||
|
<MenuItem text="No result" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,37 +1,18 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
|
|
||||||
import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
|
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
||||||
import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
|
|
||||||
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
|
|
||||||
import {
|
import {
|
||||||
ObjectRecordForSelect,
|
ObjectRecordForSelect,
|
||||||
SelectedObjectRecordId,
|
SelectedObjectRecordId,
|
||||||
useMultiObjectSearch,
|
useMultiObjectSearch,
|
||||||
} from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
} from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
|
||||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
|
||||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const StyledSelectableItem = styled(SelectableItem)`
|
export const StyledSelectableItem = styled(SelectableItem)`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type EntitiesForMultipleObjectRecordSelect = {
|
|
||||||
filteredSelectedObjectRecords: ObjectRecordForSelect[];
|
|
||||||
objectRecordsToSelect: ObjectRecordForSelect[];
|
|
||||||
loading: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MultipleObjectRecordSelect = ({
|
export const MultipleObjectRecordSelect = ({
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@ -41,12 +22,9 @@ export const MultipleObjectRecordSelect = ({
|
|||||||
changedRecordForSelect: ObjectRecordForSelect,
|
changedRecordForSelect: ObjectRecordForSelect,
|
||||||
newSelectedValue: boolean,
|
newSelectedValue: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
onCancel?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
|
|
||||||
onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
|
onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
|
||||||
selectedObjectRecordIds: SelectedObjectRecordId[];
|
selectedObjectRecordIds: SelectedObjectRecordId[];
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [searchFilter, setSearchFilter] = useState<string>('');
|
const [searchFilter, setSearchFilter] = useState<string>('');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -73,122 +51,18 @@ export const MultipleObjectRecordSelect = ({
|
|||||||
[selectedObjectRecords, selectedObjectRecordIds],
|
[selectedObjectRecords, selectedObjectRecordIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [internalSelectedRecords, setInternalSelectedRecords] = useState<
|
return (
|
||||||
ObjectRecordForSelect[]
|
<MultiRecordSelect
|
||||||
>([]);
|
onChange={onChange}
|
||||||
|
onSubmit={onSubmit}
|
||||||
useEffect(() => {
|
selectedObjectRecords={selectedObjectRecordsForSelect}
|
||||||
if (!loading) {
|
allRecords={[
|
||||||
setInternalSelectedRecords(selectedObjectRecordsForSelect);
|
|
||||||
}
|
|
||||||
}, [selectedObjectRecordsForSelect, loading]);
|
|
||||||
|
|
||||||
const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
|
|
||||||
leading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
debouncedSetSearchFilter(event.currentTarget.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectChange = (
|
|
||||||
changedRecordForSelect: ObjectRecordForSelect,
|
|
||||||
newSelectedValue: boolean,
|
|
||||||
) => {
|
|
||||||
const newSelectedRecords = newSelectedValue
|
|
||||||
? [...internalSelectedRecords, changedRecordForSelect]
|
|
||||||
: internalSelectedRecords.filter(
|
|
||||||
(selectedRecord) =>
|
|
||||||
selectedRecord.record.id !== changedRecordForSelect.record.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
setInternalSelectedRecords(newSelectedRecords);
|
|
||||||
|
|
||||||
onChange?.(changedRecordForSelect, newSelectedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const entitiesInDropdown = useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
...(filteredSelectedObjectRecords ?? []),
|
...(filteredSelectedObjectRecords ?? []),
|
||||||
...(objectRecordsToSelect ?? []),
|
...(objectRecordsToSelect ?? []),
|
||||||
].filter((entity) => isNonEmptyString(entity.recordIdentifier.id)),
|
]}
|
||||||
[filteredSelectedObjectRecords, objectRecordsToSelect],
|
loading={loading}
|
||||||
);
|
searchFilter={searchFilter}
|
||||||
|
setSearchFilter={setSearchFilter}
|
||||||
const selectableItemIds = entitiesInDropdown.map(
|
/>
|
||||||
(entity) => entity.record.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MultipleObjectRecordOnClickOutsideEffect
|
|
||||||
containerRef={containerRef}
|
|
||||||
onClickOutside={() => {
|
|
||||||
onSubmit?.(internalSelectedRecords);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DropdownMenu ref={containerRef} data-select-disable>
|
|
||||||
<DropdownMenuSearchInput
|
|
||||||
value={searchFilter}
|
|
||||||
onChange={handleFilterChange}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
|
||||||
{loading ? (
|
|
||||||
<MenuItem text="Loading..." />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<SelectableList
|
|
||||||
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
|
|
||||||
selectableItemIdArray={selectableItemIds}
|
|
||||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
|
||||||
onEnter={(recordId) => {
|
|
||||||
const recordIsSelected = internalSelectedRecords?.some(
|
|
||||||
(selectedRecord) => selectedRecord.record.id === recordId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const correspondingRecordForSelect = entitiesInDropdown?.find(
|
|
||||||
(entity) => entity.record.id === recordId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDefined(correspondingRecordForSelect)) {
|
|
||||||
handleSelectChange(
|
|
||||||
correspondingRecordForSelect,
|
|
||||||
!recordIsSelected,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{entitiesInDropdown?.map((objectRecordForSelect) => (
|
|
||||||
<MultipleObjectRecordSelectItem
|
|
||||||
key={objectRecordForSelect.record.id}
|
|
||||||
objectRecordForSelect={objectRecordForSelect}
|
|
||||||
onSelectedChange={(newSelectedValue) =>
|
|
||||||
handleSelectChange(
|
|
||||||
objectRecordForSelect,
|
|
||||||
newSelectedValue,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
selected={internalSelectedRecords?.some(
|
|
||||||
(selectedRecord) => {
|
|
||||||
return (
|
|
||||||
selectedRecord.record.id ===
|
|
||||||
objectRecordForSelect.record.id
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SelectableList>
|
|
||||||
{entitiesInDropdown?.length === 0 && (
|
|
||||||
<MenuItem text="No result" />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</DropdownMenu>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
||||||
import {
|
import {
|
||||||
SingleEntitySelectMenuItems,
|
SingleEntitySelectMenuItems,
|
||||||
SingleEntitySelectMenuItemsProps,
|
SingleEntitySelectMenuItemsProps,
|
||||||
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
|
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
|
||||||
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
|
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
|
||||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
|
||||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -44,32 +41,16 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
|||||||
relationPickerScopeId,
|
relationPickerScopeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { searchQueryState, relationPickerSearchFilterState } =
|
const { entities, relationPickerSearchFilter } =
|
||||||
useRelationPickerScopedStates({
|
useRelationPickerEntitiesOptions({
|
||||||
relationPickerScopedId: relationPickerScopeId,
|
relationObjectNameSingular,
|
||||||
|
relationPickerScopeId,
|
||||||
|
selectedRelationRecordIds,
|
||||||
|
excludedRelationRecordIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchQuery = useRecoilValue(searchQueryState);
|
|
||||||
const relationPickerSearchFilter = useRecoilValue(
|
|
||||||
relationPickerSearchFilterState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const showCreateButton = isDefined(onCreate);
|
const showCreateButton = isDefined(onCreate);
|
||||||
|
|
||||||
const entities = useFilteredSearchEntityQuery({
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
fieldNames:
|
|
||||||
searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [],
|
|
||||||
filter: relationPickerSearchFilter,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
orderByField: 'createdAt',
|
|
||||||
selectedIds: selectedRelationRecordIds,
|
|
||||||
excludeEntityIds: excludedRelationRecordIds,
|
|
||||||
objectNameSingular: relationObjectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
let onCreateWithInput = undefined;
|
let onCreateWithInput = undefined;
|
||||||
|
|
||||||
if (isDefined(onCreate)) {
|
if (isDefined(onCreate)) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
|
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
|
||||||
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
|
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
|
||||||
@ -28,6 +28,10 @@ export const useRelationPicker = (props?: useRelationPickeProps) => {
|
|||||||
relationPickerSearchFilterState,
|
relationPickerSearchFilterState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const relationPickerSearchFilter = useRecoilValue(
|
||||||
|
relationPickerSearchFilterState,
|
||||||
|
);
|
||||||
|
|
||||||
const [relationPickerPreselectedId, setRelationPickerPreselectedId] =
|
const [relationPickerPreselectedId, setRelationPickerPreselectedId] =
|
||||||
useRecoilState(relationPickerPreselectedIdState);
|
useRecoilState(relationPickerPreselectedIdState);
|
||||||
|
|
||||||
@ -37,5 +41,6 @@ export const useRelationPicker = (props?: useRelationPickeProps) => {
|
|||||||
setRelationPickerSearchFilter,
|
setRelationPickerSearchFilter,
|
||||||
relationPickerPreselectedId,
|
relationPickerPreselectedId,
|
||||||
setRelationPickerPreselectedId,
|
setRelationPickerPreselectedId,
|
||||||
|
relationPickerSearchFilter,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
|
||||||
|
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||||
|
|
||||||
|
export const useRelationPickerEntitiesOptions = ({
|
||||||
|
relationObjectNameSingular,
|
||||||
|
relationPickerScopeId = 'relation-picker',
|
||||||
|
selectedRelationRecordIds = [],
|
||||||
|
excludedRelationRecordIds = [],
|
||||||
|
}: {
|
||||||
|
relationObjectNameSingular: string;
|
||||||
|
relationPickerScopeId?: string;
|
||||||
|
selectedRelationRecordIds?: string[];
|
||||||
|
excludedRelationRecordIds?: string[];
|
||||||
|
}) => {
|
||||||
|
const { searchQueryState, relationPickerSearchFilterState } =
|
||||||
|
useRelationPickerScopedStates({
|
||||||
|
relationPickerScopedId: relationPickerScopeId,
|
||||||
|
});
|
||||||
|
const relationPickerSearchFilter = useRecoilValue(
|
||||||
|
relationPickerSearchFilterState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchQuery = useRecoilValue(searchQueryState);
|
||||||
|
const entities = useFilteredSearchEntityQuery({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
fieldNames:
|
||||||
|
searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [],
|
||||||
|
filter: relationPickerSearchFilter,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orderByField: 'createdAt',
|
||||||
|
selectedIds: selectedRelationRecordIds,
|
||||||
|
excludeEntityIds: excludedRelationRecordIds,
|
||||||
|
objectNameSingular: relationObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { entities, relationPickerSearchFilter };
|
||||||
|
};
|
||||||
@ -3,6 +3,7 @@ import { isString } from '@sniptt/guards';
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
|
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
|
||||||
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -30,7 +31,7 @@ export const sanitizeRecordInput = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||||
isFieldRelationValue(fieldValue)
|
isFieldRelationValue<EntityForSelect>(fieldValue)
|
||||||
) {
|
) {
|
||||||
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
||||||
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
|
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
|
||||||
|
|||||||
Reference in New Issue
Block a user