From 7b3a590f799e4b2ff3dfc77dfa424912c49cb2db Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 5 Jul 2024 18:30:59 +0200 Subject: [PATCH] 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 --- .eslintrc.cjs | 29 +-- .vscode/settings.json | 9 +- .../record-field/hooks/useIsFieldEmpty.ts | 1 + .../RecordIndexRemoveSortingModal.tsx} | 2 +- .../components/RecordIndexTableContainer.tsx | 4 +- .../components/RecordInlineCellEditButton.tsx | 2 +- .../record-table/components/GripCell.tsx | 29 --- .../record-table/components/RecordTable.tsx | 244 ++---------------- .../components/RecordTableBody.tsx | 53 ---- .../components/RecordTableBodyEffect.tsx | 61 ----- .../components/RecordTableContextProvider.tsx | 109 ++++++++ .../components/RecordTableHeader.tsx | 114 -------- .../components/RecordTableRow.tsx | 183 ------------- .../components/RecordTableRows.tsx | 16 ++ .../components/RecordTableWithWrappers.tsx | 43 +-- .../perf/RecordTableCell.perf.stories.tsx | 14 +- .../constants/HiddenTableColumnDropdownId.ts | 2 + .../contexts/RecordTableCellContext.ts | 11 +- .../contexts/RecordTableContext.ts | 2 + .../contexts/RecordTableRowContext.ts | 4 + .../components/RecordTableBody.tsx | 38 +++ .../RecordTableBodyDragDropContext.tsx} | 55 ++-- .../components/RecordTableBodyDroppable.tsx | 57 ++++ .../components/RecordTableBodyEffect.tsx | 106 ++++++++ .../RecordTableBodyFetchMoreLoader.tsx | 0 .../components/RecordTableBodyLoading.tsx | 21 +- .../components/RecordTableCell.tsx | 102 +------- .../RecordTableCellBaseContainer.tsx | 101 ++++++++ .../components/RecordTableCellButton.tsx | 2 +- .../components/RecordTableCellCheckbox.tsx} | 15 +- .../components/RecordTableCellContainer.tsx | 171 ++---------- .../RecordTableCellDisplayContainer.tsx | 2 - .../RecordTableCellFieldContextWrapper.tsx | 11 +- .../components/RecordTableCellFieldInput.tsx | 95 +++++++ .../components/RecordTableCellGrip.tsx | 44 ++++ .../components/RecordTableCellLoading.tsx | 6 +- .../components/RecordTableCellWrapper.tsx | 75 ++++++ .../components/RecordTableLastEmptyCell.tsx | 10 + .../components/RecordTableTd.tsx | 102 ++++++++ .../record-table-cell/hooks/__mocks__/cell.ts | 18 +- .../hooks/useCurrentCellPosition.ts | 18 +- .../components/RecordTableColumnHead.tsx} | 20 +- .../RecordTableColumnHeadDropdownMenu.tsx} | 10 +- .../RecordTableColumnHeadWithDropdown.tsx} | 17 +- .../components/RecordTableHeader.tsx | 91 +++++++ .../components/RecordTableHeaderCell.tsx | 25 +- .../RecordTableHeaderCheckboxColumn.tsx} | 33 ++- .../RecordTableHeaderDragDropColumn.tsx | 10 + .../RecordTableHeaderLastColumn.tsx | 89 +++++++ .../RecordTableHeaderPlusButtonContent.tsx | 0 .../components/RecordTableCells.tsx | 17 ++ .../components/RecordTableCellsEmpty.tsx | 17 ++ .../components/RecordTableCellsVisible.tsx | 39 +++ .../components/RecordTablePendingRow.tsx | 4 +- .../components/RecordTableRow.tsx | 32 +++ .../components/RecordTableRowWrapper.tsx | 87 +++++++ .../components/RecordTableTr.tsx | 11 + ...isRecordTableScrolledLeftComponentState.ts | 9 + .../isRecordTableScrolledTopComponentState.ts | 9 + .../layout/dropdown/components/Dropdown.tsx | 27 +- .../components/ExpandableList.tsx | 4 +- .../components/AnimatedContainer.tsx | 9 +- .../hooks/useRecoilComponentValue.ts | 29 +++ .../hooks/useSetRecoilComponentState.ts | 29 +++ .../component-state/types/ComponentState.ts | 8 + .../utils/createComponentState.ts | 12 +- .../utils/createComponentStateV2.ts | 37 +++ packages/twenty-front/vite.config.ts | 5 + 68 files changed, 1531 insertions(+), 1130 deletions(-) rename packages/twenty-front/src/modules/object-record/{record-table/components/RemoveSortingModal.tsx => record-index/components/RecordIndexRemoveSortingModal.tsx} (96%) delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/GripCell.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRows.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/constants/HiddenTableColumnDropdownId.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx rename packages/twenty-front/src/modules/{ui/layout/draggable-list/components/DraggableTableBody.tsx => object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx} (58%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx rename packages/twenty-front/src/modules/object-record/record-table/{ => record-table-body}/components/RecordTableBodyFetchMoreLoader.tsx (100%) rename packages/twenty-front/src/modules/object-record/record-table/{ => record-table-body}/components/RecordTableBodyLoading.tsx (64%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx rename packages/twenty-front/src/modules/object-record/record-table/{components/CheckboxCell.tsx => record-table-cell/components/RecordTableCellCheckbox.tsx} (75%) rename packages/twenty-front/src/modules/object-record/record-table/{ => record-table-cell}/components/RecordTableCellFieldContextWrapper.tsx (89%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellWrapper.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx rename packages/twenty-front/src/modules/object-record/record-table/{components/ColumnHead.tsx => record-table-header/components/RecordTableColumnHead.tsx} (67%) rename packages/twenty-front/src/modules/object-record/record-table/{components/RecordTableColumnDropdownMenu.tsx => record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx} (92%) rename packages/twenty-front/src/modules/object-record/record-table/{components/ColumnHeadWithDropdown.tsx => record-table-header/components/RecordTableColumnHeadWithDropdown.tsx} (56%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx rename packages/twenty-front/src/modules/object-record/record-table/{ => record-table-header}/components/RecordTableHeaderCell.tsx (85%) rename packages/twenty-front/src/modules/object-record/record-table/{components/SelectAllCheckbox.tsx => record-table-header/components/RecordTableHeaderCheckboxColumn.tsx} (61%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx rename packages/twenty-front/src/modules/object-record/record-table/{ => record-table-header}/components/RecordTableHeaderPlusButtonContent.tsx (100%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCells.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsEmpty.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx rename packages/twenty-front/src/modules/object-record/record-table/{ => record-table-row}/components/RecordTablePendingRow.tsx (76%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRow.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledLeftComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts rename packages/twenty-front/src/modules/{object-record/record-table => ui/utilities/animation}/components/AnimatedContainer.tsx (65%) create mode 100644 packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentValue.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useSetRecoilComponentState.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/state/component-state/types/ComponentState.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 030209c47d..95f91b2a78 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,14 +1,7 @@ module.exports = { root: true, extends: ['plugin:prettier/recommended'], - plugins: [ - '@nx', - 'prefer-arrow', - 'import', - 'simple-import-sort', - 'unused-imports', - 'unicorn', - ], + plugins: ['@nx', 'prefer-arrow', 'import', 'unused-imports', 'unicorn'], rules: { 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], '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-vars': [ 'warn', diff --git a/.vscode/settings.json b/.vscode/settings.json index c6ff47f129..d63c92973c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,21 +5,24 @@ "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", - "source.addMissingImports": "always" + "source.addMissingImports": "always", + "source.organizeImports": "always" } }, "[javascript]": { "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", - "source.addMissingImports": "always" + "source.addMissingImports": "always", + "source.organizeImports": "always" } }, "[typescriptreact]": { "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", - "source.addMissingImports": "always" + "source.addMissingImports": "always", + "source.organizeImports": "always" } }, "[json]": { diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldEmpty.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldEmpty.ts index 9126d761a1..f1e100566e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldEmpty.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldEmpty.ts @@ -9,6 +9,7 @@ import { FieldContext } from '../contexts/FieldContext'; export const useIsFieldEmpty = () => { const { entityId, fieldDefinition, overridenIsFieldEmpty } = useContext(FieldContext); + const fieldValue = useRecordFieldValue( entityId, fieldDefinition?.metadata?.fieldName ?? '', diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RemoveSortingModal.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRemoveSortingModal.tsx similarity index 96% rename from packages/twenty-front/src/modules/object-record/record-table/components/RemoveSortingModal.tsx rename to packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRemoveSortingModal.tsx index 9bfb93b604..efe7e4cb92 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RemoveSortingModal.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRemoveSortingModal.tsx @@ -5,7 +5,7 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa import { useCombinedViewSorts } from '@/views/hooks/useCombinedViewSorts'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; -export const RemoveSortingModal = ({ +export const RecordIndexRemoveSortingModal = ({ recordTableId, }: { recordTableId: string; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx index 31be052ff4..f5af2e96fa 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx @@ -1,8 +1,8 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; 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 { 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'; type RecordIndexTableContainerProps = { @@ -39,7 +39,7 @@ export const RecordIndexTableContainer = ({ createRecord={createRecord} /> - + ); diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx index 0475a92560..2af1e91d7c 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; import { IconComponent } from 'twenty-ui'; -import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; +import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer'; const StyledInlineCellButtonContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/GripCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/GripCell.tsx deleted file mode 100644 index b9900026db..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/components/GripCell.tsx +++ /dev/null @@ -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 ( - - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 84a8e13cbd..5407b756da 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -1,154 +1,20 @@ -import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; -import { MOBILE_VIEWPORT, RGBA } from 'twenty-ui'; +import { isNonEmptyString } from '@sniptt/guards'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { RecordTableBody } from '@/object-record/record-table/components/RecordTableBody'; -import { RecordTableBodyEffect } from '@/object-record/record-table/components/RecordTableBodyEffect'; -import { RecordTableHeader } from '@/object-record/record-table/components/RecordTableHeader'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; -import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; +import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody'; +import { RecordTableBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEffect'; +import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; +import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; 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 { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; -import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; -const StyledTable = styled.table<{ - freezeFirstColumns?: boolean; -}>` +const StyledTable = styled.table` border-radius: ${({ theme }) => theme.border.radius.sm}; border-spacing: 0; margin-right: ${({ theme }) => theme.table.horizontalCellMargin}; table-layout: fixed; 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 = { @@ -164,97 +30,27 @@ export const RecordTable = ({ onColumnsChange, createRecord, }: RecordTableProps) => { - const { scopeId, visibleTableColumnsSelector } = - useRecordTableStates(recordTableId); + const { scopeId } = 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()); + if (!isNonEmptyString(objectNameSingular)) { + return <>; + } return ( - {!!objectNameSingular && ( - - - - - - - - )} + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx deleted file mode 100644 index e3a83b8348..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx +++ /dev/null @@ -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 ; - } - - return ( - <> - - {tableRowIds.map((recordId, rowIndex) => { - return ( - - ); - })} - - } - /> - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx deleted file mode 100644 index 508c2c8c8f..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx +++ /dev/null @@ -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 <>; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx new file mode 100644 index 0000000000..7925fb8fbf --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx @@ -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 ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx deleted file mode 100644 index 5310ae1f11..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx +++ /dev/null @@ -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 ( - - - - - - - {visibleTableColumns.map((column) => ( - - ))} - - {hiddenTableColumns.length > 0 && ( - - - - } - dropdownComponents={} - dropdownPlacement="bottom-start" - dropdownHotkeyScope={{ - scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID, - }} - /> - )} - - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx deleted file mode 100644 index 2f7112a255..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx +++ /dev/null @@ -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; -}) => ( - - {children} - -); - -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 ( - - - - - {(draggableProvided, draggableSnapshot) => ( - { - 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} - > - - - - - {!draggableSnapshot.isDragging && } - - {inView || draggableSnapshot.isDragging - ? visibleTableColumns.map((column, columnIndex) => ( - - {draggableSnapshot.isDragging && columnIndex > 0 ? null : ( - - )} - - )) - : visibleTableColumns.map((column) => ( - - ))} - - - )} - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRows.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRows.tsx new file mode 100644 index 0000000000..40db65bbb8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRows.tsx @@ -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 ( + + ); + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index 63cced18d3..2161a2fe0b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -75,6 +75,28 @@ export const RecordTableWithWrappers = ({ const isRemote = foundObjectMetadataItem?.isRemote ?? false; + const handleColumnsChange = useRecoilCallback( + () => (columns) => { + saveViewFields( + mapColumnDefinitionsToViewFields( + columns as ColumnDefinition[], + ), + ); + }, + [saveViewFields], + ); + + if (!isRecordTableInitialLoading && tableRowIds.length === 0) { + return ( + + ); + } + return ( @@ -85,16 +107,7 @@ export const RecordTableWithWrappers = ({ (columns) => { - saveViewFields( - mapColumnDefinitionsToViewFields( - columns as ColumnDefinition[], - ), - ); - }, - [saveViewFields], - )} + onColumnsChange={handleColumnsChange} createRecord={createRecord} /> - {!isRecordTableInitialLoading && - // we cannot rely on count states because this is not available for remote objects - tableRowIds.length === 0 && ( - - )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx index 487465b609..5720a2292c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx @@ -1,5 +1,5 @@ -import { useEffect } from 'react'; import { Meta, StoryObj } from '@storybook/react'; +import { useEffect } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { ComponentDecorator } from 'twenty-ui'; @@ -12,7 +12,6 @@ import { useSetRecordValue, } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; 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 { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; 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 { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; +import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper'; import { mockPerformance } from './mock'; const objectMetadataItems = getObjectMetadataItemsMock(); @@ -73,6 +73,9 @@ const meta: Meta = { onContextMenu: () => {}, onCellMouseEnter: () => {}, visibleTableColumns: mockPerformance.visibleTableColumns as any, + objectNameSingular: + mockPerformance.objectMetadataItem.nameSingular, + recordTableId: 'recordTableId', }} > ; columnIndex: number; + isInEditMode: boolean; + hasSoftFocus: boolean; + cellPosition: TableCellPosition; }; -export const RecordTableCellContext = createContext( - {} as RecordTableRowContextProps, -); +export const RecordTableCellContext = + createContext({} as RecordTableCellContextProps); diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts index 4d25606854..9bab734d90 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts @@ -26,6 +26,8 @@ export type RecordTableContextProps = { onContextMenu: (event: React.MouseEvent, recordId: string) => void; onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void; visibleTableColumns: ColumnDefinition[]; + recordTableId: string; + objectNameSingular: string; }; export const RecordTableContext = createContext( diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts index f04afe4928..d0ed1aea29 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts @@ -1,4 +1,5 @@ import { createContext } from 'react'; +import { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd'; export type RecordTableRowContextProps = { pathToShowPage: string; @@ -8,6 +9,9 @@ export type RecordTableRowContextProps = { isSelected: boolean; isReadOnly: boolean; isPendingRow?: boolean; + isDragging: boolean; + dragHandleProps: DraggableProvidedDragHandleProps | null; + inView?: boolean; }; export const RecordTableRowContext = createContext( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx new file mode 100644 index 0000000000..2a6e46c219 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx @@ -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 ; + } + + return ( + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/draggable-list/components/DraggableTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx similarity index 58% rename from packages/twenty-front/src/modules/ui/layout/draggable-list/components/DraggableTableBody.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx index c1513d828b..ae35271459 100644 --- a/packages/twenty-front/src/modules/ui/layout/draggable-list/components/DraggableTableBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx @@ -1,42 +1,30 @@ -import { useState } from 'react'; -import styled from '@emotion/styled'; -import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd'; +import { ReactNode, useContext } from 'react'; +import { DragDropContext, DropResult } from '@hello-pangea/dnd'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { v4 } from 'uuid'; 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 { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { isDefined } from '~/utils/isDefined'; -type DraggableTableBodyProps = { - draggableItems: React.ReactNode; - objectNameSingular: string; - recordTableId: string; -}; - -const StyledTbody = styled.tbody` - overflow: hidden; -`; - -export const DraggableTableBody = ({ - objectNameSingular, - draggableItems, - recordTableId, -}: DraggableTableBodyProps) => { - const [v4Persistable] = useState(v4()); - - const { tableRowIdsState } = useRecordTableStates(); - - const tableRowIds = useRecoilValue(tableRowIdsState); +export const RecordTableBodyDragDropContext = ({ + children, +}: { + children: ReactNode; +}) => { + const { objectNameSingular, recordTableId } = useContext(RecordTableContext); const { updateOneRecord: updateOneRow } = useUpdateOneRecord({ objectNameSingular, }); + const { tableRowIdsState } = useRecordTableStates(); + + const tableRowIds = useRecoilValue(tableRowIdsState); + const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(recordTableId); @@ -45,6 +33,7 @@ export const DraggableTableBody = ({ const setIsRemoveSortingModalOpenState = useSetRecoilState( isRemoveSortingModalOpenState, ); + const computeNewRowPosition = useComputeNewRowPosition(); const handleDragEnd = (result: DropResult) => { @@ -68,20 +57,6 @@ export const DraggableTableBody = ({ }; return ( - - - {(provided) => ( - - - {draggableItems} - {provided.placeholder} - - )} - - + {children} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx new file mode 100644 index 0000000000..34aba90370 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx @@ -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 ( + + {(provided) => ( + + {children} + {provided.placeholder} + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx new file mode 100644 index 0000000000..2ba8361210 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx @@ -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 <>; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyFetchMoreLoader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyFetchMoreLoader.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyLoading.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyLoading.tsx similarity index 64% rename from packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyLoading.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyLoading.tsx index 0f89384ec0..8a80403ded 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyLoading.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyLoading.tsx @@ -1,13 +1,10 @@ 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 { 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 { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr'; export const RecordTableBodyLoading = () => { const { visibleTableColumnsSelector } = useRecordTableStates(); @@ -16,22 +13,18 @@ export const RecordTableBodyLoading = () => { return ( {Array.from({ length: 8 }).map((_, rowIndex) => ( - - - - - - - + + {visibleTableColumns.map((column) => ( ))} - + ))} ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx index a38d61a39d..4d6fcdd74f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx @@ -1,109 +1,13 @@ -import { useContext } from 'react'; - 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 { 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 { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; - -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'); - }; +import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput'; +export const RecordTableCell = () => { return ( - } + editModeContent={} nonEditModeContent={} /> diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx new file mode 100644 index 0000000000..3a2a8c5c9b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx @@ -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 ( + + + {children} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx index 287df8331f..25b0e10f63 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; import { IconComponent } from 'twenty-ui'; -import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; +import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer'; const StyledButtonContainer = styled.div` margin: ${({ theme }) => theme.spacing(1)}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx similarity index 75% rename from packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx index 74af8f6ab5..a261fa2ae3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx @@ -1,9 +1,10 @@ -import { useCallback, useContext } from 'react'; import styled from '@emotion/styled'; +import { useCallback, useContext } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; 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 { Checkbox } from '@/ui/input/components/Checkbox'; import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState'; @@ -18,7 +19,9 @@ const StyledContainer = styled.div` justify-content: center; `; -export const CheckboxCell = () => { +export const RecordTableCellCheckbox = () => { + const { isSelected } = useContext(RecordTableRowContext); + const { recordId } = useContext(RecordTableRowContext); const { isRowSelectedFamilyState } = useRecordTableStates(); const setActionBarOpenState = useSetRecoilState(actionBarOpenState); @@ -31,8 +34,10 @@ export const CheckboxCell = () => { }, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]); return ( - - - + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx index a120b9ebba..0197e7e026 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx @@ -1,176 +1,41 @@ -import React, { ReactElement, useContext } from 'react'; -import { styled } from '@linaria/react'; -import { useRecoilValue } from 'recoil'; -import { BORDER_COMMON, ThemeContext } from 'twenty-ui'; +import { ReactElement, useContext } from 'react'; -import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; -import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; +import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; +import { RecordTableCellBaseContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer'; 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 { 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 = { editModeContent: ReactElement; nonEditModeContent: ReactElement; - editHotkeyScope?: HotkeyScope; transparent?: boolean; maxContentWidth?: number; onSubmit?: () => void; onCancel?: () => void; }; -const DEFAULT_CELL_SCOPE: HotkeyScope = { - scope: TableHotkeyScope.CellEditMode, -}; - export const RecordTableCellContainer = ({ editModeContent, nonEditModeContent, - editHotkeyScope, }: RecordTableCellContainerProps) => { - const { theme } = useContext(ThemeContext); - - 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; + const { hasSoftFocus, isInEditMode } = useContext(RecordTableCellContext); return ( - - - - {isInEditMode ? ( - {editModeContent} - ) : hasSoftFocus ? ( - - ) : ( - - {nonEditModeContent} - - )} - - - + + {isInEditMode ? ( + {editModeContent} + ) : hasSoftFocus ? ( + + ) : ( + + {nonEditModeContent} + + )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx index 695689ec66..20941b7c63 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx @@ -10,8 +10,6 @@ const StyledOuterContainer = styled.div<{ overflow: hidden; padding-left: 6px; width: 100%; - - margin: ${({ hasSoftFocus }) => (hasSoftFocus === true ? '-1px' : 'none')}; `; const StyledInnerContainer = styled.div` diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellFieldContextWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx similarity index 89% rename from packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellFieldContextWrapper.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx index ec30ebe03e..16a571fcd5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellFieldContextWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { ReactNode, useContext } from 'react'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; 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 { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; 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 { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -export const RecordTableCellFieldContextWrapper = () => { +export const RecordTableCellFieldContextWrapper = ({ + children, +}: { + children: ReactNode; +}) => { const { objectMetadataItem } = useContext(RecordTableContext); const { columnDefinition } = useContext(RecordTableCellContext); const { recordId, pathToShowPage } = useContext(RecordTableRowContext); @@ -49,7 +52,7 @@ export const RecordTableCellFieldContextWrapper = () => { }), }} > - + {children} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx new file mode 100644 index 0000000000..8b56f8d24b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx @@ -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 ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx new file mode 100644 index 0000000000..5636466044 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx @@ -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 ( + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellLoading.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellLoading.tsx index 12050e5b67..a8ca441b60 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellLoading.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellLoading.tsx @@ -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 { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; export const RecordTableCellLoading = () => { return ( - + - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellWrapper.tsx new file mode 100644 index 0000000000..c552fa47be --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellWrapper.tsx @@ -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; + 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 ( + + + {children} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx new file mode 100644 index 0000000000..c8f5bccdfe --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx @@ -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 ; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx new file mode 100644 index 0000000000..49f51197db --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx @@ -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 | 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 ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts index 792b33adc0..9aad45bc4e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts @@ -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 { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const recordTableRow: RecordTableRowContextProps = { @@ -10,12 +9,13 @@ export const recordTableRow: RecordTableRowContextProps = { pathToShowPage: '/', objectNameSingular: 'objectNameSingular', isReadOnly: false, + dragHandleProps: {} as any, + isDragging: false, + inView: true, + isPendingRow: false, }; -export const recordTableCell: { - columnDefinition: ColumnDefinition; - columnIndex: number; -} = { +export const recordTableCell:RecordTableCellContextProps= { columnIndex: 3, columnDefinition: { size: 1, @@ -29,4 +29,10 @@ export const recordTableCell: { fieldName: 'fieldName', }, }, + cellPosition: { + row: 2, + column: 3, + }, + hasSoftFocus: false, + isInEditMode: false, }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition.ts index d3384909cf..383a81de40 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition.ts @@ -1,21 +1,9 @@ -import { useContext, useMemo } from 'react'; +import { useContext } from 'react'; 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 = () => { - const { rowIndex } = useContext(RecordTableRowContext); - const { columnIndex } = useContext(RecordTableCellContext); + const { cellPosition } = useContext(RecordTableCellContext); - const currentTableCellPosition: TableCellPosition = useMemo( - () => ({ - column: columnIndex, - row: rowIndex, - }), - [columnIndex, rowIndex], - ); - - return currentTableCellPosition; + return cellPosition; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHead.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx similarity index 67% rename from packages/twenty-front/src/modules/object-record/record-table/components/ColumnHead.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx index 89caf5a48f..7a5c9f3cd6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHead.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx @@ -1,14 +1,14 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; import { MOBILE_VIEWPORT, useIcons } from 'twenty-ui'; 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; }; @@ -46,16 +46,22 @@ const StyledText = styled.span` white-space: nowrap; `; -export const ColumnHead = ({ column }: ColumnHeadProps) => { +export const RecordTableColumnHead = ({ + column, +}: RecordTableColumnHeadProps) => { const theme = useTheme(); const { getIcon } = useIcons(); const Icon = getIcon(column.iconName); - const scrollLeft = useRecoilValue(scrollLeftState); + const isRecordTableScrolledLeft = useRecoilComponentValue( + isRecordTableScrolledLeftComponentState, + ); return ( - 0}> + diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx similarity index 92% rename from packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx index bbd2f131c5..6065e45a11 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx @@ -14,16 +14,16 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { useTableColumns } from '../hooks/useTableColumns'; -import { ColumnDefinition } from '../types/ColumnDefinition'; +import { useTableColumns } from '../../hooks/useTableColumns'; +import { ColumnDefinition } from '../../types/ColumnDefinition'; -export type RecordTableColumnDropdownMenuProps = { +export type RecordTableColumnHeadDropdownMenuProps = { column: ColumnDefinition; }; -export const RecordTableColumnDropdownMenu = ({ +export const RecordTableColumnHeadDropdownMenu = ({ column, -}: RecordTableColumnDropdownMenuProps) => { +}: RecordTableColumnHeadDropdownMenuProps) => { const { visibleTableColumnsSelector, onToggleColumnFilterState, diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHeadWithDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx similarity index 56% rename from packages/twenty-front/src/modules/object-record/record-table/components/ColumnHeadWithDropdown.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx index 475fb4beba..d1ad7e1de4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHeadWithDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx @@ -4,26 +4,29 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; -import { ColumnHead } from './ColumnHead'; -import { RecordTableColumnDropdownMenu } from './RecordTableColumnDropdownMenu'; +import { RecordTableColumnHeadDropdownMenu } from './RecordTableColumnHeadDropdownMenu'; -type ColumnHeadWithDropdownProps = { +import { RecordTableColumnHead } from './RecordTableColumnHead'; + +type RecordTableColumnHeadWithDropdownProps = { column: ColumnDefinition; }; const StyledDropdown = styled(Dropdown)` display: flex; + flex: 1; + z-index: ${({ theme }) => theme.lastLayerZIndex}; `; -export const ColumnHeadWithDropdown = ({ +export const RecordTableColumnHeadWithDropdown = ({ column, -}: ColumnHeadWithDropdownProps) => { +}: RecordTableColumnHeadWithDropdownProps) => { return ( } - dropdownComponents={} + clickableComponent={} + dropdownComponents={} dropdownOffset={{ x: -1 }} dropdownPlacement="bottom-start" dropdownHotkeyScope={{ scope: column.fieldMetadataId + '-header' }} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx new file mode 100644 index 0000000000..49faaf868a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -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 ( + + + + + {visibleTableColumns.map((column) => ( + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx similarity index 85% rename from packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 702b644ffc..d243b2a29b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -1,27 +1,35 @@ -import { useCallback, useMemo, useState } from 'react'; import styled from '@emotion/styled'; +import { useCallback, useMemo, useState } from 'react'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { IconPlus } from 'twenty-ui'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; 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 { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; 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 { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown'; - const COLUMN_MIN_WIDTH = 104; const StyledColumnHeaderCell = styled.th<{ columnWidth: number; 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 }) => ` min-width: ${columnWidth}px; width: ${columnWidth}px; @@ -165,11 +173,14 @@ export const RecordTableHeaderCell = ({ onMouseUp: handleResizeHandlerEnd, }); + const isRecordTableScrolledLeft = useRecoilComponentValue( + isRecordTableScrolledLeftComponentState, + ); + const isMobile = useIsMobile(); - const scrollLeft = useRecoilValue(scrollLeftState); const disableColumnResize = - column.isLabelIdentifier && isMobile && scrollLeft > 0; + column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft; return ( setIconVisibility(false)} > - + {(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && ( theme.background.primary}; `; -export const SelectAllCheckbox = () => { +export const RecordTableHeaderCheckboxColumn = () => { const { allRowsSelectedStatusSelector } = useRecordTableStates(); const allRowsSelectedStatus = useRecoilValue(allRowsSelectedStatusSelector()); @@ -36,13 +36,26 @@ export const SelectAllCheckbox = () => { } }; + const theme = useTheme(); + return ( - - - + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn.tsx new file mode 100644 index 0000000000..3acf7afa9a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn.tsx @@ -0,0 +1,10 @@ +import { styled } from '@linaria/react'; + +const StyledTh = styled.th` + border-bottom: none; + border-top: none; +`; + +export const RecordTableHeaderDragDropColumn = () => { + return ; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx new file mode 100644 index 0000000000..170ec63f74 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx @@ -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 ( + + {hiddenTableColumns.length > 0 && ( + + + + } + dropdownComponents={} + dropdownPlacement="bottom-start" + dropdownHotkeyScope={{ + scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID, + }} + /> + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderPlusButtonContent.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCells.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCells.tsx new file mode 100644 index 0000000000..25c557144d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCells.tsx @@ -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 ? ( + + ) : ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsEmpty.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsEmpty.tsx new file mode 100644 index 0000000000..2df7a27f4a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsEmpty.tsx @@ -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) => ( + + )); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx new file mode 100644 index 0000000000..e6b75b2650 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx @@ -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 ( + <> + + + + + + {!isDragging && + tableColumnsAfterFirst.map((column, columnIndex) => ( + + + + + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTablePendingRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTablePendingRow.tsx similarity index 76% rename from packages/twenty-front/src/modules/object-record/record-table/components/RecordTablePendingRow.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTablePendingRow.tsx index cde96d52e1..addf2d747b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTablePendingRow.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTablePendingRow.tsx @@ -1,13 +1,13 @@ import { useRecoilValue } from 'recoil'; -import { RecordTableRow } from '@/object-record/record-table/components/RecordTableRow'; 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 = () => { const { pendingRecordIdState } = useRecordTableStates(); const pendingRecordId = useRecoilValue(pendingRecordIdState); - if (!pendingRecordId) return; + if (!pendingRecordId) return <>; return ( { + return ( + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx new file mode 100644 index 0000000000..d64f316e12 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx @@ -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 ( + + {(draggableProvided, draggableSnapshot) => ( + { + 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} + > + + {children} + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx new file mode 100644 index 0000000000..0ccfc98ac7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledLeftComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledLeftComponentState.ts new file mode 100644 index 0000000000..6f26d8a608 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledLeftComponentState.ts @@ -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({ + key: 'isRecordTableScrolledLeftComponentState', + componentContext: RecordTableScopeInternalContext, + defaultValue: true, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts new file mode 100644 index 0000000000..564c567a60 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts @@ -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({ + key: 'isRecordTableScrolledTopComponentState', + componentContext: RecordTableScopeInternalContext, + defaultValue: true, + }); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index 5964f92cc7..a7ba44db23 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -1,12 +1,13 @@ -import { useRef } from 'react'; -import { Keys } from 'react-hotkeys-hook'; import { autoUpdate, flip, + FloatingPortal, offset, Placement, useFloating, } from '@floating-ui/react'; +import { useRef } from 'react'; +import { Keys } from 'react-hotkeys-hook'; import { Key } from 'ts-key-enum'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; @@ -85,7 +86,7 @@ export const Dropdown = ({ }; useListenClickOutside({ - refs: [containerRef], + refs: [refs.floating], callback: () => { onClickOutside?.(); @@ -131,15 +132,17 @@ export const Dropdown = ({ /> )} {isDropdownOpen && ( - - {dropdownComponents} - + + + {dropdownComponents} + + )} ( - {children} - + ); diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentValue.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentValue.ts new file mode 100644 index 0000000000..d72ee3cae3 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentValue.ts @@ -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 = ( + componentState: ComponentState, + 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 }), + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useSetRecoilComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useSetRecoilComponentState.ts new file mode 100644 index 0000000000..938d699b3f --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useSetRecoilComponentState.ts @@ -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 = ( + componentState: ComponentState, + 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 }), + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/types/ComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/types/ComponentState.ts new file mode 100644 index 0000000000..8fc43e8a29 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/types/ComponentState.ts @@ -0,0 +1,8 @@ +import { RecoilState } from 'recoil'; + +import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey'; + +export type ComponentState = { + key: string; + atomFamily: (componentStateKey: ComponentStateKey) => RecoilState; +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentState.ts index b72932d862..ffbd9dd7eb 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentState.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentState.ts @@ -2,15 +2,17 @@ import { AtomEffect, atomFamily } from 'recoil'; import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey'; +type CreateComponentStateType = { + key: string; + defaultValue: ValueType; + effects?: AtomEffect[]; +}; + export const createComponentState = ({ key, defaultValue, effects, -}: { - key: string; - defaultValue: ValueType; - effects?: AtomEffect[]; -}) => { +}: CreateComponentStateType) => { return atomFamily({ key, default: defaultValue, diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2.ts new file mode 100644 index 0000000000..1fda1db821 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2.ts @@ -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 = { + key: string; + defaultValue: ValueType; + componentContext?: ScopeInternalContext | null; + effects?: AtomEffect[]; +}; + +export const createComponentStateV2 = ({ + key, + defaultValue, + componentContext, + effects, +}: CreateComponentStateV2Type): ComponentState => { + 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({ + key, + default: defaultValue, + effects: effects, + }), + }; +}; diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index 08534bf0a1..705c2dcc51 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -67,6 +67,11 @@ export default defineConfig(({ command, mode }) => { '**/RecordTableCellContainer.tsx', '**/RecordTableCellDisplayContainer.tsx', '**/Avatar.tsx', + '**/RecordTableBodyDroppable.tsx', + '**/RecordTableCellBaseContainer.tsx', + '**/RecordTableCellTd.tsx', + '**/RecordTableTd.tsx', + '**/RecordTableHeaderDragDropColumn.tsx', ], babelOptions: { presets: ['@babel/preset-typescript', '@babel/preset-react'],