Refactor/inplace input (#541)

* wip

* Changed all other components

* Removed console log

* Console.log

* lint

* Removed internal state

* Fix

* Lint
This commit is contained in:
Lucas Bordeau
2023-07-09 01:45:52 +02:00
committed by GitHub
parent b3d0061e0d
commit e03d5ed8a7
47 changed files with 680 additions and 326 deletions

View File

@ -1,55 +0,0 @@
import styled from '@emotion/styled';
import { DropResult } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
export const StyledBoard = styled.div`
border-radius: ${({ theme }) => theme.spacing(2)};
display: flex;
flex-direction: row;
height: calc(100%);
overflow-x: auto;
width: 100%;
`;
export type Column = {
id: string;
title: string;
colorCode?: string;
itemKeys: string[];
};
export function getOptimisticlyUpdatedBoard(
board: Column[],
result: DropResult,
) {
const newBoard = JSON.parse(JSON.stringify(board));
const { destination, source } = result;
if (!destination) return;
const sourceColumnIndex = newBoard.findIndex(
(column: Column) => column.id === source.droppableId,
);
const sourceColumn = newBoard[sourceColumnIndex];
const destinationColumnIndex = newBoard.findIndex(
(column: Column) => column.id === destination.droppableId,
);
const destinationColumn = newBoard[destinationColumnIndex];
if (!destinationColumn || !sourceColumn) return;
const sourceItems = sourceColumn.itemKeys;
const destinationItems = destinationColumn.itemKeys;
const [removed] = sourceItems.splice(source.index, 1);
destinationItems.splice(destination.index, 0, removed);
const newSourceColumn = {
...sourceColumn,
itemKeys: sourceItems,
};
const newDestinationColumn = {
...destinationColumn,
itemKeys: destinationItems,
};
newBoard.splice(sourceColumnIndex, 1, newSourceColumn);
newBoard.splice(destinationColumnIndex, 1, newDestinationColumn);
return newBoard;
}

View File

@ -1,36 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
export const StyledColumn = styled.div`
background-color: ${({ theme }) => theme.background.primary};
display: flex;
flex-direction: column;
min-width: 200px;
padding: ${({ theme }) => theme.spacing(2)};
`;
export const StyledColumnTitle = styled.h3`
color: ${({ color }) => color};
font-family: 'Inter';
font-size: ${({ theme }) => theme.font.size.md};
font-style: normal;
font-weight: ${({ theme }) => theme.font.weight.medium};
line-height: ${({ theme }) => theme.text.lineHeight};
margin: 0;
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
type OwnProps = {
colorCode?: string;
title: string;
children: React.ReactNode;
};
export function BoardColumn({ colorCode, title, children }: OwnProps) {
return (
<StyledColumn>
<StyledColumnTitle color={colorCode}> {title}</StyledColumnTitle>
{children}
</StyledColumn>
);
}

View File

@ -1,37 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconPlus } from '@/ui/icons/index';
const StyledButton = styled.button`
align-items: center;
align-self: baseline;
background-color: ${({ theme }) => theme.background.primary};
border: none;
border-radius: ${({ theme }) => theme.border.radius.md};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: center;
padding: ${({ theme }) => theme.spacing(1)};
&:hover {
background-color: ${({ theme }) => theme.background.secondary};
}
`;
type OwnProps = {
onClick: () => void;
};
export function NewButton({ onClick }: OwnProps) {
const theme = useTheme();
return (
<StyledButton onClick={onClick}>
<IconPlus size={theme.icon.size.md} />
New
</StyledButton>
);
}

View File

@ -1,55 +0,0 @@
import { DropResult } from '@hello-pangea/dnd';
import { BoardItemKey, getOptimisticlyUpdatedBoard } from '../Board';
describe('getOptimisticlyUpdatedBoard', () => {
it('should return a new board with the updated cell', () => {
const initialColumn1: BoardItemKey[] = ['item-1', 'item-2', 'item-3'];
const initialColumn2: BoardItemKey[] = ['item-4', 'item-5'];
const finalColumn1: BoardItemKey[] = ['item-2', 'item-3'];
const finalColumn2: BoardItemKey[] = ['item-4', 'item-1', 'item-5'];
const dropResult = {
source: {
droppableId: 'column-1',
index: 0,
},
destination: {
droppableId: 'column-2',
index: 1,
},
} as DropResult;
const initialBoard = [
{
id: 'column-1',
title: 'My Column',
itemKeys: initialColumn1,
},
{
id: 'column-2',
title: 'My Column',
itemKeys: initialColumn2,
},
];
const updatedBoard = getOptimisticlyUpdatedBoard(initialBoard, dropResult);
const finalBoard = [
{
id: 'column-1',
title: 'My Column',
itemKeys: finalColumn1,
},
{
id: 'column-2',
title: 'My Column',
itemKeys: finalColumn2,
},
];
expect(updatedBoard).toEqual(finalBoard);
expect(updatedBoard).not.toBe(initialBoard);
});
});

View File

@ -1,14 +1,13 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useEditableCell } from './hooks/useCloseEditableCell';
import { useCurrentCellEditMode } from './hooks/useCurrentCellEditMode';
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
import { useSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
import { useSetSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { EditableCellEditMode } from './EditableCellEditMode';
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
@ -41,14 +40,12 @@ export function EditableCell({
}: OwnProps) {
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
const setSoftFocusOnCurrentCell = useSoftFocusOnCurrentCell();
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
const { openEditableCell } = useEditableCell();
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
// TODO: we might have silent problematic behavior because of the setTimeout in openEditableCell, investigate
// Maybe we could build a switchEditableCell to handle the case where we go from one cell to another.
// See https://github.com/twentyhq/twenty/issues/446
@ -58,8 +55,7 @@ export function EditableCell({
}
if (hasSoftFocus) {
openEditableCell();
addToHotkeysScopeStack(
openEditableCell(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},

View File

@ -1,6 +1,5 @@
import React from 'react';
import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
@ -14,20 +13,18 @@ export function EditableCellSoftFocusMode({
editHotkeysScope,
}: React.PropsWithChildren<{ editHotkeysScope?: HotkeysScopeStackItem }>) {
const { closeEditableCell, openEditableCell } = useEditableCell();
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
useScopedHotkeys(
'enter',
() => {
openEditableCell();
addToHotkeysScopeStack(
openEditableCell(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
},
InternalHotkeysScope.TableSoftFocus,
[closeEditableCell],
[closeEditableCell, editHotkeysScope],
);
useScopedHotkeys(
@ -42,15 +39,14 @@ export function EditableCellSoftFocusMode({
return;
}
openEditableCell();
addToHotkeysScopeStack(
openEditableCell(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
},
InternalHotkeysScope.TableSoftFocus,
[openEditableCell, addToHotkeysScopeStack, editHotkeysScope],
[openEditableCell, editHotkeysScope],
{
preventDefault: false,
},

View File

@ -1,6 +1,8 @@
import { useRecoilCallback } from 'recoil';
import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
import { useRemoveHighestHotkeysScopeStackItem } from '@/hotkeys/hooks/useRemoveHighestHotkeysScopeStackItem';
import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
import { useCloseCurrentCellInEditMode } from '@/ui/tables/hooks/useClearCellInEditMode';
import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState';
@ -10,6 +12,8 @@ import { useCurrentCellEditMode } from './useCurrentCellEditMode';
export function useEditableCell() {
const { setCurrentCellInEditMode } = useCurrentCellEditMode();
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
const removeHighestHotkeysScopedStackItem =
@ -22,7 +26,7 @@ export function useEditableCell() {
const openEditableCell = useRecoilCallback(
({ snapshot, set }) =>
() => {
(hotkeysScopeStackItem: HotkeysScopeStackItem) => {
const isSomeInputInEditMode = snapshot
.getLoadable(isSomeInputInEditModeState)
.valueOrThrow();
@ -32,9 +36,11 @@ export function useEditableCell() {
set(isSoftFocusActiveState, false);
setCurrentCellInEditMode();
addToHotkeysScopeStack(hotkeysScopeStackItem);
}
},
[setCurrentCellInEditMode],
[setCurrentCellInEditMode, addToHotkeysScopeStack],
);
return {

View File

@ -12,7 +12,7 @@ import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveStat
import { RowContext } from '@/ui/tables/states/RowContext';
import { CellPosition } from '@/ui/tables/types/CellPosition';
export function useSoftFocusOnCurrentCell() {
export function useSetSoftFocusOnCurrentCell() {
const setSoftFocusPosition = useSetSoftFocusPosition();
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,

View File

@ -0,0 +1,29 @@
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { InplaceInputDateDisplayMode } from '@/ui/inplace-inputs/components/InplaceInputDateDisplayMode';
import { EditableCell } from '../EditableCell';
import { EditableCellDateEditMode } from './EditableCellDateEditMode';
export type EditableDateProps = {
value: Date;
onChange: (date: Date) => void;
editModeHorizontalAlign?: 'left' | 'right';
};
export function EditableCellDate({
value,
onChange,
editModeHorizontalAlign,
}: EditableDateProps) {
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<EditableCellDateEditMode onChange={onChange} value={value} />
}
nonEditModeContent={<InplaceInputDateDisplayMode value={value} />}
editHotkeysScope={{ scope: InternalHotkeysScope.CellDateEditMode }}
></EditableCell>
);
}

View File

@ -0,0 +1,22 @@
import { InplaceInputDateEditMode } from '@/ui/inplace-inputs/components/InplaceInputDateEditMode';
import { useEditableCell } from '../hooks/useCloseEditableCell';
export type EditableDateProps = {
value: Date;
onChange: (date: Date) => void;
};
export function EditableCellDateEditMode({
value,
onChange,
}: EditableDateProps) {
const { closeEditableCell } = useEditableCell();
function handleDateChange(newDate: Date) {
onChange(newDate);
closeEditableCell();
}
return <InplaceInputDateEditMode onChange={handleDateChange} value={value} />;
}

View File

@ -4,7 +4,7 @@ import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysSc
import { EditableCell } from '../EditableCell';
import { EditableDoubleTextEditMode } from './EditableDoubleTextEditMode';
import { EditableCellDoubleTextEditMode } from './EditableCellDoubleTextEditMode';
type OwnProps = {
firstValue: string;
@ -15,7 +15,7 @@ type OwnProps = {
onChange: (firstValue: string, secondValue: string) => void;
};
export function EditableDoubleText({
export function EditableCellDoubleText({
firstValue,
secondValue,
firstValuePlaceholder,
@ -27,7 +27,7 @@ export function EditableDoubleText({
<EditableCell
editHotkeysScope={{ scope: InternalHotkeysScope.CellDoubleTextInput }}
editModeContent={
<EditableDoubleTextEditMode
<EditableCellDoubleTextEditMode
firstValue={firstValue}
secondValue={secondValue}
firstValuePlaceholder={firstValuePlaceholder}

View File

@ -4,8 +4,8 @@ import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { InplaceInputTextEditMode } from '@/ui/inplace-inputs/components/InplaceInputTextEditMode';
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
import { textInputStyle } from '@/ui/themes/effects';
import { useEditableCell } from '../hooks/useCloseEditableCell';
@ -28,15 +28,7 @@ const StyledContainer = styled.div`
}
`;
const StyledEditInplaceInput = styled.input`
height: 18px;
margin: 0;
width: 45%;
${textInputStyle}
`;
export function EditableDoubleTextEditMode({
export function EditableCellDoubleTextEditMode({
firstValue,
secondValue,
firstValuePlaceholder,
@ -77,7 +69,7 @@ export function EditableDoubleTextEditMode({
useScopedHotkeys(
'tab',
async (keyboardEvent, hotkeyEvent) => {
() => {
if (focusPosition === 'left') {
setFocusPosition('right');
secondValueInputRef.current?.focus();
@ -107,7 +99,7 @@ export function EditableDoubleTextEditMode({
return (
<StyledContainer>
<StyledEditInplaceInput
<InplaceInputTextEditMode
autoFocus
placeholder={firstValuePlaceholder}
ref={firstValueInputRef}
@ -116,7 +108,7 @@ export function EditableDoubleTextEditMode({
onChange(event.target.value, secondValue);
}}
/>
<StyledEditInplaceInput
<InplaceInputTextEditMode
placeholder={secondValuePlaceholder}
ref={secondValueInputRef}
value={secondValue}

View File

@ -0,0 +1,39 @@
import { ChangeEvent, useRef, useState } from 'react';
import { InplaceInputPhoneDisplayMode } from '@/ui/inplace-inputs/components/InplaceInputPhoneDisplayMode';
import { InplaceInputTextEditMode } from '@/ui/inplace-inputs/components/InplaceInputTextEditMode';
import { EditableCell } from '../EditableCell';
type OwnProps = {
placeholder?: string;
value: string;
changeHandler: (updated: string) => void;
};
export function EditableCellPhone({
value,
placeholder,
changeHandler,
}: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(value);
return (
<EditableCell
editModeContent={
<InplaceInputTextEditMode
autoFocus
placeholder={placeholder || ''}
ref={inputRef}
value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
changeHandler(event.target.value);
}}
/>
}
nonEditModeContent={<InplaceInputPhoneDisplayMode value={inputValue} />}
/>
);
}

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled';
export const EditableRelationCreateButton = styled.button`
export const EditableCellRelationCreateButton = styled.button`
align-items: center;
background: none;
border: none;

View File

@ -0,0 +1,39 @@
import { ChangeEvent } from 'react';
import { InplaceInputTextDisplayMode } from '@/ui/inplace-inputs/components/InplaceInputTextDisplayMode';
import { InplaceInputTextEditMode } from '@/ui/inplace-inputs/components/InplaceInputTextEditMode';
import { EditableCell } from '../EditableCell';
type OwnProps = {
placeholder?: string;
value: string;
onChange: (newValue: string) => void;
editModeHorizontalAlign?: 'left' | 'right';
};
export function EditableCellText({
value,
placeholder,
onChange,
editModeHorizontalAlign,
}: OwnProps) {
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<InplaceInputTextEditMode
placeholder={placeholder || ''}
autoFocus
value={value}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value);
}}
/>
}
nonEditModeContent={
<InplaceInputTextDisplayMode>{value}</InplaceInputTextDisplayMode>
}
></EditableCell>
);
}

View File

@ -39,7 +39,8 @@ const RightContainer = styled.div`
margin-left: ${(props) => props.theme.spacing(1)};
`;
function EditableChip({
// TODO: move right end content in EditableCell
export function EditableCellChip({
value,
placeholder,
changeHandler,
@ -89,5 +90,3 @@ function EditableChip({
/>
);
}
export default EditableChip;

View File

@ -1,81 +0,0 @@
import { forwardRef, useState } from 'react';
import styled from '@emotion/styled';
import { humanReadableDate } from '@/utils/utils';
import DatePicker from '../../form/DatePicker';
import { EditableCell } from '../EditableCell';
export type EditableDateProps = {
value: Date;
changeHandler: (date: Date) => void;
editModeHorizontalAlign?: 'left' | 'right';
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
margin: 0px ${({ theme }) => theme.spacing(2)};
`;
export type StyledCalendarContainerProps = {
editModeHorizontalAlign?: 'left' | 'right';
};
const StyledCalendarContainer = styled.div<StyledCalendarContainerProps>`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: 8px;
box-shadow: ${({ theme }) => theme.boxShadow.strong};
left: -10px;
position: absolute;
top: 10px;
z-index: 1;
`;
export function EditableDate({
value,
changeHandler,
editModeHorizontalAlign,
}: EditableDateProps) {
const [inputValue, setInputValue] = useState(value);
type DivProps = React.HTMLProps<HTMLDivElement>;
const DateDisplay = forwardRef<HTMLDivElement, DivProps>(
({ value, onClick }, ref) => (
<div onClick={onClick} ref={ref}>
{value && humanReadableDate(new Date(value as string))}
</div>
),
);
type DatePickerContainerProps = {
children: React.ReactNode;
};
const DatePickerContainer = ({ children }: DatePickerContainerProps) => {
return <StyledCalendarContainer>{children}</StyledCalendarContainer>;
};
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<StyledContainer>
<DatePicker
date={inputValue}
onChangeHandler={(date: Date) => {
changeHandler(date);
setInputValue(date);
}}
customInput={<DateDisplay />}
customCalendarContainer={DatePickerContainer}
/>
</StyledContainer>
}
nonEditModeContent={
<div>{inputValue && humanReadableDate(inputValue)}</div>
}
></EditableCell>
);
}

