Display table record creation row when clicking on Add new from table empty state (#6174)
As per title
This commit is contained in:
@ -1,12 +1,15 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isNonEmptyString, isNull } from '@sniptt/guards';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
|
||||
import { RecordTableEmptyState } from '@/object-record/record-table/components/RecordTableEmptyState';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
|
||||
import { RecordTableBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEffect';
|
||||
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
|
||||
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
const StyledTable = styled.table`
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
@ -32,6 +35,27 @@ export const RecordTable = ({
|
||||
}: RecordTableProps) => {
|
||||
const { scopeId } = useRecordTableStates(recordTableId);
|
||||
|
||||
const {
|
||||
isRecordTableInitialLoadingState,
|
||||
tableRowIdsState,
|
||||
pendingRecordIdState,
|
||||
} = useRecordTableStates(recordTableId);
|
||||
|
||||
const isRecordTableInitialLoading = useRecoilValue(
|
||||
isRecordTableInitialLoadingState,
|
||||
);
|
||||
|
||||
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||
|
||||
const pendingRecordId = useRecoilValue(pendingRecordIdState);
|
||||
|
||||
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
|
||||
{ objectNameSingular },
|
||||
);
|
||||
|
||||
const objectLabel = foundObjectMetadataItem?.labelSingular;
|
||||
const isRemote = foundObjectMetadataItem?.isRemote ?? false;
|
||||
|
||||
if (!isNonEmptyString(objectNameSingular)) {
|
||||
return <></>;
|
||||
}
|
||||
@ -45,11 +69,22 @@ export const RecordTable = ({
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordTableId={recordTableId}
|
||||
>
|
||||
<StyledTable className="entity-table-cell">
|
||||
<RecordTableHeader createRecord={createRecord} />
|
||||
<RecordTableBodyEffect />
|
||||
<RecordTableBody />
|
||||
</StyledTable>
|
||||
<RecordTableBodyEffect />
|
||||
{!isRecordTableInitialLoading &&
|
||||
tableRowIds.length === 0 &&
|
||||
isNull(pendingRecordId) ? (
|
||||
<RecordTableEmptyState
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectLabel={objectLabel}
|
||||
createRecord={createRecord}
|
||||
isRemote={isRemote}
|
||||
/>
|
||||
) : (
|
||||
<StyledTable className="entity-table-cell">
|
||||
<RecordTableHeader createRecord={createRecord} />
|
||||
<RecordTableBody />
|
||||
</StyledTable>
|
||||
)}
|
||||
</RecordTableContextProvider>
|
||||
</RecordTableScope>
|
||||
);
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
useOpenRecordTableCellV2,
|
||||
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
|
||||
import { useTriggerContextMenu } from '@/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu';
|
||||
import { useUpsertRecordV2 } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecordV2';
|
||||
import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord';
|
||||
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
|
||||
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||
|
||||
@ -32,7 +32,7 @@ export const RecordTableContextProvider = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { upsertRecord } = useUpsertRecordV2({
|
||||
const { upsertRecord } = useUpsertRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import { useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { useRef } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordTable } from '@/object-record/record-table/components/RecordTable';
|
||||
import { RecordTableEmptyState } from '@/object-record/record-table/components/RecordTableEmptyState';
|
||||
import { EntityDeleteContext } from '@/object-record/record-table/contexts/EntityDeleteHookContext';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
@ -31,6 +28,10 @@ const StyledTableContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledTableInternalContainer = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
type RecordTableWithWrappersProps = {
|
||||
objectNameSingular: string;
|
||||
recordTableId: string;
|
||||
@ -48,33 +49,14 @@ export const RecordTableWithWrappers = ({
|
||||
}: RecordTableWithWrappersProps) => {
|
||||
const tableBodyRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { isRecordTableInitialLoadingState, tableRowIdsState } =
|
||||
useRecordTableStates(recordTableId);
|
||||
|
||||
const isRecordTableInitialLoading = useRecoilValue(
|
||||
isRecordTableInitialLoadingState,
|
||||
);
|
||||
|
||||
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||
|
||||
const { resetTableRowSelection, setRowSelected } = useRecordTable({
|
||||
recordTableId,
|
||||
});
|
||||
|
||||
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
|
||||
{
|
||||
objectNameSingular,
|
||||
},
|
||||
);
|
||||
|
||||
const { saveViewFields } = useSaveCurrentViewFields(viewBarId);
|
||||
|
||||
const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular });
|
||||
|
||||
const objectLabel = foundObjectMetadataItem?.labelSingular;
|
||||
|
||||
const isRemote = foundObjectMetadataItem?.isRemote ?? false;
|
||||
|
||||
const handleColumnsChange = useRecoilCallback(
|
||||
() => (columns) => {
|
||||
saveViewFields(
|
||||
@ -86,24 +68,13 @@ export const RecordTableWithWrappers = ({
|
||||
[saveViewFields],
|
||||
);
|
||||
|
||||
if (!isRecordTableInitialLoading && tableRowIds.length === 0) {
|
||||
return (
|
||||
<RecordTableEmptyState
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectLabel={objectLabel}
|
||||
createRecord={createRecord}
|
||||
isRemote={isRemote}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EntityDeleteContext.Provider value={deleteOneRecord}>
|
||||
<ScrollWrapper>
|
||||
<RecordUpdateContext.Provider value={updateRecordMutation}>
|
||||
<StyledTableWithHeader>
|
||||
<StyledTableContainer>
|
||||
<div ref={tableBodyRef}>
|
||||
<StyledTableInternalContainer ref={tableBodyRef}>
|
||||
<RecordTable
|
||||
recordTableId={recordTableId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
@ -115,7 +86,7 @@ export const RecordTableWithWrappers = ({
|
||||
onDragSelectionStart={resetTableRowSelection}
|
||||
onDragSelectionChange={setRowSelected}
|
||||
/>
|
||||
</div>
|
||||
</StyledTableInternalContainer>
|
||||
<RecordTableInternalEffect
|
||||
recordTableId={recordTableId}
|
||||
tableBodyRef={tableBodyRef}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
@ -10,9 +10,10 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
|
||||
import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
|
||||
const pendingRecordId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9';
|
||||
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(),
|
||||
@ -93,17 +94,25 @@ describe('useUpsertRecord', () => {
|
||||
});
|
||||
|
||||
it('calls update record if there is no pending record', async () => {
|
||||
const { result } = renderHook(() => useUpsertRecord(), {
|
||||
wrapper: ({ children }) =>
|
||||
Wrapper({
|
||||
pendingRecordIdMockedValue: null,
|
||||
draftValueMockedValue: null,
|
||||
children,
|
||||
}),
|
||||
});
|
||||
const { result } = renderHook(
|
||||
() => useUpsertRecord({ objectNameSingular: 'person' }),
|
||||
{
|
||||
wrapper: ({ children }) =>
|
||||
Wrapper({
|
||||
pendingRecordIdMockedValue: null,
|
||||
draftValueMockedValue: null,
|
||||
children,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upsertRecord(updateOneRecordMock);
|
||||
await result.current.upsertRecord(
|
||||
updateOneRecordMock,
|
||||
'entityId',
|
||||
'name',
|
||||
'recordTableId',
|
||||
);
|
||||
});
|
||||
|
||||
expect(createOneRecordMock).not.toHaveBeenCalled();
|
||||
@ -111,42 +120,28 @@ describe('useUpsertRecord', () => {
|
||||
});
|
||||
|
||||
it('calls update record if pending record is empty', async () => {
|
||||
const { result } = renderHook(() => useUpsertRecord(), {
|
||||
wrapper: ({ children }) =>
|
||||
Wrapper({
|
||||
pendingRecordIdMockedValue: null,
|
||||
draftValueMockedValue: draftValue,
|
||||
children,
|
||||
}),
|
||||
});
|
||||
const { result } = renderHook(
|
||||
() => useUpsertRecord({ objectNameSingular: 'person' }),
|
||||
{
|
||||
wrapper: ({ children }) =>
|
||||
Wrapper({
|
||||
pendingRecordIdMockedValue: null,
|
||||
draftValueMockedValue: draftValue,
|
||||
children,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upsertRecord(updateOneRecordMock);
|
||||
await result.current.upsertRecord(
|
||||
updateOneRecordMock,
|
||||
'entityId',
|
||||
'name',
|
||||
'recordTableId',
|
||||
);
|
||||
});
|
||||
|
||||
expect(createOneRecordMock).not.toHaveBeenCalled();
|
||||
expect(updateOneRecordMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls create record if pending record is not empty', async () => {
|
||||
const { result } = renderHook(() => useUpsertRecord(), {
|
||||
wrapper: ({ children }) =>
|
||||
Wrapper({
|
||||
pendingRecordIdMockedValue: pendingRecordId,
|
||||
draftValueMockedValue: draftValue,
|
||||
children,
|
||||
}),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upsertRecord(updateOneRecordMock);
|
||||
});
|
||||
|
||||
expect(createOneRecordMock).toHaveBeenCalledWith({
|
||||
id: pendingRecordId,
|
||||
name: draftValue,
|
||||
position: 'first',
|
||||
});
|
||||
expect(updateOneRecordMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,41 +1,65 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useRecordFieldInputStates } from '@/object-record/record-field/hooks/internal/useRecordFieldInputStates';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
|
||||
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 { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useUpsertRecord = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const { pendingRecordIdState } = useRecordTableStates();
|
||||
|
||||
const pendingRecordId = useRecoilValue(pendingRecordIdState);
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
const { getDraftValueSelector } = useRecordFieldInputStates(
|
||||
`${entityId}-${fieldName}`,
|
||||
);
|
||||
const draftValue = useRecoilValue(getDraftValueSelector());
|
||||
|
||||
const objectNameSingular =
|
||||
fieldDefinition.metadata.objectMetadataNameSingular ?? '';
|
||||
export const useUpsertRecord = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const { createOneRecord } = useCreateOneRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const upsertRecord = (persistField: () => void) => {
|
||||
if (isDefined(pendingRecordId) && isDefined(draftValue)) {
|
||||
createOneRecord({
|
||||
id: pendingRecordId,
|
||||
name: draftValue,
|
||||
position: 'first',
|
||||
});
|
||||
} else if (!pendingRecordId) {
|
||||
persistField();
|
||||
}
|
||||
};
|
||||
const upsertRecord = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(
|
||||
persistField: () => void,
|
||||
entityId: string,
|
||||
fieldName: string,
|
||||
recordTableId: string,
|
||||
) => {
|
||||
const tableScopeId = getScopeIdFromComponentId(recordTableId);
|
||||
|
||||
const recordTablePendingRecordIdState = extractComponentState(
|
||||
recordTablePendingRecordIdComponentState,
|
||||
tableScopeId,
|
||||
);
|
||||
|
||||
const recordTablePendingRecordId = getSnapshotValue(
|
||||
snapshot,
|
||||
recordTablePendingRecordIdState,
|
||||
);
|
||||
const fieldScopeId = getScopeIdFromComponentId(
|
||||
`${entityId}-${fieldName}`,
|
||||
);
|
||||
|
||||
const draftValueSelector = extractComponentSelector(
|
||||
recordFieldInputDraftValueComponentSelector,
|
||||
fieldScopeId,
|
||||
);
|
||||
|
||||
const draftValue = getSnapshotValue(snapshot, draftValueSelector());
|
||||
|
||||
if (isDefined(recordTablePendingRecordId) && isDefined(draftValue)) {
|
||||
createOneRecord({
|
||||
id: recordTablePendingRecordId,
|
||||
name: draftValue,
|
||||
position: 'first',
|
||||
});
|
||||
} else if (!recordTablePendingRecordId) {
|
||||
persistField();
|
||||
}
|
||||
},
|
||||
[createOneRecord],
|
||||
);
|
||||
|
||||
return { upsertRecord };
|
||||
};
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
|
||||
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 { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useUpsertRecordV2 = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const { createOneRecord } = useCreateOneRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const upsertRecord = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(
|
||||
persistField: () => void,
|
||||
entityId: string,
|
||||
fieldName: string,
|
||||
recordTableId: string,
|
||||
) => {
|
||||
const tableScopeId = getScopeIdFromComponentId(recordTableId);
|
||||
|
||||
const recordTablePendingRecordIdState = extractComponentState(
|
||||
recordTablePendingRecordIdComponentState,
|
||||
tableScopeId,
|
||||
);
|
||||
|
||||
const recordTablePendingRecordId = getSnapshotValue(
|
||||
snapshot,
|
||||
recordTablePendingRecordIdState,
|
||||
);
|
||||
const fieldScopeId = getScopeIdFromComponentId(
|
||||
`${entityId}-${fieldName}`,
|
||||
);
|
||||
|
||||
const draftValueSelector = extractComponentSelector(
|
||||
recordFieldInputDraftValueComponentSelector,
|
||||
fieldScopeId,
|
||||
);
|
||||
|
||||
const draftValue = getSnapshotValue(snapshot, draftValueSelector());
|
||||
|
||||
if (isDefined(recordTablePendingRecordId) && isDefined(draftValue)) {
|
||||
createOneRecord({
|
||||
id: recordTablePendingRecordId,
|
||||
name: draftValue,
|
||||
position: 'first',
|
||||
});
|
||||
} else if (!recordTablePendingRecordId) {
|
||||
persistField();
|
||||
}
|
||||
},
|
||||
[createOneRecord],
|
||||
);
|
||||
|
||||
return { upsertRecord };
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
|
||||
|
||||
Reference in New Issue
Block a user