Lucas/t 231 timebox i can create a company at the same time im creating (#140)

This PR is a bit messy:

adding graphql schema
adding create company creation on company select on People page
some frontend refactoring to be continued

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-05-25 23:09:23 +02:00
committed by GitHub
parent fecf45f3bc
commit b0044ed1a2
533 changed files with 20601 additions and 333 deletions
@@ -0,0 +1,12 @@
import styled from '@emotion/styled';
export const CellBaseContainer = styled.div`
position: relative;
box-sizing: border-box;
height: 32px;
display: flex;
align-items: center;
width: 100%;
cursor: pointer;
user-select: none;
`;
@@ -0,0 +1,28 @@
import styled from '@emotion/styled';
type OwnProps = {
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
};
export const CellEditModeContainer = styled.div<OwnProps>`
display: flex;
align-items: center;
min-width: 100%;
min-height: 100%;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
position: absolute;
left: ${(props) =>
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
right: ${(props) =>
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
background: ${(props) => props.theme.primaryBackground};
border: 1px solid ${(props) => props.theme.primaryBorder};
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.16);
z-index: 1;
border-radius: 4px;
backdrop-filter: blur(20px);
`;
@@ -0,0 +1,12 @@
import styled from '@emotion/styled';
export const CellNormalModeContainer = styled.div`
display: flex;
align-items: center;
width: calc(100% - ${(props) => props.theme.spacing(5)});
height: 100%;
overflow: hidden;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
`;
@@ -0,0 +1,84 @@
import { ReactElement, useRef } from 'react';
import { useOutsideAlerter } from '../../hooks/useOutsideAlerter';
import { useHotkeys } from 'react-hotkeys-hook';
import { CellBaseContainer } from './CellBaseContainer';
import { CellEditModeContainer } from './CellEditModeContainer';
import { CellNormalModeContainer } from './CellNormalModeContainer';
type OwnProps = {
editModeContent: ReactElement;
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
isEditMode?: boolean;
isCreateMode?: boolean;
onOutsideClick?: () => void;
onInsideClick?: () => void;
};
export function EditableCell({
editModeContent,
nonEditModeContent,
editModeHorizontalAlign = 'left',
editModeVerticalPosition = 'over',
isEditMode = false,
onOutsideClick,
onInsideClick,
}: OwnProps) {
const wrapperRef = useRef(null);
const editableContainerRef = useRef(null);
useOutsideAlerter(wrapperRef, () => {
onOutsideClick?.();
});
useHotkeys(
'esc',
() => {
if (isEditMode) {
onOutsideClick?.();
}
},
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
[isEditMode, onOutsideClick],
);
useHotkeys(
'enter',
() => {
if (isEditMode) {
onOutsideClick?.();
}
},
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
[isEditMode, onOutsideClick],
);
return (
<CellBaseContainer
ref={wrapperRef}
onClick={() => {
onInsideClick && onInsideClick();
}}
>
<CellNormalModeContainer>{nonEditModeContent}</CellNormalModeContainer>
{isEditMode && (
<CellEditModeContainer
ref={editableContainerRef}
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
>
{editModeContent}
</CellEditModeContainer>
)}
</CellBaseContainer>
);
}
@@ -0,0 +1,95 @@
import { ReactElement, useRef } from 'react';
import { useOutsideAlerter } from '../../hooks/useOutsideAlerter';
import { useHotkeys } from 'react-hotkeys-hook';
import { CellBaseContainer } from './CellBaseContainer';
import styled from '@emotion/styled';
import { EditableCellMenuEditModeContainer } from './EditableCellMenuEditModeContainer';
const EditableCellMenuNormalModeContainer = styled.div`
display: flex;
align-items: center;
width: calc(100% - ${(props) => props.theme.spacing(5)});
height: 100%;
overflow: hidden;
`;
type OwnProps = {
editModeContent: ReactElement;
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
isEditMode?: boolean;
isCreateMode?: boolean;
onOutsideClick?: () => void;
onInsideClick?: () => void;
};
// TODO: refactor
export function EditableCellMenu({
editModeContent,
nonEditModeContent,
editModeHorizontalAlign = 'left',
editModeVerticalPosition = 'over',
isEditMode = false,
onOutsideClick,
onInsideClick,
}: OwnProps) {
const wrapperRef = useRef(null);
const editableContainerRef = useRef(null);
useOutsideAlerter(wrapperRef, () => {
onOutsideClick?.();
});
useHotkeys(
'esc',
() => {
if (isEditMode) {
onOutsideClick?.();
}
},
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
[isEditMode, onOutsideClick],
);
useHotkeys(
'enter',
() => {
if (isEditMode) {
onOutsideClick?.();
}
},
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
[isEditMode, onOutsideClick],
);
return (
<CellBaseContainer
ref={wrapperRef}
onClick={() => {
onInsideClick && onInsideClick();
}}
>
<EditableCellMenuNormalModeContainer>
{nonEditModeContent}
</EditableCellMenuNormalModeContainer>
{isEditMode && (
<EditableCellMenuEditModeContainer
ref={editableContainerRef}
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
>
{editModeContent}
</EditableCellMenuEditModeContainer>
)}
</CellBaseContainer>
);
}
@@ -0,0 +1,27 @@
import styled from '@emotion/styled';
type OwnProps = {
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
};
// TODO: refactor
export const EditableCellMenuEditModeContainer = styled.div<OwnProps>`
display: flex;
align-items: center;
min-width: 100%;
min-height: 100%;
position: absolute;
left: ${(props) =>
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
right: ${(props) =>
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
background: ${(props) => props.theme.primaryBackground};
border: 1px solid ${(props) => props.theme.primaryBorder};
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.16);
z-index: 1;
border-radius: 4px;
backdrop-filter: blur(20px);
`;
@@ -1,99 +0,0 @@
import styled from '@emotion/styled';
import { ReactElement, useRef } from 'react';
import { useOutsideAlerter } from '../../hooks/useOutsideAlerter';
type OwnProps = {
editModeContent: ReactElement;
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
isEditMode?: boolean;
onOutsideClick?: () => void;
onInsideClick?: () => void;
};
const StyledWrapper = styled.div`
position: relative;
box-sizing: border-box;
height: 32px;
display: flex;
align-items: center;
width: 100%;
cursor: pointer;
user-select: none;
`;
type StyledEditModeContainerProps = {
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
};
const StyledNonEditModeContainer = styled.div`
display: flex;
align-items: center;
width: calc(100% - ${(props) => props.theme.spacing(5)});
height: 100%;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
overflow: hidden;
`;
const StyledEditModeContainer = styled.div<StyledEditModeContainerProps>`
display: flex;
align-items: center;
min-width: 100%;
min-height: 100%;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
position: absolute;
left: ${(props) =>
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
right: ${(props) =>
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
background: ${(props) => props.theme.primaryBackground};
border: 1px solid ${(props) => props.theme.primaryBorder};
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.16);
z-index: 1;
border-radius: 4px;
backdrop-filter: blur(20px);
`;
function EditableCellWrapper({
editModeContent,
nonEditModeContent,
editModeHorizontalAlign = 'left',
editModeVerticalPosition = 'over',
isEditMode = false,
onOutsideClick,
onInsideClick,
}: OwnProps) {
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef, () => {
onOutsideClick && onOutsideClick();
});
return (
<StyledWrapper
ref={wrapperRef}
onClick={() => {
onInsideClick && onInsideClick();
}}
>
<StyledNonEditModeContainer>
{nonEditModeContent}
</StyledNonEditModeContainer>
{isEditMode && (
<StyledEditModeContainer
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
>
{editModeContent}
</StyledEditModeContainer>
)}
</StyledWrapper>
);
}
export default EditableCellWrapper;
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { ChangeEvent, ComponentType, useRef, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper';
import { EditableCell } from './EditableCell';
export type EditableChipProps = {
placeholder?: string;
@@ -11,16 +11,33 @@ export type EditableChipProps = {
ChipComponent: ComponentType<{ name: string; picture: string }>;
};
// TODO: refactor
const StyledInplaceInput = styled.input`
width: 100%;
border: none;
outline: none;
padding-left: ${(props) => props.theme.spacing(1)};
padding-right: ${(props) => props.theme.spacing(1)};
&::placeholder {
font-weight: 'bold';
color: props.theme.text20;
}
`;
const StyledInplaceShow = styled.input`
width: 100%;
border: none;
outline: none;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
&::placeholder {
font-weight: 'bold';
color: props.theme.text20;
}
`;
function EditableChip({
value,
placeholder,
@@ -34,7 +51,7 @@ function EditableChip({
const [isEditMode, setIsEditMode] = useState(false);
return (
<EditableCellWrapper
<EditableCell
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => setIsEditMode(true)}
isEditMode={isEditMode}
@@ -51,8 +68,12 @@ function EditableChip({
}}
/>
}
nonEditModeContent={<ChipComponent name={inputValue} picture={picture} />}
></EditableCellWrapper>
nonEditModeContent={
<StyledInplaceShow>
<ChipComponent name={inputValue} picture={picture} />
</StyledInplaceShow>
}
></EditableCell>
);
}
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { forwardRef, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper';
import { EditableCell } from './EditableCell';
import DatePicker from '../form/DatePicker';
import { modalBackground } from '../../layout/styles/themes';
import { humanReadableDate } from '../../services/utils';
@@ -57,7 +57,7 @@ function EditableDate({
};
return (
<EditableCellWrapper
<EditableCell
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => setIsEditMode(true)}
@@ -80,7 +80,7 @@ function EditableDate({
<div>{inputValue && humanReadableDate(inputValue)}</div>
</StyledContainer>
}
></EditableCellWrapper>
></EditableCell>
);
}
@@ -0,0 +1,77 @@
import styled from '@emotion/styled';
import { ChangeEvent, ReactElement, useRef, useState } from 'react';
import { EditableCell } from './EditableCell';
type OwnProps = {
firstValue: string;
secondValue: string;
firstValuePlaceholder: string;
secondValuePlaceholder: string;
nonEditModeContent: ReactElement;
onChange: (firstValue: string, secondValue: string) => void;
};
const StyledContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
& > input:last-child {
padding-left: ${(props) => props.theme.spacing(2)};
border-left: 1px solid ${(props) => props.theme.primaryBorder};
}
`;
const StyledEditInplaceInput = styled.input`
width: 45%;
border: none;
outline: none;
height: 18px;
&::placeholder {
font-weight: bold;
color: ${(props) => props.theme.text20};
}
`;
export function EditableDoubleText({
firstValue,
secondValue,
firstValuePlaceholder,
secondValuePlaceholder,
nonEditModeContent,
onChange,
}: OwnProps) {
const firstValueInputRef = useRef<HTMLInputElement>(null);
const [isEditMode, setIsEditMode] = useState(false);
return (
<EditableCell
onInsideClick={() => setIsEditMode(true)}
onOutsideClick={() => setIsEditMode(false)}
isEditMode={isEditMode}
editModeContent={
<StyledContainer>
<StyledEditInplaceInput
autoFocus
placeholder={firstValuePlaceholder}
ref={firstValueInputRef}
value={firstValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value, secondValue);
}}
/>
<StyledEditInplaceInput
placeholder={secondValuePlaceholder}
ref={firstValueInputRef}
value={secondValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(firstValue, event.target.value);
}}
/>
</StyledContainer>
}
nonEditModeContent={nonEditModeContent}
></EditableCell>
);
}
@@ -1,77 +0,0 @@
import styled from '@emotion/styled';
import { ChangeEvent, useRef, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper';
import PersonChip from '../chips/PersonChip';
type OwnProps = {
firstname: string;
lastname: string;
changeHandler: (firstname: string, lastname: string) => void;
};
const StyledContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
& > input:last-child {
padding-left: ${(props) => props.theme.spacing(2)};
border-left: 1px solid ${(props) => props.theme.primaryBorder};
}
`;
const StyledEditInplaceInput = styled.input`
width: 45%;
border: none;
outline: none;
height: 18px;
&::placeholder {
font-weight: bold;
color: ${(props) => props.theme.text20};
}
`;
function EditableFullName({ firstname, lastname, changeHandler }: OwnProps) {
const firstnameInputRef = useRef<HTMLInputElement>(null);
const [firstnameValue, setFirstnameValue] = useState(firstname);
const [lastnameValue, setLastnameValue] = useState(lastname);
const [isEditMode, setIsEditMode] = useState(false);
return (
<EditableCellWrapper
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => setIsEditMode(true)}
isEditMode={isEditMode}
editModeContent={
<StyledContainer>
<StyledEditInplaceInput
autoFocus
placeholder="Firstname"
ref={firstnameInputRef}
value={firstnameValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFirstnameValue(event.target.value);
changeHandler(event.target.value, lastnameValue);
}}
/>
<StyledEditInplaceInput
autoFocus
placeholder={'Lastname'}
ref={firstnameInputRef}
value={lastnameValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setLastnameValue(event.target.value);
changeHandler(firstnameValue, event.target.value);
}}
/>
</StyledContainer>
}
nonEditModeContent={
<PersonChip name={firstnameValue + ' ' + lastnameValue} />
}
></EditableCellWrapper>
);
}
export default EditableFullName;
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { ChangeEvent, MouseEvent, useRef, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper';
import { EditableCell } from './EditableCell';
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
import Link from '../link/Link';
@@ -14,6 +14,7 @@ type StyledEditModeProps = {
isEditMode: boolean;
};
// TODO: refactor
const StyledEditInplaceInput = styled.input<StyledEditModeProps>`
width: 100%;
border: none;
@@ -31,7 +32,7 @@ function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
const [isEditMode, setIsEditMode] = useState(false);
return (
<EditableCellWrapper
<EditableCell
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => setIsEditMode(true)}
@@ -1,34 +1,52 @@
import { ChangeEvent, ComponentType, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper';
import { ChangeEvent, ComponentType, useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { useSearch } from '../../services/api/search/search';
import { SearchConfigType } from '../../interfaces/search/interface';
import { AnyEntity } from '../../interfaces/entities/generic.interface';
import { EditableRelationCreateButton } from './EditableRelationCreateButton';
import { isNonEmptyString } from '../../modules/utils/type-guards/isNonEmptyString';
import { isDefined } from '../../modules/utils/type-guards/isDefined';
import { FaPlus } from 'react-icons/fa';
import { HoverableMenuItem } from './HoverableMenuItem';
import { EditableCellMenu } from './EditableCellMenu';
import { CellNormalModeContainer } from './CellNormalModeContainer';
const StyledEditModeContainer = styled.div`
width: 200px;
margin-left: calc(-1 * ${(props) => props.theme.spacing(2)});
margin-right: calc(-1 * ${(props) => props.theme.spacing(2)});
// margin-left: calc(-1 * ${(props) => props.theme.spacing(2)});
// margin-right: calc(-1 * ${(props) => props.theme.spacing(2)});
`;
const StyledEditModeSelectedContainer = styled.div`
height: 31px;
display: flex;
align-items: center;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
padding-left: ${(props) => props.theme.spacing(1)};
padding-right: ${(props) => props.theme.spacing(1)};
`;
const StyledEditModeSearchContainer = styled.div`
height: 32px;
display: flex;
align-items: center;
border-top: 1px solid ${(props) => props.theme.primaryBorder};
padding-left: ${(props) => props.theme.spacing(1)};
padding-right: ${(props) => props.theme.spacing(1)};
`;
const StyledEditModeCreateButtonContainer = styled.div`
height: 36px;
display: flex;
align-items: center;
border-top: 1px solid ${(props) => props.theme.primaryBorder};
padding: ${(props) => props.theme.spacing(1)};
color: ${(props) => props.theme.text60};
`;
const StyledEditModeSearchInput = styled.input`
width: 100%;
border: none;
outline: none;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
&::placeholder {
font-weight: 'bold';
@@ -38,9 +56,10 @@ const StyledEditModeSearchInput = styled.input`
const StyledEditModeResults = styled.div`
border-top: 1px solid ${(props) => props.theme.primaryBorder};
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
padding-left: ${(props) => props.theme.spacing(1)};
padding-right: ${(props) => props.theme.spacing(1)};
`;
const StyledEditModeResultItem = styled.div`
height: 32px;
display: flex;
@@ -49,6 +68,16 @@ const StyledEditModeResultItem = styled.div`
user-select: none;
`;
const StyledCreateButtonIcon = styled.div`
color: ${(props) => props.theme.text100};
align-self: center;
padding-top: 4px;
`;
const StyledCreateButtonText = styled.div`
color: ${(props) => props.theme.text60};
`;
export type EditableRelationProps<
RelationType extends AnyEntity,
ChipComponentPropsType,
@@ -56,14 +85,18 @@ export type EditableRelationProps<
relation?: RelationType | null;
searchPlaceholder: string;
searchConfig: SearchConfigType<RelationType>;
changeHandler: (relation: RelationType) => void;
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
function EditableRelation<
RelationType extends AnyEntity,
ChipComponentPropsType,
@@ -71,72 +104,109 @@ function EditableRelation<
relation,
searchPlaceholder,
searchConfig,
changeHandler,
onChange,
onChangeSearchInput,
editModeHorizontalAlign,
ChipComponent,
chipComponentPropsMapper,
onCreate,
}: EditableRelationProps<RelationType, ChipComponentPropsType>) {
const [selectedRelation, setSelectedRelation] = useState(relation);
const [isEditMode, setIsEditMode] = useState(false);
const [filterSearchResults, setSearchInput, setFilterSearch] =
// 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);
setIsEditMode(false);
}
return (
<EditableCellWrapper
editModeHorizontalAlign={editModeHorizontalAlign}
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => {
if (!isEditMode) {
setIsEditMode(true);
<>
<EditableCellMenu
editModeHorizontalAlign={editModeHorizontalAlign}
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => {
if (!isEditMode) {
setIsEditMode(true);
}
}}
editModeContent={
<StyledEditModeContainer>
<StyledEditModeSelectedContainer>
{relation ? (
<ChipComponent {...chipComponentPropsMapper(relation)} />
) : (
<></>
)}
</StyledEditModeSelectedContainer>
<StyledEditModeSearchContainer>
<StyledEditModeSearchInput
placeholder={searchPlaceholder}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterSearch(searchConfig);
setSearchInput(event.target.value);
}}
/>
</StyledEditModeSearchContainer>
{createButtonIsVisible && (
<StyledEditModeCreateButtonContainer>
<HoverableMenuItem>
<EditableRelationCreateButton
onClick={handleCreateNewRelationButtonClick}
>
<StyledCreateButtonIcon>
<FaPlus />
</StyledCreateButtonIcon>
<StyledCreateButtonText>Create new</StyledCreateButtonText>
</EditableRelationCreateButton>
</HoverableMenuItem>
</StyledEditModeCreateButtonContainer>
)}
<StyledEditModeResults>
{filterSearchResults.results &&
filterSearchResults.results.map((result, index) => (
<StyledEditModeResultItem
key={index}
onClick={() => {
onChange(result.value);
setIsEditMode(false);
}}
>
<HoverableMenuItem>
<ChipComponent
{...chipComponentPropsMapper(result.value)}
/>
</HoverableMenuItem>
</StyledEditModeResultItem>
))}
</StyledEditModeResults>
</StyledEditModeContainer>
}
}}
editModeContent={
<StyledEditModeContainer>
<StyledEditModeSelectedContainer>
{selectedRelation ? (
<ChipComponent {...chipComponentPropsMapper(selectedRelation)} />
nonEditModeContent={
<CellNormalModeContainer>
{relation ? (
<ChipComponent {...chipComponentPropsMapper(relation)} />
) : (
<></>
)}
</StyledEditModeSelectedContainer>
<StyledEditModeSearchContainer>
<StyledEditModeSearchInput
placeholder={searchPlaceholder}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterSearch(searchConfig);
setSearchInput(event.target.value);
}}
/>
</StyledEditModeSearchContainer>
<StyledEditModeResults>
{filterSearchResults.results &&
filterSearchResults.results.map((result, index) => (
<StyledEditModeResultItem
key={index}
onClick={() => {
setSelectedRelation(result.value);
changeHandler(result.value);
setIsEditMode(false);
}}
>
<ChipComponent {...chipComponentPropsMapper(result.value)} />
</StyledEditModeResultItem>
))}
</StyledEditModeResults>
</StyledEditModeContainer>
}
nonEditModeContent={
<div>
{selectedRelation ? (
<ChipComponent {...chipComponentPropsMapper(selectedRelation)} />
) : (
<></>
)}
</div>
}
/>
</CellNormalModeContainer>
}
/>
</>
);
}
@@ -0,0 +1,24 @@
import styled from '@emotion/styled';
export const EditableRelationCreateButton = styled.button`
display: flex;
align-items: center;
border: none;
font-size: ${(props) => props.theme.fontSizeMedium};
cursor: pointer;
user-select: none;
padding-top: ${(props) => props.theme.spacing(1)};
padding-bottom: ${(props) => props.theme.spacing(1)};
padding-left: ${(props) => props.theme.spacing(1)};
font-family: 'Inter';
border-radius: 4px;
width: 100%;
height: 31px;
background: none;
gap: 8px;
// :hover {
// background: rgba(0, 0, 0, 0.04);
// color: ${(props) => props.theme.text100};
// }
// margin-bottom: calc(${(props) => props.theme.spacing(1)} / 2);
`;
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { ChangeEvent, useRef, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper';
import { EditableCell } from './EditableCell';
type OwnProps = {
placeholder?: string;
@@ -13,6 +13,7 @@ type StyledEditModeProps = {
isEditMode: boolean;
};
// TODO: refactor
const StyledInplaceInput = styled.input<StyledEditModeProps>`
width: 100%;
border: none;
@@ -40,7 +41,7 @@ function EditableText({
const [isEditMode, setIsEditMode] = useState(false);
return (
<EditableCellWrapper
<EditableCell
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => setIsEditMode(true)}
@@ -59,7 +60,7 @@ function EditableText({
/>
}
nonEditModeContent={<StyledNoEditText>{inputValue}</StyledNoEditText>}
></EditableCellWrapper>
></EditableCell>
);
}
@@ -0,0 +1,19 @@
import styled from '@emotion/styled';
export const HoverableMenuItem = styled.div`
position: relative;
box-sizing: border-box;
height: 100%;
width: 100%;
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
border-radius: 4px;
background: rgba(0, 0, 0, 0);
transition: background 0.1s ease;
&:hover {
background: rgba(0, 0, 0, 0.04);
}
`;
@@ -1,4 +1,4 @@
import EditableFullName from '../EditableFullName';
import { EditablePeopleFullName } from '../../people/EditablePeopleFullName';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
@@ -6,23 +6,23 @@ import { MemoryRouter } from 'react-router-dom';
const component = {
title: 'EditableFullName',
component: EditableFullName,
component: EditablePeopleFullName,
};
type OwnProps = {
firstname: string;
lastname: string;
changeHandler: (firstname: string, lastname: string) => void;
onChange: (firstname: string, lastname: string) => void;
};
export default component;
const Template: StoryFn<typeof EditableFullName> = (args: OwnProps) => {
const Template: StoryFn<typeof EditablePeopleFullName> = (args: OwnProps) => {
return (
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditableFullName {...args} />
<EditablePeopleFullName {...args} />
</div>
</ThemeProvider>
</MemoryRouter>
@@ -33,7 +33,7 @@ export const EditableFullNameStory = Template.bind({});
EditableFullNameStory.args = {
firstname: 'John',
lastname: 'Doe',
changeHandler: () => {
console.log('changed');
onChange: () => {
console.log('validated');
},
};
@@ -87,7 +87,7 @@ EditableRelationStory.args = {
: undefined,
};
},
changeHandler: (relation: Company) => {
onChange: (relation: Company) => {
console.log('changed', relation);
},
searchConfig: {
@@ -15,7 +15,7 @@ it('Checks the EditableRelation editing event bubbles up', async () => {
Company,
CompanyChipPropsType
>)}
changeHandler={func}
onChange={func}
/>,
);
@@ -0,0 +1,65 @@
import styled from '@emotion/styled';
import { ChangeEvent, useRef } from 'react';
type OwnProps = {
leftValue: string;
rightValue: string;
leftValuePlaceholder: string;
rightValuePlaceholder: string;
onChange: (leftValue: string, rightValue: string) => void;
};
const StyledContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
& > input:last-child {
padding-left: ${(props) => props.theme.spacing(2)};
border-left: 1px solid ${(props) => props.theme.primaryBorder};
}
`;
const StyledEditInplaceInput = styled.input`
width: 45%;
border: none;
outline: none;
height: 18px;
&::placeholder {
font-weight: bold;
color: ${(props) => props.theme.text20};
}
`;
export function DoubleTextInput({
leftValue,
rightValue,
leftValuePlaceholder,
rightValuePlaceholder,
onChange,
}: OwnProps) {
const firstValueInputRef = useRef<HTMLInputElement>(null);
return (
<StyledContainer>
<StyledEditInplaceInput
autoFocus
placeholder={leftValuePlaceholder}
ref={firstValueInputRef}
value={leftValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value, rightValue);
}}
/>
<StyledEditInplaceInput
placeholder={rightValuePlaceholder}
ref={firstValueInputRef}
value={rightValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(leftValue, event.target.value);
}}
/>
</StyledContainer>
);
}
@@ -0,0 +1,39 @@
import { useState } from 'react';
import PersonChip from '../chips/PersonChip';
import { EditableDoubleText } from '../editable-cell/EditableDoubleText';
type OwnProps = {
firstname: string;
lastname: string;
onChange: (firstname: string, lastname: string) => void;
};
export function EditablePeopleFullName({
firstname,
lastname,
onChange,
}: OwnProps) {
const [firstnameValue, setFirstnameValue] = useState(firstname);
const [lastnameValue, setLastnameValue] = useState(lastname);
function handleDoubleTextChange(
firstValue: string,
secondValue: string,
): void {
setFirstnameValue(firstValue);
setLastnameValue(secondValue);
onChange(firstnameValue, lastnameValue);
}
return (
<EditableDoubleText
firstValue={firstnameValue}
secondValue={lastnameValue}
firstValuePlaceholder="First name"
secondValuePlaceholder="Last name"
onChange={handleDoubleTextChange}
nonEditModeContent={<PersonChip name={firstname + ' ' + lastname} />}
/>
);
}
@@ -0,0 +1,111 @@
import {
QueryMode,
useInsertCompanyMutation,
useUpdatePeopleMutation,
} from '../../generated/graphql';
import {
Person,
mapToGqlPerson,
} from '../../interfaces/entities/person.interface';
import {
Company,
mapToCompany,
} from '../../interfaces/entities/company.interface';
import CompanyChip, { CompanyChipPropsType } from '../chips/CompanyChip';
import EditableRelation from '../editable-cell/EditableRelation';
import { SEARCH_COMPANY_QUERY } from '../../services/api/search/search';
import { SearchConfigType } from '../../interfaces/search/interface';
import { useState } from 'react';
import { PeopleCompanyCreateCell } from './PeopleCompanyCreateCell';
import { v4 } from 'uuid';
export type OwnProps = {
people: Person;
};
export function PeopleCompanyCell({ people }: OwnProps) {
const [isCreating, setIsCreating] = useState(false);
const [insertCompany] = useInsertCompanyMutation();
const [updatePeople] = useUpdatePeopleMutation();
const [initialCompanyName, setInitialCompanyName] = useState('');
async function handleCompanyCreate(
companyName: string,
companyDomainName: string,
) {
const newCompanyId = v4();
try {
await insertCompany({
variables: {
id: newCompanyId,
name: companyName,
domain_name: companyDomainName,
address: '',
created_at: new Date().toISOString(),
},
});
await updatePeople({
variables: {
...mapToGqlPerson(people),
company_id: newCompanyId,
},
});
} catch (error) {
// TODO: handle error better
console.log(error);
}
setIsCreating(false);
}
// TODO: should be replaced with search context
function handleChangeSearchInput(searchInput: string) {
setInitialCompanyName(searchInput);
}
return isCreating ? (
<PeopleCompanyCreateCell
initialCompanyName={initialCompanyName}
onCreate={handleCompanyCreate}
/>
) : (
<EditableRelation<Company, CompanyChipPropsType>
relation={people.company}
searchPlaceholder="Company"
ChipComponent={CompanyChip}
chipComponentPropsMapper={(company): CompanyChipPropsType => {
return {
name: company.name || '',
picture: `https://www.google.com/s2/favicons?domain=${company.domainName}&sz=256`,
};
}}
onChange={async (relation) => {
await updatePeople({
variables: {
...mapToGqlPerson(people),
company_id: relation.id,
},
});
}}
onChangeSearchInput={handleChangeSearchInput}
searchConfig={
{
query: SEARCH_COMPANY_QUERY,
template: (searchInput: string) => ({
name: { contains: `%${searchInput}%`, mode: QueryMode.Insensitive },
}),
resultMapper: (company) => ({
render: (company) => company.name,
value: mapToCompany(company),
}),
} satisfies SearchConfigType<Company>
}
onCreate={() => {
setIsCreating(true);
}}
/>
);
}
@@ -0,0 +1,57 @@
import { useRef, useState } from 'react';
import { DoubleTextInput } from '../inputs/DoubleTextInput';
import { useListenClickOutsideArrayOfRef } from '../../modules/ui/hooks/useListenClickOutsideArrayOfRef';
import { useHotkeys } from 'react-hotkeys-hook';
import { CellBaseContainer } from '../editable-cell/CellBaseContainer';
import { CellEditModeContainer } from '../editable-cell/CellEditModeContainer';
type OwnProps = {
initialCompanyName: string;
onCreate: (companyName: string, companyDomainName: string) => void;
};
export function PeopleCompanyCreateCell({
initialCompanyName,
onCreate,
}: OwnProps) {
const [companyName, setCompanyName] = useState(initialCompanyName);
const [companyDomainName, setCompanyDomainName] = useState('');
const containerRef = useRef(null);
useListenClickOutsideArrayOfRef([containerRef], () => {
onCreate(companyName, companyDomainName);
});
useHotkeys(
'enter, escape',
() => {
onCreate(companyName, companyDomainName);
},
{
enableOnFormTags: true,
enableOnContentEditable: true,
preventDefault: true,
},
[containerRef, companyName, companyDomainName, onCreate],
);
function handleDoubleTextChange(leftValue: string, rightValue: string): void {
setCompanyDomainName(leftValue);
setCompanyName(rightValue);
}
return (
<CellBaseContainer ref={containerRef}>
<CellEditModeContainer editModeVerticalPosition="over">
<DoubleTextInput
leftValue={companyDomainName}
rightValue={companyName}
leftValuePlaceholder="URL"
rightValuePlaceholder="Name"
onChange={handleDoubleTextChange}
/>
</CellEditModeContainer>
</CellBaseContainer>
);
}