martmull
2024-05-03 15:03:06 +02:00
committed by GitHub
parent 1351a95754
commit 87994c26ff
51 changed files with 687 additions and 405 deletions

View File

@ -2,6 +2,7 @@ import { useContext } from 'react';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
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';
@ -13,7 +14,7 @@ import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDi
import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay';
import { JsonFieldDisplay } from '../meta-types/display/components/JsonFieldDisplay';
import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay';
import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay.tsx';
import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay';
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay';
@ -36,7 +37,13 @@ import { isFieldSelect } from '../types/guards/isFieldSelect';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldUuid } from '../types/guards/isFieldUuid';
export const FieldDisplay = () => {
type FieldDisplayProps = ExpandableListProps;
export const FieldDisplay = ({
isHovered,
reference,
fromTableCell,
}: FieldDisplayProps & { fromTableCell?: boolean }) => {
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
const isChipDisplay =
@ -74,7 +81,11 @@ export const FieldDisplay = () => {
) : isFieldSelect(fieldDefinition) ? (
<SelectFieldDisplay />
) : isFieldMultiSelect(fieldDefinition) ? (
<MultiSelectFieldDisplay />
<MultiSelectFieldDisplay
isHovered={isHovered}
reference={reference}
withDropDownBorder={fromTableCell}
/>
) : isFieldAddress(fieldDefinition) ? (
<AddressFieldDisplay />
) : isFieldRawJson(fieldDefinition) ? (

View File

@ -4,7 +4,7 @@ import { AddressFieldInput } from '@/object-record/record-field/meta-types/input
import { DateFieldInput } from '@/object-record/record-field/meta-types/input/components/DateFieldInput';
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx';
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';

View File

@ -1,6 +1,7 @@
import { useContext } from 'react';
import { IconComponent, IconPencil } from 'twenty-ui';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -17,7 +18,8 @@ export const useGetButtonIcon = (): IconComponent | undefined => {
if (
isFieldLink(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
isFieldPhone(fieldDefinition)
isFieldPhone(fieldDefinition) ||
isFieldMultiSelect(fieldDefinition)
) {
return IconPencil;
}

View File

@ -10,7 +10,7 @@ import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';

View File

@ -1,13 +1,16 @@
import styled from '@emotion/styled';
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';
const StyledTagContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const MultiSelectFieldDisplay = () => {
type MultiSelectFieldDisplayProps = ExpandableListProps;
export const MultiSelectFieldDisplay = ({
isHovered,
reference,
withDropDownBorder,
}: MultiSelectFieldDisplayProps) => {
const { fieldValues, fieldDefinition } = useMultiSelectField();
const selectedOptions = fieldValues
@ -17,7 +20,11 @@ export const MultiSelectFieldDisplay = () => {
: [];
return selectedOptions ? (
<StyledTagContainer>
<ExpandableList
isHovered={isHovered}
reference={reference}
withDropDownBorder={withDropDownBorder}
>
{selectedOptions.map((selectedOption, index) => (
<Tag
key={index}
@ -25,7 +32,7 @@ export const MultiSelectFieldDisplay = () => {
text={selectedOption.label}
/>
))}
</StyledTagContainer>
</ExpandableList>
) : (
<></>
);

View File

@ -9,7 +9,7 @@ import { assertFieldMetadata } from '@/object-record/record-field/types/guards/a
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated/graphql.tsx';
import { FieldMetadataType } from '~/generated/graphql';
export const useMultiSelectField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);

View File

@ -7,7 +7,7 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { MenuItemMultiSelectTag } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectTag.tsx';
import { MenuItemMultiSelectTag } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectTag';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';

View File

@ -1,12 +1,12 @@
import { useContext, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Tooltip } from 'react-tooltip';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
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';
@ -48,11 +48,6 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
width: ${({ width }) => width}px;
`;
const StyledEditButtonContainer = styled(motion.div)`
align-items: center;
display: flex;
`;
const StyledClickableContainer = styled.div<{ readonly?: boolean }>`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
@ -119,18 +114,24 @@ export const RecordInlineCellContainer = ({
disableHoverEffect,
}: RecordInlineCellContainerProps) => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const reference = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [isHoveredForDisplayMode, setIsHoveredForDisplayMode] = useState(false);
const [newDisplayModeContent, setNewDisplayModeContent] =
useState<React.ReactNode>(displayModeContent);
const handleContainerMouseEnter = () => {
if (!readonly) {
setIsHovered(true);
}
setIsHoveredForDisplayMode(true);
};
const handleContainerMouseLeave = () => {
if (!readonly) {
setIsHovered(false);
}
setIsHoveredForDisplayMode(false);
};
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
@ -151,6 +152,17 @@ 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]);
return (
<StyledInlineCellBaseContainer
onMouseEnter={handleContainerMouseEnter}
@ -181,7 +193,7 @@ export const RecordInlineCellContainer = ({
)}
</StyledLabelAndIconContainer>
)}
<StyledValueContainer>
<StyledValueContainer ref={reference}>
{!readonly && isInlineCellInEditMode ? (
<RecordInlineCellEditMode>{editModeContent}</RecordInlineCellEditMode>
) : editModeContentOnly ? (
@ -208,18 +220,9 @@ export const RecordInlineCellContainer = ({
isHovered={isHovered}
emptyPlaceholder={showLabel ? 'Empty' : label}
>
{displayModeContent}
{newDisplayModeContent}
</RecordInlineCellDisplayMode>
{showEditButton && (
<StyledEditButtonContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<RecordInlineCellButton Icon={buttonIcon} />
</StyledEditButtonContainer>
)}
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
</StyledClickableContainer>
)}
</StyledValueContainer>

View File

@ -1,13 +1,23 @@
import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui';
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
const StyledInlineCellButtonContainer = styled.div`
align-items: center;
display: flex;
`;
export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => {
return (
<FloatingIconButton
size="small"
Icon={Icon}
data-testid="inline-cell-edit-mode-container"
/>
<AnimatedContainer>
<StyledInlineCellButtonContainer>
<FloatingIconButton
size="small"
Icon={Icon}
data-testid="inline-cell-edit-mode-container"
/>
</StyledInlineCellButtonContainer>
</AnimatedContainer>
);
};

View File

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

View File

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

View File

@ -1,12 +1,11 @@
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { IconComponent } from 'twenty-ui';
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
const StyledEditButtonContainer = styled(motion.div)`
position: absolute;
right: 5px;
const StyledButtonContainer = styled.div`
margin: ${({ theme }) => theme.spacing(1)};
`;
type RecordTableCellButtonProps = {
@ -18,12 +17,9 @@ export const RecordTableCellButton = ({
onClick,
Icon,
}: RecordTableCellButtonProps) => (
<StyledEditButtonContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
</StyledEditButtonContainer>
<AnimatedContainer>
<StyledButtonContainer>
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
</StyledButtonContainer>
</AnimatedContainer>
);

View File

@ -1,4 +1,10 @@
import React, { ReactElement, useContext, useState } from 'react';
import React, {
ReactElement,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconArrowUpRight } from 'twenty-ui';
@ -14,6 +20,7 @@ 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';
@ -33,7 +40,7 @@ const StyledTd = styled.td<{ isSelected: boolean; isInEditMode: boolean }>`
z-index: ${({ isInEditMode }) => (isInEditMode ? '4 !important' : '3')};
`;
const StyledCellBaseContainer = styled.div`
const StyledCellBaseContainer = styled.div<{ softFocus: boolean }>`
align-items: center;
box-sizing: border-box;
cursor: pointer;
@ -41,6 +48,12 @@ const StyledCellBaseContainer = styled.div`
height: 32px;
position: relative;
user-select: none;
${(props) =>
props.softFocus
? `background: ${props.theme.background.transparent.secondary};
border-radius: ${props.theme.border.radius.sm};
outline: 1px solid ${props.theme.font.color.extraLight};`
: ''}
`;
export type RecordTableCellContainerProps = {
@ -63,6 +76,10 @@ 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);
const { isReadOnly, isSelected, recordId } = useContext(
RecordTableRowContext,
);
@ -71,8 +88,6 @@ export const RecordTableCellContainer = ({
const cellPosition = useCurrentTableCellPosition();
const [isHovered, setIsHovered] = useState(false);
const { openTableCell } = useOpenRecordTableCellFromCell();
const tableScopeId = useAvailableScopeIdOrThrow(
@ -135,8 +150,20 @@ 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}
isSelected={isSelected}
onContextMenu={handleContextMenu}
isInEditMode={isCurrentTableCellInEditMode}
@ -147,34 +174,37 @@ export const RecordTableCellContainer = ({
<StyledCellBaseContainer
onMouseEnter={handleContainerMouseEnter}
onMouseLeave={handleContainerMouseLeave}
softFocus={hasSoftFocus}
>
{isCurrentTableCellInEditMode ? (
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
) : hasSoftFocus ? (
<>
<RecordTableCellSoftFocusMode>
{editModeContentOnly ? editModeContent : newNonEditModeContent}
</RecordTableCellSoftFocusMode>
{showButton && (
<RecordTableCellButton
onClick={handleButtonClick}
Icon={buttonIcon}
/>
)}
<RecordTableCellSoftFocusMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellSoftFocusMode>
</>
) : (
<>
{!isEmpty && (
<RecordTableCellDisplayMode>
{editModeContentOnly
? editModeContent
: newNonEditModeContent}
</RecordTableCellDisplayMode>
)}
{showButton && (
<RecordTableCellButton
onClick={handleButtonClick}
Icon={buttonIcon}
/>
)}
{!isEmpty && (
<RecordTableCellDisplayMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellDisplayMode>
)}
</>
)}
</StyledCellBaseContainer>

View File

@ -1,29 +0,0 @@
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { IconComponent } from 'twenty-ui';
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
const StyledEditButtonContainer = styled(motion.div)`
position: absolute;
right: 5px;
`;
type RecordTableCellEditButtonProps = {
onClick?: () => void;
Icon: IconComponent;
};
export const RecordTableCellEditButton = ({
onClick,
Icon,
}: RecordTableCellEditButtonProps) => (
<StyledEditButtonContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
</StyledEditButtonContainer>
);

View File

@ -101,7 +101,6 @@ export const RecordTableCellSoftFocusMode = ({
return (
<RecordTableCellDisplayContainer
onClick={handleClick}
softFocus
scrollRef={scrollRef}
>
{children}