Enable Date edition on People view (#105)

* Enable Date edition on People view

* Fix linter
This commit is contained in:
Charles Bochet
2023-05-05 18:52:04 +02:00
committed by GitHub
parent b8cd842633
commit 406e1dc02e
11 changed files with 372 additions and 8 deletions

100
front/package-lock.json generated
View File

@ -20,6 +20,7 @@
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.26",
"react": "^18.2.0",
"react-datepicker": "^4.11.0",
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-router-dom": "^6.4.4",
@ -43,6 +44,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/react-datepicker": "^4.11.2",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"babel-plugin-named-exports-order": "^0.0.2",
"eslint": "^8.28.0",
@ -5787,6 +5789,15 @@
"node": ">= 8"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@remix-run/router": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.0.tgz",
@ -8761,6 +8772,18 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-datepicker": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.11.2.tgz",
"integrity": "sha512-ELYyX3lb3K1WltqdlF1hbnaDGgzlF6PIR5T4W38cSEcfrQDIrPE+Ioq5pwRe/KEJ+ihHMjvTVZQkwJx0pWMNHQ==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.9.2",
"@types/react": "*",
"date-fns": "^2.0.1",
"react-popper": "^2.2.5"
}
},
"node_modules/@types/react-dom": {
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.1.tgz",
@ -11379,6 +11402,11 @@
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==",
"dev": true
},
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/clean-css": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
@ -12493,6 +12521,21 @@
"integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==",
"dev": true
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@ -24562,6 +24605,23 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/react-datepicker": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.11.0.tgz",
"integrity": "sha512-50n93o7mQwBEhg05tbopjFKgs8qgi8VBCAOMC4VqrKut72eAjESc/wXS/k5hRtnP0oe2FCGw7MJuIwh37wuXOw==",
"dependencies": {
"@popperjs/core": "^2.9.2",
"classnames": "^2.2.6",
"date-fns": "^2.24.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.12.2",
"react-popper": "^2.3.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18",
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@ -24866,6 +24926,11 @@
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
"dev": true
},
"node_modules/react-fast-compare": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz",
"integrity": "sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg=="
},
"node_modules/react-icons": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz",
@ -24888,6 +24953,33 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-onclickoutside": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz",
"integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==",
"funding": {
"type": "individual",
"url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
},
"peerDependencies": {
"react": "^15.5.x || ^16.x || ^17.x || ^18.x",
"react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
}
},
"node_modules/react-popper": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
"dependencies": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
},
"peerDependencies": {
"@popperjs/core": "^2.0.0",
"react": "^16.8.0 || ^17 || ^18",
"react-dom": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-refresh": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
@ -28324,6 +28416,14 @@
"makeerror": "1.0.12"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",

View File

@ -15,6 +15,7 @@
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.26",
"react": "^18.2.0",
"react-datepicker": "^4.11.0",
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-router-dom": "^6.4.4",
@ -97,6 +98,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/react-datepicker": "^4.11.2",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"babel-plugin-named-exports-order": "^0.0.2",
"eslint": "^8.28.0",

View File

@ -0,0 +1,86 @@
import styled from '@emotion/styled';
import { forwardRef, useState } from 'react';
import ReactDatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
export type DatePickerProps = {
isOpen?: boolean;
date: Date;
onChangeHandler: (date: Date) => void;
};
const StyledContainer = styled.div`
& .react-datepicker {
border-color: ${(props) => props.theme.primaryBorder};
font-family: 'Inter';
}
& .react-datepicker__triangle::after {
display: none;
}
& .react-datepicker__triangle::before {
display: none;
}
& .react-datepicker__header {
background-color: ${(props) => props.theme.primaryBackground};
border-bottom-color: ${(props) => props.theme.primaryBorder};
}
& .react-datepicker__day--selected {
background-color: ${(props) => props.theme.blue};
}
`;
function DatePicker({ date, onChangeHandler, isOpen }: DatePickerProps) {
const [startDate, setStartDate] = useState(date);
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>
),
);
return (
<StyledContainer>
<ReactDatePicker
open={isOpen}
selected={startDate}
onChange={(date: Date) => {
setStartDate(date);
onChangeHandler(date);
}}
popperPlacement="bottom"
popperModifiers={[
{
name: 'offset',
options: {
offset: [55, 0],
},
},
{
name: 'preventOverflow',
options: {
rootBoundary: 'viewport',
tether: false,
altAxis: true,
},
},
]}
customInput={<DateDisplay />}
/>
</StyledContainer>
);
}
export default DatePicker;

