mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 20:27:55 +00:00 
			
		
		
		
	5421 box shadow on frozen header and first column (#6130)
- Refactored components in table - Added a isTableRecordScrolledLeftState and isTableRecordScrolledTopState to subscribe to table scroll - Added a zIndex logic that subscribes to those new states in new tinier components --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
		| @@ -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', | ||||
|   | ||||
							
								
								
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -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]": { | ||||
|   | ||||
| @@ -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 ?? '', | ||||
|   | ||||
| @@ -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; | ||||
| @@ -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} | ||||
|       /> | ||||
|       <RecordTableActionBar recordTableId={recordTableId} /> | ||||
|       <RemoveSortingModal recordTableId={recordTableId} /> | ||||
|       <RecordIndexRemoveSortingModal recordTableId={recordTableId} /> | ||||
|       <RecordTableContextMenu recordTableId={recordTableId} /> | ||||
|     </> | ||||
|   ); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -1,29 +0,0 @@ | ||||
| import styled from '@emotion/styled'; | ||||
|  | ||||
| import { IconListViewGrip } from '@/ui/input/components/IconListViewGrip'; | ||||
|  | ||||
| const StyledContainer = styled.div` | ||||
|   cursor: grab; | ||||
|   width: 16px; | ||||
|   height: 32px; | ||||
|   z-index: 200; | ||||
|   display: flex; | ||||
|   &:hover .icon { | ||||
|     opacity: 1; | ||||
|   } | ||||
| `; | ||||
|  | ||||
| const StyledIconWrapper = styled.div<{ isDragging: boolean }>` | ||||
|   opacity: ${({ isDragging }) => (isDragging ? 1 : 0)}; | ||||
|   transition: opacity 0.1s; | ||||
| `; | ||||
|  | ||||
| export const GripCell = ({ isDragging }: { isDragging: boolean }) => { | ||||
|   return ( | ||||
|     <StyledContainer> | ||||
|       <StyledIconWrapper className="icon" isDragging={isDragging}> | ||||
|         <IconListViewGrip /> | ||||
|       </StyledIconWrapper> | ||||
|     </StyledContainer> | ||||
|   ); | ||||
| }; | ||||
| @@ -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 ( | ||||
|     <RecordTableScope | ||||
|       recordTableScopeId={scopeId} | ||||
|       onColumnsChange={onColumnsChange} | ||||
|     > | ||||
|       {!!objectNameSingular && ( | ||||
|         <RecordTableContext.Provider | ||||
|           value={{ | ||||
|             objectMetadataItem, | ||||
|             onUpsertRecord: handleUpsertRecord, | ||||
|             onOpenTableCell: handleOpenTableCell, | ||||
|             onMoveFocus: handleMoveFocus, | ||||
|             onCloseTableCell: handleCloseTableCell, | ||||
|             onMoveSoftFocusToCell: handleMoveSoftFocusToCell, | ||||
|             onContextMenu: handleContextMenu, | ||||
|             onCellMouseEnter: handleContainerMouseEnter, | ||||
|             visibleTableColumns, | ||||
|           }} | ||||
|         > | ||||
|           <StyledTable className="entity-table-cell"> | ||||
|             <RecordTableHeader createRecord={createRecord} /> | ||||
|             <RecordTableBodyEffect objectNameSingular={objectNameSingular} /> | ||||
|             <RecordTableBody | ||||
|               objectNameSingular={objectNameSingular} | ||||
|               recordTableId={recordTableId} | ||||
|             /> | ||||
|           </StyledTable> | ||||
|         </RecordTableContext.Provider> | ||||
|       )} | ||||
|       <RecordTableContextProvider | ||||
|         objectNameSingular={objectNameSingular} | ||||
|         recordTableId={recordTableId} | ||||
|       > | ||||
|         <StyledTable className="entity-table-cell"> | ||||
|           <RecordTableHeader createRecord={createRecord} /> | ||||
|           <RecordTableBodyEffect /> | ||||
|           <RecordTableBody /> | ||||
|         </StyledTable> | ||||
|       </RecordTableContextProvider> | ||||
|     </RecordTableScope> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -1,53 +0,0 @@ | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/components/RecordTableBodyFetchMoreLoader'; | ||||
| import { RecordTableBodyLoading } from '@/object-record/record-table/components/RecordTableBodyLoading'; | ||||
| import { RecordTableRow } from '@/object-record/record-table/components/RecordTableRow'; | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { DraggableTableBody } from '@/ui/layout/draggable-list/components/DraggableTableBody'; | ||||
|  | ||||
| type RecordTableBodyProps = { | ||||
|   objectNameSingular: string; | ||||
|   recordTableId: string; | ||||
| }; | ||||
|  | ||||
| export const RecordTableBody = ({ | ||||
|   objectNameSingular, | ||||
|   recordTableId, | ||||
| }: RecordTableBodyProps) => { | ||||
|   const { tableRowIdsState, isRecordTableInitialLoadingState } = | ||||
|     useRecordTableStates(); | ||||
|  | ||||
|   const tableRowIds = useRecoilValue(tableRowIdsState); | ||||
|  | ||||
|   const isRecordTableInitialLoading = useRecoilValue( | ||||
|     isRecordTableInitialLoadingState, | ||||
|   ); | ||||
|  | ||||
|   if (isRecordTableInitialLoading && tableRowIds.length === 0) { | ||||
|     return <RecordTableBodyLoading />; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DraggableTableBody | ||||
|         objectNameSingular={objectNameSingular} | ||||
|         recordTableId={recordTableId} | ||||
|         draggableItems={ | ||||
|           <> | ||||
|             {tableRowIds.map((recordId, rowIndex) => { | ||||
|               return ( | ||||
|                 <RecordTableRow | ||||
|                   key={recordId} | ||||
|                   recordId={recordId} | ||||
|                   rowIndex={rowIndex} | ||||
|                 /> | ||||
|               ); | ||||
|             })} | ||||
|           </> | ||||
|         } | ||||
|       /> | ||||
|       <RecordTableBodyFetchMoreLoader objectNameSingular={objectNameSingular} /> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -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 <></>; | ||||
| }; | ||||
| @@ -0,0 +1,109 @@ | ||||
| import { ReactNode } from 'react'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||
| import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; | ||||
| import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; | ||||
| import { useCloseRecordTableCellV2 } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2'; | ||||
| import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2'; | ||||
| import { | ||||
|   OpenTableCellArgs, | ||||
|   useOpenRecordTableCellV2, | ||||
| } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; | ||||
| import { useTriggerContextMenu } from '@/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu'; | ||||
| import { useUpsertRecordV2 } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecordV2'; | ||||
| import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; | ||||
| import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; | ||||
|  | ||||
| export const RecordTableContextProvider = ({ | ||||
|   recordTableId, | ||||
|   objectNameSingular, | ||||
|   children, | ||||
| }: { | ||||
|   recordTableId: string; | ||||
|   objectNameSingular: string; | ||||
|   children: ReactNode; | ||||
| }) => { | ||||
|   const { visibleTableColumnsSelector } = useRecordTableStates(recordTableId); | ||||
|  | ||||
|   const { objectMetadataItem } = useObjectMetadataItem({ | ||||
|     objectNameSingular, | ||||
|   }); | ||||
|  | ||||
|   const { upsertRecord } = useUpsertRecordV2({ | ||||
|     objectNameSingular, | ||||
|   }); | ||||
|  | ||||
|   const handleUpsertRecord = ({ | ||||
|     persistField, | ||||
|     entityId, | ||||
|     fieldName, | ||||
|   }: { | ||||
|     persistField: () => void; | ||||
|     entityId: string; | ||||
|     fieldName: string; | ||||
|   }) => { | ||||
|     upsertRecord(persistField, entityId, fieldName, recordTableId); | ||||
|   }; | ||||
|  | ||||
|   const { openTableCell } = useOpenRecordTableCellV2(recordTableId); | ||||
|  | ||||
|   const handleOpenTableCell = (args: OpenTableCellArgs) => { | ||||
|     openTableCell(args); | ||||
|   }; | ||||
|  | ||||
|   const { moveFocus } = useRecordTableMoveFocus(recordTableId); | ||||
|  | ||||
|   const handleMoveFocus = (direction: MoveFocusDirection) => { | ||||
|     moveFocus(direction); | ||||
|   }; | ||||
|  | ||||
|   const { closeTableCell } = useCloseRecordTableCellV2(recordTableId); | ||||
|  | ||||
|   const handleCloseTableCell = () => { | ||||
|     closeTableCell(); | ||||
|   }; | ||||
|  | ||||
|   const { moveSoftFocusToCell } = | ||||
|     useMoveSoftFocusToCellOnHoverV2(recordTableId); | ||||
|  | ||||
|   const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => { | ||||
|     moveSoftFocusToCell(cellPosition); | ||||
|   }; | ||||
|  | ||||
|   const { triggerContextMenu } = useTriggerContextMenu({ | ||||
|     recordTableId, | ||||
|   }); | ||||
|  | ||||
|   const handleContextMenu = (event: React.MouseEvent, recordId: string) => { | ||||
|     triggerContextMenu(event, recordId); | ||||
|   }; | ||||
|  | ||||
|   const { handleContainerMouseEnter } = useHandleContainerMouseEnter({ | ||||
|     recordTableId, | ||||
|   }); | ||||
|  | ||||
|   const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); | ||||
|  | ||||
|   return ( | ||||
|     <RecordTableContext.Provider | ||||
|       value={{ | ||||
|         objectMetadataItem, | ||||
|         onUpsertRecord: handleUpsertRecord, | ||||
|         onOpenTableCell: handleOpenTableCell, | ||||
|         onMoveFocus: handleMoveFocus, | ||||
|         onCloseTableCell: handleCloseTableCell, | ||||
|         onMoveSoftFocusToCell: handleMoveSoftFocusToCell, | ||||
|         onContextMenu: handleContextMenu, | ||||
|         onCellMouseEnter: handleContainerMouseEnter, | ||||
|         visibleTableColumns, | ||||
|         recordTableId, | ||||
|         objectNameSingular, | ||||
|       }} | ||||
|     > | ||||
|       {children} | ||||
|     </RecordTableContext.Provider> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,114 +0,0 @@ | ||||
| import { useTheme } from '@emotion/react'; | ||||
| import styled from '@emotion/styled'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| import { IconPlus } from 'twenty-ui'; | ||||
|  | ||||
| import { RecordTableHeaderCell } from '@/object-record/record-table/components/RecordTableHeaderCell'; | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; | ||||
| import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef'; | ||||
|  | ||||
| import { RecordTableHeaderPlusButtonContent } from './RecordTableHeaderPlusButtonContent'; | ||||
| import { SelectAllCheckbox } from './SelectAllCheckbox'; | ||||
|  | ||||
| const StyledTableHead = styled.thead` | ||||
|   cursor: pointer; | ||||
| `; | ||||
|  | ||||
| const StyledPlusIconHeaderCell = styled.th<{ isTableWiderThanScreen: boolean }>` | ||||
|   ${({ theme }) => { | ||||
|     return ` | ||||
|   &:hover { | ||||
|     background: ${theme.background.transparent.light}; | ||||
|   }; | ||||
|   padding-left: ${theme.spacing(3)}; | ||||
|   `; | ||||
|   }}; | ||||
|   border-left: none !important; | ||||
|   min-width: 32px; | ||||
|   ${({ isTableWiderThanScreen, theme }) => | ||||
|     isTableWiderThanScreen && | ||||
|     ` | ||||
|     width: 32px; | ||||
|     border-right: none !important; | ||||
|     background-color: ${theme.background.primary}; | ||||
|     `}; | ||||
|   z-index: 1; | ||||
| `; | ||||
|  | ||||
| const StyledPlusIconContainer = styled.div` | ||||
|   align-items: center; | ||||
|   display: flex; | ||||
|   height: 32px; | ||||
|   justify-content: center; | ||||
|   width: 32px; | ||||
| `; | ||||
|  | ||||
| export const HIDDEN_TABLE_COLUMN_DROPDOWN_ID = | ||||
|   'hidden-table-columns-dropdown-scope-id'; | ||||
|  | ||||
| const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID = | ||||
|   'hidden-table-columns-dropdown-hotkey-scope-id'; | ||||
|  | ||||
| export const RecordTableHeader = ({ | ||||
|   createRecord, | ||||
| }: { | ||||
|   createRecord: () => void; | ||||
| }) => { | ||||
|   const { visibleTableColumnsSelector, hiddenTableColumnsSelector } = | ||||
|     useRecordTableStates(); | ||||
|  | ||||
|   const scrollWrapper = useScrollWrapperScopedRef(); | ||||
|   const isTableWiderThanScreen = | ||||
|     (scrollWrapper.current?.clientWidth ?? 0) < | ||||
|     (scrollWrapper.current?.scrollWidth ?? 0); | ||||
|  | ||||
|   const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); | ||||
|   const hiddenTableColumns = useRecoilValue(hiddenTableColumnsSelector()); | ||||
|  | ||||
|   const theme = useTheme(); | ||||
|  | ||||
|   return ( | ||||
|     <StyledTableHead data-select-disable> | ||||
|       <tr> | ||||
|         <th></th> | ||||
|         <th | ||||
|           style={{ | ||||
|             width: 30, | ||||
|             minWidth: 30, | ||||
|             maxWidth: 30, | ||||
|             borderRight: 'transparent', | ||||
|           }} | ||||
|         > | ||||
|           <SelectAllCheckbox /> | ||||
|         </th> | ||||
|         {visibleTableColumns.map((column) => ( | ||||
|           <RecordTableHeaderCell | ||||
|             key={column.fieldMetadataId} | ||||
|             column={column} | ||||
|             createRecord={createRecord} | ||||
|           /> | ||||
|         ))} | ||||
|         <StyledPlusIconHeaderCell | ||||
|           isTableWiderThanScreen={isTableWiderThanScreen} | ||||
|         > | ||||
|           {hiddenTableColumns.length > 0 && ( | ||||
|             <Dropdown | ||||
|               dropdownId={HIDDEN_TABLE_COLUMN_DROPDOWN_ID} | ||||
|               clickableComponent={ | ||||
|                 <StyledPlusIconContainer> | ||||
|                   <IconPlus size={theme.icon.size.md} /> | ||||
|                 </StyledPlusIconContainer> | ||||
|               } | ||||
|               dropdownComponents={<RecordTableHeaderPlusButtonContent />} | ||||
|               dropdownPlacement="bottom-start" | ||||
|               dropdownHotkeyScope={{ | ||||
|                 scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID, | ||||
|               }} | ||||
|             /> | ||||
|           )} | ||||
|         </StyledPlusIconHeaderCell> | ||||
|       </tr> | ||||
|     </StyledTableHead> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,183 +0,0 @@ | ||||
| import { useContext } from 'react'; | ||||
| import { useInView } from 'react-intersection-observer'; | ||||
| import { useTheme } from '@emotion/react'; | ||||
| import styled from '@emotion/styled'; | ||||
| import { Draggable } from '@hello-pangea/dnd'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; | ||||
| import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; | ||||
| import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/components/RecordTableCellFieldContextWrapper'; | ||||
| import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; | ||||
| import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper'; | ||||
|  | ||||
| import { CheckboxCell } from './CheckboxCell'; | ||||
| import { GripCell } from './GripCell'; | ||||
|  | ||||
| type RecordTableRowProps = { | ||||
|   recordId: string; | ||||
|   rowIndex: number; | ||||
|   isPendingRow?: boolean; | ||||
| }; | ||||
|  | ||||
| export const StyledTd = styled.td<{ isSelected?: boolean }>` | ||||
|   background: ${({ theme }) => theme.background.primary}; | ||||
|   position: relative; | ||||
|   user-select: none; | ||||
|  | ||||
|   ${({ isSelected, theme }) => | ||||
|     isSelected && | ||||
|     ` | ||||
|     background: ${theme.accent.quaternary}; | ||||
|  | ||||
|   `} | ||||
| `; | ||||
|  | ||||
| export const StyledTr = styled.tr<{ isDragging: boolean }>` | ||||
|   border: 1px solid transparent; | ||||
|   transition: border-left-color 0.2s ease-in-out; | ||||
|  | ||||
|   td:nth-of-type(-n + 2) { | ||||
|     border-right-color: ${({ theme }) => theme.background.primary}; | ||||
|   } | ||||
|  | ||||
|   ${({ isDragging }) => | ||||
|     isDragging && | ||||
|     ` | ||||
|     td:nth-of-type(1) { | ||||
|       background-color: transparent; | ||||
|       border-color: transparent; | ||||
|     } | ||||
|  | ||||
|     td:nth-of-type(2) { | ||||
|       background-color: transparent; | ||||
|       border-color: transparent; | ||||
|     } | ||||
|  | ||||
|     td:nth-of-type(3) { | ||||
|       background-color: transparent; | ||||
|       border-color: transparent; | ||||
|     } | ||||
|  | ||||
|   `} | ||||
| `; | ||||
|  | ||||
| const SelectableStyledTd = ({ | ||||
|   isSelected, | ||||
|   children, | ||||
|   style, | ||||
| }: { | ||||
|   isSelected: boolean; | ||||
|   children?: React.ReactNode; | ||||
|   style?: React.CSSProperties; | ||||
| }) => ( | ||||
|   <StyledTd isSelected={isSelected} style={style}> | ||||
|     {children} | ||||
|   </StyledTd> | ||||
| ); | ||||
|  | ||||
| export const RecordTableRow = ({ | ||||
|   recordId, | ||||
|   rowIndex, | ||||
|   isPendingRow, | ||||
| }: RecordTableRowProps) => { | ||||
|   const { visibleTableColumnsSelector, isRowSelectedFamilyState } = | ||||
|     useRecordTableStates(); | ||||
|   const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId)); | ||||
|   const { objectMetadataItem } = useContext(RecordTableContext); | ||||
|  | ||||
|   const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); | ||||
|  | ||||
|   const scrollWrapperRef = useContext(ScrollWrapperContext); | ||||
|  | ||||
|   const { ref: elementRef, inView } = useInView({ | ||||
|     root: scrollWrapperRef.current?.querySelector( | ||||
|       '[data-overlayscrollbars-viewport="scrollbarHidden"]', | ||||
|     ), | ||||
|     rootMargin: '1000px', | ||||
|   }); | ||||
|  | ||||
|   const theme = useTheme(); | ||||
|  | ||||
|   return ( | ||||
|     <RecordTableRowContext.Provider | ||||
|       value={{ | ||||
|         recordId, | ||||
|         rowIndex, | ||||
|         pathToShowPage: | ||||
|           getBasePathToShowPage({ | ||||
|             objectNameSingular: objectMetadataItem.nameSingular, | ||||
|           }) + recordId, | ||||
|         objectNameSingular: objectMetadataItem.nameSingular, | ||||
|         isSelected: currentRowSelected, | ||||
|         isReadOnly: objectMetadataItem.isRemote ?? false, | ||||
|         isPendingRow, | ||||
|       }} | ||||
|     > | ||||
|       <RecordValueSetterEffect recordId={recordId} /> | ||||
|  | ||||
|       <Draggable key={recordId} draggableId={recordId} index={rowIndex}> | ||||
|         {(draggableProvided, draggableSnapshot) => ( | ||||
|           <StyledTr | ||||
|             ref={(node) => { | ||||
|               elementRef(node); | ||||
|               draggableProvided.innerRef(node); | ||||
|             }} | ||||
|             // eslint-disable-next-line react/jsx-props-no-spreading | ||||
|             {...draggableProvided.draggableProps} | ||||
|             style={{ | ||||
|               ...draggableProvided.draggableProps.style, | ||||
|               background: draggableSnapshot.isDragging | ||||
|                 ? theme.background.transparent.light | ||||
|                 : 'none', | ||||
|               borderColor: draggableSnapshot.isDragging | ||||
|                 ? `${theme.border.color.medium}` | ||||
|                 : 'transparent', | ||||
|             }} | ||||
|             isDragging={draggableSnapshot.isDragging} | ||||
|             data-testid={`row-id-${recordId}`} | ||||
|             data-selectable-id={recordId} | ||||
|           > | ||||
|             <StyledTd | ||||
|               // eslint-disable-next-line react/jsx-props-no-spreading | ||||
|               {...draggableProvided.dragHandleProps} | ||||
|               data-select-disable | ||||
|             > | ||||
|               <GripCell isDragging={draggableSnapshot.isDragging} /> | ||||
|             </StyledTd> | ||||
|             <SelectableStyledTd | ||||
|               isSelected={currentRowSelected} | ||||
|               style={{ borderRight: 'transparent' }} | ||||
|             > | ||||
|               {!draggableSnapshot.isDragging && <CheckboxCell />} | ||||
|             </SelectableStyledTd> | ||||
|             {inView || draggableSnapshot.isDragging | ||||
|               ? visibleTableColumns.map((column, columnIndex) => ( | ||||
|                   <RecordTableCellContext.Provider | ||||
|                     value={{ | ||||
|                       columnDefinition: column, | ||||
|                       columnIndex, | ||||
|                     }} | ||||
|                     key={column.fieldMetadataId} | ||||
|                   > | ||||
|                     {draggableSnapshot.isDragging && columnIndex > 0 ? null : ( | ||||
|                       <RecordTableCellFieldContextWrapper /> | ||||
|                     )} | ||||
|                   </RecordTableCellContext.Provider> | ||||
|                 )) | ||||
|               : visibleTableColumns.map((column) => ( | ||||
|                   <StyledTd | ||||
|                     isSelected={currentRowSelected} | ||||
|                     key={column.fieldMetadataId} | ||||
|                   ></StyledTd> | ||||
|                 ))} | ||||
|             <SelectableStyledTd isSelected={currentRowSelected} /> | ||||
|           </StyledTr> | ||||
|         )} | ||||
|       </Draggable> | ||||
|     </RecordTableRowContext.Provider> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,16 @@ | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; | ||||
|  | ||||
| export const RecordTableRows = () => { | ||||
|   const { tableRowIdsState } = useRecordTableStates(); | ||||
|  | ||||
|   const tableRowIds = useRecoilValue(tableRowIdsState); | ||||
|  | ||||
|   return tableRowIds.map((recordId, rowIndex) => { | ||||
|     return ( | ||||
|       <RecordTableRow key={recordId} recordId={recordId} rowIndex={rowIndex} /> | ||||
|     ); | ||||
|   }); | ||||
| }; | ||||
| @@ -75,6 +75,28 @@ export const RecordTableWithWrappers = ({ | ||||
|  | ||||
|   const isRemote = foundObjectMetadataItem?.isRemote ?? false; | ||||
|  | ||||
|   const handleColumnsChange = useRecoilCallback( | ||||
|     () => (columns) => { | ||||
|       saveViewFields( | ||||
|         mapColumnDefinitionsToViewFields( | ||||
|           columns as ColumnDefinition<FieldMetadata>[], | ||||
|         ), | ||||
|       ); | ||||
|     }, | ||||
|     [saveViewFields], | ||||
|   ); | ||||
|  | ||||
|   if (!isRecordTableInitialLoading && tableRowIds.length === 0) { | ||||
|     return ( | ||||
|       <RecordTableEmptyState | ||||
|         objectNameSingular={objectNameSingular} | ||||
|         objectLabel={objectLabel} | ||||
|         createRecord={createRecord} | ||||
|         isRemote={isRemote} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <EntityDeleteContext.Provider value={deleteOneRecord}> | ||||
|       <ScrollWrapper> | ||||
| @@ -85,16 +107,7 @@ export const RecordTableWithWrappers = ({ | ||||
|                 <RecordTable | ||||
|                   recordTableId={recordTableId} | ||||
|                   objectNameSingular={objectNameSingular} | ||||
|                   onColumnsChange={useRecoilCallback( | ||||
|                     () => (columns) => { | ||||
|                       saveViewFields( | ||||
|                         mapColumnDefinitionsToViewFields( | ||||
|                           columns as ColumnDefinition<FieldMetadata>[], | ||||
|                         ), | ||||
|                       ); | ||||
|                     }, | ||||
|                     [saveViewFields], | ||||
|                   )} | ||||
|                   onColumnsChange={handleColumnsChange} | ||||
|                   createRecord={createRecord} | ||||
|                 /> | ||||
|                 <DragSelect | ||||
| @@ -107,16 +120,6 @@ export const RecordTableWithWrappers = ({ | ||||
|                 recordTableId={recordTableId} | ||||
|                 tableBodyRef={tableBodyRef} | ||||
|               /> | ||||
|               {!isRecordTableInitialLoading && | ||||
|                 // we cannot rely on count states because this is not available for remote objects | ||||
|                 tableRowIds.length === 0 && ( | ||||
|                   <RecordTableEmptyState | ||||
|                     objectNameSingular={objectNameSingular} | ||||
|                     objectLabel={objectLabel} | ||||
|                     createRecord={createRecord} | ||||
|                     isRemote={isRemote} | ||||
|                   /> | ||||
|                 )} | ||||
|             </StyledTableContainer> | ||||
|           </StyledTableWithHeader> | ||||
|         </RecordUpdateContext.Provider> | ||||
|   | ||||
| @@ -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', | ||||
|             }} | ||||
|           > | ||||
|             <RecordTableScope | ||||
| @@ -92,12 +95,19 @@ const meta: Meta = { | ||||
|                     }) + mockPerformance.entityId, | ||||
|                   isSelected: false, | ||||
|                   isReadOnly: false, | ||||
|                   isDragging: false, | ||||
|                   dragHandleProps: null, | ||||
|                   inView: true, | ||||
|                   isPendingRow: false, | ||||
|                 }} | ||||
|               > | ||||
|                 <RecordTableCellContext.Provider | ||||
|                   value={{ | ||||
|                     columnDefinition: mockPerformance.fieldDefinition, | ||||
|                     columnIndex: 0, | ||||
|                     cellPosition: { row: 0, column: 0 }, | ||||
|                     hasSoftFocus: false, | ||||
|                     isInEditMode: false, | ||||
|                   }} | ||||
|                 > | ||||
|                   <FieldContext.Provider | ||||
|   | ||||
| @@ -0,0 +1,2 @@ | ||||
| export const HIDDEN_TABLE_COLUMN_DROPDOWN_ID = | ||||
|   'hidden-table-columns-dropdown-scope-id'; | ||||
| @@ -2,12 +2,15 @@ import { createContext } from 'react'; | ||||
|  | ||||
| import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; | ||||
| import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; | ||||
| import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; | ||||
|  | ||||
| type RecordTableRowContextProps = { | ||||
| export type RecordTableCellContextProps = { | ||||
|   columnDefinition: ColumnDefinition<FieldMetadata>; | ||||
|   columnIndex: number; | ||||
|   isInEditMode: boolean; | ||||
|   hasSoftFocus: boolean; | ||||
|   cellPosition: TableCellPosition; | ||||
| }; | ||||
|  | ||||
| export const RecordTableCellContext = createContext<RecordTableRowContextProps>( | ||||
|   {} as RecordTableRowContextProps, | ||||
| ); | ||||
| export const RecordTableCellContext = | ||||
|   createContext<RecordTableCellContextProps>({} as RecordTableCellContextProps); | ||||
|   | ||||
| @@ -26,6 +26,8 @@ export type RecordTableContextProps = { | ||||
|   onContextMenu: (event: React.MouseEvent, recordId: string) => void; | ||||
|   onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void; | ||||
|   visibleTableColumns: ColumnDefinition<FieldMetadata>[]; | ||||
|   recordTableId: string; | ||||
|   objectNameSingular: string; | ||||
| }; | ||||
|  | ||||
| export const RecordTableContext = createContext<RecordTableContextProps>( | ||||
|   | ||||
| @@ -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<RecordTableRowContextProps>( | ||||
|   | ||||
| @@ -0,0 +1,38 @@ | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { RecordTableRows } from '@/object-record/record-table/components/RecordTableRows'; | ||||
| import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { RecordTableBodyDragDropContext } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext'; | ||||
| import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; | ||||
| import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader'; | ||||
| import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; | ||||
| import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow'; | ||||
| import { useContext } from 'react'; | ||||
|  | ||||
| export const RecordTableBody = () => { | ||||
|   const { tableRowIdsState, isRecordTableInitialLoadingState } = | ||||
|     useRecordTableStates(); | ||||
|  | ||||
|   const { objectNameSingular } = useContext(RecordTableContext); | ||||
|  | ||||
|   const tableRowIds = useRecoilValue(tableRowIdsState); | ||||
|  | ||||
|   const isRecordTableInitialLoading = useRecoilValue( | ||||
|     isRecordTableInitialLoadingState, | ||||
|   ); | ||||
|  | ||||
|   if (isRecordTableInitialLoading && tableRowIds.length === 0) { | ||||
|     return <RecordTableBodyLoading />; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <RecordTableBodyDragDropContext> | ||||
|       <RecordTableBodyDroppable> | ||||
|         <RecordTablePendingRow /> | ||||
|         <RecordTableRows /> | ||||
|       </RecordTableBodyDroppable> | ||||
|       <RecordTableBodyFetchMoreLoader objectNameSingular={objectNameSingular} /> | ||||
|     </RecordTableBodyDragDropContext> | ||||
|   ); | ||||
| }; | ||||
| @@ -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 ( | ||||
|     <DragDropContext onDragEnd={handleDragEnd}> | ||||
|       <Droppable droppableId={v4Persistable}> | ||||
|         {(provided) => ( | ||||
|           <StyledTbody | ||||
|             ref={provided.innerRef} | ||||
|             // eslint-disable-next-line react/jsx-props-no-spreading
 | ||||
|             {...provided.droppableProps} | ||||
|           > | ||||
|             <RecordTablePendingRow /> | ||||
|             {draggableItems} | ||||
|             {provided.placeholder} | ||||
|           </StyledTbody> | ||||
|         )} | ||||
|       </Droppable> | ||||
|     </DragDropContext> | ||||
|     <DragDropContext onDragEnd={handleDragEnd}>{children}</DragDropContext> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,57 @@ | ||||
| import { Theme } from '@emotion/react'; | ||||
| import { Droppable } from '@hello-pangea/dnd'; | ||||
| import { styled } from '@linaria/react'; | ||||
| import { ReactNode, useContext, useState } from 'react'; | ||||
| import { ThemeContext } from 'twenty-ui'; | ||||
| import { v4 } from 'uuid'; | ||||
|  | ||||
| const StyledTbody = styled.tbody<{ | ||||
|   theme: Theme; | ||||
| }>` | ||||
|   overflow: hidden; | ||||
|  | ||||
|   &.first-columns-sticky { | ||||
|     td:nth-child(1) { | ||||
|       position: sticky; | ||||
|       left: 0; | ||||
|       z-index: 5; | ||||
|     } | ||||
|     td:nth-child(2) { | ||||
|       position: sticky; | ||||
|       left: 9px; | ||||
|       z-index: 5; | ||||
|     } | ||||
|     td:nth-child(3) { | ||||
|       position: sticky; | ||||
|       left: 39px; | ||||
|       z-index: 5; | ||||
|     } | ||||
|   } | ||||
| `; | ||||
|  | ||||
| export const RecordTableBodyDroppable = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: ReactNode; | ||||
| }) => { | ||||
|   const [v4Persistable] = useState(v4()); | ||||
|  | ||||
|   const { theme } = useContext(ThemeContext); | ||||
|  | ||||
|   return ( | ||||
|     <Droppable droppableId={v4Persistable}> | ||||
|       {(provided) => ( | ||||
|         <StyledTbody | ||||
|           id="record-table-body" | ||||
|           theme={theme} | ||||
|           ref={provided.innerRef} | ||||
|           // eslint-disable-next-line react/jsx-props-no-spreading | ||||
|           {...provided.droppableProps} | ||||
|         > | ||||
|           {children} | ||||
|           {provided.placeholder} | ||||
|         </StyledTbody> | ||||
|       )} | ||||
|     </Droppable> | ||||
|   ); | ||||
| }; | ||||
| @@ -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 <></>; | ||||
| }; | ||||
| @@ -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 ( | ||||
|     <tbody> | ||||
|       {Array.from({ length: 8 }).map((_, rowIndex) => ( | ||||
|         <StyledTr | ||||
|         <RecordTableTr | ||||
|           isDragging={false} | ||||
|           data-testid={`row-id-${rowIndex}`} | ||||
|           data-selectable-id={`row-id-${rowIndex}`} | ||||
|           key={rowIndex} | ||||
|         > | ||||
|           <StyledTd data-select-disable> | ||||
|             <GripCell isDragging={false} /> | ||||
|           </StyledTd> | ||||
|           <StyledTd> | ||||
|             <CheckboxCell /> | ||||
|           </StyledTd> | ||||
|           <RecordTableCellGrip /> | ||||
|           <RecordTableCellCheckbox /> | ||||
|           {visibleTableColumns.map((column) => ( | ||||
|             <RecordTableCellLoading key={column.fieldMetadataId} /> | ||||
|           ))} | ||||
|         </StyledTr> | ||||
|         </RecordTableTr> | ||||
|       ))} | ||||
|     </tbody> | ||||
|   ); | ||||
| @@ -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 ( | ||||
|     <FieldFocusContextProvider> | ||||
|       <RecordTableCellContainer | ||||
|         editHotkeyScope={customHotkeyScope} | ||||
|         editModeContent={ | ||||
|           <FieldInput | ||||
|             recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`} | ||||
|             onCancel={handleCancel} | ||||
|             onClickOutside={handleClickOutside} | ||||
|             onEnter={handleEnter} | ||||
|             onEscape={handleEscape} | ||||
|             onShiftTab={handleShiftTab} | ||||
|             onSubmit={handleSubmit} | ||||
|             onTab={handleTab} | ||||
|             isReadOnly={isReadOnly} | ||||
|           /> | ||||
|         } | ||||
|         editModeContent={<RecordTableCellFieldInput />} | ||||
|         nonEditModeContent={<FieldDisplay />} | ||||
|       /> | ||||
|     </FieldFocusContextProvider> | ||||
|   | ||||
| @@ -0,0 +1,101 @@ | ||||
| import { ReactNode, useContext } from 'react'; | ||||
| import { styled } from '@linaria/react'; | ||||
| import { BORDER_COMMON, ThemeContext } from 'twenty-ui'; | ||||
|  | ||||
| import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; | ||||
| import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; | ||||
| import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext'; | ||||
| import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; | ||||
| import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
| import { | ||||
|   DEFAULT_CELL_SCOPE, | ||||
|   useOpenRecordTableCellFromCell, | ||||
| } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell'; | ||||
|  | ||||
| const StyledBaseContainer = styled.div<{ | ||||
|   hasSoftFocus: boolean; | ||||
|   fontColorExtraLight: string; | ||||
|   backgroundColorTransparentSecondary: string; | ||||
| }>` | ||||
|   align-items: center; | ||||
|   box-sizing: border-box; | ||||
|   cursor: pointer; | ||||
|   display: flex; | ||||
|   height: 32px; | ||||
|   position: relative; | ||||
|   user-select: none; | ||||
|  | ||||
|   background: ${({ hasSoftFocus, backgroundColorTransparentSecondary }) => | ||||
|     hasSoftFocus ? backgroundColorTransparentSecondary : 'none'}; | ||||
|  | ||||
|   border-radius: ${({ hasSoftFocus }) => | ||||
|     hasSoftFocus ? BORDER_COMMON.radius.sm : 'none'}; | ||||
|  | ||||
|   outline: ${({ hasSoftFocus, fontColorExtraLight }) => | ||||
|     hasSoftFocus ? `1px solid ${fontColorExtraLight}` : 'none'}; | ||||
| `; | ||||
|  | ||||
| export const RecordTableCellBaseContainer = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: ReactNode; | ||||
| }) => { | ||||
|   const { setIsFocused } = useFieldFocus(); | ||||
|   const { openTableCell } = useOpenRecordTableCellFromCell(); | ||||
|   const { theme } = useContext(ThemeContext); | ||||
|   const { recordId } = useContext(RecordTableRowContext); | ||||
|  | ||||
|   const { hasSoftFocus, cellPosition } = useContext(RecordTableCellContext); | ||||
|  | ||||
|   const { onMoveSoftFocusToCell, onCellMouseEnter } = | ||||
|     useContext(RecordTableContext); | ||||
|  | ||||
|   const handleContainerMouseMove = () => { | ||||
|     setIsFocused(true); | ||||
|     if (!hasSoftFocus) { | ||||
|       onCellMouseEnter({ | ||||
|         cellPosition, | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleContainerMouseLeave = () => { | ||||
|     setIsFocused(false); | ||||
|   }; | ||||
|  | ||||
|   const handleContainerClick = () => { | ||||
|     if (!hasSoftFocus) { | ||||
|       onMoveSoftFocusToCell(cellPosition); | ||||
|       openTableCell(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const { onContextMenu } = useContext(RecordTableContext); | ||||
|  | ||||
|   const handleContextMenu = (event: React.MouseEvent) => { | ||||
|     onContextMenu(event, recordId); | ||||
|   }; | ||||
|  | ||||
|   const { hotkeyScope } = useContext(FieldContext); | ||||
|  | ||||
|   const editHotkeyScope = { scope: hotkeyScope } ?? DEFAULT_CELL_SCOPE; | ||||
|  | ||||
|   return ( | ||||
|     <CellHotkeyScopeContext.Provider value={editHotkeyScope}> | ||||
|       <StyledBaseContainer | ||||
|         onMouseLeave={handleContainerMouseLeave} | ||||
|         onMouseMove={handleContainerMouseMove} | ||||
|         onClick={handleContainerClick} | ||||
|         onContextMenu={handleContextMenu} | ||||
|         backgroundColorTransparentSecondary={ | ||||
|           theme.background.transparent.secondary | ||||
|         } | ||||
|         fontColorExtraLight={theme.font.color.extraLight} | ||||
|         hasSoftFocus={hasSoftFocus} | ||||
|       > | ||||
|         {children} | ||||
|       </StyledBaseContainer> | ||||
|     </CellHotkeyScopeContext.Provider> | ||||
|   ); | ||||
| }; | ||||
| @@ -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)}; | ||||
|   | ||||
| @@ -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 ( | ||||
|     <StyledContainer onClick={handleClick}> | ||||
|       <Checkbox checked={currentRowSelected} /> | ||||
|     </StyledContainer> | ||||
|     <RecordTableTd isSelected={isSelected} hasRightBorder={false}> | ||||
|       <StyledContainer onClick={handleClick}> | ||||
|         <Checkbox checked={currentRowSelected} /> | ||||
|       </StyledContainer> | ||||
|     </RecordTableTd> | ||||
|   ); | ||||
| }; | ||||
| @@ -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 ( | ||||
|     <StyledTd | ||||
|       backgroundColor={tdBackgroundColor} | ||||
|       isInEditMode={isInEditMode} | ||||
|       onContextMenu={handleContextMenu} | ||||
|     > | ||||
|       <CellHotkeyScopeContext.Provider | ||||
|         value={editHotkeyScope ?? DEFAULT_CELL_SCOPE} | ||||
|       > | ||||
|         <StyledBaseContainer | ||||
|           onMouseLeave={handleContainerMouseLeave} | ||||
|           onMouseMove={handleContainerMouseMove} | ||||
|           onClick={handleContainerClick} | ||||
|           backgroundColorTransparentSecondary={ | ||||
|             theme.background.transparent.secondary | ||||
|           } | ||||
|           fontColorExtraLight={theme.font.color.extraLight} | ||||
|           hasSoftFocus={hasSoftFocus} | ||||
|         > | ||||
|           {isInEditMode ? ( | ||||
|             <RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode> | ||||
|           ) : hasSoftFocus ? ( | ||||
|             <RecordTableCellSoftFocusMode | ||||
|               editModeContent={editModeContent} | ||||
|               nonEditModeContent={nonEditModeContent} | ||||
|             /> | ||||
|           ) : ( | ||||
|             <RecordTableCellDisplayMode> | ||||
|               {nonEditModeContent} | ||||
|             </RecordTableCellDisplayMode> | ||||
|           )} | ||||
|         </StyledBaseContainer> | ||||
|       </CellHotkeyScopeContext.Provider> | ||||
|     </StyledTd> | ||||
|     <RecordTableCellBaseContainer> | ||||
|       {isInEditMode ? ( | ||||
|         <RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode> | ||||
|       ) : hasSoftFocus ? ( | ||||
|         <RecordTableCellSoftFocusMode | ||||
|           editModeContent={editModeContent} | ||||
|           nonEditModeContent={nonEditModeContent} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <RecordTableCellDisplayMode> | ||||
|           {nonEditModeContent} | ||||
|         </RecordTableCellDisplayMode> | ||||
|       )} | ||||
|     </RecordTableCellBaseContainer> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -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` | ||||
|   | ||||
| @@ -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 = () => { | ||||
|         }), | ||||
|       }} | ||||
|     > | ||||
|       <RecordTableCell customHotkeyScope={{ scope: customHotkeyScope }} /> | ||||
|       {children} | ||||
|     </FieldContext.Provider> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,95 @@ | ||||
| import { useContext } from 'react'; | ||||
|  | ||||
| import { FieldInput } from '@/object-record/record-field/components/FieldInput'; | ||||
| import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; | ||||
| import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; | ||||
| import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
|  | ||||
| export const RecordTableCellFieldInput = () => { | ||||
|   const { onUpsertRecord, onMoveFocus, onCloseTableCell } = | ||||
|     useContext(RecordTableContext); | ||||
|   const { entityId, fieldDefinition } = useContext(FieldContext); | ||||
|   const { isReadOnly } = useContext(RecordTableRowContext); | ||||
|  | ||||
|   const handleEnter: FieldInputEvent = (persistField) => { | ||||
|     onUpsertRecord({ | ||||
|       persistField, | ||||
|       entityId, | ||||
|       fieldName: fieldDefinition.metadata.fieldName, | ||||
|     }); | ||||
|  | ||||
|     onCloseTableCell(); | ||||
|     onMoveFocus('down'); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmit: FieldInputEvent = (persistField) => { | ||||
|     onUpsertRecord({ | ||||
|       persistField, | ||||
|       entityId, | ||||
|       fieldName: fieldDefinition.metadata.fieldName, | ||||
|     }); | ||||
|  | ||||
|     onCloseTableCell(); | ||||
|   }; | ||||
|  | ||||
|   const handleCancel = () => { | ||||
|     onCloseTableCell(); | ||||
|   }; | ||||
|  | ||||
|   const handleClickOutside: FieldInputEvent = (persistField) => { | ||||
|     onUpsertRecord({ | ||||
|       persistField, | ||||
|       entityId, | ||||
|       fieldName: fieldDefinition.metadata.fieldName, | ||||
|     }); | ||||
|  | ||||
|     onCloseTableCell(); | ||||
|   }; | ||||
|  | ||||
|   const handleEscape: FieldInputEvent = (persistField) => { | ||||
|     onUpsertRecord({ | ||||
|       persistField, | ||||
|       entityId, | ||||
|       fieldName: fieldDefinition.metadata.fieldName, | ||||
|     }); | ||||
|  | ||||
|     onCloseTableCell(); | ||||
|   }; | ||||
|  | ||||
|   const handleTab: FieldInputEvent = (persistField) => { | ||||
|     onUpsertRecord({ | ||||
|       persistField, | ||||
|       entityId, | ||||
|       fieldName: fieldDefinition.metadata.fieldName, | ||||
|     }); | ||||
|  | ||||
|     onCloseTableCell(); | ||||
|     onMoveFocus('right'); | ||||
|   }; | ||||
|  | ||||
|   const handleShiftTab: FieldInputEvent = (persistField) => { | ||||
|     onUpsertRecord({ | ||||
|       persistField, | ||||
|       entityId, | ||||
|       fieldName: fieldDefinition.metadata.fieldName, | ||||
|     }); | ||||
|  | ||||
|     onCloseTableCell(); | ||||
|     onMoveFocus('left'); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <FieldInput | ||||
|       recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`} | ||||
|       onCancel={handleCancel} | ||||
|       onClickOutside={handleClickOutside} | ||||
|       onEnter={handleEnter} | ||||
|       onEscape={handleEscape} | ||||
|       onShiftTab={handleShiftTab} | ||||
|       onSubmit={handleSubmit} | ||||
|       onTab={handleTab} | ||||
|       isReadOnly={isReadOnly} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,44 @@ | ||||
| import styled from '@emotion/styled'; | ||||
| import { useContext } from 'react'; | ||||
|  | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
| import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; | ||||
| import { IconListViewGrip } from '@/ui/input/components/IconListViewGrip'; | ||||
|  | ||||
| const StyledContainer = styled.div` | ||||
|   cursor: grab; | ||||
|   width: 16px; | ||||
|   height: 32px; | ||||
|   z-index: 200; | ||||
|   display: flex; | ||||
|   &:hover .icon { | ||||
|     opacity: 1; | ||||
|   } | ||||
|  | ||||
|   border-color: transparent; | ||||
| `; | ||||
|  | ||||
| const StyledIconWrapper = styled.div<{ isDragging: boolean }>` | ||||
|   opacity: ${({ isDragging }) => (isDragging ? 1 : 0)}; | ||||
|   transition: opacity 0.1s; | ||||
| `; | ||||
|  | ||||
| export const RecordTableCellGrip = () => { | ||||
|   const { dragHandleProps, isDragging } = useContext(RecordTableRowContext); | ||||
|  | ||||
|   return ( | ||||
|     <RecordTableTd | ||||
|       // eslint-disable-next-line react/jsx-props-no-spreading | ||||
|       {...dragHandleProps} | ||||
|       data-select-disable | ||||
|       hasRightBorder={false} | ||||
|       hasBottomBorder={false} | ||||
|     > | ||||
|       <StyledContainer> | ||||
|         <StyledIconWrapper className="icon" isDragging={isDragging}> | ||||
|           <IconListViewGrip /> | ||||
|         </StyledIconWrapper> | ||||
|       </StyledContainer> | ||||
|     </RecordTableTd> | ||||
|   ); | ||||
| }; | ||||
| @@ -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 ( | ||||
|     <StyledTd> | ||||
|     <RecordTableTd> | ||||
|       <RecordTableCellSkeletonLoader /> | ||||
|     </StyledTd> | ||||
|     </RecordTableTd> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -0,0 +1,75 @@ | ||||
| import { useContext, useMemo } from 'react'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; | ||||
| import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
| import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper'; | ||||
| import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; | ||||
| import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState'; | ||||
| import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState'; | ||||
| import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; | ||||
| import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; | ||||
| import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; | ||||
| import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; | ||||
| import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState'; | ||||
|  | ||||
| export const RecordTableCellWrapper = ({ | ||||
|   children, | ||||
|   column, | ||||
|   columnIndex, | ||||
| }: { | ||||
|   column: ColumnDefinition<FieldMetadata>; | ||||
|   columnIndex: number; | ||||
|   children: React.ReactNode; | ||||
| }) => { | ||||
|   const tableScopeId = useAvailableScopeIdOrThrow( | ||||
|     RecordTableScopeInternalContext, | ||||
|     getScopeIdOrUndefinedFromComponentId(), | ||||
|   ); | ||||
|  | ||||
|   const { rowIndex } = useContext(RecordTableRowContext); | ||||
|  | ||||
|   const currentTableCellPosition: TableCellPosition = useMemo( | ||||
|     () => ({ | ||||
|       column: columnIndex, | ||||
|       row: rowIndex, | ||||
|     }), | ||||
|     [columnIndex, rowIndex], | ||||
|   ); | ||||
|  | ||||
|   const isTableCellInEditModeFamilyState = extractComponentFamilyState( | ||||
|     isTableCellInEditModeComponentFamilyState, | ||||
|     tableScopeId, | ||||
|   ); | ||||
|  | ||||
|   const isSoftFocusOnTableCellFamilyState = extractComponentFamilyState( | ||||
|     isSoftFocusOnTableCellComponentFamilyState, | ||||
|     tableScopeId, | ||||
|   ); | ||||
|  | ||||
|   const isInEditMode = useRecoilValue( | ||||
|     isTableCellInEditModeFamilyState(currentTableCellPosition), | ||||
|   ); | ||||
|  | ||||
|   const hasSoftFocus = useRecoilValue( | ||||
|     isSoftFocusOnTableCellFamilyState(currentTableCellPosition), | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <RecordTableCellContext.Provider | ||||
|       value={{ | ||||
|         columnDefinition: column, | ||||
|         columnIndex, | ||||
|         isInEditMode, | ||||
|         hasSoftFocus, | ||||
|         cellPosition: currentTableCellPosition, | ||||
|       }} | ||||
|       key={column.fieldMetadataId} | ||||
|     > | ||||
|       <RecordTableCellFieldContextWrapper> | ||||
|         {children} | ||||
|       </RecordTableCellFieldContextWrapper> | ||||
|     </RecordTableCellContext.Provider> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,10 @@ | ||||
| import { useContext } from 'react'; | ||||
|  | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
| import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; | ||||
|  | ||||
| export const RecordTableLastEmptyCell = () => { | ||||
|   const { isSelected } = useContext(RecordTableRowContext); | ||||
|  | ||||
|   return <RecordTableTd isSelected={isSelected} hasRightBorder={false} />; | ||||
| }; | ||||
| @@ -0,0 +1,102 @@ | ||||
| import { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd'; | ||||
| import { styled } from '@linaria/react'; | ||||
| import { ReactNode, useContext } from 'react'; | ||||
| import { MOBILE_VIEWPORT, ThemeContext } from 'twenty-ui'; | ||||
|  | ||||
| import { isDefined } from '~/utils/isDefined'; | ||||
|  | ||||
| const StyledTd = styled.td<{ | ||||
|   zIndex?: number; | ||||
|   backgroundColor: string; | ||||
|   borderColor: string; | ||||
|   isDragging?: boolean; | ||||
|   fontColor: string; | ||||
|   sticky?: boolean; | ||||
|   freezeFirstColumns?: boolean; | ||||
|   left?: number; | ||||
|   hasRightBorder?: boolean; | ||||
|   hasBottomBorder?: boolean; | ||||
| }>` | ||||
|   border-bottom: 1px solid | ||||
|     ${({ borderColor, hasBottomBorder }) => | ||||
|       hasBottomBorder ? borderColor : 'transparent'}; | ||||
|   color: ${({ fontColor }) => fontColor}; | ||||
|   border-right: 1px solid | ||||
|     ${({ borderColor, hasRightBorder }) => | ||||
|       hasRightBorder ? borderColor : 'transparent'}; | ||||
|  | ||||
|   padding: 0; | ||||
|  | ||||
|   text-align: left; | ||||
|  | ||||
|   background: ${({ backgroundColor }) => backgroundColor}; | ||||
|   z-index: ${({ zIndex }) => (isDefined(zIndex) ? zIndex : 'auto')}; | ||||
|  | ||||
|   ${({ isDragging }) => | ||||
|     isDragging | ||||
|       ? ` | ||||
|       background-color: transparent; | ||||
|       border-color: transparent; | ||||
|   ` | ||||
|       : ''} | ||||
|  | ||||
|   ${({ freezeFirstColumns }) => | ||||
|     freezeFirstColumns | ||||
|       ? `@media (max-width: ${MOBILE_VIEWPORT}px) { | ||||
|       width: 35px; | ||||
|       max-width: 35px; | ||||
|     }` | ||||
|       : ''} | ||||
| `; | ||||
|  | ||||
| export const RecordTableTd = ({ | ||||
|   children, | ||||
|   zIndex, | ||||
|   isSelected, | ||||
|   isDragging, | ||||
|   sticky, | ||||
|   freezeFirstColumns, | ||||
|   left, | ||||
|   hasRightBorder = true, | ||||
|   hasBottomBorder = true, | ||||
|   ...dragHandleProps | ||||
| }: { | ||||
|   className?: string; | ||||
|   children?: ReactNode; | ||||
|   zIndex?: number; | ||||
|   isSelected?: boolean; | ||||
|   isDragging?: boolean; | ||||
|   sticky?: boolean; | ||||
|   freezeFirstColumns?: boolean; | ||||
|   hasRightBorder?: boolean; | ||||
|   hasBottomBorder?: boolean; | ||||
|   left?: number; | ||||
| } & (Partial<DraggableProvidedDragHandleProps> | null)) => { | ||||
|   const { theme } = useContext(ThemeContext); | ||||
|  | ||||
|   const tdBackgroundColor = isSelected | ||||
|     ? theme.accent.quaternary | ||||
|     : theme.background.primary; | ||||
|  | ||||
|   const borderColor = theme.border.color.light; | ||||
|   const fontColor = theme.font.color.primary; | ||||
|  | ||||
|   return ( | ||||
|     <StyledTd | ||||
|       isDragging={isDragging} | ||||
|       zIndex={zIndex} | ||||
|       backgroundColor={tdBackgroundColor} | ||||
|       borderColor={borderColor} | ||||
|       fontColor={fontColor} | ||||
|       sticky={sticky} | ||||
|       freezeFirstColumns={freezeFirstColumns} | ||||
|       left={left} | ||||
|       hasRightBorder={hasRightBorder} | ||||
|       hasBottomBorder={hasBottomBorder} | ||||
|       // eslint-disable-next-line react/jsx-props-no-spreading | ||||
|       {...dragHandleProps} | ||||
|     > | ||||
|       {children} | ||||
|     </StyledTd> | ||||
|   ); | ||||
| }; | ||||
| @@ -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<FieldMetadata>; | ||||
|   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, | ||||
| }; | ||||
|   | ||||
| @@ -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; | ||||
| }; | ||||
|   | ||||
| @@ -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<FieldMetadata>; | ||||
| }; | ||||
| 
 | ||||
| @@ -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 ( | ||||
|     <StyledTitle hideTitle={!!column.isLabelIdentifier && scrollLeft > 0}> | ||||
|     <StyledTitle | ||||
|       hideTitle={!!column.isLabelIdentifier && !isRecordTableScrolledLeft} | ||||
|     > | ||||
|       <StyledIcon> | ||||
|         <Icon size={theme.icon.size.md} /> | ||||
|       </StyledIcon> | ||||
| @@ -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<FieldMetadata>; | ||||
| }; | ||||
| 
 | ||||
| export const RecordTableColumnDropdownMenu = ({ | ||||
| export const RecordTableColumnHeadDropdownMenu = ({ | ||||
|   column, | ||||
| }: RecordTableColumnDropdownMenuProps) => { | ||||
| }: RecordTableColumnHeadDropdownMenuProps) => { | ||||
|   const { | ||||
|     visibleTableColumnsSelector, | ||||
|     onToggleColumnFilterState, | ||||
| @@ -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<FieldMetadata>; | ||||
| }; | ||||
| 
 | ||||
| const StyledDropdown = styled(Dropdown)` | ||||
|   display: flex; | ||||
| 
 | ||||
|   flex: 1; | ||||
|   z-index: ${({ theme }) => theme.lastLayerZIndex}; | ||||
| `;
 | ||||
| 
 | ||||
| export const ColumnHeadWithDropdown = ({ | ||||
| export const RecordTableColumnHeadWithDropdown = ({ | ||||
|   column, | ||||
| }: ColumnHeadWithDropdownProps) => { | ||||
| }: RecordTableColumnHeadWithDropdownProps) => { | ||||
|   return ( | ||||
|     <StyledDropdown | ||||
|       dropdownId={column.fieldMetadataId + '-header'} | ||||
|       clickableComponent={<ColumnHead column={column} />} | ||||
|       dropdownComponents={<RecordTableColumnDropdownMenu column={column} />} | ||||
|       clickableComponent={<RecordTableColumnHead column={column} />} | ||||
|       dropdownComponents={<RecordTableColumnHeadDropdownMenu column={column} />} | ||||
|       dropdownOffset={{ x: -1 }} | ||||
|       dropdownPlacement="bottom-start" | ||||
|       dropdownHotkeyScope={{ scope: column.fieldMetadataId + '-header' }} | ||||
| @@ -0,0 +1,91 @@ | ||||
| import styled from '@emotion/styled'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| import { MOBILE_VIEWPORT } from 'twenty-ui'; | ||||
|  | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell'; | ||||
| import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn'; | ||||
| import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn'; | ||||
| import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn'; | ||||
|  | ||||
| const StyledTableHead = styled.thead<{ | ||||
|   isScrolledTop?: boolean; | ||||
|   isScrolledLeft?: boolean; | ||||
| }>` | ||||
|   cursor: pointer; | ||||
|  | ||||
|   th:nth-of-type(1) { | ||||
|     width: 9px; | ||||
|     left: 0; | ||||
|     border-right-color: ${({ theme }) => theme.background.primary}; | ||||
|   } | ||||
|  | ||||
|   th:nth-of-type(2) { | ||||
|     border-right-color: ${({ theme }) => theme.background.primary}; | ||||
|   } | ||||
|  | ||||
|   &.first-columns-sticky { | ||||
|     th:nth-child(1) { | ||||
|       position: sticky; | ||||
|       left: 0; | ||||
|       z-index: 5; | ||||
|     } | ||||
|     th:nth-child(2) { | ||||
|       position: sticky; | ||||
|       left: 9px; | ||||
|       z-index: 5; | ||||
|     } | ||||
|     th:nth-child(3) { | ||||
|       position: sticky; | ||||
|       left: 39px; | ||||
|       z-index: 5; | ||||
|       @media (max-width: ${MOBILE_VIEWPORT}px) { | ||||
|         width: 35px; | ||||
|         max-width: 35px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.header-sticky { | ||||
|     th { | ||||
|       position: sticky; | ||||
|       top: 0; | ||||
|       z-index: 5; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.header-sticky.first-columns-sticky { | ||||
|     th:nth-child(1), | ||||
|     th:nth-child(2), | ||||
|     th:nth-child(3) { | ||||
|       z-index: 10; | ||||
|     } | ||||
|   } | ||||
| `; | ||||
|  | ||||
| export const RecordTableHeader = ({ | ||||
|   createRecord, | ||||
| }: { | ||||
|   createRecord: () => void; | ||||
| }) => { | ||||
|   const { visibleTableColumnsSelector } = useRecordTableStates(); | ||||
|  | ||||
|   const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); | ||||
|  | ||||
|   return ( | ||||
|     <StyledTableHead id="record-table-header" data-select-disable> | ||||
|       <tr> | ||||
|         <RecordTableHeaderDragDropColumn /> | ||||
|         <RecordTableHeaderCheckboxColumn /> | ||||
|         {visibleTableColumns.map((column) => ( | ||||
|           <RecordTableHeaderCell | ||||
|             key={column.fieldMetadataId} | ||||
|             column={column} | ||||
|             createRecord={createRecord} | ||||
|           /> | ||||
|         ))} | ||||
|         <RecordTableHeaderLastColumn /> | ||||
|       </tr> | ||||
|     </StyledTableHead> | ||||
|   ); | ||||
| }; | ||||
| @@ -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 ( | ||||
|     <StyledColumnHeaderCell | ||||
| @@ -185,7 +196,7 @@ export const RecordTableHeaderCell = ({ | ||||
|       onMouseLeave={() => setIconVisibility(false)} | ||||
|     > | ||||
|       <StyledColumnHeadContainer> | ||||
|         <ColumnHeadWithDropdown column={column} /> | ||||
|         <RecordTableColumnHeadWithDropdown column={column} /> | ||||
|         {(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && ( | ||||
|           <StyledHeaderIcon> | ||||
|             <LightIconButton | ||||
| @@ -2,9 +2,9 @@ import styled from '@emotion/styled'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| 
 | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; | ||||
| import { Checkbox } from '@/ui/input/components/Checkbox'; | ||||
| 
 | ||||
| import { useRecordTable } from '../hooks/useRecordTable'; | ||||
| import { useTheme } from '@emotion/react'; | ||||
| 
 | ||||
| const StyledContainer = styled.div` | ||||
|   align-items: center; | ||||
| @@ -16,7 +16,7 @@ const StyledContainer = styled.div` | ||||
|   background-color: ${({ theme }) => 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 ( | ||||
|     <StyledContainer> | ||||
|       <Checkbox | ||||
|         checked={checked} | ||||
|         onChange={onChange} | ||||
|         indeterminate={indeterminate} | ||||
|       /> | ||||
|     </StyledContainer> | ||||
|     <th | ||||
|       style={{ | ||||
|         borderBottom: `1px solid ${theme.border.color.light}`, | ||||
|         borderTop: `1px solid ${theme.border.color.light}`, | ||||
|         width: 30, | ||||
|         minWidth: 30, | ||||
|         maxWidth: 30, | ||||
|         borderRight: 'transparent', | ||||
|       }} | ||||
|     > | ||||
|       <StyledContainer> | ||||
|         <Checkbox | ||||
|           checked={checked} | ||||
|           onChange={onChange} | ||||
|           indeterminate={indeterminate} | ||||
|         /> | ||||
|       </StyledContainer> | ||||
|     </th> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,10 @@ | ||||
| import { styled } from '@linaria/react'; | ||||
|  | ||||
| const StyledTh = styled.th` | ||||
|   border-bottom: none; | ||||
|   border-top: none; | ||||
| `; | ||||
|  | ||||
| export const RecordTableHeaderDragDropColumn = () => { | ||||
|   return <StyledTh></StyledTh>; | ||||
| }; | ||||
| @@ -0,0 +1,89 @@ | ||||
| import { Theme } from '@emotion/react'; | ||||
| import styled from '@emotion/styled'; | ||||
| import { useContext } from 'react'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| import { IconPlus, ThemeContext } from 'twenty-ui'; | ||||
|  | ||||
| import { HIDDEN_TABLE_COLUMN_DROPDOWN_ID } from '@/object-record/record-table/constants/HiddenTableColumnDropdownId'; | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent'; | ||||
| import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; | ||||
| import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef'; | ||||
|  | ||||
| const StyledPlusIconHeaderCell = styled.th<{ | ||||
|   theme: Theme; | ||||
|   isTableWiderThanScreen: boolean; | ||||
| }>` | ||||
|   ${({ theme }) => { | ||||
|     return ` | ||||
|   &:hover { | ||||
|     background: ${theme.background.transparent.light}; | ||||
|   }; | ||||
|   padding-left: ${theme.spacing(3)}; | ||||
|   `; | ||||
|   }}; | ||||
|   border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; | ||||
|   border-top: 1px solid ${({ theme }) => theme.border.color.light}; | ||||
|   background-color: ${({ theme }) => theme.background.primary}; | ||||
|   border-left: none !important; | ||||
|   color: ${({ theme }) => theme.font.color.tertiary}; | ||||
|   min-width: 32px; | ||||
|   border-right: none !important; | ||||
|  | ||||
|   ${({ isTableWiderThanScreen, theme }) => | ||||
|     isTableWiderThanScreen | ||||
|       ? ` | ||||
|     width: 32px; | ||||
|     background-color: ${theme.background.primary}; | ||||
|     ` | ||||
|       : ''}; | ||||
|   z-index: 1; | ||||
| `; | ||||
|  | ||||
| const StyledPlusIconContainer = styled.div` | ||||
|   align-items: center; | ||||
|   display: flex; | ||||
|   height: 32px; | ||||
|   justify-content: center; | ||||
|   width: 32px; | ||||
| `; | ||||
|  | ||||
| const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID = | ||||
|   'hidden-table-columns-dropdown-hotkey-scope-id'; | ||||
|  | ||||
| export const RecordTableHeaderLastColumn = () => { | ||||
|   const { theme } = useContext(ThemeContext); | ||||
|  | ||||
|   const scrollWrapper = useScrollWrapperScopedRef(); | ||||
|  | ||||
|   const isTableWiderThanScreen = | ||||
|     (scrollWrapper.current?.clientWidth ?? 0) < | ||||
|     (scrollWrapper.current?.scrollWidth ?? 0); | ||||
|  | ||||
|   const { hiddenTableColumnsSelector } = useRecordTableStates(); | ||||
|  | ||||
|   const hiddenTableColumns = useRecoilValue(hiddenTableColumnsSelector()); | ||||
|  | ||||
|   return ( | ||||
|     <StyledPlusIconHeaderCell | ||||
|       theme={theme} | ||||
|       isTableWiderThanScreen={isTableWiderThanScreen} | ||||
|     > | ||||
|       {hiddenTableColumns.length > 0 && ( | ||||
|         <Dropdown | ||||
|           dropdownId={HIDDEN_TABLE_COLUMN_DROPDOWN_ID} | ||||
|           clickableComponent={ | ||||
|             <StyledPlusIconContainer> | ||||
|               <IconPlus size={theme.icon.size.md} /> | ||||
|             </StyledPlusIconContainer> | ||||
|           } | ||||
|           dropdownComponents={<RecordTableHeaderPlusButtonContent />} | ||||
|           dropdownPlacement="bottom-start" | ||||
|           dropdownHotkeyScope={{ | ||||
|             scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID, | ||||
|           }} | ||||
|         /> | ||||
|       )} | ||||
|     </StyledPlusIconHeaderCell> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,17 @@ | ||||
| import { useContext } from 'react'; | ||||
|  | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
| import { RecordTableCellsEmpty } from '@/object-record/record-table/record-table-row/components/RecordTableCellsEmpty'; | ||||
| import { RecordTableCellsVisible } from '@/object-record/record-table/record-table-row/components/RecordTableCellsVisible'; | ||||
|  | ||||
| export const RecordTableCells = () => { | ||||
|   const { inView, isDragging } = useContext(RecordTableRowContext); | ||||
|  | ||||
|   const areCellsVisible = inView || isDragging; | ||||
|  | ||||
|   return areCellsVisible ? ( | ||||
|     <RecordTableCellsVisible /> | ||||
|   ) : ( | ||||
|     <RecordTableCellsEmpty /> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,17 @@ | ||||
| import { useContext } from 'react'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
|  | ||||
| export const RecordTableCellsEmpty = () => { | ||||
|   const { isSelected } = useContext(RecordTableRowContext); | ||||
|   const { visibleTableColumnsSelector } = useRecordTableStates(); | ||||
|  | ||||
|   const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); | ||||
|  | ||||
|   return visibleTableColumns.map((column) => ( | ||||
|     <RecordTableTd isSelected={isSelected} key={column.fieldMetadataId} /> | ||||
|   )); | ||||
| }; | ||||
| @@ -0,0 +1,39 @@ | ||||
| import { useContext } from 'react'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell'; | ||||
| import { RecordTableCellWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellWrapper'; | ||||
| import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; | ||||
|  | ||||
| export const RecordTableCellsVisible = () => { | ||||
|   const { isDragging } = useContext(RecordTableRowContext); | ||||
|   const { visibleTableColumnsSelector } = useRecordTableStates(); | ||||
|  | ||||
|   const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); | ||||
|  | ||||
|   const tableColumnsAfterFirst = visibleTableColumns.slice(1); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <RecordTableCellWrapper column={visibleTableColumns[0]} columnIndex={0}> | ||||
|         <RecordTableTd> | ||||
|           <RecordTableCell /> | ||||
|         </RecordTableTd> | ||||
|       </RecordTableCellWrapper> | ||||
|       {!isDragging && | ||||
|         tableColumnsAfterFirst.map((column, columnIndex) => ( | ||||
|           <RecordTableCellWrapper | ||||
|             key={column.fieldMetadataId} | ||||
|             column={column} | ||||
|             columnIndex={columnIndex + 1} | ||||
|           > | ||||
|             <RecordTableTd> | ||||
|               <RecordTableCell /> | ||||
|             </RecordTableTd> | ||||
|           </RecordTableCellWrapper> | ||||
|         ))} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -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 ( | ||||
|     <RecordTableRow | ||||
| @@ -0,0 +1,32 @@ | ||||
| import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; | ||||
| import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox'; | ||||
| import { RecordTableCellGrip } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip'; | ||||
| import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell'; | ||||
| import { RecordTableCells } from '@/object-record/record-table/record-table-row/components/RecordTableCells'; | ||||
| import { RecordTableRowWrapper } from '@/object-record/record-table/record-table-row/components/RecordTableRowWrapper'; | ||||
|  | ||||
| type RecordTableRowProps = { | ||||
|   recordId: string; | ||||
|   rowIndex: number; | ||||
|   isPendingRow?: boolean; | ||||
| }; | ||||
|  | ||||
| export const RecordTableRow = ({ | ||||
|   recordId, | ||||
|   rowIndex, | ||||
|   isPendingRow, | ||||
| }: RecordTableRowProps) => { | ||||
|   return ( | ||||
|     <RecordTableRowWrapper | ||||
|       recordId={recordId} | ||||
|       rowIndex={rowIndex} | ||||
|       isPendingRow={isPendingRow} | ||||
|     > | ||||
|       <RecordTableCellGrip /> | ||||
|       <RecordTableCellCheckbox /> | ||||
|       <RecordTableCells /> | ||||
|       <RecordTableLastEmptyCell /> | ||||
|       <RecordValueSetterEffect recordId={recordId} /> | ||||
|     </RecordTableRowWrapper> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,87 @@ | ||||
| import { ReactNode, useContext } from 'react'; | ||||
| import { useInView } from 'react-intersection-observer'; | ||||
| import { useTheme } from '@emotion/react'; | ||||
| import { Draggable } from '@hello-pangea/dnd'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; | ||||
| import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; | ||||
| import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; | ||||
| import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; | ||||
| import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr'; | ||||
| import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper'; | ||||
|  | ||||
| export const RecordTableRowWrapper = ({ | ||||
|   recordId, | ||||
|   rowIndex, | ||||
|   isPendingRow, | ||||
|   children, | ||||
| }: { | ||||
|   recordId: string; | ||||
|   rowIndex: number; | ||||
|   isPendingRow?: boolean; | ||||
|   children: ReactNode; | ||||
| }) => { | ||||
|   const { objectMetadataItem } = useContext(RecordTableContext); | ||||
|  | ||||
|   const theme = useTheme(); | ||||
|  | ||||
|   const { isRowSelectedFamilyState } = useRecordTableStates(); | ||||
|   const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId)); | ||||
|  | ||||
|   const scrollWrapperRef = useContext(ScrollWrapperContext); | ||||
|  | ||||
|   const { ref: elementRef, inView } = useInView({ | ||||
|     root: scrollWrapperRef.current?.querySelector( | ||||
|       '[data-overlayscrollbars-viewport="scrollbarHidden"]', | ||||
|     ), | ||||
|     rootMargin: '1000px', | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <Draggable key={recordId} draggableId={recordId} index={rowIndex}> | ||||
|       {(draggableProvided, draggableSnapshot) => ( | ||||
|         <RecordTableTr | ||||
|           ref={(node) => { | ||||
|             elementRef(node); | ||||
|             draggableProvided.innerRef(node); | ||||
|           }} | ||||
|           // eslint-disable-next-line react/jsx-props-no-spreading | ||||
|           {...draggableProvided.draggableProps} | ||||
|           style={{ | ||||
|             ...draggableProvided.draggableProps.style, | ||||
|             background: draggableSnapshot.isDragging | ||||
|               ? theme.background.transparent.light | ||||
|               : 'none', | ||||
|             borderColor: draggableSnapshot.isDragging | ||||
|               ? `${theme.border.color.medium}` | ||||
|               : 'transparent', | ||||
|           }} | ||||
|           isDragging={draggableSnapshot.isDragging} | ||||
|           data-testid={`row-id-${recordId}`} | ||||
|           data-selectable-id={recordId} | ||||
|         > | ||||
|           <RecordTableRowContext.Provider | ||||
|             value={{ | ||||
|               recordId, | ||||
|               rowIndex, | ||||
|               pathToShowPage: | ||||
|                 getBasePathToShowPage({ | ||||
|                   objectNameSingular: objectMetadataItem.nameSingular, | ||||
|                 }) + recordId, | ||||
|               objectNameSingular: objectMetadataItem.nameSingular, | ||||
|               isSelected: currentRowSelected, | ||||
|               isReadOnly: objectMetadataItem.isRemote ?? false, | ||||
|               isPendingRow, | ||||
|               isDragging: draggableSnapshot.isDragging, | ||||
|               dragHandleProps: draggableProvided.dragHandleProps, | ||||
|               inView, | ||||
|             }} | ||||
|           > | ||||
|             {children} | ||||
|           </RecordTableRowContext.Provider> | ||||
|         </RecordTableTr> | ||||
|       )} | ||||
|     </Draggable> | ||||
|   ); | ||||
| }; | ||||
| @@ -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; | ||||
| @@ -0,0 +1,9 @@ | ||||
| import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; | ||||
| import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; | ||||
|  | ||||
| export const isRecordTableScrolledLeftComponentState = | ||||
|   createComponentStateV2<boolean>({ | ||||
|     key: 'isRecordTableScrolledLeftComponentState', | ||||
|     componentContext: RecordTableScopeInternalContext, | ||||
|     defaultValue: true, | ||||
|   }); | ||||
| @@ -0,0 +1,9 @@ | ||||
| import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; | ||||
| import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; | ||||
|  | ||||
| export const isRecordTableScrolledTopComponentState = | ||||
|   createComponentStateV2<boolean>({ | ||||
|     key: 'isRecordTableScrolledTopComponentState', | ||||
|     componentContext: RecordTableScopeInternalContext, | ||||
|     defaultValue: true, | ||||
|   }); | ||||
| @@ -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 && ( | ||||
|           <DropdownMenu | ||||
|             disableBlur={disableBlur} | ||||
|             width={dropdownMenuWidth ?? dropdownWidth} | ||||
|             data-select-disable | ||||
|             ref={refs.setFloating} | ||||
|             style={floatingStyles} | ||||
|           > | ||||
|             {dropdownComponents} | ||||
|           </DropdownMenu> | ||||
|           <FloatingPortal> | ||||
|             <DropdownMenu | ||||
|               disableBlur={disableBlur} | ||||
|               width={dropdownMenuWidth ?? dropdownWidth} | ||||
|               data-select-disable | ||||
|               ref={refs.setFloating} | ||||
|               style={floatingStyles} | ||||
|             > | ||||
|               {dropdownComponents} | ||||
|             </DropdownMenu> | ||||
|           </FloatingPortal> | ||||
|         )} | ||||
|         <DropdownOnToggleEffect | ||||
|           onDropdownClose={onClose} | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { ReactElement, useCallback, useEffect, useRef, useState } from 'react'; | ||||
| import styled from '@emotion/styled'; | ||||
| import { ReactElement, useCallback, useEffect, useRef, useState } from 'react'; | ||||
| import { Chip, ChipVariant } from 'twenty-ui'; | ||||
|  | ||||
| import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer'; | ||||
| import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown'; | ||||
| import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement'; | ||||
| import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer'; | ||||
| import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; | ||||
| import { isDefined } from '~/utils/isDefined'; | ||||
|  | ||||
|   | ||||
| @@ -1,20 +1,17 @@ | ||||
| import React from 'react'; | ||||
| import styled from '@emotion/styled'; | ||||
| import { motion } from 'framer-motion'; | ||||
| 
 | ||||
| const StyledAnimatedChipContainer = styled(motion.div)``; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| export const AnimatedContainer = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode; | ||||
| }) => ( | ||||
|   <StyledAnimatedChipContainer | ||||
|   <motion.div | ||||
|     initial={{ opacity: 0 }} | ||||
|     animate={{ opacity: 1 }} | ||||
|     transition={{ duration: 0.1 }} | ||||
|     whileHover={{ scale: 1.04 }} | ||||
|   > | ||||
|     {children} | ||||
|   </StyledAnimatedChipContainer> | ||||
|   </motion.div> | ||||
| ); | ||||
| @@ -0,0 +1,29 @@ | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; | ||||
| import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; | ||||
| import { ComponentState } from '@/ui/utilities/state/component-state/types/ComponentState'; | ||||
|  | ||||
| export const useRecoilComponentValue = <StateType>( | ||||
|   componentState: ComponentState<StateType>, | ||||
|   componentId?: string, | ||||
| ) => { | ||||
|   const componentContext = (window as any).componentContextStateMap?.get( | ||||
|     componentState.key, | ||||
|   ); | ||||
|  | ||||
|   if (!componentContext) { | ||||
|     throw new Error( | ||||
|       `Component context for key "${componentState.key}" is not defined`, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   const internalComponentId = useAvailableScopeIdOrThrow( | ||||
|     componentContext, | ||||
|     getScopeIdOrUndefinedFromComponentId(componentId), | ||||
|   ); | ||||
|  | ||||
|   return useRecoilValue( | ||||
|     componentState.atomFamily({ scopeId: internalComponentId }), | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,29 @@ | ||||
| import { useSetRecoilState } from 'recoil'; | ||||
|  | ||||
| import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; | ||||
| import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; | ||||
| import { ComponentState } from '@/ui/utilities/state/component-state/types/ComponentState'; | ||||
|  | ||||
| export const useSetRecoilComponentState = <StateType>( | ||||
|   componentState: ComponentState<StateType>, | ||||
|   componentId?: string, | ||||
| ) => { | ||||
|   const componentContext = (window as any).componentContextStateMap?.get( | ||||
|     componentState.key, | ||||
|   ); | ||||
|  | ||||
|   if (!componentContext) { | ||||
|     throw new Error( | ||||
|       `Component context for key "${componentState.key}" is not defined`, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   const internalComponentId = useAvailableScopeIdOrThrow( | ||||
|     componentContext, | ||||
|     getScopeIdOrUndefinedFromComponentId(componentId), | ||||
|   ); | ||||
|  | ||||
|   return useSetRecoilState( | ||||
|     componentState.atomFamily({ scopeId: internalComponentId }), | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,8 @@ | ||||
| import { RecoilState } from 'recoil'; | ||||
|  | ||||
| import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey'; | ||||
|  | ||||
| export type ComponentState<StateType> = { | ||||
|   key: string; | ||||
|   atomFamily: (componentStateKey: ComponentStateKey) => RecoilState<StateType>; | ||||
| }; | ||||
| @@ -2,15 +2,17 @@ import { AtomEffect, atomFamily } from 'recoil'; | ||||
|  | ||||
| import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey'; | ||||
|  | ||||
| type CreateComponentStateType<ValueType> = { | ||||
|   key: string; | ||||
|   defaultValue: ValueType; | ||||
|   effects?: AtomEffect<ValueType>[]; | ||||
| }; | ||||
|  | ||||
| export const createComponentState = <ValueType>({ | ||||
|   key, | ||||
|   defaultValue, | ||||
|   effects, | ||||
| }: { | ||||
|   key: string; | ||||
|   defaultValue: ValueType; | ||||
|   effects?: AtomEffect<ValueType>[]; | ||||
| }) => { | ||||
| }: CreateComponentStateType<ValueType>) => { | ||||
|   return atomFamily<ValueType, ComponentStateKey>({ | ||||
|     key, | ||||
|     default: defaultValue, | ||||
|   | ||||
| @@ -0,0 +1,37 @@ | ||||
| import { AtomEffect, atomFamily } from 'recoil'; | ||||
|  | ||||
| import { ScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/types/ScopeInternalContext'; | ||||
| import { ComponentState } from '@/ui/utilities/state/component-state/types/ComponentState'; | ||||
| import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey'; | ||||
| import { isDefined } from '~/utils/isDefined'; | ||||
|  | ||||
| type CreateComponentStateV2Type<ValueType> = { | ||||
|   key: string; | ||||
|   defaultValue: ValueType; | ||||
|   componentContext?: ScopeInternalContext<any> | null; | ||||
|   effects?: AtomEffect<ValueType>[]; | ||||
| }; | ||||
|  | ||||
| export const createComponentStateV2 = <ValueType>({ | ||||
|   key, | ||||
|   defaultValue, | ||||
|   componentContext, | ||||
|   effects, | ||||
| }: CreateComponentStateV2Type<ValueType>): ComponentState<ValueType> => { | ||||
|   if (isDefined(componentContext)) { | ||||
|     if (!isDefined((window as any).componentContextStateMap)) { | ||||
|       (window as any).componentContextStateMap = new Map(); | ||||
|     } | ||||
|  | ||||
|     (window as any).componentContextStateMap.set(key, componentContext); | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     key, | ||||
|     atomFamily: atomFamily<ValueType, ComponentStateKey>({ | ||||
|       key, | ||||
|       default: defaultValue, | ||||
|       effects: effects, | ||||
|     }), | ||||
|   }; | ||||
| }; | ||||
| @@ -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'], | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Lucas Bordeau
					Lucas Bordeau