From 406e1dc02ec66c1ed5f1756e108c056e9d1802f3 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 5 May 2023 18:52:04 +0200 Subject: [PATCH] Enable Date edition on People view (#105) * Enable Date edition on People view * Fix linter --- front/package-lock.json | 100 ++++++++++++++++++ front/package.json | 2 + front/src/components/form/DatePicker.tsx | 86 +++++++++++++++ .../form/__stories__/Datepicker.stories.tsx | 36 +++++++ .../form/__tests__/Datepicker.test.tsx | 22 ++++ .../table/editable-cell/EditableDate.tsx | 47 ++++++++ .../table/editable-cell/EditableText.tsx | 2 +- .../__stories__/EditableDate.stories.tsx | 29 +++++ .../__tests__/EditableDate.test.tsx | 37 +++++++ front/src/pages/people/people-table.tsx | 16 +-- front/src/services/people/update.ts | 3 + 11 files changed, 372 insertions(+), 8 deletions(-) create mode 100644 front/src/components/form/DatePicker.tsx create mode 100644 front/src/components/form/__stories__/Datepicker.stories.tsx create mode 100644 front/src/components/form/__tests__/Datepicker.test.tsx create mode 100644 front/src/components/table/editable-cell/EditableDate.tsx create mode 100644 front/src/components/table/editable-cell/__stories__/EditableDate.stories.tsx create mode 100644 front/src/components/table/editable-cell/__tests__/EditableDate.test.tsx diff --git a/front/package-lock.json b/front/package-lock.json index 0599603dd..439effa4a 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -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", diff --git a/front/package.json b/front/package.json index 393c2c771..3af479f03 100644 --- a/front/package.json +++ b/front/package.json @@ -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", diff --git a/front/src/components/form/DatePicker.tsx b/front/src/components/form/DatePicker.tsx new file mode 100644 index 000000000..03bd607d0 --- /dev/null +++ b/front/src/components/form/DatePicker.tsx @@ -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; + const DateDisplay = forwardRef( + ({ value, onClick }, ref) => ( +
+ {value && + new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }).format(new Date(value as string))} +
+ ), + ); + + return ( + + { + setStartDate(date); + onChangeHandler(date); + }} + popperPlacement="bottom" + popperModifiers={[ + { + name: 'offset', + options: { + offset: [55, 0], + }, + }, + { + name: 'preventOverflow', + options: { + rootBoundary: 'viewport', + tether: false, + altAxis: true, + }, + }, + ]} + customInput={} + /> + + ); +} + +export default DatePicker; diff --git a/front/src/components/form/__stories__/Datepicker.stories.tsx b/front/src/components/form/__stories__/Datepicker.stories.tsx new file mode 100644 index 000000000..a1aade391 --- /dev/null +++ b/front/src/components/form/__stories__/Datepicker.stories.tsx @@ -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 = (args: DatePickerProps) => { + return ( + + + + + + ); +}; + +export const DatePickerStory = Template.bind({}); +DatePickerStory.args = { + isOpen: true, + date: new Date(), + onChangeHandler: () => { + console.log('changed'); + }, +}; diff --git a/front/src/components/form/__tests__/Datepicker.test.tsx b/front/src/components/form/__tests__/Datepicker.test.tsx new file mode 100644 index 000000000..323cd9945 --- /dev/null +++ b/front/src/components/form/__tests__/Datepicker.test.tsx @@ -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( + , + ); + 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')); +}); diff --git a/front/src/components/table/editable-cell/EditableDate.tsx b/front/src/components/table/editable-cell/EditableDate.tsx new file mode 100644 index 000000000..4653c1c85 --- /dev/null +++ b/front/src/components/table/editable-cell/EditableDate.tsx @@ -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 ( + + + { + changeHandler(date); + setInputValue(date); + }} + /> + + + ); +} + +export default EditableDate; diff --git a/front/src/components/table/editable-cell/EditableText.tsx b/front/src/components/table/editable-cell/EditableText.tsx index f689ae1dc..3a52221ce 100644 --- a/front/src/components/table/editable-cell/EditableText.tsx +++ b/front/src/components/table/editable-cell/EditableText.tsx @@ -26,7 +26,7 @@ const StyledInplaceInput = styled.input` `; const StyledNoEditText = styled.div` - max-width: 200px; + width: 100%; `; function EditableText({ diff --git a/front/src/components/table/editable-cell/__stories__/EditableDate.stories.tsx b/front/src/components/table/editable-cell/__stories__/EditableDate.stories.tsx new file mode 100644 index 000000000..ae5dc6da5 --- /dev/null +++ b/front/src/components/table/editable-cell/__stories__/EditableDate.stories.tsx @@ -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 = (args: EditableDateProps) => { + return ( + +
+ +
+
+ ); +}; + +export const EditableDateStory = Template.bind({}); +EditableDateStory.args = { + value: new Date(), + changeHandler: () => { + console.log('changed'); + }, +}; diff --git a/front/src/components/table/editable-cell/__tests__/EditableDate.test.tsx b/front/src/components/table/editable-cell/__tests__/EditableDate.test.tsx new file mode 100644 index 000000000..d37f13686 --- /dev/null +++ b/front/src/components/table/editable-cell/__tests__/EditableDate.test.tsx @@ -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( + , + ); + + 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')); +}); diff --git a/front/src/pages/people/people-table.tsx b/front/src/pages/people/people-table.tsx index 93b1cd941..db1509bd5 100644 --- a/front/src/pages/people/people-table.tsx +++ b/front/src/pages/people/people-table.tsx @@ -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: () => } />, cell: (props) => ( - - {new Intl.DateTimeFormat(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric', - }).format(props.row.original.creationDate)} - + { + const person = props.row.original; + person.creationDate = value; + updatePerson(person).catch((error) => console.error(error)); // TODO: handle error + }} + /> ), }), columnHelper.accessor('pipe', { diff --git a/front/src/services/people/update.ts b/front/src/services/people/update.ts index 3154920ae..f88755b13 100644 --- a/front/src/services/people/update.ts +++ b/front/src/services/people/update.ts @@ -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 } } }