Restructure project (#124)
This commit is contained in:
96
front/src/components/editable-cell/EditableCellWrapper.tsx
Normal file
96
front/src/components/editable-cell/EditableCellWrapper.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
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%;
|
||||
`;
|
||||
|
||||
type StyledEditModeContainerProps = {
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
};
|
||||
|
||||
const StyledNonEditModeContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-left: ${(props) => props.theme.spacing(2)};
|
||||
padding-right: ${(props) => props.theme.spacing(2)};
|
||||
`;
|
||||
|
||||
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;
|
||||
59
front/src/components/editable-cell/EditableChip.tsx
Normal file
59
front/src/components/editable-cell/EditableChip.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { ChangeEvent, ComponentType, useRef, useState } from 'react';
|
||||
import EditableCellWrapper from './EditableCellWrapper';
|
||||
|
||||
export type EditableChipProps = {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
picture: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
ChipComponent: ComponentType<{ name: string; picture: string }>;
|
||||
};
|
||||
|
||||
const StyledInplaceInput = styled.input`
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
font-weight: 'bold';
|
||||
color: props.theme.text20;
|
||||
}
|
||||
`;
|
||||
function EditableChip({
|
||||
value,
|
||||
placeholder,
|
||||
changeHandler,
|
||||
picture,
|
||||
editModeHorizontalAlign,
|
||||
ChipComponent,
|
||||
}: EditableChipProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
return (
|
||||
<EditableCellWrapper
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => setIsEditMode(true)}
|
||||
isEditMode={isEditMode}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<StyledInplaceInput
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<ChipComponent name={inputValue} picture={picture} />}
|
||||
></EditableCellWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableChip;
|
||||
109
front/src/components/editable-cell/EditableDate.tsx
Normal file
109
front/src/components/editable-cell/EditableDate.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import EditableCellWrapper from './EditableCellWrapper';
|
||||
import DatePicker from '../form/DatePicker';
|
||||
import { CalendarContainer } from 'react-datepicker';
|
||||
import { modalBackground } from '../../layout/styles/themes';
|
||||
|
||||
export type EditableDateProps = {
|
||||
value: Date;
|
||||
changeHandler: (date: Date) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export type StyledCalendarContainerProps = {
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
const StyledCalendarContainer = styled.div<StyledCalendarContainerProps>`
|
||||
position: absolute;
|
||||
border: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
border-radius: 8px;
|
||||
width: 280px;
|
||||
box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09);
|
||||
z-index: 1;
|
||||
left: -10px;
|
||||
${modalBackground};
|
||||
`;
|
||||
function EditableDate({
|
||||
value,
|
||||
changeHandler,
|
||||
editModeHorizontalAlign,
|
||||
}: EditableDateProps) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
type DivProps = React.HTMLProps<HTMLDivElement>;
|
||||
|
||||
const DateDisplay = forwardRef<HTMLDivElement, DivProps>(
|
||||
({ value, onClick }, ref) => (
|
||||
<div onClick={onClick} ref={ref}>
|
||||
{value &&
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}).format(new Date(value as string))}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
interface DatePickerContainerProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DatePickerContainer = ({
|
||||
className,
|
||||
children,
|
||||
}: DatePickerContainerProps) => {
|
||||
return (
|
||||
<StyledCalendarContainer>
|
||||
<CalendarContainer className={className}>
|
||||
<div style={{ position: 'relative' }}>{children}</div>
|
||||
</CalendarContainer>
|
||||
</StyledCalendarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EditableCellWrapper
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => setIsEditMode(true)}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<StyledContainer>
|
||||
<DatePicker
|
||||
date={inputValue}
|
||||
onChangeHandler={(date: Date) => {
|
||||
changeHandler(date);
|
||||
setInputValue(date);
|
||||
}}
|
||||
customInput={<DateDisplay />}
|
||||
customContainer={DatePickerContainer}
|
||||
/>
|
||||
</StyledContainer>
|
||||
}
|
||||
nonEditModeContent={
|
||||
<StyledContainer>
|
||||
<div>
|
||||
{inputValue &&
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}).format(inputValue)}
|
||||
</div>
|
||||
</StyledContainer>
|
||||
}
|
||||
></EditableCellWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableDate;
|
||||
77
front/src/components/editable-cell/EditableFullName.tsx
Normal file
77
front/src/components/editable-cell/EditableFullName.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
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;
|
||||
72
front/src/components/editable-cell/EditablePhone.tsx
Normal file
72
front/src/components/editable-cell/EditablePhone.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { ChangeEvent, MouseEvent, useRef, useState } from 'react';
|
||||
import EditableCellWrapper from './EditableCellWrapper';
|
||||
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
|
||||
import Link from '../link/Link';
|
||||
|
||||
type OwnProps = {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
};
|
||||
|
||||
type StyledEditModeProps = {
|
||||
isEditMode: boolean;
|
||||
};
|
||||
|
||||
const StyledEditInplaceInput = styled.input<StyledEditModeProps>`
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
font-weight: bold;
|
||||
color: ${(props) => props.theme.text20};
|
||||
}
|
||||
`;
|
||||
|
||||
function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
return (
|
||||
<EditableCellWrapper
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => setIsEditMode(true)}
|
||||
editModeContent={
|
||||
<StyledEditInplaceInput
|
||||
autoFocus
|
||||
isEditMode={isEditMode}
|
||||
placeholder={placeholder || ''}
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={
|
||||
<div>
|
||||
{isValidPhoneNumber(inputValue) ? (
|
||||
<Link
|
||||
href={parsePhoneNumber(inputValue, 'FR')?.getURI()}
|
||||
onClick={(event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{parsePhoneNumber(inputValue, 'FR')?.formatInternational() ||
|
||||
inputValue}
|
||||
</Link>
|
||||
) : (
|
||||
<Link href="#">{inputValue}</Link>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditablePhone;
|
||||
144
front/src/components/editable-cell/EditableRelation.tsx
Normal file
144
front/src/components/editable-cell/EditableRelation.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import { ChangeEvent, ComponentType, useState } from 'react';
|
||||
import EditableCellWrapper from './EditableCellWrapper';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSearch } from '../../services/api/search/search';
|
||||
import {
|
||||
SearchConfigType,
|
||||
SearchableType,
|
||||
} from '../../interfaces/search/interface';
|
||||
|
||||
const StyledEditModeContainer = styled.div`
|
||||
width: 200px;
|
||||
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)};
|
||||
`;
|
||||
const StyledEditModeSearchContainer = styled.div`
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
`;
|
||||
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';
|
||||
color: ${(props) => props.theme.text20};
|
||||
}
|
||||
`;
|
||||
|
||||
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)};
|
||||
`;
|
||||
const StyledEditModeResultItem = styled.div`
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export type EditableRelationProps<
|
||||
RelationType extends SearchableType,
|
||||
ChipComponentPropsType,
|
||||
> = {
|
||||
relation?: RelationType | null;
|
||||
searchPlaceholder: string;
|
||||
searchConfig: SearchConfigType<RelationType>;
|
||||
changeHandler: (relation: RelationType) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
ChipComponent: ComponentType<ChipComponentPropsType>;
|
||||
chipComponentPropsMapper: (
|
||||
relation: RelationType,
|
||||
) => ChipComponentPropsType & JSX.IntrinsicAttributes;
|
||||
};
|
||||
|
||||
function EditableRelation<
|
||||
RelationType extends SearchableType,
|
||||
ChipComponentPropsType,
|
||||
>({
|
||||
relation,
|
||||
searchPlaceholder,
|
||||
searchConfig,
|
||||
changeHandler,
|
||||
editModeHorizontalAlign,
|
||||
ChipComponent,
|
||||
chipComponentPropsMapper,
|
||||
}: EditableRelationProps<RelationType, ChipComponentPropsType>) {
|
||||
const [selectedRelation, setSelectedRelation] = useState(relation);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
const [filterSearchResults, setSearchInput, setFilterSearch] =
|
||||
useSearch<RelationType>();
|
||||
|
||||
return (
|
||||
<EditableCellWrapper
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => {
|
||||
if (!isEditMode) {
|
||||
setIsEditMode(true);
|
||||
}
|
||||
}}
|
||||
editModeContent={
|
||||
<StyledEditModeContainer>
|
||||
<StyledEditModeSelectedContainer>
|
||||
{selectedRelation ? (
|
||||
<ChipComponent {...chipComponentPropsMapper(selectedRelation)} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableRelation;
|
||||
66
front/src/components/editable-cell/EditableText.tsx
Normal file
66
front/src/components/editable-cell/EditableText.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import EditableCellWrapper from './EditableCellWrapper';
|
||||
|
||||
type OwnProps = {
|
||||
placeholder?: string;
|
||||
content: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
type StyledEditModeProps = {
|
||||
isEditMode: boolean;
|
||||
};
|
||||
|
||||
const StyledInplaceInput = styled.input<StyledEditModeProps>`
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
font-weight: ${(props) => (props.isEditMode ? 'bold' : 'normal')};
|
||||
color: ${(props) =>
|
||||
props.isEditMode ? props.theme.text20 : 'transparent'};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledNoEditText = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
function EditableText({
|
||||
content,
|
||||
placeholder,
|
||||
changeHandler,
|
||||
editModeHorizontalAlign,
|
||||
}: OwnProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(content);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
return (
|
||||
<EditableCellWrapper
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => setIsEditMode(true)}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<StyledInplaceInput
|
||||
isEditMode={isEditMode}
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<StyledNoEditText>{inputValue}</StyledNoEditText>}
|
||||
></EditableCellWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableText;
|
||||
@ -0,0 +1,33 @@
|
||||
import EditableChip, { EditableChipProps } from '../EditableChip';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../layout/styles/themes';
|
||||
import { StoryFn } from '@storybook/react';
|
||||
import CompanyChip from '../../chips/CompanyChip';
|
||||
|
||||
const component = {
|
||||
title: 'EditableChip',
|
||||
component: EditableChip,
|
||||
};
|
||||
|
||||
export default component;
|
||||
|
||||
const Template: StoryFn<typeof EditableChip> = (args: EditableChipProps) => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<div data-testid="content-editable-parent">
|
||||
<EditableChip {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableChipStory = Template.bind({});
|
||||
EditableChipStory.args = {
|
||||
ChipComponent: CompanyChip,
|
||||
placeholder: 'Test',
|
||||
value: 'Test',
|
||||
picture: 'https://picsum.photos/200',
|
||||
changeHandler: () => {
|
||||
console.log('changed');
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import EditableDate, { EditableDateProps } from '../EditableDate';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../layout/styles/themes';
|
||||
import { StoryFn } from '@storybook/react';
|
||||
|
||||
const component = {
|
||||
title: 'EditableDate',
|
||||
component: EditableDate,
|
||||
};
|
||||
|
||||
export default component;
|
||||
|
||||
const Template: StoryFn<typeof EditableDate> = (args: EditableDateProps) => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<div data-testid="content-editable-parent">
|
||||
<EditableDate {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableDateStory = Template.bind({});
|
||||
EditableDateStory.args = {
|
||||
value: new Date(),
|
||||
changeHandler: () => {
|
||||
console.log('changed');
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import EditableFullName from '../EditableFullName';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../layout/styles/themes';
|
||||
import { StoryFn } from '@storybook/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
const component = {
|
||||
title: 'EditableFullName',
|
||||
component: EditableFullName,
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
changeHandler: (firstname: string, lastname: string) => void;
|
||||
};
|
||||
|
||||
export default component;
|
||||
|
||||
const Template: StoryFn<typeof EditableFullName> = (args: OwnProps) => {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<div data-testid="content-editable-parent">
|
||||
<EditableFullName {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableFullNameStory = Template.bind({});
|
||||
EditableFullNameStory.args = {
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
changeHandler: () => {
|
||||
console.log('changed');
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
import EditablePhone from '../EditablePhone';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../layout/styles/themes';
|
||||
import { StoryFn } from '@storybook/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
const component = {
|
||||
title: 'EditablePhone',
|
||||
component: EditablePhone,
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
value: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
};
|
||||
|
||||
export default component;
|
||||
|
||||
const Template: StoryFn<typeof EditablePhone> = (args: OwnProps) => {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<div data-testid="content-editable-parent">
|
||||
<EditablePhone {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditablePhoneStory = Template.bind({});
|
||||
EditablePhoneStory.args = {
|
||||
placeholder: 'Test placeholder',
|
||||
value: '+33657646543',
|
||||
changeHandler: () => {
|
||||
console.log('changed');
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,102 @@
|
||||
import EditableRelation, { EditableRelationProps } from '../EditableRelation';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../layout/styles/themes';
|
||||
import { StoryFn } from '@storybook/react';
|
||||
import CompanyChip, { CompanyChipPropsType } from '../../chips/CompanyChip';
|
||||
import {
|
||||
Company,
|
||||
mapToCompany,
|
||||
} from '../../../interfaces/entities/company.interface';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { SEARCH_COMPANY_QUERY } from '../../../services/api/search/search';
|
||||
import styled from '@emotion/styled';
|
||||
import { SearchConfigType } from '../../../interfaces/search/interface';
|
||||
|
||||
const component = {
|
||||
title: 'editable-cell/EditableRelation',
|
||||
component: EditableRelation,
|
||||
};
|
||||
|
||||
export default component;
|
||||
|
||||
const StyledParent = styled.div`
|
||||
height: 400px;
|
||||
`;
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: SEARCH_COMPANY_QUERY,
|
||||
variables: {
|
||||
where: undefined,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
companies: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: SEARCH_COMPANY_QUERY,
|
||||
variables: {
|
||||
where: { name: { _ilike: '%%' } },
|
||||
limit: 5,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
searchResults: [
|
||||
{ id: 'abnb', name: 'Airbnb', domain_name: 'abnb.com' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const Template: StoryFn<
|
||||
typeof EditableRelation<Company, CompanyChipPropsType>
|
||||
> = (args: EditableRelationProps<Company, CompanyChipPropsType>) => {
|
||||
return (
|
||||
<MockedProvider mocks={mocks}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<StyledParent data-testid="content-editable-parent">
|
||||
<EditableRelation<Company, CompanyChipPropsType> {...args} />
|
||||
</StyledParent>
|
||||
</ThemeProvider>
|
||||
</MockedProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableRelationStory = Template.bind({});
|
||||
EditableRelationStory.args = {
|
||||
relation: {
|
||||
__typename: 'companies',
|
||||
id: '123',
|
||||
name: 'Heroku',
|
||||
domain_name: 'heroku.com',
|
||||
} as Company,
|
||||
ChipComponent: CompanyChip,
|
||||
chipComponentPropsMapper: (company: Company): CompanyChipPropsType => {
|
||||
return {
|
||||
name: company.name || '',
|
||||
picture: company.domainName
|
||||
? `https://www.google.com/s2/favicons?domain=${company.domainName}&sz=256`
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
changeHandler: (relation: Company) => {
|
||||
console.log('changed', relation);
|
||||
},
|
||||
searchConfig: {
|
||||
query: SEARCH_COMPANY_QUERY,
|
||||
template: (searchInput: string) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
resultMapper: (company) => ({
|
||||
render: (company) => company.name,
|
||||
value: mapToCompany(company),
|
||||
}),
|
||||
} satisfies SearchConfigType<Company>,
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import EditableText from '../EditableText';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../layout/styles/themes';
|
||||
import { StoryFn } from '@storybook/react';
|
||||
|
||||
const component = {
|
||||
title: 'EditableText',
|
||||
component: EditableText,
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
content: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
};
|
||||
|
||||
export default component;
|
||||
|
||||
const Template: StoryFn<typeof EditableText> = (args: OwnProps) => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<div data-testid="content-editable-parent">
|
||||
<EditableText {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableTextStory = Template.bind({});
|
||||
EditableTextStory.args = {
|
||||
placeholder: 'Test placeholder',
|
||||
content: 'Test string',
|
||||
changeHandler: () => {
|
||||
console.log('changed');
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { EditableChipStory } from '../__stories__/EditableChip.stories';
|
||||
import CompanyChip from '../../chips/CompanyChip';
|
||||
|
||||
it('Checks the EditableChip editing event bubbles up', async () => {
|
||||
const func = jest.fn(() => null);
|
||||
const { getByTestId } = render(
|
||||
<EditableChipStory
|
||||
value="test"
|
||||
picture="http://"
|
||||
changeHandler={func}
|
||||
ChipComponent={CompanyChip}
|
||||
/>,
|
||||
);
|
||||
|
||||
const parent = getByTestId('content-editable-parent');
|
||||
|
||||
const wrapper = parent.querySelector('div');
|
||||
|
||||
if (!wrapper) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
fireEvent.click(wrapper);
|
||||
|
||||
const editableInput = parent.querySelector('input');
|
||||
|
||||
if (!editableInput) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
|
||||
fireEvent.change(editableInput, { target: { value: 'Test' } });
|
||||
expect(func).toBeCalledWith('Test');
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import { EditableDateStory } from '../__stories__/EditableDate.stories';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
it('Checks the EditableDate editing event bubbles up', async () => {
|
||||
const changeHandler = jest.fn(() => null);
|
||||
const { getByTestId, getByText } = render(
|
||||
<EditableDateStory
|
||||
value={new Date('2021-03-03')}
|
||||
changeHandler={changeHandler}
|
||||
/>,
|
||||
);
|
||||
|
||||
const parent = getByTestId('content-editable-parent');
|
||||
|
||||
const wrapper = parent.querySelector('div');
|
||||
|
||||
if (!wrapper) {
|
||||
throw new Error('Cell Wrapper not found');
|
||||
}
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(wrapper);
|
||||
const dateDisplay = parent.querySelector('div');
|
||||
if (!dateDisplay) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
});
|
||||
waitFor(() => {
|
||||
expect(getByText('March 2021')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(getByText('5'));
|
||||
expect(changeHandler).toHaveBeenCalledWith(new Date('2021-03-05'));
|
||||
});
|
||||
@ -0,0 +1,41 @@
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { EditableFullNameStory } from '../__stories__/EditableFullName.stories';
|
||||
|
||||
it('Checks the EditableFullName editing event bubbles up', async () => {
|
||||
const func = jest.fn(() => null);
|
||||
const { getByTestId } = render(
|
||||
<EditableFullNameStory
|
||||
firstname="Jone"
|
||||
lastname="Doe"
|
||||
changeHandler={func}
|
||||
/>,
|
||||
);
|
||||
|
||||
const parent = getByTestId('content-editable-parent');
|
||||
|
||||
const wrapper = parent.querySelector('div');
|
||||
|
||||
if (!wrapper) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
fireEvent.click(wrapper);
|
||||
|
||||
const firstnameInput = parent.querySelector('input:first-child');
|
||||
|
||||
if (!firstnameInput) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
|
||||
fireEvent.change(firstnameInput, { target: { value: 'Jo' } });
|
||||
expect(func).toBeCalledWith('Jo', 'Doe');
|
||||
|
||||
const lastnameInput = parent.querySelector('input:last-child');
|
||||
|
||||
if (!lastnameInput) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
|
||||
fireEvent.change(lastnameInput, { target: { value: 'Do' } });
|
||||
expect(func).toBeCalledWith('Jo', 'Do');
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { EditablePhoneStory } from '../__stories__/EditablePhone.stories';
|
||||
|
||||
it('Checks the EditablePhone editing event bubbles up', async () => {
|
||||
const func = jest.fn(() => null);
|
||||
const { getByTestId } = render(
|
||||
<EditablePhoneStory value="+33786405315" changeHandler={func} />,
|
||||
);
|
||||
|
||||
const parent = getByTestId('content-editable-parent');
|
||||
|
||||
const wrapper = parent.querySelector('div');
|
||||
|
||||
if (!wrapper) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
fireEvent.click(wrapper);
|
||||
|
||||
const editableInput = parent.querySelector('input');
|
||||
|
||||
if (!editableInput) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
|
||||
fireEvent.change(editableInput, { target: { value: '23' } });
|
||||
expect(func).toBeCalledWith('23');
|
||||
});
|
||||
@ -0,0 +1,64 @@
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import { EditableRelationStory } from '../__stories__/EditableRelation.stories';
|
||||
import { CompanyChipPropsType } from '../../chips/CompanyChip';
|
||||
|
||||
import { EditableRelationProps } from '../EditableRelation';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Company } from '../../../interfaces/company.interface';
|
||||
|
||||
it('Checks the EditableRelation editing event bubbles up', async () => {
|
||||
const func = jest.fn(() => null);
|
||||
const { getByTestId, getByText } = render(
|
||||
<EditableRelationStory
|
||||
{...(EditableRelationStory.args as EditableRelationProps<
|
||||
Company,
|
||||
CompanyChipPropsType
|
||||
>)}
|
||||
changeHandler={func}
|
||||
/>,
|
||||
);
|
||||
|
||||
const parent = getByTestId('content-editable-parent');
|
||||
|
||||
const wrapper = parent.querySelector('div');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Heroku')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
if (!wrapper) {
|
||||
throw new Error('Editable relation not found');
|
||||
}
|
||||
fireEvent.click(wrapper);
|
||||
|
||||
const input = parent.querySelector('input');
|
||||
if (!input) {
|
||||
throw new Error('Search input not found');
|
||||
}
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { value: 'Ai' } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Airbnb')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByText('Airbnb'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(func).toBeCalledWith({
|
||||
__typename: 'companies',
|
||||
accountOwner: undefined,
|
||||
address: undefined,
|
||||
domainName: 'abnb.com',
|
||||
employees: undefined,
|
||||
creationDate: undefined,
|
||||
id: 'abnb',
|
||||
name: 'Airbnb',
|
||||
pipes: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { EditableTextStory } from '../__stories__/EditableText.stories';
|
||||
|
||||
it('Checks the EditableText editing event bubbles up', async () => {
|
||||
const func = jest.fn(() => null);
|
||||
const { getByTestId } = render(
|
||||
<EditableTextStory content="test" changeHandler={func} />,
|
||||
);
|
||||
|
||||
const parent = getByTestId('content-editable-parent');
|
||||
|
||||
const wrapper = parent.querySelector('div');
|
||||
|
||||
if (!wrapper) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
fireEvent.click(wrapper);
|
||||
|
||||
const editableInput = parent.querySelector('input');
|
||||
|
||||
if (!editableInput) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
|
||||
fireEvent.change(editableInput, { target: { value: '23' } });
|
||||
expect(func).toBeCalledWith('23');
|
||||
});
|
||||
Reference in New Issue
Block a user