From 8455e154432e247f848f12f91fde5fe60493c61b Mon Sep 17 00:00:00 2001 From: Kanav Arora Date: Fri, 5 Jan 2024 22:31:51 +0530 Subject: [PATCH] Behaviour Fix on new record addition (#3113) * Delete record if no company added * EditMode on First column of new row added * Fix * Minor fixes * Passed scopeId * Changed FieldInputs to accept onChange handler * Removed getFieldType --------- Co-authored-by: Lucas Bordeau --- .../components/RecordTablePage.tsx | 15 ++- .../hooks/useIsFieldEditModeValueEmpty.ts | 22 ++++ .../field/hooks/useSaveFieldEditModeValue.ts | 112 ++++++++++++++++++ .../input/components/CurrencyFieldInput.tsx | 11 ++ .../input/components/DateFieldInput.tsx | 7 ++ .../input/components/EmailFieldInput.tsx | 7 ++ .../input/components/FullNameFieldInput.tsx | 22 +++- .../input/components/LinkFieldInput.tsx | 11 ++ .../input/components/NumberFieldInput.tsx | 8 ++ .../input/components/PhoneFieldInput.tsx | 8 ++ .../input/components/TextFieldInput.tsx | 7 ++ .../entityFieldsEditModeValueFamilyState.ts | 9 ++ ...entityFieldsEditModeValueFamilySelector.ts | 18 +++ ...sEntityFieldEditModeEmptyFamilySelector.ts | 31 +++++ .../isEntityFieldEmptyFamilySelector.ts | 77 ++---------- .../field/utils/isFieldValueEmpty.ts | 69 +++++++++++ .../components/RecordTableWithWrappers.tsx | 76 ++++++------ .../contexts/EntityDeleteHookContext.ts | 5 + .../components/RecordTableCell.tsx | 38 +++++- .../useMoveSoftFocusToCurrentCellOnHover.ts | 14 ++- .../hooks/useSelectedTableCellEditMode.ts | 21 ++++ .../hooks/useSetSoftFocus.ts | 35 ++++++ .../useSetSoftFocusOnCurrentTableCell.ts | 40 +------ .../record-table-cell/hooks/useTableCell.ts | 25 +++- .../ui/field/input/components/DateInput.tsx | 3 + .../input/components/DoubleTextInput.tsx | 7 ++ .../ui/field/input/components/PhoneInput.tsx | 9 +- .../ui/field/input/components/TextInput.tsx | 3 + 28 files changed, 551 insertions(+), 159 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/field/hooks/useIsFieldEditModeValueEmpty.ts create mode 100644 packages/twenty-front/src/modules/object-record/field/hooks/useSaveFieldEditModeValue.ts create mode 100644 packages/twenty-front/src/modules/object-record/field/states/entityFieldsEditModeValueFamilyState.ts create mode 100644 packages/twenty-front/src/modules/object-record/field/states/selectors/entityFieldsEditModeValueFamilySelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/field/states/selectors/isEntityFieldEditModeEmptyFamilySelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/field/utils/isFieldValueEmpty.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/contexts/EntityDeleteHookContext.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetSoftFocus.ts diff --git a/packages/twenty-front/src/modules/object-record/components/RecordTablePage.tsx b/packages/twenty-front/src/modules/object-record/components/RecordTablePage.tsx index 57c078632..b41b08deb 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordTablePage.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordTablePage.tsx @@ -10,12 +10,15 @@ import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObje import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar'; import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu'; +import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode'; +import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useTableCell'; import { useIcons } from '@/ui/display/icon/hooks/useIcons'; import { PageAddButton } from '@/ui/layout/page/PageAddButton'; import { PageBody } from '@/ui/layout/page/PageBody'; import { PageContainer } from '@/ui/layout/page/PageContainer'; import { PageHeader } from '@/ui/layout/page/PageHeader'; import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { RecordTableContainer } from './RecordTableContainer'; @@ -57,11 +60,19 @@ export const RecordTablePage = () => { objectNameSingular, }); + const recordTableId = objectNamePlural ?? ''; + + const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({ + scopeId: recordTableId, + }); + const setHotkeyScope = useSetHotkeyScope(); + const handleAddButtonClick = async () => { await createOneObject?.({}); - }; - const recordTableId = objectNamePlural ?? ''; + setSelectedTableCellEditMode(0, 0); + setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes); + }; return ( diff --git a/packages/twenty-front/src/modules/object-record/field/hooks/useIsFieldEditModeValueEmpty.ts b/packages/twenty-front/src/modules/object-record/field/hooks/useIsFieldEditModeValueEmpty.ts new file mode 100644 index 000000000..515c17140 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/field/hooks/useIsFieldEditModeValueEmpty.ts @@ -0,0 +1,22 @@ +import { useContext } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { isEntityFieldEditModeEmptyFamilySelector } from '@/object-record/field/states/selectors/isEntityFieldEditModeEmptyFamilySelector'; + +import { FieldContext } from '../contexts/FieldContext'; + +export const useIsFieldEditModeValueEmpty = () => { + const { entityId, fieldDefinition } = useContext(FieldContext); + + const isFieldEditModeValueEmpty = useRecoilValue( + isEntityFieldEditModeEmptyFamilySelector({ + fieldDefinition: { + type: fieldDefinition.type, + }, + fieldName: fieldDefinition.metadata.fieldName, + entityId, + }), + ); + + return isFieldEditModeValueEmpty; +}; diff --git a/packages/twenty-front/src/modules/object-record/field/hooks/useSaveFieldEditModeValue.ts b/packages/twenty-front/src/modules/object-record/field/hooks/useSaveFieldEditModeValue.ts new file mode 100644 index 000000000..db9c8d870 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/field/hooks/useSaveFieldEditModeValue.ts @@ -0,0 +1,112 @@ +import { useContext } from 'react'; +import { useRecoilCallback } from 'recoil'; + +import { entityFieldsEditModeValueFamilySelector } from '@/object-record/field/states/selectors/entityFieldsEditModeValueFamilySelector'; +import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName'; +import { isFieldFullNameValue } from '@/object-record/field/types/guards/isFieldFullNameValue'; + +import { FieldContext } from '../contexts/FieldContext'; +import { isFieldBoolean } from '../types/guards/isFieldBoolean'; +import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue'; +import { isFieldCurrency } from '../types/guards/isFieldCurrency'; +import { isFieldCurrencyValue } from '../types/guards/isFieldCurrencyValue'; +import { isFieldDateTime } from '../types/guards/isFieldDateTime'; +import { isFieldDateTimeValue } from '../types/guards/isFieldDateTimeValue'; +import { isFieldEmail } from '../types/guards/isFieldEmail'; +import { isFieldEmailValue } from '../types/guards/isFieldEmailValue'; +import { isFieldLink } from '../types/guards/isFieldLink'; +import { isFieldLinkValue } from '../types/guards/isFieldLinkValue'; +import { isFieldNumber } from '../types/guards/isFieldNumber'; +import { isFieldNumberValue } from '../types/guards/isFieldNumberValue'; +import { isFieldPhone } from '../types/guards/isFieldPhone'; +import { isFieldPhoneValue } from '../types/guards/isFieldPhoneValue'; +import { isFieldRating } from '../types/guards/isFieldRating'; +import { isFieldRatingValue } from '../types/guards/isFieldRatingValue'; +import { isFieldRelation } from '../types/guards/isFieldRelation'; +import { isFieldRelationValue } from '../types/guards/isFieldRelationValue'; +import { isFieldText } from '../types/guards/isFieldText'; +import { isFieldTextValue } from '../types/guards/isFieldTextValue'; + +export const useSaveFieldEditModeValue = () => { + const { entityId, fieldDefinition } = useContext(FieldContext); + + const saveFieldEditModeValue = useRecoilCallback( + ({ set }) => + (currentValue: unknown) => { + const fieldIsRelation = + isFieldRelation(fieldDefinition) && + isFieldRelationValue(currentValue); + + const fieldIsText = + isFieldText(fieldDefinition) && isFieldTextValue(currentValue); + + const fieldIsEmail = + isFieldEmail(fieldDefinition) && isFieldEmailValue(currentValue); + + const fieldIsDateTime = + isFieldDateTime(fieldDefinition) && + isFieldDateTimeValue(currentValue); + + const fieldIsLink = + isFieldLink(fieldDefinition) && isFieldLinkValue(currentValue); + + const fieldIsBoolean = + isFieldBoolean(fieldDefinition) && isFieldBooleanValue(currentValue); + + const fieldIsProbability = + isFieldRating(fieldDefinition) && isFieldRatingValue(currentValue); + + const fieldIsNumber = + isFieldNumber(fieldDefinition) && isFieldNumberValue(currentValue); + + const fieldIsCurrency = + isFieldCurrency(fieldDefinition) && + isFieldCurrencyValue(currentValue); + + const fieldIsFullName = + isFieldFullName(fieldDefinition) && + isFieldFullNameValue(currentValue); + + const fieldIsPhone = + isFieldPhone(fieldDefinition) && isFieldPhoneValue(currentValue); + + if (fieldIsRelation) { + const fieldName = fieldDefinition.metadata.fieldName; + + set( + entityFieldsEditModeValueFamilySelector({ entityId, fieldName }), + currentValue, + ); + } else if ( + fieldIsText || + fieldIsBoolean || + fieldIsEmail || + fieldIsProbability || + fieldIsNumber || + fieldIsDateTime || + fieldIsPhone || + fieldIsLink || + fieldIsCurrency || + fieldIsFullName + ) { + const fieldName = fieldDefinition.metadata.fieldName; + + set( + entityFieldsEditModeValueFamilySelector({ entityId, fieldName }), + currentValue, + ); + } else { + throw new Error( + `Invalid value to save: ${JSON.stringify( + currentValue, + )} for type : ${ + fieldDefinition.type + }, type may not be implemented in useSaveFieldEditModeValue.`, + ); + } + }, + [entityId, fieldDefinition], + ); + + return saveFieldEditModeValue; +}; diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/CurrencyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/CurrencyFieldInput.tsx index c1dc73e55..cdaa4b18a 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/CurrencyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/CurrencyFieldInput.tsx @@ -1,3 +1,4 @@ +import { useSaveFieldEditModeValue } from '@/object-record/field/hooks/useSaveFieldEditModeValue'; import { TextInput } from '@/ui/field/input/components/TextInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; @@ -27,6 +28,8 @@ export const CurrencyFieldInput = ({ persistCurrencyField, } = useCurrencyField(); + const saveEditModeValue = useSaveFieldEditModeValue(); + const handleEnter = (newValue: string) => { onEnter?.(() => { persistCurrencyField({ @@ -75,6 +78,13 @@ export const CurrencyFieldInput = ({ ); }; + const handleChange = (newValue: string) => { + saveEditModeValue({ + amountText: newValue, + currencyCode: initialCurrencyCode, + }); + }; + return ( ); diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/DateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/DateFieldInput.tsx index e22f4601a..6172b01c5 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/DateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/DateFieldInput.tsx @@ -1,3 +1,4 @@ +import { useSaveFieldEditModeValue } from '@/object-record/field/hooks/useSaveFieldEditModeValue'; import { DateInput } from '@/ui/field/input/components/DateInput'; import { Nullable } from '~/types/Nullable'; @@ -20,6 +21,7 @@ export const DateFieldInput = ({ const { fieldValue, hotkeyScope, clearable } = useDateTimeField(); const persistField = usePersistField(); + const saveEditModeValue = useSaveFieldEditModeValue(); const persistDate = (newDate: Nullable) => { if (!newDate) { @@ -46,6 +48,10 @@ export const DateFieldInput = ({ onClickOutside?.(() => persistDate(newDate)); }; + const handleChange = (newDate: Nullable) => { + saveEditModeValue(newDate); + }; + const dateValue = fieldValue ? new Date(fieldValue) : null; return ( @@ -56,6 +62,7 @@ export const DateFieldInput = ({ onEscape={handleEscape} value={dateValue} clearable={clearable} + onChange={handleChange} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/EmailFieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/EmailFieldInput.tsx index d34f2ce92..c5e1ee5fe 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/EmailFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/EmailFieldInput.tsx @@ -1,3 +1,4 @@ +import { useSaveFieldEditModeValue } from '@/object-record/field/hooks/useSaveFieldEditModeValue'; import { TextInput } from '@/ui/field/input/components/TextInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; @@ -24,6 +25,7 @@ export const EmailFieldInput = ({ const { fieldDefinition, initialValue, hotkeyScope } = useEmailField(); const persistField = usePersistField(); + const saveEditModeValue = useSaveFieldEditModeValue(); const handleEnter = (newText: string) => { onEnter?.(() => persistField(newText)); @@ -48,6 +50,10 @@ export const EmailFieldInput = ({ onShiftTab?.(() => persistField(newText)); }; + const handleChange = (newText: string) => { + saveEditModeValue(newText); + }; + return ( ); diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/FullNameFieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/FullNameFieldInput.tsx index 09b482e51..1a1896aef 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/FullNameFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/FullNameFieldInput.tsx @@ -1,3 +1,4 @@ +import { useSaveFieldEditModeValue } from '@/object-record/field/hooks/useSaveFieldEditModeValue'; import { useFullNameField } from '@/object-record/field/meta-types/hooks/useFullNameField'; import { FieldDoubleText } from '@/object-record/field/types/FieldDoubleText'; import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput'; @@ -7,6 +8,12 @@ import { usePersistField } from '../../../hooks/usePersistField'; import { FieldInputEvent } from './DateFieldInput'; +const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS = + 'F‌‌irst name'; + +const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS = + 'L‌‌ast name'; + export type FullNameFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; @@ -25,6 +32,8 @@ export const FullNameFieldInput = ({ const { hotkeyScope, initialValue } = useFullNameField(); const persistField = usePersistField(); + const saveEditModeValue = useSaveFieldEditModeValue(); + const convertToFullName = (newDoubleText: FieldDoubleText) => { return { firstName: newDoubleText.firstValue, @@ -55,19 +64,28 @@ export const FullNameFieldInput = ({ onShiftTab?.(() => persistField(convertToFullName(newDoubleText))); }; + const handleChange = (newDoubleText: FieldDoubleText) => { + saveEditModeValue(convertToFullName(newDoubleText)); + }; + return ( ); diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/LinkFieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/LinkFieldInput.tsx index b5dab9112..594457368 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/LinkFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/LinkFieldInput.tsx @@ -1,3 +1,4 @@ +import { useSaveFieldEditModeValue } from '@/object-record/field/hooks/useSaveFieldEditModeValue'; import { TextInput } from '@/ui/field/input/components/TextInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; @@ -22,6 +23,8 @@ export const LinkFieldInput = ({ }: LinkFieldInputProps) => { const { initialValue, hotkeyScope, persistLinkField } = useLinkField(); + const saveEditModeValue = useSaveFieldEditModeValue(); + const handleEnter = (newURL: string) => { onEnter?.(() => persistLinkField({ @@ -70,6 +73,13 @@ export const LinkFieldInput = ({ ); }; + const handleChange = (newURL: string) => { + saveEditModeValue({ + url: newURL, + label: newURL, + }); + }; + return ( ); diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/NumberFieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/NumberFieldInput.tsx index 5fde281e5..5f3de7804 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/NumberFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/NumberFieldInput.tsx @@ -1,3 +1,4 @@ +import { useSaveFieldEditModeValue } from '@/object-record/field/hooks/useSaveFieldEditModeValue'; import { TextInput } from '@/ui/field/input/components/TextInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; @@ -23,6 +24,8 @@ export const NumberFieldInput = ({ const { fieldDefinition, initialValue, hotkeyScope, persistNumberField } = useNumberField(); + const saveEditModeValue = useSaveFieldEditModeValue(); + const handleEnter = (newText: string) => { onEnter?.(() => persistNumberField(newText)); }; @@ -46,6 +49,10 @@ export const NumberFieldInput = ({ onShiftTab?.(() => persistNumberField(newText)); }; + const handleChange = (newText: string) => { + saveEditModeValue(newText); + }; + return ( ); diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/PhoneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/PhoneFieldInput.tsx index 712f5c1e9..ff5f112cf 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/PhoneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/PhoneFieldInput.tsx @@ -1,3 +1,4 @@ +import { useSaveFieldEditModeValue } from '@/object-record/field/hooks/useSaveFieldEditModeValue'; import { PhoneInput } from '@/ui/field/input/components/PhoneInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; @@ -23,6 +24,8 @@ export const PhoneFieldInput = ({ const { fieldDefinition, initialValue, hotkeyScope, persistPhoneField } = usePhoneField(); + const saveEditModeValue = useSaveFieldEditModeValue(); + const handleEnter = (newText: string) => { onEnter?.(() => persistPhoneField(newText)); }; @@ -46,6 +49,10 @@ export const PhoneFieldInput = ({ onShiftTab?.(() => persistPhoneField(newText)); }; + const handleChange = (newText: string) => { + saveEditModeValue(newText); + }; + return ( ); diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/TextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/TextFieldInput.tsx index 78f82ccb8..ea83380c4 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/TextFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/TextFieldInput.tsx @@ -1,3 +1,4 @@ +import { useSaveFieldEditModeValue } from '@/object-record/field/hooks/useSaveFieldEditModeValue'; import { TextInput } from '@/ui/field/input/components/TextInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; @@ -24,6 +25,7 @@ export const TextFieldInput = ({ const { fieldDefinition, initialValue, hotkeyScope } = useTextField(); const persistField = usePersistField(); + const saveEditModeValue = useSaveFieldEditModeValue(); const handleEnter = (newText: string) => { onEnter?.(() => persistField(newText)); @@ -48,6 +50,10 @@ export const TextFieldInput = ({ onShiftTab?.(() => persistField(newText)); }; + const handleChange = (newText: string) => { + saveEditModeValue(newText); + }; + return ( ); diff --git a/packages/twenty-front/src/modules/object-record/field/states/entityFieldsEditModeValueFamilyState.ts b/packages/twenty-front/src/modules/object-record/field/states/entityFieldsEditModeValueFamilyState.ts new file mode 100644 index 000000000..72d6def07 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/field/states/entityFieldsEditModeValueFamilyState.ts @@ -0,0 +1,9 @@ +import { atomFamily } from 'recoil'; + +export const entityFieldsEditModeValueFamilyState = atomFamily< + Record | null, + string +>({ + key: 'entityFieldsEditModeValueFamilyState', + default: null, +}); diff --git a/packages/twenty-front/src/modules/object-record/field/states/selectors/entityFieldsEditModeValueFamilySelector.ts b/packages/twenty-front/src/modules/object-record/field/states/selectors/entityFieldsEditModeValueFamilySelector.ts new file mode 100644 index 000000000..c7478e544 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/field/states/selectors/entityFieldsEditModeValueFamilySelector.ts @@ -0,0 +1,18 @@ +import { selectorFamily } from 'recoil'; + +import { entityFieldsEditModeValueFamilyState } from '@/object-record/field/states/entityFieldsEditModeValueFamilyState'; + +export const entityFieldsEditModeValueFamilySelector = selectorFamily({ + key: 'entityFieldsEditModeValueFamilySelector', + get: + ({ fieldName, entityId }: { fieldName: string; entityId: string }) => + ({ get }) => + get(entityFieldsEditModeValueFamilyState(entityId))?.[fieldName] as T, + set: + ({ fieldName, entityId }: { fieldName: string; entityId: string }) => + ({ set }, newValue: T) => + set(entityFieldsEditModeValueFamilyState(entityId), (prevState) => ({ + ...prevState, + [fieldName]: newValue, + })), +}); diff --git a/packages/twenty-front/src/modules/object-record/field/states/selectors/isEntityFieldEditModeEmptyFamilySelector.ts b/packages/twenty-front/src/modules/object-record/field/states/selectors/isEntityFieldEditModeEmptyFamilySelector.ts new file mode 100644 index 000000000..1421ab30a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/field/states/selectors/isEntityFieldEditModeEmptyFamilySelector.ts @@ -0,0 +1,31 @@ +import { selectorFamily } from 'recoil'; + +import { entityFieldsEditModeValueFamilyState } from '@/object-record/field/states/entityFieldsEditModeValueFamilyState'; +import { isFieldValueEmpty } from '@/object-record/field/utils/isFieldValueEmpty'; + +import { FieldDefinition } from '../../types/FieldDefinition'; +import { FieldMetadata } from '../../types/FieldMetadata'; + +export const isEntityFieldEditModeEmptyFamilySelector = selectorFamily({ + key: 'isEntityFieldEditModeEmptyFamilySelector', + get: ({ + fieldDefinition, + fieldName, + entityId, + }: { + fieldDefinition: Pick, 'type'>; + fieldName: string; + entityId: string; + }) => { + return ({ get }) => { + const fieldValue = get(entityFieldsEditModeValueFamilyState(entityId))?.[ + fieldName + ]; + + return isFieldValueEmpty({ + fieldDefinition, + fieldValue, + }); + }; + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/field/states/selectors/isEntityFieldEmptyFamilySelector.ts b/packages/twenty-front/src/modules/object-record/field/states/selectors/isEntityFieldEmptyFamilySelector.ts index 739fdfdd4..b075a8438 100644 --- a/packages/twenty-front/src/modules/object-record/field/states/selectors/isEntityFieldEmptyFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-record/field/states/selectors/isEntityFieldEmptyFamilySelector.ts @@ -1,28 +1,10 @@ import { selectorFamily } from 'recoil'; -import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName'; -import { isFieldFullNameValue } from '@/object-record/field/types/guards/isFieldFullNameValue'; -import { isFieldSelect } from '@/object-record/field/types/guards/isFieldSelect'; -import { isFieldUuid } from '@/object-record/field/types/guards/isFieldUuid'; -import { assertNotNull } from '~/utils/assert'; +import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState'; +import { isFieldValueEmpty } from '@/object-record/field/utils/isFieldValueEmpty'; import { FieldDefinition } from '../../types/FieldDefinition'; import { FieldMetadata } from '../../types/FieldMetadata'; -import { isFieldBoolean } from '../../types/guards/isFieldBoolean'; -import { isFieldCurrency } from '../../types/guards/isFieldCurrency'; -import { isFieldCurrencyValue } from '../../types/guards/isFieldCurrencyValue'; -import { isFieldDateTime } from '../../types/guards/isFieldDateTime'; -import { isFieldEmail } from '../../types/guards/isFieldEmail'; -import { isFieldLink } from '../../types/guards/isFieldLink'; -import { isFieldLinkValue } from '../../types/guards/isFieldLinkValue'; -import { isFieldNumber } from '../../types/guards/isFieldNumber'; -import { isFieldRating } from '../../types/guards/isFieldRating'; -import { isFieldRelation } from '../../types/guards/isFieldRelation'; -import { isFieldRelationValue } from '../../types/guards/isFieldRelationValue'; -import { isFieldText } from '../../types/guards/isFieldText'; -import { entityFieldsFamilyState } from '../entityFieldsFamilyState'; - -const isValueEmpty = (value: unknown) => !assertNotNull(value) || value === ''; export const isEntityFieldEmptyFamilySelector = selectorFamily({ key: 'isEntityFieldEmptyFamilySelector', @@ -36,57 +18,12 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({ entityId: string; }) => { return ({ get }) => { - if ( - isFieldUuid(fieldDefinition) || - isFieldText(fieldDefinition) || - isFieldDateTime(fieldDefinition) || - isFieldNumber(fieldDefinition) || - isFieldRating(fieldDefinition) || - isFieldEmail(fieldDefinition) || - isFieldBoolean(fieldDefinition) || - isFieldSelect(fieldDefinition) - //|| isFieldPhone(fieldDefinition) - ) { - const fieldValue = get(entityFieldsFamilyState(entityId))?.[ - fieldName - ] as string | number | boolean | null; + const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName]; - return isValueEmpty(fieldValue); - } - - if (isFieldRelation(fieldDefinition)) { - const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName]; - - return isFieldRelationValue(fieldValue) && isValueEmpty(fieldValue); - } - - if (isFieldCurrency(fieldDefinition)) { - const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName]; - - return ( - !isFieldCurrencyValue(fieldValue) || - isValueEmpty(fieldValue?.amountMicros) - ); - } - - if (isFieldFullName(fieldDefinition)) { - const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName]; - - return ( - !isFieldFullNameValue(fieldValue) || - isValueEmpty(fieldValue?.firstName + fieldValue?.lastName) - ); - } - - if (isFieldLink(fieldDefinition)) { - const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName]; - - return !isFieldLinkValue(fieldValue) || isValueEmpty(fieldValue?.url); - } - - throw new Error( - `Entity field type not supported in isEntityFieldEmptyFamilySelector : ${fieldDefinition.type}}`, - ); + return isFieldValueEmpty({ + fieldDefinition, + fieldValue, + }); }; }, }); diff --git a/packages/twenty-front/src/modules/object-record/field/utils/isFieldValueEmpty.ts b/packages/twenty-front/src/modules/object-record/field/utils/isFieldValueEmpty.ts new file mode 100644 index 000000000..8a83c7573 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/field/utils/isFieldValueEmpty.ts @@ -0,0 +1,69 @@ +import { FieldDefinition } from '@/object-record/field/types/FieldDefinition'; +import { FieldMetadata } from '@/object-record/field/types/FieldMetadata'; +import { isFieldBoolean } from '@/object-record/field/types/guards/isFieldBoolean'; +import { isFieldCurrency } from '@/object-record/field/types/guards/isFieldCurrency'; +import { isFieldCurrencyValue } from '@/object-record/field/types/guards/isFieldCurrencyValue'; +import { isFieldDateTime } from '@/object-record/field/types/guards/isFieldDateTime'; +import { isFieldEmail } from '@/object-record/field/types/guards/isFieldEmail'; +import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName'; +import { isFieldFullNameValue } from '@/object-record/field/types/guards/isFieldFullNameValue'; +import { isFieldLink } from '@/object-record/field/types/guards/isFieldLink'; +import { isFieldLinkValue } from '@/object-record/field/types/guards/isFieldLinkValue'; +import { isFieldNumber } from '@/object-record/field/types/guards/isFieldNumber'; +import { isFieldRating } from '@/object-record/field/types/guards/isFieldRating'; +import { isFieldRelation } from '@/object-record/field/types/guards/isFieldRelation'; +import { isFieldRelationValue } from '@/object-record/field/types/guards/isFieldRelationValue'; +import { isFieldSelect } from '@/object-record/field/types/guards/isFieldSelect'; +import { isFieldText } from '@/object-record/field/types/guards/isFieldText'; +import { isFieldUuid } from '@/object-record/field/types/guards/isFieldUuid'; +import { assertNotNull } from '~/utils/assert'; + +const isValueEmpty = (value: unknown) => !assertNotNull(value) || value === ''; + +export const isFieldValueEmpty = ({ + fieldDefinition, + fieldValue, +}: { + fieldDefinition: Pick, 'type'>; + fieldValue: unknown; +}) => { + if ( + isFieldUuid(fieldDefinition) || + isFieldText(fieldDefinition) || + isFieldDateTime(fieldDefinition) || + isFieldNumber(fieldDefinition) || + isFieldRating(fieldDefinition) || + isFieldEmail(fieldDefinition) || + isFieldBoolean(fieldDefinition) || + isFieldSelect(fieldDefinition) + //|| isFieldPhone(fieldDefinition) + ) { + return isValueEmpty(fieldValue); + } + + if (isFieldRelation(fieldDefinition)) { + return isFieldRelationValue(fieldValue) && isValueEmpty(fieldValue); + } + + if (isFieldCurrency(fieldDefinition)) { + return ( + !isFieldCurrencyValue(fieldValue) || + isValueEmpty(fieldValue?.amountMicros) + ); + } + + if (isFieldFullName(fieldDefinition)) { + return ( + !isFieldFullNameValue(fieldValue) || + isValueEmpty(fieldValue?.firstName + fieldValue?.lastName) + ); + } + + if (isFieldLink(fieldDefinition)) { + return !isFieldLinkValue(fieldValue) || isValueEmpty(fieldValue?.url); + } + + throw new Error( + `Entity field type not supported in isEntityFieldEditModeEmptyFamilySelector : ${fieldDefinition.type}}`, + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index b99e6daa5..c6f6f1371 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -4,9 +4,11 @@ import { useRecoilCallback, useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { RecordTable } from '@/object-record/record-table/components/RecordTable'; import { RecordTableFirstColumnScrollObserver } from '@/object-record/record-table/components/RecordTableFirstColumnScrollObserver'; import { RecordTableRefContextWrapper } from '@/object-record/record-table/components/RecordTableRefContext'; +import { EntityDeleteContext } from '@/object-record/record-table/contexts/EntityDeleteHookContext'; import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates'; import { getRecordTableScopeInjector } from '@/object-record/record-table/utils/getRecordTableScopeInjector'; import { IconPlus } from '@/ui/display/icon'; @@ -122,6 +124,8 @@ export const RecordTableWithWrappers = ({ const { persistViewFields } = useViewFields(viewBarId); + const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular }); + return ( - - - - - - -
- - -
- - {!isRecordTableInitialLoading && numberOfTableRows === 0 && ( - - - No {foundObjectMetadataItem?.namePlural} - - - Create one: - -