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 { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
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');
}
const recordTableId = getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
currentViewId,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewTableRecord } = useCreateNewTableRecord({
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
recordTableId,
});
const onClick = () => {
createNewTableRecord();
};
return {
shouldBeRegistered: !hasObjectReadOnlyPermission,
onClick,
onClick: createNewIndexRecord,
};
};

View File

@ -7,6 +7,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
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 { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
@ -130,13 +131,19 @@ describe('useActivityTargetObjectRecords', () => {
objectMetadataItemsState,
);
const { activityTargetObjectRecords } =
useActivityTargetObjectRecords(task);
const setRecordFromStore = useSetRecoilState(
recordStoreFamilyState(task.id),
);
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
task.id,
);
return {
activityTargetObjectRecords,
setCurrentWorkspaceMember,
setObjectMetadataItems,
setRecordFromStore,
};
},
{ wrapper: Wrapper },
@ -145,6 +152,7 @@ describe('useActivityTargetObjectRecords', () => {
act(() => {
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
result.current.setObjectMetadataItems(generatedMockObjectMetadataItems);
result.current.setRecordFromStore(task);
});
const activityTargetObjectRecords =

View File

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

View File

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

View File

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

View File

@ -132,7 +132,7 @@ export const TaskRow = ({ task }: { task: Task }) => {
<TaskTargetsContextProvider>
<ActivityTargetsInlineCell
activityObjectNameSingular={CoreObjectNameSingular.Task}
activity={task}
activityRecordId={task.id}
showLabel={false}
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 { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
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 { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -104,7 +106,10 @@ export const EventRow = ({
}: EventRowProps) => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { labelIdentifierValue } = useContext(TimelineActivityContext);
const { recordId } = useContext(TimelineActivityContext);
const recordFromStore = useRecoilValue(recordStoreFamilyState(recordId));
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt);
const linkedObjectMetadataItem = useLinkedObjectObjectMetadataItem(
event.linkedObjectMetadataId,
@ -114,6 +119,18 @@ export const EventRow = ({
return null;
}
if (isUndefinedOrNull(recordFromStore)) {
return null;
}
if (isUndefinedOrNull(mainObjectMetadataItem)) {
return null;
}
const labelIdentifier = getObjectRecordIdentifier({
objectMetadataItem: mainObjectMetadataItem,
record: recordFromStore,
});
const authorFullName = getTimelineActivityAuthorFullName(
event,
currentWorkspaceMember,
@ -143,7 +160,7 @@ export const EventRow = ({
<StyledSummary>
<EventRowDynamicComponent
authorFullName={authorFullName}
labelIdentifierValue={labelIdentifierValue}
labelIdentifierValue={labelIdentifier.name}
event={event}
mainObjectMetadataItem={mainObjectMetadataItem}
linkedObjectMetadataItem={linkedObjectMetadataItem}

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
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 { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
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 { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
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 { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
@ -74,13 +76,23 @@ export const CommandMenuRecordPage = () => {
{!isNewViewableRecordLoading && (
<RecordValueSetterEffect recordId={objectRecordId} />
)}
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
/>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
}}
>
<RecordShowEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
/>
</TimelineActivityContext.Provider>
</RecordFieldValueSelectorContextProvider>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>

View File

@ -25,7 +25,6 @@ import styled from '@emotion/styled';
import { useContext, useState } from 'react';
import { InView, useInView } from 'react-intersection-observer';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { AnimatedEaseInOut } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -71,15 +70,7 @@ const StyledBoardCardWrapper = styled.div`
width: 100%;
`;
export const RecordBoardCard = ({
isCreating = false,
onCreateSuccess,
position,
}: {
isCreating?: boolean;
onCreateSuccess?: () => void;
position?: 'first' | 'last';
}) => {
export const RecordBoardCard = () => {
const navigate = useNavigateApp();
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
@ -134,18 +125,16 @@ export const RecordBoardCard = ({
};
const handleCardClick = () => {
if (!isCreating) {
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
openRecordInCommandMenu({
recordId,
objectNameSingular,
});
} else {
navigate(AppPath.RecordShowPage, {
objectNameSingular,
objectRecordId: recordId,
});
}
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
openRecordInCommandMenu({
recordId,
objectNameSingular,
});
} else {
navigate(AppPath.RecordShowPage, {
objectNameSingular,
objectRecordId: recordId,
});
}
};
@ -166,16 +155,12 @@ export const RecordBoardCard = ({
(boardField) => !boardField.isLabelIdentifier,
);
const labelIdentifierField = visibleFieldDefinitions.find(
(field) => field.isLabelIdentifier,
);
return (
<StyledBoardCardWrapper
className="record-board-card"
onContextMenu={handleActionMenuDropdown}
>
{!isCreating && <RecordValueSetterEffect recordId={recordId} />}
<RecordValueSetterEffect recordId={recordId} />
<InView>
<StyledBoardCard
ref={cardRef}
@ -183,16 +168,10 @@ export const RecordBoardCard = ({
onMouseLeave={onMouseLeaveBoard}
onClick={handleCardClick}
>
{isDefined(labelIdentifierField) && (
<RecordBoardCardHeader
identifierFieldDefinition={labelIdentifierField}
isCreating={isCreating}
onCreateSuccess={onCreateSuccess}
position={position}
isCardExpanded={isCardExpanded}
setIsCardExpanded={setIsCardExpanded}
/>
)}
<RecordBoardCardHeader
isCardExpanded={isCardExpanded}
setIsCardExpanded={setIsCardExpanded}
/>
<AnimatedEaseInOut
isOpen={isCardExpanded || !isCompactModeActive}
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 { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
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 { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
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 { 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 = ({
fieldDefinitions,
@ -42,8 +42,7 @@ export const RecordBoardCardBody = ({
value={{
recordId,
maxWidth: 156,
recoilScopeId:
(recordId || 'new') + fieldDefinition.fieldMetadataId,
recoilScopeId: `board-card-${recordId}-${fieldDefinition.fieldMetadataId}`,
isLabelIdentifier: false,
fieldDefinition: {
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 { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
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 { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
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 { 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 { TextInput } from '@/ui/input/components/TextInput';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import styled from '@emotion/styled';
import { Dispatch, SetStateAction, useContext, useState } from 'react';
import { Dispatch, SetStateAction, useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import {
@ -42,11 +28,6 @@ import {
LightIconButton,
} from 'twenty-ui';
const StyledTextInput = styled(TextInput)`
border-radius: ${({ theme }) => theme.border.radius.sm};
width: ${({ theme }) => theme.spacing(53)};
`;
const StyledCompactIconContainer = styled.div`
align-items: center;
display: flex;
@ -59,38 +40,21 @@ const StyledCheckboxContainer = styled.div`
`;
type RecordBoardCardHeaderProps = {
isCreating?: boolean;
onCreateSuccess?: () => void;
position?: 'first' | 'last';
identifierFieldDefinition: RecordBoardFieldDefinition<FieldMetadata>;
isCardExpanded?: boolean;
setIsCardExpanded?: Dispatch<SetStateAction<boolean>>;
};
export const RecordBoardCardHeader = ({
isCreating = false,
onCreateSuccess,
position,
identifierFieldDefinition,
isCardExpanded,
setIsCardExpanded,
}: 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 { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const record = useRecoilValue(recordStoreFamilyState(recordId));
const { updateOneRecord, objectMetadataItem } =
useContext(RecordBoardContext);
const { objectMetadataItem } = useContext(RecordBoardContext);
const recordBoardId = useAvailableScopeIdOrThrow(
RecordBoardScopeInternalContext,
@ -100,11 +64,6 @@ export const RecordBoardCardHeader = ({
isRecordBoardCompactModeActiveComponentState,
);
const isIdentifierEmpty = isFieldValueEmpty({
fieldDefinition: identifierFieldDefinition,
fieldValue: record?.[identifierFieldDefinition.metadata.fieldName],
});
const { checkIfLastUnselectAndCloseDropdown } =
useRecordBoardSelection(recordBoardId);
@ -114,112 +73,52 @@ export const RecordBoardCardHeader = ({
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);
return (
<RecordBoardCardHeaderContainer showCompactView={showCompactView}>
<StopPropagationContainer>
{isCreating && position !== undefined ? (
<RecordInlineCellEditMode>
<StyledTextInput
autoFocus
value={newLabelValue}
onInputEnter={() =>
handleInputEnter(newLabelValue, position, onCreateSuccess)
}
onBlur={() =>
handleBlur(newLabelValue, position, onCreateSuccess)
}
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
}
/>
)
{isDefined(record) && (
<RecordChip
objectNameSingular={objectMetadataItem.nameSingular}
record={record}
variant={AvatarChipVariant.Transparent}
maxWidth={150}
to={
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
? indexIdentifierUrl(recordId)
: undefined
}
/>
)}
</StopPropagationContainer>
{!isCreating && (
<>
{showCompactView && (
<StyledCompactIconContainer className="compact-icon-container">
<StopPropagationContainer>
<LightIconButton
Icon={isCardExpanded ? IconEyeOff : IconEye}
accent="tertiary"
onClick={() => {
setIsCardExpanded?.((prev) => !prev);
}}
/>
</StopPropagationContainer>
</StyledCompactIconContainer>
)}
<StyledCheckboxContainer className="checkbox-container">
<StopPropagationContainer>
<Checkbox
hoverable
checked={isCurrentCardSelected}
onChange={() => {
setIsCurrentCardSelected(!isCurrentCardSelected);
checkIfLastUnselectAndCloseDropdown();
}}
variant={CheckboxVariant.Secondary}
/>
</StopPropagationContainer>
</StyledCheckboxContainer>
</>
{showCompactView && (
<StyledCompactIconContainer className="compact-icon-container">
<StopPropagationContainer>
<LightIconButton
Icon={isCardExpanded ? IconEyeOff : IconEye}
accent="tertiary"
onClick={() => {
setIsCardExpanded?.((prev) => !prev);
}}
/>
</StopPropagationContainer>
</StyledCompactIconContainer>
)}
<StyledCheckboxContainer className="checkbox-container">
<StopPropagationContainer>
<Checkbox
hoverable
checked={isCurrentCardSelected}
onChange={() => {
setIsCurrentCardSelected(!isCurrentCardSelected);
checkIfLastUnselectAndCloseDropdown();
}}
variant={CheckboxVariant.Secondary}
/>
</StopPropagationContainer>
</StyledCheckboxContainer>
</RecordBoardCardHeaderContainer>
);
};

View File

@ -3,16 +3,11 @@ import { Draggable, DroppableProvided } from '@hello-pangea/dnd';
import { useContext } from 'react';
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 { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo';
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 { 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 { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
@ -50,7 +45,6 @@ export const RecordBoardColumnCardsContainer = ({
droppableProvided,
}: RecordBoardColumnCardsContainerProps) => {
const { columnDefinition } = useContext(RecordBoardColumnContext);
const { objectMetadataItem } = useContext(RecordBoardContext);
const columnId = columnDefinition.id;
@ -68,42 +62,12 @@ export const RecordBoardColumnCardsContainer = ({
isRecordBoardCompactModeActiveComponentState,
);
const { isOpportunitiesCompanyFieldDisabled } =
useIsOpportunitiesCompanyFieldDisabled();
return (
<StyledColumnCardsContainer
ref={droppableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...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 ? (
Array.from(
{
@ -138,23 +102,8 @@ export const RecordBoardColumnCardsContainer = ({
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
>
{objectMetadataItem.nameSingular ===
CoreObjectNameSingular.Opportunity &&
!isOpportunitiesCompanyFieldDisabled ? (
<RecordBoardColumnNewOpportunity
columnId={columnDefinition.id}
position="last"
/>
) : (
<RecordBoardColumnNewRecord
columnId={columnDefinition.id}
position="last"
/>
)}
<StyledNewButtonContainer>
<RecordBoardColumnNewRecordButton
columnId={columnDefinition.id}
/>
<RecordBoardColumnNewRecordButton />
</StyledNewButtonContainer>
</div>
)}

View File

@ -1,16 +1,14 @@
import styled from '@emotion/styled';
import { useContext, useState } from 'react';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
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 { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
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 { 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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { IconDotsVertical, IconPlus, LightIconButton, Tag } from 'twenty-ui';
@ -69,7 +67,8 @@ export const RecordBoardColumnHeader = () => {
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
const { objectMetadataItem } = useContext(RecordBoardContext);
const { objectMetadataItem, selectFieldMetadataItem } =
useContext(RecordBoardContext);
const {
setHotkeyScopeAndMemorizePreviousScope,
@ -94,18 +93,11 @@ export const RecordBoardColumnHeader = () => {
const { aggregateValue, aggregateLabel } =
useAggregateRecordsForRecordBoardColumn();
const { handleNewButtonClick } = useColumnNewCardActions(
columnDefinition.id ?? '',
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { isOpportunitiesCompanyFieldDisabled } =
useIsOpportunitiesCompanyFieldDisabled();
const isOpportunity =
objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity &&
!isOpportunitiesCompanyFieldDisabled;
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem: objectMetadataItem,
});
return (
<StyledColumn>
@ -153,7 +145,12 @@ export const RecordBoardColumnHeader = () => {
<LightIconButton
accent="tertiary"
Icon={IconPlus}
onClick={() => handleNewButtonClick('first', isOpportunity)}
onClick={() => {
createNewIndexRecord({
position: 'first',
[selectFieldMetadataItem.name]: columnDefinition.value,
});
}}
/>
)}
</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 { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { IconPlus } from 'twenty-ui';
const StyledNewButton = styled.button`
@ -21,23 +24,33 @@ const StyledNewButton = styled.button`
}
`;
export const RecordBoardColumnNewRecordButton = ({
columnId,
}: {
columnId: string;
}) => {
export const RecordBoardColumnNewRecordButton = () => {
const theme = useTheme();
const { handleNewButtonClick } = useColumnNewCardActions(columnId);
const { objectMetadataItem, selectFieldMetadataItem } =
useContext(RecordBoardContext);
const { columnDefinition } = useContext(RecordBoardColumnContext);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem: objectMetadataItem,
});
if (hasObjectReadOnlyPermission) {
return null;
}
return (
<StyledNewButton onClick={() => handleNewButtonClick('last', false)}>
<StyledNewButton
onClick={() => {
createNewIndexRecord({
position: 'last',
[selectFieldMetadataItem.name]: columnDefinition.value,
});
}}
>
<IconPlus size={theme.icon.size.md} />
New
</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 { 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 { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly';
@ -15,8 +16,11 @@ export const useIsFieldValueReadOnly = () => {
const { metadata, type } = fieldDefinition;
const recordFromStore = useRecoilValue<ObjectRecord | null>(
recordStoreFamilyState(recordId),
const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
recordStoreFamilySelector({
recordId,
fieldName: 'deletedAt',
}),
);
const contextStoreCurrentViewType = useRecoilComponentValueV2(
@ -34,7 +38,7 @@ export const useIsFieldValueReadOnly = () => {
fieldName: metadata.fieldName,
fieldType: type,
isObjectRemote: objectMetadataItem.isRemote,
isRecordDeleted: recordFromStore?.deletedAt,
isRecordDeleted: isDefined(recordDeletedAt),
hasObjectReadOnlyPermission,
contextStoreCurrentViewType,
});

View File

@ -18,7 +18,7 @@ export const RelationFromManyFieldDisplay = () => {
fieldDefinition?.metadata.relationObjectMetadataNameSingular;
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
undefined,
'',
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 { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
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 { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -44,23 +44,31 @@ export const RecordIndexAddRecordInGroupDropdown = ({
(field) => field.id === recordGroupFieldMetadata?.id,
);
const { createNewTableRecordInGroup } = useCreateNewTableRecordInGroup();
const { closeDropdown } = useDropdown(dropdownId);
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
});
const handleCreateNewTableRecordInGroup = useRecoilCallback(
({ set }) =>
(recordGroup: RecordGroupDefinition) => {
set(isRecordGroupTableSectionToggledState(recordGroup.id), true);
createNewTableRecordInGroup(recordGroup.id);
setActiveDropdownFocusIdAndMemorizePrevious(null);
if (!selectFieldMetadataItem) {
return;
}
createNewIndexRecord({
[selectFieldMetadataItem.name]: recordGroup.value,
});
closeDropdown();
},
[
closeDropdown,
createNewTableRecordInGroup,
setActiveDropdownFocusIdAndMemorizePrevious,
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 { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
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 { useInlineCell } from '../hooks/useInlineCell';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
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 { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
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 { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
@ -28,7 +27,6 @@ import {
RecordInlineCellContext,
RecordInlineCellContextProps,
} from './RecordInlineCellContext';
type RecordInlineCellProps = {
readonly?: 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 { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
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 { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { isDefined } from 'twenty-shared';
import { FieldMetadataType } from '~/generated/graphql';
type FieldsCardProps = {
@ -27,22 +26,20 @@ export const FieldsCard = ({
objectNameSingular,
objectRecordId,
}: FieldsCardProps) => {
const {
recordFromStore,
recordLoading,
objectMetadataItem,
labelIdentifierFieldMetadataItem,
isPrefetchLoading,
objectMetadataItems,
} = useRecordShowContainerData({
const { recordLoading, labelIdentifierFieldMetadataItem, isPrefetchLoading } =
useRecordShowContainerData({
objectNameSingular,
objectRecordId,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
objectRecordId,
});
const { objectMetadataItems } = useObjectMetadataItems();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular,
objectRecordId,
recordFromStore,
});
const availableFieldMetadataItems = objectMetadataItem.fields
@ -87,100 +84,94 @@ export const FieldsCard = ({
return (
<>
{isDefined(recordFromStore) && (
<>
<PropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
<>
{inlineRelationFieldMetadataItems?.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,
}}
>
<ActivityTargetsInlineCell
activityObjectNameSingular={
objectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activity={recordFromStore as Task | Note}
showLabel={true}
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>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
<>
{inlineRelationFieldMetadataItems?.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,
}}
>
<ActivityTargetsInlineCell
activityObjectNameSingular={
objectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activityRecordId={objectRecordId}
showLabel={true}
maxWidth={200}
/>
</FieldContext.Provider>
),
)}
</PropertyBox>
<RecordDetailDuplicatesSection
objectRecordId={objectRecordId}
objectNameSingular={objectNameSingular}
{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>
<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
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>
))}
</>
)}
</FieldContext.Provider>
))}
</>
);
};

View File

@ -41,7 +41,7 @@ export const ObjectRecordShowPageBreadcrumb = ({
objectLabelPlural: string;
labelIdentifierFieldMetadataItem?: FieldMetadataItem;
}) => {
const { record, loading } = useFindOneRecord({
const { loading } = useFindOneRecord({
objectNameSingular,
objectRecordId,
recordGqlFields: {
@ -52,7 +52,6 @@ export const ObjectRecordShowPageBreadcrumb = ({
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular,
objectRecordId,
recordFromStore: record ?? null,
});
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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreTargetedRecordsEffect';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
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 { useRecoilValue } from 'recoil';
type RecordShowContainerProps = {
objectNameSingular: string;
@ -23,16 +26,22 @@ export const RecordShowContainer = ({
isInRightDrawer = false,
isNewRightDrawerItemLoading = false,
}: RecordShowContainerProps) => {
const {
recordFromStore,
objectMetadataItem,
isPrefetchLoading,
recordLoading,
} = useRecordShowContainerData({
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { isPrefetchLoading, recordLoading } = useRecordShowContainerData({
objectNameSingular,
objectRecordId,
});
const recordDeletedAt = useRecoilValue<string | null>(
recordStoreFamilySelector({
recordId: objectRecordId,
fieldName: 'deletedAt',
}),
);
const { layout, tabs } = useRecordShowContainerTabs(
loading,
objectNameSingular as CoreObjectNameSingular,
@ -45,7 +54,7 @@ export const RecordShowContainer = ({
<RecordShowContainerContextStoreTargetedRecordsEffect
recordId={objectRecordId}
/>
{recordFromStore && recordFromStore.deletedAt && (
{recordDeletedAt && (
<InformationBannerDeletedRecord
recordId={objectRecordId}
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 { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
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 { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import { FieldMetadataType } from '~/generated/graphql';
@ -25,28 +28,36 @@ export const SummaryCard = ({
isNewRightDrawerItemLoading,
isInRightDrawer,
}: SummaryCardProps) => {
const {
recordFromStore,
recordLoading,
labelIdentifierFieldMetadataItem,
isPrefetchLoading,
recordIdentifier,
} = useRecordShowContainerData({
objectNameSingular,
objectRecordId,
});
const { recordLoading, labelIdentifierFieldMetadataItem, isPrefetchLoading } =
useRecordShowContainerData({
objectNameSingular,
objectRecordId,
});
const recordCreatedAt = useRecoilValue<string | null>(
recordStoreFamilySelector({
recordId: objectRecordId,
fieldName: 'createdAt',
}),
);
const { onUploadPicture, useUpdateOneObjectRecordMutation } =
useRecordShowContainerActions({
objectNameSingular,
objectRecordId,
recordFromStore,
});
const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular);
const isMobile = useIsMobile() || isInRightDrawer;
if (isNewRightDrawerItemLoading || !isDefined(recordFromStore)) {
const recordIdentifier = useRecoilValue(
recordStoreIdentifierFamilySelector({
objectNameSingular,
recordId: objectRecordId,
}),
);
if (isNewRightDrawerItemLoading || !isDefined(recordCreatedAt)) {
return <ShowPageSummaryCardSkeletonLoader />;
}
@ -58,7 +69,7 @@ export const SummaryCard = ({
icon={Icon}
iconColor={IconColor}
avatarPlaceholder={recordIdentifier?.name ?? ''}
date={recordFromStore.createdAt ?? ''}
date={recordCreatedAt ?? ''}
loading={isPrefetchLoading || recordLoading}
title={
<FieldContext.Provider

View File

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

View File

@ -1,12 +1,7 @@
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 { 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 { useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilState } from 'recoil';
type UseRecordShowContainerDataProps = {
objectNameSingular: string;
@ -17,10 +12,6 @@ export const useRecordShowContainerData = ({
objectNameSingular,
objectRecordId,
}: UseRecordShowContainerDataProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({
objectNameSingular,
@ -30,28 +21,11 @@ export const useRecordShowContainerData = ({
recordLoadingFamilyState(objectRecordId),
);
const [recordFromStore] = useRecoilState<ObjectRecord | null>(
recordStoreFamilyState(objectRecordId),
);
const recordIdentifier = useRecoilValue(
recordStoreIdentifierFamilySelector({
objectNameSingular,
recordId: objectRecordId,
}),
);
const isPrefetchLoading = useIsPrefetchLoading();
const { objectMetadataItems } = useObjectMetadataItems();
return {
recordFromStore,
recordLoading,
objectMetadataItem,
labelIdentifierFieldMetadataItem,
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 { RecordLayoutTab } from '@/ui/layout/tab/types/RecordLayoutTab';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import {
IconCalendarEvent,
IconHome,
IconMail,
IconNotes,
IconPrinter,
IconSettings,
IconHome,
} from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql';
@ -34,195 +35,200 @@ export const useRecordShowContainerTabs = (
// Object-specific layouts that override or extend the base layout
const OBJECT_SPECIFIC_LAYOUTS: Partial<
Record<CoreObjectNameSingular, RecordLayout>
> = {
[CoreObjectNameSingular.Note]: {
tabs: {
richText: {
title: 'Note',
position: 101,
Icon: IconNotes,
cards: [{ type: CardType.RichTextCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
> = useMemo(
() => ({
[CoreObjectNameSingular.Note]: {
tabs: {
richText: {
title: 'Note',
position: 101,
Icon: IconNotes,
cards: [{ type: CardType.RichTextCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
tasks: null,
notes: null,
},
tasks: null,
notes: null,
},
},
[CoreObjectNameSingular.Task]: {
tabs: {
richText: {
title: 'Note',
position: 101,
Icon: IconNotes,
cards: [{ type: CardType.RichTextCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
[CoreObjectNameSingular.Task]: {
tabs: {
richText: {
title: 'Note',
position: 101,
Icon: IconNotes,
cards: [{ type: CardType.RichTextCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
tasks: null,
notes: null,
},
tasks: null,
notes: null,
},
},
[CoreObjectNameSingular.Company]: {
tabs: {
emails: {
title: 'Emails',
position: 600,
Icon: IconMail,
cards: [{ type: CardType.EmailCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
[CoreObjectNameSingular.Company]: {
tabs: {
emails: {
title: 'Emails',
position: 600,
Icon: IconMail,
cards: [{ type: CardType.EmailCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
},
calendar: {
title: 'Calendar',
position: 700,
Icon: IconCalendarEvent,
cards: [{ type: CardType.CalendarCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
calendar: {
title: 'Calendar',
position: 700,
Icon: IconCalendarEvent,
cards: [{ type: CardType.CalendarCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
},
},
},
[CoreObjectNameSingular.Person]: {
tabs: {
emails: {
title: 'Emails',
position: 600,
Icon: IconMail,
cards: [{ type: CardType.EmailCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
[CoreObjectNameSingular.Person]: {
tabs: {
emails: {
title: 'Emails',
position: 600,
Icon: IconMail,
cards: [{ type: CardType.EmailCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
},
calendar: {
title: 'Calendar',
position: 700,
Icon: IconCalendarEvent,
cards: [{ type: CardType.CalendarCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
calendar: {
title: 'Calendar',
position: 700,
Icon: IconCalendarEvent,
cards: [{ type: CardType.CalendarCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
},
},
},
[CoreObjectNameSingular.Workflow]: {
hideSummaryAndFields: true,
tabs: {
workflow: {
title: 'Flow',
position: 0,
Icon: IconSettings,
cards: [{ type: CardType.WorkflowCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
[CoreObjectNameSingular.Workflow]: {
hideSummaryAndFields: true,
tabs: {
workflow: {
title: 'Flow',
position: 0,
Icon: IconSettings,
cards: [{ type: CardType.WorkflowCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
timeline: null,
fields: null,
},
timeline: null,
fields: null,
},
},
[CoreObjectNameSingular.WorkflowVersion]: {
tabs: {
workflowVersion: {
title: 'Flow',
position: 0,
Icon: IconSettings,
cards: [{ type: CardType.WorkflowVersionCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
[CoreObjectNameSingular.WorkflowVersion]: {
tabs: {
workflowVersion: {
title: 'Flow',
position: 0,
Icon: IconSettings,
cards: [{ type: CardType.WorkflowVersionCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
timeline: null,
},
timeline: null,
},
},
[CoreObjectNameSingular.WorkflowRun]: {
tabs: {
workflowRunOutput: {
title: 'Output',
position: 0,
Icon: IconPrinter,
cards: [{ type: CardType.WorkflowRunOutputCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
[CoreObjectNameSingular.WorkflowRun]: {
tabs: {
workflowRunOutput: {
title: 'Output',
position: 0,
Icon: IconPrinter,
cards: [{ type: CardType.WorkflowRunOutputCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
},
workflowRunFlow: {
title: 'Flow',
position: 0,
Icon: IconSettings,
cards: [{ type: CardType.WorkflowRunCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
workflowRunFlow: {
title: 'Flow',
position: 0,
Icon: IconSettings,
cards: [{ type: CardType.WorkflowRunCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
timeline: null,
},
timeline: null,
},
},
};
}),
[],
);
// Merge base layout with object-specific layout
const recordLayout: RecordLayout = {
...BASE_RECORD_LAYOUT,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular] || {}),
tabs: {
...BASE_RECORD_LAYOUT.tabs,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular]?.tabs || {}),
},
};
const recordLayout: RecordLayout = useMemo(() => {
return {
...BASE_RECORD_LAYOUT,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular] || {}),
tabs: {
...BASE_RECORD_LAYOUT.tabs,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular]?.tabs || {}),
},
};
}, [OBJECT_SPECIFIC_LAYOUTS, targetObjectNameSingular]);
return {
layout: recordLayout,

View File

@ -1,19 +1,8 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
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 { 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 { capitalize, isDefined } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared';
export const useRecordShowPage = (
propsObjectNameSingular: string,
@ -32,77 +21,13 @@ export const useRecordShowPage = (
}
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 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 {
objectNameSingular,
objectRecordId,
headerIcon,
loading,
pageTitle,
pageName,
isFavorite,
record,
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 { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
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 { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -52,20 +51,13 @@ export const RecordTable = () => {
recordTableId,
);
const hasPendingRecord = useRecoilComponentValueV2(
hasPendingRecordComponentSelector,
recordTableId,
);
const hasRecordGroups = useRecoilComponentValueV2(
hasRecordGroupsComponentSelector,
recordTableId,
);
const recordTableIsEmpty =
!isRecordTableInitialLoading &&
allRecordIds.length === 0 &&
!hasPendingRecord;
!isRecordTableInitialLoading && allRecordIds.length === 0;
const { resetTableRowSelection, setRowSelected } = useRecordTable({
recordTableId,

View File

@ -1,7 +1,6 @@
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
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 { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup';
import { useMoveSoftFocusToCurrentCellOnHover } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover';
@ -23,20 +22,6 @@ export const RecordTableNoRecordGroupBodyContextProvider = ({
}: RecordTableNoRecordGroupBodyContextProviderProps) => {
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 handleOpenTableCell = (args: OpenTableCellArgs) => {
@ -82,7 +67,6 @@ export const RecordTableNoRecordGroupBodyContextProvider = ({
return (
<RecordTableBodyContextProvider
value={{
onUpsertRecord: handleUpsertTableRecordNoRecordGroup,
onOpenTableCell: handleOpenTableCell,
onMoveFocus: handleMoveFocus,
onCloseTableCell: handleCloseTableCell,

View File

@ -1,7 +1,6 @@
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
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 { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup';
import { useMoveSoftFocusToCurrentCellOnHover } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover';
@ -20,26 +19,10 @@ type RecordTableRecordGroupBodyContextProviderProps = {
};
export const RecordTableRecordGroupBodyContextProvider = ({
recordGroupId,
children,
}: RecordTableRecordGroupBodyContextProviderProps) => {
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 handleOpenTableCell = (args: OpenTableCellArgs) => {
@ -52,8 +35,7 @@ export const RecordTableRecordGroupBodyContextProvider = ({
moveFocus(direction);
};
const { closeTableCellInGroup } =
useCloseRecordTableCellInGroup(recordGroupId);
const { closeTableCellInGroup } = useCloseRecordTableCellInGroup();
const handlecloseTableCellInGroup = () => {
closeTableCellInGroup();
@ -86,7 +68,6 @@ export const RecordTableRecordGroupBodyContextProvider = ({
return (
<RecordTableBodyContextProvider
value={{
onUpsertRecord: handleupsertTableRecordInGroup,
onOpenTableCell: handleOpenTableCell,
onMoveFocus: handleMoveFocus,
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 { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
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 { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore';
@ -57,7 +56,6 @@ export const RecordTableRecordGroupRows = () => {
/>
);
})}
<RecordTablePendingRecordGroupRow />
<RecordTableRecordGroupSectionAddNew />
<RecordTableRecordGroupSectionLoadMore />
<RecordTableAggregateFooter

View File

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

View File

@ -8,15 +8,6 @@ import { createRequiredContext } from '~/utils/createRequiredContext';
export type RecordTableBodyContextProps = {
recordGroupId?: string;
onUpsertRecord: ({
persistField,
recordId,
fieldName,
}: {
persistField: () => void;
recordId: string;
fieldName: string;
}) => void;
onOpenTableCell: (args: OpenTableCellArgs) => void;
onMoveFocus: (direction: MoveFocusDirection) => 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 { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
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';
type RecordTableEmptyHandlerProps = {
@ -25,15 +22,8 @@ export const RecordTableEmptyHandler = ({
recordTableId,
);
const pendingRecordId = useRecoilComponentValueV2(
recordTablePendingRecordIdComponentState,
recordTableId,
);
const recordTableIsEmpty =
!isRecordTableInitialLoading &&
allRecordIds.length === 0 &&
isNull(pendingRecordId);
!isRecordTableInitialLoading && allRecordIds.length === 0;
if (recordTableIsEmpty) {
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 { getEmptyStateSubTitle } from '@/object-record/record-table/empty-state/utils/getEmptyStateSubTitle';
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 = () => {
const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow();
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { createNewTableRecord } = useCreateNewTableRecord({
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
recordTableId,
});
const handleButtonClick = () => {
createNewTableRecord();
createNewIndexRecord();
};
const objectLabel = useObjectLabel(objectMetadataItem);

View File

@ -3,18 +3,17 @@ import { IconPlus } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
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 = () => {
const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow();
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { createNewTableRecord } = useCreateNewTableRecord({
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
recordTableId,
});
const handleButtonClick = () => {
createNewTableRecord();
createNewIndexRecord();
};
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 { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -25,7 +24,6 @@ export const RecordTableNoRecordGroupBody = () => {
<RecordTableNoRecordGroupBodyContextProvider>
<RecordTableBodyDragDropContextProvider>
<RecordTableBodyDroppable>
<RecordTablePendingRow />
<RecordTableNoRecordGroupRows />
</RecordTableBodyDroppable>
</RecordTableBodyDragDropContextProvider>

View File

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

View File

@ -32,7 +32,6 @@ jest.mock('@/ui/utilities/hotkey/hooks/useSetHotkeyScope', () => ({
const onColumnsChange = jest.fn();
const recordTableId = 'scopeId';
const recordGroupId = 'recordGroupId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot
@ -86,7 +85,7 @@ describe('useCloseRecordTableCellInGroup', () => {
currentTableCellInEditModePosition,
);
return {
...useCloseRecordTableCellInGroup(recordGroupId),
...useCloseRecordTableCellInGroup(),
...useDragSelect(),
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 { 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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
export const useCloseRecordTableCellInGroup = (recordGroupId: string) => {
export const useCloseRecordTableCellInGroup = () => {
const { recordTableId } = useRecordTableContextOrThrow();
const setHotkeyScope = useSetHotkeyScope();
@ -24,26 +22,15 @@ export const useCloseRecordTableCellInGroup = (recordGroupId: string) => {
const closeCurrentTableCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableId);
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
recordTableId,
);
const closeTableCellInGroup = useRecoilCallback(
({ reset }) =>
() => {
toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
reset(recordTablePendingRecordIdByGroupFamilyState(recordGroupId));
},
() => () => {
toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
},
[
closeCurrentTableCellInEditMode,
recordGroupId,
recordTablePendingRecordIdByGroupFamilyState,
setDragSelectionStartEnabled,
setHotkeyScope,
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 { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
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 { 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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useCallback } from 'react';
export const useCloseRecordTableCellNoGroup = () => {
@ -26,23 +22,13 @@ export const useCloseRecordTableCellNoGroup = () => {
const closeCurrentTableCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableId);
const pendingRecordIdState = useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdComponentState,
recordTableId,
);
const resetRecordTablePendingRecordId =
useResetRecoilState(pendingRecordIdState);
const closeTableCellNoGroup = useCallback(() => {
toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
resetRecordTablePendingRecordId();
}, [
closeCurrentTableCellInEditMode,
resetRecordTablePendingRecordId,
setDragSelectionStartEnabled,
setHotkeyScope,
toggleClickOutsideListener,

View File

@ -6,7 +6,7 @@ import { IconPlus, LightIconButton } from 'twenty-ui';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
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 { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
@ -107,7 +107,7 @@ type RecordTableHeaderCellProps = {
export const RecordTableHeaderCell = ({
column,
}: RecordTableHeaderCellProps) => {
const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow();
const { objectMetadataItem } = useRecordTableContextOrThrow();
const resizeFieldOffsetState = useRecoilComponentCallbackStateV2(
resizeFieldOffsetComponentState,
@ -202,13 +202,12 @@ export const RecordTableHeaderCell = ({
const disableColumnResize =
column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft;
const { createNewTableRecord } = useCreateNewTableRecord({
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
recordTableId,
});
const handlePlusButtonClick = () => {
createNewTableRecord();
createNewIndexRecord();
};
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 { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
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 { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { IconPlus } from 'twenty-ui';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { IconPlus } from 'twenty-ui';
export const RecordTableRecordGroupSectionAddNew = () => {
const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow();
const { objectMetadataItem } = useRecordTableContextOrThrow();
const currentRecordGroupId = useCurrentRecordGroupId();
@ -17,16 +19,19 @@ export const RecordTableRecordGroupSectionAddNew = () => {
recordIndexAllRecordIdsComponentSelector,
);
const recordGroup = useRecoilValue(
recordGroupDefinitionFamilyState(currentRecordGroupId),
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { createNewTableRecordInGroup } = useCreateNewTableRecord({
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
recordTableId,
});
const handleAddNewRecord = () => {
createNewTableRecordInGroup(currentRecordGroupId);
};
const fieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.id === recordGroup?.fieldMetadataId,
);
if (hasObjectReadOnlyPermission) {
return null;
@ -38,7 +43,16 @@ export const RecordTableRecordGroupSectionAddNew = () => {
draggableIndex={recordIds.length + 2}
LeftIcon={IconPlus}
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 { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
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 { 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 { PageBody } from '@/ui/layout/page/components/PageBody';
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 { RecordShowPageTitle } from '~/pages/object-record/RecordShowPageTitle';
export const RecordShowPage = () => {
const parameters = useParams<{
@ -23,14 +24,7 @@ export const RecordShowPage = () => {
objectRecordId: string;
}>();
const {
pageTitle,
objectNameSingular,
objectRecordId,
headerIcon,
loading,
pageName,
} = useRecordShowPage(
const { objectNameSingular, objectRecordId, headerIcon } = useRecordShowPage(
parameters.objectNameSingular ?? '',
parameters.objectRecordId ?? '',
);
@ -54,7 +48,10 @@ export const RecordShowPage = () => {
>
<RecordValueSetterEffect recordId={objectRecordId} />
<PageContainer>
<PageTitle title={pageTitle} />
<RecordShowPageTitle
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
/>
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
@ -64,12 +61,18 @@ export const RecordShowPage = () => {
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{ labelIdentifierValue: pageName }}
value={{
recordId: objectRecordId,
}}
>
<RecordShowEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={loading}
loading={false}
/>
</TimelineActivityContext.Provider>
</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: () => {},
onMoveFocus: () => {},
onMoveSoftFocusToCurrentCell: () => {},
onUpsertRecord: () => {},
}}
>
<Story />

View File

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