diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index b55d9f6060..09c58cfc23 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1939,6 +1939,7 @@ export type { MenuSectionItem } from './internal/DropdownSection'; export type { UseTimeout } from './internal/hooks'; export type { ModalProps } from './internal/Modal'; export type { AddEntitiesComplete, ModalRendererProps } from './internal/ModalRenderFactory'; +export type { Primitive } from './internal/models'; export type { TriggerType } from './internal/OverlayTrigger'; export type { ISelectRowsResult } from './internal/query/api'; export type { @@ -1968,13 +1969,13 @@ export type { UseRequestHandler } from './internal/util/RequestHandler'; export type { InjectedRouteLeaveProps, WrappedRouteLeaveProps } from './internal/util/RouteLeave'; export type { QueryParams } from './internal/util/URL'; export type { FileSizeLimitProps } from './public/files/models'; -export type { ImportTemplate } from './public/QueryInfo'; +export type { ImportTemplate, NamedParameter } from './public/QueryInfo'; export type { EditableDetailPanelProps } from './public/QueryModel/EditableDetailPanel'; export type { Action, ActionValue } from './public/QueryModel/grid/actions/Action'; -export type { QueryConfig } from './public/QueryModel/QueryModel'; +export type { QueryConfig, QueryParameters } from './public/QueryModel/QueryModel'; export type { QueryModelLoader } from './public/QueryModel/QueryModelLoader'; -export type { TabbedGridPanelProps } from './public/QueryModel/TabbedGridPanel'; +export type { TabbedGridPanelProps } from './public/QueryModel/TabbedGridPanel'; // Due to babel-loader & typescript babel plugins we need to export/import types separately. The babel plugins require // the typescript compiler option "isolatedModules", which do not export types from modules, so types must be exported // separately. diff --git a/packages/components/src/internal/components/AutoForm.test.tsx b/packages/components/src/internal/components/AutoForm.test.tsx index 6c3b8d137c..6f93898efd 100644 --- a/packages/components/src/internal/components/AutoForm.test.tsx +++ b/packages/components/src/internal/components/AutoForm.test.tsx @@ -34,6 +34,13 @@ describe('AutoForm', () => { }); } else if (field.type === 'checkbox') { expect(fieldEl.querySelectorAll('input[type="checkbox"]').length).toEqual(1); + } else if (field.type === 'datetime') { + expect(fieldEl.querySelectorAll('.auto-form-date-input').length).toEqual(1); + const input = fieldEl.querySelector('.react-datepicker__input-container input'); + expect(input.getAttribute('name')).toEqual(field.name); + if (field.placeholder) { + expect(input.getAttribute('placeholder')).toEqual(field.placeholder); + } } else if (field.type === 'select') { expect(fieldEl.querySelectorAll('select').length).toEqual(1); const expectedOptions = field.placeholder ? field.options.length + 1 : field.options.length; @@ -122,6 +129,17 @@ describe('AutoForm', () => { ], type: 'radio', }, + { + label: 'datetime field', + name: 'datetimeField', + type: 'datetime', + }, + { + label: 'datetime field w/ placeholder', + name: 'datetimeFieldPlaceholder', + placeholder: 'datetime placeholder', + type: 'datetime', + }, ], }; render(); @@ -160,6 +178,11 @@ describe('AutoForm', () => { name: 'checkboxField', type: 'checkbox', }, + { + label: 'datetime field', + name: 'datetimeField', + type: 'datetime', + }, ], }; @@ -173,5 +196,48 @@ describe('AutoForm', () => { expect(onChange).toHaveBeenLastCalledWith('radioField', 'option2'); await userEvent.selectOptions(document.querySelectorAll('select')[0], 'option2'); expect(onChange).toHaveBeenLastCalledWith('selectField', 'option2'); + await userEvent.click(document.querySelectorAll('input[type="checkbox"]')[0]); + expect(onChange).toHaveBeenLastCalledWith('checkboxField', true); + await userEvent.type( + document.querySelector('.auto-form-date-input .react-datepicker__input-container input'), + '2024-03-15 13:45' + ); + expect(onChange).toHaveBeenLastCalledWith('datetimeField', new Date(2024, 2, 15, 13, 45)); + }); + + test('datetime value formatting', () => { + const formSchema: FormSchema = { + fields: [ + { + label: 'datetime field', + name: 'datetimeField', + type: 'datetime', + }, + ], + }; + render( + + ); + // DateTimeInput formats with the container dateTimeFormat, which is yyyy-MM-dd HH:mm in tests. + expect(document.querySelector('.react-datepicker__input-container input')).toHaveValue('2024-03-15 13:45'); + }); + + test('datetime time select', async () => { + const formSchema: FormSchema = { + fields: [ + { + label: 'datetime field', + name: 'datetimeField', + type: 'datetime', + }, + ], + }; + render(); + await userEvent.click(document.querySelector('.react-datepicker__input-container input')); + expect(document.querySelectorAll('.react-datepicker__time-container')).toHaveLength(1); }); }); diff --git a/packages/components/src/internal/components/AutoForm.tsx b/packages/components/src/internal/components/AutoForm.tsx index efb64025bc..2f83c1097a 100644 --- a/packages/components/src/internal/components/AutoForm.tsx +++ b/packages/components/src/internal/components/AutoForm.tsx @@ -5,6 +5,8 @@ import React, { ChangeEvent, FC, useCallback } from 'react'; import { HelpIcon } from './HelpIcon'; +import { DateInput } from './DateInput'; +import { getDateFNSDateTimeFormat } from '../util/Date'; const INPUT_CLASSES = { checkbox: 'form-check', @@ -34,7 +36,7 @@ export interface Field { label: string; name: string; // Options are used in Select and Radio fields. - options?: Array>; + options?: Option[]; placeholder?: string; required?: boolean; type: string; @@ -48,16 +50,16 @@ export interface FormSchema { } export interface FieldClassProps { + // className for the div that wraps each field component + fieldWrapperCls?: string; // A map of input types to classNames (see INPUT_CLASSES for the default values) inputClasses?: Record; // className for the div that wraps each input element inputWrapperCls?: string; - // className for the the label element + // className for the label element labelCls?: string; // className for the div that wraps the label element labelWrapperCls?: string; - // className for the div that wraps each field component - fieldWrapperCls?: string; } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -85,7 +87,7 @@ const Label: FC = ({ cls, field, id, wrapperCls }) => { if (helpTextHref) { helpLink = (

- + More info

@@ -119,10 +121,10 @@ const TextInput: FC = ({ field, id, inputClasses, onChange, className={className} id={id} name={name} + onChange={_onChange} placeholder={placeholder} type="text" value={_value} - onChange={_onChange} /> ); }; @@ -139,11 +141,11 @@ const NumberInput: FC = ({ field, id, inputClasses, onChange id={id} inputMode="numeric" name={name} + onChange={_onChange} pattern="[0-9]*" placeholder={placeholder} type="text" value={_value} - onChange={_onChange} /> ); }; @@ -156,7 +158,7 @@ const TextareaInput: FC = ({ field, id, inputClasses, onChan ); const className = inputClasses.textarea ?? ''; const _value = value === null || value === undefined ? '' : value; - return