View File

@ -1,70 +0,0 @@
import { ChangeEvent, MouseEvent, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
import { textInputStyle } from '@/ui/themes/effects';
import { RawLink } from '../../links/RawLink';
import { EditableCell } from '../EditableCell';
type OwnProps = {
placeholder?: string;
value: string;
changeHandler: (updated: string) => void;
};
const StyledRawLink = styled(RawLink)`
overflow: hidden;
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
// TODO: refactor
const StyledEditInplaceInput = styled.input`
margin: 0;
width: 100%;
${textInputStyle}
`;
export function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(value);
return (
<EditableCell
editModeContent={
<StyledEditInplaceInput
autoFocus
placeholder={placeholder || ''}
ref={inputRef}
value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
changeHandler(event.target.value);
}}
/>
}
nonEditModeContent={
<>
{isValidPhoneNumber(inputValue) ? (
<StyledRawLink
href={parsePhoneNumber(inputValue, 'FR')?.getURI()}
onClick={(event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
}}
>
{parsePhoneNumber(inputValue, 'FR')?.formatInternational() ||
inputValue}
</StyledRawLink>
) : (
<StyledRawLink href="#">{inputValue}</StyledRawLink>
)}
</>
}
/>
);
}

View File

@ -1,56 +0,0 @@
import { ChangeEvent, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/themes/effects';
import { EditableCell } from '../EditableCell';
type OwnProps = {
placeholder?: string;
content: string;
changeHandler: (updated: string) => void;
editModeHorizontalAlign?: 'left' | 'right';
};
// TODO: refactor
const StyledInplaceInput = styled.input`
margin: 0;
width: 100%;
${textInputStyle}
`;
const StyledNoEditText = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
`;
export function EditableText({
content,
placeholder,
changeHandler,
editModeHorizontalAlign,
}: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(content);
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<StyledInplaceInput
placeholder={placeholder || ''}
autoFocus
ref={inputRef}
value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
changeHandler(event.target.value);
}}
/>
}
nonEditModeContent={<StyledNoEditText>{inputValue}</StyledNoEditText>}
></EditableCell>
);
}