Right drawer to edit records (#5551)
This PR introduces a new side panel to edit records and the ability to minimize the side panel. The goal is leverage this sidepanel to be able to create records while being in another show page. I'm opening the PR for feedback since it involved refactoring and therefore already touches a lot of files, even though it was quick to implement. <img width="1503" alt="Screenshot 2024-05-23 at 17 41 37" src="https://github.com/twentyhq/twenty/assets/6399865/6f17e7a8-f4e9-4eb4-b392-c756db7198ac">
This commit is contained in:
@ -1,9 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { IconForbid } from 'twenty-ui';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
|
||||
@ -39,11 +42,33 @@ export const RelationPicker = ({
|
||||
selectedEntity: EntityForSelect | null | undefined,
|
||||
) => onSubmit(selectedEntity ?? null);
|
||||
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === fieldDefinition.metadata.relationFieldMetadataId,
|
||||
);
|
||||
|
||||
const { entityId } = useContext(FieldContext);
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
useAddNewRecordAndOpenRightDrawer({
|
||||
relationObjectMetadataNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
relationObjectMetadataItem,
|
||||
relationFieldMetadataItem,
|
||||
entityId,
|
||||
});
|
||||
|
||||
return (
|
||||
<SingleEntitySelect
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
onCancel={onCancel}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
onEntitySelected={handleEntitySelected}
|
||||
width={width}
|
||||
relationObjectNameSingular={
|
||||
|
||||
@ -122,7 +122,7 @@ export const SingleEntitySelectMenuItems = ({
|
||||
selectedEntity={selectedEntity}
|
||||
/>
|
||||
))}
|
||||
{showCreateButton && !loading && (
|
||||
{showCreateButton && (
|
||||
<>
|
||||
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
<CreateNewButton
|
||||
|
||||
@ -15,7 +15,7 @@ import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
|
||||
|
||||
export type SingleEntitySelectMenuItemsWithSearchProps = {
|
||||
excludedRelationRecordIds?: string[];
|
||||
onCreate?: () => void;
|
||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||
relationObjectNameSingular: string;
|
||||
relationPickerScopeId?: string;
|
||||
selectedRelationRecordIds: string[];
|
||||
@ -54,8 +54,7 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
relationPickerSearchFilterState,
|
||||
);
|
||||
|
||||
const showCreateButton =
|
||||
isDefined(onCreate) && relationPickerSearchFilter !== '';
|
||||
const showCreateButton = isDefined(onCreate);
|
||||
|
||||
const entities = useFilteredSearchEntityQuery({
|
||||
filters: [
|
||||
@ -71,6 +70,20 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
objectNameSingular: relationObjectNameSingular,
|
||||
});
|
||||
|
||||
let onCreateWithInput = undefined;
|
||||
|
||||
if (isDefined(onCreate)) {
|
||||
onCreateWithInput = () => {
|
||||
if (onCreate.length > 0) {
|
||||
(onCreate as (searchInput?: string) => void)(
|
||||
relationPickerSearchFilter,
|
||||
);
|
||||
} else {
|
||||
(onCreate as () => void)();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ObjectMetadataItemsRelationPickerEffect
|
||||
@ -81,12 +94,17 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
<SingleEntitySelectMenuItems
|
||||
entitiesToSelect={entities.entitiesToSelect}
|
||||
loading={entities.loading}
|
||||
selectedEntity={selectedEntity ?? entities.selectedEntities[0]}
|
||||
selectedEntity={
|
||||
selectedEntity ??
|
||||
(entities.selectedEntities.length === 1
|
||||
? entities.selectedEntities[0]
|
||||
: undefined)
|
||||
}
|
||||
onCreate={onCreateWithInput}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
showCreateButton,
|
||||
}}
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type RecordDetailRelationSectionProps = {
|
||||
relationObjectMetadataNameSingular: string;
|
||||
relationObjectMetadataItem: ObjectMetadataItem;
|
||||
relationFieldMetadataItem?: FieldMetadataItem;
|
||||
entityId: string;
|
||||
};
|
||||
export const useAddNewRecordAndOpenRightDrawer = ({
|
||||
relationObjectMetadataNameSingular,
|
||||
relationObjectMetadataItem,
|
||||
relationFieldMetadataItem,
|
||||
entityId,
|
||||
}: RecordDetailRelationSectionProps) => {
|
||||
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
|
||||
const setViewableRecordNameSingular = useSetRecoilState(
|
||||
viewableRecordNameSingularState,
|
||||
);
|
||||
|
||||
const { createOneRecord } = useCreateOneRecord({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular:
|
||||
relationFieldMetadataItem?.relationDefinition?.targetObjectMetadata
|
||||
.nameSingular ?? 'workspaceMember',
|
||||
});
|
||||
|
||||
const { openRightDrawer } = useRightDrawer();
|
||||
|
||||
if (
|
||||
relationObjectMetadataNameSingular === 'workspaceMember' ||
|
||||
!isDefined(
|
||||
relationFieldMetadataItem?.relationDefinition?.targetObjectMetadata
|
||||
.nameSingular,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
createNewRecordAndOpenRightDrawer: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createNewRecordAndOpenRightDrawer: async (searchInput?: string) => {
|
||||
const newRecordId = v4();
|
||||
const labelIdentifierType = getLabelIdentifierFieldMetadataItem(
|
||||
relationObjectMetadataItem,
|
||||
)?.type;
|
||||
const createRecordPayload: {
|
||||
id: string;
|
||||
name:
|
||||
| string
|
||||
| { firstName: string | undefined; lastName: string | undefined };
|
||||
[key: string]: any;
|
||||
} =
|
||||
labelIdentifierType === FieldMetadataType.FullName
|
||||
? {
|
||||
id: newRecordId,
|
||||
name:
|
||||
searchInput && searchInput.split(' ').length > 1
|
||||
? {
|
||||
firstName: searchInput.split(' ')[0],
|
||||
lastName: searchInput.split(' ').slice(1).join(' '),
|
||||
}
|
||||
: { firstName: searchInput, lastName: '' },
|
||||
}
|
||||
: { id: newRecordId, name: searchInput ?? '' };
|
||||
|
||||
if (
|
||||
relationFieldMetadataItem?.relationDefinition?.direction ===
|
||||
RelationDefinitionType.ManyToOne
|
||||
) {
|
||||
createRecordPayload[
|
||||
`${relationFieldMetadataItem?.relationDefinition?.targetFieldMetadata.name}Id`
|
||||
] = entityId;
|
||||
}
|
||||
|
||||
await createOneRecord(createRecordPayload);
|
||||
|
||||
if (
|
||||
relationFieldMetadataItem?.relationDefinition?.direction ===
|
||||
RelationDefinitionType.OneToMany
|
||||
) {
|
||||
await updateOneRecord({
|
||||
idToUpdate: entityId,
|
||||
updateOneRecordInput: {
|
||||
[`${relationFieldMetadataItem?.relationDefinition?.targetFieldMetadata.name}Id`]:
|
||||
newRecordId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setViewableRecordId(newRecordId);
|
||||
setViewableRecordNameSingular(relationObjectMetadataNameSingular);
|
||||
openRightDrawer(RightDrawerPages.ViewRecord);
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user