mirror of
https://github.com/twentyhq/twenty.git
synced 2024-08-17 09:51:03 +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 = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: ['plugin:prettier/recommended'],
|
extends: ['plugin:prettier/recommended'],
|
||||||
plugins: [
|
plugins: ['@nx', 'prefer-arrow', 'import', 'unused-imports', 'unicorn'],
|
||||||
'@nx',
|
|
||||||
'prefer-arrow',
|
|
||||||
'import',
|
|
||||||
'simple-import-sort',
|
|
||||||
'unused-imports',
|
|
||||||
'unicorn',
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||||
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
|
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
|
||||||
@ -53,26 +46,6 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
'simple-import-sort/imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
groups: [
|
|
||||||
// Packages
|
|
||||||
['^react', '^@?\\w'],
|
|
||||||
// Internal modules
|
|
||||||
['^(@|~|src|@ui)(/.*|$)'],
|
|
||||||
// Side effect imports
|
|
||||||
['^\\u0000'],
|
|
||||||
// Relative imports
|
|
||||||
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
|
||||||
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
|
||||||
// CSS imports
|
|
||||||
['^.+\\.?(css)$'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'simple-import-sort/exports': 'error',
|
|
||||||
|
|
||||||
'unused-imports/no-unused-imports': 'warn',
|
'unused-imports/no-unused-imports': 'warn',
|
||||||
'unused-imports/no-unused-vars': [
|
'unused-imports/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
|
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -5,21 +5,24 @@
|
|||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.addMissingImports": "always"
|
"source.addMissingImports": "always",
|
||||||
|
"source.organizeImports": "always"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.addMissingImports": "always"
|
"source.addMissingImports": "always",
|
||||||
|
"source.organizeImports": "always"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.addMissingImports": "always"
|
"source.addMissingImports": "always",
|
||||||
|
"source.organizeImports": "always"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"[json]": {
|
"[json]": {
|
||||||
|
@ -9,6 +9,7 @@ import { FieldContext } from '../contexts/FieldContext';
|
|||||||
export const useIsFieldEmpty = () => {
|
export const useIsFieldEmpty = () => {
|
||||||
const { entityId, fieldDefinition, overridenIsFieldEmpty } =
|
const { entityId, fieldDefinition, overridenIsFieldEmpty } =
|
||||||
useContext(FieldContext);
|
useContext(FieldContext);
|
||||||
|
|
||||||
const fieldValue = useRecordFieldValue(
|
const fieldValue = useRecordFieldValue(
|
||||||
entityId,
|
entityId,
|
||||||
fieldDefinition?.metadata?.fieldName ?? '',
|
fieldDefinition?.metadata?.fieldName ?? '',
|
||||||
|
@ -5,7 +5,7 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
|
|||||||
import { useCombinedViewSorts } from '@/views/hooks/useCombinedViewSorts';
|
import { useCombinedViewSorts } from '@/views/hooks/useCombinedViewSorts';
|
||||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
|
|
||||||
export const RemoveSortingModal = ({
|
export const RecordIndexRemoveSortingModal = ({
|
||||||
recordTableId,
|
recordTableId,
|
||||||
}: {
|
}: {
|
||||||
recordTableId: string;
|
recordTableId: string;
|
@ -1,8 +1,8 @@
|
|||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
|
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
|
||||||
import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
|
import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
|
||||||
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
|
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
|
||||||
import { RemoveSortingModal } from '@/object-record/record-table/components/RemoveSortingModal';
|
|
||||||
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
|
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
|
||||||
|
|
||||||
type RecordIndexTableContainerProps = {
|
type RecordIndexTableContainerProps = {
|
||||||
@ -39,7 +39,7 @@ export const RecordIndexTableContainer = ({
|
|||||||
createRecord={createRecord}
|
createRecord={createRecord}
|
||||||
/>
|
/>
|
||||||
<RecordTableActionBar recordTableId={recordTableId} />
|
<RecordTableActionBar recordTableId={recordTableId} />
|
||||||
<RemoveSortingModal recordTableId={recordTableId} />
|
<RecordIndexRemoveSortingModal recordTableId={recordTableId} />
|
||||||
<RecordTableContextMenu recordTableId={recordTableId} />
|
<RecordTableContextMenu recordTableId={recordTableId} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
|
|
||||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||||
|
import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer';
|
||||||
|
|
||||||
const StyledInlineCellButtonContainer = styled.div`
|
const StyledInlineCellButtonContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -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 styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { MOBILE_VIEWPORT, RGBA } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
|
||||||
import { RecordTableBody } from '@/object-record/record-table/components/RecordTableBody';
|
import { RecordTableBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEffect';
|
||||||
import { RecordTableBodyEffect } from '@/object-record/record-table/components/RecordTableBodyEffect';
|
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
|
||||||
import { RecordTableHeader } from '@/object-record/record-table/components/RecordTableHeader';
|
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
|
||||||
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
|
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
|
|
||||||
import { useCloseRecordTableCellV2 } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2';
|
|
||||||
import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
|
|
||||||
import {
|
|
||||||
OpenTableCellArgs,
|
|
||||||
useOpenRecordTableCellV2,
|
|
||||||
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
|
|
||||||
import { useTriggerContextMenu } from '@/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu';
|
|
||||||
import { useUpsertRecordV2 } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecordV2';
|
|
||||||
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
|
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
|
||||||
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
|
|
||||||
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
|
||||||
|
|
||||||
const StyledTable = styled.table<{
|
const StyledTable = styled.table`
|
||||||
freezeFirstColumns?: boolean;
|
|
||||||
}>`
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
|
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
|
||||||
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
|
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
|
||||||
|
|
||||||
th {
|
|
||||||
border-block: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
|
||||||
padding: 0;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
:last-child {
|
|
||||||
border-right-color: transparent;
|
|
||||||
}
|
|
||||||
:first-of-type {
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
border-right: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
:last-child {
|
|
||||||
border-right-color: transparent;
|
|
||||||
}
|
|
||||||
:first-of-type {
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
|
||||||
border-right: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:nth-of-type(1),
|
|
||||||
thead th:nth-of-type(2),
|
|
||||||
thead th:nth-of-type(3) {
|
|
||||||
z-index: 12;
|
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:nth-of-type(1) {
|
|
||||||
width: 9px;
|
|
||||||
left: 0;
|
|
||||||
border-right-color: ${({ theme }) => theme.background.primary};
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:nth-of-type(2) {
|
|
||||||
left: 9px;
|
|
||||||
border-right-color: ${({ theme }) => theme.background.primary};
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:nth-of-type(3) {
|
|
||||||
left: 39px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td:nth-of-type(1),
|
|
||||||
tbody td:nth-of-type(2),
|
|
||||||
tbody td:nth-of-type(3) {
|
|
||||||
position: sticky;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td:nth-of-type(1) {
|
|
||||||
left: 0;
|
|
||||||
z-index: 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td:nth-of-type(2) {
|
|
||||||
left: 9px;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td:nth-of-type(3) {
|
|
||||||
left: 39px;
|
|
||||||
z-index: 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:nth-of-type(3),
|
|
||||||
tbody td:nth-of-type(3) {
|
|
||||||
${({ freezeFirstColumns }) =>
|
|
||||||
freezeFirstColumns &&
|
|
||||||
css`
|
|
||||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
|
||||||
width: 35px;
|
|
||||||
max-width: 35px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
height: calc(100% + 1px);
|
|
||||||
position: absolute;
|
|
||||||
width: 4px;
|
|
||||||
right: -4px;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
${({ freezeFirstColumns, theme }) =>
|
|
||||||
freezeFirstColumns &&
|
|
||||||
css`
|
|
||||||
box-shadow: 4px 0px 4px -4px ${theme.name === 'dark'
|
|
||||||
? RGBA(theme.grayScale.gray50, 0.8)
|
|
||||||
: RGBA(theme.grayScale.gray100, 0.25)} inset;
|
|
||||||
`}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type RecordTableProps = {
|
type RecordTableProps = {
|
||||||
@ -164,97 +30,27 @@ export const RecordTable = ({
|
|||||||
onColumnsChange,
|
onColumnsChange,
|
||||||
createRecord,
|
createRecord,
|
||||||
}: RecordTableProps) => {
|
}: RecordTableProps) => {
|
||||||
const { scopeId, visibleTableColumnsSelector } =
|
const { scopeId } = useRecordTableStates(recordTableId);
|
||||||
useRecordTableStates(recordTableId);
|
|
||||||
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
if (!isNonEmptyString(objectNameSingular)) {
|
||||||
objectNameSingular,
|
return <></>;
|
||||||
});
|
}
|
||||||
|
|
||||||
const { upsertRecord } = useUpsertRecordV2({
|
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleUpsertRecord = ({
|
|
||||||
persistField,
|
|
||||||
entityId,
|
|
||||||
fieldName,
|
|
||||||
}: {
|
|
||||||
persistField: () => void;
|
|
||||||
entityId: string;
|
|
||||||
fieldName: string;
|
|
||||||
}) => {
|
|
||||||
upsertRecord(persistField, entityId, fieldName, recordTableId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
|
|
||||||
|
|
||||||
const handleOpenTableCell = (args: OpenTableCellArgs) => {
|
|
||||||
openTableCell(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
|
|
||||||
|
|
||||||
const handleMoveFocus = (direction: MoveFocusDirection) => {
|
|
||||||
moveFocus(direction);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { closeTableCell } = useCloseRecordTableCellV2(recordTableId);
|
|
||||||
|
|
||||||
const handleCloseTableCell = () => {
|
|
||||||
closeTableCell();
|
|
||||||
};
|
|
||||||
|
|
||||||
const { moveSoftFocusToCell } =
|
|
||||||
useMoveSoftFocusToCellOnHoverV2(recordTableId);
|
|
||||||
|
|
||||||
const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
|
|
||||||
moveSoftFocusToCell(cellPosition);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { triggerContextMenu } = useTriggerContextMenu({
|
|
||||||
recordTableId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleContextMenu = (event: React.MouseEvent, recordId: string) => {
|
|
||||||
triggerContextMenu(event, recordId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
|
|
||||||
recordTableId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordTableScope
|
<RecordTableScope
|
||||||
recordTableScopeId={scopeId}
|
recordTableScopeId={scopeId}
|
||||||
onColumnsChange={onColumnsChange}
|
onColumnsChange={onColumnsChange}
|
||||||
>
|
>
|
||||||
{!!objectNameSingular && (
|
<RecordTableContextProvider
|
||||||
<RecordTableContext.Provider
|
objectNameSingular={objectNameSingular}
|
||||||
value={{
|
recordTableId={recordTableId}
|
||||||
objectMetadataItem,
|
>
|
||||||
onUpsertRecord: handleUpsertRecord,
|
<StyledTable className="entity-table-cell">
|
||||||
onOpenTableCell: handleOpenTableCell,
|
<RecordTableHeader createRecord={createRecord} />
|
||||||
onMoveFocus: handleMoveFocus,
|
<RecordTableBodyEffect />
|
||||||
onCloseTableCell: handleCloseTableCell,
|
<RecordTableBody />
|
||||||
onMoveSoftFocusToCell: handleMoveSoftFocusToCell,
|
</StyledTable>
|
||||||
onContextMenu: handleContextMenu,
|
</RecordTableContextProvider>
|
||||||
onCellMouseEnter: handleContainerMouseEnter,
|
|
||||||
visibleTableColumns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledTable className="entity-table-cell">
|
|
||||||
<RecordTableHeader createRecord={createRecord} />
|
|
||||||
<RecordTableBodyEffect objectNameSingular={objectNameSingular} />
|
|
||||||
<RecordTableBody
|
|
||||||
objectNameSingular={objectNameSingular}
|
|
||||||
recordTableId={recordTableId}
|
|
||||||
/>
|
|
||||||
</StyledTable>
|
|
||||||
</RecordTableContext.Provider>
|
|
||||||
)}
|
|
||||||
</RecordTableScope>
|
</RecordTableScope>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 isRemote = foundObjectMetadataItem?.isRemote ?? false;
|
||||||
|
|
||||||
|
const handleColumnsChange = useRecoilCallback(
|
||||||
|
() => (columns) => {
|
||||||
|
saveViewFields(
|
||||||
|
mapColumnDefinitionsToViewFields(
|
||||||
|
columns as ColumnDefinition<FieldMetadata>[],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[saveViewFields],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isRecordTableInitialLoading && tableRowIds.length === 0) {
|
||||||
|
return (
|
||||||
|
<RecordTableEmptyState
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
objectLabel={objectLabel}
|
||||||
|
createRecord={createRecord}
|
||||||
|
isRemote={isRemote}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityDeleteContext.Provider value={deleteOneRecord}>
|
<EntityDeleteContext.Provider value={deleteOneRecord}>
|
||||||
<ScrollWrapper>
|
<ScrollWrapper>
|
||||||
@ -85,16 +107,7 @@ export const RecordTableWithWrappers = ({
|
|||||||
<RecordTable
|
<RecordTable
|
||||||
recordTableId={recordTableId}
|
recordTableId={recordTableId}
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
onColumnsChange={useRecoilCallback(
|
onColumnsChange={handleColumnsChange}
|
||||||
() => (columns) => {
|
|
||||||
saveViewFields(
|
|
||||||
mapColumnDefinitionsToViewFields(
|
|
||||||
columns as ColumnDefinition<FieldMetadata>[],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[saveViewFields],
|
|
||||||
)}
|
|
||||||
createRecord={createRecord}
|
createRecord={createRecord}
|
||||||
/>
|
/>
|
||||||
<DragSelect
|
<DragSelect
|
||||||
@ -107,16 +120,6 @@ export const RecordTableWithWrappers = ({
|
|||||||
recordTableId={recordTableId}
|
recordTableId={recordTableId}
|
||||||
tableBodyRef={tableBodyRef}
|
tableBodyRef={tableBodyRef}
|
||||||
/>
|
/>
|
||||||
{!isRecordTableInitialLoading &&
|
|
||||||
// we cannot rely on count states because this is not available for remote objects
|
|
||||||
tableRowIds.length === 0 && (
|
|
||||||
<RecordTableEmptyState
|
|
||||||
objectNameSingular={objectNameSingular}
|
|
||||||
objectLabel={objectLabel}
|
|
||||||
createRecord={createRecord}
|
|
||||||
isRemote={isRemote}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StyledTableContainer>
|
</StyledTableContainer>
|
||||||
</StyledTableWithHeader>
|
</StyledTableWithHeader>
|
||||||
</RecordUpdateContext.Provider>
|
</RecordUpdateContext.Provider>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { ComponentDecorator } from 'twenty-ui';
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
@ -12,7 +12,6 @@ import {
|
|||||||
useSetRecordValue,
|
useSetRecordValue,
|
||||||
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/components/RecordTableCellFieldContextWrapper';
|
|
||||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
@ -21,6 +20,7 @@ import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDeco
|
|||||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||||
|
|
||||||
|
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
|
||||||
import { mockPerformance } from './mock';
|
import { mockPerformance } from './mock';
|
||||||
|
|
||||||
const objectMetadataItems = getObjectMetadataItemsMock();
|
const objectMetadataItems = getObjectMetadataItemsMock();
|
||||||
@ -73,6 +73,9 @@ const meta: Meta = {
|
|||||||
onContextMenu: () => {},
|
onContextMenu: () => {},
|
||||||
onCellMouseEnter: () => {},
|
onCellMouseEnter: () => {},
|
||||||
visibleTableColumns: mockPerformance.visibleTableColumns as any,
|
visibleTableColumns: mockPerformance.visibleTableColumns as any,
|
||||||
|
objectNameSingular:
|
||||||
|
mockPerformance.objectMetadataItem.nameSingular,
|
||||||
|
recordTableId: 'recordTableId',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RecordTableScope
|
<RecordTableScope
|
||||||
@ -92,12 +95,19 @@ const meta: Meta = {
|
|||||||
}) + mockPerformance.entityId,
|
}) + mockPerformance.entityId,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
|
isDragging: false,
|
||||||
|
dragHandleProps: null,
|
||||||
|
inView: true,
|
||||||
|
isPendingRow: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RecordTableCellContext.Provider
|
<RecordTableCellContext.Provider
|
||||||
value={{
|
value={{
|
||||||
columnDefinition: mockPerformance.fieldDefinition,
|
columnDefinition: mockPerformance.fieldDefinition,
|
||||||
columnIndex: 0,
|
columnIndex: 0,
|
||||||
|
cellPosition: { row: 0, column: 0 },
|
||||||
|
hasSoftFocus: false,
|
||||||
|
isInEditMode: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FieldContext.Provider
|
<FieldContext.Provider
|
||||||
|
@ -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 { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
|
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||||
|
|
||||||
type RecordTableRowContextProps = {
|
export type RecordTableCellContextProps = {
|
||||||
columnDefinition: ColumnDefinition<FieldMetadata>;
|
columnDefinition: ColumnDefinition<FieldMetadata>;
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
|
isInEditMode: boolean;
|
||||||
|
hasSoftFocus: boolean;
|
||||||
|
cellPosition: TableCellPosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordTableCellContext = createContext<RecordTableRowContextProps>(
|
export const RecordTableCellContext =
|
||||||
{} as RecordTableRowContextProps,
|
createContext<RecordTableCellContextProps>({} as RecordTableCellContextProps);
|
||||||
);
|
|
||||||
|
@ -26,6 +26,8 @@ export type RecordTableContextProps = {
|
|||||||
onContextMenu: (event: React.MouseEvent, recordId: string) => void;
|
onContextMenu: (event: React.MouseEvent, recordId: string) => void;
|
||||||
onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
|
onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
|
||||||
visibleTableColumns: ColumnDefinition<FieldMetadata>[];
|
visibleTableColumns: ColumnDefinition<FieldMetadata>[];
|
||||||
|
recordTableId: string;
|
||||||
|
objectNameSingular: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordTableContext = createContext<RecordTableContextProps>(
|
export const RecordTableContext = createContext<RecordTableContextProps>(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
import { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
|
||||||
|
|
||||||
export type RecordTableRowContextProps = {
|
export type RecordTableRowContextProps = {
|
||||||
pathToShowPage: string;
|
pathToShowPage: string;
|
||||||
@ -8,6 +9,9 @@ export type RecordTableRowContextProps = {
|
|||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
isPendingRow?: boolean;
|
isPendingRow?: boolean;
|
||||||
|
isDragging: boolean;
|
||||||
|
dragHandleProps: DraggableProvidedDragHandleProps | null;
|
||||||
|
inView?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordTableRowContext = createContext<RecordTableRowContextProps>(
|
export const RecordTableRowContext = createContext<RecordTableRowContextProps>(
|
||||||
|
@ -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 { ReactNode, useContext } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import { DragDropContext, DropResult } from '@hello-pangea/dnd';
|
||||||
import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd';
|
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { RecordTablePendingRow } from '@/object-record/record-table/components/RecordTablePendingRow';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition';
|
import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition';
|
||||||
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
|
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
|
||||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
type DraggableTableBodyProps = {
|
export const RecordTableBodyDragDropContext = ({
|
||||||
draggableItems: React.ReactNode;
|
children,
|
||||||
objectNameSingular: string;
|
}: {
|
||||||
recordTableId: string;
|
children: ReactNode;
|
||||||
};
|
}) => {
|
||||||
|
const { objectNameSingular, recordTableId } = useContext(RecordTableContext);
|
||||||
const StyledTbody = styled.tbody`
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DraggableTableBody = ({
|
|
||||||
objectNameSingular,
|
|
||||||
draggableItems,
|
|
||||||
recordTableId,
|
|
||||||
}: DraggableTableBodyProps) => {
|
|
||||||
const [v4Persistable] = useState(v4());
|
|
||||||
|
|
||||||
const { tableRowIdsState } = useRecordTableStates();
|
|
||||||
|
|
||||||
const tableRowIds = useRecoilValue(tableRowIdsState);
|
|
||||||
|
|
||||||
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
|
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { tableRowIdsState } = useRecordTableStates();
|
||||||
|
|
||||||
|
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||||
|
|
||||||
const { currentViewWithCombinedFiltersAndSorts } =
|
const { currentViewWithCombinedFiltersAndSorts } =
|
||||||
useGetCurrentView(recordTableId);
|
useGetCurrentView(recordTableId);
|
||||||
|
|
||||||
@ -45,6 +33,7 @@ export const DraggableTableBody = ({
|
|||||||
const setIsRemoveSortingModalOpenState = useSetRecoilState(
|
const setIsRemoveSortingModalOpenState = useSetRecoilState(
|
||||||
isRemoveSortingModalOpenState,
|
isRemoveSortingModalOpenState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const computeNewRowPosition = useComputeNewRowPosition();
|
const computeNewRowPosition = useComputeNewRowPosition();
|
||||||
|
|
||||||
const handleDragEnd = (result: DropResult) => {
|
const handleDragEnd = (result: DropResult) => {
|
||||||
@ -68,20 +57,6 @@ export const DraggableTableBody = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={handleDragEnd}>
|
<DragDropContext onDragEnd={handleDragEnd}>{children}</DragDropContext>
|
||||||
<Droppable droppableId={v4Persistable}>
|
|
||||||
{(provided) => (
|
|
||||||
<StyledTbody
|
|
||||||
ref={provided.innerRef}
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
||||||
{...provided.droppableProps}
|
|
||||||
>
|
|
||||||
<RecordTablePendingRow />
|
|
||||||
{draggableItems}
|
|
||||||
{provided.placeholder}
|
|
||||||
</StyledTbody>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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 { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { CheckboxCell } from '@/object-record/record-table/components/CheckboxCell';
|
|
||||||
import { GripCell } from '@/object-record/record-table/components/GripCell';
|
|
||||||
import {
|
|
||||||
StyledTd,
|
|
||||||
StyledTr,
|
|
||||||
} from '@/object-record/record-table/components/RecordTableRow';
|
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
|
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
|
||||||
|
import { RecordTableCellGrip } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip';
|
||||||
import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading';
|
import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading';
|
||||||
|
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
|
||||||
|
|
||||||
export const RecordTableBodyLoading = () => {
|
export const RecordTableBodyLoading = () => {
|
||||||
const { visibleTableColumnsSelector } = useRecordTableStates();
|
const { visibleTableColumnsSelector } = useRecordTableStates();
|
||||||
@ -16,22 +13,18 @@ export const RecordTableBodyLoading = () => {
|
|||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
{Array.from({ length: 8 }).map((_, rowIndex) => (
|
{Array.from({ length: 8 }).map((_, rowIndex) => (
|
||||||
<StyledTr
|
<RecordTableTr
|
||||||
isDragging={false}
|
isDragging={false}
|
||||||
data-testid={`row-id-${rowIndex}`}
|
data-testid={`row-id-${rowIndex}`}
|
||||||
data-selectable-id={`row-id-${rowIndex}`}
|
data-selectable-id={`row-id-${rowIndex}`}
|
||||||
key={rowIndex}
|
key={rowIndex}
|
||||||
>
|
>
|
||||||
<StyledTd data-select-disable>
|
<RecordTableCellGrip />
|
||||||
<GripCell isDragging={false} />
|
<RecordTableCellCheckbox />
|
||||||
</StyledTd>
|
|
||||||
<StyledTd>
|
|
||||||
<CheckboxCell />
|
|
||||||
</StyledTd>
|
|
||||||
{visibleTableColumns.map((column) => (
|
{visibleTableColumns.map((column) => (
|
||||||
<RecordTableCellLoading key={column.fieldMetadataId} />
|
<RecordTableCellLoading key={column.fieldMetadataId} />
|
||||||
))}
|
))}
|
||||||
</StyledTr>
|
</RecordTableTr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
);
|
);
|
@ -1,109 +1,13 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
|
|
||||||
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
||||||
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
|
||||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
|
||||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
|
||||||
import { RecordTableCellContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellContainer';
|
import { RecordTableCellContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellContainer';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput';
|
||||||
|
|
||||||
export const RecordTableCell = ({
|
|
||||||
customHotkeyScope,
|
|
||||||
}: {
|
|
||||||
customHotkeyScope: HotkeyScope;
|
|
||||||
}) => {
|
|
||||||
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
|
|
||||||
useContext(RecordTableContext);
|
|
||||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
|
||||||
const { isReadOnly } = useContext(RecordTableRowContext);
|
|
||||||
|
|
||||||
const handleEnter: FieldInputEvent = (persistField) => {
|
|
||||||
onUpsertRecord({
|
|
||||||
persistField,
|
|
||||||
entityId,
|
|
||||||
fieldName: fieldDefinition.metadata.fieldName,
|
|
||||||
});
|
|
||||||
|
|
||||||
onCloseTableCell();
|
|
||||||
onMoveFocus('down');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit: FieldInputEvent = (persistField) => {
|
|
||||||
onUpsertRecord({
|
|
||||||
persistField,
|
|
||||||
entityId,
|
|
||||||
fieldName: fieldDefinition.metadata.fieldName,
|
|
||||||
});
|
|
||||||
|
|
||||||
onCloseTableCell();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
onCloseTableCell();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickOutside: FieldInputEvent = (persistField) => {
|
|
||||||
onUpsertRecord({
|
|
||||||
persistField,
|
|
||||||
entityId,
|
|
||||||
fieldName: fieldDefinition.metadata.fieldName,
|
|
||||||
});
|
|
||||||
|
|
||||||
onCloseTableCell();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEscape: FieldInputEvent = (persistField) => {
|
|
||||||
onUpsertRecord({
|
|
||||||
persistField,
|
|
||||||
entityId,
|
|
||||||
fieldName: fieldDefinition.metadata.fieldName,
|
|
||||||
});
|
|
||||||
|
|
||||||
onCloseTableCell();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTab: FieldInputEvent = (persistField) => {
|
|
||||||
onUpsertRecord({
|
|
||||||
persistField,
|
|
||||||
entityId,
|
|
||||||
fieldName: fieldDefinition.metadata.fieldName,
|
|
||||||
});
|
|
||||||
|
|
||||||
onCloseTableCell();
|
|
||||||
onMoveFocus('right');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleShiftTab: FieldInputEvent = (persistField) => {
|
|
||||||
onUpsertRecord({
|
|
||||||
persistField,
|
|
||||||
entityId,
|
|
||||||
fieldName: fieldDefinition.metadata.fieldName,
|
|
||||||
});
|
|
||||||
|
|
||||||
onCloseTableCell();
|
|
||||||
onMoveFocus('left');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export const RecordTableCell = () => {
|
||||||
return (
|
return (
|
||||||
<FieldFocusContextProvider>
|
<FieldFocusContextProvider>
|
||||||
<RecordTableCellContainer
|
<RecordTableCellContainer
|
||||||
editHotkeyScope={customHotkeyScope}
|
editModeContent={<RecordTableCellFieldInput />}
|
||||||
editModeContent={
|
|
||||||
<FieldInput
|
|
||||||
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
onClickOutside={handleClickOutside}
|
|
||||||
onEnter={handleEnter}
|
|
||||||
onEscape={handleEscape}
|
|
||||||
onShiftTab={handleShiftTab}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onTab={handleTab}
|
|
||||||
isReadOnly={isReadOnly}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
nonEditModeContent={<FieldDisplay />}
|
nonEditModeContent={<FieldDisplay />}
|
||||||
/>
|
/>
|
||||||
</FieldFocusContextProvider>
|
</FieldFocusContextProvider>
|
||||||
|
@ -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 styled from '@emotion/styled';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
|
|
||||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||||
|
import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer';
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
const StyledButtonContainer = styled.div`
|
||||||
margin: ${({ theme }) => theme.spacing(1)};
|
margin: ${({ theme }) => theme.spacing(1)};
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useCallback, useContext } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useCallback, useContext } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
|
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
|
||||||
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
|
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
|
||||||
import { Checkbox } from '@/ui/input/components/Checkbox';
|
import { Checkbox } from '@/ui/input/components/Checkbox';
|
||||||
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
|
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
|
||||||
@ -18,7 +19,9 @@ const StyledContainer = styled.div`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CheckboxCell = () => {
|
export const RecordTableCellCheckbox = () => {
|
||||||
|
const { isSelected } = useContext(RecordTableRowContext);
|
||||||
|
|
||||||
const { recordId } = useContext(RecordTableRowContext);
|
const { recordId } = useContext(RecordTableRowContext);
|
||||||
const { isRowSelectedFamilyState } = useRecordTableStates();
|
const { isRowSelectedFamilyState } = useRecordTableStates();
|
||||||
const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
|
const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
|
||||||
@ -31,8 +34,10 @@ export const CheckboxCell = () => {
|
|||||||
}, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]);
|
}, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer onClick={handleClick}>
|
<RecordTableTd isSelected={isSelected} hasRightBorder={false}>
|
||||||
<Checkbox checked={currentRowSelected} />
|
<StyledContainer onClick={handleClick}>
|
||||||
</StyledContainer>
|
<Checkbox checked={currentRowSelected} />
|
||||||
|
</StyledContainer>
|
||||||
|
</RecordTableTd>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,176 +1,41 @@
|
|||||||
import React, { ReactElement, useContext } from 'react';
|
import { ReactElement, useContext } from 'react';
|
||||||
import { styled } from '@linaria/react';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { BORDER_COMMON, ThemeContext } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableCellBaseContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer';
|
||||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
|
||||||
import { RecordTableCellSoftFocusMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode';
|
import { RecordTableCellSoftFocusMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode';
|
||||||
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
|
|
||||||
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
|
|
||||||
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
|
|
||||||
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
|
|
||||||
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
|
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
|
||||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
|
||||||
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
|
||||||
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
|
|
||||||
|
|
||||||
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
|
|
||||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
|
||||||
|
|
||||||
import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode';
|
import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode';
|
||||||
import { RecordTableCellEditMode } from './RecordTableCellEditMode';
|
import { RecordTableCellEditMode } from './RecordTableCellEditMode';
|
||||||
|
|
||||||
const StyledTd = styled.td<{
|
|
||||||
isInEditMode: boolean;
|
|
||||||
backgroundColor: string;
|
|
||||||
}>`
|
|
||||||
background: ${({ backgroundColor }) => backgroundColor};
|
|
||||||
z-index: ${({ isInEditMode }) => (isInEditMode ? '4 !important' : 3)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const borderRadiusSm = BORDER_COMMON.radius.sm;
|
|
||||||
|
|
||||||
const StyledBaseContainer = styled.div<{
|
|
||||||
hasSoftFocus: boolean;
|
|
||||||
fontColorExtraLight: string;
|
|
||||||
backgroundColorTransparentSecondary: string;
|
|
||||||
}>`
|
|
||||||
align-items: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
height: 32px;
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
background: ${({ hasSoftFocus, backgroundColorTransparentSecondary }) =>
|
|
||||||
hasSoftFocus ? backgroundColorTransparentSecondary : 'none'};
|
|
||||||
|
|
||||||
border-radius: ${({ hasSoftFocus }) =>
|
|
||||||
hasSoftFocus ? borderRadiusSm : 'none'};
|
|
||||||
|
|
||||||
border: ${({ hasSoftFocus, fontColorExtraLight }) =>
|
|
||||||
hasSoftFocus ? `1px solid ${fontColorExtraLight}` : 'none'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type RecordTableCellContainerProps = {
|
export type RecordTableCellContainerProps = {
|
||||||
editModeContent: ReactElement;
|
editModeContent: ReactElement;
|
||||||
nonEditModeContent: ReactElement;
|
nonEditModeContent: ReactElement;
|
||||||
editHotkeyScope?: HotkeyScope;
|
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
maxContentWidth?: number;
|
maxContentWidth?: number;
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_CELL_SCOPE: HotkeyScope = {
|
|
||||||
scope: TableHotkeyScope.CellEditMode,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RecordTableCellContainer = ({
|
export const RecordTableCellContainer = ({
|
||||||
editModeContent,
|
editModeContent,
|
||||||
nonEditModeContent,
|
nonEditModeContent,
|
||||||
editHotkeyScope,
|
|
||||||
}: RecordTableCellContainerProps) => {
|
}: RecordTableCellContainerProps) => {
|
||||||
const { theme } = useContext(ThemeContext);
|
const { hasSoftFocus, isInEditMode } = useContext(RecordTableCellContext);
|
||||||
|
|
||||||
const { setIsFocused } = useFieldFocus();
|
|
||||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
|
||||||
|
|
||||||
const { isSelected, recordId } = useContext(RecordTableRowContext);
|
|
||||||
|
|
||||||
const { onMoveSoftFocusToCell, onContextMenu, onCellMouseEnter } =
|
|
||||||
useContext(RecordTableContext);
|
|
||||||
|
|
||||||
const tableScopeId = useAvailableScopeIdOrThrow(
|
|
||||||
RecordTableScopeInternalContext,
|
|
||||||
getScopeIdOrUndefinedFromComponentId(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isTableCellInEditModeFamilyState = extractComponentFamilyState(
|
|
||||||
isTableCellInEditModeComponentFamilyState,
|
|
||||||
tableScopeId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSoftFocusOnTableCellFamilyState = extractComponentFamilyState(
|
|
||||||
isSoftFocusOnTableCellComponentFamilyState,
|
|
||||||
tableScopeId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const cellPosition = useCurrentTableCellPosition();
|
|
||||||
|
|
||||||
const isInEditMode = useRecoilValue(
|
|
||||||
isTableCellInEditModeFamilyState(cellPosition),
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasSoftFocus = useRecoilValue(
|
|
||||||
isSoftFocusOnTableCellFamilyState(cellPosition),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleContextMenu = (event: React.MouseEvent) => {
|
|
||||||
onContextMenu(event, recordId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContainerMouseMove = () => {
|
|
||||||
setIsFocused(true);
|
|
||||||
if (!hasSoftFocus) {
|
|
||||||
onCellMouseEnter({
|
|
||||||
cellPosition,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContainerMouseLeave = () => {
|
|
||||||
setIsFocused(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContainerClick = () => {
|
|
||||||
if (!hasSoftFocus) {
|
|
||||||
onMoveSoftFocusToCell(cellPosition);
|
|
||||||
openTableCell();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const tdBackgroundColor = isSelected
|
|
||||||
? theme.accent.quaternary
|
|
||||||
: theme.background.primary;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTd
|
<RecordTableCellBaseContainer>
|
||||||
backgroundColor={tdBackgroundColor}
|
{isInEditMode ? (
|
||||||
isInEditMode={isInEditMode}
|
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
|
||||||
onContextMenu={handleContextMenu}
|
) : hasSoftFocus ? (
|
||||||
>
|
<RecordTableCellSoftFocusMode
|
||||||
<CellHotkeyScopeContext.Provider
|
editModeContent={editModeContent}
|
||||||
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
|
nonEditModeContent={nonEditModeContent}
|
||||||
>
|
/>
|
||||||
<StyledBaseContainer
|
) : (
|
||||||
onMouseLeave={handleContainerMouseLeave}
|
<RecordTableCellDisplayMode>
|
||||||
onMouseMove={handleContainerMouseMove}
|
{nonEditModeContent}
|
||||||
onClick={handleContainerClick}
|
</RecordTableCellDisplayMode>
|
||||||
backgroundColorTransparentSecondary={
|
)}
|
||||||
theme.background.transparent.secondary
|
</RecordTableCellBaseContainer>
|
||||||
}
|
|
||||||
fontColorExtraLight={theme.font.color.extraLight}
|
|
||||||
hasSoftFocus={hasSoftFocus}
|
|
||||||
>
|
|
||||||
{isInEditMode ? (
|
|
||||||
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
|
|
||||||
) : hasSoftFocus ? (
|
|
||||||
<RecordTableCellSoftFocusMode
|
|
||||||
editModeContent={editModeContent}
|
|
||||||
nonEditModeContent={nonEditModeContent}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<RecordTableCellDisplayMode>
|
|
||||||
{nonEditModeContent}
|
|
||||||
</RecordTableCellDisplayMode>
|
|
||||||
)}
|
|
||||||
</StyledBaseContainer>
|
|
||||||
</CellHotkeyScopeContext.Provider>
|
|
||||||
</StyledTd>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,8 +10,6 @@ const StyledOuterContainer = styled.div<{
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
margin: ${({ hasSoftFocus }) => (hasSoftFocus === true ? '-1px' : 'none')};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInnerContainer = styled.div`
|
const StyledInnerContainer = styled.div`
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useContext } from 'react';
|
import { ReactNode, useContext } from 'react';
|
||||||
|
|
||||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
@ -8,13 +8,16 @@ import { RecordUpdateContext } from '@/object-record/record-table/contexts/Entit
|
|||||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell';
|
|
||||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
|
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
export const RecordTableCellFieldContextWrapper = () => {
|
export const RecordTableCellFieldContextWrapper = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) => {
|
||||||
const { objectMetadataItem } = useContext(RecordTableContext);
|
const { objectMetadataItem } = useContext(RecordTableContext);
|
||||||
const { columnDefinition } = useContext(RecordTableCellContext);
|
const { columnDefinition } = useContext(RecordTableCellContext);
|
||||||
const { recordId, pathToShowPage } = useContext(RecordTableRowContext);
|
const { recordId, pathToShowPage } = useContext(RecordTableRowContext);
|
||||||
@ -49,7 +52,7 @@ export const RecordTableCellFieldContextWrapper = () => {
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RecordTableCell customHotkeyScope={{ scope: customHotkeyScope }} />
|
{children}
|
||||||
</FieldContext.Provider>
|
</FieldContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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 { RecordTableCellSkeletonLoader } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader';
|
||||||
|
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
|
||||||
|
|
||||||
export const RecordTableCellLoading = () => {
|
export const RecordTableCellLoading = () => {
|
||||||
return (
|
return (
|
||||||
<StyledTd>
|
<RecordTableTd>
|
||||||
<RecordTableCellSkeletonLoader />
|
<RecordTableCellSkeletonLoader />
|
||||||
</StyledTd>
|
</RecordTableTd>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 { RecordTableRowContextProps } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const recordTableRow: RecordTableRowContextProps = {
|
export const recordTableRow: RecordTableRowContextProps = {
|
||||||
@ -10,12 +9,13 @@ export const recordTableRow: RecordTableRowContextProps = {
|
|||||||
pathToShowPage: '/',
|
pathToShowPage: '/',
|
||||||
objectNameSingular: 'objectNameSingular',
|
objectNameSingular: 'objectNameSingular',
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
|
dragHandleProps: {} as any,
|
||||||
|
isDragging: false,
|
||||||
|
inView: true,
|
||||||
|
isPendingRow: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const recordTableCell: {
|
export const recordTableCell:RecordTableCellContextProps= {
|
||||||
columnDefinition: ColumnDefinition<FieldMetadata>;
|
|
||||||
columnIndex: number;
|
|
||||||
} = {
|
|
||||||
columnIndex: 3,
|
columnIndex: 3,
|
||||||
columnDefinition: {
|
columnDefinition: {
|
||||||
size: 1,
|
size: 1,
|
||||||
@ -29,4 +29,10 @@ export const recordTableCell: {
|
|||||||
fieldName: 'fieldName',
|
fieldName: 'fieldName',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
cellPosition: {
|
||||||
|
row: 2,
|
||||||
|
column: 3,
|
||||||
|
},
|
||||||
|
hasSoftFocus: false,
|
||||||
|
isInEditMode: false,
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,9 @@
|
|||||||
import { useContext, useMemo } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
|
||||||
|
|
||||||
import { TableCellPosition } from '../../types/TableCellPosition';
|
|
||||||
|
|
||||||
export const useCurrentTableCellPosition = () => {
|
export const useCurrentTableCellPosition = () => {
|
||||||
const { rowIndex } = useContext(RecordTableRowContext);
|
const { cellPosition } = useContext(RecordTableCellContext);
|
||||||
const { columnIndex } = useContext(RecordTableCellContext);
|
|
||||||
|
|
||||||
const currentTableCellPosition: TableCellPosition = useMemo(
|
return cellPosition;
|
||||||
() => ({
|
|
||||||
column: columnIndex,
|
|
||||||
row: rowIndex,
|
|
||||||
}),
|
|
||||||
[columnIndex, rowIndex],
|
|
||||||
);
|
|
||||||
|
|
||||||
return currentTableCellPosition;
|
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { css, useTheme } from '@emotion/react';
|
import { css, useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { MOBILE_VIEWPORT, useIcons } from 'twenty-ui';
|
import { MOBILE_VIEWPORT, useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
|
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
|
||||||
|
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||||
|
|
||||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
import { ColumnDefinition } from '../../types/ColumnDefinition';
|
||||||
|
|
||||||
type ColumnHeadProps = {
|
type RecordTableColumnHeadProps = {
|
||||||
column: ColumnDefinition<FieldMetadata>;
|
column: ColumnDefinition<FieldMetadata>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,16 +46,22 @@ const StyledText = styled.span`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ColumnHead = ({ column }: ColumnHeadProps) => {
|
export const RecordTableColumnHead = ({
|
||||||
|
column,
|
||||||
|
}: RecordTableColumnHeadProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const Icon = getIcon(column.iconName);
|
const Icon = getIcon(column.iconName);
|
||||||
|
|
||||||
const scrollLeft = useRecoilValue(scrollLeftState);
|
const isRecordTableScrolledLeft = useRecoilComponentValue(
|
||||||
|
isRecordTableScrolledLeftComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTitle hideTitle={!!column.isLabelIdentifier && scrollLeft > 0}>
|
<StyledTitle
|
||||||
|
hideTitle={!!column.isLabelIdentifier && !isRecordTableScrolledLeft}
|
||||||
|
>
|
||||||
<StyledIcon>
|
<StyledIcon>
|
||||||
<Icon size={theme.icon.size.md} />
|
<Icon size={theme.icon.size.md} />
|
||||||
</StyledIcon>
|
</StyledIcon>
|
@ -14,16 +14,16 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
|
|||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
|
||||||
import { useTableColumns } from '../hooks/useTableColumns';
|
import { useTableColumns } from '../../hooks/useTableColumns';
|
||||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
import { ColumnDefinition } from '../../types/ColumnDefinition';
|
||||||
|
|
||||||
export type RecordTableColumnDropdownMenuProps = {
|
export type RecordTableColumnHeadDropdownMenuProps = {
|
||||||
column: ColumnDefinition<FieldMetadata>;
|
column: ColumnDefinition<FieldMetadata>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordTableColumnDropdownMenu = ({
|
export const RecordTableColumnHeadDropdownMenu = ({
|
||||||
column,
|
column,
|
||||||
}: RecordTableColumnDropdownMenuProps) => {
|
}: RecordTableColumnHeadDropdownMenuProps) => {
|
||||||
const {
|
const {
|
||||||
visibleTableColumnsSelector,
|
visibleTableColumnsSelector,
|
||||||
onToggleColumnFilterState,
|
onToggleColumnFilterState,
|
@ -4,26 +4,29 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
|
|||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
|
||||||
import { ColumnHead } from './ColumnHead';
|
import { RecordTableColumnHeadDropdownMenu } from './RecordTableColumnHeadDropdownMenu';
|
||||||
import { RecordTableColumnDropdownMenu } from './RecordTableColumnDropdownMenu';
|
|
||||||
|
|
||||||
type ColumnHeadWithDropdownProps = {
|
import { RecordTableColumnHead } from './RecordTableColumnHead';
|
||||||
|
|
||||||
|
type RecordTableColumnHeadWithDropdownProps = {
|
||||||
column: ColumnDefinition<FieldMetadata>;
|
column: ColumnDefinition<FieldMetadata>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledDropdown = styled(Dropdown)`
|
const StyledDropdown = styled(Dropdown)`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ColumnHeadWithDropdown = ({
|
export const RecordTableColumnHeadWithDropdown = ({
|
||||||
column,
|
column,
|
||||||
}: ColumnHeadWithDropdownProps) => {
|
}: RecordTableColumnHeadWithDropdownProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledDropdown
|
<StyledDropdown
|
||||||
dropdownId={column.fieldMetadataId + '-header'}
|
dropdownId={column.fieldMetadataId + '-header'}
|
||||||
clickableComponent={<ColumnHead column={column} />}
|
clickableComponent={<RecordTableColumnHead column={column} />}
|
||||||
dropdownComponents={<RecordTableColumnDropdownMenu column={column} />}
|
dropdownComponents={<RecordTableColumnHeadDropdownMenu column={column} />}
|
||||||
dropdownOffset={{ x: -1 }}
|
dropdownOffset={{ x: -1 }}
|
||||||
dropdownPlacement="bottom-start"
|
dropdownPlacement="bottom-start"
|
||||||
dropdownHotkeyScope={{ scope: column.fieldMetadataId + '-header' }}
|
dropdownHotkeyScope={{ scope: column.fieldMetadataId + '-header' }}
|
@ -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 styled from '@emotion/styled';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { IconPlus } from 'twenty-ui';
|
import { IconPlus } from 'twenty-ui';
|
||||||
|
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
|
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
|
||||||
|
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
|
||||||
|
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
|
||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
|
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||||
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
||||||
|
|
||||||
import { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown';
|
|
||||||
|
|
||||||
const COLUMN_MIN_WIDTH = 104;
|
const COLUMN_MIN_WIDTH = 104;
|
||||||
|
|
||||||
const StyledColumnHeaderCell = styled.th<{
|
const StyledColumnHeaderCell = styled.th<{
|
||||||
columnWidth: number;
|
columnWidth: number;
|
||||||
isResizing?: boolean;
|
isResizing?: boolean;
|
||||||
}>`
|
}>`
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
|
border-right: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
${({ columnWidth }) => `
|
${({ columnWidth }) => `
|
||||||
min-width: ${columnWidth}px;
|
min-width: ${columnWidth}px;
|
||||||
width: ${columnWidth}px;
|
width: ${columnWidth}px;
|
||||||
@ -165,11 +173,14 @@ export const RecordTableHeaderCell = ({
|
|||||||
onMouseUp: handleResizeHandlerEnd,
|
onMouseUp: handleResizeHandlerEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isRecordTableScrolledLeft = useRecoilComponentValue(
|
||||||
|
isRecordTableScrolledLeftComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const scrollLeft = useRecoilValue(scrollLeftState);
|
|
||||||
|
|
||||||
const disableColumnResize =
|
const disableColumnResize =
|
||||||
column.isLabelIdentifier && isMobile && scrollLeft > 0;
|
column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledColumnHeaderCell
|
<StyledColumnHeaderCell
|
||||||
@ -185,7 +196,7 @@ export const RecordTableHeaderCell = ({
|
|||||||
onMouseLeave={() => setIconVisibility(false)}
|
onMouseLeave={() => setIconVisibility(false)}
|
||||||
>
|
>
|
||||||
<StyledColumnHeadContainer>
|
<StyledColumnHeadContainer>
|
||||||
<ColumnHeadWithDropdown column={column} />
|
<RecordTableColumnHeadWithDropdown column={column} />
|
||||||
{(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && (
|
{(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && (
|
||||||
<StyledHeaderIcon>
|
<StyledHeaderIcon>
|
||||||
<LightIconButton
|
<LightIconButton
|
@ -2,9 +2,9 @@ import styled from '@emotion/styled';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { Checkbox } from '@/ui/input/components/Checkbox';
|
import { Checkbox } from '@/ui/input/components/Checkbox';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import { useRecordTable } from '../hooks/useRecordTable';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -16,7 +16,7 @@ const StyledContainer = styled.div`
|
|||||||
background-color: ${({ theme }) => theme.background.primary};
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SelectAllCheckbox = () => {
|
export const RecordTableHeaderCheckboxColumn = () => {
|
||||||
const { allRowsSelectedStatusSelector } = useRecordTableStates();
|
const { allRowsSelectedStatusSelector } = useRecordTableStates();
|
||||||
|
|
||||||
const allRowsSelectedStatus = useRecoilValue(allRowsSelectedStatusSelector());
|
const allRowsSelectedStatus = useRecoilValue(allRowsSelectedStatusSelector());
|
||||||
@ -36,13 +36,26 @@ export const SelectAllCheckbox = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<th
|
||||||
<Checkbox
|
style={{
|
||||||
checked={checked}
|
borderBottom: `1px solid ${theme.border.color.light}`,
|
||||||
onChange={onChange}
|
borderTop: `1px solid ${theme.border.color.light}`,
|
||||||
indeterminate={indeterminate}
|
width: 30,
|
||||||
/>
|
minWidth: 30,
|
||||||
</StyledContainer>
|
maxWidth: 30,
|
||||||
|
borderRight: 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledContainer>
|
||||||
|
<Checkbox
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
indeterminate={indeterminate}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
</th>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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 { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { RecordTableRow } from '@/object-record/record-table/components/RecordTableRow';
|
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
|
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
|
||||||
|
|
||||||
export const RecordTablePendingRow = () => {
|
export const RecordTablePendingRow = () => {
|
||||||
const { pendingRecordIdState } = useRecordTableStates();
|
const { pendingRecordIdState } = useRecordTableStates();
|
||||||
const pendingRecordId = useRecoilValue(pendingRecordIdState);
|
const pendingRecordId = useRecoilValue(pendingRecordIdState);
|
||||||
|
|
||||||
if (!pendingRecordId) return;
|
if (!pendingRecordId) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordTableRow
|
<RecordTableRow
|
@ -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 {
|
import {
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
flip,
|
flip,
|
||||||
|
FloatingPortal,
|
||||||
offset,
|
offset,
|
||||||
Placement,
|
Placement,
|
||||||
useFloating,
|
useFloating,
|
||||||
} from '@floating-ui/react';
|
} from '@floating-ui/react';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { Keys } from 'react-hotkeys-hook';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
@ -85,7 +86,7 @@ export const Dropdown = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [containerRef],
|
refs: [refs.floating],
|
||||||
callback: () => {
|
callback: () => {
|
||||||
onClickOutside?.();
|
onClickOutside?.();
|
||||||
|
|
||||||
@ -131,15 +132,17 @@ export const Dropdown = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isDropdownOpen && (
|
{isDropdownOpen && (
|
||||||
<DropdownMenu
|
<FloatingPortal>
|
||||||
disableBlur={disableBlur}
|
<DropdownMenu
|
||||||
width={dropdownMenuWidth ?? dropdownWidth}
|
disableBlur={disableBlur}
|
||||||
data-select-disable
|
width={dropdownMenuWidth ?? dropdownWidth}
|
||||||
ref={refs.setFloating}
|
data-select-disable
|
||||||
style={floatingStyles}
|
ref={refs.setFloating}
|
||||||
>
|
style={floatingStyles}
|
||||||
{dropdownComponents}
|
>
|
||||||
</DropdownMenu>
|
{dropdownComponents}
|
||||||
|
</DropdownMenu>
|
||||||
|
</FloatingPortal>
|
||||||
)}
|
)}
|
||||||
<DropdownOnToggleEffect
|
<DropdownOnToggleEffect
|
||||||
onDropdownClose={onClose}
|
onDropdownClose={onClose}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Chip, ChipVariant } from 'twenty-ui';
|
import { Chip, ChipVariant } from 'twenty-ui';
|
||||||
|
|
||||||
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
|
|
||||||
import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown';
|
import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown';
|
||||||
import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement';
|
import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement';
|
||||||
|
import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import React from 'react';
|
||||||
const StyledAnimatedChipContainer = styled(motion.div)``;
|
|
||||||
|
|
||||||
export const AnimatedContainer = ({
|
export const AnimatedContainer = ({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => (
|
}) => (
|
||||||
<StyledAnimatedChipContainer
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.1 }}
|
transition={{ duration: 0.1 }}
|
||||||
whileHover={{ scale: 1.04 }}
|
whileHover={{ scale: 1.04 }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledAnimatedChipContainer>
|
</motion.div>
|
||||||
);
|
);
|
@ -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';
|
import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey';
|
||||||
|
|
||||||
|
type CreateComponentStateType<ValueType> = {
|
||||||
|
key: string;
|
||||||
|
defaultValue: ValueType;
|
||||||
|
effects?: AtomEffect<ValueType>[];
|
||||||
|
};
|
||||||
|
|
||||||
export const createComponentState = <ValueType>({
|
export const createComponentState = <ValueType>({
|
||||||
key,
|
key,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
effects,
|
effects,
|
||||||
}: {
|
}: CreateComponentStateType<ValueType>) => {
|
||||||
key: string;
|
|
||||||
defaultValue: ValueType;
|
|
||||||
effects?: AtomEffect<ValueType>[];
|
|
||||||
}) => {
|
|
||||||
return atomFamily<ValueType, ComponentStateKey>({
|
return atomFamily<ValueType, ComponentStateKey>({
|
||||||
key,
|
key,
|
||||||
default: defaultValue,
|
default: defaultValue,
|
||||||
|
@ -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',
|
'**/RecordTableCellContainer.tsx',
|
||||||
'**/RecordTableCellDisplayContainer.tsx',
|
'**/RecordTableCellDisplayContainer.tsx',
|
||||||
'**/Avatar.tsx',
|
'**/Avatar.tsx',
|
||||||
|
'**/RecordTableBodyDroppable.tsx',
|
||||||
|
'**/RecordTableCellBaseContainer.tsx',
|
||||||
|
'**/RecordTableCellTd.tsx',
|
||||||
|
'**/RecordTableTd.tsx',
|
||||||
|
'**/RecordTableHeaderDragDropColumn.tsx',
|
||||||
],
|
],
|
||||||
babelOptions: {
|
babelOptions: {
|
||||||
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
||||||
|
Loading…
Reference in New Issue
Block a user