5421 box shadow on frozen header and first column (#6130)

- Refactored components in table
- Added a isTableRecordScrolledLeftState and
isTableRecordScrolledTopState to subscribe to table scroll
- Added a zIndex logic that subscribes to those new states in new tinier
components

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau 2024-07-05 18:30:59 +02:00 committed by GitHub
parent cc6ce142ce
commit 7b3a590f79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 1531 additions and 1130 deletions

View File

@ -1,14 +1,7 @@
module.exports = { module.exports = {
root: true, root: true,
extends: ['plugin:prettier/recommended'], extends: ['plugin:prettier/recommended'],
plugins: [ plugins: ['@nx', 'prefer-arrow', 'import', 'unused-imports', 'unicorn'],
'@nx',
'prefer-arrow',
'import',
'simple-import-sort',
'unused-imports',
'unicorn',
],
rules: { rules: {
'func-style': ['error', 'declaration', { allowArrowFunctions: true }], 'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }], 'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
@ -53,26 +46,6 @@ module.exports = {
}, },
], ],
'simple-import-sort/imports': [
'error',
{
groups: [
// Packages
['^react', '^@?\\w'],
// Internal modules
['^(@|~|src|@ui)(/.*|$)'],
// Side effect imports
['^\\u0000'],
// Relative imports
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
// CSS imports
['^.+\\.?(css)$'],
],
},
],
'simple-import-sort/exports': 'error',
'unused-imports/no-unused-imports': 'warn', 'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [ 'unused-imports/no-unused-vars': [
'warn', 'warn',

View File

@ -5,21 +5,24 @@
"editor.formatOnSave": false, "editor.formatOnSave": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit",
"source.addMissingImports": "always" "source.addMissingImports": "always",
"source.organizeImports": "always"
} }
}, },
"[javascript]": { "[javascript]": {
"editor.formatOnSave": false, "editor.formatOnSave": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit",
"source.addMissingImports": "always" "source.addMissingImports": "always",
"source.organizeImports": "always"
} }
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.formatOnSave": false, "editor.formatOnSave": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit",
"source.addMissingImports": "always" "source.addMissingImports": "always",
"source.organizeImports": "always"
} }
}, },
"[json]": { "[json]": {

View File

@ -9,6 +9,7 @@ import { FieldContext } from '../contexts/FieldContext';
export const useIsFieldEmpty = () => { export const useIsFieldEmpty = () => {
const { entityId, fieldDefinition, overridenIsFieldEmpty } = const { entityId, fieldDefinition, overridenIsFieldEmpty } =
useContext(FieldContext); useContext(FieldContext);
const fieldValue = useRecordFieldValue( const fieldValue = useRecordFieldValue(
entityId, entityId,
fieldDefinition?.metadata?.fieldName ?? '', fieldDefinition?.metadata?.fieldName ?? '',

View File

@ -5,7 +5,7 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
import { useCombinedViewSorts } from '@/views/hooks/useCombinedViewSorts'; import { useCombinedViewSorts } from '@/views/hooks/useCombinedViewSorts';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
export const RemoveSortingModal = ({ export const RecordIndexRemoveSortingModal = ({
recordTableId, recordTableId,
}: { }: {
recordTableId: string; recordTableId: string;

View File

@ -1,8 +1,8 @@
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext'; import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar'; import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { RemoveSortingModal } from '@/object-record/record-table/components/RemoveSortingModal';
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu'; import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
type RecordIndexTableContainerProps = { type RecordIndexTableContainerProps = {
@ -39,7 +39,7 @@ export const RecordIndexTableContainer = ({
createRecord={createRecord} createRecord={createRecord}
/> />
<RecordTableActionBar recordTableId={recordTableId} /> <RecordTableActionBar recordTableId={recordTableId} />
<RemoveSortingModal recordTableId={recordTableId} /> <RecordIndexRemoveSortingModal recordTableId={recordTableId} />
<RecordTableContextMenu recordTableId={recordTableId} /> <RecordTableContextMenu recordTableId={recordTableId} />
</> </>
); );

View File

@ -1,8 +1,8 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui'; import { IconComponent } from 'twenty-ui';
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer';
const StyledInlineCellButtonContainer = styled.div` const StyledInlineCellButtonContainer = styled.div`
align-items: center; align-items: center;

View File

@ -1,29 +0,0 @@
import styled from '@emotion/styled';
import { IconListViewGrip } from '@/ui/input/components/IconListViewGrip';
const StyledContainer = styled.div`
cursor: grab;
width: 16px;
height: 32px;
z-index: 200;
display: flex;
&:hover .icon {
opacity: 1;
}
`;
const StyledIconWrapper = styled.div<{ isDragging: boolean }>`
opacity: ${({ isDragging }) => (isDragging ? 1 : 0)};
transition: opacity 0.1s;
`;
export const GripCell = ({ isDragging }: { isDragging: boolean }) => {
return (
<StyledContainer>
<StyledIconWrapper className="icon" isDragging={isDragging}>
<IconListViewGrip />
</StyledIconWrapper>
</StyledContainer>
);
};

View File

@ -1,154 +1,20 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { isNonEmptyString } from '@sniptt/guards';
import { MOBILE_VIEWPORT, RGBA } from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
import { RecordTableBody } from '@/object-record/record-table/components/RecordTableBody'; import { RecordTableBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEffect';
import { RecordTableBodyEffect } from '@/object-record/record-table/components/RecordTableBodyEffect'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableHeader } from '@/object-record/record-table/components/RecordTableHeader'; import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useCloseRecordTableCellV2 } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2';
import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
import {
OpenTableCellArgs,
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 { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope'; import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
const StyledTable = styled.table<{ const StyledTable = styled.table`
freezeFirstColumns?: boolean;
}>`
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
border-spacing: 0; border-spacing: 0;
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 {
border-block: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.tertiary};
padding: 0;
text-align: left;
:last-child {
border-right-color: transparent;
}
:first-of-type {
border-top-color: transparent;
border-bottom-color: transparent;
}
}
td {
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.primary};
border-right: 1px solid ${({ theme }) => theme.border.color.light};
padding: 0;
text-align: left;
:last-child {
border-right-color: transparent;
}
:first-of-type {
border-top-color: transparent;
border-bottom-color: transparent;
}
}
th {
background-color: ${({ theme }) => theme.background.primary};
border-right: 1px solid ${({ theme }) => theme.border.color.light};
}
thead th {
position: sticky;
top: 0;
z-index: 9;
}
thead th:nth-of-type(1),
thead th:nth-of-type(2),
thead th:nth-of-type(3) {
z-index: 12;
background-color: ${({ theme }) => theme.background.primary};
}
thead th:nth-of-type(1) {
width: 9px;
left: 0;
border-right-color: ${({ theme }) => theme.background.primary};
}
thead th:nth-of-type(2) {
left: 9px;
border-right-color: ${({ theme }) => theme.background.primary};
}
thead th:nth-of-type(3) {
left: 39px;
}
tbody td:nth-of-type(1),
tbody td:nth-of-type(2),
tbody td:nth-of-type(3) {
position: sticky;
z-index: 1;
}
tbody td:nth-of-type(1) {
left: 0;
z-index: 7;
}
tbody td:nth-of-type(2) {
left: 9px;
z-index: 5;
}
tbody td:nth-of-type(3) {
left: 39px;
z-index: 6;
}
thead th:nth-of-type(3),
tbody td:nth-of-type(3) {
${({ freezeFirstColumns }) =>
freezeFirstColumns &&
css`
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: 35px;
max-width: 35px;
}
`}
&::after {
content: '';
height: calc(100% + 1px);
position: absolute;
width: 4px;
right: -4px;
top: 0;
${({ freezeFirstColumns, theme }) =>
freezeFirstColumns &&
css`
box-shadow: 4px 0px 4px -4px ${theme.name === 'dark'
? RGBA(theme.grayScale.gray50, 0.8)
: RGBA(theme.grayScale.gray100, 0.25)} inset;
`}
}
}
`; `;
type RecordTableProps = { type RecordTableProps = {
@ -164,97 +30,27 @@ export const RecordTable = ({
onColumnsChange, onColumnsChange,
createRecord, createRecord,
}: RecordTableProps) => { }: RecordTableProps) => {
const { scopeId, visibleTableColumnsSelector } = const { scopeId } = useRecordTableStates(recordTableId);
useRecordTableStates(recordTableId);
const { objectMetadataItem } = useObjectMetadataItem({ if (!isNonEmptyString(objectNameSingular)) {
objectNameSingular, return <></>;
}); }
const { upsertRecord } = useUpsertRecordV2({
objectNameSingular,
});
const handleUpsertRecord = ({
persistField,
entityId,
fieldName,
}: {
persistField: () => void;
entityId: string;
fieldName: string;
}) => {
upsertRecord(persistField, entityId, fieldName, recordTableId);
};
const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
const handleOpenTableCell = (args: OpenTableCellArgs) => {
openTableCell(args);
};
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
const handleMoveFocus = (direction: MoveFocusDirection) => {
moveFocus(direction);
};
const { closeTableCell } = useCloseRecordTableCellV2(recordTableId);
const handleCloseTableCell = () => {
closeTableCell();
};
const { moveSoftFocusToCell } =
useMoveSoftFocusToCellOnHoverV2(recordTableId);
const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
moveSoftFocusToCell(cellPosition);
};
const { triggerContextMenu } = useTriggerContextMenu({
recordTableId,
});
const handleContextMenu = (event: React.MouseEvent, recordId: string) => {
triggerContextMenu(event, recordId);
};
const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
recordTableId,
});
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
return ( return (
<RecordTableScope <RecordTableScope
recordTableScopeId={scopeId} recordTableScopeId={scopeId}
onColumnsChange={onColumnsChange} onColumnsChange={onColumnsChange}
> >
{!!objectNameSingular && ( <RecordTableContextProvider
<RecordTableContext.Provider objectNameSingular={objectNameSingular}
value={{ recordTableId={recordTableId}
objectMetadataItem, >
onUpsertRecord: handleUpsertRecord, <StyledTable className="entity-table-cell">
onOpenTableCell: handleOpenTableCell, <RecordTableHeader createRecord={createRecord} />
onMoveFocus: handleMoveFocus, <RecordTableBodyEffect />
onCloseTableCell: handleCloseTableCell, <RecordTableBody />
onMoveSoftFocusToCell: handleMoveSoftFocusToCell, </StyledTable>
onContextMenu: handleContextMenu, </RecordTableContextProvider>
onCellMouseEnter: handleContainerMouseEnter,
visibleTableColumns,
}}
>
<StyledTable className="entity-table-cell">
<RecordTableHeader createRecord={createRecord} />
<RecordTableBodyEffect objectNameSingular={objectNameSingular} />
<RecordTableBody
objectNameSingular={objectNameSingular}
recordTableId={recordTableId}
/>
</StyledTable>
</RecordTableContext.Provider>
)}
</RecordTableScope> </RecordTableScope>
); );
}; };

View File

@ -1,53 +0,0 @@
import { useRecoilValue } from 'recoil';
import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/components/RecordTableBodyFetchMoreLoader';
import { RecordTableBodyLoading } from '@/object-record/record-table/components/RecordTableBodyLoading';
import { RecordTableRow } from '@/object-record/record-table/components/RecordTableRow';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { DraggableTableBody } from '@/ui/layout/draggable-list/components/DraggableTableBody';
type RecordTableBodyProps = {
objectNameSingular: string;
recordTableId: string;
};
export const RecordTableBody = ({
objectNameSingular,
recordTableId,
}: RecordTableBodyProps) => {
const { tableRowIdsState, isRecordTableInitialLoadingState } =
useRecordTableStates();
const tableRowIds = useRecoilValue(tableRowIdsState);
const isRecordTableInitialLoading = useRecoilValue(
isRecordTableInitialLoadingState,
);
if (isRecordTableInitialLoading && tableRowIds.length === 0) {
return <RecordTableBodyLoading />;
}
return (
<>
<DraggableTableBody
objectNameSingular={objectNameSingular}
recordTableId={recordTableId}
draggableItems={
<>
{tableRowIds.map((recordId, rowIndex) => {
return (
<RecordTableRow
key={recordId}
recordId={recordId}
rowIndex={rowIndex}
/>
);
})}
</>
}
/>
<RecordTableBodyFetchMoreLoader objectNameSingular={objectNameSingular} />
</>
);
};

View File

@ -1,61 +0,0 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
type RecordTableBodyEffectProps = {
objectNameSingular: string;
};
export const RecordTableBodyEffect = ({
objectNameSingular,
}: RecordTableBodyEffectProps) => {
const {
fetchMoreRecords: fetchMoreObjects,
records,
totalCount,
setRecordTableData,
loading,
queryStateIdentifier,
} = useLoadRecordIndexTable(objectNameSingular);
const isFetchingMoreObjects = useRecoilValue(
isFetchingMoreRecordsFamilyState(queryStateIdentifier),
);
const { tableLastRowVisibleState } = useRecordTableStates();
const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState);
const rowHeight = 32;
const viewportHeight = records.length * rowHeight;
useScrollRestoration(viewportHeight);
useEffect(() => {
if (!loading) {
setRecordTableData(records, totalCount);
}
}, [records, totalCount, setRecordTableData, loading]);
const fetchMoreDebouncedIfRequested = useDebouncedCallback(async () => {
// We are debouncing here to give the user some room to scroll if they want to within this throttle window
await fetchMoreObjects();
}, 100);
useEffect(() => {
if (!isFetchingMoreObjects && tableLastRowVisible) {
fetchMoreDebouncedIfRequested();
}
}, [
fetchMoreDebouncedIfRequested,
isFetchingMoreObjects,
tableLastRowVisible,
]);
return <></>;
};

View File

@ -0,0 +1,109 @@
import { ReactNode } from 'react';
import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useCloseRecordTableCellV2 } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2';
import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
import {
OpenTableCellArgs,
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 { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
export const RecordTableContextProvider = ({
recordTableId,
objectNameSingular,
children,
}: {
recordTableId: string;
objectNameSingular: string;
children: ReactNode;
}) => {
const { visibleTableColumnsSelector } = useRecordTableStates(recordTableId);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { upsertRecord } = useUpsertRecordV2({
objectNameSingular,
});
const handleUpsertRecord = ({
persistField,
entityId,
fieldName,
}: {
persistField: () => void;
entityId: string;
fieldName: string;
}) => {
upsertRecord(persistField, entityId, fieldName, recordTableId);
};
const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
const handleOpenTableCell = (args: OpenTableCellArgs) => {
openTableCell(args);
};
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
const handleMoveFocus = (direction: MoveFocusDirection) => {
moveFocus(direction);
};
const { closeTableCell } = useCloseRecordTableCellV2(recordTableId);
const handleCloseTableCell = () => {
closeTableCell();
};
const { moveSoftFocusToCell } =
useMoveSoftFocusToCellOnHoverV2(recordTableId);
const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
moveSoftFocusToCell(cellPosition);
};
const { triggerContextMenu } = useTriggerContextMenu({
recordTableId,
});
const handleContextMenu = (event: React.MouseEvent, recordId: string) => {
triggerContextMenu(event, recordId);
};
const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
recordTableId,
});
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
return (
<RecordTableContext.Provider
value={{
objectMetadataItem,
onUpsertRecord: handleUpsertRecord,
onOpenTableCell: handleOpenTableCell,
onMoveFocus: handleMoveFocus,
onCloseTableCell: handleCloseTableCell,
onMoveSoftFocusToCell: handleMoveSoftFocusToCell,
onContextMenu: handleContextMenu,
onCellMouseEnter: handleContainerMouseEnter,
visibleTableColumns,
recordTableId,
objectNameSingular,
}}
>
{children}
</RecordTableContext.Provider>
);
};

View File

@ -1,114 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconPlus } from 'twenty-ui';
import { RecordTableHeaderCell } from '@/object-record/record-table/components/RecordTableHeaderCell';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
import { RecordTableHeaderPlusButtonContent } from './RecordTableHeaderPlusButtonContent';
import { SelectAllCheckbox } from './SelectAllCheckbox';
const StyledTableHead = styled.thead`
cursor: pointer;
`;
const StyledPlusIconHeaderCell = styled.th<{ isTableWiderThanScreen: boolean }>`
${({ theme }) => {
return `
&:hover {
background: ${theme.background.transparent.light};
};
padding-left: ${theme.spacing(3)};
`;
}};
border-left: none !important;
min-width: 32px;
${({ isTableWiderThanScreen, theme }) =>
isTableWiderThanScreen &&
`
width: 32px;
border-right: none !important;
background-color: ${theme.background.primary};
`};
z-index: 1;
`;
const StyledPlusIconContainer = styled.div`
align-items: center;
display: flex;
height: 32px;
justify-content: center;
width: 32px;
`;
export const HIDDEN_TABLE_COLUMN_DROPDOWN_ID =
'hidden-table-columns-dropdown-scope-id';
const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
'hidden-table-columns-dropdown-hotkey-scope-id';
export const RecordTableHeader = ({
createRecord,
}: {
createRecord: () => void;
}) => {
const { visibleTableColumnsSelector, hiddenTableColumnsSelector } =
useRecordTableStates();
const scrollWrapper = useScrollWrapperScopedRef();
const isTableWiderThanScreen =
(scrollWrapper.current?.clientWidth ?? 0) <
(scrollWrapper.current?.scrollWidth ?? 0);
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
const hiddenTableColumns = useRecoilValue(hiddenTableColumnsSelector());
const theme = useTheme();
return (
<StyledTableHead data-select-disable>
<tr>
<th></th>
<th
style={{
width: 30,
minWidth: 30,
maxWidth: 30,
borderRight: 'transparent',
}}
>
<SelectAllCheckbox />
</th>
{visibleTableColumns.map((column) => (
<RecordTableHeaderCell
key={column.fieldMetadataId}
column={column}
createRecord={createRecord}
/>
))}
<StyledPlusIconHeaderCell
isTableWiderThanScreen={isTableWiderThanScreen}
>
{hiddenTableColumns.length > 0 && (
<Dropdown
dropdownId={HIDDEN_TABLE_COLUMN_DROPDOWN_ID}
clickableComponent={
<StyledPlusIconContainer>
<IconPlus size={theme.icon.size.md} />
</StyledPlusIconContainer>
}
dropdownComponents={<RecordTableHeaderPlusButtonContent />}
dropdownPlacement="bottom-start"
dropdownHotkeyScope={{
scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID,
}}
/>
)}
</StyledPlusIconHeaderCell>
</tr>
</StyledTableHead>
);
};

View File

@ -1,183 +0,0 @@
import { useContext } from 'react';
import { useInView } from 'react-intersection-observer';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Draggable } from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/components/RecordTableCellFieldContextWrapper';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { CheckboxCell } from './CheckboxCell';
import { GripCell } from './GripCell';
type RecordTableRowProps = {
recordId: string;
rowIndex: number;
isPendingRow?: boolean;
};
export const StyledTd = styled.td<{ isSelected?: boolean }>`
background: ${({ theme }) => theme.background.primary};
position: relative;
user-select: none;
${({ isSelected, theme }) =>
isSelected &&
`
background: ${theme.accent.quaternary};
`}
`;
export const StyledTr = styled.tr<{ isDragging: boolean }>`
border: 1px solid transparent;
transition: border-left-color 0.2s ease-in-out;
td:nth-of-type(-n + 2) {
border-right-color: ${({ theme }) => theme.background.primary};
}
${({ isDragging }) =>
isDragging &&
`
td:nth-of-type(1) {
background-color: transparent;
border-color: transparent;
}
td:nth-of-type(2) {
background-color: transparent;
border-color: transparent;
}
td:nth-of-type(3) {
background-color: transparent;
border-color: transparent;
}
`}
`;
const SelectableStyledTd = ({
isSelected,
children,
style,
}: {
isSelected: boolean;
children?: React.ReactNode;
style?: React.CSSProperties;
}) => (
<StyledTd isSelected={isSelected} style={style}>
{children}
</StyledTd>
);
export const RecordTableRow = ({
recordId,
rowIndex,
isPendingRow,
}: RecordTableRowProps) => {
const { visibleTableColumnsSelector, isRowSelectedFamilyState } =
useRecordTableStates();
const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId));
const { objectMetadataItem } = useContext(RecordTableContext);
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
const scrollWrapperRef = useContext(ScrollWrapperContext);
const { ref: elementRef, inView } = useInView({
root: scrollWrapperRef.current?.querySelector(
'[data-overlayscrollbars-viewport="scrollbarHidden"]',
),
rootMargin: '1000px',
});
const theme = useTheme();
return (
<RecordTableRowContext.Provider
value={{
recordId,
rowIndex,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular: objectMetadataItem.nameSingular,
}) + recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isSelected: currentRowSelected,
isReadOnly: objectMetadataItem.isRemote ?? false,
isPendingRow,
}}
>
<RecordValueSetterEffect recordId={recordId} />
<Draggable key={recordId} draggableId={recordId} index={rowIndex}>
{(draggableProvided, draggableSnapshot) => (
<StyledTr
ref={(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}
>
<StyledTd
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.dragHandleProps}
data-select-disable
>
<GripCell isDragging={draggableSnapshot.isDragging} />
</StyledTd>
<SelectableStyledTd
isSelected={currentRowSelected}
style={{ borderRight: 'transparent' }}
>
{!draggableSnapshot.isDragging && <CheckboxCell />}
</SelectableStyledTd>
{inView || draggableSnapshot.isDragging
? visibleTableColumns.map((column, columnIndex) => (
<RecordTableCellContext.Provider
value={{
columnDefinition: column,
columnIndex,
}}
key={column.fieldMetadataId}
>
{draggableSnapshot.isDragging && columnIndex > 0 ? null : (
<RecordTableCellFieldContextWrapper />
)}
</RecordTableCellContext.Provider>
))
: visibleTableColumns.map((column) => (
<StyledTd
isSelected={currentRowSelected}
key={column.fieldMetadataId}
></StyledTd>
))}
<SelectableStyledTd isSelected={currentRowSelected} />
</StyledTr>
)}
</Draggable>
</RecordTableRowContext.Provider>
);
};

