refactor: apply relation optimistic effects on record update (#3556)
* refactor: apply relation optimistic effects on record update Related to #3509 * refactor: remove need to pass relation id field to create and update mutations * fix: fix tests * fix: fix SingleEntitySelect glitch * fix: fix usePersistField tests * fix: fix wrong import after rebase * fix: fix several tests * fix: fix test types
This commit is contained in:
@ -28,21 +28,27 @@ export const useCreateOneRecord = <
|
||||
});
|
||||
|
||||
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
|
||||
const optimisticallyCreatedRecord =
|
||||
generateCachedObjectRecord<CreatedObjectRecord>(input);
|
||||
|
||||
const sanitizedCreateOneRecordInput = sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: { ...input, id: optimisticallyCreatedRecord.id },
|
||||
recordInput: input,
|
||||
});
|
||||
|
||||
const optimisticallyCreatedRecord =
|
||||
generateCachedObjectRecord<CreatedObjectRecord>({
|
||||
...input,
|
||||
...sanitizedCreateOneRecordInput,
|
||||
});
|
||||
|
||||
const mutationResponseField =
|
||||
getCreateOneRecordMutationResponseField(objectNameSingular);
|
||||
|
||||
const createdObject = await apolloClient.mutate({
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: sanitizedCreateOneRecordInput,
|
||||
input: {
|
||||
...sanitizedCreateOneRecordInput,
|
||||
id: optimisticallyCreatedRecord.id,
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
[mutationResponseField]: optimisticallyCreatedRecord,
|
||||
|
||||
@ -37,7 +37,7 @@ export const useGetRecordFromCache = ({
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
return cache.readFragment<CachedObjectRecord>({
|
||||
return cache.readFragment<CachedObjectRecord & { __typename: string }>({
|
||||
id: cachedRecordId,
|
||||
fragment: cacheReadFragment,
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@ export const useModifyRecordFromCache = ({
|
||||
}) => {
|
||||
const { cache } = useApolloClient();
|
||||
|
||||
return <CachedObjectRecord extends ObjectRecord>(
|
||||
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
|
||||
recordId: string,
|
||||
fieldModifiers: Modifiers<CachedObjectRecord>,
|
||||
) => {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { useGetRelationFieldsToOptimisticallyUpdate } from '@/apollo/optimistic-effect/hooks/useGetRelationFieldsToOptimisticallyUpdate';
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
import { triggerUpdateRelationFieldOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationFieldOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
@ -19,6 +21,9 @@ export const useUpdateOneRecord = <
|
||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const getRelationFieldsToOptimisticallyUpdate =
|
||||
useGetRelationFieldsToOptimisticallyUpdate();
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const updateOneRecord = async ({
|
||||
@ -30,18 +35,27 @@ export const useUpdateOneRecord = <
|
||||
}) => {
|
||||
const cachedRecord = getRecordFromCache<UpdatedObjectRecord>(idToUpdate);
|
||||
|
||||
const optimisticallyUpdatedRecord = {
|
||||
...(cachedRecord ?? {}),
|
||||
...updateOneRecordInput,
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToUpdate,
|
||||
};
|
||||
|
||||
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: updateOneRecordInput,
|
||||
});
|
||||
|
||||
const optimisticallyUpdatedRecord = {
|
||||
...(cachedRecord ?? {}),
|
||||
...updateOneRecordInput,
|
||||
...sanitizedUpdateOneRecordInput,
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToUpdate,
|
||||
};
|
||||
|
||||
const updatedRelationFields = cachedRecord
|
||||
? getRelationFieldsToOptimisticallyUpdate({
|
||||
cachedRecord,
|
||||
objectMetadataItem,
|
||||
updateRecordInput: updateOneRecordInput,
|
||||
})
|
||||
: [];
|
||||
|
||||
const mutationResponseField =
|
||||
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
||||
|
||||
@ -64,6 +78,24 @@ export const useUpdateOneRecord = <
|
||||
objectMetadataItem,
|
||||
record,
|
||||
});
|
||||
|
||||
updatedRelationFields.forEach(
|
||||
({
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
previousRelationRecord,
|
||||
nextRelationRecord,
|
||||
}) =>
|
||||
triggerUpdateRelationFieldOptimisticEffect({
|
||||
cache,
|
||||
objectNameSingular,
|
||||
record,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
previousRelationRecord,
|
||||
nextRelationRecord,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -46,7 +46,6 @@ const mocks = [
|
||||
variables: {
|
||||
input: {
|
||||
id: mockedUuid,
|
||||
name: 'Opportunity',
|
||||
pipelineStepId: 'pipelineStepId',
|
||||
companyId: 'New Opportunity',
|
||||
},
|
||||
|
||||
@ -1,17 +1,28 @@
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import {
|
||||
FieldBooleanMetadata,
|
||||
FieldFullNameMetadata,
|
||||
FieldLinkMetadata,
|
||||
FieldPhoneMetadata,
|
||||
FieldRatingMetadata,
|
||||
FieldRelationMetadata,
|
||||
FieldSelectMetadata,
|
||||
FieldTextMetadata,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import {
|
||||
mockedCompaniesMetadata,
|
||||
mockedPeopleMetadata,
|
||||
} from '~/testing/mock-data/metadata';
|
||||
|
||||
export const fieldMetadataId = 'fieldMetadataId';
|
||||
|
||||
const mockedPersonObjectMetadataItem = {
|
||||
...mockedPeopleMetadata.node,
|
||||
fields: mockedPeopleMetadata.node.fields.edges.map(({ node }) => node),
|
||||
};
|
||||
const mockedCompanyObjectMetadataItem = {
|
||||
...mockedCompaniesMetadata.node,
|
||||
fields: mockedCompaniesMetadata.node.fields.edges.map(({ node }) => node),
|
||||
};
|
||||
|
||||
export const textfieldDefinition: FieldDefinition<FieldTextMetadata> = {
|
||||
fieldMetadataId,
|
||||
label: 'User Name',
|
||||
@ -20,29 +31,15 @@ export const textfieldDefinition: FieldDefinition<FieldTextMetadata> = {
|
||||
metadata: { placeHolder: 'John Doe', fieldName: 'userName' },
|
||||
};
|
||||
|
||||
export const booleanFieldDefinition: FieldDefinition<FieldBooleanMetadata> = {
|
||||
fieldMetadataId,
|
||||
label: 'Is Active?',
|
||||
iconName: 'iconName',
|
||||
type: 'BOOLEAN',
|
||||
metadata: {
|
||||
objectMetadataNameSingular: 'person',
|
||||
fieldName: 'isActive',
|
||||
const relationFieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
);
|
||||
export const relationFieldDefinition = formatFieldMetadataItemAsFieldDefinition(
|
||||
{
|
||||
field: relationFieldMetadataItem!,
|
||||
objectMetadataItem: mockedPersonObjectMetadataItem,
|
||||
},
|
||||
};
|
||||
|
||||
export const relationFieldDefinition: FieldDefinition<FieldRelationMetadata> = {
|
||||
fieldMetadataId,
|
||||
label: 'Contact',
|
||||
iconName: 'Phone',
|
||||
type: 'RELATION',
|
||||
metadata: {
|
||||
fieldName: 'contact',
|
||||
relationFieldMetadataId: 'relationFieldMetadataId',
|
||||
relationObjectMetadataNamePlural: 'users',
|
||||
relationObjectMetadataNameSingular: 'user',
|
||||
},
|
||||
};
|
||||
);
|
||||
|
||||
export const selectFieldDefinition: FieldDefinition<FieldSelectMetadata> = {
|
||||
fieldMetadataId,
|
||||
@ -77,17 +74,13 @@ export const linkFieldDefinition: FieldDefinition<FieldLinkMetadata> = {
|
||||
},
|
||||
};
|
||||
|
||||
export const phoneFieldDefinition: FieldDefinition<FieldPhoneMetadata> = {
|
||||
fieldMetadataId,
|
||||
label: 'Contact',
|
||||
iconName: 'Phone',
|
||||
type: 'TEXT',
|
||||
metadata: {
|
||||
objectMetadataNameSingular: 'person',
|
||||
placeHolder: '(+256)-712-345-6789',
|
||||
fieldName: 'phone',
|
||||
},
|
||||
};
|
||||
const phoneFieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'phone',
|
||||
);
|
||||
export const phoneFieldDefinition = formatFieldMetadataItemAsFieldDefinition({
|
||||
field: phoneFieldMetadataItem!,
|
||||
objectMetadataItem: mockedPersonObjectMetadataItem,
|
||||
});
|
||||
|
||||
export const ratingfieldDefinition: FieldDefinition<FieldRatingMetadata> = {
|
||||
fieldMetadataId,
|
||||
@ -98,3 +91,11 @@ export const ratingfieldDefinition: FieldDefinition<FieldRatingMetadata> = {
|
||||
fieldName: 'rating',
|
||||
},
|
||||
};
|
||||
|
||||
const booleanFieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'idealCustomerProfile',
|
||||
);
|
||||
export const booleanFieldDefinition = formatFieldMetadataItemAsFieldDefinition({
|
||||
field: booleanFieldMetadataItem!,
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
});
|
||||
|
||||
@ -25,11 +25,8 @@ jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
|
||||
}));
|
||||
|
||||
const query = gql`
|
||||
mutation UpdateOneWorkspaceMember(
|
||||
$idToUpdate: ID!
|
||||
$input: WorkspaceMemberUpdateInput!
|
||||
) {
|
||||
updateWorkspaceMember(id: $idToUpdate, data: $input) {
|
||||
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
|
||||
updatePerson(id: $idToUpdate, data: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@ -43,7 +40,7 @@ const mocks: MockedResponse[] = [
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
updateWorkspaceMember: {
|
||||
updatePerson: {
|
||||
id: 'entityId',
|
||||
},
|
||||
},
|
||||
@ -54,12 +51,12 @@ const mocks: MockedResponse[] = [
|
||||
query,
|
||||
variables: {
|
||||
idToUpdate: 'entityId',
|
||||
input: { contactId: null, contact: { foo: 'bar' } },
|
||||
input: { companyId: 'companyId' },
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
updateWorkspaceMember: {
|
||||
updatePerson: {
|
||||
id: 'entityId',
|
||||
},
|
||||
},
|
||||
@ -68,14 +65,13 @@ const mocks: MockedResponse[] = [
|
||||
];
|
||||
|
||||
const entityId = 'entityId';
|
||||
const fieldName = 'phone';
|
||||
|
||||
const getWrapper =
|
||||
(fieldDefinition: FieldDefinition<FieldMetadata>) =>
|
||||
({ children }: { children: ReactNode }) => {
|
||||
const useUpdateOneRecordMutation: RecordUpdateHook = () => {
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
});
|
||||
|
||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||
@ -113,7 +109,7 @@ describe('usePersistField', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const entityFields = useRecoilValue(
|
||||
recordStoreFamilySelector({ recordId: entityId, fieldName }),
|
||||
recordStoreFamilySelector({ recordId: entityId, fieldName: 'phone' }),
|
||||
);
|
||||
|
||||
return {
|
||||
@ -137,7 +133,10 @@ describe('usePersistField', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const entityFields = useRecoilValue(
|
||||
recordStoreFamilySelector({ recordId: entityId, fieldName }),
|
||||
recordStoreFamilySelector({
|
||||
recordId: entityId,
|
||||
fieldName: 'company',
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
@ -149,7 +148,7 @@ describe('usePersistField', () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.persistField({ foo: 'bar' });
|
||||
result.current.persistField({ id: 'companyId' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@ -24,13 +24,19 @@ const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
|
||||
updatePerson(id: $idToUpdate, data: $input) {
|
||||
mutation UpdateOneCompany(
|
||||
$idToUpdate: ID!
|
||||
$input: CompanyUpdateInput!
|
||||
) {
|
||||
updateCompany(id: $idToUpdate, data: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { idToUpdate: 'entityId', input: { isActive: true } },
|
||||
variables: {
|
||||
idToUpdate: 'entityId',
|
||||
input: { idealCustomerProfile: true },
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
@ -45,7 +51,7 @@ const mocks: MockedResponse[] = [
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => {
|
||||
const useUpdateOneRecordMutation: RecordUpdateHook = () => {
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
objectNameSingular: CoreObjectNameSingular.Company,
|
||||
});
|
||||
|
||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||
|
||||
@ -82,24 +82,8 @@ export const usePersistField = () => {
|
||||
const fieldIsSelect =
|
||||
isFieldSelect(fieldDefinition) && isFieldSelectValue(valueToPersist);
|
||||
|
||||
if (fieldIsRelation) {
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
set(
|
||||
recordStoreFamilySelector({ recordId: entityId, fieldName }),
|
||||
valueToPersist,
|
||||
);
|
||||
|
||||
updateRecord?.({
|
||||
variables: {
|
||||
where: { id: entityId },
|
||||
updateOneRecordInput: {
|
||||
[`${fieldName}Id`]: valueToPersist?.id ?? null,
|
||||
[fieldName]: valueToPersist ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
if (
|
||||
fieldIsRelation ||
|
||||
fieldIsText ||
|
||||
fieldIsBoolean ||
|
||||
fieldIsEmail ||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useEffect } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RelationPicker } from '@/object-record/relation-picker/components/RelationPicker';
|
||||
@ -33,8 +32,6 @@ export const RelationFieldInput = ({
|
||||
onSubmit?.(() => persistField(newEntity?.record ?? null));
|
||||
};
|
||||
|
||||
useEffect(() => {}, [initialSearchValue]);
|
||||
|
||||
return (
|
||||
<StyledRelationPickerContainer>
|
||||
<RelationPicker
|
||||
|
||||
@ -11,10 +11,10 @@ import {
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
|
||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
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 } from '~/testing/mock-data/users';
|
||||
@ -53,28 +53,25 @@ const RelationFieldInputWithContext = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ObjectMetadataItemsProvider>
|
||||
<RelationPickerScope relationPickerScopeId="relation-picker">
|
||||
<FieldContextProvider
|
||||
fieldDefinition={{
|
||||
fieldMetadataId: 'relation',
|
||||
label: 'Relation',
|
||||
type: 'RELATION',
|
||||
iconName: 'IconLink',
|
||||
metadata: {
|
||||
fieldName: 'Relation',
|
||||
relationObjectMetadataNamePlural: 'workspaceMembers',
|
||||
relationObjectMetadataNameSingular: 'workspaceMember',
|
||||
},
|
||||
}}
|
||||
entityId={entityId}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</FieldContextProvider>
|
||||
</RelationPickerScope>
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
</ObjectMetadataItemsProvider>
|
||||
<FieldContextProvider
|
||||
fieldDefinition={{
|
||||
fieldMetadataId: 'relation',
|
||||
label: 'Relation',
|
||||
type: 'RELATION',
|
||||
iconName: 'IconLink',
|
||||
metadata: {
|
||||
fieldName: 'Relation',
|
||||
relationObjectMetadataNamePlural: 'workspaceMembers',
|
||||
relationObjectMetadataNameSingular:
|
||||
CoreObjectNameSingular.WorkspaceMember,
|
||||
},
|
||||
}}
|
||||
entityId={entityId}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</FieldContextProvider>
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -102,7 +99,11 @@ const meta: Meta = {
|
||||
onSubmit: { control: false },
|
||||
onCancel: { control: false },
|
||||
},
|
||||
decorators: [SnackBarDecorator, clearMocksDecorator],
|
||||
decorators: [
|
||||
clearMocksDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
parameters: {
|
||||
clearMocks: true,
|
||||
msw: graphqlMocks,
|
||||
|
||||
@ -3,7 +3,6 @@ import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { LightIconButton, MenuItem } from 'tsup.ui.index';
|
||||
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
@ -57,20 +56,15 @@ export const RecordRelationFieldCardContent = ({
|
||||
divider,
|
||||
relationRecord,
|
||||
}: RecordRelationFieldCardContentProps) => {
|
||||
const { fieldDefinition, entityId } = useContext(FieldContext);
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const {
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationType,
|
||||
fieldName,
|
||||
objectMetadataNameSingular,
|
||||
} = fieldDefinition.metadata as FieldRelationMetadata;
|
||||
|
||||
const { modifyRecordFromCache } = useObjectMetadataItem({
|
||||
objectNameSingular: objectMetadataNameSingular ?? '',
|
||||
});
|
||||
|
||||
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
@ -102,31 +96,9 @@ export const RecordRelationFieldCardContent = ({
|
||||
updateOneRelationRecord({
|
||||
idToUpdate: relationRecord.id,
|
||||
updateOneRecordInput: {
|
||||
[`${relationFieldMetadataItem.name}Id`]: null,
|
||||
[relationFieldMetadataItem.name]: null,
|
||||
},
|
||||
});
|
||||
|
||||
modifyRecordFromCache(entityId, {
|
||||
[fieldName]: (cachedRelationConnection, { readField }) => {
|
||||
const edges = readField<CachedObjectRecordEdge[]>(
|
||||
'edges',
|
||||
cachedRelationConnection,
|
||||
);
|
||||
|
||||
if (!edges) {
|
||||
return cachedRelationConnection;
|
||||
}
|
||||
|
||||
return {
|
||||
...cachedRelationConnection,
|
||||
edges: edges.filter(({ node }) => {
|
||||
const id = readField('id', node);
|
||||
return id !== relationRecord.id;
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isOpportunityCompanyRelation =
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Reference } from '@apollo/client';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import qs from 'qs';
|
||||
@ -8,7 +7,6 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
|
||||
import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
@ -21,7 +19,6 @@ import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRela
|
||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { IconForbid, IconPlus } from '@/ui/display/icon';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
@ -89,7 +86,6 @@ export const RecordRelationFieldCardSection = () => {
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationType,
|
||||
objectMetadataNameSingular,
|
||||
} = fieldDefinition.metadata as FieldRelationMetadata;
|
||||
const record = useRecoilValue(recordStoreFamilyState(entityId));
|
||||
|
||||
@ -100,10 +96,6 @@ export const RecordRelationFieldCardSection = () => {
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: objectMetadataNameSingular ?? '',
|
||||
});
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === relationFieldMetadataId,
|
||||
);
|
||||
@ -124,24 +116,8 @@ export const RecordRelationFieldCardSection = () => {
|
||||
|
||||
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownId);
|
||||
|
||||
const { relationPickerSearchFilter, setRelationPickerSearchFilter } =
|
||||
useRelationPicker({ relationPickerScopeId: dropdownId });
|
||||
|
||||
const { searchQuery } = useRelationPicker();
|
||||
|
||||
const entities = useFilteredSearchEntityQuery({
|
||||
filters: [
|
||||
{
|
||||
fieldNames:
|
||||
searchQuery?.computeFilterFields?.(
|
||||
relationObjectMetadataNameSingular,
|
||||
) ?? [],
|
||||
filter: relationPickerSearchFilter,
|
||||
},
|
||||
],
|
||||
orderByField: 'createdAt',
|
||||
selectedIds: relationRecordIds,
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
const { setRelationPickerSearchFilter } = useRelationPicker({
|
||||
relationPickerScopeId: dropdownId,
|
||||
});
|
||||
|
||||
const handleCloseRelationPickerDropdown = useCallback(() => {
|
||||
@ -153,46 +129,24 @@ export const RecordRelationFieldCardSection = () => {
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const modifyRecordFromCache = useModifyRecordFromCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const handleRelationPickerEntitySelected = (
|
||||
selectedRelationEntity?: EntityForSelect,
|
||||
) => {
|
||||
closeDropdown();
|
||||
|
||||
if (!selectedRelationEntity?.id) return;
|
||||
if (!selectedRelationEntity?.id || !relationFieldMetadataItem?.name) return;
|
||||
|
||||
if (isToOneObject) {
|
||||
persistField(selectedRelationEntity.record);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!relationFieldMetadataItem?.name) return;
|
||||
|
||||
updateOneRelationRecord({
|
||||
idToUpdate: selectedRelationEntity.id,
|
||||
updateOneRecordInput: {
|
||||
[`${relationFieldMetadataItem.name}Id`]: entityId,
|
||||
[relationFieldMetadataItem.name]: record,
|
||||
},
|
||||
});
|
||||
|
||||
modifyRecordFromCache(entityId, {
|
||||
[fieldName]: (relationRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>('edges', relationRef);
|
||||
|
||||
if (!edges) {
|
||||
return relationRef;
|
||||
}
|
||||
|
||||
return {
|
||||
...relationRef,
|
||||
edges: [...edges, { node: record }],
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const filterQueryParams: FilterQueryParams = {
|
||||
@ -208,55 +162,58 @@ export const RecordRelationFieldCardSection = () => {
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<RelationPickerScope relationPickerScopeId={dropdownId}>
|
||||
<StyledHeader isDropdownOpen={isDropdownOpen}>
|
||||
<StyledTitle>
|
||||
<StyledTitleLabel>{fieldDefinition.label}</StyledTitleLabel>
|
||||
{parseFieldRelationType(relationFieldMetadataItem) ===
|
||||
'TO_ONE_OBJECT' && (
|
||||
<StyledLink to={filterLinkHref}>
|
||||
All ({relationRecords.length})
|
||||
</StyledLink>
|
||||
)}
|
||||
</StyledTitle>
|
||||
<DropdownScope dropdownScopeId={dropdownId}>
|
||||
<StyledAddDropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="right-start"
|
||||
onClose={handleCloseRelationPickerDropdown}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
className="displayOnHover"
|
||||
Icon={IconPlus}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<StyledHeader isDropdownOpen={isDropdownOpen}>
|
||||
<StyledTitle>
|
||||
<StyledTitleLabel>{fieldDefinition.label}</StyledTitleLabel>
|
||||
{parseFieldRelationType(relationFieldMetadataItem) ===
|
||||
'TO_ONE_OBJECT' && (
|
||||
<StyledLink to={filterLinkHref}>
|
||||
All ({relationRecords.length})
|
||||
</StyledLink>
|
||||
)}
|
||||
</StyledTitle>
|
||||
<DropdownScope dropdownScopeId={dropdownId}>
|
||||
<StyledAddDropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="right-start"
|
||||
onClose={handleCloseRelationPickerDropdown}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
className="displayOnHover"
|
||||
Icon={IconPlus}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<RelationPickerScope relationPickerScopeId={dropdownId}>
|
||||
<SingleEntitySelectMenuItemsWithSearch
|
||||
EmptyIcon={IconForbid}
|
||||
entitiesToSelect={entities.entitiesToSelect}
|
||||
loading={entities.loading}
|
||||
onEntitySelected={handleRelationPickerEntitySelected}
|
||||
selectedRelationRecordIds={relationRecordIds}
|
||||
relationObjectNameSingular={
|
||||
relationObjectMetadataNameSingular
|
||||
}
|
||||
relationPickerScopeId={dropdownId}
|
||||
/>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
</RelationPickerScope>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
/>
|
||||
</DropdownScope>
|
||||
</StyledHeader>
|
||||
{!!relationRecords.length && (
|
||||
<Card>
|
||||
{relationRecords.slice(0, 5).map((relationRecord, index) => (
|
||||
<RecordRelationFieldCardContent
|
||||
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
|
||||
divider={index < relationRecords.length - 1}
|
||||
relationRecord={relationRecord}
|
||||
/>
|
||||
</DropdownScope>
|
||||
</StyledHeader>
|
||||
{!!relationRecords.length && (
|
||||
<Card>
|
||||
{relationRecords.slice(0, 5).map((relationRecord, index) => (
|
||||
<RecordRelationFieldCardContent
|
||||
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
|
||||
divider={index < relationRecords.length - 1}
|
||||
relationRecord={relationRecord}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
</RelationPickerScope>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,7 +5,6 @@ import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldM
|
||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { IconForbid } from '@/ui/display/icon';
|
||||
|
||||
export type RelationPickerProps = {
|
||||
@ -27,33 +26,15 @@ export const RelationPicker = ({
|
||||
initialSearchFilter,
|
||||
fieldDefinition,
|
||||
}: RelationPickerProps) => {
|
||||
const {
|
||||
relationPickerSearchFilter,
|
||||
setRelationPickerSearchFilter,
|
||||
searchQuery,
|
||||
} = useRelationPicker({ relationPickerScopeId: 'relation-picker' });
|
||||
const relationPickerScopeId = 'relation-picker';
|
||||
const { setRelationPickerSearchFilter } = useRelationPicker({
|
||||
relationPickerScopeId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setRelationPickerSearchFilter(initialSearchFilter ?? '');
|
||||
}, [initialSearchFilter, setRelationPickerSearchFilter]);
|
||||
|
||||
const entities = useFilteredSearchEntityQuery({
|
||||
filters: [
|
||||
{
|
||||
fieldNames:
|
||||
searchQuery?.computeFilterFields?.(
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
) ?? [],
|
||||
filter: relationPickerSearchFilter,
|
||||
},
|
||||
],
|
||||
orderByField: 'createdAt',
|
||||
selectedIds: recordId ? [recordId] : [],
|
||||
excludeEntityIds: excludeRecordIds,
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const handleEntitySelected = (
|
||||
selectedEntity: EntityForSelect | null | undefined,
|
||||
) => onSubmit(selectedEntity ?? null);
|
||||
@ -62,12 +43,15 @@ export const RelationPicker = ({
|
||||
<SingleEntitySelect
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
entitiesToSelect={entities.entitiesToSelect}
|
||||
loading={entities.loading}
|
||||
onCancel={onCancel}
|
||||
onEntitySelected={handleEntitySelected}
|
||||
selectedEntity={entities.selectedEntities[0]}
|
||||
width={width}
|
||||
relationObjectNameSingular={
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular
|
||||
}
|
||||
relationPickerScopeId={relationPickerScopeId}
|
||||
selectedRelationRecordIds={recordId ? [recordId] : []}
|
||||
excludedRelationRecordIds={excludeRecordIds}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -13,15 +13,17 @@ export type SingleEntitySelectProps = {
|
||||
} & SingleEntitySelectMenuItemsWithSearchProps;
|
||||
|
||||
export const SingleEntitySelect = ({
|
||||
EmptyIcon,
|
||||
disableBackgroundBlur = false,
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
loading,
|
||||
excludedRelationRecordIds,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
relationObjectNameSingular,
|
||||
relationPickerScopeId,
|
||||
selectedEntity,
|
||||
selectedRelationRecordIds,
|
||||
width = 200,
|
||||
}: SingleEntitySelectProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -52,12 +54,14 @@ export const SingleEntitySelect = ({
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
loading,
|
||||
excludedRelationRecordIds,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
relationObjectNameSingular,
|
||||
relationPickerScopeId,
|
||||
selectedEntity,
|
||||
selectedRelationRecordIds,
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -86,58 +86,54 @@ export const SingleEntitySelectMenuItems = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
||||
<MenuItem text="No result" />
|
||||
) : (
|
||||
<>
|
||||
{isAllEntitySelectShown &&
|
||||
selectAllLabel &&
|
||||
onAllEntitySelected && (
|
||||
<MenuItemSelect
|
||||
key="select-all"
|
||||
onClick={() => onAllEntitySelected()}
|
||||
LeftIcon={SelectAllIcon}
|
||||
text={selectAllLabel}
|
||||
selected={!!isAllEntitySelected}
|
||||
/>
|
||||
)}
|
||||
{emptyLabel && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
||||
<MenuItem text="No result" />
|
||||
) : (
|
||||
<>
|
||||
{isAllEntitySelectShown &&
|
||||
selectAllLabel &&
|
||||
onAllEntitySelected && (
|
||||
<MenuItemSelect
|
||||
key="select-none"
|
||||
onClick={() => onEntitySelected()}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={!selectedEntity}
|
||||
key="select-all"
|
||||
onClick={() => onAllEntitySelected()}
|
||||
LeftIcon={SelectAllIcon}
|
||||
text={selectAllLabel}
|
||||
selected={!!isAllEntitySelected}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{entitiesInDropdown?.map((entity) => (
|
||||
<SelectableMenuItemSelect
|
||||
key={entity.id}
|
||||
entity={entity}
|
||||
onEntitySelected={onEntitySelected}
|
||||
selectedEntity={selectedEntity}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
{showCreateButton && !loading && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
<CreateNewButton
|
||||
onClick={onCreate}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
{emptyLabel && (
|
||||
<MenuItemSelect
|
||||
key="select-none"
|
||||
onClick={() => onEntitySelected()}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={!selectedEntity}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{entitiesInDropdown?.map((entity) => (
|
||||
<SelectableMenuItemSelect
|
||||
key={entity.id}
|
||||
entity={entity}
|
||||
onEntitySelected={onEntitySelected}
|
||||
selectedEntity={selectedEntity}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
))}
|
||||
{showCreateButton && !loading && (
|
||||
<>
|
||||
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
<CreateNewButton
|
||||
onClick={onCreate}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</SelectableList>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
||||
import {
|
||||
SingleEntitySelectMenuItems,
|
||||
SingleEntitySelectMenuItemsProps,
|
||||
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
|
||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -9,13 +11,15 @@ import { isDefined } from '~/utils/isDefined';
|
||||
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
|
||||
|
||||
export type SingleEntitySelectMenuItemsWithSearchProps = {
|
||||
excludedRelationRecordIds?: string[];
|
||||
onCreate?: () => void;
|
||||
relationObjectNameSingular: string;
|
||||
relationPickerScopeId?: string;
|
||||
selectedRelationRecordIds: string[];
|
||||
} & Pick<
|
||||
SingleEntitySelectMenuItemsProps,
|
||||
| 'EmptyIcon'
|
||||
| 'emptyLabel'
|
||||
| 'entitiesToSelect'
|
||||
| 'loading'
|
||||
| 'onCancel'
|
||||
| 'onEntitySelected'
|
||||
| 'selectedEntity'
|
||||
@ -24,19 +28,41 @@ export type SingleEntitySelectMenuItemsWithSearchProps = {
|
||||
export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
loading,
|
||||
excludedRelationRecordIds,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
relationObjectNameSingular,
|
||||
relationPickerScopeId = 'relation-picker',
|
||||
selectedEntity,
|
||||
selectedRelationRecordIds,
|
||||
}: SingleEntitySelectMenuItemsWithSearchProps) => {
|
||||
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
|
||||
const { searchFilter, searchQuery, handleSearchFilterChange } =
|
||||
useEntitySelectSearch({
|
||||
relationPickerScopeId,
|
||||
});
|
||||
|
||||
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
|
||||
|
||||
const entities = useFilteredSearchEntityQuery({
|
||||
filters: [
|
||||
{
|
||||
fieldNames:
|
||||
searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [],
|
||||
filter: searchFilter,
|
||||
},
|
||||
],
|
||||
orderByField: 'createdAt',
|
||||
selectedIds: selectedRelationRecordIds,
|
||||
excludeEntityIds: excludedRelationRecordIds,
|
||||
objectNameSingular: relationObjectNameSingular,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ObjectMetadataItemsRelationPickerEffect
|
||||
relationPickerScopeId={relationPickerScopeId}
|
||||
/>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={handleSearchFilterChange}
|
||||
@ -44,15 +70,15 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<SingleEntitySelectMenuItems
|
||||
entitiesToSelect={entities.entitiesToSelect}
|
||||
loading={entities.loading}
|
||||
selectedEntity={selectedEntity ?? entities.selectedEntities[0]}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
loading,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
selectedEntity,
|
||||
showCreateButton,
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { IconUserCircle } from '@/ui/display/icon';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { RelationPickerDecorator } from '~/testing/decorators/RelationPickerDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedPeopleData } from '~/testing/mock-data/people';
|
||||
import { sleep } from '~/testing/sleep';
|
||||
|
||||
@ -26,7 +30,13 @@ const meta: Meta<typeof SingleEntitySelect> = {
|
||||
ComponentDecorator,
|
||||
ComponentWithRecoilScopeDecorator,
|
||||
RelationPickerDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
args: {
|
||||
relationObjectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
selectedRelationRecordIds: [],
|
||||
},
|
||||
argTypes: {
|
||||
selectedEntity: {
|
||||
options: entities.map(({ name }) => name),
|
||||
@ -36,37 +46,8 @@ const meta: Meta<typeof SingleEntitySelect> = {
|
||||
),
|
||||
},
|
||||
},
|
||||
render: ({
|
||||
EmptyIcon,
|
||||
disableBackgroundBlur = false,
|
||||
emptyLabel,
|
||||
loading,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
selectedEntity,
|
||||
width,
|
||||
}) => {
|
||||
const filteredEntities = entities.filter(
|
||||
(entity) => entity.id !== selectedEntity?.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<SingleEntitySelect
|
||||
{...{
|
||||
EmptyIcon,
|
||||
disableBackgroundBlur,
|
||||
emptyLabel,
|
||||
loading,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
selectedEntity,
|
||||
width,
|
||||
}}
|
||||
entitiesToSelect={filteredEntities}
|
||||
/>
|
||||
);
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
@ -89,7 +70,7 @@ export const WithEmptyOption: Story = {
|
||||
export const WithSearchFilter: Story = {
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const searchInput = canvas.getByRole('textbox');
|
||||
const searchInput = await canvas.findByRole('textbox');
|
||||
|
||||
await step('Enter search text', async () => {
|
||||
await sleep(50);
|
||||
|
||||
@ -2,12 +2,17 @@ import debounce from 'lodash.debounce';
|
||||
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
|
||||
export const useEntitySelectSearch = () => {
|
||||
export const useEntitySelectSearch = ({
|
||||
relationPickerScopeId,
|
||||
}: {
|
||||
relationPickerScopeId?: string;
|
||||
} = {}) => {
|
||||
const {
|
||||
setRelationPickerPreselectedId,
|
||||
relationPickerSearchFilter,
|
||||
searchQuery,
|
||||
setRelationPickerPreselectedId,
|
||||
setRelationPickerSearchFilter,
|
||||
} = useRelationPicker();
|
||||
} = useRelationPicker({ relationPickerScopeId });
|
||||
|
||||
const debouncedSetSearchFilter = debounce(
|
||||
setRelationPickerSearchFilter,
|
||||
@ -26,6 +31,7 @@ export const useEntitySelectSearch = () => {
|
||||
|
||||
return {
|
||||
searchFilter: relationPickerSearchFilter,
|
||||
searchQuery,
|
||||
handleSearchFilterChange,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const sanitizeRecordInput = ({
|
||||
objectMetadataItem,
|
||||
@ -9,12 +11,30 @@ export const sanitizeRecordInput = ({
|
||||
recordInput: Record<string, unknown>;
|
||||
}) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(recordInput).filter(([fieldName]) => {
|
||||
const fieldDefinition = objectMetadataItem.fields.find(
|
||||
(field) => field.name === fieldName,
|
||||
);
|
||||
Object.entries(recordInput)
|
||||
.map<[string, unknown] | undefined>(([fieldName, fieldValue]) => {
|
||||
const fieldDefinition = objectMetadataItem.fields.find(
|
||||
(field) => field.name === fieldName,
|
||||
);
|
||||
|
||||
return fieldDefinition?.type !== FieldMetadataType.Relation;
|
||||
}),
|
||||
if (!fieldDefinition) return undefined;
|
||||
|
||||
if (
|
||||
fieldDefinition.type === FieldMetadataType.Relation &&
|
||||
isFieldRelationValue(fieldValue)
|
||||
) {
|
||||
const relationIdFieldName = `${fieldDefinition.name}Id`;
|
||||
const relationIdFieldDefinition = objectMetadataItem.fields.find(
|
||||
(field) => field.name === relationIdFieldName,
|
||||
);
|
||||
|
||||
return relationIdFieldDefinition
|
||||
? [relationIdFieldName, fieldValue?.id ?? null]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
return [fieldName, fieldValue];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user