Remove opportunity specific code on Kanban (#11000)

In this PR:
- clean board from opportunity specific logic
- remove in place creation in board
- trigger creation in right drawer instead
This commit is contained in:
Charles Bochet
2025-03-18 23:54:40 +01:00
committed by GitHub
parent d47debaff6
commit a4bd00ae29
73 changed files with 725 additions and 2366 deletions

View File

@ -1,7 +1,6 @@
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook'; import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -15,24 +14,14 @@ export const useCreateNewTableRecordNoSelectionRecordAction: ActionHookWithObjec
throw new Error('Current view ID is not defined'); throw new Error('Current view ID is not defined');
} }
const recordTableId = getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
currentViewId,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewTableRecord } = useCreateNewTableRecord({ const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem, objectMetadataItem,
recordTableId,
}); });
const onClick = () => {
createNewTableRecord();
};
return { return {
shouldBeRegistered: !hasObjectReadOnlyPermission, shouldBeRegistered: !hasObjectReadOnlyPermission,
onClick, onClick: createNewIndexRecord,
}; };
}; };

View File

@ -7,6 +7,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords'; import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
@ -130,13 +131,19 @@ describe('useActivityTargetObjectRecords', () => {
objectMetadataItemsState, objectMetadataItemsState,
); );
const { activityTargetObjectRecords } = const setRecordFromStore = useSetRecoilState(
useActivityTargetObjectRecords(task); recordStoreFamilyState(task.id),
);
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
task.id,
);
return { return {
activityTargetObjectRecords, activityTargetObjectRecords,
setCurrentWorkspaceMember, setCurrentWorkspaceMember,
setObjectMetadataItems, setObjectMetadataItems,
setRecordFromStore,
}; };
}, },
{ wrapper: Wrapper }, { wrapper: Wrapper },
@ -145,6 +152,7 @@ describe('useActivityTargetObjectRecords', () => {
act(() => { act(() => {
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]); result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
result.current.setObjectMetadataItems(generatedMockObjectMetadataItems); result.current.setObjectMetadataItems(generatedMockObjectMetadataItems);
result.current.setRecordFromStore(task);
}); });
const activityTargetObjectRecords = const activityTargetObjectRecords =

View File

