fix: can't properly drag and drop in empty record group (#9039)

Fix #8968 

Fix issue with drag and drop when record group are empty.
Happy to discuss the implementation, as it's limited to the
`@hello-pangea/dnd` api.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M 2024-12-13 09:37:23 +01:00 committed by GitHub
parent d56c815897
commit 07aaf0801c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 349 additions and 242 deletions

View File

@ -1,14 +1,6 @@
import React, { Ref, RefCallback } from 'react';
import { isFunction } from '@sniptt/guards';
import { Ref, RefCallback } from 'react';
import { combineRefs } from '~/utils/combineRefs';
export const useCombinedRefs =
<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> =>
(node: T) => {
for (const ref of refs) {
if (isFunction(ref)) {
ref(node);
} else if (ref !== null && ref !== undefined) {
(ref as React.MutableRefObject<T | null>).current = node;
}
}
};
export const useCombinedRefs = <T>(
...refs: (Ref<T> | undefined)[]
): RefCallback<T> => combineRefs<T>(...refs);

View File

@ -14,13 +14,14 @@ import {
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { mockPerformance } from './mock';
@ -87,7 +88,7 @@ const meta: Meta = {
onCellMouseEnter: () => {},
}}
>
<RecordTableRowContext.Provider
<RecordTableRowContextProvider
value={{
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
@ -99,43 +100,48 @@ const meta: Meta = {
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
}) + mockPerformance.recordId,
isSelected: false,
isDragging: false,
dragHandleProps: null,
inView: true,
isPendingRow: false,
inView: true,
}}
>
<RecordTableCellContext.Provider
<RecordTableRowDraggableContextProvider
value={{
columnDefinition: mockPerformance.fieldDefinition,
columnIndex: 0,
cellPosition: { row: 0, column: 0 },
hasSoftFocus: false,
isInEditMode: false,
isDragging: false,
dragHandleProps: null,
}}
>
<FieldContext.Provider
<RecordTableCellContext.Provider
value={{
recordId: mockPerformance.recordId,
basePathToShowPage: '/object-record/',
isLabelIdentifier: false,
fieldDefinition: {
...mockPerformance.fieldDefinition,
},
hotkeyScope: 'hotkey-scope',
columnDefinition: mockPerformance.fieldDefinition,
columnIndex: 0,
cellPosition: { row: 0, column: 0 },
hasSoftFocus: false,
isInEditMode: false,
}}
>
<RelationFieldValueSetterEffect />
<table>
<tbody>
<tr>
<Story />
</tr>
</tbody>
</table>
</FieldContext.Provider>
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
<FieldContext.Provider
value={{
recordId: mockPerformance.recordId,
basePathToShowPage: '/object-record/',
isLabelIdentifier: false,
fieldDefinition: {
...mockPerformance.fieldDefinition,
},
hotkeyScope: 'hotkey-scope',
}}
>
<RelationFieldValueSetterEffect />
<table>
<tbody>
<tr>
<Story />
</tr>
</tbody>
</table>
</FieldContext.Provider>
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</RecordTableBodyContextProvider>
</RecordTableComponentInstance>
</RecordTableContextProvider>

View File

