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 <bordeau.lucas@gmail.com>
This commit is contained in:
Kanav Arora
2024-01-05 22:31:51 +05:30
committed by GitHub
parent 9def3d5b57
commit 8455e15443
28 changed files with 551 additions and 159 deletions

View File

@ -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 (
<PageContainer>

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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 (
<FieldInputOverlay>
<TextInput
@ -87,6 +97,7 @@ export const CurrencyFieldInput = ({
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
onChange={handleChange}
/>
</FieldInputOverlay>
);

View File

@ -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<Date>) => {
if (!newDate) {
@ -46,6 +48,10 @@ export const DateFieldInput = ({
onClickOutside?.(() => persistDate(newDate));
};
const handleChange = (newDate: Nullable<Date>) => {
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}
/>
);
};

View File

@ -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 (
<FieldInputOverlay>
<TextInput
@ -60,6 +66,7 @@ export const EmailFieldInput = ({
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
onChange={handleChange}
/>
</FieldInputOverlay>
);

View File

@ -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 =
'First name';
const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
'Last 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 (
<FieldInputOverlay>
<DoubleTextInput
firstValue={initialValue.firstName}
secondValue={initialValue.lastName}
firstValuePlaceholder={'First name'}
secondValuePlaceholder={'Last name'}
firstValuePlaceholder={
FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
}
secondValuePlaceholder={
LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
onChange={handleChange}
/>
</FieldInputOverlay>
);

View File

@ -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 (
<FieldInputOverlay>
<TextInput
@ -82,6 +92,7 @@ export const LinkFieldInput = ({
onEscape={handleEscape}
onTab={handleTab}
onShiftTab={handleShiftTab}
onChange={handleChange}
/>
</FieldInputOverlay>
);

View File

@ -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 (
<FieldInputOverlay>
<TextInput
@ -58,6 +65,7 @@ export const NumberFieldInput = ({
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
onChange={handleChange}
/>
</FieldInputOverlay>
);

View File

@ -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 (
<FieldInputOverlay>
<PhoneInput
@ -58,6 +65,7 @@ export const PhoneFieldInput = ({
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
onChange={handleChange}
/>
</FieldInputOverlay>
);

View File

@ -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 (
<FieldInputOverlay>
<TextInput
@ -60,6 +66,7 @@ export const TextFieldInput = ({
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
onChange={handleChange}
/>
</FieldInputOverlay>
);

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const entityFieldsEditModeValueFamilyState = atomFamily<
Record<string, unknown> | null,
string
>({
key: 'entityFieldsEditModeValueFamilyState',
default: null,
});

View File

@ -0,0 +1,18 @@
import { selectorFamily } from 'recoil';
import { entityFieldsEditModeValueFamilyState } from '@/object-record/field/states/entityFieldsEditModeValueFamilyState';
export const entityFieldsEditModeValueFamilySelector = selectorFamily({
key: 'entityFieldsEditModeValueFamilySelector',
get:
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
({ get }) =>
get(entityFieldsEditModeValueFamilyState(entityId))?.[fieldName] as T,
set:
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
({ set }, newValue: T) =>
set(entityFieldsEditModeValueFamilyState(entityId), (prevState) => ({
...prevState,
[fieldName]: newValue,
})),
});

View File

@ -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<FieldDefinition<FieldMetadata>, 'type'>;
fieldName: string;
entityId: string;
}) => {
return ({ get }) => {
const fieldValue = get(entityFieldsEditModeValueFamilyState(entityId))?.[
fieldName
];
return isFieldValueEmpty({
fieldDefinition,
fieldValue,
});
};
},
});

View File

@ -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,
});
};
},
});

View File

@ -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<FieldDefinition<FieldMetadata>, '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}}`,
);
};

View File

@ -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 (
<RecordTableScope
recordTableScopeId={recordTableId}
@ -129,42 +133,44 @@ export const RecordTableWithWrappers = ({
persistViewFields(mapColumnDefinitionsToViewFields(columns));
})}
>
<ScrollWrapper>
<RecordTableRefContextWrapper>
<RecordTableFirstColumnScrollObserver />
<RecordUpdateContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader>
<StyledTableContainer>
<div ref={tableBodyRef}>
<RecordTable createRecord={createRecord} />
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={resetTableRowSelection}
onDragSelectionChange={setRowSelectedState}
/>
</div>
<RecordTableInternalEffect tableBodyRef={tableBodyRef} />
{!isRecordTableInitialLoading && numberOfTableRows === 0 && (
<StyledObjectEmptyContainer>
<StyledEmptyObjectTitle>
No {foundObjectMetadataItem?.namePlural}
</StyledEmptyObjectTitle>
<StyledEmptyObjectSubTitle>
Create one:
</StyledEmptyObjectSubTitle>
<Button
Icon={IconPlus}
title={`Add a ${foundObjectMetadataItem?.nameSingular}`}
variant={'secondary'}
onClick={createRecord}
<EntityDeleteContext.Provider value={deleteOneRecord}>
<ScrollWrapper>
<RecordTableRefContextWrapper>
<RecordTableFirstColumnScrollObserver />
<RecordUpdateContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader>
<StyledTableContainer>
<div ref={tableBodyRef}>
<RecordTable createRecord={createRecord} />
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={resetTableRowSelection}
onDragSelectionChange={setRowSelectedState}
/>
</StyledObjectEmptyContainer>
)}
</StyledTableContainer>
</StyledTableWithHeader>
</RecordUpdateContext.Provider>
</RecordTableRefContextWrapper>
</ScrollWrapper>
</div>
<RecordTableInternalEffect tableBodyRef={tableBodyRef} />
{!isRecordTableInitialLoading && numberOfTableRows === 0 && (
<StyledObjectEmptyContainer>
<StyledEmptyObjectTitle>
No {foundObjectMetadataItem?.namePlural}
</StyledEmptyObjectTitle>
<StyledEmptyObjectSubTitle>
Create one:
</StyledEmptyObjectSubTitle>
<Button
Icon={IconPlus}
title={`Add a ${foundObjectMetadataItem?.nameSingular}`}
variant={'secondary'}
onClick={createRecord}
/>
</StyledObjectEmptyContainer>
)}
</StyledTableContainer>
</StyledTableWithHeader>
</RecordUpdateContext.Provider>
</RecordTableRefContextWrapper>
</ScrollWrapper>
</EntityDeleteContext.Provider>
</RecordTableScope>
);
};

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
export const EntityDeleteContext = createContext<
(idToDelete: string) => Promise<unknown>
>(async () => {});

