mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-21 16:12:18 +03: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:
parent
cc6ce142ce
commit
7b3a590f79
@ -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,
|
||||
}}
|
||||
<RecordTableContextProvider
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordTableId={recordTableId}
|
||||
>
|
||||
<StyledTable className="entity-table-cell">
|
||||
<RecordTableHeader createRecord={createRecord} />
|
||||
<RecordTableBodyEffect objectNameSingular={objectNameSingular} />
|
||||
<RecordTableBody
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordTableId={recordTableId}
|
||||
/>
|
||||
<RecordTableBodyEffect />
|
||||
<RecordTableBody />
|
||||
</StyledTable>
|
||||
</RecordTableContext.Provider>
|
||||
)}
|
||||
</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 (
|
||||
<RecordTableTd isSelected={isSelected} hasRightBorder={false}>
|
||||
<StyledContainer onClick={handleClick}>
|
||||
<Checkbox checked={currentRowSelected} />
|
||||
</StyledContainer>
|
||||
</RecordTableTd>
|
||||
);
|
||||
};
|
@ -1,162 +1,29 @@
|
||||
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}
|
||||
>
|
||||
<RecordTableCellBaseContainer>
|
||||
{isInEditMode ? (
|
||||
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
|
||||
) : hasSoftFocus ? (
|
||||
@ -169,8 +36,6 @@ export const RecordTableCellContainer = ({
|
||||
{nonEditModeContent}
|
||||
</RecordTableCellDisplayMode>
|
||||
)}
|
||||
</StyledBaseContainer>
|
||||
</CellHotkeyScopeContext.Provider>
|
||||
</StyledTd>
|
||||
</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,7 +36,19 @@ export const SelectAllCheckbox = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<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}
|
||||
@ -44,5 +56,6 @@ export const SelectAllCheckbox = () => {
|
||||
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,6 +132,7 @@ export const Dropdown = ({
|
||||
/>
|
||||
)}
|
||||
{isDropdownOpen && (
|
||||
<FloatingPortal>
|
||||
<DropdownMenu
|
||||
disableBlur={disableBlur}
|
||||
width={dropdownMenuWidth ?? dropdownWidth}
|
||||
@ -140,6 +142,7 @@ export const Dropdown = ({
|
||||
>
|
||||
{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'],
|
||||
|
Loading…
Reference in New Issue
Block a user