mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console: add support for specific data types (datetime / time) for gdc
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8393 GitOrigin-RevId: c1eeb1fb6a5cb825853ec29d7ec1f8b0b702ad07
This commit is contained in:
parent
0a1628c0cc
commit
21f9918677
@ -133,7 +133,7 @@ export const ManageTable: React.VFC<ManageTableProps> = (
|
|||||||
if (isLoading) return <IndicatorCard status="info">Loading...</IndicatorCard>;
|
if (isLoading) return <IndicatorCard status="info">Loading...</IndicatorCard>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full overflow-y-auto bg-gray-50">
|
<div className="w-full bg-gray-50">
|
||||||
<div className="px-md pt-md mb-xs">
|
<div className="px-md pt-md mb-xs">
|
||||||
<Breadcrumbs dataSourceName={dataSourceName} tableName={tableName} />
|
<Breadcrumbs dataSourceName={dataSourceName} tableName={tableName} />
|
||||||
<TableName
|
<TableName
|
||||||
|
@ -5,9 +5,9 @@ import { IntrospectedTable, TableColumn } from '../../types';
|
|||||||
export const adaptIntrospectedBigQueryTables = (
|
export const adaptIntrospectedBigQueryTables = (
|
||||||
runSqlResponse: RunSQLResponse
|
runSqlResponse: RunSQLResponse
|
||||||
): IntrospectedTable[] => {
|
): IntrospectedTable[] => {
|
||||||
/*
|
/*
|
||||||
The `slice(1)` on the result is done because the first item of the result is always the columns names from the SQL output.
|
The `slice(1)` on the result is done because the first item of the result is always the columns names from the SQL output.
|
||||||
It is not required for the final result and should be avoided
|
It is not required for the final result and should be avoided
|
||||||
*/
|
*/
|
||||||
const adaptedResponse = runSqlResponse.result
|
const adaptedResponse = runSqlResponse.result
|
||||||
?.slice(1)
|
?.slice(1)
|
||||||
@ -41,6 +41,7 @@ export function adaptSQLDataType(
|
|||||||
],
|
],
|
||||||
number: ['BIGNUMERIC', 'FLOAT64', 'INT64', 'INTERVAL', 'NUMERIC'],
|
number: ['BIGNUMERIC', 'FLOAT64', 'INT64', 'INTERVAL', 'NUMERIC'],
|
||||||
json: ['JSON', 'xml'],
|
json: ['JSON', 'xml'],
|
||||||
|
float: ['FLOAT64'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const [dataType] = getEntries(DataTypeToSQLTypeMap).find(([, value]) =>
|
const [dataType] = getEntries(DataTypeToSQLTypeMap).find(([, value]) =>
|
||||||
|
@ -66,6 +66,7 @@ export function adaptSQLDataType(
|
|||||||
'serial4',
|
'serial4',
|
||||||
],
|
],
|
||||||
json: ['interval', 'json', 'jsonb', 'xml'],
|
json: ['interval', 'json', 'jsonb', 'xml'],
|
||||||
|
float: ['float8', 'float4'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const [dataType] = getEntries(consoleDataTypeToSQLTypeMap).find(([, value]) =>
|
const [dataType] = getEntries(consoleDataTypeToSQLTypeMap).find(([, value]) =>
|
||||||
|
@ -22,6 +22,7 @@ export function adaptSQLDataType(
|
|||||||
],
|
],
|
||||||
text: ['text'],
|
text: ['text'],
|
||||||
json: [],
|
json: [],
|
||||||
|
float: ['float'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const [dataType] = getEntries(DataTypeToSQLTypeMap).find(([, value]) =>
|
const [dataType] = getEntries(DataTypeToSQLTypeMap).find(([, value]) =>
|
||||||
|
@ -74,6 +74,7 @@ export function adaptSQLDataType(
|
|||||||
'serial4',
|
'serial4',
|
||||||
],
|
],
|
||||||
json: ['interval', 'json', 'jsonb', 'xml'],
|
json: ['interval', 'json', 'jsonb', 'xml'],
|
||||||
|
float: ['float8'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const [dataType] = getEntries(consoleDataTypeToSQLTypeMap).find(([, value]) =>
|
const [dataType] = getEntries(consoleDataTypeToSQLTypeMap).find(([, value]) =>
|
||||||
|
@ -66,7 +66,7 @@ export type TableColumn = {
|
|||||||
/**
|
/**
|
||||||
* console data type: the dataType property is group into one of these types and console uses this internally
|
* console data type: the dataType property is group into one of these types and console uses this internally
|
||||||
*/
|
*/
|
||||||
consoleDataType: 'string' | 'text' | 'json' | 'number' | 'boolean';
|
consoleDataType: 'string' | 'text' | 'json' | 'number' | 'boolean' | 'float';
|
||||||
nullable?: boolean;
|
nullable?: boolean;
|
||||||
isPrimaryKey?: boolean;
|
isPrimaryKey?: boolean;
|
||||||
graphQLProperties?: {
|
graphQLProperties?: {
|
||||||
|
@ -27,6 +27,9 @@ const columns: InsertRowFormProps['columns'] = [
|
|||||||
},
|
},
|
||||||
isPrimaryKey: true,
|
isPrimaryKey: true,
|
||||||
placeholder: 'bigint',
|
placeholder: 'bigint',
|
||||||
|
insertable: true,
|
||||||
|
description: '',
|
||||||
|
nullable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@ -36,6 +39,9 @@ const columns: InsertRowFormProps['columns'] = [
|
|||||||
comment: '',
|
comment: '',
|
||||||
},
|
},
|
||||||
placeholder: 'text',
|
placeholder: 'text',
|
||||||
|
insertable: true,
|
||||||
|
description: '',
|
||||||
|
nullable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'json',
|
name: 'json',
|
||||||
@ -46,6 +52,8 @@ const columns: InsertRowFormProps['columns'] = [
|
|||||||
},
|
},
|
||||||
nullable: true,
|
nullable: true,
|
||||||
placeholder: '{"name":"john"}',
|
placeholder: '{"name":"john"}',
|
||||||
|
insertable: true,
|
||||||
|
description: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'date',
|
||||||
@ -55,7 +63,33 @@ const columns: InsertRowFormProps['columns'] = [
|
|||||||
comment: '',
|
comment: '',
|
||||||
},
|
},
|
||||||
nullable: true,
|
nullable: true,
|
||||||
placeholder: '2023-02-01',
|
insertable: true,
|
||||||
|
description: '',
|
||||||
|
placeholder: '2023-01-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'datetime',
|
||||||
|
dataType: 'datetime',
|
||||||
|
consoleDataType: 'text',
|
||||||
|
config: {
|
||||||
|
comment: '',
|
||||||
|
},
|
||||||
|
nullable: true,
|
||||||
|
insertable: true,
|
||||||
|
description: '',
|
||||||
|
placeholder: '2020-01-14T16:30:00+01:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
dataType: 'time with time zone',
|
||||||
|
consoleDataType: 'text',
|
||||||
|
config: {
|
||||||
|
comment: '',
|
||||||
|
},
|
||||||
|
nullable: true,
|
||||||
|
insertable: true,
|
||||||
|
description: '',
|
||||||
|
placeholder: '11:00:00+01:00',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ describe('convertTableValue', () => {
|
|||||||
${'false'} | ${'boolean'} | ${false}
|
${'false'} | ${'boolean'} | ${false}
|
||||||
${'true'} | ${'bool'} | ${true}
|
${'true'} | ${'bool'} | ${true}
|
||||||
${'false'} | ${'bool'} | ${false}
|
${'false'} | ${'bool'} | ${false}
|
||||||
|
${'123'} | ${'float'} | ${123}
|
||||||
`(
|
`(
|
||||||
'given $dataType it returns $expected',
|
'given $dataType it returns $expected',
|
||||||
({ columnValue, dataType, expected }) => {
|
({ columnValue, dataType, expected }) => {
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { TableColumn } from '../DataSource';
|
import { TableColumn } from '../DataSource';
|
||||||
|
|
||||||
|
const numericDataTypes = ['number', 'float', 'integer'];
|
||||||
|
|
||||||
export const convertTableValue = (
|
export const convertTableValue = (
|
||||||
value: unknown,
|
value: unknown,
|
||||||
dataType: TableColumn['dataType'] | undefined
|
dataType: TableColumn['dataType'] | undefined
|
||||||
): string | number | boolean | unknown => {
|
): string | number | boolean | unknown => {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
if (dataType === 'number') {
|
if (dataType && numericDataTypes.includes(dataType)) {
|
||||||
return parseFloat(value);
|
return parseFloat(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { dataSource } from '../../../dataSources';
|
import { dataSource } from '../../../dataSources';
|
||||||
import { BooleanInput } from './BooleanInput';
|
import { BooleanInput } from './BooleanInput';
|
||||||
import { DateInput } from './DateInput';
|
import { DateInput } from './DateInput';
|
||||||
|
import { DateTimeInput } from './DateTimeInput';
|
||||||
import {
|
import {
|
||||||
ExpandableTextInput,
|
ExpandableTextInput,
|
||||||
ExpandableTextInputProps,
|
ExpandableTextInputProps,
|
||||||
} from './ExpandableTextInput';
|
} from './ExpandableTextInput';
|
||||||
import { JsonInput } from './JsonInput';
|
import { JsonInput } from './JsonInput';
|
||||||
import { TextInput } from './TextInput';
|
import { TextInput } from './TextInput';
|
||||||
|
import { TimeInput } from './TimeInput';
|
||||||
|
|
||||||
type ColumnRowInputProps = ExpandableTextInputProps & {
|
type ColumnRowInputProps = ExpandableTextInputProps & {
|
||||||
dataType: string;
|
dataType: string;
|
||||||
@ -28,6 +30,14 @@ export const ColumnRowInput: React.VFC<ColumnRowInputProps> = ({
|
|||||||
return <DateInput {...props} />;
|
return <DateInput {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dataType === dataSource.columnDataTypes.DATETIME) {
|
||||||
|
return <DateTimeInput {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataType === dataSource.columnDataTypes.TIME) {
|
||||||
|
return <TimeInput {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
dataType === dataSource.columnDataTypes.JSONDTYPE ||
|
dataType === dataSource.columnDataTypes.JSONDTYPE ||
|
||||||
dataType === dataSource.columnDataTypes.JSONB
|
dataType === dataSource.columnDataTypes.JSONB
|
||||||
|
@ -3,12 +3,12 @@ import { Input } from '../../../new-components/Form/Input';
|
|||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import { FaCalendar } from 'react-icons/fa';
|
import { FaCalendar } from 'react-icons/fa';
|
||||||
import DatePicker, { CalendarContainer } from 'react-datepicker';
|
import DatePicker, { CalendarContainer } from 'react-datepicker';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { CustomEventHandler, TextInputProps } from './TextInput';
|
import { CustomEventHandler, TextInputProps } from './TextInput';
|
||||||
|
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import './date-input.css';
|
import './date-input.css';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
export const DateInput: React.VFC<TextInputProps> = ({
|
export const DateInput: React.VFC<TextInputProps> = ({
|
||||||
name,
|
name,
|
||||||
@ -88,6 +88,7 @@ export const DateInput: React.VFC<TextInputProps> = ({
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
inline
|
inline
|
||||||
onSelect={() => setCalendarPickerVisible(false)}
|
onSelect={() => setCalendarPickerVisible(false)}
|
||||||
|
onClickOutside={() => setCalendarPickerVisible(false)}
|
||||||
onChange={onDateChange}
|
onChange={onDateChange}
|
||||||
calendarContainer={CustomPickerContainer}
|
calendarContainer={CustomPickerContainer}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||||
|
import { handlers } from '../../../mocks/metadata.mock';
|
||||||
|
import { userEvent, within } from '@storybook/testing-library';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
import { DateTimeInput } from './DateTimeInput';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Data/Insert Row/components/DateTimeInput',
|
||||||
|
component: DateTimeInput,
|
||||||
|
parameters: {
|
||||||
|
msw: handlers(),
|
||||||
|
mockdate: new Date('2020-01-14T15:47:18.502Z'),
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onChange: { action: true },
|
||||||
|
onInput: { action: true },
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof DateTimeInput>;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof DateTimeInput> = args => {
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
return <DateTimeInput {...args} inputRef={inputRef} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Base = Template.bind({});
|
||||||
|
Base.args = {
|
||||||
|
name: 'date',
|
||||||
|
placeholder: 'date...',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled = Template.bind({});
|
||||||
|
Disabled.args = {
|
||||||
|
...Base.args,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
Base.play = async ({ args, canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
userEvent.type(
|
||||||
|
await canvas.findByPlaceholderText('date...'),
|
||||||
|
'2020-01-14T12:00:00.000Z'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(args.onChange).toHaveBeenCalled();
|
||||||
|
expect(args.onInput).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const baseDate = new Date();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await canvas.findByDisplayValue('2020-01-14T12:00:00.000Z')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
userEvent.click(await canvas.findByRole('button'));
|
||||||
|
|
||||||
|
const baseDateLabel = `Choose ${format(baseDate, 'EEEE, LLLL do, u')}`;
|
||||||
|
|
||||||
|
userEvent.click((await canvas.findAllByLabelText(baseDateLabel))[0]);
|
||||||
|
userEvent.click(await canvas.findByText('4:30 PM'));
|
||||||
|
|
||||||
|
expect(await canvas.findByLabelText('date')).toHaveDisplayValue(
|
||||||
|
'2020-01-14T16:30:00.502Z'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(args.onChange).toHaveBeenCalled();
|
||||||
|
expect(args.onInput).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// click outside the calendar picker
|
||||||
|
userEvent.click(await canvas.findByLabelText('date'));
|
||||||
|
|
||||||
|
expect(await canvas.queryByText('January 2020')).not.toBeInTheDocument();
|
||||||
|
expect(await canvas.queryByText('Time')).not.toBeInTheDocument();
|
||||||
|
};
|
@ -0,0 +1,98 @@
|
|||||||
|
import { Button } from '../../../new-components/Button';
|
||||||
|
import { Input } from '../../../new-components/Form/Input';
|
||||||
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
|
import { FaCalendar } from 'react-icons/fa';
|
||||||
|
import DatePicker, { CalendarContainer } from 'react-datepicker';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { sub } from 'date-fns';
|
||||||
|
import { CustomEventHandler, TextInputProps } from './TextInput';
|
||||||
|
|
||||||
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
|
import './date-input.css';
|
||||||
|
|
||||||
|
const CustomPickerContainer: React.FC<{ className: string }> = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}) => (
|
||||||
|
<div className="absolute top-10 left-0 z-50 min-w-[300px]">
|
||||||
|
<CalendarContainer className={className}>
|
||||||
|
<div style={{ position: 'relative' }}>{children}</div>
|
||||||
|
</CalendarContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DateTimeInput: React.VFC<TextInputProps> = ({
|
||||||
|
name,
|
||||||
|
disabled,
|
||||||
|
placeholder,
|
||||||
|
inputRef,
|
||||||
|
onChange,
|
||||||
|
onInput,
|
||||||
|
onBlur,
|
||||||
|
}) => {
|
||||||
|
const [isCalendarPickerVisible, setCalendarPickerVisible] = useState(false);
|
||||||
|
|
||||||
|
const onDateTimeChange = (date: Date) => {
|
||||||
|
const timeZoneOffsetMinutes = date.getTimezoneOffset();
|
||||||
|
const utcDate = sub(date, { minutes: timeZoneOffsetMinutes });
|
||||||
|
const dateString = utcDate.toISOString();
|
||||||
|
|
||||||
|
if (inputRef && 'current' in inputRef && inputRef.current) {
|
||||||
|
inputRef.current.value = dateString;
|
||||||
|
}
|
||||||
|
if (onChange) {
|
||||||
|
const changeCb = onChange as CustomEventHandler;
|
||||||
|
changeCb({ target: { value: dateString } });
|
||||||
|
}
|
||||||
|
if (onInput) {
|
||||||
|
const inputCb = onInput as CustomEventHandler;
|
||||||
|
inputCb({ target: { value: dateString } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full relative">
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
name={name}
|
||||||
|
label={name}
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={onChange as ChangeEventHandler<HTMLInputElement>}
|
||||||
|
onInput={onInput as ChangeEventHandler<HTMLInputElement>}
|
||||||
|
inputProps={{
|
||||||
|
onBlur: onBlur,
|
||||||
|
ref: inputRef,
|
||||||
|
}}
|
||||||
|
rightButton={
|
||||||
|
<Button
|
||||||
|
icon={
|
||||||
|
<FaCalendar
|
||||||
|
className={clsx(
|
||||||
|
isCalendarPickerVisible && 'fill-yellow-600 stroke-yellow-600'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCalendarPickerVisible(!isCalendarPickerVisible);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{isCalendarPickerVisible && (
|
||||||
|
<DatePicker
|
||||||
|
inline
|
||||||
|
showTimeSelect
|
||||||
|
onClickOutside={() => setCalendarPickerVisible(false)}
|
||||||
|
onChange={onDateTimeChange}
|
||||||
|
calendarContainer={CustomPickerContainer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -21,7 +21,7 @@ export type TextInputProps = Omit<
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const baseInputTw =
|
export const baseInputTw =
|
||||||
'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder:text-slate-300 placeholder:italic';
|
'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder:text-slate-400';
|
||||||
|
|
||||||
export const TextInput: React.VFC<TextInputProps> = ({
|
export const TextInput: React.VFC<TextInputProps> = ({
|
||||||
name,
|
name,
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||||
|
import { handlers } from '../../../mocks/metadata.mock';
|
||||||
|
import { userEvent, within } from '@storybook/testing-library';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
import { TimeInput } from './TimeInput';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Data/Insert Row/components/TimeInput',
|
||||||
|
component: TimeInput,
|
||||||
|
parameters: {
|
||||||
|
msw: handlers(),
|
||||||
|
mockdate: new Date('2020-01-14T15:47:18.502Z'),
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onChange: { action: true },
|
||||||
|
onInput: { action: true },
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof TimeInput>;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof TimeInput> = args => {
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
return <TimeInput {...args} inputRef={inputRef} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Base = Template.bind({});
|
||||||
|
Base.args = {
|
||||||
|
name: 'date',
|
||||||
|
placeholder: 'date...',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled = Template.bind({});
|
||||||
|
Disabled.args = {
|
||||||
|
...Base.args,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
Base.play = async ({ args, canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
userEvent.type(await canvas.findByPlaceholderText('date...'), '15:30:00');
|
||||||
|
|
||||||
|
expect(args.onChange).toHaveBeenCalled();
|
||||||
|
expect(args.onInput).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(await canvas.findByDisplayValue('15:30:00')).toBeInTheDocument();
|
||||||
|
|
||||||
|
userEvent.click(await canvas.findByRole('button'));
|
||||||
|
|
||||||
|
userEvent.click(await canvas.findByText('4:30 PM'));
|
||||||
|
|
||||||
|
expect(await canvas.findByLabelText('date')).toHaveDisplayValue('16:30:00');
|
||||||
|
|
||||||
|
expect(args.onChange).toHaveBeenCalled();
|
||||||
|
expect(args.onInput).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// the picker is automatically hidden on selection
|
||||||
|
expect(await canvas.queryByText('Time')).not.toBeInTheDocument();
|
||||||
|
};
|
@ -0,0 +1,106 @@
|
|||||||
|
import { Button } from '../../../new-components/Button';
|
||||||
|
import { Input } from '../../../new-components/Form/Input';
|
||||||
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
|
import { FaClock } from 'react-icons/fa';
|
||||||
|
import DatePicker, { CalendarContainer } from 'react-datepicker';
|
||||||
|
import { sub } from 'date-fns';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { CustomEventHandler, TextInputProps } from './TextInput';
|
||||||
|
|
||||||
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
|
import './date-input.css';
|
||||||
|
|
||||||
|
const CustomPickerContainer: React.FC<{ className: string }> = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}) => (
|
||||||
|
<div className="absolute top-10 left-0 z-50">
|
||||||
|
<CalendarContainer className={className}>
|
||||||
|
<div style={{ position: 'relative' }}>{children}</div>
|
||||||
|
</CalendarContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const getISOTimePart = (date: Date) => date.toISOString().slice(11, 19);
|
||||||
|
|
||||||
|
export const TimeInput: React.VFC<TextInputProps> = ({
|
||||||
|
name,
|
||||||
|
disabled,
|
||||||
|
placeholder,
|
||||||
|
inputRef,
|
||||||
|
onChange,
|
||||||
|
onInput,
|
||||||
|
onBlur,
|
||||||
|
}) => {
|
||||||
|
const [isCalendarPickerVisible, setCalendarPickerVisible] = useState(false);
|
||||||
|
|
||||||
|
const onTimeChange = (date: Date) => {
|
||||||
|
const timeZoneOffsetMinutes = date.getTimezoneOffset();
|
||||||
|
const utcDate = sub(date, { minutes: timeZoneOffsetMinutes });
|
||||||
|
const timeString = getISOTimePart(utcDate);
|
||||||
|
|
||||||
|
if (inputRef && 'current' in inputRef && inputRef.current) {
|
||||||
|
inputRef.current.value = timeString;
|
||||||
|
}
|
||||||
|
if (onChange) {
|
||||||
|
const changeCb = onChange as CustomEventHandler;
|
||||||
|
changeCb({ target: { value: timeString } });
|
||||||
|
}
|
||||||
|
if (onInput) {
|
||||||
|
const inputCb = onInput as CustomEventHandler;
|
||||||
|
inputCb({ target: { value: timeString } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full relative">
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
name={name}
|
||||||
|
label={name}
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={onChange as ChangeEventHandler<HTMLInputElement>}
|
||||||
|
onInput={onInput as ChangeEventHandler<HTMLInputElement>}
|
||||||
|
inputProps={{
|
||||||
|
onBlur: onBlur,
|
||||||
|
ref: inputRef,
|
||||||
|
}}
|
||||||
|
rightButton={
|
||||||
|
<Button
|
||||||
|
icon={
|
||||||
|
<FaClock
|
||||||
|
className={clsx(
|
||||||
|
isCalendarPickerVisible && 'fill-yellow-600 stroke-yellow-600'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCalendarPickerVisible(!isCalendarPickerVisible);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{isCalendarPickerVisible && (
|
||||||
|
<DatePicker
|
||||||
|
inline
|
||||||
|
showTimeSelect
|
||||||
|
showTimeSelectOnly
|
||||||
|
onClickOutside={() => setCalendarPickerVisible(false)}
|
||||||
|
onChange={date => {
|
||||||
|
if (date) {
|
||||||
|
onTimeChange(date);
|
||||||
|
setCalendarPickerVisible(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
calendarContainer={CustomPickerContainer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,3 +1,7 @@
|
|||||||
|
.react-datepicker {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.react-datepicker-popper[data-placement^='bottom']
|
.react-datepicker-popper[data-placement^='bottom']
|
||||||
.react-datepicker__triangle::before,
|
.react-datepicker__triangle::before,
|
||||||
.react-datepicker-popper[data-placement^='top']
|
.react-datepicker-popper[data-placement^='top']
|
||||||
@ -106,6 +110,15 @@
|
|||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-datepicker__time,
|
||||||
|
.react-datepicker__time ul {
|
||||||
|
height: 173px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__time-box {
|
||||||
|
height: 173px;
|
||||||
|
}
|
||||||
|
|
||||||
.react-datepicker__time-container
|
.react-datepicker__time-container
|
||||||
.react-datepicker__time
|
.react-datepicker__time
|
||||||
.react-datepicker__time-box
|
.react-datepicker__time-box
|
||||||
|
@ -152,7 +152,7 @@ export const Input = ({
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
data-test={dataTest}
|
data-test={dataTest}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder:text-muted',
|
'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder:text-slate-400',
|
||||||
prependLabel !== '' ? 'rounded-l-none' : '',
|
prependLabel !== '' ? 'rounded-l-none' : '',
|
||||||
appendLabel !== '' ? 'rounded-r-none' : '',
|
appendLabel !== '' ? 'rounded-r-none' : '',
|
||||||
maybeError
|
maybeError
|
||||||
|
Loading…
Reference in New Issue
Block a user