Show Header in RecordTable on empty state and show groups in Group By views (#11416)

Closes #11298

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Harshit Rai Verma
2025-04-08 01:35:59 +05:30
committed by GitHub
parent 07b25a2aad
commit 6bc18960c9
6 changed files with 152 additions and 56 deletions

View File

@ -1,36 +1,17 @@
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useRef } from 'react';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableStickyBottomEffect } from '@/object-record/record-table/components/RecordTableStickyBottomEffect'; import { RecordTableBodyEffectsWrapper } from '@/object-record/record-table/components/RecordTableBodyEffectsWrapper';
import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect'; import { RecordTableContent } from '@/object-record/record-table/components/RecordTableContent';
import { RecordTableEmpty } from '@/object-record/record-table/components/RecordTableEmpty';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { RecordTableBodyUnselectEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect';
import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody';
import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects';
import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRef } from 'react';
const StyledTable = styled.table`
border-radius: ${({ theme }) => theme.border.radius.sm};
border-spacing: 0;
table-layout: fixed;
width: 100%;
.footer-sticky tr:nth-last-of-type(2) td {
border-bottom-color: ${({ theme }) => theme.background.transparent};
}
`;
export const RecordTable = () => { export const RecordTable = () => {
const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow(); const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow();
@ -56,51 +37,46 @@ export const RecordTable = () => {
recordTableId, recordTableId,
); );
const recordTableIsEmpty =
!isRecordTableInitialLoading && allRecordIds.length === 0;
const { resetTableRowSelection, setRowSelected } = useRecordTable({ const { resetTableRowSelection, setRowSelected } = useRecordTable({
recordTableId, recordTableId,
}); });
const recordTableIsEmpty =
!isRecordTableInitialLoading && allRecordIds.length === 0;
if (!isNonEmptyString(objectNameSingular)) { if (!isNonEmptyString(objectNameSingular)) {
return <></>; return <></>;
} }
const handleDragSelectionStart = () => {
resetTableRowSelection();
toggleClickOutsideListener(false);
};
const handleDragSelectionEnd = () => {
toggleClickOutsideListener(true);
};
return ( return (
<> <>
{!hasRecordGroups ? ( <RecordTableBodyEffectsWrapper
<RecordTableNoRecordGroupBodyEffect /> hasRecordGroups={hasRecordGroups}
) : ( tableBodyRef={tableBodyRef}
<RecordTableRecordGroupBodyEffects /> />
)}
<RecordTableBodyUnselectEffect tableBodyRef={tableBodyRef} />
{recordTableIsEmpty ? ( {recordTableIsEmpty ? (
<RecordTableEmptyState /> <RecordTableEmpty
tableBodyRef={tableBodyRef}
hasRecordGroups={hasRecordGroups}
/>
) : ( ) : (
<> <RecordTableContent
<StyledTable ref={tableBodyRef}> tableBodyRef={tableBodyRef}
<RecordTableHeader /> handleDragSelectionStart={handleDragSelectionStart}
{!hasRecordGroups ? ( handleDragSelectionEnd={handleDragSelectionEnd}
<RecordTableNoRecordGroupBody /> setRowSelected={setRowSelected}
) : ( hasRecordGroups={hasRecordGroups}
<RecordTableRecordGroupsBody /> />
)}
<RecordTableStickyEffect />
<RecordTableStickyBottomEffect />
</StyledTable>
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={() => {
resetTableRowSelection();
toggleClickOutsideListener(false);
}}
onDragSelectionChange={setRowSelected}
onDragSelectionEnd={() => {
toggleClickOutsideListener(true);
}}
/>
</>
)} )}
</> </>
); );

View File

@ -0,0 +1,22 @@
import { RecordTableBodyUnselectEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect';
import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects';
export interface RecordTableBodyEffectsWrapperProps {
hasRecordGroups: boolean;
tableBodyRef: React.RefObject<HTMLTableElement>;
}
export const RecordTableBodyEffectsWrapper = ({
hasRecordGroups,
tableBodyRef,
}: RecordTableBodyEffectsWrapperProps) => (
<>
{hasRecordGroups ? (
<RecordTableRecordGroupBodyEffects />
) : (
<RecordTableNoRecordGroupBodyEffect />
)}
<RecordTableBodyUnselectEffect tableBodyRef={tableBodyRef} />
</>
);

View File

@ -0,0 +1,42 @@
import { RecordTableStickyBottomEffect } from '@/object-record/record-table/components/RecordTableStickyBottomEffect';
import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect';
import { StyledTable } from '@/object-record/record-table/components/RecordTableStyles';
import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody';
import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
export interface RecordTableContentProps {
tableBodyRef: React.RefObject<HTMLTableElement>;
handleDragSelectionStart: () => void;
handleDragSelectionEnd: () => void;
setRowSelected: (rowId: string, selected: boolean) => void;
hasRecordGroups: boolean;
}
export const RecordTableContent = ({
tableBodyRef,
handleDragSelectionStart,
handleDragSelectionEnd,
setRowSelected,
hasRecordGroups,
}: RecordTableContentProps) => (
<>
<StyledTable ref={tableBodyRef}>
<RecordTableHeader />
{hasRecordGroups ? (
<RecordTableRecordGroupsBody />
) : (
<RecordTableNoRecordGroupBody />
)}
<RecordTableStickyEffect />
<RecordTableStickyBottomEffect />
</StyledTable>
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={handleDragSelectionStart}
onDragSelectionChange={setRowSelected}
onDragSelectionEnd={handleDragSelectionEnd}
/>
</>
);

View File

@ -0,0 +1,25 @@
import { StyledTable } from '@/object-record/record-table/components/RecordTableStyles';
import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
export interface RecordTableEmptyProps {
tableBodyRef: React.RefObject<HTMLTableElement>;
hasRecordGroups: boolean;
}
export const RecordTableEmpty = ({
tableBodyRef,
hasRecordGroups,
}: RecordTableEmptyProps) => (
<>
<StyledTable ref={tableBodyRef}>
<RecordTableHeader />
</StyledTable>
{hasRecordGroups ? (
<RecordTableRecordGroupsBody />
) : (
<RecordTableEmptyState />
)}
</>
);

View File

@ -0,0 +1,12 @@
import styled from '@emotion/styled';
export const StyledTable = styled.table`
border-radius: ${({ theme }) => theme.border.radius.sm};
border-spacing: 0;
table-layout: fixed;
width: 100%;
.footer-sticky tr:nth-last-of-type(2) td {
border-bottom-color: ${({ theme }) => theme.background.transparent};
}
`;

View File

@ -1,6 +1,9 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector'; import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { Checkbox } from 'twenty-ui/input'; import { Checkbox } from 'twenty-ui/input';
@ -30,6 +33,21 @@ export const RecordTableHeaderCheckboxColumn = () => {
allRowsSelectedStatus === 'all' || allRowsSelectedStatus === 'some'; allRowsSelectedStatus === 'all' || allRowsSelectedStatus === 'some';
const indeterminate = allRowsSelectedStatus === 'some'; const indeterminate = allRowsSelectedStatus === 'some';
const { recordTableId } = useRecordTableContextOrThrow();
const isRecordTableInitialLoading = useRecoilComponentValueV2(
isRecordTableInitialLoadingComponentState,
recordTableId,
);
const allRecordIds = useRecoilComponentValueV2(
recordIndexAllRecordIdsComponentSelector,
recordTableId,
);
const recordTableIsEmpty =
!isRecordTableInitialLoading && allRecordIds.length === 0;
const onChange = () => { const onChange = () => {
if (checked) { if (checked) {
setHasUserSelectedAllRows(false); setHasUserSelectedAllRows(false);
@ -48,6 +66,7 @@ export const RecordTableHeaderCheckboxColumn = () => {
checked={checked} checked={checked}
onChange={onChange} onChange={onChange}
indeterminate={indeterminate} indeterminate={indeterminate}
disabled={recordTableIsEmpty}
/> />
</StyledContainer> </StyledContainer>
</StyledColumnHeaderCell> </StyledColumnHeaderCell>