Refactoring shortcuts and commandbar (#412)

* Begin refactoring shortcuts and commandbar

* Continue refacto hotkeys

* Remove debug logs

* Add new story

* Simplify hotkeys

* Simplify hotkeys

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Félix Malfait
2023-06-25 22:25:31 -07:00
committed by GitHub
parent 9bd8f6df01
commit 827d6390e4
19 changed files with 387 additions and 414 deletions

View File

@ -1,288 +0,0 @@
import { ChangeEvent, ComponentType, useEffect, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { SearchConfigType } from '@/search/interfaces/interface';
import { useSearch } from '@/search/services/search';
import { IconPlus } from '@/ui/icons/index';
import { textInputStyle } from '@/ui/layout/styles/themes';
import { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState';
import { isDefined } from '@/utils/type-guards/isDefined';
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
import { EditableCell } from '../EditableCell';
import { HoverableMenuItem } from '../HoverableMenuItem';
import { EditableRelationCreateButton } from './EditableRelationCreateButton';
const StyledEditModeContainer = styled.div`
width: 200px;
`;
const StyledEditModeSelectedContainer = styled.div`
align-items: center;
display: flex;
height: 31px;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(1)};
`;
const StyledEditModeSearchContainer = styled.div`
align-items: center;
border-top: 1px solid ${(props) => props.theme.primaryBorder};
display: flex;
height: 32px;
padding-left: ${(props) => props.theme.spacing(1)};
padding-right: ${(props) => props.theme.spacing(1)};
`;
const StyledEditModeCreateButtonContainer = styled.div`
align-items: center;
border-top: 1px solid ${(props) => props.theme.primaryBorder};
color: ${(props) => props.theme.text60};
display: flex;
height: 36px;
padding: ${(props) => props.theme.spacing(1)};
`;
const StyledEditModeSearchInput = styled.input`
width: 100%;
${textInputStyle}
`;
const StyledEditModeResults = styled.div`
border-top: 1px solid ${(props) => props.theme.primaryBorder};
padding-left: ${(props) => props.theme.spacing(1)};
padding-right: ${(props) => props.theme.spacing(1)};
`;
type StyledEditModeResultItemProps = {
isSelected: boolean;
};
const StyledEditModeResultItem = styled.div<StyledEditModeResultItemProps>`
align-items: center;
cursor: pointer;
display: flex;
height: 32px;
user-select: none;
${(props) =>
props.isSelected &&
`
background-color: ${props.theme.tertiaryBackground};
`}
`;
const StyledCreateButtonIcon = styled.div`
align-self: center;
color: ${(props) => props.theme.text100};
padding-top: 4px;
`;
const StyledCreateButtonText = styled.div`
color: ${(props) => props.theme.text60};
`;
export type EditableRelationProps<RelationType, ChipComponentPropsType> = {
relation?: any;
searchPlaceholder: string;
searchConfig: SearchConfigType;
onChange: (relation: RelationType) => void;
onChangeSearchInput?: (searchInput: string) => void;
editModeHorizontalAlign?: 'left' | 'right';
ChipComponent: ComponentType<ChipComponentPropsType>;
chipComponentPropsMapper: (
relation: RelationType,
) => ChipComponentPropsType & JSX.IntrinsicAttributes;
// TODO: refactor, newRelationName is too hard coded.
onCreate?: (newRelationName: string) => void;
};
// TODO: split this component
export function EditableRelation<RelationType, ChipComponentPropsType>({
relation,
searchPlaceholder,
searchConfig,
onChange,
onChangeSearchInput,
editModeHorizontalAlign,
ChipComponent,
chipComponentPropsMapper,
onCreate,
}: EditableRelationProps<RelationType, ChipComponentPropsType>) {
const [isEditMode, setIsEditMode] = useState(false);
const [, setIsSomeInputInEditMode] = useRecoilState(
isSomeInputInEditModeState,
);
// TODO: Tie this to a react context
const [filterSearchResults, setSearchInput, setFilterSearch, searchInput] =
useSearch<RelationType>();
useEffect(() => {
if (isDefined(onChangeSearchInput)) {
onChangeSearchInput(searchInput);
}
}, [onChangeSearchInput, searchInput]);
const canCreate = isDefined(onCreate);
const createButtonIsVisible =
canCreate && isEditMode && isNonEmptyString(searchInput);
function handleCreateNewRelationButtonClick() {
onCreate?.(searchInput);
closeEditMode();
}
function closeEditMode() {
setIsEditMode(false);
setIsSomeInputInEditMode(false);
}
const [selectedIndex, setSelectedIndex] = useState(0);
useHotkeys(
'down',
() => {
setSelectedIndex((prevSelectedIndex) =>
Math.min(
prevSelectedIndex + 1,
(filterSearchResults.results?.length ?? 0) - 1,
),
);
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[setSelectedIndex, filterSearchResults.results],
);
useHotkeys(
'up',
() => {
setSelectedIndex((prevSelectedIndex) =>
Math.max(prevSelectedIndex - 1, 0),
);
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[setSelectedIndex],
);
useHotkeys(
'enter',
() => {
if (isEditMode) {
if (
filterSearchResults.results &&
selectedIndex < filterSearchResults.results.length
) {
const selectedResult = filterSearchResults.results[selectedIndex];
onChange(selectedResult.value);
closeEditMode();
} else if (canCreate && isNonEmptyString(searchInput)) {
onCreate(searchInput);
closeEditMode();
}
}
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
},
[
filterSearchResults.results,
selectedIndex,
onChange,
closeEditMode,
canCreate,
searchInput,
onCreate,
],
);
return (
<>
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => {
if (!isEditMode) {
setIsEditMode(true);
}
}}
editModeContent={
<StyledEditModeContainer>
<StyledEditModeSelectedContainer>
{relation ? (
<ChipComponent {...chipComponentPropsMapper(relation)} />
) : (
<></>
)}
</StyledEditModeSelectedContainer>
<StyledEditModeSearchContainer>
<StyledEditModeSearchInput
autoFocus
placeholder={searchPlaceholder}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterSearch(searchConfig);
setSearchInput(event.target.value);
}}
/>
</StyledEditModeSearchContainer>
{createButtonIsVisible && (
<StyledEditModeCreateButtonContainer>
<HoverableMenuItem>
<EditableRelationCreateButton
onClick={handleCreateNewRelationButtonClick}
>
<StyledCreateButtonIcon>
<IconPlus />
</StyledCreateButtonIcon>
<StyledCreateButtonText>Create new</StyledCreateButtonText>
</EditableRelationCreateButton>
</HoverableMenuItem>
</StyledEditModeCreateButtonContainer>
)}
<StyledEditModeResults>
{filterSearchResults.results &&
filterSearchResults.results.map((result, index) => (
<StyledEditModeResultItem
key={index}
isSelected={index === selectedIndex}
onClick={() => {
onChange(result.value);
closeEditMode();
}}
>
<HoverableMenuItem>
<ChipComponent
{...chipComponentPropsMapper(result.value)}
/>
</HoverableMenuItem>
</StyledEditModeResultItem>
))}
</StyledEditModeResults>
</StyledEditModeContainer>
}
nonEditModeContent={
<>
{relation ? (
<ChipComponent {...chipComponentPropsMapper(relation)} />
) : (
<></>
)}
</>
}
/>
</>
);
}

View File

@ -20,6 +20,7 @@ const StyledContainer = styled.div<StyledContainerProps>`
border: 1px solid ${(props) => props.theme.primaryBorder};
border-radius: 8px;
bottom: ${(props) => (props.position.x ? 'auto' : '38px')};
box-shadow: ${(props) => props.theme.modalBoxShadow};
display: flex;
height: 48px;