View File

@ -1,6 +1,10 @@
import { useContext } from 'react';
import { FieldDisplay } from '@/object-record/field/components/FieldDisplay';
import { FieldInput } from '@/object-record/field/components/FieldInput';
import { useIsFieldEditModeValueEmpty } from '@/object-record/field/hooks/useIsFieldEditModeValueEmpty';
import { FieldInputEvent } from '@/object-record/field/types/FieldInputEvent';
import { ColumnIndexContext } from '@/object-record/record-table/contexts/ColumnIndexContext';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecordTable } from '../../hooks/useRecordTable';
@ -17,14 +21,25 @@ export const RecordTableCell = ({
const { moveLeft, moveRight, moveDown } = useRecordTable();
const isFirstColumnCell = useContext(ColumnIndexContext) === 0;
const isEditModeValueEmpty = useIsFieldEditModeValueEmpty();
const skipFieldPersist = isFirstColumnCell && isEditModeValueEmpty;
const handleEnter: FieldInputEvent = (persistField) => {
persistField();
if (!skipFieldPersist) {
persistField();
}
closeTableCell();
moveDown();
};
const handleSubmit: FieldInputEvent = (persistField) => {
persistField();
if (!skipFieldPersist) {
persistField();
}
closeTableCell();
};
@ -33,24 +48,35 @@ export const RecordTableCell = ({
};
const handleClickOutside: FieldInputEvent = (persistField) => {
persistField();
if (!skipFieldPersist) {
persistField();
}
closeTableCell();
};
const handleEscape: FieldInputEvent = (persistField) => {
persistField();
if (!skipFieldPersist) {
persistField();
}
closeTableCell();
};
const handleTab: FieldInputEvent = (persistField) => {
persistField();
if (!skipFieldPersist) {
persistField();
}
closeTableCell();
moveRight();
};
const handleShiftTab: FieldInputEvent = (persistField) => {
persistField();
if (!skipFieldPersist) {
persistField();
}
closeTableCell();
moveLeft();
};

View File

@ -33,13 +33,17 @@ export const useMoveSoftFocusToCurrentCellOnHover = () => {
currentTableCellInEditModePositionScopeInjector,
);
const isSomeCellInEditMode = snapshot.getLoadable(
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
);
const isSomeCellInEditMode = snapshot
.getLoadable(
isTableCellInEditModeFamilyState(
currentTableCellInEditModePosition,
),
)
.getValue();
const currentHotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.valueOrThrow();
.getValue();
if (
currentHotkeyScope.scope !== TableHotkeyScope.TableSoftFocus &&
@ -49,7 +53,7 @@ export const useMoveSoftFocusToCurrentCellOnHover = () => {
return;
}
if (!isSomeCellInEditMode.contents) {
if (!isSomeCellInEditMode) {
setSoftFocusOnCurrentTableCell();
}
},

View File

@ -0,0 +1,21 @@
import { useCallback } from 'react';
import { useMoveEditModeToTableCellPosition } from '../../hooks/internal/useMoveEditModeToCellPosition';
export const useSelectedTableCellEditMode = ({
scopeId,
}: {
scopeId: string;
}) => {
const moveEditModeToTableCellPosition =
useMoveEditModeToTableCellPosition(scopeId);
const setSelectedTableCellEditMode = useCallback(
(row: number, column: number) => {
moveEditModeToTableCellPosition({ column, row });
},
[moveEditModeToTableCellPosition],
);
return { setSelectedTableCellEditMode };
};

View File

@ -0,0 +1,35 @@
import { useRecoilCallback } from 'recoil';
import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { getRecordTableScopeInjector } from '@/object-record/record-table/utils/getRecordTableScopeInjector';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const useSetSoftFocus = () => {
const { setSoftFocusPosition } = useRecordTable();
const { isSoftFocusActiveScopeInjector } = getRecordTableScopeInjector();
const { injectStateWithRecordTableScopeId } = useRecordTableScopedStates();
const isSoftFocusActiveState = injectStateWithRecordTableScopeId(
isSoftFocusActiveScopeInjector,
);
const setHotkeyScope = useSetHotkeyScope();
return useRecoilCallback(
({ set }) =>
(newPosition: TableCellPosition) => {
setSoftFocusPosition(newPosition);
set(isSoftFocusActiveState, true);
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
},
[setSoftFocusPosition, isSoftFocusActiveState, setHotkeyScope],
);
};

View File

@ -1,43 +1,13 @@
import { useRecoilCallback } from 'recoil';
import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { getRecordTableScopeInjector } from '@/object-record/record-table/utils/getRecordTableScopeInjector';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useSetSoftFocus } from '@/object-record/record-table/record-table-cell/hooks/useSetSoftFocus';
import { useCurrentTableCellPosition } from './useCurrentCellPosition';
export const useSetSoftFocusOnCurrentTableCell = () => {
const { setSoftFocusPosition } = useRecordTable();
const { isSoftFocusActiveScopeInjector } = getRecordTableScopeInjector();
const { injectStateWithRecordTableScopeId } = useRecordTableScopedStates();
const isSoftFocusActiveState = injectStateWithRecordTableScopeId(
isSoftFocusActiveScopeInjector,
);
const setSoftFocus = useSetSoftFocus();
const currentTableCellPosition = useCurrentTableCellPosition();
const setHotkeyScope = useSetHotkeyScope();
return useRecoilCallback(
({ set }) =>
() => {
setSoftFocusPosition(currentTableCellPosition);
set(isSoftFocusActiveState, true);
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
},
[
setSoftFocusPosition,
currentTableCellPosition,
isSoftFocusActiveState,
setHotkeyScope,
],
);
return () => {
setSoftFocus(currentTableCellPosition);
};
};

View File

@ -1,11 +1,13 @@
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { useIsFieldEditModeValueEmpty } from '@/object-record/field/hooks/useIsFieldEditModeValueEmpty';
import { useIsFieldEmpty } from '@/object-record/field/hooks/useIsFieldEmpty';
import { entityFieldInitialValueFamilyState } from '@/object-record/field/states/entityFieldInitialValueFamilyState';
import { FieldInitialValue } from '@/object-record/field/types/FieldInitialValue';
import { EntityDeleteContext } from '@/object-record/record-table/contexts/EntityDeleteHookContext';
import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { getRecordTableScopeInjector } from '@/object-record/record-table/utils/getRecordTableScopeInjector';
@ -20,7 +22,7 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useCurrentTableCellEditMode } from './useCurrentTableCellEditMode';
const DEFAULT_CELL_SCOPE: HotkeyScope = {
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
scope: TableHotkeyScope.CellEditMode,
};
@ -51,9 +53,12 @@ export const useTableCell = () => {
const isFirstColumnCell = useContext(ColumnIndexContext) === 0;
const isEmpty = useIsFieldEmpty();
const isEditModeValueEmpty = useIsFieldEditModeValueEmpty();
const { entityId, fieldDefinition } = useContext(FieldContext);
const deleteOneRecord = useContext(EntityDeleteContext);
const [, setFieldInitialValue] = useRecoilState(
entityFieldInitialValueFamilyState({
entityId,
@ -61,6 +66,16 @@ export const useTableCell = () => {
}),
);
const { tableRowIdsScopeInjector } = getRecordTableScopeInjector();
const deleteRow = useRecoilCallback(({ snapshot }) => async () => {
const tableRowIds = snapshot
.getLoadable(tableRowIdsScopeInjector(recordTableScopeId))
.getValue();
await deleteOneRecord(tableRowIds[0]);
});
const openTableCell = (options?: { initialValue?: FieldInitialValue }) => {
if (isFirstColumnCell && !isEmpty && basePathToShowPage) {
navigate(`${basePathToShowPage}${entityId}`);
@ -84,11 +99,15 @@ export const useTableCell = () => {
}
};
const closeTableCell = () => {
const closeTableCell = async () => {
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setFieldInitialValue(undefined);
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
if (isFirstColumnCell && isEditModeValueEmpty) {
await deleteRow();
}
};
return {

View File

@ -38,6 +38,7 @@ export type DateInputProps = {
) => void;
hotkeyScope: string;
clearable?: boolean;
onChange?: (newDate: Nullable<Date>) => void;
};
export const DateInput = ({
@ -47,6 +48,7 @@ export const DateInput = ({
onEscape,
onClickOutside,
clearable,
onChange,
}: DateInputProps) => {
const theme = useTheme();
@ -66,6 +68,7 @@ export const DateInput = ({
const handleChange = (newDate: Date) => {
setInternalValue(newDate);
onChange?.(newDate);
};
useEffect(() => {

View File

@ -38,6 +38,7 @@ type DoubleTextInputProps = {
event: MouseEvent | TouchEvent,
newDoubleTextValue: FieldDoubleText,
) => void;
onChange?: (newDoubleTextValue: FieldDoubleText) => void;
};
export const DoubleTextInput = ({
@ -51,6 +52,7 @@ export const DoubleTextInput = ({
onEscape,
onShiftTab,
onTab,
onChange,
}: DoubleTextInputProps) => {
const [firstInternalValue, setFirstInternalValue] = useState(firstValue);
const [secondInternalValue, setSecondInternalValue] = useState(secondValue);
@ -70,6 +72,11 @@ export const DoubleTextInput = ({
): void => {
setFirstInternalValue(newFirstValue);
setSecondInternalValue(newSecondValue);
onChange?.({
firstValue: newFirstValue,
secondValue: newSecondValue,
});
};
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');

View File

@ -54,6 +54,7 @@ export type PhoneInputProps = {
onTab?: (newText: string) => void;
onShiftTab?: (newText: string) => void;
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
onChange?: (newText: string) => void;
hotkeyScope: string;
};
@ -66,11 +67,17 @@ export const PhoneInput = ({
onShiftTab,
onClickOutside,
hotkeyScope,
onChange,
}: PhoneInputProps) => {
const [internalValue, setInternalValue] = useState<string | undefined>(value);
const wrapperRef = useRef<HTMLDivElement>(null);
const handleChange = (newValue: string) => {
setInternalValue(newValue);
onChange?.(newValue);
};
useEffect(() => {
setInternalValue(value);
}, [value]);
@ -92,7 +99,7 @@ export const PhoneInput = ({
autoFocus={autoFocus}
placeholder="Phone number"
value={value}
onChange={setInternalValue}
onChange={handleChange}
international={true}
withCountryCallingCode={true}
countrySelectComponent={CountryPickerDropdownButton}

View File

@ -21,6 +21,7 @@ type TextInputProps = {
onShiftTab?: (newText: string) => void;
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
hotkeyScope: string;
onChange?: (newText: string) => void;
};
export const TextInput = ({
@ -33,6 +34,7 @@ export const TextInput = ({
onTab,
onShiftTab,
onClickOutside,
onChange,
}: TextInputProps) => {
const [internalText, setInternalText] = useState(value);
@ -40,6 +42,7 @@ export const TextInput = ({
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setInternalText(event.target.value);
onChange?.(event.target.value);
};
useEffect(() => {