View File

@ -0,0 +1,36 @@
import DatePicker, { DatePickerProps } from '../DatePicker';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
import styled from '@emotion/styled';
const component = {
title: 'DatePicker',
component: DatePicker,
};
export default component;
const StyledContainer = styled.div`
height: 300px;
width: 200px;
}`;
const Template: StoryFn<typeof DatePicker> = (args: DatePickerProps) => {
return (
<ThemeProvider theme={lightTheme}>
<StyledContainer>
<DatePicker {...args} />
</StyledContainer>
</ThemeProvider>
);
};
export const DatePickerStory = Template.bind({});
DatePickerStory.args = {
isOpen: true,
date: new Date(),
onChangeHandler: () => {
console.log('changed');
},
};

View File

@ -0,0 +1,22 @@
import { fireEvent, render } from '@testing-library/react';
import { DatePickerStory } from '../__stories__/Datepicker.stories';
import { act } from 'react-dom/test-utils';
it('Checks the datepicker renders', () => {
const changeHandler = jest.fn();
const { getByText } = render(
<DatePickerStory
date={new Date('2021-03-03')}
onChangeHandler={changeHandler}
/>,
);
act(() => {
fireEvent.click(getByText('Mar 3, 2021'));
});
expect(getByText('March 2021')).toBeInTheDocument();
act(() => {
fireEvent.click(getByText('5'));
});
expect(changeHandler).toHaveBeenCalledWith(new Date('2021-03-05'));
});

View File

@ -0,0 +1,47 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper';
import DatePicker from '../../form/DatePicker';
export type EditableDateProps = {
value: Date;
changeHandler: (date: Date) => void;
shouldAlignRight?: boolean;
};
const StyledContainer = styled.div`
display: flex;
align-items: center;
`;
function EditableDate({
value,
changeHandler,
shouldAlignRight,
}: EditableDateProps) {
const [inputValue, setInputValue] = useState(value);
const [isEditMode, setIsEditMode] = useState(false);
const onEditModeChange = (isEditMode: boolean) => {
setIsEditMode(isEditMode);
};
return (
<EditableCellWrapper
onEditModeChange={onEditModeChange}
shouldAlignRight={shouldAlignRight}
>
<StyledContainer>
<DatePicker
isOpen={isEditMode}
date={inputValue}
onChangeHandler={(date: Date) => {
changeHandler(date);
setInputValue(date);
}}
/>
</StyledContainer>
</EditableCellWrapper>
);
}
export default EditableDate;

View File

@ -26,7 +26,7 @@ const StyledInplaceInput = styled.input<StyledEditModeProps>`
`;
const StyledNoEditText = styled.div`
max-width: 200px;
width: 100%;
`;
function EditableText({

View File

@ -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');
},
};

View File

@ -0,0 +1,37 @@
import { fireEvent, render } 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');
}
expect(getByText('March 2021')).toBeInTheDocument();
act(() => {
fireEvent.click(getByText('5'));
});
expect(changeHandler).toHaveBeenCalledWith(new Date('2021-03-05'));
});

View File

@ -34,6 +34,7 @@ import {
import { GraphqlQueryCompany } from '../../interfaces/company.interface';
import EditablePhone from '../../components/table/editable-cell/EditablePhone';
import EditableFullName from '../../components/table/editable-cell/EditableFullName';
import EditableDate from '../../components/table/editable-cell/EditableDate';
export const availableSorts = [
{
@ -309,13 +310,14 @@ export const peopleColumns = [
columnHelper.accessor('creationDate', {
header: () => <ColumnHead viewName="Creation" viewIcon={<FaCalendar />} />,
cell: (props) => (
<ClickableCell href="#">
{new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(props.row.original.creationDate)}
</ClickableCell>
<EditableDate
value={props.row.original.creationDate}
changeHandler={(value: Date) => {
const person = props.row.original;
person.creationDate = value;
updatePerson(person).catch((error) => console.error(error)); // TODO: handle error
}}
/>
),
}),
columnHelper.accessor('pipe', {

View File

@ -11,6 +11,7 @@ export const UPDATE_PERSON = gql`
$city: String
$company_id: uuid
$email: String
$created_at: timestamptz
) {
update_people(
where: { id: { _eq: $id } }
@ -22,6 +23,7 @@ export const UPDATE_PERSON = gql`
id: $id
lastname: $lastname
phone: $phone
created_at: $created_at
}
) {
returning {
@ -36,6 +38,7 @@ export const UPDATE_PERSON = gql`
id
lastname
phone
created_at
}
}
}