@ -4,7 +4,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
export type RecordTableCellContextProps = {
export type RecordTableCellContextValue = {
columnDefinition: ColumnDefinition<FieldMetadata>;
columnIndex: number;
isInEditMode: boolean;
@ -13,4 +13,4 @@ export type RecordTableCellContextProps = {
};
export const RecordTableCellContext =
createContext<RecordTableCellContextProps>({} as RecordTableCellContextProps);
createContext<RecordTableCellContextValue>({} as RecordTableCellContextValue);

View File

@ -1,19 +1,14 @@
import { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
import { createContext } from 'react';
import { createRequiredContext } from '~/utils/createRequiredContext';
export type RecordTableRowContextProps = {
export type RecordTableRowContextValue = {
pathToShowPage: string;
objectNameSingular: string;
recordId: string;
rowIndex: number;
isSelected: boolean;
inView: boolean;
isPendingRow?: boolean;
isDragging: boolean;
dragHandleProps: DraggableProvidedDragHandleProps | null;
inView?: boolean;
isDragDisabled?: boolean;
};
export const RecordTableRowContext = createContext<RecordTableRowContextProps>(
{} as RecordTableRowContextProps,
);
export const [RecordTableRowContextProvider, useRecordTableRowContextOrThrow] =
createRequiredContext<RecordTableRowContextValue>('RecordTableRowContext');

View File

@ -0,0 +1,14 @@
import { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
import { createRequiredContext } from '~/utils/createRequiredContext';
export type RecordTableRowDraggableContextValue = {
isDragging: boolean;
dragHandleProps: DraggableProvidedDragHandleProps | null;
};
export const [
RecordTableRowDraggableContextProvider,
useRecordTableRowDraggableContextOrThrow,
] = createRequiredContext<RecordTableRowDraggableContextValue>(
'RecordTableRowDraggableContext',
);

View File

@ -1,3 +1,5 @@
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
import { RecordTableCellGrip } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip';
import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading';
@ -13,18 +15,36 @@ export const RecordTableBodyLoading = () => {
return (
<tbody>
{Array.from({ length: 8 }).map((_, rowIndex) => (
<RecordTableTr
isDragging={false}
data-testid={`row-id-${rowIndex}`}
data-selectable-id={`row-id-${rowIndex}`}
<RecordTableRowContextProvider
key={rowIndex}
value={{
pathToShowPage: '',
objectNameSingular: '',
recordId: `${rowIndex}`,
rowIndex,
isSelected: false,
inView: true,
}}
>
<RecordTableCellGrip />
<RecordTableCellCheckbox />
{visibleTableColumns.map((column) => (
<RecordTableCellLoading key={column.fieldMetadataId} />
))}
</RecordTableTr>
<RecordTableRowDraggableContextProvider
value={{
dragHandleProps: {} as any,
isDragging: false,
}}
>
<RecordTableTr
isDragging={false}
data-testid={`row-id-${rowIndex}`}
data-selectable-id={`row-id-${rowIndex}`}
>
<RecordTableCellGrip />
<RecordTableCellCheckbox />
{visibleTableColumns.map((column) => (
<RecordTableCellLoading key={column.fieldMetadataId} />
))}
</RecordTableTr>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
))}
</tbody>
);

View File

@ -35,9 +35,9 @@ export const RecordTableRecordGroupsBody = () => {
key={recordGroupId}
recordGroupId={recordGroupId}
>
{index > 0 && <RecordTableRecordGroupEmptyRow />}
<RecordGroupContext.Provider value={{ recordGroupId }}>
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
{index > 0 && <RecordTableRecordGroupEmptyRow />}
<RecordTableRecordGroupSection />
<RecordTableRecordGroupRows />
</RecordTableBodyDroppable>

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { useCallback, useContext } from 'react';
import { useCallback } from 'react';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { Checkbox } from 'twenty-ui';
@ -16,7 +16,7 @@ const StyledContainer = styled.div`
`;
export const RecordTableCellCheckbox = () => {
const { isSelected } = useContext(RecordTableRowContext);
const { isSelected } = useRecordTableRowContextOrThrow();
const { setCurrentRowSelected } = useSetCurrentRowSelected();

View File

@ -7,7 +7,7 @@ import { isFieldSelect } from '@/object-record/record-field/types/guards/isField
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
@ -22,7 +22,7 @@ export const RecordTableCellFieldContextWrapper = ({
const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId, pathToShowPage } = useContext(RecordTableRowContext);
const { recordId, pathToShowPage } = useRecordTableRowContextOrThrow();
const updateRecord = useContext(RecordUpdateContext);

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { useContext } from 'react';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowDraggableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { css } from '@emotion/react';
import { IconListViewGrip } from 'twenty-ui';
@ -30,9 +30,10 @@ const StyledIconWrapper = styled.div<{ isDragging: boolean }>`
`;
export const RecordTableCellGrip = () => {
const { dragHandleProps, isDragging, isPendingRow } = useContext(
RecordTableRowContext,
);
const { isPendingRow } = useRecordTableRowContextOrThrow();
const { dragHandleProps, isDragging } =
useRecordTableRowDraggableContextOrThrow();
return (
<RecordTableTd

View File

@ -1,8 +1,8 @@
import { useContext, useMemo } from 'react';
import { useMemo } from 'react';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
@ -19,7 +19,7 @@ export const RecordTableCellWrapper = ({
columnIndex: number;
children: React.ReactNode;
}) => {
const { rowIndex } = useContext(RecordTableRowContext);
const { rowIndex } = useRecordTableRowContextOrThrow();
const currentTableCellPosition: TableCellPosition = useMemo(
() => ({

View File

@ -1,10 +1,8 @@
import { useContext } from 'react';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
export const RecordTableLastEmptyCell = () => {
const { isSelected } = useContext(RecordTableRowContext);
const { isSelected } = useRecordTableRowContextOrThrow();
return (
<>

View File

@ -1,20 +1,24 @@
import { RecordTableCellContextProps } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContextProps } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { RecordTableCellContextValue } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContextValue } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowDraggableContextValue } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { FieldMetadataType } from '~/generated/graphql';
export const recordTableRow: RecordTableRowContextProps = {
export const recordTableRowContextValue: RecordTableRowContextValue = {
rowIndex: 2,
isSelected: false,
recordId: 'recordId',
pathToShowPage: '/',
objectNameSingular: 'objectNameSingular',
dragHandleProps: {} as any,
isDragging: false,
inView: true,
isPendingRow: false,
inView: true,
};
export const recordTableCell: RecordTableCellContextProps = {
export const recordTableRowDraggableContextValue: RecordTableRowDraggableContextValue = {
dragHandleProps: {} as any,
isDragging: false,
};
export const recordTableCellContextValue: RecordTableCellContextValue = {
columnIndex: 3,
columnDefinition: {
size: 1,

View File

@ -1,19 +1,28 @@
import { renderHook } from '@testing-library/react';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { recordTableCell, recordTableRow } from '../__mocks__/cell';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import {
recordTableCellContextValue,
recordTableRowContextValue,
recordTableRowDraggableContextValue,
} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
import { useCurrentTableCellPosition } from '../useCurrentCellPosition';
describe('useCurrentTableCellPosition', () => {
it('should return the current table cell position', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<RecordTableRowContext.Provider value={recordTableRow}>
<RecordTableCellContext.Provider value={recordTableCell}>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
<RecordTableCellContext.Provider value={recordTableCellContextValue}>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
);
const { result } = renderHook(() => useCurrentTableCellPosition(), {

View File

@ -3,10 +3,12 @@ import { RecoilRoot } from 'recoil';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import {
recordTableCell,
recordTableRow,
recordTableCellContextValue,
recordTableRowContextValue,
recordTableRowDraggableContextValue,
} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
import { useIsSoftFocusOnCurrentTableCell } from '@/object-record/record-table/record-table-cell/hooks/useIsSoftFocusOnCurrentTableCell';
@ -16,11 +18,15 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
recordTableId="scopeId"
onColumnsChange={jest.fn()}
>
<RecordTableRowContext.Provider value={recordTableRow}>
<RecordTableCellContext.Provider value={recordTableCell}>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
<RecordTableCellContext.Provider value={recordTableCellContextValue}>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</RecordTableComponentInstance>
</RecoilRoot>
);

View File

@ -3,10 +3,12 @@ import { CallbackInterface, RecoilRoot } from 'recoil';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import {
recordTableCell,
recordTableRow,
recordTableCellContextValue,
recordTableRowContextValue,
recordTableRowDraggableContextValue,
} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
import { useMoveSoftFocusToCurrentCellOnHover } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
@ -80,11 +82,15 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
recordTableId="scopeId"
onColumnsChange={jest.fn()}
>
<RecordTableRowContext.Provider value={recordTableRow}>
<RecordTableCellContext.Provider value={recordTableCell}>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
<RecordTableCellContext.Provider value={recordTableCellContextValue}>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</RecordTableComponentInstance>
</RecoilRoot>
);

View File

@ -8,10 +8,12 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import {
recordTableCell,
recordTableRow,
recordTableCellContextValue,
recordTableRowContextValue,
recordTableRowDraggableContextValue,
} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup';
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
@ -55,13 +57,17 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
isLabelIdentifier: false,
}}
>
<RecordTableRowContext.Provider value={recordTableRow}>
<RecordTableCellContext.Provider
value={{ ...recordTableCell, columnIndex: 0 }}
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue, columnIndex: 0 }}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>

View File

@ -8,10 +8,12 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import {
recordTableCell,
recordTableRow,
recordTableCellContextValue,
recordTableRowContextValue,
recordTableRowDraggableContextValue,
} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup';
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
@ -54,13 +56,17 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
isLabelIdentifier: false,
}}
>
<RecordTableRowContext.Provider value={recordTableRow}>
<RecordTableCellContext.Provider
value={{ ...recordTableCell, columnIndex: 0 }}
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue, columnIndex: 0 }}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>

View File

@ -5,7 +5,7 @@ import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useI
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@ -30,9 +30,9 @@ export type OpenTableCellArgs = {
export const useOpenRecordTableCellFromCell = () => {
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
const { recordId, fieldDefinition } = useContext(FieldContext);
const { pathToShowPage, objectNameSingular } = useContext(
RecordTableRowContext,
);
const { pathToShowPage, objectNameSingular } =
useRecordTableRowContextOrThrow();
const { onOpenTableCell } = useRecordTableBodyContextOrThrow();

View File

@ -1,12 +1,12 @@
import styled from '@emotion/styled';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
import { useTheme } from '@emotion/react';
import { IconComponent } from 'twenty-ui';
const StyledTrContainer = styled.tr`
const StyledRecordTableDraggableTr = styled(RecordTableDraggableTr)`
cursor: pointer;
`;
@ -36,33 +36,40 @@ const StyledEmptyTd = styled.td`
`;
type RecordTableActionRowProps = {
draggableId: string;
draggableIndex: number;
LeftIcon: IconComponent;
text: string;
onClick?: (event?: React.MouseEvent<HTMLTableRowElement>) => void;
};
export const RecordTableActionRow = ({
draggableId,
draggableIndex,
LeftIcon,
text,
onClick,
}: RecordTableActionRowProps) => {
const theme = useTheme();
const visibleColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
);
const { visibleTableColumns } = useRecordTableContextOrThrow();
return (
<StyledTrContainer onClick={onClick}>
<StyledRecordTableDraggableTr
draggableId={draggableId}
draggableIndex={draggableIndex}
onClick={onClick}
isDragDisabled
>
<td aria-hidden />
<StyledIconContainer>
<LeftIcon size={theme.icon.size.sm} color={theme.font.color.tertiary} />
</StyledIconContainer>
<StyledRecordTableTdTextContainer colSpan={visibleColumns.length}>
<StyledRecordTableTdTextContainer colSpan={visibleTableColumns.length}>
<StyledText>{text}</StyledText>
</StyledRecordTableTdTextContainer>
<StyledEmptyTd />
<StyledEmptyTd />
</StyledTrContainer>
</StyledRecordTableDraggableTr>
);
};

View File

@ -1,11 +1,12 @@
import { useContext } from 'react';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowDraggableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableCellsEmpty } from '@/object-record/record-table/record-table-row/components/RecordTableCellsEmpty';
import { RecordTableCellsVisible } from '@/object-record/record-table/record-table-row/components/RecordTableCellsVisible';
export const RecordTableCells = () => {
const { inView, isDragging } = useContext(RecordTableRowContext);
const { inView } = useRecordTableRowContextOrThrow();
const { isDragging } = useRecordTableRowDraggableContextOrThrow();
const areCellsVisible = inView || isDragging;

View File

@ -1,11 +1,10 @@
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useContext } from 'react';
export const RecordTableCellsEmpty = () => {
const { isSelected } = useContext(RecordTableRowContext);
const { isSelected } = useRecordTableRowContextOrThrow();
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,

View File

@ -1,20 +1,15 @@
import { useContext } from 'react';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowDraggableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell';
import { RecordTableCellWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellWrapper';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { tableCellWidthsComponentState } from '@/object-record/record-table/states/tableCellWidthsComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableCellsVisible = () => {
const { isSelected, isDragging } = useContext(RecordTableRowContext);
const { isSelected } = useRecordTableRowContextOrThrow();
const [tableCellWidths] = useRecoilComponentStateV2(
tableCellWidthsComponentState,
);
const { isDragging } = useRecordTableRowDraggableContextOrThrow();
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
@ -28,7 +23,7 @@ export const RecordTableCellsVisible = () => {
<RecordTableTd
isSelected={isSelected}
isDragging={isDragging}
width={tableCellWidths[2]}
width={visibleTableColumns[0].size}
>
<RecordTableCell />
</RecordTableTd>
@ -42,7 +37,7 @@ export const RecordTableCellsVisible = () => {
<RecordTableTd
isSelected={isSelected}
isDragging={isDragging}
width={tableCellWidths[columnIndex + 3]}
width={column.size}
>
<RecordTableCell />
</RecordTableTd>

View File

@ -0,0 +1,63 @@
import { useTheme } from '@emotion/react';
import { Draggable, DraggableId } from '@hello-pangea/dnd';
import { forwardRef, ReactNode } from 'react';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
import { combineRefs } from '~/utils/combineRefs';
type RecordTableDraggableTrProps = {
draggableId: DraggableId;
draggableIndex: number;
isDragDisabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLTableRowElement>) => void;
children: ReactNode;
};
export const RecordTableDraggableTr = forwardRef<
HTMLTableRowElement,
RecordTableDraggableTrProps
>(({ draggableId, draggableIndex, isDragDisabled, onClick, children }, ref) => {
const theme = useTheme();
return (
<Draggable
draggableId={draggableId}
index={draggableIndex}
isDragDisabled={isDragDisabled}
>
{(draggableProvided, draggableSnapshot) => (
<RecordTableTr
ref={combineRefs<HTMLTableRowElement>(
ref,
draggableProvided.innerRef,
)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps}
style={{
...draggableProvided.draggableProps.style,
background: draggableSnapshot.isDragging
? theme.background.transparent.light
: 'none',
borderColor: draggableSnapshot.isDragging
? `${theme.border.color.medium}`
: 'transparent',
}}
isDragging={draggableSnapshot.isDragging}
data-testid={`row-id-${draggableId}`}
data-selectable-id={draggableId}
onClick={onClick}
>
<RecordTableRowDraggableContextProvider
value={{
isDragging: draggableSnapshot.isDragging,
dragHandleProps: draggableProvided.dragHandleProps,
}}
>
{children}
</RecordTableRowDraggableContextProvider>
</RecordTableTr>
)}
</Draggable>
);
});

View File

@ -1,18 +1,14 @@
import { useTheme } from '@emotion/react';
import { Draggable } from '@hello-pangea/dnd';
import { ReactNode, useContext, useEffect, useRef } from 'react';
import { useInView } from 'react-intersection-observer';
import { ReactNode, useContext, useEffect } from 'react';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { tableCellWidthsComponentState } from '@/object-record/record-table/states/tableCellWidthsComponentState';
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useInView } from 'react-intersection-observer';
type RecordTableRowWrapperProps = {
recordId: string;
@ -29,18 +25,15 @@ export const RecordTableRowWrapper = ({
isPendingRow,
children,
}: RecordTableRowWrapperProps) => {
const trRef = useRef<HTMLTableRowElement>(null);
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { onIndexRecordsLoaded } = useRecordIndexContextOrThrow();
const theme = useTheme();
const currentRowSelected = useRecoilComponentFamilyValueV2(
isRowSelectedComponentFamilyState,
recordId,
);
const { onIndexRecordsLoaded } = useRecordIndexContextOrThrow();
const scrollWrapperRef = useContext(
RecordTableWithWrappersScrollWrapperContext,
);
@ -52,24 +45,6 @@ export const RecordTableRowWrapper = ({
rootMargin: '1000px',
});
const setTableCellWidths = useSetRecoilComponentStateV2(
tableCellWidthsComponentState,
);
useEffect(() => {
if (rowIndexForFocus === 0) {
const tdArray = Array.from(
trRef.current?.getElementsByTagName('td') ?? [],
);
const tdWidths = tdArray.map((td) => {
return td.getBoundingClientRect().width;
});
setTableCellWidths(tdWidths);
}
}, [trRef, rowIndexForFocus, setTableCellWidths]);
// TODO: find a better way to emit this event
useEffect(() => {
if (inView) {
@ -78,55 +53,29 @@ export const RecordTableRowWrapper = ({
}, [inView, onIndexRecordsLoaded]);
return (
<Draggable
<RecordTableDraggableTr
ref={elementRef}
key={recordId}
draggableId={recordId}
index={rowIndexForDrag}
draggableIndex={rowIndexForDrag}
isDragDisabled={isPendingRow}
>
{(draggableProvided, draggableSnapshot) => (
<RecordTableTr
ref={(node) => {
// @ts-expect-error - TS doesn't know that node.current is assignable
trRef.current = node;
elementRef(node);
draggableProvided.innerRef(node);
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps}
style={{
...draggableProvided.draggableProps.style,
background: draggableSnapshot.isDragging
? theme.background.transparent.light
: 'none',
borderColor: draggableSnapshot.isDragging
? `${theme.border.color.medium}`
: 'transparent',
}}
isDragging={draggableSnapshot.isDragging}
data-testid={`row-id-${recordId}`}
data-selectable-id={recordId}
>
<RecordTableRowContext.Provider
value={{
recordId,
rowIndex: rowIndexForFocus,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular: objectMetadataItem.nameSingular,
}) + recordId,
<RecordTableRowContextProvider
value={{
recordId,
rowIndex: rowIndexForFocus,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular: objectMetadataItem.nameSingular,
isSelected: currentRowSelected,
isPendingRow,
isDragging: draggableSnapshot.isDragging,
dragHandleProps: draggableProvided.dragHandleProps,
inView,
}}
>
{children}
</RecordTableRowContext.Provider>
</RecordTableTr>
)}
</Draggable>
}) + recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isSelected: currentRowSelected,
isPendingRow,
inView,
}}
>
{children}
</RecordTableRowContextProvider>
</RecordTableDraggableTr>
);
};

View File

@ -1,13 +1,12 @@
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
export const useSetCurrentRowSelected = () => {
const { recordId } = useContext(RecordTableRowContext);
const { recordId } = useRecordTableRowContextOrThrow();
const isRowSelectedFamilyState = useRecoilComponentCallbackStateV2(
isRowSelectedComponentFamilyState,

View File

@ -1,9 +1,11 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { IconPlus } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
@ -17,6 +19,10 @@ export const RecordTableRecordGroupSectionAddNew = () => {
currentRecordGroupId,
);
const recordIds = useRecoilComponentValueV2(
recordIndexAllRecordIdsComponentSelector,
);
const { createNewTableRecordInGroup } =
useCreateNewTableRecord(recordTableId);
@ -28,6 +34,8 @@ export const RecordTableRecordGroupSectionAddNew = () => {
return (
<RecordTableActionRow
draggableId={`add-new-record-${currentRecordGroupId}`}
draggableIndex={recordIds.length + 2}
LeftIcon={IconPlus}
text="Add new"
onClick={handleAddNewRecord}

View File

@ -1,9 +1,11 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { IconArrowDown } from 'twenty-ui';
export const RecordTableRecordGroupSectionLoadMore = () => {
@ -18,6 +20,10 @@ export const RecordTableRecordGroupSectionLoadMore = () => {
currentRecordGroupId,
);
const recordIds = useRecoilComponentValueV2(
recordIndexAllRecordIdsComponentSelector,
);
const handleLoadMore = () => {
fetchMoreRecords();
};
@ -28,6 +34,8 @@ export const RecordTableRecordGroupSectionLoadMore = () => {
return (
<RecordTableActionRow
draggableId={`load-more-records-${currentRecordGroupId}`}
draggableIndex={recordIds.length + 1}
LeftIcon={IconArrowDown}
text="Load more"
onClick={handleLoadMore}

View File

@ -0,0 +1,15 @@
import { isFunction } from '@sniptt/guards';
import { MutableRefObject, Ref } from 'react';
import { isDefined } from '~/utils/isDefined';
export const combineRefs = <T>(...refs: (Ref<T> | undefined)[]) => {
return (node: T) => {
for (const ref of refs) {
if (isFunction(ref)) {
ref(node);
} else if (isDefined(ref) && 'current' in ref) {
(ref as MutableRefObject<T | null>).current = node;
}
}
};
};