first column of objects table fixed (#3147)

* ui:first column of objects table fixed

* refactor shadow style logic

* Minor renaming fixes

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Muralidhar 2024-01-02 16:07:29 +05:30 committed by GitHub
parent 858c294f14
commit 2204345300
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 269 additions and 154 deletions

View File

@ -6,7 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { RecordUpdateHookParams } from '@/object-record/field/contexts/FieldContext'; import { RecordUpdateHookParams } from '@/object-record/field/contexts/FieldContext';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordTable } from '@/object-record/record-table/components/RecordTable'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId'; import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown'; import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown';
@ -24,6 +24,7 @@ const StyledContainer = styled.div`
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
`; `;
export const RecordTableContainer = ({ export const RecordTableContainer = ({
@ -103,7 +104,7 @@ export const RecordTableContainer = ({
/> />
</SpreadsheetImportProvider> </SpreadsheetImportProvider>
<RecordTableEffect recordTableId={recordTableId} viewBarId={viewBarId} /> <RecordTableEffect recordTableId={recordTableId} viewBarId={viewBarId} />
<RecordTable <RecordTableWithWrappers
recordTableId={recordTableId} recordTableId={recordTableId}
viewBarId={viewBarId} viewBarId={viewBarId}
updateRecordMutation={updateEntity} updateRecordMutation={updateEntity}

View File

@ -1,70 +1,22 @@
import { useRef } from 'react'; import { useContext } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordTableBody } from '@/object-record/record-table/components/RecordTableBody';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { RecordTableBodyEffect } from '@/object-record/record-table/components/RecordTableBodyEffect';
import { isRecordTableInitialLoadingState } from '@/object-record/record-table/states/isRecordTableInitialLoadingState'; import { RecordTableHeader } from '@/object-record/record-table/components/RecordTableHeader';
import { IconPlus } from '@/ui/display/icon'; import { RecordTableRefContext } from '@/object-record/record-table/contexts/RecordTableRefContext';
import { Button } from '@/ui/input/button/components/Button'; import { rgba } from '@/ui/theme/constants/colors';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useViewFields } from '@/views/hooks/internal/useViewFields';
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { useRecordTable } from '../hooks/useRecordTable';
import { RecordTableScope } from '../scopes/RecordTableScope';
import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
import { RecordTableBody } from './RecordTableBody';
import { RecordTableBodyEffect } from './RecordTableBodyEffect';
import { RecordTableHeader } from './RecordTableHeader';
import { RecordTableInternalEffect } from './RecordTableInternalEffect';
const StyledObjectEmptyContainer = styled.div`
align-items: center;
align-self: stretch;
display: flex;
flex: 1 0 0;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: center;
padding-bottom: ${({ theme }) => theme.spacing(16)};
padding-left: ${({ theme }) => theme.spacing(4)};
padding-right: ${({ theme }) => theme.spacing(4)};
padding-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledEmptyObjectTitle = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.xxl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: ${({ theme }) => theme.text.lineHeight.md};
`;
const StyledEmptyObjectSubTitle = styled.div`
color: ${({ theme }) => theme.font.color.extraLight};
font-size: ${({ theme }) => theme.font.size.xxl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: ${({ theme }) => theme.text.lineHeight.md};
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledTable = styled.table` const StyledTable = styled.table`
border-collapse: collapse;
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
border-spacing: 0; border-spacing: 0;
margin-left: ${({ theme }) => theme.table.horizontalCellMargin};
margin-right: ${({ theme }) => theme.table.horizontalCellMargin}; margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
table-layout: fixed; table-layout: fixed;
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2); width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
th { th {
border: 1px solid ${({ theme }) => theme.border.color.light}; border-block: 1px solid ${({ theme }) => theme.border.color.light};
border-collapse: collapse;
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
padding: 0; padding: 0;
text-align: left; text-align: left;
@ -79,8 +31,7 @@ const StyledTable = styled.table`
} }
td { td {
border: 1px solid ${({ theme }) => theme.border.color.light}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-collapse: collapse;
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
padding: 0; padding: 0;
@ -94,107 +45,67 @@ const StyledTable = styled.table`
border-right-color: transparent; border-right-color: transparent;
} }
} }
`;
const StyledTableWithHeader = styled.div` th,
display: flex; td {
flex: 1; background-color: ${({ theme }) => theme.background.primary};
flex-direction: column; border-right: 1px solid ${({ theme }) => theme.border.color.light};
width: 100%; }
`;
const StyledTableContainer = styled.div` thead th:nth-of-type(-n + 2),
display: flex; tbody td:nth-of-type(-n + 2) {
flex-direction: column; position: sticky;
height: 100%; z-index: 2;
position: relative; border-right: none;
}
thead th:nth-of-type(1),
tbody td:nth-of-type(1) {
left: 0;
}
thead th:nth-of-type(2),
tbody td:nth-of-type(2) {
left: calc(${({ theme }) => theme.table.checkboxColumnWidth} - 2px);
}
tbody td:nth-of-type(2)::after,
thead th:nth-of-type(2)::after {
content: '';
height: calc(100% + 1px);
position: absolute;
top: 0;
width: 4px;
right: -4px;
}
&.freeze-first-columns-shadow thead th:nth-of-type(2)::after,
&.freeze-first-columns-shadow tbody td:nth-of-type(2)::after {
box-shadow: ${({ theme }) =>
`4px 0px 4px -4px ${
theme.name === 'dark'
? rgba(theme.grayScale.gray50, 0.8)
: rgba(theme.grayScale.gray100, 0.25)
} inset`};
}
thead th:nth-of-type(3),
tbody td:nth-of-type(3) {
border-left: 1px solid ${({ theme }) => theme.border.color.light};
}
`; `;
type RecordTableProps = { type RecordTableProps = {
recordTableId: string;
viewBarId: string;
updateRecordMutation: (params: any) => void;
createRecord: () => void; createRecord: () => void;
}; };
export const RecordTable = ({ export const RecordTable = ({ createRecord }: RecordTableProps) => {
updateRecordMutation, const recordTableRef = useContext(RecordTableRefContext);
createRecord,
recordTableId,
viewBarId,
}: RecordTableProps) => {
const tableBodyRef = useRef<HTMLDivElement>(null);
const numberOfTableRows = useRecoilValue(numberOfTableRowsState);
const isRecordTableInitialLoading = useRecoilValue(
isRecordTableInitialLoadingState,
);
const {
scopeId: objectNamePlural,
resetTableRowSelection,
setRowSelectedState,
} = useRecordTable({
recordTableScopeId: recordTableId,
});
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
{
objectNameSingular,
},
);
const { persistViewFields } = useViewFields(viewBarId);
return ( return (
<RecordTableScope <StyledTable ref={recordTableRef} className="entity-table-cell">
recordTableScopeId={recordTableId} <RecordTableHeader createRecord={createRecord} />
onColumnsChange={useRecoilCallback(() => (columns) => { <RecordTableBodyEffect />
persistViewFields(mapColumnDefinitionsToViewFields(columns)); <RecordTableBody />
})} </StyledTable>
>
<ScrollWrapper>
<RecordUpdateContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader>
<StyledTableContainer>
<div ref={tableBodyRef}>
<StyledTable className="entity-table-cell">
<RecordTableHeader createRecord={createRecord} />
<RecordTableBodyEffect />
<RecordTableBody />
</StyledTable>
<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}
/>
</StyledObjectEmptyContainer>
)}
</StyledTableContainer>
</StyledTableWithHeader>
</RecordUpdateContext.Provider>
</ScrollWrapper>
</RecordTableScope>
); );
}; };

