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

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

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2024-07-05 18:30:59 +02:00
committed by GitHub
parent cc6ce142ce
commit 7b3a590f79
68 changed files with 1531 additions and 1130 deletions

View File

@ -1,87 +0,0 @@
import { useState } from 'react';
import styled from '@emotion/styled';
import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordTablePendingRow } from '@/object-record/record-table/components/RecordTablePendingRow';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { isDefined } from '~/utils/isDefined';
type DraggableTableBodyProps = {
draggableItems: React.ReactNode;
objectNameSingular: string;
recordTableId: string;
};
const StyledTbody = styled.tbody`
overflow: hidden;
`;
export const DraggableTableBody = ({
objectNameSingular,
draggableItems,
recordTableId,
}: DraggableTableBodyProps) => {
const [v4Persistable] = useState(v4());
const { tableRowIdsState } = useRecordTableStates();
const tableRowIds = useRecoilValue(tableRowIdsState);
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
objectNameSingular,
});
const { currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView(recordTableId);
const viewSorts = currentViewWithCombinedFiltersAndSorts?.viewSorts || [];
const setIsRemoveSortingModalOpenState = useSetRecoilState(
isRemoveSortingModalOpenState,
);
const computeNewRowPosition = useComputeNewRowPosition();
const handleDragEnd = (result: DropResult) => {
if (viewSorts.length > 0) {
setIsRemoveSortingModalOpenState(true);
return;
}
const computeResult = computeNewRowPosition(result, tableRowIds);
if (!isDefined(computeResult)) {
return;
}
updateOneRow({
idToUpdate: computeResult.draggedRecordId,
updateOneRecordInput: {
position: computeResult.newPosition,
},
});
};
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId={v4Persistable}>
{(provided) => (
<StyledTbody
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
<RecordTablePendingRow />
{draggableItems}
{provided.placeholder}
</StyledTbody>
)}
</Droppable>
</DragDropContext>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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