Do not override value for composite types address and links when entering input (#6502)

Closes #6434.

We don't want to override the values of the records' address or links as
they are composite field and it is costly to loose the data.
We will need a more unified behaviour here - maybe introduce a Ctrl+Z
option.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie
2024-08-03 20:12:31 +02:00
committed by GitHub
parent 6e0c1b4c73
commit e01d3fd0be
10 changed files with 61 additions and 231 deletions

View File

@ -0,0 +1,6 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const FIELD_NOT_OVERWRITTEN_AT_DRAFT = [
FieldMetadataType.Address,
FieldMetadataType.Links,
];

View File

@ -1,6 +1,7 @@
import { isUndefined } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { FIELD_NOT_OVERWRITTEN_AT_DRAFT } from '@/object-record/constants/FieldsNotOverwrittenAtDraft';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
@ -37,7 +38,10 @@ export const useInitDraftValueV2 = <FieldValue>() => {
)
.getValue();
if (isUndefined(value)) {
if (
isUndefined(value) ||
FIELD_NOT_OVERWRITTEN_AT_DRAFT.includes(fieldDefinition.type)
) {
set(
getDraftValueSelector(),
computeDraftValueFromFieldValue<FieldValue>({
@ -48,7 +52,10 @@ export const useInitDraftValueV2 = <FieldValue>() => {
} else {
set(
getDraftValueSelector(),
computeDraftValueFromString<FieldValue>({ value, fieldDefinition }),
computeDraftValueFromString<FieldValue>({
value,
fieldDefinition,
}),
);
}
},

View File

@ -1,13 +1,7 @@
import { useContext } from 'react';
import { isUndefined } from '@sniptt/guards';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useSetRecoilState } from 'recoil';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useRecordFieldInputStates } from '@/object-record/record-field/hooks/internal/useRecordFieldInputStates';
import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { computeDraftValueFromFieldValue } from '@/object-record/record-field/utils/computeDraftValueFromFieldValue';
import { computeDraftValueFromString } from '@/object-record/record-field/utils/computeDraftValueFromString';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
export const useRecordFieldInput = <FieldValue>(
recordFieldInputId?: string,
@ -15,40 +9,8 @@ export const useRecordFieldInput = <FieldValue>(
const { scopeId, getDraftValueSelector } =
useRecordFieldInputStates<FieldValue>(recordFieldInputId);
const { entityId, fieldDefinition } = useContext(FieldContext);
const setDraftValue = useSetRecoilState(getDraftValueSelector());
const initDraftValue = useRecoilCallback(
({ set, snapshot }) =>
(value?: string) => {
const recordFieldValue = snapshot
.getLoadable(
recordStoreFamilySelector<FieldValue>({
recordId: entityId,
fieldName: fieldDefinition.metadata.fieldName,
}),
)
.getValue();
if (isUndefined(value)) {
set(
getDraftValueSelector(),
computeDraftValueFromFieldValue<FieldValue>({
fieldValue: recordFieldValue,
fieldDefinition,
}),
);
} else {
set(
getDraftValueSelector(),
computeDraftValueFromString<FieldValue>({ value, fieldDefinition }),
);
}
},
[entityId, fieldDefinition, getDraftValueSelector],
);
const isDraftValueEmpty = (
value: FieldInputDraftValue<FieldValue> | undefined,
) => {
@ -67,7 +29,6 @@ export const useRecordFieldInput = <FieldValue>(
scopeId,
setDraftValue,
getDraftValueSelector,
initDraftValue,
isDraftValueEmpty,
};
};

View File

@ -2,12 +2,15 @@ import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime';
import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
@ -19,18 +22,20 @@ type computeDraftValueFromStringParams = {
export const computeDraftValueFromString = <FieldValue>({
fieldDefinition,
value,
}: computeDraftValueFromStringParams): FieldInputDraftValue<FieldValue> => {
}: computeDraftValueFromStringParams):
| FieldInputDraftValue<FieldValue>
| undefined => {
// Todo: improve typing
if (
isFieldUuid(fieldDefinition) ||
isFieldText(fieldDefinition) ||
isFieldDateTime(fieldDefinition) ||
isFieldNumber(fieldDefinition) ||
isFieldEmail(fieldDefinition)
isFieldEmail(fieldDefinition) ||
isFieldRelation(fieldDefinition)
) {
return value as FieldInputDraftValue<FieldValue>;
}
if (isFieldLink(fieldDefinition)) {
return { url: value, label: value } as FieldInputDraftValue<FieldValue>;
}
@ -49,5 +54,17 @@ export const computeDraftValueFromString = <FieldValue>({
} as FieldInputDraftValue<FieldValue>;
}
if (isFieldAddress(fieldDefinition)) {
return {
addressStreet1: value,
} as FieldInputDraftValue<FieldValue>;
}
if (isFieldLinks(fieldDefinition)) {
return {
primaryLinkUrl: value,
} as FieldInputDraftValue<FieldValue>;
}
throw new Error(`Record field type not supported : ${fieldDefinition.type}}`);
};

View File

@ -2,11 +2,12 @@ import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime';
import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
@ -33,8 +34,22 @@ export const computeEmptyDraftValue = <FieldValue>({
return '' as FieldInputDraftValue<FieldValue>;
}
if (isFieldLink(fieldDefinition)) {
return { url: '', label: '' } as FieldInputDraftValue<FieldValue>;
if (isFieldLinks(fieldDefinition)) {
return {
primaryLinkUrl: '',
primaryLinkLabel: '',
} as FieldInputDraftValue<FieldValue>;
}
if (isFieldAddress(fieldDefinition)) {
return {
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
} as FieldInputDraftValue<FieldValue>;
}
if (isFieldCurrency(fieldDefinition)) {

View File

@ -2,11 +2,11 @@ import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { isDefined } from '~/utils/isDefined';
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
import { isInlineCellInEditModeScopedState } from '../states/isInlineCellInEditModeScopedState';
import { InlineCellHotkeyScope } from '../types/InlineCellHotkeyScope';
@ -26,9 +26,7 @@ export const useInlineCell = () => {
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const { initDraftValue: initFieldInputDraftValue } = useRecordFieldInput(
`${entityId}-${fieldDefinition?.metadata?.fieldName}`,
);
const initFieldInputDraftValue = useInitDraftValueV2();
const closeInlineCell = () => {
setIsInlineCellInEditMode(false);
@ -38,7 +36,7 @@ export const useInlineCell = () => {
const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => {
setIsInlineCellInEditMode(true);
initFieldInputDraftValue();
initFieldInputDraftValue({ entityId, fieldDefinition });
if (isDefined(customEditHotkeyScopeForField)) {
setHotkeyScopeAndMemorizePreviousScope(

View File

@ -1,76 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
import { act, renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import {
recordTableCell,
recordTableRow,
} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
import { useOpenRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCell';
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
const setHotkeyScope = jest.fn();
jest.mock('@/ui/utilities/hotkey/hooks/useSetHotkeyScope', () => ({
useSetHotkeyScope: () => setHotkeyScope,
}));
const onColumnsChange = jest.fn();
const scopeId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>
<RecordTableScope
recordTableScopeId={scopeId}
onColumnsChange={onColumnsChange}
>
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
entityId: 'entityId',
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
}}
>
<RecordTableRowContext.Provider value={recordTableRow}>
<RecordTableCellContext.Provider value={recordTableCell}>
<MemoryRouter initialEntries={['/one', '/two']} initialIndex={1}>
{children}
</MemoryRouter>
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
</FieldContext.Provider>
</RecordTableScope>
</RecoilRoot>
);
describe('useOpenRecordTableCell', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => {
return { ...useOpenRecordTableCell(), ...useDragSelect() };
},
{
wrapper: Wrapper,
},
);
expect(result.current.isDragSelectionStartEnabled()).toBe(true);
act(() => {
result.current.openTableCell();
});
await waitFor(() => {
expect(result.current.isDragSelectionStartEnabled()).toBe(false);
});
expect(setHotkeyScope).toHaveBeenCalledWith('cell-edit-mode', undefined);
});
});

View File

@ -1,101 +0,0 @@
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { isDefined } from '~/utils/isDefined';
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useCurrentTableCellEditMode } from './useCurrentTableCellEditMode';
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
scope: TableHotkeyScope.CellEditMode,
};
export const useOpenRecordTableCell = () => {
const { pathToShowPage, isReadOnly } = useContext(RecordTableRowContext);
const { setCurrentTableCellInEditMode } = useCurrentTableCellEditMode();
const setHotkeyScope = useSetHotkeyScope();
const { setDragSelectionStartEnabled } = useDragSelect();
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
const navigate = useNavigate();
const leaveTableFocus = useLeaveTableFocus();
const { toggleClickOutsideListener } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
);
const { columnIndex } = useContext(RecordTableCellContext);
const isFirstColumnCell = columnIndex === 0;
const isEmpty = useIsFieldEmpty();
const { entityId, fieldDefinition } = useContext(FieldContext);
const { initDraftValue: initFieldInputDraftValue } = useRecordFieldInput(
`${entityId}-${fieldDefinition?.metadata?.fieldName}`,
);
const openTableCell = useRecoilCallback(
() => (options?: { initialValue?: string }) => {
if (isReadOnly) {
return;
}
if (isFirstColumnCell && !isEmpty) {
leaveTableFocus();
navigate(pathToShowPage);
return;
}
setDragSelectionStartEnabled(false);
setCurrentTableCellInEditMode();
initFieldInputDraftValue(options?.initialValue);
toggleClickOutsideListener(false);
if (isDefined(customCellHotkeyScope)) {
setHotkeyScope(
customCellHotkeyScope.scope,
customCellHotkeyScope.customScopes,
);
} else {
setHotkeyScope(
DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes,
);
}
},
[
isReadOnly,
isFirstColumnCell,
isEmpty,
setDragSelectionStartEnabled,
setCurrentTableCellInEditMode,
initFieldInputDraftValue,
toggleClickOutsideListener,
customCellHotkeyScope,
leaveTableFocus,
navigate,
pathToShowPage,
setHotkeyScope,
],
);
return {
openTableCell,
};
};

View File

@ -5,7 +5,7 @@ import { v4 } from 'uuid';
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCell';
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 { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';

View File

@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-workspace/jobs/data-seed-demo-workspace.job';
@ -11,6 +12,7 @@ import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.modu
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { EmailSenderJob } from 'src/engine/integrations/email/email-sender.job';
import { EmailModule } from 'src/engine/integrations/email/email.module';
@ -27,6 +29,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
DataSourceModule,
ObjectMetadataModule,
TypeORMModule,