View File

@ -0,0 +1,16 @@
import { useRecoilValue } from 'recoil';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
export const RecordTableRows = () => {
const { tableRowIdsState } = useRecordTableStates();
const tableRowIds = useRecoilValue(tableRowIdsState);
return tableRowIds.map((recordId, rowIndex) => {
return (
<RecordTableRow key={recordId} recordId={recordId} rowIndex={rowIndex} />
);
});
};

View File

@ -75,6 +75,28 @@ export const RecordTableWithWrappers = ({
const isRemote = foundObjectMetadataItem?.isRemote ?? false; const isRemote = foundObjectMetadataItem?.isRemote ?? false;
const handleColumnsChange = useRecoilCallback(
() => (columns) => {
saveViewFields(
mapColumnDefinitionsToViewFields(
columns as ColumnDefinition<FieldMetadata>[],
),
);
},
[saveViewFields],
);
if (!isRecordTableInitialLoading && tableRowIds.length === 0) {
return (
<RecordTableEmptyState
objectNameSingular={objectNameSingular}
objectLabel={objectLabel}
createRecord={createRecord}
isRemote={isRemote}
/>
);
}
return ( return (
<EntityDeleteContext.Provider value={deleteOneRecord}> <EntityDeleteContext.Provider value={deleteOneRecord}>
<ScrollWrapper> <ScrollWrapper>
@ -85,16 +107,7 @@ export const RecordTableWithWrappers = ({
<RecordTable <RecordTable
recordTableId={recordTableId} recordTableId={recordTableId}
objectNameSingular={objectNameSingular} objectNameSingular={objectNameSingular}
onColumnsChange={useRecoilCallback( onColumnsChange={handleColumnsChange}
() => (columns) => {
saveViewFields(
mapColumnDefinitionsToViewFields(
columns as ColumnDefinition<FieldMetadata>[],
),
);
},
[saveViewFields],
)}
createRecord={createRecord} createRecord={createRecord}
/> />
<DragSelect <DragSelect
@ -107,16 +120,6 @@ export const RecordTableWithWrappers = ({
recordTableId={recordTableId} recordTableId={recordTableId}
tableBodyRef={tableBodyRef} tableBodyRef={tableBodyRef}
/> />
{!isRecordTableInitialLoading &&
// we cannot rely on count states because this is not available for remote objects
tableRowIds.length === 0 && (
<RecordTableEmptyState
objectNameSingular={objectNameSingular}
objectLabel={objectLabel}
createRecord={createRecord}
isRemote={isRemote}
/>
)}
</StyledTableContainer> </StyledTableContainer>
</StyledTableWithHeader> </StyledTableWithHeader>
</RecordUpdateContext.Provider> </RecordUpdateContext.Provider>

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { useEffect } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { ComponentDecorator } from 'twenty-ui'; import { ComponentDecorator } from 'twenty-ui';
@ -12,7 +12,6 @@ import {
useSetRecordValue, useSetRecordValue,
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/components/RecordTableCellFieldContextWrapper';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
@ -21,6 +20,7 @@ import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDeco
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
import { mockPerformance } from './mock'; import { mockPerformance } from './mock';
const objectMetadataItems = getObjectMetadataItemsMock(); const objectMetadataItems = getObjectMetadataItemsMock();
@ -73,6 +73,9 @@ const meta: Meta = {
onContextMenu: () => {}, onContextMenu: () => {},
onCellMouseEnter: () => {}, onCellMouseEnter: () => {},
visibleTableColumns: mockPerformance.visibleTableColumns as any, visibleTableColumns: mockPerformance.visibleTableColumns as any,
objectNameSingular:
mockPerformance.objectMetadataItem.nameSingular,
recordTableId: 'recordTableId',
}} }}
> >
<RecordTableScope <RecordTableScope
@ -92,12 +95,19 @@ const meta: Meta = {
}) + mockPerformance.entityId, }) + mockPerformance.entityId,
isSelected: false, isSelected: false,
isReadOnly: false, isReadOnly: false,
isDragging: false,
dragHandleProps: null,
inView: true,
isPendingRow: false,
}} }}
> >
<RecordTableCellContext.Provider <RecordTableCellContext.Provider
value={{ value={{
columnDefinition: mockPerformance.fieldDefinition, columnDefinition: mockPerformance.fieldDefinition,
columnIndex: 0, columnIndex: 0,
cellPosition: { row: 0, column: 0 },
hasSoftFocus: false,
isInEditMode: false,
}} }}
> >
<FieldContext.Provider <FieldContext.Provider