@ -8,14 +8,19 @@ import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget'; import { TaskTarget } from '@/activities/types/TaskTarget';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
export const useActivityTargetObjectRecords = ( export const useActivityTargetObjectRecords = (
activity?: Task | Note, activityRecordId?: string,
activityTargets?: NoteTarget[] | TaskTarget[], activityTargets?: NoteTarget[] | TaskTarget[],
) => { ) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const activity = useRecoilValue(
recordStoreFamilyState(activityRecordId ?? ''),
) as Note | Task | null;
if (!isDefined(activity) && !isDefined(activityTargets)) { if (!isDefined(activity) && !isDefined(activityTargets)) {
return { activityTargetObjectRecords: [] }; return { activityTargetObjectRecords: [] };
} }

View File

@ -7,8 +7,6 @@ import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTa
import { useOpenActivityTargetInlineCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode'; import { useOpenActivityTargetInlineCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode';
import { useUpdateActivityTargetFromInlineCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell'; import { useUpdateActivityTargetFromInlineCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope'; import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext'; import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -22,7 +20,7 @@ import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-rec
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
type ActivityTargetsInlineCellProps = { type ActivityTargetsInlineCellProps = {
activity: Task | Note; activityRecordId: string;
showLabel?: boolean; showLabel?: boolean;
maxWidth?: number; maxWidth?: number;
activityObjectNameSingular: activityObjectNameSingular:
@ -31,15 +29,15 @@ type ActivityTargetsInlineCellProps = {
}; };
export const ActivityTargetsInlineCell = ({ export const ActivityTargetsInlineCell = ({
activity, activityRecordId,
showLabel = true, showLabel = true,
maxWidth, maxWidth,
activityObjectNameSingular, activityObjectNameSingular,
}: ActivityTargetsInlineCellProps) => { }: ActivityTargetsInlineCellProps) => {
const { activityTargetObjectRecords } = const { activityTargetObjectRecords } =
useActivityTargetObjectRecords(activity); useActivityTargetObjectRecords(activityRecordId);
const multipleRecordPickerInstanceId = `multiple-record-picker-target-${activity.id}`; const multipleRecordPickerInstanceId = `multiple-record-picker-target-${activityRecordId}`;
const { closeInlineCell } = useInlineCell(); const { closeInlineCell } = useInlineCell();
@ -58,7 +56,7 @@ export const ActivityTargetsInlineCell = ({
const { FieldContextProvider: ActivityTargetsContextProvider } = const { FieldContextProvider: ActivityTargetsContextProvider } =
useFieldContext({ useFieldContext({
objectNameSingular: activityObjectNameSingular, objectNameSingular: activityObjectNameSingular,
objectRecordId: activity.id, objectRecordId: activityRecordId,
fieldMetadataName: fieldDefinition.metadata.fieldName, fieldMetadataName: fieldDefinition.metadata.fieldName,
fieldPosition: 3, fieldPosition: 3,
overridenIsFieldEmpty: activityTargetObjectRecords.length === 0, overridenIsFieldEmpty: activityTargetObjectRecords.length === 0,
@ -70,11 +68,11 @@ export const ActivityTargetsInlineCell = ({
const { updateActivityTargetFromInlineCell } = const { updateActivityTargetFromInlineCell } =
useUpdateActivityTargetFromInlineCell({ useUpdateActivityTargetFromInlineCell({
activityObjectNameSingular, activityObjectNameSingular,
activityId: activity.id, activityId: activityRecordId,
}); });
return ( return (
<RecordFieldInputScope recordFieldInputScopeId={activity?.id ?? ''}> <RecordFieldInputScope recordFieldInputScopeId={activityRecordId}>
<FieldFocusContextProvider> <FieldFocusContextProvider>
{ActivityTargetsContextProvider && ( {ActivityTargetsContextProvider && (
<ActivityTargetsContextProvider> <ActivityTargetsContextProvider>

View File

@ -96,7 +96,7 @@ export const NoteCard = ({
{NoteTargetsContextProvider && ( {NoteTargetsContextProvider && (
<NoteTargetsContextProvider> <NoteTargetsContextProvider>
<ActivityTargetsInlineCell <ActivityTargetsInlineCell
activity={note} activityRecordId={note.id}
activityObjectNameSingular={CoreObjectNameSingular.Note} activityObjectNameSingular={CoreObjectNameSingular.Note}
/> />
</NoteTargetsContextProvider> </NoteTargetsContextProvider>

View File

@ -132,7 +132,7 @@ export const TaskRow = ({ task }: { task: Task }) => {
<TaskTargetsContextProvider> <TaskTargetsContextProvider>
<ActivityTargetsInlineCell <ActivityTargetsInlineCell
activityObjectNameSingular={CoreObjectNameSingular.Task} activityObjectNameSingular={CoreObjectNameSingular.Task}
activity={task} activityRecordId={task.id}
showLabel={false} showLabel={false}
maxWidth={200} maxWidth={200}
/> />

View File

@ -11,6 +11,8 @@ import { TimelineActivity } from '@/activities/timeline-activities/types/Timelin
import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName'; import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { MOBILE_VIEWPORT } from 'twenty-ui'; import { MOBILE_VIEWPORT } from 'twenty-ui';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -104,7 +106,10 @@ export const EventRow = ({
}: EventRowProps) => { }: EventRowProps) => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { labelIdentifierValue } = useContext(TimelineActivityContext); const { recordId } = useContext(TimelineActivityContext);
const recordFromStore = useRecoilValue(recordStoreFamilyState(recordId));
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt); const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt);
const linkedObjectMetadataItem = useLinkedObjectObjectMetadataItem( const linkedObjectMetadataItem = useLinkedObjectObjectMetadataItem(
event.linkedObjectMetadataId, event.linkedObjectMetadataId,
@ -114,6 +119,18 @@ export const EventRow = ({
return null; return null;
} }
if (isUndefinedOrNull(recordFromStore)) {
return null;
}
if (isUndefinedOrNull(mainObjectMetadataItem)) {
return null;
}
const labelIdentifier = getObjectRecordIdentifier({
objectMetadataItem: mainObjectMetadataItem,
record: recordFromStore,
});
const authorFullName = getTimelineActivityAuthorFullName( const authorFullName = getTimelineActivityAuthorFullName(
event, event,
currentWorkspaceMember, currentWorkspaceMember,
@ -143,7 +160,7 @@ export const EventRow = ({
<StyledSummary> <StyledSummary>
<EventRowDynamicComponent <EventRowDynamicComponent
authorFullName={authorFullName} authorFullName={authorFullName}
labelIdentifierValue={labelIdentifierValue} labelIdentifierValue={labelIdentifier.name}
event={event} event={event}
mainObjectMetadataItem={mainObjectMetadataItem} mainObjectMetadataItem={mainObjectMetadataItem}
linkedObjectMetadataItem={linkedObjectMetadataItem} linkedObjectMetadataItem={linkedObjectMetadataItem}

View File

@ -17,9 +17,7 @@ const meta: Meta<typeof TimelineActivities> = {
SnackBarDecorator, SnackBarDecorator,
(Story) => { (Story) => {
return ( return (
<TimelineActivityContext.Provider <TimelineActivityContext.Provider value={{ recordId: 'mock-id' }}>
value={{ labelIdentifierValue: 'Mock' }}
>
<Story /> <Story />
</TimelineActivityContext.Provider> </TimelineActivityContext.Provider>
); );

View File

@ -1,10 +1,10 @@
import { createContext } from 'react'; import { createContext } from 'react';
type TimelineActivityContextValue = { type TimelineActivityContextValue = {
labelIdentifierValue: string; recordId: string;
}; };
export const TimelineActivityContext = export const TimelineActivityContext =
createContext<TimelineActivityContextValue>({ createContext<TimelineActivityContextValue>({
labelIdentifierValue: '', recordId: '',
}); });

View File

@ -16,9 +16,7 @@ const meta: Meta<typeof EventCardMessage> = {
SnackBarDecorator, SnackBarDecorator,
(Story) => { (Story) => {
return ( return (
<TimelineActivityContext.Provider <TimelineActivityContext.Provider value={{ recordId: 'mock-id' }}>
value={{ labelIdentifierValue: 'Mock' }}
>
<Story /> <Story />
</TimelineActivityContext.Provider> </TimelineActivityContext.Provider>
); );

View File

@ -1,4 +1,5 @@
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { isNewViewableRecordLoadingComponentState } from '@/command-menu/pages/record-page/states/isNewViewableRecordLoadingComponentState'; import { isNewViewableRecordLoadingComponentState } from '@/command-menu/pages/record-page/states/isNewViewableRecordLoadingComponentState';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState'; import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
@ -7,6 +8,7 @@ import { ContextStoreComponentInstanceContext } from '@/context-store/states/con
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
@ -74,13 +76,23 @@ export const CommandMenuRecordPage = () => {
{!isNewViewableRecordLoading && ( {!isNewViewableRecordLoading && (
<RecordValueSetterEffect recordId={objectRecordId} /> <RecordValueSetterEffect recordId={objectRecordId} />
)} )}
<RecordShowContainer <TimelineActivityContext.Provider
objectNameSingular={objectNameSingular} value={{
objectRecordId={objectRecordId} recordId: objectRecordId,
loading={false} }}
isInRightDrawer={true} >
isNewRightDrawerItemLoading={isNewViewableRecordLoading} <RecordShowEffect
/> objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
/>
</TimelineActivityContext.Provider>
</RecordFieldValueSelectorContextProvider> </RecordFieldValueSelectorContextProvider>
</StyledRightDrawerRecord> </StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider> </ActionMenuComponentInstanceContext.Provider>

View File

@ -25,7 +25,6 @@ import styled from '@emotion/styled';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { InView, useInView } from 'react-intersection-observer'; import { InView, useInView } from 'react-intersection-observer';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { AnimatedEaseInOut } from 'twenty-ui'; import { AnimatedEaseInOut } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -71,15 +70,7 @@ const StyledBoardCardWrapper = styled.div`
width: 100%; width: 100%;
`; `;
export const RecordBoardCard = ({ export const RecordBoardCard = () => {
isCreating = false,
onCreateSuccess,
position,
}: {
isCreating?: boolean;
onCreateSuccess?: () => void;
position?: 'first' | 'last';
}) => {
const navigate = useNavigateApp(); const navigate = useNavigateApp();
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
@ -134,18 +125,16 @@ export const RecordBoardCard = ({
}; };
const handleCardClick = () => { const handleCardClick = () => {
if (!isCreating) { if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) { openRecordInCommandMenu({
openRecordInCommandMenu({ recordId,
recordId, objectNameSingular,
objectNameSingular, });
}); } else {
} else { navigate(AppPath.RecordShowPage, {
navigate(AppPath.RecordShowPage, { objectNameSingular,
objectNameSingular, objectRecordId: recordId,
objectRecordId: recordId, });
});
}
} }
}; };
@ -166,16 +155,12 @@ export const RecordBoardCard = ({
(boardField) => !boardField.isLabelIdentifier, (boardField) => !boardField.isLabelIdentifier,
); );
const labelIdentifierField = visibleFieldDefinitions.find(
(field) => field.isLabelIdentifier,
);
return ( return (
<StyledBoardCardWrapper <StyledBoardCardWrapper
className="record-board-card" className="record-board-card"
onContextMenu={handleActionMenuDropdown} onContextMenu={handleActionMenuDropdown}
> >
{!isCreating && <RecordValueSetterEffect recordId={recordId} />} <RecordValueSetterEffect recordId={recordId} />
<InView> <InView>
<StyledBoardCard <StyledBoardCard
ref={cardRef} ref={cardRef}
@ -183,16 +168,10 @@ export const RecordBoardCard = ({
onMouseLeave={onMouseLeaveBoard} onMouseLeave={onMouseLeaveBoard}
onClick={handleCardClick} onClick={handleCardClick}
> >
{isDefined(labelIdentifierField) && ( <RecordBoardCardHeader
<RecordBoardCardHeader isCardExpanded={isCardExpanded}
identifierFieldDefinition={labelIdentifierField} setIsCardExpanded={setIsCardExpanded}
isCreating={isCreating} />
onCreateSuccess={onCreateSuccess}
position={position}
isCardExpanded={isCardExpanded}
setIsCardExpanded={setIsCardExpanded}
/>
)}
<AnimatedEaseInOut <AnimatedEaseInOut
isOpen={isCardExpanded || !isCompactModeActive} isOpen={isCardExpanded || !isCompactModeActive}
initial={false} initial={false}

View File

@ -1,18 +1,18 @@
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCardBodyContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBodyContainer';
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition'; import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { import {
FieldContext, FieldContext,
RecordUpdateHook, RecordUpdateHook,
RecordUpdateHookParams, RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext'; } from '@/object-record/record-field/contexts/FieldContext';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon'; import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer'; import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { useContext } from 'react'; import { useContext } from 'react';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCardBodyContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBodyContainer';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
export const RecordBoardCardBody = ({ export const RecordBoardCardBody = ({
fieldDefinitions, fieldDefinitions,
@ -42,8 +42,7 @@ export const RecordBoardCardBody = ({
value={{ value={{
recordId, recordId,
maxWidth: 156, maxWidth: 156,
recoilScopeId: recoilScopeId: `board-card-${recordId}-${fieldDefinition.fieldMetadataId}`,
(recordId || 'new') + fieldDefinition.fieldMetadataId,
isLabelIdentifier: false, isLabelIdentifier: false,
fieldDefinition: { fieldDefinition: {
disableTooltip: false, disableTooltip: false,

View File

@ -4,33 +4,19 @@ import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useR
import { RecordBoardCardHeaderContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeaderContainer'; import { RecordBoardCardHeaderContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeaderContainer';
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer'; import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { TextInput } from '@/ui/input/components/TextInput';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Dispatch, SetStateAction, useContext, useState } from 'react'; import { Dispatch, SetStateAction, useContext } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { import {
@ -42,11 +28,6 @@ import {
LightIconButton, LightIconButton,
} from 'twenty-ui'; } from 'twenty-ui';
const StyledTextInput = styled(TextInput)`
border-radius: ${({ theme }) => theme.border.radius.sm};
width: ${({ theme }) => theme.spacing(53)};
`;
const StyledCompactIconContainer = styled.div` const StyledCompactIconContainer = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
@ -59,38 +40,21 @@ const StyledCheckboxContainer = styled.div`
`; `;
type RecordBoardCardHeaderProps = { type RecordBoardCardHeaderProps = {
isCreating?: boolean;
onCreateSuccess?: () => void;
position?: 'first' | 'last';
identifierFieldDefinition: RecordBoardFieldDefinition<FieldMetadata>;
isCardExpanded?: boolean; isCardExpanded?: boolean;
setIsCardExpanded?: Dispatch<SetStateAction<boolean>>; setIsCardExpanded?: Dispatch<SetStateAction<boolean>>;
}; };
export const RecordBoardCardHeader = ({ export const RecordBoardCardHeader = ({
isCreating = false,
onCreateSuccess,
position,
identifierFieldDefinition,
isCardExpanded, isCardExpanded,
setIsCardExpanded, setIsCardExpanded,
}: RecordBoardCardHeaderProps) => { }: RecordBoardCardHeaderProps) => {
const [newLabelValue, setNewLabelValue] = useState('');
const columnId = useContext(RecordBoardColumnContext)?.columnId;
const { handleBlur, handleInputEnter } = useAddNewCard({
recordPickerComponentInstanceId: `add-new-card-record-picker-column-${columnId}`,
});
const { recordId } = useContext(RecordBoardCardContext); const { recordId } = useContext(RecordBoardCardContext);
const { indexIdentifierUrl } = useRecordIndexContextOrThrow(); const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const record = useRecoilValue(recordStoreFamilyState(recordId)); const record = useRecoilValue(recordStoreFamilyState(recordId));
const { updateOneRecord, objectMetadataItem } = const { objectMetadataItem } = useContext(RecordBoardContext);
useContext(RecordBoardContext);
const recordBoardId = useAvailableScopeIdOrThrow( const recordBoardId = useAvailableScopeIdOrThrow(
RecordBoardScopeInternalContext, RecordBoardScopeInternalContext,
@ -100,11 +64,6 @@ export const RecordBoardCardHeader = ({
isRecordBoardCompactModeActiveComponentState, isRecordBoardCompactModeActiveComponentState,
); );
const isIdentifierEmpty = isFieldValueEmpty({
fieldDefinition: identifierFieldDefinition,
fieldValue: record?.[identifierFieldDefinition.metadata.fieldName],
});
const { checkIfLastUnselectAndCloseDropdown } = const { checkIfLastUnselectAndCloseDropdown } =
useRecordBoardSelection(recordBoardId); useRecordBoardSelection(recordBoardId);
@ -114,112 +73,52 @@ export const RecordBoardCardHeader = ({
recordId, recordId,
); );
const useUpdateOneRecordHook: RecordUpdateHook = () => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
return [updateEntity, { loading: false }];
};
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState); const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
return ( return (
<RecordBoardCardHeaderContainer showCompactView={showCompactView}> <RecordBoardCardHeaderContainer showCompactView={showCompactView}>
<StopPropagationContainer> <StopPropagationContainer>
{isCreating && position !== undefined ? ( {isDefined(record) && (
<RecordInlineCellEditMode> <RecordChip
<StyledTextInput objectNameSingular={objectMetadataItem.nameSingular}
autoFocus record={record}
value={newLabelValue} variant={AvatarChipVariant.Transparent}
onInputEnter={() => maxWidth={150}
handleInputEnter(newLabelValue, position, onCreateSuccess) to={
} recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
onBlur={() => ? indexIdentifierUrl(recordId)
handleBlur(newLabelValue, position, onCreateSuccess) : undefined
} }
onChange={(text: string) => setNewLabelValue(text)} />
placeholder={identifierFieldDefinition.label}
/>
</RecordInlineCellEditMode>
) : isIdentifierEmpty ? (
<FieldContext.Provider
value={{
recordId,
maxWidth: 156,
recoilScopeId:
(isCreating ? 'new' : recordId) +
identifierFieldDefinition.fieldMetadataId,
isLabelIdentifier: true,
fieldDefinition: {
disableTooltip: false,
fieldMetadataId: identifierFieldDefinition.fieldMetadataId,
label: `Set ${identifierFieldDefinition.label}`,
iconName: identifierFieldDefinition.iconName,
type: identifierFieldDefinition.type,
metadata: identifierFieldDefinition.metadata,
defaultValue: identifierFieldDefinition.defaultValue,
editButtonIcon: getFieldButtonIcon({
metadata: identifierFieldDefinition.metadata,
type: identifierFieldDefinition.type,
}),
},
useUpdateRecord: useUpdateOneRecordHook,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell />
</FieldContext.Provider>
) : (
isDefined(record) && (
<RecordChip
objectNameSingular={objectMetadataItem.nameSingular}
record={record}
variant={AvatarChipVariant.Transparent}
maxWidth={150}
to={
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
? indexIdentifierUrl(recordId)
: undefined
}
/>
)
)} )}
</StopPropagationContainer> </StopPropagationContainer>
{!isCreating && ( {showCompactView && (
<> <StyledCompactIconContainer className="compact-icon-container">
{showCompactView && ( <StopPropagationContainer>
<StyledCompactIconContainer className="compact-icon-container"> <LightIconButton
<StopPropagationContainer> Icon={isCardExpanded ? IconEyeOff : IconEye}
<LightIconButton accent="tertiary"
Icon={isCardExpanded ? IconEyeOff : IconEye} onClick={() => {
accent="tertiary" setIsCardExpanded?.((prev) => !prev);
onClick={() => { }}
setIsCardExpanded?.((prev) => !prev); />
}} </StopPropagationContainer>
/> </StyledCompactIconContainer>
</StopPropagationContainer>
</StyledCompactIconContainer>
)}
<StyledCheckboxContainer className="checkbox-container">
<StopPropagationContainer>
<Checkbox
hoverable
checked={isCurrentCardSelected}
onChange={() => {
setIsCurrentCardSelected(!isCurrentCardSelected);
checkIfLastUnselectAndCloseDropdown();
}}
variant={CheckboxVariant.Secondary}
/>
</StopPropagationContainer>
</StyledCheckboxContainer>
</>
)} )}
<StyledCheckboxContainer className="checkbox-container">
<StopPropagationContainer>
<Checkbox
hoverable
checked={isCurrentCardSelected}
onChange={() => {
setIsCurrentCardSelected(!isCurrentCardSelected);
checkIfLastUnselectAndCloseDropdown();
}}
variant={CheckboxVariant.Secondary}
/>
</StopPropagationContainer>
</StyledCheckboxContainer>
</RecordBoardCardHeaderContainer> </RecordBoardCardHeaderContainer>
); );
}; };

View File

@ -3,16 +3,11 @@ import { Draggable, DroppableProvided } from '@hello-pangea/dnd';
import { useContext } from 'react'; import { useContext } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader'; import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader';
import { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo'; import { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo';
import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader'; import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader';
import { RecordBoardColumnNewOpportunity } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity';
import { RecordBoardColumnNewRecord } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecord';
import { RecordBoardColumnNewRecordButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton'; import { RecordBoardColumnNewRecordButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading'; import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading';
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
@ -50,7 +45,6 @@ export const RecordBoardColumnCardsContainer = ({
droppableProvided, droppableProvided,
}: RecordBoardColumnCardsContainerProps) => { }: RecordBoardColumnCardsContainerProps) => {
const { columnDefinition } = useContext(RecordBoardColumnContext); const { columnDefinition } = useContext(RecordBoardColumnContext);
const { objectMetadataItem } = useContext(RecordBoardContext);
const columnId = columnDefinition.id; const columnId = columnDefinition.id;
@ -68,42 +62,12 @@ export const RecordBoardColumnCardsContainer = ({
isRecordBoardCompactModeActiveComponentState, isRecordBoardCompactModeActiveComponentState,
); );
const { isOpportunitiesCompanyFieldDisabled } =
useIsOpportunitiesCompanyFieldDisabled();
return ( return (
<StyledColumnCardsContainer <StyledColumnCardsContainer
ref={droppableProvided?.innerRef} ref={droppableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...droppableProvided?.droppableProps} {...droppableProvided?.droppableProps}
> >
<Draggable
draggableId={`new-${columnDefinition.id}-top`}
index={-1}
isDragDisabled={true}
>
{(draggableProvided) => (
<div
ref={draggableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
>
{objectMetadataItem.nameSingular ===
CoreObjectNameSingular.Opportunity &&
!isOpportunitiesCompanyFieldDisabled ? (
<RecordBoardColumnNewOpportunity
columnId={columnDefinition.id}
position="first"
/>
) : (
<RecordBoardColumnNewRecord
columnId={columnDefinition.id}
position="first"
/>
)}
</div>
)}
</Draggable>
{isRecordIndexBoardColumnLoading ? ( {isRecordIndexBoardColumnLoading ? (
Array.from( Array.from(
{ {
@ -138,23 +102,8 @@ export const RecordBoardColumnCardsContainer = ({
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps} {...draggableProvided?.draggableProps}
> >
{objectMetadataItem.nameSingular ===
CoreObjectNameSingular.Opportunity &&
!isOpportunitiesCompanyFieldDisabled ? (
<RecordBoardColumnNewOpportunity
columnId={columnDefinition.id}
position="last"
/>
) : (
<RecordBoardColumnNewRecord
columnId={columnDefinition.id}
position="last"
/>
)}
<StyledNewButtonContainer> <StyledNewButtonContainer>
<RecordBoardColumnNewRecordButton <RecordBoardColumnNewRecordButton />
columnId={columnDefinition.id}
/>
</StyledNewButtonContainer> </StyledNewButtonContainer>
</div> </div>
)} )}

View File

@ -1,16 +1,14 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu'; import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useAggregateRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn'; import { useAggregateRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn';
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { IconDotsVertical, IconPlus, LightIconButton, Tag } from 'twenty-ui'; import { IconDotsVertical, IconPlus, LightIconButton, Tag } from 'twenty-ui';
@ -69,7 +67,8 @@ export const RecordBoardColumnHeader = () => {
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false); const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
const [isHeaderHovered, setIsHeaderHovered] = useState(false); const [isHeaderHovered, setIsHeaderHovered] = useState(false);
const { objectMetadataItem } = useContext(RecordBoardContext); const { objectMetadataItem, selectFieldMetadataItem } =
useContext(RecordBoardContext);
const { const {
setHotkeyScopeAndMemorizePreviousScope, setHotkeyScopeAndMemorizePreviousScope,
@ -94,18 +93,11 @@ export const RecordBoardColumnHeader = () => {
const { aggregateValue, aggregateLabel } = const { aggregateValue, aggregateLabel } =
useAggregateRecordsForRecordBoardColumn(); useAggregateRecordsForRecordBoardColumn();
const { handleNewButtonClick } = useColumnNewCardActions(
columnDefinition.id ?? '',
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { isOpportunitiesCompanyFieldDisabled } = const { createNewIndexRecord } = useCreateNewIndexRecord({
useIsOpportunitiesCompanyFieldDisabled(); objectMetadataItem: objectMetadataItem,
});
const isOpportunity =
objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity &&
!isOpportunitiesCompanyFieldDisabled;
return ( return (
<StyledColumn> <StyledColumn>
@ -153,7 +145,12 @@ export const RecordBoardColumnHeader = () => {
<LightIconButton <LightIconButton
accent="tertiary" accent="tertiary"
Icon={IconPlus} Icon={IconPlus}
onClick={() => handleNewButtonClick('first', isOpportunity)} onClick={() => {
createNewIndexRecord({
position: 'first',
[selectFieldMetadataItem.name]: columnDefinition.value,
});
}}
/> />
)} )}
</StyledHeaderActions> </StyledHeaderActions>

View File

@ -1,82 +0,0 @@
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { v4 } from 'uuid';
export const RecordBoardColumnNewOpportunity = ({
columnId,
position,
}: {
columnId: string;
position: 'last' | 'first';
}) => {
const newRecord = useRecoilValue(
recordBoardNewRecordByColumnIdSelector({
familyKey: columnId,
scopeId: columnId,
}),
);
const { handleCreateSuccess, handleEntitySelect } = useAddNewCard({
recordPickerComponentInstanceId: `add-new-card-record-picker-column-${columnId}`,
});
const { createOneRecord: createCompany } = useCreateOneRecord({
objectNameSingular: CoreObjectNameSingular.Company,
});
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
);
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const createCompanyOpportunityAndOpenCommandMenu = async (
searchInput?: string,
) => {
const newRecordId = v4();
const createdCompany = await createCompany({
id: newRecordId,
name: searchInput,
});
setViewableRecordId(newRecordId);
setViewableRecordNameSingular(CoreObjectNameSingular.Company);
openRecordInCommandMenu({
recordId: newRecordId,
objectNameSingular: CoreObjectNameSingular.Company,
});
if (isDefined(createdCompany)) {
handleEntitySelect(position, createdCompany);
}
};
return (
<>
{newRecord.isCreating && newRecord.position === position && (
<OverlayContainer>
<SingleRecordPicker
componentInstanceId={`add-new-card-record-picker-column-${columnId}`}
onCancel={() => handleCreateSuccess(position, columnId, false)}
onRecordSelected={(company) =>
company ? handleEntitySelect(position, company) : null
}
objectNameSingular={CoreObjectNameSingular.Company}
onCreate={createCompanyOpportunityAndOpenCommandMenu}
/>
</OverlayContainer>
)}
</>
);
};

View File

@ -1,42 +0,0 @@
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilValue } from 'recoil';
export const RecordBoardColumnNewRecord = ({
columnId,
position,
}: {
columnId: string;
position: 'first' | 'last';
}) => {
const newRecord = useRecoilValue(
recordBoardNewRecordByColumnIdSelector({
familyKey: columnId,
scopeId: columnId,
}),
);
const { handleCreateSuccess } = useAddNewCard({
recordPickerComponentInstanceId: `add-new-card-record-picker-column-${columnId}`,
});
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
if (hasObjectReadOnlyPermission) {
return null;
}
return (
<>
{newRecord.isCreating && newRecord.position === position && (
<RecordBoardCard
isCreating={true}
onCreateSuccess={() => handleCreateSuccess(position)}
position={position}
/>
)}
</>
);
};

View File

@ -1,7 +1,10 @@
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useContext } from 'react';
import { IconPlus } from 'twenty-ui'; import { IconPlus } from 'twenty-ui';
const StyledNewButton = styled.button` const StyledNewButton = styled.button`
@ -21,23 +24,33 @@ const StyledNewButton = styled.button`
} }
`; `;
export const RecordBoardColumnNewRecordButton = ({ export const RecordBoardColumnNewRecordButton = () => {
columnId,
}: {
columnId: string;
}) => {
const theme = useTheme(); const theme = useTheme();
const { handleNewButtonClick } = useColumnNewCardActions(columnId); const { objectMetadataItem, selectFieldMetadataItem } =
useContext(RecordBoardContext);
const { columnDefinition } = useContext(RecordBoardColumnContext);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem: objectMetadataItem,
});
if (hasObjectReadOnlyPermission) { if (hasObjectReadOnlyPermission) {
return null; return null;
} }
return ( return (
<StyledNewButton onClick={() => handleNewButtonClick('last', false)}> <StyledNewButton
onClick={() => {
createNewIndexRecord({
position: 'last',
[selectFieldMetadataItem.name]: columnDefinition.value,
});
}}
>
<IconPlus size={theme.icon.size.md} /> <IconPlus size={theme.icon.size.md} />
New New
</StyledNewButton> </StyledNewButton>

View File

@ -1,235 +0,0 @@
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useCallback, useContext } from 'react';
import { RecoilState, useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared';
import { v4 as uuidv4 } from 'uuid';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type SetFunction = <T>(
recoilVal: RecoilState<T>,
valOrUpdater: T | ((currVal: T) => T),
) => void;
type UseAddNewCardProps = {
recordPickerComponentInstanceId: string;
};
export const useAddNewCard = ({
recordPickerComponentInstanceId,
}: UseAddNewCardProps) => {
const columnContext = useContext(RecordBoardColumnContext);
const { createOneRecord, selectFieldMetadataItem, objectMetadataItem } =
useContext(RecordBoardContext);
const { resetSearchFilter } = useSingleRecordPickerSearch(
recordPickerComponentInstanceId,
);
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const getColumnDefinitionId = useCallback(
(columnId?: string) => {
const columnDefinitionId = columnId || columnContext?.columnDefinition.id;
if (!columnDefinitionId) {
throw new Error('Column ID is required');
}
return columnDefinitionId;
},
[columnContext],
);
const addNewItem = useCallback(
(
set: SetFunction,
columnDefinitionId: string,
position: 'first' | 'last',
isOpportunity: boolean,
) => {
set(
recordBoardNewRecordByColumnIdSelector({
familyKey: columnDefinitionId,
scopeId: columnDefinitionId,
}),
{
id: uuidv4(),
columnId: columnDefinitionId,
isCreating: true,
position,
isOpportunity,
company: null,
},
);
},
[],
);
const createRecord = useCallback(
(
labelValue: string,
position: 'first' | 'last',
isOpportunity: boolean,
company?: SingleRecordPickerRecord,
) => {
if (
(isOpportunity && company !== null) ||
(!isOpportunity && labelValue !== '')
) {
// TODO: Refactor this whole section (Add new card): this should be:
// - simpler
// - piloted by metadata,
// - avoid drill down props, especially internal stuff
// - and follow record table pending record creation logic
let computedLabelIdentifierValue: any = labelValue;
const labelIdentifierField = objectMetadataItem?.fields.find(
(field) =>
field.id === objectMetadataItem.labelIdentifierFieldMetadataId,
);
if (!isDefined(labelIdentifierField)) {
throw new Error('Label identifier field not found');
}
if (labelIdentifierField.type === FieldMetadataType.FULL_NAME) {
computedLabelIdentifierValue = {
firstName: labelValue,
lastName: '',
};
}
createOneRecord({
[selectFieldMetadataItem.name]: columnContext?.columnDefinition.value,
position,
...(isOpportunity
? { companyId: company?.id, name: company?.name }
: {
[labelIdentifierField.name]: computedLabelIdentifierValue,
}),
});
}
},
[
objectMetadataItem?.fields,
objectMetadataItem?.labelIdentifierFieldMetadataId,
createOneRecord,
selectFieldMetadataItem?.name,
columnContext?.columnDefinition?.value,
],
);
const handleAddNewCardClick = useRecoilCallback(
({ set }) =>
(
labelValue: string,
position: 'first' | 'last',
isOpportunity: boolean,
columnId?: string,
): void => {
const columnDefinitionId = getColumnDefinitionId(columnId);
addNewItem(set, columnDefinitionId, position, isOpportunity);
if (isOpportunity) {
setHotkeyScopeAndMemorizePreviousScope(
SingleRecordPickerHotkeyScope.SingleRecordPicker,
);
} else {
createRecord(labelValue, position, isOpportunity);
}
},
[
addNewItem,
createRecord,
getColumnDefinitionId,
setHotkeyScopeAndMemorizePreviousScope,
],
);
const handleCreateSuccess = useRecoilCallback(
({ set }) =>
(
position: 'first' | 'last',
columnId?: string,
isOpportunity = false,
): void => {
const columnDefinitionId = getColumnDefinitionId(columnId);
set(
recordBoardNewRecordByColumnIdSelector({
familyKey: columnDefinitionId,
scopeId: columnDefinitionId,
}),
{
id: '',
columnId: columnDefinitionId,
isCreating: false,
position,
isOpportunity: Boolean(isOpportunity),
company: null,
},
);
resetSearchFilter();
if (isOpportunity === true) {
goBackToPreviousHotkeyScope();
}
},
[getColumnDefinitionId, goBackToPreviousHotkeyScope, resetSearchFilter],
);
const handleCreate = (
labelValue: string,
position: 'first' | 'last',
onCreateSuccess?: () => void,
) => {
if (labelValue.trim() !== '' && position !== undefined) {
handleAddNewCardClick(labelValue.trim(), position, false, '');
onCreateSuccess?.();
}
};
const handleBlur = (
labelValue: string,
position: 'first' | 'last',
onCreateSuccess?: () => void,
) => {
if (labelValue.trim() === '') {
onCreateSuccess?.();
} else {
handleCreate(labelValue, position, onCreateSuccess);
}
};
const handleInputEnter = (
labelValue: string,
position: 'first' | 'last',
onCreateSuccess?: () => void,
) => {
handleCreate(labelValue, position, onCreateSuccess);
};
const handleEntitySelect = useCallback(
(
position: 'first' | 'last',
company: SingleRecordPickerRecord,
columnId?: string,
) => {
const columnDefinitionId = getColumnDefinitionId(columnId);
createRecord('', position, true, company);
handleCreateSuccess(position, columnDefinitionId, true);
},
[createRecord, handleCreateSuccess, getColumnDefinitionId],
);
return {
handleAddNewCardClick,
handleCreateSuccess,
handleBlur,
handleInputEnter,
handleEntitySelect,
};
};

View File

@ -1,18 +0,0 @@
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
export const useColumnNewCardActions = (columnId: string) => {
const { handleAddNewCardClick } = useAddNewCard({
recordPickerComponentInstanceId: `add-new-card-record-picker-column-${columnId}`,
});
const handleNewButtonClick = (
position: 'first' | 'last',
isOpportunity: boolean,
) => {
handleAddNewCardClick('', position, isOpportunity, columnId);
};
return {
handleNewButtonClick,
};
};

View File

@ -1,17 +0,0 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
export const useIsOpportunitiesCompanyFieldDisabled = () => {
const { objectMetadataItem: opportunityMetadataItem } = useObjectMetadataItem(
{
objectNameSingular: CoreObjectNameSingular.Opportunity,
},
);
const isOpportunitiesCompanyFieldDisabled =
!opportunityMetadataItem.fields.find(
(field) => field.name === CoreObjectNameSingular.Company,
)?.isActive || false;
return {
isOpportunitiesCompanyFieldDisabled,
};
};

View File

@ -1,24 +0,0 @@
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
export type NewCard = {
id: string;
columnId: string;
isCreating: boolean;
position: 'first' | 'last';
isOpportunity: boolean;
company: SingleRecordPickerRecord | null;
};
export const recordBoardNewRecordByColumnIdComponentFamilyState =
createComponentFamilyState<NewCard, string>({
key: 'recordBoardNewRecordByColumnIdComponentFamilyState',
defaultValue: {
id: '',
columnId: '',
isCreating: false,
position: 'last',
isOpportunity: false,
company: null,
},
});

View File

@ -1,31 +0,0 @@
import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector';
import {
NewCard,
recordBoardNewRecordByColumnIdComponentFamilyState,
} from '../recordBoardNewRecordByColumnIdComponentFamilyState';
export const recordBoardNewRecordByColumnIdSelector =
createComponentFamilySelector<NewCard, string>({
key: 'recordBoardNewRecordByColumnIdSelector',
get:
({ familyKey, scopeId }: { familyKey: string; scopeId: string }) =>
({ get }) => {
return get(
recordBoardNewRecordByColumnIdComponentFamilyState({
familyKey,
scopeId,
}),
) as NewCard;
},
set:
({ familyKey, scopeId }: { familyKey: string; scopeId: string }) =>
({ set }, newValue) => {
set(
recordBoardNewRecordByColumnIdComponentFamilyState({
familyKey,
scopeId,
}),
newValue as NewCard,
);
},
});

View File

@ -2,11 +2,12 @@ import { useContext } from 'react';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import { FieldContext } from '../contexts/FieldContext'; import { FieldContext } from '../contexts/FieldContext';
import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly'; import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly';
@ -15,8 +16,11 @@ export const useIsFieldValueReadOnly = () => {
const { metadata, type } = fieldDefinition; const { metadata, type } = fieldDefinition;
const recordFromStore = useRecoilValue<ObjectRecord | null>( const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
recordStoreFamilyState(recordId), recordStoreFamilySelector({
recordId,
fieldName: 'deletedAt',
}),
); );
const contextStoreCurrentViewType = useRecoilComponentValueV2( const contextStoreCurrentViewType = useRecoilComponentValueV2(
@ -34,7 +38,7 @@ export const useIsFieldValueReadOnly = () => {
fieldName: metadata.fieldName, fieldName: metadata.fieldName,
fieldType: type, fieldType: type,
isObjectRemote: objectMetadataItem.isRemote, isObjectRemote: objectMetadataItem.isRemote,
isRecordDeleted: recordFromStore?.deletedAt, isRecordDeleted: isDefined(recordDeletedAt),
hasObjectReadOnlyPermission, hasObjectReadOnlyPermission,
contextStoreCurrentViewType, contextStoreCurrentViewType,
}); });

View File

@ -18,7 +18,7 @@ export const RelationFromManyFieldDisplay = () => {
fieldDefinition?.metadata.relationObjectMetadataNameSingular; fieldDefinition?.metadata.relationObjectMetadataNameSingular;
const { activityTargetObjectRecords } = useActivityTargetObjectRecords( const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
undefined, '',
fieldValue as NoteTarget[] | TaskTarget[], fieldValue as NoteTarget[] | TaskTarget[],
); );

View File

@ -3,7 +3,7 @@ import { availableRecordGroupIdsComponentSelector } from '@/object-record/record
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useCreateNewTableRecordInGroup } from '@/object-record/record-table/hooks/useCreateNewTableRecordInGroup'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState'; import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -44,23 +44,31 @@ export const RecordIndexAddRecordInGroupDropdown = ({
(field) => field.id === recordGroupFieldMetadata?.id, (field) => field.id === recordGroupFieldMetadata?.id,
); );
const { createNewTableRecordInGroup } = useCreateNewTableRecordInGroup();
const { closeDropdown } = useDropdown(dropdownId); const { closeDropdown } = useDropdown(dropdownId);
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
});
const handleCreateNewTableRecordInGroup = useRecoilCallback( const handleCreateNewTableRecordInGroup = useRecoilCallback(
({ set }) => ({ set }) =>
(recordGroup: RecordGroupDefinition) => { (recordGroup: RecordGroupDefinition) => {
set(isRecordGroupTableSectionToggledState(recordGroup.id), true); set(isRecordGroupTableSectionToggledState(recordGroup.id), true);
createNewTableRecordInGroup(recordGroup.id);
setActiveDropdownFocusIdAndMemorizePrevious(null); setActiveDropdownFocusIdAndMemorizePrevious(null);
if (!selectFieldMetadataItem) {
return;
}
createNewIndexRecord({
[selectFieldMetadataItem.name]: recordGroup.value,
});
closeDropdown(); closeDropdown();
}, },
[ [
closeDropdown,
createNewTableRecordInGroup,
setActiveDropdownFocusIdAndMemorizePrevious,
isRecordGroupTableSectionToggledState, isRecordGroupTableSectionToggledState,
setActiveDropdownFocusIdAndMemorizePrevious,
selectFieldMetadataItem,
createNewIndexRecord,
closeDropdown,
], ],
); );

View File

@ -1,89 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
export const RecordIndexPageKanbanAddButton = () => {
const dropdownId = `record-index-page-add-button-dropdown`;
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentFamilySelector,
ViewType.Kanban,
);
const recordIndexKanbanFieldMetadataId = useRecoilValue(
recordIndexKanbanFieldMetadataIdState,
);
const selectFieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.id === recordIndexKanbanFieldMetadataId,
);
const isOpportunity =
objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity;
const { closeDropdown } = useDropdown(dropdownId);
const { isOpportunitiesCompanyFieldDisabled } =
useIsOpportunitiesCompanyFieldDisabled();
const { handleAddNewCardClick } = useAddNewCard({
recordPickerComponentInstanceId: `add-new-card-record-picker`,
});
const handleItemClick = useCallback(
(columnDefinition: RecordGroupDefinition) => {
const isOpportunityEnabled =
isOpportunity && !isOpportunitiesCompanyFieldDisabled;
handleAddNewCardClick(
'',
'first',
isOpportunityEnabled,
columnDefinition.id,
);
closeDropdown();
},
[
isOpportunity,
handleAddNewCardClick,
closeDropdown,
isOpportunitiesCompanyFieldDisabled,
],
);
if (!selectFieldMetadataItem) {
return null;
}
return (
<Dropdown
dropdownMenuWidth="200px"
dropdownPlacement="bottom-start"
clickableComponent={<PageAddButton />}
dropdownId={dropdownId}
dropdownComponents={
<DropdownMenuItemsContainer>
{visibleRecordGroupIds.map((recordGroupId) => (
<RecordIndexPageKanbanAddMenuItem
key={recordGroupId}
columnId={recordGroupId}
onItemClick={handleItemClick}
/>
))}
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownId }}
/>
);
};

View File

@ -6,11 +6,9 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider'; import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon'; import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { useInlineCell } from '../hooks/useInlineCell'; import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode'; import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput'; import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
@ -18,6 +16,7 @@ import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinit
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope'; import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope'; import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
@ -28,7 +27,6 @@ import {
RecordInlineCellContext, RecordInlineCellContext,
RecordInlineCellContextProps, RecordInlineCellContextProps,
} from './RecordInlineCellContext'; } from './RecordInlineCellContext';
type RecordInlineCellProps = { type RecordInlineCellProps = {
readonly?: boolean; readonly?: boolean;
loading?: boolean; loading?: boolean;

View File

@ -1,82 +0,0 @@
import { useRecoilValue } from 'recoil';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RIGHT_DRAWER_RECORD_INSTANCE_ID } from '@/object-record/record-right-drawer/constants/RightDrawerRecordInstanceId';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import styled from '@emotion/styled';
const StyledRightDrawerRecord = styled.div<{ isMobile: boolean }>`
height: ${({ theme, isMobile }) =>
isMobile ? `calc(100% - ${theme.spacing(16)})` : '100%'};
`;
export const RightDrawerRecord = () => {
const isMobile = useIsMobile();
const viewableRecordNameSingular = useRecoilValue(
viewableRecordNameSingularState,
);
const isNewViewableRecordLoading = useRecoilValue(
isNewViewableRecordLoadingState,
);
const viewableRecordId = useRecoilValue(viewableRecordIdState);
if (!viewableRecordNameSingular && !isNewViewableRecordLoading) {
throw new Error(`Object name is not defined`);
}
const { objectNameSingular, objectRecordId } = useRecordShowPage(
viewableRecordNameSingular ?? '',
viewableRecordId ?? '',
);
return (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID }}
>
<StyledRightDrawerRecord isMobile={isMobile}>
<RecordFieldValueSelectorContextProvider>
{!isNewViewableRecordLoading && (
<RecordValueSetterEffect recordId={objectRecordId} />
)}
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
/>
</RecordFieldValueSelectorContextProvider>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
);
};

View File

@ -1,36 +0,0 @@
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
// TODO: this should be better implemented if we refactor field input so that it's easier to implement logic like that
// Idea : maybe we could use draftValue instead of the newValue in the events
// Idea : we can remove all our listeners in the many input types and replace them with a onClose event that gives the event type
// (tab, shift-tab, click-outside, escape, enter) and the newValue, that will reduce the boilerplate
// and also the need to have our difficult to understand persist logic
// the goal would be that we could easily call usePersistField anywhere under a FieldContext and it would work
export const RightDrawerTitleRecordInlineCell = () => {
const { closeInlineCell } = useInlineCell();
const persistField = usePersistField();
const { recordId, fieldDefinition } = useContext(FieldContext);
const { getDraftValueSelector } = useRecordFieldInput<unknown>(
`${recordId}-${fieldDefinition.metadata.fieldName}`,
);
const draftValue = useRecoilValue(getDraftValueSelector());
useListenRightDrawerClose(() => {
if (draftValue !== undefined) {
persistField(draftValue);
}
closeInlineCell();
});
return <RecordInlineCell />;
};

View File

@ -1 +0,0 @@
export const RIGHT_DRAWER_RECORD_INSTANCE_ID = 'right-drawer-record';

View File

@ -1,8 +1,8 @@
import groupBy from 'lodash.groupby'; import groupBy from 'lodash.groupby';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { Note } from '@/activities/types/Note'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { Task } from '@/activities/types/Task'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -15,7 +15,6 @@ import { useRecordShowContainerData } from '@/object-record/record-show/hooks/us
import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection'; import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection';
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection'; import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported'; import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { isDefined } from 'twenty-shared';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';
type FieldsCardProps = { type FieldsCardProps = {
@ -27,22 +26,20 @@ export const FieldsCard = ({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
}: FieldsCardProps) => { }: FieldsCardProps) => {
const { const { recordLoading, labelIdentifierFieldMetadataItem, isPrefetchLoading } =
recordFromStore, useRecordShowContainerData({
recordLoading, objectNameSingular,
objectMetadataItem, objectRecordId,
labelIdentifierFieldMetadataItem, });
isPrefetchLoading,
objectMetadataItems, const { objectMetadataItem } = useObjectMetadataItem({
} = useRecordShowContainerData({
objectNameSingular, objectNameSingular,
objectRecordId,
}); });
const { objectMetadataItems } = useObjectMetadataItems();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({ const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
recordFromStore,
}); });
const availableFieldMetadataItems = objectMetadataItem.fields const availableFieldMetadataItems = objectMetadataItem.fields
@ -87,100 +84,94 @@ export const FieldsCard = ({
return ( return (
<> <>
{isDefined(recordFromStore) && ( <PropertyBox>
<> {isPrefetchLoading ? (
<PropertyBox> <PropertyBoxSkeletonLoader />
{isPrefetchLoading ? ( ) : (
<PropertyBoxSkeletonLoader /> <>
) : ( {inlineRelationFieldMetadataItems?.map(
<> (fieldMetadataItem, index) => (
{inlineRelationFieldMetadataItems?.map( <FieldContext.Provider
(fieldMetadataItem, index) => ( key={objectRecordId + fieldMetadataItem.id}
<FieldContext.Provider value={{
key={objectRecordId + fieldMetadataItem.id} recordId: objectRecordId,
value={{ maxWidth: 200,
recordId: objectRecordId, recoilScopeId: objectRecordId + fieldMetadataItem.id,
maxWidth: 200, isLabelIdentifier: false,
recoilScopeId: objectRecordId + fieldMetadataItem.id, fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
isLabelIdentifier: false, field: fieldMetadataItem,
fieldDefinition: position: index,
formatFieldMetadataItemAsColumnDefinition({ objectMetadataItem,
field: fieldMetadataItem, showLabel: true,
position: index, labelWidth: 90,
objectMetadataItem, }),
showLabel: true, useUpdateRecord: useUpdateOneObjectRecordMutation,
labelWidth: 90, hotkeyScope: InlineCellHotkeyScope.InlineCell,
}), }}
useUpdateRecord: useUpdateOneObjectRecordMutation, >
hotkeyScope: InlineCellHotkeyScope.InlineCell, <ActivityTargetsInlineCell
}} activityObjectNameSingular={
> objectNameSingular as
<ActivityTargetsInlineCell | CoreObjectNameSingular.Note
activityObjectNameSingular={ | CoreObjectNameSingular.Task
objectNameSingular as }
| CoreObjectNameSingular.Note activityRecordId={objectRecordId}
| CoreObjectNameSingular.Task showLabel={true}
} maxWidth={200}
activity={recordFromStore as Task | Note} />
showLabel={true} </FieldContext.Provider>
maxWidth={200} ),
/>
</FieldContext.Provider>
),
)}
{inlineFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
maxWidth: 200,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition:
formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell loading={recordLoading} />
</FieldContext.Provider>
))}
</>
)} )}
</PropertyBox> {inlineFieldMetadataItems?.map((fieldMetadataItem, index) => (
<RecordDetailDuplicatesSection <FieldContext.Provider
objectRecordId={objectRecordId} key={objectRecordId + fieldMetadataItem.id}
objectNameSingular={objectNameSingular} value={{
recordId: objectRecordId,
maxWidth: 200,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell loading={recordLoading} />
</FieldContext.Provider>
))}
</>
)}
</PropertyBox>
<RecordDetailDuplicatesSection
objectRecordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
{boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordDetailRelationSection
loading={isPrefetchLoading || recordLoading}
/> />
{boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => ( </FieldContext.Provider>
<FieldContext.Provider ))}
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordDetailRelationSection
loading={isPrefetchLoading || recordLoading}
/>
</FieldContext.Provider>
))}
</>
)}
</> </>
); );
}; };

View File

@ -41,7 +41,7 @@ export const ObjectRecordShowPageBreadcrumb = ({
objectLabelPlural: string; objectLabelPlural: string;
labelIdentifierFieldMetadataItem?: FieldMetadataItem; labelIdentifierFieldMetadataItem?: FieldMetadataItem;
}) => { }) => {
const { record, loading } = useFindOneRecord({ const { loading } = useFindOneRecord({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
recordGqlFields: { recordGqlFields: {
@ -52,7 +52,6 @@ export const ObjectRecordShowPageBreadcrumb = ({
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({ const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
recordFromStore: record ?? null,
}); });
if (loading) { if (loading) {

View File

@ -3,10 +3,13 @@ import { ShowPageContainer } from '@/ui/layout/page/components/ShowPageContainer
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord'; import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreTargetedRecordsEffect'; import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreTargetedRecordsEffect';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData'; import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs'; import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { ShowPageSubContainer } from '@/ui/layout/show-page/components/ShowPageSubContainer'; import { ShowPageSubContainer } from '@/ui/layout/show-page/components/ShowPageSubContainer';
import { useRecoilValue } from 'recoil';
type RecordShowContainerProps = { type RecordShowContainerProps = {
objectNameSingular: string; objectNameSingular: string;
@ -23,16 +26,22 @@ export const RecordShowContainer = ({
isInRightDrawer = false, isInRightDrawer = false,
isNewRightDrawerItemLoading = false, isNewRightDrawerItemLoading = false,
}: RecordShowContainerProps) => { }: RecordShowContainerProps) => {
const { const { objectMetadataItem } = useObjectMetadataItem({
recordFromStore, objectNameSingular,
objectMetadataItem, });
isPrefetchLoading,
recordLoading, const { isPrefetchLoading, recordLoading } = useRecordShowContainerData({
} = useRecordShowContainerData({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
}); });
const recordDeletedAt = useRecoilValue<string | null>(
recordStoreFamilySelector({
recordId: objectRecordId,
fieldName: 'deletedAt',
}),
);
const { layout, tabs } = useRecordShowContainerTabs( const { layout, tabs } = useRecordShowContainerTabs(
loading, loading,
objectNameSingular as CoreObjectNameSingular, objectNameSingular as CoreObjectNameSingular,
@ -45,7 +54,7 @@ export const RecordShowContainer = ({
<RecordShowContainerContextStoreTargetedRecordsEffect <RecordShowContainerContextStoreTargetedRecordsEffect
recordId={objectRecordId} recordId={objectRecordId}
/> />
{recordFromStore && recordFromStore.deletedAt && ( {recordDeletedAt && (
<InformationBannerDeletedRecord <InformationBannerDeletedRecord
recordId={objectRecordId} recordId={objectRecordId}
objectNameSingular={objectNameSingular} objectNameSingular={objectNameSingular}

View File

@ -0,0 +1,47 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { buildFindOneRecordForShowPageOperationSignature } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type RecordShowEffectProps = {
objectNameSingular: string;
recordId: string;
};
export const RecordShowEffect = ({
objectNameSingular,
recordId,
}: RecordShowEffectProps) => {
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const { objectMetadataItems } = useObjectMetadataItems();
const FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE =
buildFindOneRecordForShowPageOperationSignature({
objectMetadataItem,
objectMetadataItems,
});
const { record } = useFindOneRecord({
objectRecordId: recordId,
objectNameSingular,
recordGqlFields: FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE.fields,
withSoftDeleted: true,
});
const [recordFromStore, setRecordFromStore] = useRecoilState(
recordStoreFamilyState(recordId),
);
useEffect(() => {
if (isDefined(record) && !isDeeplyEqual(record, recordFromStore)) {
setRecordFromStore(record);
}
}, [record, recordFromStore, setRecordFromStore]);
return <></>;
};

View File

@ -4,10 +4,13 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions'; import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData'; import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell'; import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard'; import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader'; import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';
@ -25,28 +28,36 @@ export const SummaryCard = ({
isNewRightDrawerItemLoading, isNewRightDrawerItemLoading,
isInRightDrawer, isInRightDrawer,
}: SummaryCardProps) => { }: SummaryCardProps) => {
const { const { recordLoading, labelIdentifierFieldMetadataItem, isPrefetchLoading } =
recordFromStore, useRecordShowContainerData({
recordLoading, objectNameSingular,
labelIdentifierFieldMetadataItem, objectRecordId,
isPrefetchLoading, });
recordIdentifier,
} = useRecordShowContainerData({ const recordCreatedAt = useRecoilValue<string | null>(
objectNameSingular, recordStoreFamilySelector({
objectRecordId, recordId: objectRecordId,
}); fieldName: 'createdAt',
}),
);
const { onUploadPicture, useUpdateOneObjectRecordMutation } = const { onUploadPicture, useUpdateOneObjectRecordMutation } =
useRecordShowContainerActions({ useRecordShowContainerActions({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
recordFromStore,
}); });
const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular); const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular);
const isMobile = useIsMobile() || isInRightDrawer; const isMobile = useIsMobile() || isInRightDrawer;
if (isNewRightDrawerItemLoading || !isDefined(recordFromStore)) { const recordIdentifier = useRecoilValue(
recordStoreIdentifierFamilySelector({
objectNameSingular,
recordId: objectRecordId,
}),
);
if (isNewRightDrawerItemLoading || !isDefined(recordCreatedAt)) {
return <ShowPageSummaryCardSkeletonLoader />; return <ShowPageSummaryCardSkeletonLoader />;
} }
@ -58,7 +69,7 @@ export const SummaryCard = ({
icon={Icon} icon={Icon}
iconColor={IconColor} iconColor={IconColor}
avatarPlaceholder={recordIdentifier?.name ?? ''} avatarPlaceholder={recordIdentifier?.name ?? ''}
date={recordFromStore.createdAt ?? ''} date={recordCreatedAt ?? ''}
loading={isPrefetchLoading || recordLoading} loading={isPrefetchLoading || recordLoading}
title={ title={
<FieldContext.Provider <FieldContext.Provider

View File

@ -3,7 +3,6 @@ import {
RecordUpdateHook, RecordUpdateHook,
RecordUpdateHookParams, RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext'; } from '@/object-record/record-field/contexts/FieldContext';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FileFolder } from '~/generated-metadata/graphql'; import { FileFolder } from '~/generated-metadata/graphql';
import { useUploadImageMutation } from '~/generated/graphql'; import { useUploadImageMutation } from '~/generated/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -11,13 +10,11 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
interface UseRecordShowContainerActionsProps { interface UseRecordShowContainerActionsProps {
objectNameSingular: string; objectNameSingular: string;
objectRecordId: string; objectRecordId: string;
recordFromStore: ObjectRecord | null;
} }
export const useRecordShowContainerActions = ({ export const useRecordShowContainerActions = ({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
recordFromStore,
}: UseRecordShowContainerActionsProps) => { }: UseRecordShowContainerActionsProps) => {
const [uploadImage] = useUploadImageMutation(); const [uploadImage] = useUploadImageMutation();
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular }); const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
@ -47,7 +44,7 @@ export const useRecordShowContainerActions = ({
const avatarUrl = result?.data?.uploadImage; const avatarUrl = result?.data?.uploadImage;
if (!avatarUrl || isUndefinedOrNull(updateOneRecord) || !recordFromStore) { if (!avatarUrl || isUndefinedOrNull(updateOneRecord)) {
return; return;
} }

View File

@ -1,12 +1,7 @@
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem'; import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState'; import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState } from 'recoil';
type UseRecordShowContainerDataProps = { type UseRecordShowContainerDataProps = {
objectNameSingular: string; objectNameSingular: string;
@ -17,10 +12,6 @@ export const useRecordShowContainerData = ({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
}: UseRecordShowContainerDataProps) => { }: UseRecordShowContainerDataProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { labelIdentifierFieldMetadataItem } = const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({ useLabelIdentifierFieldMetadataItem({
objectNameSingular, objectNameSingular,
@ -30,28 +21,11 @@ export const useRecordShowContainerData = ({
recordLoadingFamilyState(objectRecordId), recordLoadingFamilyState(objectRecordId),
); );
const [recordFromStore] = useRecoilState<ObjectRecord | null>(
recordStoreFamilyState(objectRecordId),
);
const recordIdentifier = useRecoilValue(
recordStoreIdentifierFamilySelector({
objectNameSingular,
recordId: objectRecordId,
}),
);
const isPrefetchLoading = useIsPrefetchLoading(); const isPrefetchLoading = useIsPrefetchLoading();
const { objectMetadataItems } = useObjectMetadataItems();
return { return {
recordFromStore,
recordLoading, recordLoading,
objectMetadataItem,
labelIdentifierFieldMetadataItem, labelIdentifierFieldMetadataItem,
isPrefetchLoading, isPrefetchLoading,
recordIdentifier,
objectMetadataItems,
}; };
}; };

View File

@ -8,14 +8,15 @@ import { RecordLayout } from '@/object-record/record-show/types/RecordLayout';
import { SingleTabProps } from '@/ui/layout/tab/components/TabList'; import { SingleTabProps } from '@/ui/layout/tab/components/TabList';
import { RecordLayoutTab } from '@/ui/layout/tab/types/RecordLayoutTab'; import { RecordLayoutTab } from '@/ui/layout/tab/types/RecordLayoutTab';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { import {
IconCalendarEvent, IconCalendarEvent,
IconHome,
IconMail, IconMail,
IconNotes, IconNotes,
IconPrinter, IconPrinter,
IconSettings, IconSettings,
IconHome,
} from 'twenty-ui'; } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql'; import { FeatureFlagKey } from '~/generated/graphql';
@ -34,195 +35,200 @@ export const useRecordShowContainerTabs = (
// Object-specific layouts that override or extend the base layout // Object-specific layouts that override or extend the base layout
const OBJECT_SPECIFIC_LAYOUTS: Partial< const OBJECT_SPECIFIC_LAYOUTS: Partial<
Record<CoreObjectNameSingular, RecordLayout> Record<CoreObjectNameSingular, RecordLayout>
> = { > = useMemo(
[CoreObjectNameSingular.Note]: { () => ({
tabs: { [CoreObjectNameSingular.Note]: {
richText: { tabs: {
title: 'Note', richText: {
position: 101, title: 'Note',
Icon: IconNotes, position: 101,
cards: [{ type: CardType.RichTextCard }], Icon: IconNotes,
hide: { cards: [{ type: CardType.RichTextCard }],
ifMobile: false, hide: {
ifDesktop: false, ifMobile: false,
ifInRightDrawer: false, ifDesktop: false,
ifFeaturesDisabled: [], ifInRightDrawer: false,
ifRequiredObjectsInactive: [], ifFeaturesDisabled: [],
ifRelationsMissing: [], ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
}, },
tasks: null,
notes: null,
}, },
tasks: null,
notes: null,
}, },
}, [CoreObjectNameSingular.Task]: {
[CoreObjectNameSingular.Task]: { tabs: {
tabs: { richText: {
richText: { title: 'Note',
title: 'Note', position: 101,
position: 101, Icon: IconNotes,
Icon: IconNotes, cards: [{ type: CardType.RichTextCard }],
cards: [{ type: CardType.RichTextCard }], hide: {
hide: { ifMobile: false,
ifMobile: false, ifDesktop: false,
ifDesktop: false, ifInRightDrawer: false,
ifInRightDrawer: false, ifFeaturesDisabled: [],
ifFeaturesDisabled: [], ifRequiredObjectsInactive: [],
ifRequiredObjectsInactive: [], ifRelationsMissing: [],
ifRelationsMissing: [], },
}, },
tasks: null,
notes: null,
}, },
tasks: null,
notes: null,
}, },
}, [CoreObjectNameSingular.Company]: {
[CoreObjectNameSingular.Company]: { tabs: {
tabs: { emails: {
emails: { title: 'Emails',
title: 'Emails', position: 600,
position: 600, Icon: IconMail,
Icon: IconMail, cards: [{ type: CardType.EmailCard }],
cards: [{ type: CardType.EmailCard }], hide: {
hide: { ifMobile: false,
ifMobile: false, ifDesktop: false,
ifDesktop: false, ifInRightDrawer: false,
ifInRightDrawer: false, ifFeaturesDisabled: [],
ifFeaturesDisabled: [], ifRequiredObjectsInactive: [],
ifRequiredObjectsInactive: [], ifRelationsMissing: [],
ifRelationsMissing: [], },
}, },
}, calendar: {
calendar: { title: 'Calendar',
title: 'Calendar', position: 700,
position: 700, Icon: IconCalendarEvent,
Icon: IconCalendarEvent, cards: [{ type: CardType.CalendarCard }],
cards: [{ type: CardType.CalendarCard }], hide: {
hide: { ifMobile: false,
ifMobile: false, ifDesktop: false,
ifDesktop: false, ifInRightDrawer: false,
ifInRightDrawer: false, ifFeaturesDisabled: [],
ifFeaturesDisabled: [], ifRequiredObjectsInactive: [],
ifRequiredObjectsInactive: [], ifRelationsMissing: [],
ifRelationsMissing: [], },
}, },
}, },
}, },
}, [CoreObjectNameSingular.Person]: {
[CoreObjectNameSingular.Person]: { tabs: {
tabs: { emails: {
emails: { title: 'Emails',
title: 'Emails', position: 600,
position: 600, Icon: IconMail,
Icon: IconMail, cards: [{ type: CardType.EmailCard }],
cards: [{ type: CardType.EmailCard }], hide: {
hide: { ifMobile: false,
ifMobile: false, ifDesktop: false,
ifDesktop: false, ifInRightDrawer: false,
ifInRightDrawer: false, ifFeaturesDisabled: [],
ifFeaturesDisabled: [], ifRequiredObjectsInactive: [],
ifRequiredObjectsInactive: [], ifRelationsMissing: [],
ifRelationsMissing: [], },
}, },
}, calendar: {
calendar: { title: 'Calendar',
title: 'Calendar', position: 700,
position: 700, Icon: IconCalendarEvent,
Icon: IconCalendarEvent, cards: [{ type: CardType.CalendarCard }],
cards: [{ type: CardType.CalendarCard }], hide: {
hide: { ifMobile: false,
ifMobile: false, ifDesktop: false,
ifDesktop: false, ifInRightDrawer: false,
ifInRightDrawer: false, ifFeaturesDisabled: [],
ifFeaturesDisabled: [], ifRequiredObjectsInactive: [],
ifRequiredObjectsInactive: [], ifRelationsMissing: [],
ifRelationsMissing: [], },
}, },
}, },
}, },
}, [CoreObjectNameSingular.Workflow]: {
[CoreObjectNameSingular.Workflow]: { hideSummaryAndFields: true,
hideSummaryAndFields: true, tabs: {
tabs: { workflow: {
workflow: { title: 'Flow',
title: 'Flow', position: 0,
position: 0, Icon: IconSettings,
Icon: IconSettings, cards: [{ type: CardType.WorkflowCard }],
cards: [{ type: CardType.WorkflowCard }], hide: {
hide: { ifMobile: false,
ifMobile: false, ifDesktop: false,
ifDesktop: false, ifInRightDrawer: false,
ifInRightDrawer: false, ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled], ifRequiredObjectsInactive: [],
ifRequiredObjectsInactive: [], ifRelationsMissing: [],
ifRelationsMissing: [], },
}, },
timeline: null,
fields: null,
}, },
timeline: null,
fields: null,
}, },
}, [CoreObjectNameSingular.WorkflowVersion]: {
[CoreObjectNameSingular.WorkflowVersion]: { tabs: {
tabs: { workflowVersion: {
workflowVersion: { title: 'Flow',
title: 'Flow', position: 0,
position: 0, Icon: IconSettings,
Icon: IconSettings, cards: [{ type: CardType.WorkflowVersionCard }],
cards: [{ type: CardType.WorkflowVersionCard }], hide: {
hide: { ifMobile: false,
ifMobile: false, ifDesktop: false,
ifDesktop: false, ifInRightDrawer: false,
ifInRightDrawer: false, ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled], ifRequiredObjectsInactive: [],
ifRequiredObjectsInactive: [], ifRelationsMissing: [],
ifRelationsMissing: [], },
}, },
timeline: null,
}, },
timeline: null,
}, },
}, [CoreObjectNameSingular.WorkflowRun]: {
[CoreObjectNameSingular.WorkflowRun]: { tabs: {
tabs: { workflowRunOutput: {
workflowRunOutput: { title: 'Output',
title: 'Output', position: 0,
position: 0, Icon: IconPrinter,
Icon: IconPrinter, cards: [{ type: CardType.WorkflowRunOutputCard }],
cards: [{ type: CardType.WorkflowRunOutputCard }], hide: {
hide: { ifMobile: false,
ifMobile: false, ifDesktop: false,
ifDesktop: false, ifInRightDrawer: false,
ifInRightDrawer: false, ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled], ifRequiredObjectsInactive: [],
ifRequiredObjectsInactive: [], ifRelationsMissing: [],
ifRelationsMissing: [], },
}, },
}, workflowRunFlow: {
workflowRunFlow: { title: 'Flow',
title: 'Flow', position: 0,
position: 0, Icon: IconSettings,
Icon: IconSettings, cards: [{ type: CardType.WorkflowRunCard }],
cards: [{ type: CardType.WorkflowRunCard }], hide: {
hide: { ifMobile: false,
ifMobile: false, ifDesktop: false,
ifDesktop: false, ifInRightDrawer: false,
ifInRightDrawer: false, ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled], ifRequiredObjectsInactive: [],
ifRequiredObjectsInactive: [], ifRelationsMissing: [],
ifRelationsMissing: [], },
}, },
timeline: null,
}, },
timeline: null,
}, },
}, }),
}; [],
);
// Merge base layout with object-specific layout // Merge base layout with object-specific layout
const recordLayout: RecordLayout = { const recordLayout: RecordLayout = useMemo(() => {
...BASE_RECORD_LAYOUT, return {
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular] || {}), ...BASE_RECORD_LAYOUT,
tabs: { ...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular] || {}),
...BASE_RECORD_LAYOUT.tabs, tabs: {
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular]?.tabs || {}), ...BASE_RECORD_LAYOUT.tabs,
}, ...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular]?.tabs || {}),
}; },
};
}, [OBJECT_SPECIFIC_LAYOUTS, targetObjectNameSingular]);
return { return {
layout: recordLayout, layout: recordLayout,

View File

@ -1,19 +1,8 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { useIcons } from 'twenty-ui'; import { useIcons } from 'twenty-ui';
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { isDefined } from 'twenty-shared';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { buildFindOneRecordForShowPageOperationSignature } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { capitalize, isDefined } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useRecordShowPage = ( export const useRecordShowPage = (
propsObjectNameSingular: string, propsObjectNameSingular: string,
@ -32,77 +21,13 @@ export const useRecordShowPage = (
} }
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const { objectMetadataItems } = useObjectMetadataItems();
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({ objectNameSingular });
const { sortedFavorites: favorites } = useFavorites();
const { createFavorite } = useCreateFavorite();
const { deleteFavorite } = useDeleteFavorite();
const setEntityFields = useSetRecoilState(
recordStoreFamilyState(objectRecordId),
);
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const headerIcon = getIcon(objectMetadataItem?.icon); const headerIcon = getIcon(objectMetadataItem?.icon);
const FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE =
buildFindOneRecordForShowPageOperationSignature({
objectMetadataItem,
objectMetadataItems,
});
const { record, loading } = useFindOneRecord({
objectRecordId,
objectNameSingular,
recordGqlFields: FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE.fields,
withSoftDeleted: true,
});
useEffect(() => {
if (isDefined(record)) {
setEntityFields(record);
}
}, [record, setEntityFields]);
const correspondingFavorite = favorites.find(
(favorite) => favorite.recordId === objectRecordId,
);
const isFavorite = isDefined(correspondingFavorite);
const handleFavoriteButtonClick = async () => {
if (!objectNameSingular || !record) return;
if (isFavorite) {
deleteFavorite(correspondingFavorite.id);
} else {
createFavorite(record, objectNameSingular);
}
};
const labelIdentifierFieldValue =
record?.[labelIdentifierFieldMetadataItem?.name ?? ''];
const pageName =
labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FULL_NAME
? [
labelIdentifierFieldValue?.firstName,
labelIdentifierFieldValue?.lastName,
].join(' ')
: isDefined(labelIdentifierFieldValue)
? `${labelIdentifierFieldValue}`
: '';
const pageTitle = pageName.trim()
? `${pageName} - ${capitalize(objectNameSingular)}`
: capitalize(objectNameSingular);
return { return {
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
headerIcon, headerIcon,
loading,
pageTitle,
pageName,
isFavorite,
record,
objectMetadataItem, objectMetadataItem,
handleFavoriteButtonClick,
}; };
}; };

View File

@ -16,7 +16,6 @@ import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/
import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody'; import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { hasPendingRecordComponentSelector } from '@/object-record/record-table/states/selectors/hasPendingRecordComponentSelector';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -52,20 +51,13 @@ export const RecordTable = () => {
recordTableId, recordTableId,
); );
const hasPendingRecord = useRecoilComponentValueV2(
hasPendingRecordComponentSelector,
recordTableId,
);
const hasRecordGroups = useRecoilComponentValueV2( const hasRecordGroups = useRecoilComponentValueV2(
hasRecordGroupsComponentSelector, hasRecordGroupsComponentSelector,
recordTableId, recordTableId,
); );
const recordTableIsEmpty = const recordTableIsEmpty =
!isRecordTableInitialLoading && !isRecordTableInitialLoading && allRecordIds.length === 0;
allRecordIds.length === 0 &&
!hasPendingRecord;
const { resetTableRowSelection, setRowSelected } = useRecordTable({ const { resetTableRowSelection, setRowSelected } = useRecordTable({
recordTableId, recordTableId,

View File

@ -1,7 +1,6 @@
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useUpsertTableRecordNoGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup'; import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup';
import { useMoveSoftFocusToCurrentCellOnHover } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover'; import { useMoveSoftFocusToCurrentCellOnHover } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover';
@ -23,20 +22,6 @@ export const RecordTableNoRecordGroupBodyContextProvider = ({
}: RecordTableNoRecordGroupBodyContextProviderProps) => { }: RecordTableNoRecordGroupBodyContextProviderProps) => {
const { recordTableId } = useRecordTableContextOrThrow(); const { recordTableId } = useRecordTableContextOrThrow();
const { upsertTableRecordNoGroup } = useUpsertTableRecordNoGroup();
const handleUpsertTableRecordNoRecordGroup = ({
persistField,
recordId,
fieldName,
}: {
persistField: () => void;
recordId: string;
fieldName: string;
}) => {
upsertTableRecordNoGroup(persistField, recordId, fieldName);
};
const { openTableCell } = useOpenRecordTableCellV2(recordTableId); const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
const handleOpenTableCell = (args: OpenTableCellArgs) => { const handleOpenTableCell = (args: OpenTableCellArgs) => {
@ -82,7 +67,6 @@ export const RecordTableNoRecordGroupBodyContextProvider = ({
return ( return (
<RecordTableBodyContextProvider <RecordTableBodyContextProvider
value={{ value={{
onUpsertRecord: handleUpsertTableRecordNoRecordGroup,
onOpenTableCell: handleOpenTableCell, onOpenTableCell: handleOpenTableCell,
onMoveFocus: handleMoveFocus, onMoveFocus: handleMoveFocus,
onCloseTableCell: handleCloseTableCell, onCloseTableCell: handleCloseTableCell,

View File

@ -1,7 +1,6 @@
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useUpsertTableRecordInGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup'; import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup';
import { useMoveSoftFocusToCurrentCellOnHover } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover'; import { useMoveSoftFocusToCurrentCellOnHover } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover';
@ -20,26 +19,10 @@ type RecordTableRecordGroupBodyContextProviderProps = {
}; };
export const RecordTableRecordGroupBodyContextProvider = ({ export const RecordTableRecordGroupBodyContextProvider = ({
recordGroupId,
children, children,
}: RecordTableRecordGroupBodyContextProviderProps) => { }: RecordTableRecordGroupBodyContextProviderProps) => {
const { recordTableId } = useRecordTableContextOrThrow(); const { recordTableId } = useRecordTableContextOrThrow();
const { upsertTableRecordInGroup } =
useUpsertTableRecordInGroup(recordGroupId);
const handleupsertTableRecordInGroup = ({
persistField,
recordId,
fieldName,
}: {
persistField: () => void;
recordId: string;
fieldName: string;
}) => {
upsertTableRecordInGroup(persistField, recordId, fieldName);
};
const { openTableCell } = useOpenRecordTableCellV2(recordTableId); const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
const handleOpenTableCell = (args: OpenTableCellArgs) => { const handleOpenTableCell = (args: OpenTableCellArgs) => {
@ -52,8 +35,7 @@ export const RecordTableRecordGroupBodyContextProvider = ({
moveFocus(direction); moveFocus(direction);
}; };
const { closeTableCellInGroup } = const { closeTableCellInGroup } = useCloseRecordTableCellInGroup();
useCloseRecordTableCellInGroup(recordGroupId);
const handlecloseTableCellInGroup = () => { const handlecloseTableCellInGroup = () => {
closeTableCellInGroup(); closeTableCellInGroup();
@ -86,7 +68,6 @@ export const RecordTableRecordGroupBodyContextProvider = ({
return ( return (
<RecordTableBodyContextProvider <RecordTableBodyContextProvider
value={{ value={{
onUpsertRecord: handleupsertTableRecordInGroup,
onOpenTableCell: handleOpenTableCell, onOpenTableCell: handleOpenTableCell,
onMoveFocus: handleMoveFocus, onMoveFocus: handleMoveFocus,
onCloseTableCell: handlecloseTableCellInGroup, onCloseTableCell: handlecloseTableCellInGroup,

View File

@ -2,7 +2,6 @@ import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useC
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter'; import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew'; import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore'; import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore';
@ -57,7 +56,6 @@ export const RecordTableRecordGroupRows = () => {
/> />
); );
})} })}
<RecordTablePendingRecordGroupRow />
<RecordTableRecordGroupSectionAddNew /> <RecordTableRecordGroupSectionAddNew />
<RecordTableRecordGroupSectionLoadMore /> <RecordTableRecordGroupSectionLoadMore />
<RecordTableAggregateFooter <RecordTableAggregateFooter

View File

@ -90,7 +90,6 @@ const meta: Meta = {
> >
<RecordTableBodyContextProvider <RecordTableBodyContextProvider
value={{ value={{
onUpsertRecord: () => {},
onOpenTableCell: () => {}, onOpenTableCell: () => {},
onMoveFocus: () => {}, onMoveFocus: () => {},
onCloseTableCell: () => {}, onCloseTableCell: () => {},

View File

@ -8,15 +8,6 @@ import { createRequiredContext } from '~/utils/createRequiredContext';
export type RecordTableBodyContextProps = { export type RecordTableBodyContextProps = {
recordGroupId?: string; recordGroupId?: string;
onUpsertRecord: ({
persistField,
recordId,
fieldName,
}: {
persistField: () => void;
recordId: string;
fieldName: string;
}) => void;
onOpenTableCell: (args: OpenTableCellArgs) => void; onOpenTableCell: (args: OpenTableCellArgs) => void;
onMoveFocus: (direction: MoveFocusDirection) => void; onMoveFocus: (direction: MoveFocusDirection) => void;
onCloseTableCell: () => void; onCloseTableCell: () => void;

View File

@ -1,9 +1,6 @@
import { isNull } from '@sniptt/guards';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState'; import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
type RecordTableEmptyHandlerProps = { type RecordTableEmptyHandlerProps = {
@ -25,15 +22,8 @@ export const RecordTableEmptyHandler = ({
recordTableId, recordTableId,
); );
const pendingRecordId = useRecoilComponentValueV2(
recordTablePendingRecordIdComponentState,
recordTableId,
);
const recordTableIsEmpty = const recordTableIsEmpty =
!isRecordTableInitialLoading && !isRecordTableInitialLoading && allRecordIds.length === 0;
allRecordIds.length === 0 &&
isNull(pendingRecordId);
if (recordTableIsEmpty) { if (recordTableIsEmpty) {
return <RecordTableEmptyState />; return <RecordTableEmptyState />;

View File

@ -5,18 +5,17 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay'; import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { getEmptyStateSubTitle } from '@/object-record/record-table/empty-state/utils/getEmptyStateSubTitle'; import { getEmptyStateSubTitle } from '@/object-record/record-table/empty-state/utils/getEmptyStateSubTitle';
import { getEmptyStateTitle } from '@/object-record/record-table/empty-state/utils/getEmptyStateTitle'; import { getEmptyStateTitle } from '@/object-record/record-table/empty-state/utils/getEmptyStateTitle';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
export const RecordTableEmptyStateNoGroupNoRecordAtAll = () => { export const RecordTableEmptyStateNoGroupNoRecordAtAll = () => {
const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
const { createNewTableRecord } = useCreateNewTableRecord({ const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem, objectMetadataItem,
recordTableId,
}); });
const handleButtonClick = () => { const handleButtonClick = () => {
createNewTableRecord(); createNewIndexRecord();
}; };
const objectLabel = useObjectLabel(objectMetadataItem); const objectLabel = useObjectLabel(objectMetadataItem);

View File

@ -3,18 +3,17 @@ import { IconPlus } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel'; import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay'; import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
export const RecordTableEmptyStateNoRecordFoundForFilter = () => { export const RecordTableEmptyStateNoRecordFoundForFilter = () => {
const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
const { createNewTableRecord } = useCreateNewTableRecord({ const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem, objectMetadataItem,
recordTableId,
}); });
const handleButtonClick = () => { const handleButtonClick = () => {
createNewTableRecord(); createNewIndexRecord();
}; };
const objectLabel = useObjectLabel(objectMetadataItem); const objectLabel = useObjectLabel(objectMetadataItem);

View File

@ -1,174 +0,0 @@
import { renderHook } from '@testing-library/react';
import { createState } from '@ui/utilities/state/utils/createState';
import { ReactNode, act } from 'react';
import { RecoilRoot } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { useUpsertTableRecordInGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const draftValue = 'updated Name';
const recordGroupId = 'recordGroupId';
// Todo refactor this test to inject the states in a cleaner way instead of mocking hooks
// (this is not easy to maintain while refactoring)
jest.mock('@/object-record/hooks/useCreateOneRecord', () => ({
__esModule: true,
useCreateOneRecord: jest.fn(),
}));
const draftValueState = createState<string | null>({
key: 'draftValueState',
defaultValue: null,
});
jest.mock(
'@/object-record/record-field/hooks/internal/useRecordFieldInputStates',
() => ({
__esModule: true,
useRecordFieldInputStates: jest.fn(() => ({
getDraftValueSelector: () => draftValueState,
})),
}),
);
const recordTablePendingRecordIdByGroupComponentFamilyState = createFamilyState<
string | null,
string
>({
key: 'recordTablePendingRecordIdByGroupComponentFamilyState',
defaultValue: null,
});
const createOneRecordMock = jest.fn();
const updateOneRecordMock = jest.fn();
(useCreateOneRecord as jest.Mock).mockReturnValue({
createOneRecord: createOneRecordMock,
});
const Wrapper = ({
children,
pendingRecordIdMockedValue,
draftValueMockedValue,
}: {
children: ReactNode;
pendingRecordIdMockedValue: string | null;
draftValueMockedValue: string | null;
}) => (
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
snapshot.set(
recordTablePendingRecordIdByGroupComponentFamilyState(recordGroupId),
pendingRecordIdMockedValue,
);
snapshot.set(draftValueState, draftValueMockedValue);
}}
>
<RecordTableContextProvider
recordTableId="recordTableId"
objectNameSingular={CoreObjectNameSingular.Person}
viewBarId="viewBarId"
>
<RecordTableComponentInstanceContext.Provider
value={{
instanceId: CoreObjectNamePlural.Person,
onColumnsChange: jest.fn(),
}}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: CoreObjectNamePlural.Person }}
>
<FieldContext.Provider
value={{
recordId: 'recordId',
fieldDefinition: {
...textfieldDefinition,
metadata: {
...textfieldDefinition.metadata,
objectMetadataNameSingular: CoreObjectNameSingular.Person,
},
},
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
}}
>
{children}
</FieldContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordTableContextProvider>
</RecoilRoot>
);
describe('useUpsertTableRecordInGroup', () => {
beforeEach(async () => {
createOneRecordMock.mockClear();
updateOneRecordMock.mockClear();
});
it('calls update record if there is no pending record', async () => {
/**
* {
objectNameSingular: 'person',
recordTableId: 'recordTableId',
}
*/
const { result } = renderHook(
() => useUpsertTableRecordInGroup(recordGroupId),
{
wrapper: ({ children }) =>
Wrapper({
pendingRecordIdMockedValue: null,
draftValueMockedValue: null,
children,
}),
},
);
await act(async () => {
await result.current.upsertTableRecordInGroup(
updateOneRecordMock,
'recordId',
'name',
);
});
expect(createOneRecordMock).not.toHaveBeenCalled();
expect(updateOneRecordMock).toHaveBeenCalled();
});
it('calls update record if pending record is empty', async () => {
const { result } = renderHook(
() => useUpsertTableRecordInGroup(recordGroupId),
{
wrapper: ({ children }) =>
Wrapper({
pendingRecordIdMockedValue: null,
draftValueMockedValue: draftValue,
children,
}),
},
);
await act(async () => {
await result.current.upsertTableRecordInGroup(
updateOneRecordMock,
'recordId',
'name',
);
});
expect(createOneRecordMock).not.toHaveBeenCalled();
expect(updateOneRecordMock).toHaveBeenCalled();
});
});

View File

@ -1,160 +0,0 @@
import { renderHook } from '@testing-library/react';
import { createState } from '@ui/utilities/state/utils/createState';
import { ReactNode, act } from 'react';
import { RecoilRoot } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { useUpsertTableRecordNoGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const draftValue = 'updated Name';
// Todo refactor this test to inject the states in a cleaner way instead of mocking hooks
// (this is not easy to maintain while refactoring)
jest.mock('@/object-record/hooks/useCreateOneRecord', () => ({
__esModule: true,
useCreateOneRecord: jest.fn(),
}));
const draftValueState = createState<string | null>({
key: 'draftValueState',
defaultValue: null,
});
jest.mock(
'@/object-record/record-field/hooks/internal/useRecordFieldInputStates',
() => ({
__esModule: true,
useRecordFieldInputStates: jest.fn(() => ({
getDraftValueSelector: () => draftValueState,
})),
}),
);
const pendingRecordIdState = createState<string | null>({
key: 'pendingRecordIdState',
defaultValue: null,
});
const createOneRecordMock = jest.fn();
const updateOneRecordMock = jest.fn();
(useCreateOneRecord as jest.Mock).mockReturnValue({
createOneRecord: createOneRecordMock,
});
const Wrapper = ({
children,
pendingRecordIdMockedValue,
draftValueMockedValue,
}: {
children: ReactNode;
pendingRecordIdMockedValue: string | null;
draftValueMockedValue: string | null;
}) => (
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
snapshot.set(pendingRecordIdState, pendingRecordIdMockedValue);
snapshot.set(draftValueState, draftValueMockedValue);
}}
>
<RecordTableContextProvider
recordTableId="recordTableId"
objectNameSingular={CoreObjectNameSingular.Person}
viewBarId="viewBarId"
>
<RecordTableComponentInstanceContext.Provider
value={{
instanceId: CoreObjectNamePlural.Person,
onColumnsChange: jest.fn(),
}}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: CoreObjectNamePlural.Person }}
>
<FieldContext.Provider
value={{
recordId: 'recordId',
fieldDefinition: {
...textfieldDefinition,
metadata: {
...textfieldDefinition.metadata,
objectMetadataNameSingular: CoreObjectNameSingular.Person,
},
},
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
}}
>
{children}
</FieldContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordTableContextProvider>
</RecoilRoot>
);
describe('useUpsertTableRecordNoGroup', () => {
beforeEach(async () => {
createOneRecordMock.mockClear();
updateOneRecordMock.mockClear();
});
it('calls update record if there is no pending record', async () => {
/**
* {
objectNameSingular: 'person',
recordTableId: 'recordTableId',
}
*/
const { result } = renderHook(() => useUpsertTableRecordNoGroup(), {
wrapper: ({ children }) =>
Wrapper({
pendingRecordIdMockedValue: null,
draftValueMockedValue: null,
children,
}),
});
await act(async () => {
await result.current.upsertTableRecordNoGroup(
updateOneRecordMock,
'recordId',
'name',
);
});
expect(createOneRecordMock).not.toHaveBeenCalled();
expect(updateOneRecordMock).toHaveBeenCalled();
});
it('calls update record if pending record is empty', async () => {
const { result } = renderHook(() => useUpsertTableRecordNoGroup(), {
wrapper: ({ children }) =>
Wrapper({
pendingRecordIdMockedValue: null,
draftValueMockedValue: draftValue,
children,
}),
});
await act(async () => {
await result.current.upsertTableRecordNoGroup(
updateOneRecordMock,
'recordId',
'name',
);
});
expect(createOneRecordMock).not.toHaveBeenCalled();
expect(updateOneRecordMock).toHaveBeenCalled();
});
});

View File

@ -1,107 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
import { isDefined } from 'twenty-shared';
export const useUpsertTableRecordInGroup = (recordGroupId: string) => {
const { objectMetadataItem, objectNameSingular } =
useRecordTableContextOrThrow();
const { createOneRecord } = useCreateOneRecord({
objectNameSingular,
shouldMatchRootQueryFilter: true,
});
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
);
const recordIndexRecordIdsByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordIndexRecordIdsByGroupComponentFamilyState,
);
const upsertTableRecordInGroup = useRecoilCallback(
({ snapshot }) =>
(persistField: () => void, recordId: string, fieldName: string) => {
const labelIdentifierFieldMetadataItem =
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
const fieldScopeId = getScopeIdFromComponentId(
`${recordId}-${fieldName}`,
);
const draftValueSelector = extractComponentSelector(
recordFieldInputDraftValueComponentSelector,
fieldScopeId,
);
const draftValue = getSnapshotValue(snapshot, draftValueSelector());
// We're in a record group
const recordTablePendingRecordId = getSnapshotValue(
snapshot,
recordTablePendingRecordIdByGroupFamilyState(recordGroupId),
);
const recordGroupDefinition = getSnapshotValue(
snapshot,
recordGroupDefinitionFamilyState(recordGroupId),
);
const recordGroupIds = getSnapshotValue(
snapshot,
recordIndexRecordIdsByGroupFamilyState(recordGroupId),
);
const recordGroupFieldMetadataItem = objectMetadataItem.fields.find(
(fieldMetadata) =>
fieldMetadata.id === recordGroupDefinition?.fieldMetadataId,
);
const lastId = recordGroupIds?.[recordGroupIds.length - 1];
const objectRecord = getSnapshotValue(
snapshot,
recordStoreFamilyState(lastId),
);
if (
isDefined(recordTablePendingRecordId) &&
isDefined(recordGroupDefinition) &&
isDefined(recordGroupFieldMetadataItem) &&
isDefined(draftValue)
) {
createOneRecord({
id: recordTablePendingRecordId,
[labelIdentifierFieldMetadataItem?.name ?? 'name']: draftValue,
[recordGroupFieldMetadataItem.name]: recordGroupDefinition.value,
position: (objectRecord?.position ?? 0) + 0.0001,
});
} else if (!recordTablePendingRecordId) {
persistField();
}
},
[
createOneRecord,
objectMetadataItem,
recordGroupId,
recordIndexRecordIdsByGroupFamilyState,
recordTablePendingRecordIdByGroupFamilyState,
],
);
return { upsertTableRecordInGroup };
};

View File

@ -1,63 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
import { isDefined } from 'twenty-shared';
export const useUpsertTableRecordNoGroup = () => {
const { objectMetadataItem, objectNameSingular, recordTableId } =
useRecordTableContextOrThrow();
const { createOneRecord } = useCreateOneRecord({
objectNameSingular,
});
const recordTablePendingRecordIdState = useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdComponentState,
recordTableId,
);
const upsertTableRecordNoGroup = useRecoilCallback(
({ snapshot }) =>
(persistField: () => void, recordId: string, fieldName: string) => {
const labelIdentifierFieldMetadataItem =
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
const fieldScopeId = getScopeIdFromComponentId(
`${recordId}-${fieldName}`,
);
const draftValueSelector = extractComponentSelector(
recordFieldInputDraftValueComponentSelector,
fieldScopeId,
);
const draftValue = getSnapshotValue(snapshot, draftValueSelector());
const recordTablePendingRecordId = getSnapshotValue(
snapshot,
recordTablePendingRecordIdState,
);
if (isDefined(recordTablePendingRecordId) && isDefined(draftValue)) {
createOneRecord({
id: recordTablePendingRecordId,
[labelIdentifierFieldMetadataItem?.name ?? 'name']: draftValue,
position: 'first',
});
} else if (!recordTablePendingRecordId) {
persistField();
}
},
[createOneRecord, objectMetadataItem, recordTablePendingRecordIdState],
);
return { upsertTableRecordNoGroup };
};

View File

@ -0,0 +1,71 @@
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { AppPath } from '@/types/AppPath';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useCreateNewIndexRecord = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const { createOneRecord } = useCreateOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
shouldMatchRootQueryFilter: true,
});
const navigate = useNavigateApp();
const { openRecordTitleCell } = useRecordTitleCell();
const createNewIndexRecord = useRecoilCallback(
({ snapshot }) =>
async (recordInput?: Partial<ObjectRecord>) => {
const recordId = v4();
const recordIndexOpenRecordIn = snapshot
.getLoadable(recordIndexOpenRecordInState)
.getValue();
await createOneRecord({ id: recordId, ...recordInput });
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
openRecordInCommandMenu({
recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isNewRecord: true,
});
openRecordTitleCell({
recordId,
fieldMetadataId: objectMetadataItem.labelIdentifierFieldMetadataId,
});
} else {
navigate(AppPath.RecordShowPage, {
objectNameSingular: objectMetadataItem.nameSingular,
objectRecordId: recordId,
});
}
},
[
createOneRecord,
navigate,
objectMetadataItem.labelIdentifierFieldMetadataId,
objectMetadataItem.nameSingular,
openRecordInCommandMenu,
openRecordTitleCell,
],
);
return {
createNewIndexRecord,
};
};

View File

@ -1,68 +0,0 @@
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared';
import { v4 } from 'uuid';
export const useCreateNewTableRecordInGroup = () => {
const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
scopeId: recordIndexId,
});
const setHotkeyScope = useSetHotkeyScope();
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
recordIndexId,
);
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const createNewTableRecordInGroup = useRecoilCallback(
({ set }) =>
(recordGroupId: string) => {
const recordId = v4();
set(
recordTablePendingRecordIdByGroupFamilyState(recordGroupId),
recordId,
);
setSelectedTableCellEditMode(-1, 0);
setHotkeyScope(
DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes,
);
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
recordId,
objectMetadataItem.labelIdentifierFieldMetadataId,
'table-cell',
),
);
}
},
[
objectMetadataItem,
recordTablePendingRecordIdByGroupFamilyState,
setActiveDropdownFocusIdAndMemorizePrevious,
setHotkeyScope,
setSelectedTableCellEditMode,
],
);
return {
createNewTableRecordInGroup,
};
};

View File

@ -1,129 +0,0 @@
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { AppPath } from '@/types/AppPath';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared';
import { v4 } from 'uuid';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useCreateNewTableRecord = ({
objectMetadataItem,
recordTableId,
}: {
objectMetadataItem: ObjectMetadataItem;
recordTableId: string;
}) => {
const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
scopeId: recordTableId,
});
const setHotkeyScope = useSetHotkeyScope();
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
recordTableId,
);
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const { createOneRecord } = useCreateOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
shouldMatchRootQueryFilter: true,
});
const navigate = useNavigateApp();
const { openRecordTitleCell } = useRecordTitleCell();
const createNewTableRecord = useRecoilCallback(
({ snapshot }) =>
async () => {
const recordId = v4();
const recordIndexOpenRecordIn = snapshot
.getLoadable(recordIndexOpenRecordInState)
.getValue();
await createOneRecord({ id: recordId });
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
openRecordInCommandMenu({
recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isNewRecord: true,
});
openRecordTitleCell({
recordId,
fieldMetadataId: objectMetadataItem.labelIdentifierFieldMetadataId,
});
} else {
navigate(AppPath.RecordShowPage, {
objectNameSingular: objectMetadataItem.nameSingular,
objectRecordId: recordId,
});
}
},
[
createOneRecord,
navigate,
objectMetadataItem.labelIdentifierFieldMetadataId,
objectMetadataItem.nameSingular,
openRecordInCommandMenu,
openRecordTitleCell,
],
);
const createNewTableRecordInGroup = useRecoilCallback(
({ set }) =>
(recordGroupId: string) => {
const recordId = v4();
set(
recordTablePendingRecordIdByGroupFamilyState(recordGroupId),
recordId,
);
setSelectedTableCellEditMode(-1, 0);
setHotkeyScope(
DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes,
);
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
recordId,
objectMetadataItem.labelIdentifierFieldMetadataId,
'table-cell',
),
);
}
},
[
objectMetadataItem,
recordTablePendingRecordIdByGroupFamilyState,
setActiveDropdownFocusIdAndMemorizePrevious,
setHotkeyScope,
setSelectedTableCellEditMode,
],
);
return {
createNewTableRecord,
createNewTableRecordInGroup,
};
};

View File

@ -4,7 +4,6 @@ import { RecordTableNoRecordGroupRows } from '@/object-record/record-table/compo
import { RecordTableBodyDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider'; import { RecordTableBodyDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider';
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -25,7 +24,6 @@ export const RecordTableNoRecordGroupBody = () => {
<RecordTableNoRecordGroupBodyContextProvider> <RecordTableNoRecordGroupBodyContextProvider>
<RecordTableBodyDragDropContextProvider> <RecordTableBodyDragDropContextProvider>
<RecordTableBodyDroppable> <RecordTableBodyDroppable>
<RecordTablePendingRow />
<RecordTableNoRecordGroupRows /> <RecordTableNoRecordGroupRows />
</RecordTableBodyDroppable> </RecordTableBodyDroppable>
</RecordTableBodyDragDropContextProvider> </RecordTableBodyDragDropContextProvider>

View File

@ -10,28 +10,19 @@ import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInput
export const RecordTableCellFieldInput = () => { export const RecordTableCellFieldInput = () => {
const { recordId, fieldDefinition } = useContext(FieldContext); const { recordId, fieldDefinition } = useContext(FieldContext);
const { onUpsertRecord, onMoveFocus, onCloseTableCell } = const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
useRecordTableBodyContextOrThrow();
const isFieldReadOnly = useIsFieldValueReadOnly(); const isFieldReadOnly = useIsFieldValueReadOnly();
const handleEnter: FieldInputEvent = (persistField) => { const handleEnter: FieldInputEvent = (persistField) => {
onUpsertRecord({ persistField();
persistField,
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell(); onCloseTableCell();
onMoveFocus('down'); onMoveFocus('down');
}; };
const handleSubmit: FieldInputEvent = (persistField) => { const handleSubmit: FieldInputEvent = (persistField) => {
onUpsertRecord({ persistField();
persistField,
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell(); onCloseTableCell();
}; };
@ -46,42 +37,26 @@ export const RecordTableCellFieldInput = () => {
) => { ) => {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
onUpsertRecord({ persistField();
persistField,
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell(); onCloseTableCell();
}; };
const handleEscape: FieldInputEvent = (persistField) => { const handleEscape: FieldInputEvent = (persistField) => {
onUpsertRecord({ persistField();
persistField,
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell(); onCloseTableCell();
}; };
const handleTab: FieldInputEvent = (persistField) => { const handleTab: FieldInputEvent = (persistField) => {
onUpsertRecord({ persistField();
persistField,
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell(); onCloseTableCell();
onMoveFocus('right'); onMoveFocus('right');
}; };
const handleShiftTab: FieldInputEvent = (persistField) => { const handleShiftTab: FieldInputEvent = (persistField) => {
onUpsertRecord({ persistField();
persistField,
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell(); onCloseTableCell();
onMoveFocus('left'); onMoveFocus('left');

View File

@ -32,7 +32,6 @@ jest.mock('@/ui/utilities/hotkey/hooks/useSetHotkeyScope', () => ({
const onColumnsChange = jest.fn(); const onColumnsChange = jest.fn();
const recordTableId = 'scopeId'; const recordTableId = 'scopeId';
const recordGroupId = 'recordGroupId';
const Wrapper = ({ children }: { children: React.ReactNode }) => ( const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot <RecoilRoot
@ -86,7 +85,7 @@ describe('useCloseRecordTableCellInGroup', () => {
currentTableCellInEditModePosition, currentTableCellInEditModePosition,
); );
return { return {
...useCloseRecordTableCellInGroup(recordGroupId), ...useCloseRecordTableCellInGroup(),
...useDragSelect(), ...useDragSelect(),
isTableCellInEditMode, isTableCellInEditMode,
}; };

View File

@ -7,11 +7,9 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode'; import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
export const useCloseRecordTableCellInGroup = (recordGroupId: string) => { export const useCloseRecordTableCellInGroup = () => {
const { recordTableId } = useRecordTableContextOrThrow(); const { recordTableId } = useRecordTableContextOrThrow();
const setHotkeyScope = useSetHotkeyScope(); const setHotkeyScope = useSetHotkeyScope();
@ -24,26 +22,15 @@ export const useCloseRecordTableCellInGroup = (recordGroupId: string) => {
const closeCurrentTableCellInEditMode = const closeCurrentTableCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableId); useCloseCurrentTableCellInEditMode(recordTableId);
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
recordTableId,
);
const closeTableCellInGroup = useRecoilCallback( const closeTableCellInGroup = useRecoilCallback(
({ reset }) => () => () => {
() => { toggleClickOutsideListener(true);
toggleClickOutsideListener(true); setDragSelectionStartEnabled(true);
setDragSelectionStartEnabled(true); closeCurrentTableCellInEditMode();
closeCurrentTableCellInEditMode(); setHotkeyScope(TableHotkeyScope.TableSoftFocus);
setHotkeyScope(TableHotkeyScope.TableSoftFocus); },
reset(recordTablePendingRecordIdByGroupFamilyState(recordGroupId));
},
[ [
closeCurrentTableCellInEditMode, closeCurrentTableCellInEditMode,
recordGroupId,
recordTablePendingRecordIdByGroupFamilyState,
setDragSelectionStartEnabled, setDragSelectionStartEnabled,
setHotkeyScope, setHotkeyScope,
toggleClickOutsideListener, toggleClickOutsideListener,

View File

@ -1,5 +1,3 @@
import { useResetRecoilState } from 'recoil';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId'; import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
@ -7,9 +5,7 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode'; import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useCallback } from 'react'; import { useCallback } from 'react';
export const useCloseRecordTableCellNoGroup = () => { export const useCloseRecordTableCellNoGroup = () => {
@ -26,23 +22,13 @@ export const useCloseRecordTableCellNoGroup = () => {
const closeCurrentTableCellInEditMode = const closeCurrentTableCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableId); useCloseCurrentTableCellInEditMode(recordTableId);
const pendingRecordIdState = useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdComponentState,
recordTableId,
);
const resetRecordTablePendingRecordId =
useResetRecoilState(pendingRecordIdState);
const closeTableCellNoGroup = useCallback(() => { const closeTableCellNoGroup = useCallback(() => {
toggleClickOutsideListener(true); toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true); setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode(); closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus); setHotkeyScope(TableHotkeyScope.TableSoftFocus);
resetRecordTablePendingRecordId();
}, [ }, [
closeCurrentTableCellInEditMode, closeCurrentTableCellInEditMode,
resetRecordTablePendingRecordId,
setDragSelectionStartEnabled, setDragSelectionStartEnabled,
setHotkeyScope, setHotkeyScope,
toggleClickOutsideListener, toggleClickOutsideListener,

View File

@ -6,7 +6,7 @@ import { IconPlus, LightIconButton } from 'twenty-ui';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
@ -107,7 +107,7 @@ type RecordTableHeaderCellProps = {
export const RecordTableHeaderCell = ({ export const RecordTableHeaderCell = ({
column, column,
}: RecordTableHeaderCellProps) => { }: RecordTableHeaderCellProps) => {
const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
const resizeFieldOffsetState = useRecoilComponentCallbackStateV2( const resizeFieldOffsetState = useRecoilComponentCallbackStateV2(
resizeFieldOffsetComponentState, resizeFieldOffsetComponentState,
@ -202,13 +202,12 @@ export const RecordTableHeaderCell = ({
const disableColumnResize = const disableColumnResize =
column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft; column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft;
const { createNewTableRecord } = useCreateNewTableRecord({ const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem, objectMetadataItem,
recordTableId,
}); });
const handlePlusButtonClick = () => { const handlePlusButtonClick = () => {
createNewTableRecord(); createNewIndexRecord();
}; };
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem); const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);

View File

@ -1,25 +0,0 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
export const RecordTablePendingRecordGroupRow = () => {
const currentRecordGroupId = useCurrentRecordGroupId();
const pendingRecordId = useRecoilComponentFamilyValueV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
currentRecordGroupId,
);
if (!pendingRecordId) return <></>;
return (
<RecordTableRow
key={pendingRecordId}
recordId={pendingRecordId}
rowIndexForDrag={-1}
rowIndexForFocus={-1}
isPendingRow
/>
);
};

View File

@ -1,21 +0,0 @@
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTablePendingRow = () => {
const pendingRecordId = useRecoilComponentValueV2(
recordTablePendingRecordIdComponentState,
);
if (!pendingRecordId) return <></>;
return (
<RecordTableRow
key={pendingRecordId}
recordId={pendingRecordId}
rowIndexForDrag={-1}
rowIndexForFocus={-1}
isPendingRow
/>
);
};

View File

@ -1,15 +1,17 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow'; import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { IconPlus } from 'twenty-ui';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { IconPlus } from 'twenty-ui';
export const RecordTableRecordGroupSectionAddNew = () => { export const RecordTableRecordGroupSectionAddNew = () => {
const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
const currentRecordGroupId = useCurrentRecordGroupId(); const currentRecordGroupId = useCurrentRecordGroupId();
@ -17,16 +19,19 @@ export const RecordTableRecordGroupSectionAddNew = () => {
recordIndexAllRecordIdsComponentSelector, recordIndexAllRecordIdsComponentSelector,
); );
const recordGroup = useRecoilValue(
recordGroupDefinitionFamilyState(currentRecordGroupId),
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewTableRecordInGroup } = useCreateNewTableRecord({ const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem, objectMetadataItem,
recordTableId,
}); });
const handleAddNewRecord = () => { const fieldMetadataItem = objectMetadataItem.fields.find(
createNewTableRecordInGroup(currentRecordGroupId); (field) => field.id === recordGroup?.fieldMetadataId,
}; );
if (hasObjectReadOnlyPermission) { if (hasObjectReadOnlyPermission) {
return null; return null;
@ -38,7 +43,16 @@ export const RecordTableRecordGroupSectionAddNew = () => {
draggableIndex={recordIds.length + 2} draggableIndex={recordIds.length + 2}
LeftIcon={IconPlus} LeftIcon={IconPlus}
text={t`Add new`} text={t`Add new`}
onClick={handleAddNewRecord} onClick={() => {
if (!fieldMetadataItem) {
return;
}
createNewIndexRecord({
position: 'last',
[fieldMetadataItem.name]: recordGroup?.value,
});
}}
/> />
); );
}; };

View File

@ -1,10 +0,0 @@
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
export const recordTablePendingRecordIdByGroupComponentFamilyState =
createComponentFamilyStateV2<string | null, RecordGroupDefinition['id']>({
key: 'recordTablePendingRecordIdByGroupComponentFamilyState',
defaultValue: null,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -1,10 +0,0 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const recordTablePendingRecordIdComponentState = createComponentStateV2<
string | null
>({
key: 'recordTablePendingRecordIdComponentState',
defaultValue: null,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -1,46 +0,0 @@
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
import { isDefined } from 'twenty-shared';
export const hasPendingRecordComponentSelector = createComponentSelectorV2({
key: 'hasPendingRecordComponentSelector',
componentInstanceContext: RecordTableComponentInstanceContext,
get:
({ instanceId }) =>
({ get }) => {
const hasRecordGroups = get(
hasRecordGroupsComponentSelector.selectorFamily({ instanceId }),
);
if (!hasRecordGroups) {
const pendingRecordId = get(
recordTablePendingRecordIdComponentState.atomFamily({ instanceId }),
);
return isDefined(pendingRecordId);
}
const recordGroupIds = get(
recordGroupIdsComponentState.atomFamily({ instanceId }),
);
for (const recordGroupId of recordGroupIds) {
const pendingRecordId = get(
recordTablePendingRecordIdByGroupComponentFamilyState.atomFamily({
instanceId,
familyKey: recordGroupId,
}),
);
if (isDefined(pendingRecordId)) {
return true;
}
}
return false;
},
});

View File

@ -8,14 +8,15 @@ import { ContextStoreComponentInstanceContext } from '@/context-store/states/con
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { PageBody } from '@/ui/layout/page/components/PageBody'; import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PageContainer } from '@/ui/layout/page/components/PageContainer'; import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader'; import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
import { RecordShowPageTitle } from '~/pages/object-record/RecordShowPageTitle';
export const RecordShowPage = () => { export const RecordShowPage = () => {
const parameters = useParams<{ const parameters = useParams<{
@ -23,14 +24,7 @@ export const RecordShowPage = () => {
objectRecordId: string; objectRecordId: string;
}>(); }>();
const { const { objectNameSingular, objectRecordId, headerIcon } = useRecordShowPage(
pageTitle,
objectNameSingular,
objectRecordId,
headerIcon,
loading,
pageName,
} = useRecordShowPage(
parameters.objectNameSingular ?? '', parameters.objectNameSingular ?? '',
parameters.objectRecordId ?? '', parameters.objectRecordId ?? '',
); );
@ -54,7 +48,10 @@ export const RecordShowPage = () => {
> >
<RecordValueSetterEffect recordId={objectRecordId} /> <RecordValueSetterEffect recordId={objectRecordId} />
<PageContainer> <PageContainer>
<PageTitle title={pageTitle} /> <RecordShowPageTitle
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
/>
<RecordShowPageHeader <RecordShowPageHeader
objectNameSingular={objectNameSingular} objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId} objectRecordId={objectRecordId}
@ -64,12 +61,18 @@ export const RecordShowPage = () => {
</RecordShowPageHeader> </RecordShowPageHeader>
<PageBody> <PageBody>
<TimelineActivityContext.Provider <TimelineActivityContext.Provider
value={{ labelIdentifierValue: pageName }} value={{
recordId: objectRecordId,
}}
> >
<RecordShowEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
<RecordShowContainer <RecordShowContainer
objectNameSingular={objectNameSingular} objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId} objectRecordId={objectRecordId}
loading={loading} loading={false}
/> />
</TimelineActivityContext.Provider> </TimelineActivityContext.Provider>
</PageBody> </PageBody>

View File

@ -0,0 +1,35 @@
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { useRecoilValue } from 'recoil';
import { FieldMetadataType, capitalize, isDefined } from 'twenty-shared';
export const RecordShowPageTitle = ({
objectNameSingular,
objectRecordId,
}: {
objectNameSingular: string;
objectRecordId: string;
}) => {
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({ objectNameSingular });
const record = useRecoilValue(recordStoreFamilyState(objectRecordId));
const labelIdentifierFieldValue = record?.labelIdentifierFieldValue;
const pageName =
labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FULL_NAME
? [
labelIdentifierFieldValue?.firstName,
labelIdentifierFieldValue?.lastName,
].join(' ')
: isDefined(labelIdentifierFieldValue)
? `${labelIdentifierFieldValue}`
: '';
const pageTitle = pageName.trim()
? `${pageName} - ${capitalize(objectNameSingular)}`
: capitalize(objectNameSingular);
return <PageTitle title={pageTitle} />;
};

View File

@ -46,7 +46,6 @@ export const RecordTableDecorator: Decorator = (Story) => {
onActionMenuDropdownOpened: () => {}, onActionMenuDropdownOpened: () => {},
onMoveFocus: () => {}, onMoveFocus: () => {},
onMoveSoftFocusToCurrentCell: () => {}, onMoveSoftFocusToCurrentCell: () => {},
onUpsertRecord: () => {},
}} }}
> >
<Story /> <Story />

View File

@ -104,7 +104,7 @@ const StyledInput = styled.input<InputProps>`
disabled && isChecked disabled && isChecked
? theme.adaptiveColors.blue3 ? theme.adaptiveColors.blue3
: indeterminate || isChecked : indeterminate || isChecked
? theme.adaptiveColors.blue3 ? theme.color.blue
: 'transparent'}; : 'transparent'};
border-color: ${({ border-color: ${({
theme, theme,