feat: display Links field as Expandable List (#5374)

Closes #5114
This commit is contained in:
Thaïs
2024-05-15 15:52:23 +02:00
committed by GitHub
parent 38eb293b3c
commit 602d5422a2
23 changed files with 440 additions and 454 deletions

View File

@ -3,7 +3,6 @@ import { useContext } from 'react';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
import { FieldContext } from '../contexts/FieldContext';
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
@ -38,13 +37,17 @@ import { isFieldSelect } from '../types/guards/isFieldSelect';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldUuid } from '../types/guards/isFieldUuid';
type FieldDisplayProps = ExpandableListProps;
type FieldDisplayProps = {
isCellSoftFocused?: boolean;
cellElement?: HTMLElement;
fromTableCell?: boolean;
};
export const FieldDisplay = ({
isHovered,
reference,
isCellSoftFocused,
cellElement,
fromTableCell,
}: FieldDisplayProps & { fromTableCell?: boolean }) => {
}: FieldDisplayProps) => {
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
const isChipDisplay =
@ -75,7 +78,11 @@ export const FieldDisplay = ({
) : isFieldLink(fieldDefinition) ? (
<LinkFieldDisplay />
) : isFieldLinks(fieldDefinition) ? (
<LinksFieldDisplay />
<LinksFieldDisplay
isCellSoftFocused={isCellSoftFocused}
cellElement={cellElement}
fromTableCell={fromTableCell}
/>
) : isFieldCurrency(fieldDefinition) ? (
<CurrencyFieldDisplay />
) : isFieldFullName(fieldDefinition) ? (
@ -84,9 +91,9 @@ export const FieldDisplay = ({
<SelectFieldDisplay />
) : isFieldMultiSelect(fieldDefinition) ? (
<MultiSelectFieldDisplay
isHovered={isHovered}
reference={reference}
withDropDownBorder={fromTableCell}
isCellSoftFocused={isCellSoftFocused}
cellElement={cellElement}
fromTableCell={fromTableCell}
/>
) : isFieldAddress(fieldDefinition) ? (
<AddressFieldDisplay />

View File

@ -1,8 +1,25 @@
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
export const LinksFieldDisplay = () => {
type LinksFieldDisplayProps = {
isCellSoftFocused?: boolean;
cellElement?: HTMLElement;
fromTableCell?: boolean;
};
export const LinksFieldDisplay = ({
isCellSoftFocused,
cellElement,
fromTableCell,
}: LinksFieldDisplayProps) => {
const { fieldValue } = useLinksField();
return <LinksDisplay value={fieldValue} />;
return (
<LinksDisplay
value={fieldValue}
anchorElement={cellElement}
isChipCountDisplayed={isCellSoftFocused}
withExpandedListBorder={fromTableCell}
/>
);
};

View File

@ -1,15 +1,17 @@
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
import { Tag } from '@/ui/display/tag/components/Tag';
import {
ExpandableList,
ExpandableListProps,
} from '@/ui/layout/expandable-list/components/ExpandableList';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
type MultiSelectFieldDisplayProps = {
isCellSoftFocused?: boolean;
cellElement?: HTMLElement;
fromTableCell?: boolean;
};
type MultiSelectFieldDisplayProps = ExpandableListProps;
export const MultiSelectFieldDisplay = ({
isHovered,
reference,
withDropDownBorder,
isCellSoftFocused,
cellElement,
fromTableCell,
}: MultiSelectFieldDisplayProps) => {
const { fieldValues, fieldDefinition } = useMultiSelectField();
@ -19,11 +21,13 @@ export const MultiSelectFieldDisplay = ({
)
: [];
return selectedOptions ? (
if (!selectedOptions) return null;
return (
<ExpandableList
isHovered={isHovered}
reference={reference}
withDropDownBorder={withDropDownBorder}
anchorElement={cellElement}
isChipCountDisplayed={isCellSoftFocused}
withExpandedListBorder={fromTableCell}
>
{selectedOptions.map((selectedOption, index) => (
<Tag
@ -33,7 +37,5 @@ export const MultiSelectFieldDisplay = ({
/>
))}
</ExpandableList>
) : (
<></>
);
};

View File

@ -99,7 +99,12 @@ export const RecordInlineCell = ({
isReadOnly={readonly}
/>
}
displayModeContent={<FieldDisplay />}
displayModeContent={({ cellElement, isCellSoftFocused }) => (
<FieldDisplay
cellElement={cellElement}
isCellSoftFocused={isCellSoftFocused}
/>
)}
isDisplayModeContentEmpty={isFieldEmpty}
isDisplayModeFixHeight
editModeContentOnly={isFieldInputOnly}

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import React, { useContext, useState } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { Tooltip } from 'react-tooltip';
import { css, useTheme } from '@emotion/react';
@ -7,7 +7,6 @@ import { IconComponent } from 'twenty-ui';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useInlineCell } from '../hooks/useInlineCell';
@ -111,7 +110,13 @@ type RecordInlineCellContainerProps = {
buttonIcon?: IconComponent;
editModeContent?: React.ReactNode;
editModeContentOnly?: boolean;
displayModeContent: React.ReactNode;
displayModeContent: ({
isCellSoftFocused,
cellElement,
}: {
isCellSoftFocused: boolean;
cellElement?: HTMLDivElement;
}) => React.ReactNode;
customEditHotkeyScope?: HotkeyScope;
isDisplayModeContentEmpty?: boolean;
isDisplayModeFixHeight?: boolean;
@ -136,24 +141,25 @@ export const RecordInlineCellContainer = ({
loading = false,
}: RecordInlineCellContainerProps) => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const reference = useRef<HTMLDivElement>(null);
// Used by some fields in ExpandableList as an anchor for the floating element.
// floating-ui mentions that `useState` must be used instead of `useRef`,
// see https://floating-ui.com/docs/useFloating#elements
const [cellElement, setCellElement] = useState<HTMLDivElement | null>(null);
const [isHovered, setIsHovered] = useState(false);
const [isHoveredForDisplayMode, setIsHoveredForDisplayMode] = useState(false);
const [newDisplayModeContent, setNewDisplayModeContent] =
useState<React.ReactNode>(displayModeContent);
const [isCellSoftFocused, setIsCellSoftFocused] = useState(false);
const handleContainerMouseEnter = () => {
if (!readonly) {
setIsHovered(true);
}
setIsHoveredForDisplayMode(true);
setIsCellSoftFocused(true);
};
const handleContainerMouseLeave = () => {
if (!readonly) {
setIsHovered(false);
}
setIsHoveredForDisplayMode(false);
setIsCellSoftFocused(false);
};
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
@ -174,17 +180,6 @@ export const RecordInlineCellContainer = ({
const theme = useTheme();
const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`;
useEffect(() => {
if (React.isValidElement<ExpandableListProps>(displayModeContent)) {
setNewDisplayModeContent(
React.cloneElement(displayModeContent, {
isHovered: isHoveredForDisplayMode,
reference: reference.current || undefined,
}),
);
}
}, [isHoveredForDisplayMode, displayModeContent, reference]);
const showContent = () => {
if (loading) {
return <StyledInlineCellSkeletonLoader />;
@ -215,7 +210,10 @@ export const RecordInlineCellContainer = ({
isHovered={isHovered}
emptyPlaceholder={showLabel ? 'Empty' : label}
>
{newDisplayModeContent}
{displayModeContent({
isCellSoftFocused,
cellElement: cellElement ?? undefined,
})}
</RecordInlineCellDisplayMode>
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
</StyledClickableContainer>
@ -252,7 +250,7 @@ export const RecordInlineCellContainer = ({
)}
</StyledLabelAndIconContainer>
)}
<StyledValueContainer ref={reference}>
<StyledValueContainer ref={setCellElement}>
{showContent()}
</StyledValueContainer>
</StyledInlineCellBaseContainer>

View File

@ -102,7 +102,13 @@ export const RecordTableCell = ({
isReadOnly={isReadOnly}
/>
}
nonEditModeContent={<FieldDisplay fromTableCell />}
nonEditModeContent={({ isCellSoftFocused, cellElement }) => (
<FieldDisplay
isCellSoftFocused={isCellSoftFocused}
cellElement={cellElement}
fromTableCell
/>
)}
/>
);
};

View File

@ -1,10 +1,4 @@
import React, {
ReactElement,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import React, { ReactElement, useContext, useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconArrowUpRight } from 'twenty-ui';
@ -20,7 +14,6 @@ import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/rec
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 { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
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';
@ -58,7 +51,13 @@ const StyledCellBaseContainer = styled.div<{ softFocus: boolean }>`
export type RecordTableCellContainerProps = {
editModeContent: ReactElement;
nonEditModeContent: ReactElement;
nonEditModeContent?: ({
isCellSoftFocused,
cellElement,
}: {
isCellSoftFocused: boolean;
cellElement?: HTMLTableCellElement;
}) => ReactElement;
editHotkeyScope?: HotkeyScope;
transparent?: boolean;
maxContentWidth?: number;
@ -76,10 +75,14 @@ export const RecordTableCellContainer = ({
editHotkeyScope,
}: RecordTableCellContainerProps) => {
const { columnIndex } = useContext(RecordTableCellContext);
const reference = useRef<HTMLTableCellElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [newNonEditModeContent, setNewNonEditModeContent] =
useState<ReactElement>(nonEditModeContent);
// Used by some fields in ExpandableList as an anchor for the floating element.
// floating-ui mentions that `useState` must be used instead of `useRef`,
// see https://floating-ui.com/docs/useFloating#elements
const [cellElement, setCellElement] = useState<HTMLTableCellElement | null>(
null,
);
const [isCellBaseContainerHovered, setIsCellBaseContainerHovered] =
useState(false);
const { isReadOnly, isSelected, recordId } = useContext(
RecordTableRowContext,
);
@ -127,13 +130,13 @@ export const RecordTableCellContainer = ({
const handleContainerMouseEnter = () => {
onCellMouseEnter({
cellPosition,
isHovered,
setIsHovered,
isHovered: isCellBaseContainerHovered,
setIsHovered: setIsCellBaseContainerHovered,
});
};
const handleContainerMouseLeave = () => {
setIsHovered(false);
setIsCellBaseContainerHovered(false);
};
const editModeContentOnly = useIsFieldInputOnly();
@ -150,20 +153,9 @@ export const RecordTableCellContainer = ({
(!isFirstColumn || !isEmpty) &&
!isReadOnly;
useEffect(() => {
if (React.isValidElement<ExpandableListProps>(nonEditModeContent)) {
setNewNonEditModeContent(
React.cloneElement(nonEditModeContent, {
isHovered: showButton,
reference: reference.current || undefined,
}),
);
}
}, [nonEditModeContent, showButton, reference]);
return (
<StyledTd
ref={reference}
ref={setCellElement}
isSelected={isSelected}
onContextMenu={handleContextMenu}
isInEditMode={isCurrentTableCellInEditMode}
@ -181,7 +173,12 @@ export const RecordTableCellContainer = ({
) : hasSoftFocus ? (
<>
<RecordTableCellSoftFocusMode>
{editModeContentOnly ? editModeContent : newNonEditModeContent}
{editModeContentOnly
? editModeContent
: nonEditModeContent?.({
isCellSoftFocused: true,
cellElement: cellElement ?? undefined,
})}
</RecordTableCellSoftFocusMode>
{showButton && (
<RecordTableCellButton
@ -196,7 +193,10 @@ export const RecordTableCellContainer = ({
<RecordTableCellDisplayMode>
{editModeContentOnly
? editModeContent
: newNonEditModeContent}
: nonEditModeContent?.({
isCellSoftFocused: false,
cellElement: cellElement ?? undefined,
})}
</RecordTableCellDisplayMode>
)}
{showButton && (