Create and EditableRelation component and make it generic (#107)
* Create and EditableRelation component and make it generic * Refactor EditableCell component to be more flexible * Complete Company picker on people page * Fix lint
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
type OwnProps = {
|
export type CompanyChipPropsType = {
|
||||||
name: string;
|
name: string;
|
||||||
picture?: string;
|
picture?: string;
|
||||||
};
|
};
|
||||||
@ -26,7 +26,7 @@ const StyledContainer = styled.span`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function CompanyChip({ name, picture }: OwnProps) {
|
function CompanyChip({ name, picture }: CompanyChipPropsType) {
|
||||||
return (
|
return (
|
||||||
<StyledContainer data-testid="company-chip">
|
<StyledContainer data-testid="company-chip">
|
||||||
{picture && (
|
{picture && (
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { forwardRef, useState } from 'react';
|
import React, { ReactElement, forwardRef, useState } from 'react';
|
||||||
import ReactDatePicker from 'react-datepicker';
|
import ReactDatePicker from 'react-datepicker';
|
||||||
|
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
@ -8,6 +8,7 @@ export type DatePickerProps = {
|
|||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
date: Date;
|
date: Date;
|
||||||
onChangeHandler: (date: Date) => void;
|
onChangeHandler: (date: Date) => void;
|
||||||
|
customInput?: ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -34,11 +35,12 @@ const StyledContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function DatePicker({ date, onChangeHandler, isOpen }: DatePickerProps) {
|
function DatePicker({ date, onChangeHandler, customInput }: DatePickerProps) {
|
||||||
const [startDate, setStartDate] = useState(date);
|
const [startDate, setStartDate] = useState(date);
|
||||||
|
|
||||||
type DivProps = React.HTMLProps<HTMLDivElement>;
|
type DivProps = React.HTMLProps<HTMLDivElement>;
|
||||||
const DateDisplay = forwardRef<HTMLDivElement, DivProps>(
|
|
||||||
|
const DefaultDateDisplay = forwardRef<HTMLDivElement, DivProps>(
|
||||||
({ value, onClick }, ref) => (
|
({ value, onClick }, ref) => (
|
||||||
<div onClick={onClick} ref={ref}>
|
<div onClick={onClick} ref={ref}>
|
||||||
{value &&
|
{value &&
|
||||||
@ -54,30 +56,13 @@ function DatePicker({ date, onChangeHandler, isOpen }: DatePickerProps) {
|
|||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
open={isOpen}
|
open={true}
|
||||||
selected={startDate}
|
selected={startDate}
|
||||||
onChange={(date: Date) => {
|
onChange={(date: Date) => {
|
||||||
setStartDate(date);
|
setStartDate(date);
|
||||||
onChangeHandler(date);
|
onChangeHandler(date);
|
||||||
}}
|
}}
|
||||||
popperPlacement="bottom"
|
customInput={customInput ? customInput : <DefaultDateDisplay />}
|
||||||
popperModifiers={[
|
|
||||||
{
|
|
||||||
name: 'offset',
|
|
||||||
options: {
|
|
||||||
offset: [55, 0],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'preventOverflow',
|
|
||||||
options: {
|
|
||||||
rootBoundary: 'viewport',
|
|
||||||
tether: false,
|
|
||||||
altAxis: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
customInput={<DateDisplay />}
|
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { fireEvent, render } from '@testing-library/react';
|
|||||||
import { DatePickerStory } from '../__stories__/Datepicker.stories';
|
import { DatePickerStory } from '../__stories__/Datepicker.stories';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
it('Checks the datepicker renders', () => {
|
it('Checks the datepicker renders', async () => {
|
||||||
const changeHandler = jest.fn();
|
const changeHandler = jest.fn();
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<DatePickerStory
|
<DatePickerStory
|
||||||
@ -11,6 +11,11 @@ it('Checks the datepicker renders', () => {
|
|||||||
onChangeHandler={changeHandler}
|
onChangeHandler={changeHandler}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
expect(getByText('Mar 3, 2021')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.click(getByText('Mar 3, 2021'));
|
fireEvent.click(getByText('Mar 3, 2021'));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -151,28 +151,6 @@ function Table<TData extends { id: string }, SortField, FilterProperies>({
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
{table
|
|
||||||
.getFooterGroups()
|
|
||||||
.flatMap((group) => group.headers)
|
|
||||||
.filter((header) => !!header.column.columnDef.footer).length >
|
|
||||||
0 && (
|
|
||||||
<tfoot>
|
|
||||||
{table.getFooterGroups().map((footerGroup) => (
|
|
||||||
<tr key={footerGroup.id}>
|
|
||||||
{footerGroup.headers.map((header) => (
|
|
||||||
<th key={header.id}>
|
|
||||||
{header.isPlaceholder
|
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.footer,
|
|
||||||
header.getContext(),
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tfoot>
|
|
||||||
)}
|
|
||||||
</StyledTable>
|
</StyledTable>
|
||||||
</StyledTableScrollableContainer>
|
</StyledTableScrollableContainer>
|
||||||
</StyledTableWithHeader>
|
</StyledTableWithHeader>
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { css } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { ReactElement, useRef, useState } from 'react';
|
import { ReactElement, useRef } from 'react';
|
||||||
import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter';
|
import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter';
|
||||||
import { ThemeType } from '../../../layout/styles/themes';
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
children: ReactElement;
|
editModeContent: ReactElement;
|
||||||
onEditModeChange: (isEditMode: boolean) => void;
|
nonEditModeContent: ReactElement;
|
||||||
shouldAlignRight?: boolean;
|
editModeHorizontalAlign?: 'left' | 'right';
|
||||||
|
editModeVerticalPosition?: 'over' | 'below';
|
||||||
|
isEditMode?: boolean;
|
||||||
|
onOutsideClick?: () => void;
|
||||||
|
onInsideClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledWrapper = styled.div`
|
const StyledWrapper = styled.div`
|
||||||
@ -19,63 +21,74 @@ const StyledWrapper = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type styledEditModeWrapperProps = {
|
type StyledEditModeContainerProps = {
|
||||||
isEditMode: boolean;
|
editModeHorizontalAlign?: 'left' | 'right';
|
||||||
shouldAlignRight?: boolean;
|
editModeVerticalPosition?: 'over' | 'below';
|
||||||
};
|
};
|
||||||
|
|
||||||
const styledEditModeWrapper = (
|
const StyledNonEditModeContainer = styled.div`
|
||||||
props: styledEditModeWrapperProps & { theme: ThemeType },
|
display: flex;
|
||||||
) =>
|
align-items: center;
|
||||||
css`
|
|
||||||
position: absolute;
|
|
||||||
left: ${props.shouldAlignRight ? 'auto' : '0'};
|
|
||||||
right: ${props.shouldAlignRight ? '0' : 'auto'};
|
|
||||||
width: 260px;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
padding-left: ${props.theme.spacing(2)};
|
|
||||||
padding-right: ${props.theme.spacing(2)};
|
|
||||||
background: ${props.theme.primaryBackground};
|
|
||||||
border: 1px solid ${props.theme.primaryBorder};
|
|
||||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.16);
|
|
||||||
z-index: 1;
|
|
||||||
border-radius: 4px;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Container = styled.div<styledEditModeWrapperProps>`
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
padding-left: ${(props) => props.theme.spacing(2)};
|
padding-left: ${(props) => props.theme.spacing(2)};
|
||||||
padding-right: ${(props) => props.theme.spacing(2)};
|
padding-right: ${(props) => props.theme.spacing(2)};
|
||||||
${(props) => props.isEditMode && styledEditModeWrapper(props)}
|
`;
|
||||||
|
|
||||||
|
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({
|
function EditableCellWrapper({
|
||||||
children,
|
editModeContent,
|
||||||
onEditModeChange,
|
nonEditModeContent,
|
||||||
shouldAlignRight,
|
editModeHorizontalAlign = 'left',
|
||||||
|
editModeVerticalPosition = 'over',
|
||||||
|
isEditMode = false,
|
||||||
|
onOutsideClick,
|
||||||
|
onInsideClick,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
|
||||||
|
|
||||||
const wrapperRef = useRef(null);
|
const wrapperRef = useRef(null);
|
||||||
useOutsideAlerter(wrapperRef, () => {
|
useOutsideAlerter(wrapperRef, () => {
|
||||||
setIsEditMode(false);
|
onOutsideClick && onOutsideClick();
|
||||||
onEditModeChange(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper
|
<StyledWrapper
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEditMode(true);
|
onInsideClick && onInsideClick();
|
||||||
onEditModeChange(true);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container shouldAlignRight={shouldAlignRight} isEditMode={isEditMode}>
|
<StyledNonEditModeContainer>
|
||||||
{children}
|
{nonEditModeContent}
|
||||||
</Container>
|
</StyledNonEditModeContainer>
|
||||||
|
{isEditMode && (
|
||||||
|
<StyledEditModeContainer
|
||||||
|
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||||
|
editModeVerticalPosition={editModeVerticalPosition}
|
||||||
|
>
|
||||||
|
{editModeContent}
|
||||||
|
</StyledEditModeContainer>
|
||||||
|
)}
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,23 +7,18 @@ export type EditableChipProps = {
|
|||||||
value: string;
|
value: string;
|
||||||
picture: string;
|
picture: string;
|
||||||
changeHandler: (updated: string) => void;
|
changeHandler: (updated: string) => void;
|
||||||
shouldAlignRight?: boolean;
|
editModeHorizontalAlign?: 'left' | 'right';
|
||||||
ChipComponent: ComponentType<{ name: string; picture: string }>;
|
ChipComponent: ComponentType<{ name: string; picture: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StyledEditModeProps = {
|
const StyledInplaceInput = styled.input`
|
||||||
isEditMode: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledInplaceInput = styled.input<StyledEditModeProps>`
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
font-weight: ${(props) => (props.isEditMode ? 'bold' : 'normal')};
|
font-weight: 'bold';
|
||||||
color: ${(props) =>
|
color: props.theme.text20;
|
||||||
props.isEditMode ? props.theme.text20 : 'transparent'};
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
function EditableChip({
|
function EditableChip({
|
||||||
@ -31,25 +26,21 @@ function EditableChip({
|
|||||||
placeholder,
|
placeholder,
|
||||||
changeHandler,
|
changeHandler,
|
||||||
picture,
|
picture,
|
||||||
shouldAlignRight,
|
editModeHorizontalAlign,
|
||||||
ChipComponent,
|
ChipComponent,
|
||||||
}: EditableChipProps) {
|
}: EditableChipProps) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [inputValue, setInputValue] = useState(value);
|
const [inputValue, setInputValue] = useState(value);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
const onEditModeChange = (isEditMode: boolean) => {
|
|
||||||
setIsEditMode(isEditMode);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableCellWrapper
|
<EditableCellWrapper
|
||||||
onEditModeChange={onEditModeChange}
|
onOutsideClick={() => setIsEditMode(false)}
|
||||||
shouldAlignRight={shouldAlignRight}
|
onInsideClick={() => setIsEditMode(true)}
|
||||||
>
|
isEditMode={isEditMode}
|
||||||
{isEditMode ? (
|
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||||
|
editModeContent={
|
||||||
<StyledInplaceInput
|
<StyledInplaceInput
|
||||||
isEditMode={isEditMode}
|
|
||||||
placeholder={placeholder || ''}
|
placeholder={placeholder || ''}
|
||||||
autoFocus
|
autoFocus
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@ -59,10 +50,9 @@ function EditableChip({
|
|||||||
changeHandler(event.target.value);
|
changeHandler(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
}
|
||||||
<ChipComponent name={value} picture={picture} />
|
nonEditModeContent={<ChipComponent name={value} picture={picture} />}
|
||||||
)}
|
></EditableCellWrapper>
|
||||||
</EditableCellWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useState } from 'react';
|
import { forwardRef, useState } from 'react';
|
||||||
import EditableCellWrapper from './EditableCellWrapper';
|
import EditableCellWrapper from './EditableCellWrapper';
|
||||||
import DatePicker from '../../form/DatePicker';
|
import DatePicker from '../../form/DatePicker';
|
||||||
|
|
||||||
export type EditableDateProps = {
|
export type EditableDateProps = {
|
||||||
value: Date;
|
value: Date;
|
||||||
changeHandler: (date: Date) => void;
|
changeHandler: (date: Date) => void;
|
||||||
shouldAlignRight?: boolean;
|
editModeHorizontalAlign?: 'left' | 'right';
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -16,31 +16,57 @@ const StyledContainer = styled.div`
|
|||||||
function EditableDate({
|
function EditableDate({
|
||||||
value,
|
value,
|
||||||
changeHandler,
|
changeHandler,
|
||||||
shouldAlignRight,
|
editModeHorizontalAlign,
|
||||||
}: EditableDateProps) {
|
}: EditableDateProps) {
|
||||||
const [inputValue, setInputValue] = useState(value);
|
const [inputValue, setInputValue] = useState(value);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
const onEditModeChange = (isEditMode: boolean) => {
|
type DivProps = React.HTMLProps<HTMLDivElement>;
|
||||||
setIsEditMode(isEditMode);
|
|
||||||
};
|
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>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableCellWrapper
|
<EditableCellWrapper
|
||||||
onEditModeChange={onEditModeChange}
|
isEditMode={isEditMode}
|
||||||
shouldAlignRight={shouldAlignRight}
|
onOutsideClick={() => setIsEditMode(false)}
|
||||||
>
|
onInsideClick={() => setIsEditMode(true)}
|
||||||
<StyledContainer>
|
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||||
<DatePicker
|
editModeContent={
|
||||||
isOpen={isEditMode}
|
<StyledContainer>
|
||||||
date={inputValue}
|
<DatePicker
|
||||||
onChangeHandler={(date: Date) => {
|
date={inputValue}
|
||||||
changeHandler(date);
|
onChangeHandler={(date: Date) => {
|
||||||
setInputValue(date);
|
changeHandler(date);
|
||||||
}}
|
setInputValue(date);
|
||||||
/>
|
}}
|
||||||
</StyledContainer>
|
customInput={<DateDisplay />}
|
||||||
</EditableCellWrapper>
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
}
|
||||||
|
nonEditModeContent={
|
||||||
|
<StyledContainer>
|
||||||
|
<div>
|
||||||
|
{inputValue &&
|
||||||
|
new Intl.DateTimeFormat(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
}).format(inputValue)}
|
||||||
|
</div>
|
||||||
|
</StyledContainer>
|
||||||
|
}
|
||||||
|
></EditableCellWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,9 +40,10 @@ function EditableFullName({ firstname, lastname, changeHandler }: OwnProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableCellWrapper
|
<EditableCellWrapper
|
||||||
onEditModeChange={(editMode: boolean) => setIsEditMode(editMode)}
|
onOutsideClick={() => setIsEditMode(false)}
|
||||||
>
|
onInsideClick={() => setIsEditMode(true)}
|
||||||
{isEditMode ? (
|
isEditMode={isEditMode}
|
||||||
|
editModeContent={
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledEditInplaceInput
|
<StyledEditInplaceInput
|
||||||
autoFocus
|
autoFocus
|
||||||
@ -65,10 +66,11 @@ function EditableFullName({ firstname, lastname, changeHandler }: OwnProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
) : (
|
}
|
||||||
|
nonEditModeContent={
|
||||||
<PersonChip name={firstnameValue + ' ' + lastnameValue} />
|
<PersonChip name={firstnameValue + ' ' + lastnameValue} />
|
||||||
)}
|
}
|
||||||
</EditableCellWrapper>
|
></EditableCellWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,16 +30,12 @@ function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
|
|||||||
const [inputValue, setInputValue] = useState(value);
|
const [inputValue, setInputValue] = useState(value);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
const onEditModeChange = (isEditMode: boolean) => {
|
|
||||||
setIsEditMode(isEditMode);
|
|
||||||
if (isEditMode) {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableCellWrapper onEditModeChange={onEditModeChange}>
|
<EditableCellWrapper
|
||||||
{isEditMode ? (
|
isEditMode={isEditMode}
|
||||||
|
onOutsideClick={() => setIsEditMode(false)}
|
||||||
|
onInsideClick={() => setIsEditMode(true)}
|
||||||
|
editModeContent={
|
||||||
<StyledEditInplaceInput
|
<StyledEditInplaceInput
|
||||||
autoFocus
|
autoFocus
|
||||||
isEditMode={isEditMode}
|
isEditMode={isEditMode}
|
||||||
@ -51,7 +47,8 @@ function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
|
|||||||
changeHandler(event.target.value);
|
changeHandler(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
}
|
||||||
|
nonEditModeContent={
|
||||||
<div>
|
<div>
|
||||||
{isValidPhoneNumber(inputValue) ? (
|
{isValidPhoneNumber(inputValue) ? (
|
||||||
<Link
|
<Link
|
||||||
@ -67,8 +64,8 @@ function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
|
|||||||
<Link href="#">{inputValue}</Link>
|
<Link href="#">{inputValue}</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
</EditableCellWrapper>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
135
front/src/components/table/editable-cell/EditableRelation.tsx
Normal file
135
front/src/components/table/editable-cell/EditableRelation.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { ChangeEvent, ComponentType, useState } from 'react';
|
||||||
|
import EditableCellWrapper from './EditableCellWrapper';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useSearch } from '../../../services/search/search';
|
||||||
|
import { FilterType } from '../table-header/interface';
|
||||||
|
import { People_Bool_Exp } from '../../../generated/graphql';
|
||||||
|
|
||||||
|
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, ChipComponentPropsType> = {
|
||||||
|
relation: RelationType;
|
||||||
|
searchPlaceholder: string;
|
||||||
|
searchFilter: FilterType<People_Bool_Exp>;
|
||||||
|
changeHandler: (relation: RelationType) => void;
|
||||||
|
editModeHorizontalAlign?: 'left' | 'right';
|
||||||
|
ChipComponent: ComponentType<ChipComponentPropsType>;
|
||||||
|
chipComponentPropsMapper: (
|
||||||
|
relation: RelationType,
|
||||||
|
) => ChipComponentPropsType & JSX.IntrinsicAttributes;
|
||||||
|
};
|
||||||
|
|
||||||
|
function EditableRelation<RelationType, ChipComponentPropsType>({
|
||||||
|
relation,
|
||||||
|
searchPlaceholder,
|
||||||
|
searchFilter,
|
||||||
|
changeHandler,
|
||||||
|
editModeHorizontalAlign,
|
||||||
|
ChipComponent,
|
||||||
|
chipComponentPropsMapper,
|
||||||
|
}: EditableRelationProps<RelationType, ChipComponentPropsType>) {
|
||||||
|
const [selectedRelation, setSelectedRelation] = useState(relation);
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
|
const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch();
|
||||||
|
|
||||||
|
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(searchFilter);
|
||||||
|
setSearchInput(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledEditModeSearchContainer>
|
||||||
|
<StyledEditModeResults>
|
||||||
|
{filterSearchResults.results &&
|
||||||
|
filterSearchResults.results.map((result) => (
|
||||||
|
<StyledEditModeResultItem
|
||||||
|
key={result.value.id}
|
||||||
|
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;
|
||||||
@ -6,7 +6,7 @@ type OwnProps = {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
content: string;
|
content: string;
|
||||||
changeHandler: (updated: string) => void;
|
changeHandler: (updated: string) => void;
|
||||||
shouldAlignRight?: boolean;
|
editModeHorizontalAlign?: 'left' | 'right';
|
||||||
};
|
};
|
||||||
|
|
||||||
type StyledEditModeProps = {
|
type StyledEditModeProps = {
|
||||||
@ -33,22 +33,19 @@ function EditableText({
|
|||||||
content,
|
content,
|
||||||
placeholder,
|
placeholder,
|
||||||
changeHandler,
|
changeHandler,
|
||||||
shouldAlignRight,
|
editModeHorizontalAlign,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [inputValue, setInputValue] = useState(content);
|
const [inputValue, setInputValue] = useState(content);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
const onEditModeChange = (isEditMode: boolean) => {
|
|
||||||
setIsEditMode(isEditMode);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableCellWrapper
|
<EditableCellWrapper
|
||||||
onEditModeChange={onEditModeChange}
|
isEditMode={isEditMode}
|
||||||
shouldAlignRight={shouldAlignRight}
|
onOutsideClick={() => setIsEditMode(false)}
|
||||||
>
|
onInsideClick={() => setIsEditMode(true)}
|
||||||
{isEditMode ? (
|
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||||
|
editModeContent={
|
||||||
<StyledInplaceInput
|
<StyledInplaceInput
|
||||||
isEditMode={isEditMode}
|
isEditMode={isEditMode}
|
||||||
placeholder={placeholder || ''}
|
placeholder={placeholder || ''}
|
||||||
@ -60,10 +57,9 @@ function EditableText({
|
|||||||
changeHandler(event.target.value);
|
changeHandler(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
}
|
||||||
<StyledNoEditText>{inputValue}</StyledNoEditText>
|
nonEditModeContent={<StyledNoEditText>{inputValue}</StyledNoEditText>}
|
||||||
)}
|
></EditableCellWrapper>
|
||||||
</EditableCellWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,112 @@
|
|||||||
|
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 {
|
||||||
|
GraphqlQueryCompany,
|
||||||
|
PartialCompany,
|
||||||
|
} from '../../../../interfaces/company.interface';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { SEARCH_COMPANY_QUERY } from '../../../../services/search/search';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { People_Bool_Exp } from '../../../../generated/graphql';
|
||||||
|
import { FilterType } from '../../table-header/interface';
|
||||||
|
import { FaBuilding } from 'react-icons/fa';
|
||||||
|
|
||||||
|
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<PartialCompany, CompanyChipPropsType>
|
||||||
|
> = (args: EditableRelationProps<PartialCompany, CompanyChipPropsType>) => {
|
||||||
|
return (
|
||||||
|
<MockedProvider mocks={mocks}>
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<StyledParent data-testid="content-editable-parent">
|
||||||
|
<EditableRelation<PartialCompany, CompanyChipPropsType> {...args} />
|
||||||
|
</StyledParent>
|
||||||
|
</ThemeProvider>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditableRelationStory = Template.bind({});
|
||||||
|
EditableRelationStory.args = {
|
||||||
|
relation: {
|
||||||
|
id: '123',
|
||||||
|
name: 'Heroku',
|
||||||
|
domain_name: 'heroku.com',
|
||||||
|
} as PartialCompany,
|
||||||
|
ChipComponent: CompanyChip,
|
||||||
|
chipComponentPropsMapper: (company: PartialCompany): CompanyChipPropsType => {
|
||||||
|
return {
|
||||||
|
name: company.name,
|
||||||
|
picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
changeHandler: (relation: PartialCompany) => {
|
||||||
|
console.log('changed', relation);
|
||||||
|
},
|
||||||
|
searchFilter: {
|
||||||
|
key: 'company_name',
|
||||||
|
label: 'Company',
|
||||||
|
icon: <FaBuilding />,
|
||||||
|
whereTemplate: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
searchQuery: SEARCH_COMPANY_QUERY,
|
||||||
|
searchTemplate: (searchInput: string) => ({
|
||||||
|
name: { _ilike: `%${searchInput}%` },
|
||||||
|
}),
|
||||||
|
searchResultMapper: (company: GraphqlQueryCompany) => ({
|
||||||
|
displayValue: company.name,
|
||||||
|
value: {
|
||||||
|
id: company.id,
|
||||||
|
name: company.name,
|
||||||
|
domain_name: company.domain_name,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
operands: [],
|
||||||
|
} satisfies FilterType<People_Bool_Exp>,
|
||||||
|
};
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { EditableRelationStory } from '../__stories__/EditableRelation.stories';
|
||||||
|
import { CompanyChipPropsType } from '../../../chips/CompanyChip';
|
||||||
|
import { PartialCompany } from '../../../../interfaces/company.interface';
|
||||||
|
|
||||||
|
import { EditableRelationProps } from '../EditableRelation';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
it('Checks the EditableRelation editing event bubbles up', async () => {
|
||||||
|
const func = jest.fn(() => null);
|
||||||
|
const { getByTestId, getByText } = render(
|
||||||
|
<EditableRelationStory
|
||||||
|
{...(EditableRelationStory.args as EditableRelationProps<
|
||||||
|
PartialCompany,
|
||||||
|
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({
|
||||||
|
domain_name: 'abnb.com',
|
||||||
|
id: 'abnb',
|
||||||
|
name: 'Airbnb',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { fireEvent, render } from '@testing-library/react';
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
import { RegularSortDropdownButton } from '../__stories__/SortDropdownButton.stories';
|
import { RegularSortDropdownButton } from '../__stories__/SortDropdownButton.stories';
|
||||||
import { FaEnvelope } from 'react-icons/fa';
|
import { FaEnvelope, FaRegBuilding } from 'react-icons/fa';
|
||||||
|
|
||||||
it('Checks the default top option is Ascending', async () => {
|
it('Checks the default top option is Ascending', async () => {
|
||||||
const setSorts = jest.fn();
|
const setSorts = jest.fn();
|
||||||
@ -49,3 +49,26 @@ it('Checks the selection of Descending', async () => {
|
|||||||
_type: 'default_sort',
|
_type: 'default_sort',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Checks custom_sort is working', async () => {
|
||||||
|
const setSorts = jest.fn();
|
||||||
|
const { getByText } = render(
|
||||||
|
<RegularSortDropdownButton setSorts={setSorts} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortDropdownButton = getByText('Sort');
|
||||||
|
fireEvent.click(sortDropdownButton);
|
||||||
|
|
||||||
|
const sortByCompany = getByText('Company');
|
||||||
|
fireEvent.click(sortByCompany);
|
||||||
|
|
||||||
|
expect(setSorts).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'company_name',
|
||||||
|
label: 'Company',
|
||||||
|
icon: <FaRegBuilding />,
|
||||||
|
_type: 'custom_sort',
|
||||||
|
order: 'asc',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@ -6,7 +6,7 @@ export interface Opportunity {
|
|||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Company {
|
export type Company = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
domain_name: string;
|
domain_name: string;
|
||||||
@ -15,7 +15,10 @@ export interface Company {
|
|||||||
opportunities: Opportunity[];
|
opportunities: Opportunity[];
|
||||||
accountOwner?: User;
|
accountOwner?: User;
|
||||||
creationDate: Date;
|
creationDate: Date;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type PartialCompany = Partial<Company> &
|
||||||
|
Pick<Company, 'id' | 'name' | 'domain_name'>;
|
||||||
|
|
||||||
export type GraphqlQueryCompany = {
|
export type GraphqlQueryCompany = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -31,8 +31,12 @@ describe('mapPerson', () => {
|
|||||||
city: '',
|
city: '',
|
||||||
company: {
|
company: {
|
||||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||||
name: '',
|
name: 'Test',
|
||||||
domain_name: '',
|
domain_name: '',
|
||||||
|
opportunities: [],
|
||||||
|
employees: 0,
|
||||||
|
address: '',
|
||||||
|
creationDate: new Date(),
|
||||||
},
|
},
|
||||||
creationDate: new Date(),
|
creationDate: new Date(),
|
||||||
pipe: {
|
pipe: {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Company } from './company.interface';
|
import { PartialCompany } from './company.interface';
|
||||||
import { Pipe } from './pipe.interface';
|
import { Pipe } from './pipe.interface';
|
||||||
|
|
||||||
export type Person = {
|
export type Person = {
|
||||||
@ -7,10 +7,7 @@ export type Person = {
|
|||||||
lastname: string;
|
lastname: string;
|
||||||
picture?: string;
|
picture?: string;
|
||||||
email: string;
|
email: string;
|
||||||
company: Omit<
|
company: PartialCompany;
|
||||||
Company,
|
|
||||||
'employees' | 'address' | 'opportunities' | 'accountOwner' | 'creationDate'
|
|
||||||
>;
|
|
||||||
phone: string;
|
phone: string;
|
||||||
creationDate: Date;
|
creationDate: Date;
|
||||||
pipe: Pipe;
|
pipe: Pipe;
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const StyledPeopleContainer = styled.div`
|
|||||||
function People() {
|
function People() {
|
||||||
const [orderBy, setOrderBy] = useState(defaultOrderBy);
|
const [orderBy, setOrderBy] = useState(defaultOrderBy);
|
||||||
const [where, setWhere] = useState<People_Bool_Exp>({});
|
const [where, setWhere] = useState<People_Bool_Exp>({});
|
||||||
const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch();
|
const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch();
|
||||||
|
|
||||||
const updateSorts = useCallback((sorts: Array<PeopleSelectedSortType>) => {
|
const updateSorts = useCallback((sorts: Array<PeopleSelectedSortType>) => {
|
||||||
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
|
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
|
||||||
@ -61,7 +61,7 @@ function People() {
|
|||||||
onSortsUpdate={updateSorts}
|
onSortsUpdate={updateSorts}
|
||||||
onFiltersUpdate={updateFilters}
|
onFiltersUpdate={updateFilters}
|
||||||
onFilterSearch={(filter, searchValue) => {
|
onFilterSearch={(filter, searchValue) => {
|
||||||
setSearhInput(searchValue);
|
setSearchInput(searchValue);
|
||||||
setFilterSearch(filter);
|
setFilterSearch(filter);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,12 +1,85 @@
|
|||||||
import { render, waitFor } from '@testing-library/react';
|
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
import { PeopleDefault } from '../__stories__/People.stories';
|
import { PeopleDefault } from '../__stories__/People.stories';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
GraphqlMutationPerson,
|
||||||
|
GraphqlQueryPerson,
|
||||||
|
} from '../../../interfaces/person.interface';
|
||||||
|
|
||||||
it('Checks the People page render', async () => {
|
jest.mock('../../../apollo', () => {
|
||||||
const { getByTestId } = render(<PeopleDefault />);
|
const personInterface = jest.requireActual(
|
||||||
|
'../../../interfaces/person.interface',
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
apiClient: {
|
||||||
|
mutate: (arg: {
|
||||||
|
mutation: unknown;
|
||||||
|
variables: GraphqlMutationPerson;
|
||||||
|
}) => {
|
||||||
|
const gqlPerson = arg.variables as unknown as GraphqlQueryPerson;
|
||||||
|
return { data: personInterface.mapPerson(gqlPerson) };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Checks people full name edit is updating data', async () => {
|
||||||
|
const { getByText, getByDisplayValue } = render(<PeopleDefault />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const personChip = getByTestId('row-id-0');
|
expect(getByText('John Doe')).toBeDefined();
|
||||||
expect(personChip).toBeDefined();
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(getByText('John Doe'));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByDisplayValue('John')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
const nameInput = getByDisplayValue('John');
|
||||||
|
|
||||||
|
if (!nameInput) {
|
||||||
|
throw new Error('firstNameInput is null');
|
||||||
|
}
|
||||||
|
fireEvent.change(nameInput, { target: { value: 'Jo' } });
|
||||||
|
fireEvent.click(getByText('All People')); // Click outside
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('Jo Doe')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Checks people email edit is updating data', async () => {
|
||||||
|
const { getByText, getByDisplayValue } = render(<PeopleDefault />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('john@linkedin.com')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(getByText('john@linkedin.com'));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByDisplayValue('john@linkedin.com')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
const emailInput = getByDisplayValue('john@linkedin.com');
|
||||||
|
|
||||||
|
if (!emailInput) {
|
||||||
|
throw new Error('emailInput is null');
|
||||||
|
}
|
||||||
|
fireEvent.change(emailInput, { target: { value: 'john@linkedin.c' } });
|
||||||
|
fireEvent.click(getByText('All People')); // Click outside
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('john@linkedin.c')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,11 +13,12 @@ import { createColumnHelper } from '@tanstack/react-table';
|
|||||||
import ClickableCell from '../../components/table/ClickableCell';
|
import ClickableCell from '../../components/table/ClickableCell';
|
||||||
import ColumnHead from '../../components/table/ColumnHead';
|
import ColumnHead from '../../components/table/ColumnHead';
|
||||||
import Checkbox from '../../components/form/Checkbox';
|
import Checkbox from '../../components/form/Checkbox';
|
||||||
import CompanyChip from '../../components/chips/CompanyChip';
|
import CompanyChip, {
|
||||||
|
CompanyChipPropsType,
|
||||||
|
} from '../../components/chips/CompanyChip';
|
||||||
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface';
|
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface';
|
||||||
import PipeChip from '../../components/chips/PipeChip';
|
import PipeChip from '../../components/chips/PipeChip';
|
||||||
import EditableText from '../../components/table/editable-cell/EditableText';
|
import EditableText from '../../components/table/editable-cell/EditableText';
|
||||||
import { updatePerson } from '../../services/people';
|
|
||||||
import {
|
import {
|
||||||
FilterType,
|
FilterType,
|
||||||
SortType,
|
SortType,
|
||||||
@ -31,10 +32,15 @@ import {
|
|||||||
SEARCH_COMPANY_QUERY,
|
SEARCH_COMPANY_QUERY,
|
||||||
SEARCH_PEOPLE_QUERY,
|
SEARCH_PEOPLE_QUERY,
|
||||||
} from '../../services/search/search';
|
} from '../../services/search/search';
|
||||||
import { GraphqlQueryCompany } from '../../interfaces/company.interface';
|
import {
|
||||||
|
GraphqlQueryCompany,
|
||||||
|
PartialCompany,
|
||||||
|
} from '../../interfaces/company.interface';
|
||||||
import EditablePhone from '../../components/table/editable-cell/EditablePhone';
|
import EditablePhone from '../../components/table/editable-cell/EditablePhone';
|
||||||
import EditableFullName from '../../components/table/editable-cell/EditableFullName';
|
import EditableFullName from '../../components/table/editable-cell/EditableFullName';
|
||||||
import EditableDate from '../../components/table/editable-cell/EditableDate';
|
import EditableDate from '../../components/table/editable-cell/EditableDate';
|
||||||
|
import EditableRelation from '../../components/table/editable-cell/EditableRelation';
|
||||||
|
import { updatePerson } from '../../services/people';
|
||||||
|
|
||||||
export const availableSorts = [
|
export const availableSorts = [
|
||||||
{
|
{
|
||||||
@ -261,7 +267,7 @@ export const peopleColumns = [
|
|||||||
const person = props.row.original;
|
const person = props.row.original;
|
||||||
person.firstname = firstName;
|
person.firstname = firstName;
|
||||||
person.lastname = lastName;
|
person.lastname = lastName;
|
||||||
updatePerson(person).catch((error) => console.error(error)); // TODO: handle error
|
updatePerson(person);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -275,7 +281,7 @@ export const peopleColumns = [
|
|||||||
changeHandler={(value: string) => {
|
changeHandler={(value: string) => {
|
||||||
const person = props.row.original;
|
const person = props.row.original;
|
||||||
person.email = value;
|
person.email = value;
|
||||||
updatePerson(person).catch((error) => console.error(error)); // TODO: handle error
|
updatePerson(person);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -285,12 +291,47 @@ export const peopleColumns = [
|
|||||||
<ColumnHead viewName="Company" viewIcon={<FaRegBuilding />} />
|
<ColumnHead viewName="Company" viewIcon={<FaRegBuilding />} />
|
||||||
),
|
),
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<ClickableCell href="#">
|
<EditableRelation<PartialCompany, CompanyChipPropsType>
|
||||||
<CompanyChip
|
relation={props.row.original.company}
|
||||||
name={props.row.original.company.name}
|
searchPlaceholder="Company"
|
||||||
picture={`https://www.google.com/s2/favicons?domain=${props.row.original.company.domain_name}&sz=256`}
|
ChipComponent={CompanyChip}
|
||||||
/>
|
chipComponentPropsMapper={(
|
||||||
</ClickableCell>
|
company: PartialCompany,
|
||||||
|
): CompanyChipPropsType => {
|
||||||
|
return {
|
||||||
|
name: company.name,
|
||||||
|
picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
changeHandler={(relation: PartialCompany) => {
|
||||||
|
const person = props.row.original;
|
||||||
|
person.company.id = relation.id;
|
||||||
|
updatePerson(person);
|
||||||
|
}}
|
||||||
|
searchFilter={
|
||||||
|
{
|
||||||
|
key: 'company_name',
|
||||||
|
label: 'Company',
|
||||||
|
icon: <FaBuilding />,
|
||||||
|
whereTemplate: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
searchQuery: SEARCH_COMPANY_QUERY,
|
||||||
|
searchTemplate: (searchInput: string) => ({
|
||||||
|
name: { _ilike: `%${searchInput}%` },
|
||||||
|
}),
|
||||||
|
searchResultMapper: (company: GraphqlQueryCompany) => ({
|
||||||
|
displayValue: company.name,
|
||||||
|
value: {
|
||||||
|
id: company.id,
|
||||||
|
name: company.name,
|
||||||
|
domain_name: company.domain_name,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
operands: [],
|
||||||
|
} satisfies FilterType<People_Bool_Exp>
|
||||||
|
}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('phone', {
|
columnHelper.accessor('phone', {
|
||||||
@ -302,7 +343,7 @@ export const peopleColumns = [
|
|||||||
changeHandler={(value: string) => {
|
changeHandler={(value: string) => {
|
||||||
const person = props.row.original;
|
const person = props.row.original;
|
||||||
person.phone = value;
|
person.phone = value;
|
||||||
updatePerson(person).catch((error) => console.error(error)); // TODO: handle error
|
updatePerson(person);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -315,7 +356,7 @@ export const peopleColumns = [
|
|||||||
changeHandler={(value: Date) => {
|
changeHandler={(value: Date) => {
|
||||||
const person = props.row.original;
|
const person = props.row.original;
|
||||||
person.creationDate = value;
|
person.creationDate = value;
|
||||||
updatePerson(person).catch((error) => console.error(error)); // TODO: handle error
|
updatePerson(person);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -332,13 +373,13 @@ export const peopleColumns = [
|
|||||||
header: () => <ColumnHead viewName="City" viewIcon={<FaMapPin />} />,
|
header: () => <ColumnHead viewName="City" viewIcon={<FaMapPin />} />,
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<EditableText
|
<EditableText
|
||||||
shouldAlignRight={true}
|
editModeHorizontalAlign="right"
|
||||||
placeholder="City"
|
placeholder="City"
|
||||||
content={props.row.original.city}
|
content={props.row.original.city}
|
||||||
changeHandler={(value: string) => {
|
changeHandler={(value: string) => {
|
||||||
const person = props.row.original;
|
const person = props.row.original;
|
||||||
person.city = value;
|
person.city = value;
|
||||||
updatePerson(person).catch((error) => console.error(error)); // TODO: handle error
|
updatePerson(person);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -23,7 +23,8 @@ jest.mock('../../../apollo', () => {
|
|||||||
|
|
||||||
it('updates a person', async () => {
|
it('updates a person', async () => {
|
||||||
const result = await updatePerson({
|
const result = await updatePerson({
|
||||||
fullName: 'John Doe',
|
firstname: 'John',
|
||||||
|
lastname: 'Doe',
|
||||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6c',
|
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6c',
|
||||||
email: 'john@example.com',
|
email: 'john@example.com',
|
||||||
company: {
|
company: {
|
||||||
|
|||||||
Reference in New Issue
Block a user