View File

@ -0,0 +1,2 @@
export const HIDDEN_TABLE_COLUMN_DROPDOWN_ID =
'hidden-table-columns-dropdown-scope-id';

View File

@ -2,12 +2,15 @@ import { createContext } from 'react';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
type RecordTableRowContextProps = { export type RecordTableCellContextProps = {
columnDefinition: ColumnDefinition<FieldMetadata>; columnDefinition: ColumnDefinition<FieldMetadata>;
columnIndex: number; columnIndex: number;
isInEditMode: boolean;
hasSoftFocus: boolean;
cellPosition: TableCellPosition;
}; };
export const RecordTableCellContext = createContext<RecordTableRowContextProps>( export const RecordTableCellContext =
{} as RecordTableRowContextProps, createContext<RecordTableCellContextProps>({} as RecordTableCellContextProps);
);

View File

@ -26,6 +26,8 @@ export type RecordTableContextProps = {
onContextMenu: (event: React.MouseEvent, recordId: string) => void; onContextMenu: (event: React.MouseEvent, recordId: string) => void;
onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void; onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
visibleTableColumns: ColumnDefinition<FieldMetadata>[]; visibleTableColumns: ColumnDefinition<FieldMetadata>[];
recordTableId: string;
objectNameSingular: string;
}; };
export const RecordTableContext = createContext<RecordTableContextProps>( export const RecordTableContext = createContext<RecordTableContextProps>(

View File

@ -1,4 +1,5 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
export type RecordTableRowContextProps = { export type RecordTableRowContextProps = {
pathToShowPage: string; pathToShowPage: string;
@ -8,6 +9,9 @@ export type RecordTableRowContextProps = {
isSelected: boolean; isSelected: boolean;
isReadOnly: boolean; isReadOnly: boolean;
isPendingRow?: boolean; isPendingRow?: boolean;
isDragging: boolean;
dragHandleProps: DraggableProvidedDragHandleProps | null;
inView?: boolean;
}; };
export const RecordTableRowContext = createContext<RecordTableRowContextProps>( export const RecordTableRowContext = createContext<RecordTableRowContextProps>(

View File

@ -0,0 +1,38 @@
import { useRecoilValue } from 'recoil';
import { RecordTableRows } from '@/object-record/record-table/components/RecordTableRows';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableBodyDragDropContext } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext';
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader';
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow';
import { useContext } from 'react';
export const RecordTableBody = () => {
const { tableRowIdsState, isRecordTableInitialLoadingState } =
useRecordTableStates();
const { objectNameSingular } = useContext(RecordTableContext);
const tableRowIds = useRecoilValue(tableRowIdsState);
const isRecordTableInitialLoading = useRecoilValue(
isRecordTableInitialLoadingState,
);
if (isRecordTableInitialLoading && tableRowIds.length === 0) {
return <RecordTableBodyLoading />;
}
return (
<RecordTableBodyDragDropContext>
<RecordTableBodyDroppable>
<RecordTablePendingRow />
<RecordTableRows />
</RecordTableBodyDroppable>
<RecordTableBodyFetchMoreLoader objectNameSingular={objectNameSingular} />
</RecordTableBodyDragDropContext>
);
};

View File

@ -1,42 +1,30 @@
import { useState } from 'react'; import { ReactNode, useContext } from 'react';
import styled from '@emotion/styled'; import { DragDropContext, DropResult } from '@hello-pangea/dnd';
import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordTablePendingRow } from '@/object-record/record-table/components/RecordTablePendingRow'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition'; import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
type DraggableTableBodyProps = { export const RecordTableBodyDragDropContext = ({
draggableItems: React.ReactNode; children,
objectNameSingular: string; }: {
recordTableId: string; children: ReactNode;
}; }) => {
const { objectNameSingular, recordTableId } = useContext(RecordTableContext);
const StyledTbody = styled.tbody`
overflow: hidden;
`;
export const DraggableTableBody = ({
objectNameSingular,
draggableItems,
recordTableId,
}: DraggableTableBodyProps) => {
const [v4Persistable] = useState(v4());
const { tableRowIdsState } = useRecordTableStates();
const tableRowIds = useRecoilValue(tableRowIdsState);
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({ const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
objectNameSingular, objectNameSingular,
}); });
const { tableRowIdsState } = useRecordTableStates();
const tableRowIds = useRecoilValue(tableRowIdsState);
const { currentViewWithCombinedFiltersAndSorts } = const { currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView(recordTableId); useGetCurrentView(recordTableId);
@ -45,6 +33,7 @@ export const DraggableTableBody = ({
const setIsRemoveSortingModalOpenState = useSetRecoilState( const setIsRemoveSortingModalOpenState = useSetRecoilState(
isRemoveSortingModalOpenState, isRemoveSortingModalOpenState,
); );
const computeNewRowPosition = useComputeNewRowPosition(); const computeNewRowPosition = useComputeNewRowPosition();
const handleDragEnd = (result: DropResult) => { const handleDragEnd = (result: DropResult) => {
@ -68,20 +57,6 @@ export const DraggableTableBody = ({
}; };
return ( return (
<DragDropContext onDragEnd={handleDragEnd}> <DragDropContext onDragEnd={handleDragEnd}>{children}</DragDropContext>
<Droppable droppableId={v4Persistable}>
{(provided) => (
<StyledTbody
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
<RecordTablePendingRow />
{draggableItems}
{provided.placeholder}
</StyledTbody>
)}
</Droppable>
</DragDropContext>
); );
}; };

View File

@ -0,0 +1,57 @@
import { Theme } from '@emotion/react';
import { Droppable } from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
import { ReactNode, useContext, useState } from 'react';
import { ThemeContext } from 'twenty-ui';
import { v4 } from 'uuid';
const StyledTbody = styled.tbody<{
theme: Theme;
}>`
overflow: hidden;
&.first-columns-sticky {
td:nth-child(1) {
position: sticky;
left: 0;
z-index: 5;
}
td:nth-child(2) {
position: sticky;
left: 9px;
z-index: 5;
}
td:nth-child(3) {
position: sticky;
left: 39px;
z-index: 5;
}
}
`;
export const RecordTableBodyDroppable = ({
children,
}: {
children: ReactNode;
}) => {
const [v4Persistable] = useState(v4());
const { theme } = useContext(ThemeContext);
return (
<Droppable droppableId={v4Persistable}>
{(provided) => (
<StyledTbody
id="record-table-body"
theme={theme}
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{children}
{provided.placeholder}
</StyledTbody>
)}
</Droppable>
);
};

View File

@ -0,0 +1,106 @@
import { useContext, useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
import { isRecordTableScrolledTopComponentState } from '@/object-record/record-table/states/isRecordTableScrolledTopComponentState';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
export const RecordTableBodyEffect = () => {
const { objectNameSingular } = useContext(RecordTableContext);
const {
fetchMoreRecords: fetchMoreObjects,
records,
totalCount,
setRecordTableData,
loading,
queryStateIdentifier,
} = useLoadRecordIndexTable(objectNameSingular);
const isFetchingMoreObjects = useRecoilValue(
isFetchingMoreRecordsFamilyState(queryStateIdentifier),
);
const { tableLastRowVisibleState } = useRecordTableStates();
const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState);
const scrollTop = useRecoilValue(scrollTopState);
const setIsRecordTableScrolledTop = useSetRecoilComponentState(
isRecordTableScrolledTopComponentState,
);
useEffect(() => {
setIsRecordTableScrolledTop(scrollTop === 0);
if (scrollTop > 0) {
document
.getElementById('record-table-header')
?.classList.add('header-sticky');
} else {
document
.getElementById('record-table-header')
?.classList.remove('header-sticky');
}
}, [scrollTop, setIsRecordTableScrolledTop]);
const scrollLeft = useRecoilValue(scrollLeftState);
const setIsRecordTableScrolledLeft = useSetRecoilComponentState(
isRecordTableScrolledLeftComponentState,
);
useEffect(() => {
setIsRecordTableScrolledLeft(scrollLeft === 0);
if (scrollLeft > 0) {
document
.getElementById('record-table-body')
?.classList.add('first-columns-sticky');
document
.getElementById('record-table-header')
?.classList.add('first-columns-sticky');
} else {
document
.getElementById('record-table-body')
?.classList.remove('first-columns-sticky');
document
.getElementById('record-table-header')
?.classList.remove('first-columns-sticky');
}
}, [scrollLeft, setIsRecordTableScrolledLeft]);
const rowHeight = 32;
const viewportHeight = records.length * rowHeight;
useScrollRestoration(viewportHeight);
useEffect(() => {
if (!loading) {
setRecordTableData(records, totalCount);
}
}, [records, totalCount, setRecordTableData, loading]);
const fetchMoreDebouncedIfRequested = useDebouncedCallback(async () => {
// We are debouncing here to give the user some room to scroll if they want to within this throttle window
await fetchMoreObjects();
}, 100);
useEffect(() => {
if (!isFetchingMoreObjects && tableLastRowVisible) {
fetchMoreDebouncedIfRequested();
}
}, [
fetchMoreDebouncedIfRequested,
isFetchingMoreObjects,
tableLastRowVisible,
]);
return <></>;
};

View File

@ -1,13 +1,10 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { CheckboxCell } from '@/object-record/record-table/components/CheckboxCell';
import { GripCell } from '@/object-record/record-table/components/GripCell';
import {
StyledTd,
StyledTr,
} from '@/object-record/record-table/components/RecordTableRow';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
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'; import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading';
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
export const RecordTableBodyLoading = () => { export const RecordTableBodyLoading = () => {
const { visibleTableColumnsSelector } = useRecordTableStates(); const { visibleTableColumnsSelector } = useRecordTableStates();
@ -16,22 +13,18 @@ export const RecordTableBodyLoading = () => {
return ( return (
<tbody> <tbody>
{Array.from({ length: 8 }).map((_, rowIndex) => ( {Array.from({ length: 8 }).map((_, rowIndex) => (
<StyledTr <RecordTableTr
isDragging={false} isDragging={false}
data-testid={`row-id-${rowIndex}`} data-testid={`row-id-${rowIndex}`}
data-selectable-id={`row-id-${rowIndex}`} data-selectable-id={`row-id-${rowIndex}`}
key={rowIndex} key={rowIndex}
> >
<StyledTd data-select-disable> <RecordTableCellGrip />
<GripCell isDragging={false} /> <RecordTableCellCheckbox />
</StyledTd>
<StyledTd>
<CheckboxCell />
</StyledTd>
{visibleTableColumns.map((column) => ( {visibleTableColumns.map((column) => (
<RecordTableCellLoading key={column.fieldMetadataId} /> <RecordTableCellLoading key={column.fieldMetadataId} />
))} ))}
</StyledTr> </RecordTableTr>
))} ))}
</tbody> </tbody>
); );

View File

@ -1,109 +1,13 @@
import { useContext } from 'react';
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay'; import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider'; import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCellContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellContainer'; import { RecordTableCellContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellContainer';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput';
export const RecordTableCell = ({
customHotkeyScope,
}: {
customHotkeyScope: HotkeyScope;
}) => {
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
useContext(RecordTableContext);
const { entityId, fieldDefinition } = useContext(FieldContext);
const { isReadOnly } = useContext(RecordTableRowContext);
const handleEnter: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
onMoveFocus('down');
};
const handleSubmit: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
};
const handleCancel = () => {
onCloseTableCell();
};
const handleClickOutside: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
};
const handleEscape: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
};
const handleTab: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
onMoveFocus('right');
};
const handleShiftTab: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
onMoveFocus('left');
};
export const RecordTableCell = () => {
return ( return (
<FieldFocusContextProvider> <FieldFocusContextProvider>
<RecordTableCellContainer <RecordTableCellContainer
editHotkeyScope={customHotkeyScope} editModeContent={<RecordTableCellFieldInput />}
editModeContent={
<FieldInput
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
onCancel={handleCancel}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onSubmit={handleSubmit}
onTab={handleTab}
isReadOnly={isReadOnly}
/>
}
nonEditModeContent={<FieldDisplay />} nonEditModeContent={<FieldDisplay />}
/> />
</FieldFocusContextProvider> </FieldFocusContextProvider>

View File

@ -0,0 +1,101 @@
import { ReactNode, useContext } from 'react';
import { styled } from '@linaria/react';
import { BORDER_COMMON, ThemeContext } from 'twenty-ui';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import {
DEFAULT_CELL_SCOPE,
useOpenRecordTableCellFromCell,
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
const StyledBaseContainer = styled.div<{
hasSoftFocus: boolean;
fontColorExtraLight: string;
backgroundColorTransparentSecondary: string;
}>`
align-items: center;
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 32px;
position: relative;
user-select: none;
background: ${({ hasSoftFocus, backgroundColorTransparentSecondary }) =>
hasSoftFocus ? backgroundColorTransparentSecondary : 'none'};
border-radius: ${({ hasSoftFocus }) =>
hasSoftFocus ? BORDER_COMMON.radius.sm : 'none'};
outline: ${({ hasSoftFocus, fontColorExtraLight }) =>
hasSoftFocus ? `1px solid ${fontColorExtraLight}` : 'none'};
`;
export const RecordTableCellBaseContainer = ({
children,
}: {
children: ReactNode;
}) => {
const { setIsFocused } = useFieldFocus();
const { openTableCell } = useOpenRecordTableCellFromCell();
const { theme } = useContext(ThemeContext);
const { recordId } = useContext(RecordTableRowContext);
const { hasSoftFocus, cellPosition } = useContext(RecordTableCellContext);
const { onMoveSoftFocusToCell, onCellMouseEnter } =
useContext(RecordTableContext);
const handleContainerMouseMove = () => {
setIsFocused(true);
if (!hasSoftFocus) {
onCellMouseEnter({
cellPosition,
});
}
};
const handleContainerMouseLeave = () => {
setIsFocused(false);
};
const handleContainerClick = () => {
if (!hasSoftFocus) {
onMoveSoftFocusToCell(cellPosition);
openTableCell();
}
};
const { onContextMenu } = useContext(RecordTableContext);
const handleContextMenu = (event: React.MouseEvent) => {
onContextMenu(event, recordId);
};
const { hotkeyScope } = useContext(FieldContext);
const editHotkeyScope = { scope: hotkeyScope } ?? DEFAULT_CELL_SCOPE;
return (
<CellHotkeyScopeContext.Provider value={editHotkeyScope}>
<StyledBaseContainer
onMouseLeave={handleContainerMouseLeave}
onMouseMove={handleContainerMouseMove}
onClick={handleContainerClick}
onContextMenu={handleContextMenu}
backgroundColorTransparentSecondary={
theme.background.transparent.secondary
}
fontColorExtraLight={theme.font.color.extraLight}
hasSoftFocus={hasSoftFocus}
>
{children}
</StyledBaseContainer>
</CellHotkeyScopeContext.Provider>
);
};

View File

@ -1,8 +1,8 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui'; import { IconComponent } from 'twenty-ui';
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer';
const StyledButtonContainer = styled.div` const StyledButtonContainer = styled.div`
margin: ${({ theme }) => theme.spacing(1)}; margin: ${({ theme }) => theme.spacing(1)};

View File

@ -1,9 +1,10 @@
import { useCallback, useContext } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback, useContext } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
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 { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { Checkbox } from '@/ui/input/components/Checkbox'; import { Checkbox } from '@/ui/input/components/Checkbox';
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState'; import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
@ -18,7 +19,9 @@ const StyledContainer = styled.div`
justify-content: center; justify-content: center;
`; `;
export const CheckboxCell = () => { export const RecordTableCellCheckbox = () => {
const { isSelected } = useContext(RecordTableRowContext);
const { recordId } = useContext(RecordTableRowContext); const { recordId } = useContext(RecordTableRowContext);
const { isRowSelectedFamilyState } = useRecordTableStates(); const { isRowSelectedFamilyState } = useRecordTableStates();
const setActionBarOpenState = useSetRecoilState(actionBarOpenState); const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
@ -31,8 +34,10 @@ export const CheckboxCell = () => {
}, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]); }, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]);
return ( return (
<StyledContainer onClick={handleClick}> <RecordTableTd isSelected={isSelected} hasRightBorder={false}>
<Checkbox checked={currentRowSelected} /> <StyledContainer onClick={handleClick}>
</StyledContainer> <Checkbox checked={currentRowSelected} />
</StyledContainer>
</RecordTableTd>
); );
}; };

View File

@ -1,176 +1,41 @@
import React, { ReactElement, useContext } from 'react'; import { ReactElement, useContext } from 'react';
import { styled } from '@linaria/react';
import { useRecoilValue } from 'recoil';
import { BORDER_COMMON, ThemeContext } from 'twenty-ui';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableCellBaseContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCellSoftFocusMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode'; import { RecordTableCellSoftFocusMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode';
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode'; import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode';
import { RecordTableCellEditMode } from './RecordTableCellEditMode'; import { RecordTableCellEditMode } from './RecordTableCellEditMode';
const StyledTd = styled.td<{
isInEditMode: boolean;
backgroundColor: string;
}>`
background: ${({ backgroundColor }) => backgroundColor};
z-index: ${({ isInEditMode }) => (isInEditMode ? '4 !important' : 3)};
`;
const borderRadiusSm = BORDER_COMMON.radius.sm;
const StyledBaseContainer = styled.div<{
hasSoftFocus: boolean;
fontColorExtraLight: string;
backgroundColorTransparentSecondary: string;
}>`
align-items: center;
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 32px;
position: relative;
user-select: none;
background: ${({ hasSoftFocus, backgroundColorTransparentSecondary }) =>
hasSoftFocus ? backgroundColorTransparentSecondary : 'none'};
border-radius: ${({ hasSoftFocus }) =>
hasSoftFocus ? borderRadiusSm : 'none'};
border: ${({ hasSoftFocus, fontColorExtraLight }) =>
hasSoftFocus ? `1px solid ${fontColorExtraLight}` : 'none'};
`;
export type RecordTableCellContainerProps = { export type RecordTableCellContainerProps = {
editModeContent: ReactElement; editModeContent: ReactElement;
nonEditModeContent: ReactElement; nonEditModeContent: ReactElement;
editHotkeyScope?: HotkeyScope;
transparent?: boolean; transparent?: boolean;
maxContentWidth?: number; maxContentWidth?: number;
onSubmit?: () => void; onSubmit?: () => void;
onCancel?: () => void; onCancel?: () => void;
}; };
const DEFAULT_CELL_SCOPE: HotkeyScope = {
scope: TableHotkeyScope.CellEditMode,
};
export const RecordTableCellContainer = ({ export const RecordTableCellContainer = ({
editModeContent, editModeContent,
nonEditModeContent, nonEditModeContent,
editHotkeyScope,
}: RecordTableCellContainerProps) => { }: RecordTableCellContainerProps) => {
const { theme } = useContext(ThemeContext); const { hasSoftFocus, isInEditMode } = useContext(RecordTableCellContext);
const { setIsFocused } = useFieldFocus();
const { openTableCell } = useOpenRecordTableCellFromCell();
const { isSelected, recordId } = useContext(RecordTableRowContext);
const { onMoveSoftFocusToCell, onContextMenu, onCellMouseEnter } =
useContext(RecordTableContext);
const tableScopeId = useAvailableScopeIdOrThrow(
RecordTableScopeInternalContext,
getScopeIdOrUndefinedFromComponentId(),
);
const isTableCellInEditModeFamilyState = extractComponentFamilyState(
isTableCellInEditModeComponentFamilyState,
tableScopeId,
);
const isSoftFocusOnTableCellFamilyState = extractComponentFamilyState(
isSoftFocusOnTableCellComponentFamilyState,
tableScopeId,
);
const cellPosition = useCurrentTableCellPosition();
const isInEditMode = useRecoilValue(
isTableCellInEditModeFamilyState(cellPosition),
);
const hasSoftFocus = useRecoilValue(
isSoftFocusOnTableCellFamilyState(cellPosition),
);
const handleContextMenu = (event: React.MouseEvent) => {
onContextMenu(event, recordId);
};
const handleContainerMouseMove = () => {
setIsFocused(true);
if (!hasSoftFocus) {
onCellMouseEnter({
cellPosition,
});
}
};
const handleContainerMouseLeave = () => {
setIsFocused(false);
};
const handleContainerClick = () => {
if (!hasSoftFocus) {
onMoveSoftFocusToCell(cellPosition);
openTableCell();
}
};
const tdBackgroundColor = isSelected
? theme.accent.quaternary
: theme.background.primary;
return ( return (
<StyledTd <RecordTableCellBaseContainer>
backgroundColor={tdBackgroundColor} {isInEditMode ? (
isInEditMode={isInEditMode} <RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
onContextMenu={handleContextMenu} ) : hasSoftFocus ? (
> <RecordTableCellSoftFocusMode
<CellHotkeyScopeContext.Provider editModeContent={editModeContent}
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE} nonEditModeContent={nonEditModeContent}
> />
<StyledBaseContainer ) : (
onMouseLeave={handleContainerMouseLeave} <RecordTableCellDisplayMode>
onMouseMove={handleContainerMouseMove} {nonEditModeContent}
onClick={handleContainerClick} </RecordTableCellDisplayMode>
backgroundColorTransparentSecondary={ )}
theme.background.transparent.secondary </RecordTableCellBaseContainer>
}
fontColorExtraLight={theme.font.color.extraLight}
hasSoftFocus={hasSoftFocus}
>
{isInEditMode ? (
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
) : hasSoftFocus ? (
<RecordTableCellSoftFocusMode
editModeContent={editModeContent}
nonEditModeContent={nonEditModeContent}
/>
) : (
<RecordTableCellDisplayMode>
{nonEditModeContent}
</RecordTableCellDisplayMode>
)}
</StyledBaseContainer>
</CellHotkeyScopeContext.Provider>
</StyledTd>
); );
}; };

View File

@ -10,8 +10,6 @@ const StyledOuterContainer = styled.div<{
overflow: hidden; overflow: hidden;
padding-left: 6px; padding-left: 6px;
width: 100%; width: 100%;
margin: ${({ hasSoftFocus }) => (hasSoftFocus === true ? '-1px' : 'none')};
`; `;
const StyledInnerContainer = styled.div` const StyledInnerContainer = styled.div`

View File

@ -1,4 +1,4 @@
import { useContext } from 'react'; import { ReactNode, useContext } from 'react';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -8,13 +8,16 @@ import { RecordUpdateContext } from '@/object-record/record-table/contexts/Entit
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope'; import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const RecordTableCellFieldContextWrapper = () => { export const RecordTableCellFieldContextWrapper = ({
children,
}: {
children: ReactNode;
}) => {
const { objectMetadataItem } = useContext(RecordTableContext); const { objectMetadataItem } = useContext(RecordTableContext);
const { columnDefinition } = useContext(RecordTableCellContext); const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId, pathToShowPage } = useContext(RecordTableRowContext); const { recordId, pathToShowPage } = useContext(RecordTableRowContext);
@ -49,7 +52,7 @@ export const RecordTableCellFieldContextWrapper = () => {
}), }),
}} }}
> >
<RecordTableCell customHotkeyScope={{ scope: customHotkeyScope }} /> {children}
</FieldContext.Provider> </FieldContext.Provider>
); );
}; };

View File

@ -0,0 +1,95 @@
import { useContext } from 'react';
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
export const RecordTableCellFieldInput = () => {
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
useContext(RecordTableContext);
const { entityId, fieldDefinition } = useContext(FieldContext);
const { isReadOnly } = useContext(RecordTableRowContext);
const handleEnter: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
onMoveFocus('down');
};
const handleSubmit: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
};
const handleCancel = () => {
onCloseTableCell();
};
const handleClickOutside: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
};
const handleEscape: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
};
const handleTab: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
onMoveFocus('right');
};
const handleShiftTab: FieldInputEvent = (persistField) => {
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
onMoveFocus('left');
};
return (
<FieldInput
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
onCancel={handleCancel}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onSubmit={handleSubmit}
onTab={handleTab}
isReadOnly={isReadOnly}
/>
);
};

View File

@ -0,0 +1,44 @@
import styled from '@emotion/styled';
import { useContext } from 'react';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { IconListViewGrip } from '@/ui/input/components/IconListViewGrip';
const StyledContainer = styled.div`
cursor: grab;
width: 16px;
height: 32px;
z-index: 200;
display: flex;
&:hover .icon {
opacity: 1;
}
border-color: transparent;
`;
const StyledIconWrapper = styled.div<{ isDragging: boolean }>`
opacity: ${({ isDragging }) => (isDragging ? 1 : 0)};
transition: opacity 0.1s;
`;
export const RecordTableCellGrip = () => {
const { dragHandleProps, isDragging } = useContext(RecordTableRowContext);
return (
<RecordTableTd
// eslint-disable-next-line react/jsx-props-no-spreading
{...dragHandleProps}
data-select-disable
hasRightBorder={false}
hasBottomBorder={false}
>
<StyledContainer>
<StyledIconWrapper className="icon" isDragging={isDragging}>
<IconListViewGrip />
</StyledIconWrapper>
</StyledContainer>
</RecordTableTd>
);
};

View File

@ -1,10 +1,10 @@
import { StyledTd } from '@/object-record/record-table/components/RecordTableRow';
import { RecordTableCellSkeletonLoader } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader'; import { RecordTableCellSkeletonLoader } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
export const RecordTableCellLoading = () => { export const RecordTableCellLoading = () => {
return ( return (
<StyledTd> <RecordTableTd>
<RecordTableCellSkeletonLoader /> <RecordTableCellSkeletonLoader />
</StyledTd> </RecordTableTd>
); );
}; };

View File

@ -0,0 +1,75 @@
import { useContext, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
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 { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
export const RecordTableCellWrapper = ({
children,
column,
columnIndex,
}: {
column: ColumnDefinition<FieldMetadata>;
columnIndex: number;
children: React.ReactNode;
}) => {
const tableScopeId = useAvailableScopeIdOrThrow(
RecordTableScopeInternalContext,
getScopeIdOrUndefinedFromComponentId(),
);
const { rowIndex } = useContext(RecordTableRowContext);
const currentTableCellPosition: TableCellPosition = useMemo(
() => ({
column: columnIndex,
row: rowIndex,
}),
[columnIndex, rowIndex],
);
const isTableCellInEditModeFamilyState = extractComponentFamilyState(
isTableCellInEditModeComponentFamilyState,
tableScopeId,
);
const isSoftFocusOnTableCellFamilyState = extractComponentFamilyState(
isSoftFocusOnTableCellComponentFamilyState,
tableScopeId,
);
const isInEditMode = useRecoilValue(
isTableCellInEditModeFamilyState(currentTableCellPosition),
);
const hasSoftFocus = useRecoilValue(
isSoftFocusOnTableCellFamilyState(currentTableCellPosition),
);
return (
<RecordTableCellContext.Provider
value={{
columnDefinition: column,
columnIndex,
isInEditMode,
hasSoftFocus,
cellPosition: currentTableCellPosition,
}}
key={column.fieldMetadataId}
>
<RecordTableCellFieldContextWrapper>
{children}
</RecordTableCellFieldContextWrapper>
</RecordTableCellContext.Provider>
);
};

View File

@ -0,0 +1,10 @@
import { useContext } from 'react';
import { RecordTableRowContext } 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);
return <RecordTableTd isSelected={isSelected} hasRightBorder={false} />;
};

View File

@ -0,0 +1,102 @@
import { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
import { ReactNode, useContext } from 'react';
import { MOBILE_VIEWPORT, ThemeContext } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
const StyledTd = styled.td<{
zIndex?: number;
backgroundColor: string;
borderColor: string;
isDragging?: boolean;
fontColor: string;
sticky?: boolean;
freezeFirstColumns?: boolean;
left?: number;
hasRightBorder?: boolean;
hasBottomBorder?: boolean;
}>`
border-bottom: 1px solid
${({ borderColor, hasBottomBorder }) =>
hasBottomBorder ? borderColor : 'transparent'};
color: ${({ fontColor }) => fontColor};
border-right: 1px solid
${({ borderColor, hasRightBorder }) =>
hasRightBorder ? borderColor : 'transparent'};
padding: 0;
text-align: left;
background: ${({ backgroundColor }) => backgroundColor};
z-index: ${({ zIndex }) => (isDefined(zIndex) ? zIndex : 'auto')};
${({ isDragging }) =>
isDragging
? `
background-color: transparent;
border-color: transparent;
`
: ''}
${({ freezeFirstColumns }) =>
freezeFirstColumns
? `@media (max-width: ${MOBILE_VIEWPORT}px) {
width: 35px;
max-width: 35px;
}`
: ''}
`;
export const RecordTableTd = ({
children,
zIndex,
isSelected,
isDragging,
sticky,
freezeFirstColumns,
left,
hasRightBorder = true,
hasBottomBorder = true,
...dragHandleProps
}: {
className?: string;
children?: ReactNode;
zIndex?: number;
isSelected?: boolean;
isDragging?: boolean;
sticky?: boolean;
freezeFirstColumns?: boolean;
hasRightBorder?: boolean;
hasBottomBorder?: boolean;
left?: number;
} & (Partial<DraggableProvidedDragHandleProps> | null)) => {
const { theme } = useContext(ThemeContext);
const tdBackgroundColor = isSelected
? theme.accent.quaternary
: theme.background.primary;
const borderColor = theme.border.color.light;
const fontColor = theme.font.color.primary;
return (
<StyledTd
isDragging={isDragging}
zIndex={zIndex}
backgroundColor={tdBackgroundColor}
borderColor={borderColor}
fontColor={fontColor}
sticky={sticky}
freezeFirstColumns={freezeFirstColumns}
left={left}
hasRightBorder={hasRightBorder}
hasBottomBorder={hasBottomBorder}
// eslint-disable-next-line react/jsx-props-no-spreading
{...dragHandleProps}
>
{children}
</StyledTd>
);
};

View File

@ -1,6 +1,5 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { RecordTableCellContextProps } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContextProps } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableRowContextProps } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
export const recordTableRow: RecordTableRowContextProps = { export const recordTableRow: RecordTableRowContextProps = {
@ -10,12 +9,13 @@ export const recordTableRow: RecordTableRowContextProps = {
pathToShowPage: '/', pathToShowPage: '/',
objectNameSingular: 'objectNameSingular', objectNameSingular: 'objectNameSingular',
isReadOnly: false, isReadOnly: false,
dragHandleProps: {} as any,
isDragging: false,
inView: true,
isPendingRow: false,
}; };
export const recordTableCell: { export const recordTableCell:RecordTableCellContextProps= {
columnDefinition: ColumnDefinition<FieldMetadata>;
columnIndex: number;
} = {
columnIndex: 3, columnIndex: 3,
columnDefinition: { columnDefinition: {
size: 1, size: 1,
@ -29,4 +29,10 @@ export const recordTableCell: {
fieldName: 'fieldName', fieldName: 'fieldName',
}, },
}, },
cellPosition: {
row: 2,
column: 3,
},
hasSoftFocus: false,
isInEditMode: false,
}; };

View File

@ -1,21 +1,9 @@
import { useContext, useMemo } from 'react'; import { useContext } from 'react';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { TableCellPosition } from '../../types/TableCellPosition';
export const useCurrentTableCellPosition = () => { export const useCurrentTableCellPosition = () => {
const { rowIndex } = useContext(RecordTableRowContext); const { cellPosition } = useContext(RecordTableCellContext);
const { columnIndex } = useContext(RecordTableCellContext);
const currentTableCellPosition: TableCellPosition = useMemo( return cellPosition;
() => ({
column: columnIndex,
row: rowIndex,
}),
[columnIndex, rowIndex],
);
return currentTableCellPosition;
}; };

View File

@ -1,14 +1,14 @@
import { css, useTheme } from '@emotion/react'; import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { MOBILE_VIEWPORT, useIcons } from 'twenty-ui'; import { MOBILE_VIEWPORT, useIcons } from 'twenty-ui';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState'; import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { ColumnDefinition } from '../types/ColumnDefinition'; import { ColumnDefinition } from '../../types/ColumnDefinition';
type ColumnHeadProps = { type RecordTableColumnHeadProps = {
column: ColumnDefinition<FieldMetadata>; column: ColumnDefinition<FieldMetadata>;
}; };
@ -46,16 +46,22 @@ const StyledText = styled.span`
white-space: nowrap; white-space: nowrap;
`; `;
export const ColumnHead = ({ column }: ColumnHeadProps) => { export const RecordTableColumnHead = ({
column,
}: RecordTableColumnHeadProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const Icon = getIcon(column.iconName); const Icon = getIcon(column.iconName);
const scrollLeft = useRecoilValue(scrollLeftState); const isRecordTableScrolledLeft = useRecoilComponentValue(
isRecordTableScrolledLeftComponentState,
);
return ( return (
<StyledTitle hideTitle={!!column.isLabelIdentifier && scrollLeft > 0}> <StyledTitle
hideTitle={!!column.isLabelIdentifier && !isRecordTableScrolledLeft}
>
<StyledIcon> <StyledIcon>
<Icon size={theme.icon.size.md} /> <Icon size={theme.icon.size.md} />
</StyledIcon> </StyledIcon>

View File

@ -14,16 +14,16 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useTableColumns } from '../hooks/useTableColumns'; import { useTableColumns } from '../../hooks/useTableColumns';
import { ColumnDefinition } from '../types/ColumnDefinition'; import { ColumnDefinition } from '../../types/ColumnDefinition';
export type RecordTableColumnDropdownMenuProps = { export type RecordTableColumnHeadDropdownMenuProps = {
column: ColumnDefinition<FieldMetadata>; column: ColumnDefinition<FieldMetadata>;
}; };
export const RecordTableColumnDropdownMenu = ({ export const RecordTableColumnHeadDropdownMenu = ({
column, column,
}: RecordTableColumnDropdownMenuProps) => { }: RecordTableColumnHeadDropdownMenuProps) => {
const { const {
visibleTableColumnsSelector, visibleTableColumnsSelector,
onToggleColumnFilterState, onToggleColumnFilterState,

View File

@ -4,26 +4,29 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { ColumnHead } from './ColumnHead'; import { RecordTableColumnHeadDropdownMenu } from './RecordTableColumnHeadDropdownMenu';
import { RecordTableColumnDropdownMenu } from './RecordTableColumnDropdownMenu';
type ColumnHeadWithDropdownProps = { import { RecordTableColumnHead } from './RecordTableColumnHead';
type RecordTableColumnHeadWithDropdownProps = {
column: ColumnDefinition<FieldMetadata>; column: ColumnDefinition<FieldMetadata>;
}; };
const StyledDropdown = styled(Dropdown)` const StyledDropdown = styled(Dropdown)`
display: flex; display: flex;
flex: 1; flex: 1;
z-index: ${({ theme }) => theme.lastLayerZIndex};
`; `;
export const ColumnHeadWithDropdown = ({ export const RecordTableColumnHeadWithDropdown = ({
column, column,
}: ColumnHeadWithDropdownProps) => { }: RecordTableColumnHeadWithDropdownProps) => {
return ( return (
<StyledDropdown <StyledDropdown
dropdownId={column.fieldMetadataId + '-header'} dropdownId={column.fieldMetadataId + '-header'}
clickableComponent={<ColumnHead column={column} />} clickableComponent={<RecordTableColumnHead column={column} />}
dropdownComponents={<RecordTableColumnDropdownMenu column={column} />} dropdownComponents={<RecordTableColumnHeadDropdownMenu column={column} />}
dropdownOffset={{ x: -1 }} dropdownOffset={{ x: -1 }}
dropdownPlacement="bottom-start" dropdownPlacement="bottom-start"
dropdownHotkeyScope={{ scope: column.fieldMetadataId + '-header' }} dropdownHotkeyScope={{ scope: column.fieldMetadataId + '-header' }}

View File

@ -0,0 +1,91 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { MOBILE_VIEWPORT } from 'twenty-ui';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell';
import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn';
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn';
const StyledTableHead = styled.thead<{
isScrolledTop?: boolean;
isScrolledLeft?: boolean;
}>`
cursor: pointer;
th:nth-of-type(1) {
width: 9px;
left: 0;
border-right-color: ${({ theme }) => theme.background.primary};
}
th:nth-of-type(2) {
border-right-color: ${({ theme }) => theme.background.primary};
}
&.first-columns-sticky {
th:nth-child(1) {
position: sticky;
left: 0;
z-index: 5;
}
th:nth-child(2) {
position: sticky;
left: 9px;
z-index: 5;
}
th:nth-child(3) {
position: sticky;
left: 39px;
z-index: 5;
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: 35px;
max-width: 35px;
}
}
}
&.header-sticky {
th {
position: sticky;
top: 0;
z-index: 5;
}
}
&.header-sticky.first-columns-sticky {
th:nth-child(1),
th:nth-child(2),
th:nth-child(3) {
z-index: 10;
}
}
`;
export const RecordTableHeader = ({
createRecord,
}: {
createRecord: () => void;
}) => {
const { visibleTableColumnsSelector } = useRecordTableStates();
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
return (
<StyledTableHead id="record-table-header" data-select-disable>
<tr>
<RecordTableHeaderDragDropColumn />
<RecordTableHeaderCheckboxColumn />
{visibleTableColumns.map((column) => (
<RecordTableHeaderCell
key={column.fieldMetadataId}
column={column}
createRecord={createRecord}
/>
))}
<RecordTableHeaderLastColumn />
</tr>
</StyledTableHead>
);
};

View File

@ -1,27 +1,35 @@
import { useCallback, useMemo, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { IconPlus } from 'twenty-ui'; import { IconPlus } from 'twenty-ui';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown';
const COLUMN_MIN_WIDTH = 104; const COLUMN_MIN_WIDTH = 104;
const StyledColumnHeaderCell = styled.th<{ const StyledColumnHeaderCell = styled.th<{
columnWidth: number; columnWidth: number;
isResizing?: boolean; isResizing?: boolean;
}>` }>`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-top: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.tertiary};
padding: 0;
text-align: left;
background-color: ${({ theme }) => theme.background.primary};
border-right: 1px solid ${({ theme }) => theme.border.color.light};
${({ columnWidth }) => ` ${({ columnWidth }) => `
min-width: ${columnWidth}px; min-width: ${columnWidth}px;
width: ${columnWidth}px; width: ${columnWidth}px;
@ -165,11 +173,14 @@ export const RecordTableHeaderCell = ({
onMouseUp: handleResizeHandlerEnd, onMouseUp: handleResizeHandlerEnd,
}); });
const isRecordTableScrolledLeft = useRecoilComponentValue(
isRecordTableScrolledLeftComponentState,
);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const scrollLeft = useRecoilValue(scrollLeftState);
const disableColumnResize = const disableColumnResize =
column.isLabelIdentifier && isMobile && scrollLeft > 0; column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft;
return ( return (
<StyledColumnHeaderCell <StyledColumnHeaderCell
@ -185,7 +196,7 @@ export const RecordTableHeaderCell = ({
onMouseLeave={() => setIconVisibility(false)} onMouseLeave={() => setIconVisibility(false)}
> >
<StyledColumnHeadContainer> <StyledColumnHeadContainer>
<ColumnHeadWithDropdown column={column} /> <RecordTableColumnHeadWithDropdown column={column} />
{(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && ( {(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && (
<StyledHeaderIcon> <StyledHeaderIcon>
<LightIconButton <LightIconButton

View File

@ -2,9 +2,9 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { Checkbox } from '@/ui/input/components/Checkbox'; import { Checkbox } from '@/ui/input/components/Checkbox';
import { useTheme } from '@emotion/react';
import { useRecordTable } from '../hooks/useRecordTable';
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;
@ -16,7 +16,7 @@ const StyledContainer = styled.div`
background-color: ${({ theme }) => theme.background.primary}; background-color: ${({ theme }) => theme.background.primary};
`; `;
export const SelectAllCheckbox = () => { export const RecordTableHeaderCheckboxColumn = () => {
const { allRowsSelectedStatusSelector } = useRecordTableStates(); const { allRowsSelectedStatusSelector } = useRecordTableStates();
const allRowsSelectedStatus = useRecoilValue(allRowsSelectedStatusSelector()); const allRowsSelectedStatus = useRecoilValue(allRowsSelectedStatusSelector());
@ -36,13 +36,26 @@ export const SelectAllCheckbox = () => {
} }
}; };
const theme = useTheme();
return ( return (
<StyledContainer> <th
<Checkbox style={{
checked={checked} borderBottom: `1px solid ${theme.border.color.light}`,
onChange={onChange} borderTop: `1px solid ${theme.border.color.light}`,
indeterminate={indeterminate} width: 30,
/> minWidth: 30,
</StyledContainer> maxWidth: 30,
borderRight: 'transparent',
}}
>
<StyledContainer>
<Checkbox
checked={checked}
onChange={onChange}
indeterminate={indeterminate}
/>
</StyledContainer>
</th>
); );
}; };

View File

@ -0,0 +1,10 @@
import { styled } from '@linaria/react';
const StyledTh = styled.th`
border-bottom: none;
border-top: none;
`;
export const RecordTableHeaderDragDropColumn = () => {
return <StyledTh></StyledTh>;
};

View File

@ -0,0 +1,89 @@
import { Theme } from '@emotion/react';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { IconPlus, ThemeContext } from 'twenty-ui';
import { HIDDEN_TABLE_COLUMN_DROPDOWN_ID } from '@/object-record/record-table/constants/HiddenTableColumnDropdownId';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
const StyledPlusIconHeaderCell = styled.th<{
theme: Theme;
isTableWiderThanScreen: boolean;
}>`
${({ theme }) => {
return `
&:hover {
background: ${theme.background.transparent.light};
};
padding-left: ${theme.spacing(3)};
`;
}};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-top: 1px solid ${({ theme }) => theme.border.color.light};
background-color: ${({ theme }) => theme.background.primary};
border-left: none !important;
color: ${({ theme }) => theme.font.color.tertiary};
min-width: 32px;
border-right: none !important;
${({ isTableWiderThanScreen, theme }) =>
isTableWiderThanScreen
? `
width: 32px;
background-color: ${theme.background.primary};
`
: ''};
z-index: 1;
`;
const StyledPlusIconContainer = styled.div`
align-items: center;
display: flex;
height: 32px;
justify-content: center;
width: 32px;
`;
const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
'hidden-table-columns-dropdown-hotkey-scope-id';
export const RecordTableHeaderLastColumn = () => {
const { theme } = useContext(ThemeContext);
const scrollWrapper = useScrollWrapperScopedRef();
const isTableWiderThanScreen =
(scrollWrapper.current?.clientWidth ?? 0) <
(scrollWrapper.current?.scrollWidth ?? 0);
const { hiddenTableColumnsSelector } = useRecordTableStates();
const hiddenTableColumns = useRecoilValue(hiddenTableColumnsSelector());
return (
<StyledPlusIconHeaderCell
theme={theme}
isTableWiderThanScreen={isTableWiderThanScreen}
>
{hiddenTableColumns.length > 0 && (
<Dropdown
dropdownId={HIDDEN_TABLE_COLUMN_DROPDOWN_ID}
clickableComponent={
<StyledPlusIconContainer>
<IconPlus size={theme.icon.size.md} />
</StyledPlusIconContainer>
}
dropdownComponents={<RecordTableHeaderPlusButtonContent />}
dropdownPlacement="bottom-start"
dropdownHotkeyScope={{
scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID,
}}
/>
)}
</StyledPlusIconHeaderCell>
);
};

View File

@ -0,0 +1,17 @@
import { useContext } from 'react';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
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 areCellsVisible = inView || isDragging;
return areCellsVisible ? (
<RecordTableCellsVisible />
) : (
<RecordTableCellsEmpty />
);
};

View File

@ -0,0 +1,17 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
export const RecordTableCellsEmpty = () => {
const { isSelected } = useContext(RecordTableRowContext);
const { visibleTableColumnsSelector } = useRecordTableStates();
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
return visibleTableColumns.map((column) => (
<RecordTableTd isSelected={isSelected} key={column.fieldMetadataId} />
));
};

View File

@ -0,0 +1,39 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
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';
export const RecordTableCellsVisible = () => {
const { isDragging } = useContext(RecordTableRowContext);
const { visibleTableColumnsSelector } = useRecordTableStates();
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
const tableColumnsAfterFirst = visibleTableColumns.slice(1);
return (
<>
<RecordTableCellWrapper column={visibleTableColumns[0]} columnIndex={0}>
<RecordTableTd>
<RecordTableCell />
</RecordTableTd>
</RecordTableCellWrapper>
{!isDragging &&
tableColumnsAfterFirst.map((column, columnIndex) => (
<RecordTableCellWrapper
key={column.fieldMetadataId}
column={column}
columnIndex={columnIndex + 1}
>
<RecordTableTd>
<RecordTableCell />
</RecordTableTd>
</RecordTableCellWrapper>
))}
</>
);
};

View File

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { RecordTableRow } from '@/object-record/record-table/components/RecordTableRow';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
export const RecordTablePendingRow = () => { export const RecordTablePendingRow = () => {
const { pendingRecordIdState } = useRecordTableStates(); const { pendingRecordIdState } = useRecordTableStates();
const pendingRecordId = useRecoilValue(pendingRecordIdState); const pendingRecordId = useRecoilValue(pendingRecordIdState);
if (!pendingRecordId) return; if (!pendingRecordId) return <></>;
return ( return (
<RecordTableRow <RecordTableRow

View File

@ -0,0 +1,32 @@
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
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 { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
import { RecordTableCells } from '@/object-record/record-table/record-table-row/components/RecordTableCells';
import { RecordTableRowWrapper } from '@/object-record/record-table/record-table-row/components/RecordTableRowWrapper';
type RecordTableRowProps = {
recordId: string;
rowIndex: number;
isPendingRow?: boolean;
};
export const RecordTableRow = ({
recordId,
rowIndex,
isPendingRow,
}: RecordTableRowProps) => {
return (
<RecordTableRowWrapper
recordId={recordId}
rowIndex={rowIndex}
isPendingRow={isPendingRow}
>
<RecordTableCellGrip />
<RecordTableCellCheckbox />
<RecordTableCells />
<RecordTableLastEmptyCell />
<RecordValueSetterEffect recordId={recordId} />
</RecordTableRowWrapper>
);
};

View File

@ -0,0 +1,87 @@
import { ReactNode, useContext } from 'react';
import { useInView } from 'react-intersection-observer';
import { useTheme } from '@emotion/react';
import { Draggable } from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
export const RecordTableRowWrapper = ({
recordId,
rowIndex,
isPendingRow,
children,
}: {
recordId: string;
rowIndex: number;
isPendingRow?: boolean;
children: ReactNode;
}) => {
const { objectMetadataItem } = useContext(RecordTableContext);
const theme = useTheme();
const { isRowSelectedFamilyState } = useRecordTableStates();
const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId));
const scrollWrapperRef = useContext(ScrollWrapperContext);
const { ref: elementRef, inView } = useInView({
root: scrollWrapperRef.current?.querySelector(
'[data-overlayscrollbars-viewport="scrollbarHidden"]',
),
rootMargin: '1000px',
});
return (
<Draggable key={recordId} draggableId={recordId} index={rowIndex}>
{(draggableProvided, draggableSnapshot) => (
<RecordTableTr
ref={(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,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular: objectMetadataItem.nameSingular,
}) + recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isSelected: currentRowSelected,
isReadOnly: objectMetadataItem.isRemote ?? false,
isPendingRow,
isDragging: draggableSnapshot.isDragging,
dragHandleProps: draggableProvided.dragHandleProps,
inView,
}}
>
{children}
</RecordTableRowContext.Provider>
</RecordTableTr>
)}
</Draggable>
);
};

View File

@ -0,0 +1,11 @@
import styled from '@emotion/styled';
const StyledTr = styled.tr<{ isDragging: boolean }>`
border: ${({ isDragging, theme }) =>
isDragging
? `1px solid ${theme.border.color.medium}`
: '1px solid transparent'};
transition: border-left-color 0.2s ease-in-out;
`;
export const RecordTableTr = StyledTr;

View File

@ -0,0 +1,9 @@
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const isRecordTableScrolledLeftComponentState =
createComponentStateV2<boolean>({
key: 'isRecordTableScrolledLeftComponentState',
componentContext: RecordTableScopeInternalContext,
defaultValue: true,
});

View File

@ -0,0 +1,9 @@
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const isRecordTableScrolledTopComponentState =
createComponentStateV2<boolean>({
key: 'isRecordTableScrolledTopComponentState',
componentContext: RecordTableScopeInternalContext,
defaultValue: true,
});

View File

@ -1,12 +1,13 @@
import { useRef } from 'react';
import { Keys } from 'react-hotkeys-hook';
import { import {
autoUpdate, autoUpdate,
flip, flip,
FloatingPortal,
offset, offset,
Placement, Placement,
useFloating, useFloating,
} from '@floating-ui/react'; } from '@floating-ui/react';
import { useRef } from 'react';
import { Keys } from 'react-hotkeys-hook';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
@ -85,7 +86,7 @@ export const Dropdown = ({
}; };
useListenClickOutside({ useListenClickOutside({
refs: [containerRef], refs: [refs.floating],
callback: () => { callback: () => {
onClickOutside?.(); onClickOutside?.();
@ -131,15 +132,17 @@ export const Dropdown = ({
/> />
)} )}
{isDropdownOpen && ( {isDropdownOpen && (
<DropdownMenu <FloatingPortal>
disableBlur={disableBlur} <DropdownMenu
width={dropdownMenuWidth ?? dropdownWidth} disableBlur={disableBlur}
data-select-disable width={dropdownMenuWidth ?? dropdownWidth}
ref={refs.setFloating} data-select-disable
style={floatingStyles} ref={refs.setFloating}
> style={floatingStyles}
{dropdownComponents} >
</DropdownMenu> {dropdownComponents}
</DropdownMenu>
</FloatingPortal>
)} )}
<DropdownOnToggleEffect <DropdownOnToggleEffect
onDropdownClose={onClose} onDropdownClose={onClose}

View File

@ -1,10 +1,10 @@
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import { Chip, ChipVariant } from 'twenty-ui'; import { Chip, ChipVariant } from 'twenty-ui';
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown'; import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown';
import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement'; import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement';
import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';

View File

@ -1,20 +1,17 @@
import React from 'react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import React from 'react';
const StyledAnimatedChipContainer = styled(motion.div)``;
export const AnimatedContainer = ({ export const AnimatedContainer = ({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) => ( }) => (
<StyledAnimatedChipContainer <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.1 }} transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }} whileHover={{ scale: 1.04 }}
> >
{children} {children}
</StyledAnimatedChipContainer> </motion.div>
); );

View File

@ -0,0 +1,29 @@
import { useRecoilValue } from 'recoil';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
import { ComponentState } from '@/ui/utilities/state/component-state/types/ComponentState';
export const useRecoilComponentValue = <StateType>(
componentState: ComponentState<StateType>,
componentId?: string,
) => {
const componentContext = (window as any).componentContextStateMap?.get(
componentState.key,
);
if (!componentContext) {
throw new Error(
`Component context for key "${componentState.key}" is not defined`,
);
}
const internalComponentId = useAvailableScopeIdOrThrow(
componentContext,
getScopeIdOrUndefinedFromComponentId(componentId),
);
return useRecoilValue(
componentState.atomFamily({ scopeId: internalComponentId }),
);
};

View File

@ -0,0 +1,29 @@
import { useSetRecoilState } from 'recoil';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
import { ComponentState } from '@/ui/utilities/state/component-state/types/ComponentState';
export const useSetRecoilComponentState = <StateType>(
componentState: ComponentState<StateType>,
componentId?: string,
) => {
const componentContext = (window as any).componentContextStateMap?.get(
componentState.key,
);
if (!componentContext) {
throw new Error(
`Component context for key "${componentState.key}" is not defined`,
);
}
const internalComponentId = useAvailableScopeIdOrThrow(
componentContext,
getScopeIdOrUndefinedFromComponentId(componentId),
);
return useSetRecoilState(
componentState.atomFamily({ scopeId: internalComponentId }),
);
};

View File

@ -0,0 +1,8 @@
import { RecoilState } from 'recoil';
import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey';
export type ComponentState<StateType> = {
key: string;
atomFamily: (componentStateKey: ComponentStateKey) => RecoilState<StateType>;
};

View File

@ -2,15 +2,17 @@ import { AtomEffect, atomFamily } from 'recoil';
import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey'; import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey';
type CreateComponentStateType<ValueType> = {
key: string;
defaultValue: ValueType;
effects?: AtomEffect<ValueType>[];
};
export const createComponentState = <ValueType>({ export const createComponentState = <ValueType>({
key, key,
defaultValue, defaultValue,
effects, effects,
}: { }: CreateComponentStateType<ValueType>) => {
key: string;
defaultValue: ValueType;
effects?: AtomEffect<ValueType>[];
}) => {
return atomFamily<ValueType, ComponentStateKey>({ return atomFamily<ValueType, ComponentStateKey>({
key, key,
default: defaultValue, default: defaultValue,

View File

@ -0,0 +1,37 @@
import { AtomEffect, atomFamily } from 'recoil';
import { ScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/types/ScopeInternalContext';
import { ComponentState } from '@/ui/utilities/state/component-state/types/ComponentState';
import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey';
import { isDefined } from '~/utils/isDefined';
type CreateComponentStateV2Type<ValueType> = {
key: string;
defaultValue: ValueType;
componentContext?: ScopeInternalContext<any> | null;
effects?: AtomEffect<ValueType>[];
};
export const createComponentStateV2 = <ValueType>({
key,
defaultValue,
componentContext,
effects,
}: CreateComponentStateV2Type<ValueType>): ComponentState<ValueType> => {
if (isDefined(componentContext)) {
if (!isDefined((window as any).componentContextStateMap)) {
(window as any).componentContextStateMap = new Map();
}
(window as any).componentContextStateMap.set(key, componentContext);
}
return {
key,
atomFamily: atomFamily<ValueType, ComponentStateKey>({
key,
default: defaultValue,
effects: effects,
}),
};
};

View File

@ -67,6 +67,11 @@ export default defineConfig(({ command, mode }) => {
'**/RecordTableCellContainer.tsx', '**/RecordTableCellContainer.tsx',
'**/RecordTableCellDisplayContainer.tsx', '**/RecordTableCellDisplayContainer.tsx',
'**/Avatar.tsx', '**/Avatar.tsx',
'**/RecordTableBodyDroppable.tsx',
'**/RecordTableCellBaseContainer.tsx',
'**/RecordTableCellTd.tsx',
'**/RecordTableTd.tsx',
'**/RecordTableHeaderDragDropColumn.tsx',
], ],
babelOptions: { babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'], presets: ['@babel/preset-typescript', '@babel/preset-react'],