Skip to content

Commit

Permalink
🪟 🎉 Add datepicker for date/date-time fields in connector form (#19678)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim Roes <tim@airbyte.io>
Co-authored-by: Lake Mossman <lake@airbyte.io>
  • Loading branch information
3 people authored Dec 7, 2022
1 parent 9cb5714 commit 6e155d3
Show file tree
Hide file tree
Showing 15 changed files with 5,389 additions and 15,010 deletions.
1 change: 1 addition & 0 deletions airbyte-webapp/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { withProviders } from "./withProvider";

import "!style-loader!css-loader!sass-loader!../public/index.css";
import "../src/scss/global.scss";
import "../src/globals";

addDecorator(withProviders);

Expand Down
19,860 changes: 4,854 additions & 15,006 deletions airbyte-webapp/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions airbyte-webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"mdast": "^3.0.0",
"query-string": "^6.13.1",
"react": "^17.0.2",
"react-datepicker": "^4.8.0",
"react-dom": "^17.0.2",
"react-helmet-async": "^1.3.0",
"react-intl": "^6.1.1",
Expand Down Expand Up @@ -101,6 +102,7 @@
"@types/node": "^17.0.40",
"@types/query-string": "^6.3.0",
"@types/react": "^17.0.39",
"@types/react-datepicker": "^4.8.0",
"@types/react-dom": "^17.0.11",
"@types/react-helmet": "^6.1.5",
"@types/react-lazylog": "^4.5.1",
Expand Down Expand Up @@ -138,6 +140,7 @@
"stylelint-config-standard": "^26.0.0",
"stylelint-config-standard-scss": "^5.0.0",
"tar": "^6.1.11",
"timezone-mock": "^1.3.4",
"tmpl": "^1.0.5",
"ts-node": "^10.8.1",
"typescript": "^4.7.3"
Expand Down
146 changes: 146 additions & 0 deletions airbyte-webapp/src/components/ui/DatePicker/DatePicker.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/* stylelint-disable selector-class-pattern, no-descending-specificity */

@use "scss/colors";
@use "scss/fonts";
@use "scss/variables";
@use "scss/z-indices";

.wrapper {
position: relative;
}

.datepickerButtonContainer {
position: absolute;
right: 0;
top: 0;
display: flex;
height: 100%;
align-items: center;
justify-content: center;
border: none;
}

.datepickerButton svg {
font-size: 14px;
}

.popup {
z-index: z-indices.$datepicker;
}

.input {
padding-right: 25px;
}

:global(.react-datepicker) {
display: flex;
color: colors.$dark-blue-900;
font-family: fonts.$primary;
border: variables.$border-thick solid colors.$grey-100;
border-radius: variables.$border-radius-md;
box-shadow: 2px 2px 20px -6px colors.$grey-200;
}

/** Main calendar area **/
:global(.react-datepicker__header) {
background-color: colors.$grey-50;
border-top-right-radius: variables.$border-radius-md;
border-top-left-radius: variables.$border-radius-md;
border-bottom: none;
}

:global(.react-datepicker__header.react-datepicker__header--has-time-select) {
border-top-right-radius: 0;
}

:global(.react-datepicker__header:not(.react-datepicker__header--has-time-select)) {
border-top-right-radius: variables.$border-radius-md;
}

:global(.react-datepicker__day-name) {
color: colors.$grey-300;
}

:global(.react-datepicker__current-month) {
color: colors.$dark-blue-900;
font-weight: 500;
margin-bottom: variables.$spacing-md;
}

:global(.react-datepicker__day) {
color: colors.$dark-blue-900;
background-color: transparent;
border-radius: variables.$border-radius-xs;

&:hover {
background-color: colors.$grey-100;
}
}

:global(.react-datepicker__day--outside-month) {
color: colors.$grey-300;
}

:global(.react-datepicker__day--today) {
background-color: colors.$grey-50;
font-weight: 700;
}

:global(.react-datepicker__day--selected) {
color: colors.$white;
background-color: colors.$blue-400;
font-weight: 700;

&:hover {
background-color: colors.$blue-500;
}
}

:global(.react-datepicker__navigation-icon::before) {
border-width: 2px 2px 0 0;
}

/** Time **/
:global(.react-datepicker__time-container) {
border-left-color: colors.$grey-100;
border-left-width: variables.$border-thick;
border-bottom-right-radius: variables.$border-radius-md;
overflow: hidden;
}

:global(.react-datepicker-time__header) {
font-weight: 500;
color: colors.$dark-blue-900;
}

:global(.react-datepicker__time-list-item) {
background-color: transparent;
display: flex;
justify-content: center;
align-items: center;

&:hover {
background-color: colors.$grey-100;
}
}

:global(.react-datepicker__time-container
.react-datepicker__time
.react-datepicker__time-box
ul.react-datepicker__time-list
li.react-datepicker__time-list-item) {
justify-content: center;
}

:global(.react-datepicker__time-container
.react-datepicker__time
.react-datepicker__time-box
ul.react-datepicker__time-list
li.react-datepicker__time-list-item--selected) {
color: colors.$white;
background-color: colors.$blue-400;

&:hover {
background-color: colors.$blue-500;
}
}
144 changes: 144 additions & 0 deletions airbyte-webapp/src/components/ui/DatePicker/DatePicker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import dayjs from "dayjs";
import { TestWrapper } from "test-utils/testutils";
import timezoneMock from "timezone-mock";

import { DatePicker, toEquivalentLocalTime } from "./DatePicker";

describe(`${toEquivalentLocalTime.name}`, () => {
// Seems silly, but dayjs has a bug when formatting years, so this is a useful test:
// https://github.com/iamkun/dayjs/issues/1745
it("handles a date in the year 1", () => {
const TEST_UTC_TIMESTAMP = "0001-12-01T09:00:00Z";

const result = toEquivalentLocalTime(TEST_UTC_TIMESTAMP);

expect(result).toEqual(undefined);
});

it("handles an invalid date", () => {
const TEST_UTC_TIMESTAMP = "not a date";

const result = toEquivalentLocalTime(TEST_UTC_TIMESTAMP);

expect(result).toEqual(undefined);
});

it("outputs the same YYYY-MM-DDTHH:mm:ss", () => {
const TEST_UTC_TIMESTAMP = "2000-01-01T12:00:00Z";

const result = toEquivalentLocalTime(TEST_UTC_TIMESTAMP);

// Regardless of the timezone, the local time should be the same
expect(result?.toISOString().substring(0, 19)).toEqual(TEST_UTC_TIMESTAMP.substring(0, 19));
});

it("converts utc time to equivalent local time in PST", () => {
timezoneMock.register("US/Pacific");
const TEST_TIMEZONE_UTC_OFFSET_IN_MINUTES = 480; // corresponds to the registered mock timezone
const TEST_UTC_TIMESTAMP = "2022-01-01T00:00:00Z";

const expectedDateObject = dayjs
.utc(TEST_UTC_TIMESTAMP)
.add(TEST_TIMEZONE_UTC_OFFSET_IN_MINUTES, "minutes")
.toDate();

expect(toEquivalentLocalTime(TEST_UTC_TIMESTAMP)).toEqual(expectedDateObject);
});

it("converts utc time to equivalent local time in EST", () => {
timezoneMock.register("US/Eastern");
const TEST_TIMEZONE_UTC_OFFSET_IN_MINUTES = 300; // corresponds to the registered mock timezone
const TEST_UTC_TIMESTAMP = "2022-01-01T00:00:00Z";

const expectedDateObject = dayjs
.utc(TEST_UTC_TIMESTAMP)
.add(TEST_TIMEZONE_UTC_OFFSET_IN_MINUTES, "minutes")
.toDate();

expect(toEquivalentLocalTime(TEST_UTC_TIMESTAMP)).toEqual(expectedDateObject);
});

it("keeps a utc timestamp exactly the same", () => {
timezoneMock.register("UTC");
const TEST_UTC_TIMESTAMP = "2022-01-01T00:00:00Z";

const expectedDateObject = dayjs.utc(TEST_UTC_TIMESTAMP).toDate();

expect(toEquivalentLocalTime(TEST_UTC_TIMESTAMP)).toEqual(expectedDateObject);
});

afterEach(() => {
// Return global Date() object to system behavior
timezoneMock.unregister();
});
});

describe(`${DatePicker.name}`, () => {
it("allows typing a date manually", async () => {
const MOCK_DESIRED_DATETIME = "2010-09-12T00:00:00Z";
let mockValue = "";
render(
<TestWrapper>
<DatePicker
onChange={(value) => {
// necessary for controlled inputs https://github.com/testing-library/user-event/issues/387#issuecomment-819868799
mockValue = mockValue + value;
}}
value={mockValue}
/>
</TestWrapper>
);

const input = screen.getByTestId("input");
await userEvent.type(input, MOCK_DESIRED_DATETIME, { delay: 1 });

expect(mockValue).toEqual(MOCK_DESIRED_DATETIME);
});

it("allows selecting a date from the datepicker", async () => {
jest.useFakeTimers().setSystemTime(new Date("2010-09-05"));
const MOCK_DESIRED_DATETIME = "2010-09-12";
let mockValue = "";
render(
<TestWrapper>
<DatePicker
onChange={(value) => {
// necessary for controlled inputs https://github.com/testing-library/user-event/issues/387#issuecomment-819868799
mockValue = mockValue + value;
}}
value={mockValue}
/>
</TestWrapper>
);

const datepicker = screen.getByLabelText("Open datepicker");
userEvent.click(datepicker);
const date = screen.getByLabelText("Choose Sunday, September 12th, 2010");
userEvent.click(date);

expect(mockValue).toEqual(MOCK_DESIRED_DATETIME);
jest.useRealTimers();
});

it("focuses the input after selecting a date from the datepicker", async () => {
jest.useFakeTimers().setSystemTime(new Date("2010-09-05"));
let mockValue = "";
render(
<TestWrapper>
<DatePicker onChange={(value) => (mockValue = value)} value={mockValue} />
</TestWrapper>
);

const datepicker = screen.getByLabelText("Open datepicker");
userEvent.click(datepicker);
const date = screen.getByLabelText("Choose Sunday, September 12th, 2010");
userEvent.click(date);

const input = screen.getByTestId("input");

expect(input).toHaveFocus();
jest.useRealTimers();
});
});
Loading

0 comments on commit 6e155d3

Please sign in to comment.