View File

@ -0,0 +1,22 @@
import { useContext } from 'react';
import { useInView } from 'react-intersection-observer';
import { RecordTableRefContext } from '@/object-record/record-table/contexts/RecordTableRefContext';
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
export const RecordTableFirstColumnScrollObserver = () => {
const scrollWrapperRef = useContext(ScrollWrapperContext);
const recordTableRef = useContext(RecordTableRefContext);
const { ref: elementRef } = useInView({
root: scrollWrapperRef.current,
onChange: (inView) => {
recordTableRef.current?.classList.toggle(
'freeze-first-columns-shadow',
!inView,
);
},
});
return <div ref={elementRef}></div>;
};

View File

@ -28,7 +28,7 @@ const StyledColumnHeaderCell = styled.th<{
${({ theme }) => { ${({ theme }) => {
return ` return `
&:hover { &:hover {
background: ${theme.background.transparent.light}; background: ${theme.background.quaternary};
}; };
`; `;
}}; }};

View File

@ -0,0 +1,19 @@
import { useRef } from 'react';
import { RecordTableRefContext } from '@/object-record/record-table/contexts/RecordTableRefContext';
export type RecordTableRefContextWrapperProps = {
children: React.ReactNode;
};
export const RecordTableRefContextWrapper = ({
children,
}: RecordTableRefContextWrapperProps) => {
const tableRef = useRef<HTMLTableElement>(null);
return (
<RecordTableRefContext.Provider value={tableRef}>
{children}
</RecordTableRefContext.Provider>
);
};

View File

@ -0,0 +1,154 @@
import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
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 { isRecordTableInitialLoadingState } from '@/object-record/record-table/states/isRecordTableInitialLoadingState';
import { IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useViewFields } from '@/views/hooks/internal/useViewFields';
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { useRecordTable } from '../hooks/useRecordTable';
import { RecordTableScope } from '../scopes/RecordTableScope';
import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
import { RecordTableInternalEffect } from './RecordTableInternalEffect';
const StyledObjectEmptyContainer = styled.div`
align-items: center;
align-self: stretch;
display: flex;
flex: 1 0 0;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: center;
padding-bottom: ${({ theme }) => theme.spacing(16)};
padding-left: ${({ theme }) => theme.spacing(4)};
padding-right: ${({ theme }) => theme.spacing(4)};
padding-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledEmptyObjectTitle = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.xxl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: ${({ theme }) => theme.text.lineHeight.md};
`;
const StyledEmptyObjectSubTitle = styled.div`
color: ${({ theme }) => theme.font.color.extraLight};
font-size: ${({ theme }) => theme.font.size.xxl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: ${({ theme }) => theme.text.lineHeight.md};
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledTableWithHeader = styled.div`
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
`;
const StyledTableContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
position: relative;
`;
type RecordTableWithWrappersProps = {
recordTableId: string;
viewBarId: string;
updateRecordMutation: (params: any) => void;
createRecord: () => void;
};
export const RecordTableWithWrappers = ({
updateRecordMutation,
createRecord,
recordTableId,
viewBarId,
}: RecordTableWithWrappersProps) => {
const tableBodyRef = useRef<HTMLDivElement>(null);
const numberOfTableRows = useRecoilValue(numberOfTableRowsState);
const isRecordTableInitialLoading = useRecoilValue(
isRecordTableInitialLoadingState,
);
const {
scopeId: objectNamePlural,
resetTableRowSelection,
setRowSelectedState,
} = useRecordTable({
recordTableScopeId: recordTableId,
});
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
{
objectNameSingular,
},
);
const { persistViewFields } = useViewFields(viewBarId);
return (
<RecordTableScope
recordTableScopeId={recordTableId}
onColumnsChange={useRecoilCallback(() => (columns) => {
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}
/>
</StyledObjectEmptyContainer>
)}
</StyledTableContainer>
</StyledTableWithHeader>
</RecordUpdateContext.Provider>
</RecordTableRefContextWrapper>
</ScrollWrapper>
</RecordTableScope>
);
};

View File

@ -0,0 +1,7 @@
import { createContext, RefObject } from 'react';
export const RecordTableRefContext = createContext<RefObject<HTMLTableElement>>(
{
current: null,
},
);

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { RecordTable } from '@/object-record/record-table/components/RecordTable'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId'; import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown'; import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
@ -30,7 +30,7 @@ export const SignInBackgroundMockContainer = () => {
recordTableId={recordTableId} recordTableId={recordTableId}
viewId={viewBarId} viewId={viewBarId}
/> />
<RecordTable <RecordTableWithWrappers
recordTableId={recordTableId} recordTableId={recordTableId}
viewBarId={viewBarId} viewBarId={viewBarId}
createRecord={() => {}} createRecord={() => {}}

View File

@ -41,6 +41,7 @@ const common = {
table: { table: {
horizontalCellMargin: '8px', horizontalCellMargin: '8px',
checkboxColumnWidth: '32px', checkboxColumnWidth: '32px',
horizontalCellPadding: '8px',
}, },
rightDrawerWidth: '500px', rightDrawerWidth: '500px',
clickableElementBackgroundTransition: 'background 0.1s ease', clickableElementBackgroundTransition: 'background 0.1s ease',