diff --git a/packages/twenty-front/src/modules/object-record/components/RecordTableContainer.tsx b/packages/twenty-front/src/modules/object-record/components/RecordTableContainer.tsx
index 5f897dcf33..0045a46201 100644
--- a/packages/twenty-front/src/modules/object-record/components/RecordTableContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/components/RecordTableContainer.tsx
@@ -6,7 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { RecordUpdateHookParams } from '@/object-record/field/contexts/FieldContext';
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 { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown';
@@ -24,6 +24,7 @@ const StyledContainer = styled.div`
flex-direction: column;
height: 100%;
overflow: auto;
+ padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
`;
export const RecordTableContainer = ({
@@ -103,7 +104,7 @@ export const RecordTableContainer = ({
/>
- 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)};
-`;
+import { RecordTableBody } from '@/object-record/record-table/components/RecordTableBody';
+import { RecordTableBodyEffect } from '@/object-record/record-table/components/RecordTableBodyEffect';
+import { RecordTableHeader } from '@/object-record/record-table/components/RecordTableHeader';
+import { RecordTableRefContext } from '@/object-record/record-table/contexts/RecordTableRefContext';
+import { rgba } from '@/ui/theme/constants/colors';
const StyledTable = styled.table`
- border-collapse: collapse;
-
border-radius: ${({ theme }) => theme.border.radius.sm};
border-spacing: 0;
- margin-left: ${({ theme }) => theme.table.horizontalCellMargin};
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
table-layout: fixed;
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
th {
- border: 1px solid ${({ theme }) => theme.border.color.light};
- border-collapse: collapse;
+ border-block: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.tertiary};
padding: 0;
text-align: left;
@@ -79,8 +31,7 @@ const StyledTable = styled.table`
}
td {
- border: 1px solid ${({ theme }) => theme.border.color.light};
- border-collapse: collapse;
+ border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.primary};
padding: 0;
@@ -94,107 +45,67 @@ const StyledTable = styled.table`
border-right-color: transparent;
}
}
-`;
-const StyledTableWithHeader = styled.div`
- display: flex;
- flex: 1;
- flex-direction: column;
- width: 100%;
-`;
+ th,
+ td {
+ background-color: ${({ theme }) => theme.background.primary};
+ border-right: 1px solid ${({ theme }) => theme.border.color.light};
+ }
-const StyledTableContainer = styled.div`
- display: flex;
- flex-direction: column;
- height: 100%;
- position: relative;
+ thead th:nth-of-type(-n + 2),
+ tbody td:nth-of-type(-n + 2) {
+ position: sticky;
+ z-index: 2;
+ 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 = {
- recordTableId: string;
- viewBarId: string;
- updateRecordMutation: (params: any) => void;
createRecord: () => void;
};
-export const RecordTable = ({
- updateRecordMutation,
- createRecord,
- recordTableId,
- viewBarId,
-}: RecordTableProps) => {
- const tableBodyRef = useRef(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);
+export const RecordTable = ({ createRecord }: RecordTableProps) => {
+ const recordTableRef = useContext(RecordTableRefContext);
return (
- (columns) => {
- persistViewFields(mapColumnDefinitionsToViewFields(columns));
- })}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
- {!isRecordTableInitialLoading && numberOfTableRows === 0 && (
-
-
- No {foundObjectMetadataItem?.namePlural}
-
-
- Create one:
-
-
-
- )}
-
-
-
-
-
+
+
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableFirstColumnScrollObserver.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableFirstColumnScrollObserver.tsx
new file mode 100644
index 0000000000..5554eef94d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableFirstColumnScrollObserver.tsx
@@ -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 ;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx
index 8c6e6933f4..25d3aeb608 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx
@@ -28,7 +28,7 @@ const StyledColumnHeaderCell = styled.th<{
${({ theme }) => {
return `
&:hover {
- background: ${theme.background.transparent.light};
+ background: ${theme.background.quaternary};
};
`;
}};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRefContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRefContext.tsx
new file mode 100644
index 0000000000..efe0946070
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRefContext.tsx
@@ -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(null);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx
new file mode 100644
index 0000000000..2fe1effa67
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx
@@ -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(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 (
+ (columns) => {
+ persistViewFields(mapColumnDefinitionsToViewFields(columns));
+ })}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {!isRecordTableInitialLoading && numberOfTableRows === 0 && (
+
+
+ No {foundObjectMetadataItem?.namePlural}
+
+
+ Create one:
+
+
+
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRefContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRefContext.ts
new file mode 100644
index 0000000000..f927b6ab68
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRefContext.ts
@@ -0,0 +1,7 @@
+import { createContext, RefObject } from 'react';
+
+export const RecordTableRefContext = createContext>(
+ {
+ current: null,
+ },
+);
diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx
index 85c2472bd5..c6903e17e5 100644
--- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx
+++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx
@@ -1,6 +1,6 @@
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 { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
@@ -30,7 +30,7 @@ export const SignInBackgroundMockContainer = () => {
recordTableId={recordTableId}
viewId={viewBarId}
/>
- {}}
diff --git a/packages/twenty-front/src/modules/ui/theme/constants/theme.ts b/packages/twenty-front/src/modules/ui/theme/constants/theme.ts
index ca51c46f8a..fdb5db608d 100644
--- a/packages/twenty-front/src/modules/ui/theme/constants/theme.ts
+++ b/packages/twenty-front/src/modules/ui/theme/constants/theme.ts
@@ -41,6 +41,7 @@ const common = {
table: {
horizontalCellMargin: '8px',
checkboxColumnWidth: '32px',
+ horizontalCellPadding: '8px',
},
rightDrawerWidth: '500px',
clickableElementBackgroundTransition: 'background 0.1s ease',