Enable Date edition on People view (#105)
* Enable Date edition on People view * Fix linter
This commit is contained in:
100
front/package-lock.json
generated
100
front/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
86
front/src/components/form/DatePicker.tsx
Normal file
86
front/src/components/form/DatePicker.tsx
Normal 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;
|
||||
36
front/src/components/form/__stories__/Datepicker.stories.tsx
Normal file
36
front/src/components/form/__stories__/Datepicker.stories.tsx
Normal 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');
|
||||
},
|
||||
};
|
||||
22
front/src/components/form/__tests__/Datepicker.test.tsx
Normal file
22
front/src/components/form/__tests__/Datepicker.test.tsx
Normal 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'));
|
||||
});
|
||||
47
front/src/components/table/editable-cell/EditableDate.tsx
Normal file
47
front/src/components/table/editable-cell/EditableDate.tsx
Normal 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;
|
||||
@ -26,7 +26,7 @@ const StyledInplaceInput = styled.input<StyledEditModeProps>`
|
||||
`;
|
||||
|
||||
const StyledNoEditText = styled.div`
|
||||
max-width: 200px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
function EditableText({
|
||||
|
||||
@ -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,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'));
|
||||
});
|
||||
@ -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', {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user