Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
66 changes: 66 additions & 0 deletions packages/components/src/internal/components/AutoForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(<AutoForm formSchema={formSchema} onChange={jest.fn()} values={{}} />);
Expand Down Expand Up @@ -160,6 +178,11 @@ describe('AutoForm', () => {
name: 'checkboxField',
type: 'checkbox',
},
{
label: 'datetime field',
name: 'datetimeField',
type: 'datetime',
},
],
};

Expand All @@ -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(
<AutoForm
formSchema={formSchema}
onChange={jest.fn()}
values={{ datetimeField: new Date(2024, 2, 15, 13, 45) }}
/>
);
// 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(<AutoForm formSchema={formSchema} onChange={jest.fn()} values={{}} />);
await userEvent.click(document.querySelector('.react-datepicker__input-container input'));
expect(document.querySelectorAll('.react-datepicker__time-container')).toHaveLength(1);
});
});
59 changes: 43 additions & 16 deletions packages/components/src/internal/components/AutoForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -34,7 +36,7 @@ export interface Field<T = any> {
label: string;
name: string;
// Options are used in Select and Radio fields.
options?: Array<Option<T>>;
options?: Option<T>[];
placeholder?: string;
required?: boolean;
type: string;
Expand All @@ -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<string, string>;
// 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
Expand Down Expand Up @@ -85,7 +87,7 @@ const Label: FC<LabelProps> = ({ cls, field, id, wrapperCls }) => {
if (helpTextHref) {
helpLink = (
<p>
<a href={helpTextHref} target="_blank" rel="noopener noreferrer">
<a href={helpTextHref} rel="noopener noreferrer" target="_blank">
More info
</a>
</p>
Expand Down Expand Up @@ -119,10 +121,10 @@ const TextInput: FC<AutoFormFieldProps> = ({ field, id, inputClasses, onChange,
className={className}
id={id}
name={name}
onChange={_onChange}
placeholder={placeholder}
type="text"
value={_value}
onChange={_onChange}
/>
);
};
Expand All @@ -139,11 +141,11 @@ const NumberInput: FC<AutoFormFieldProps> = ({ field, id, inputClasses, onChange
id={id}
inputMode="numeric"
name={name}
onChange={_onChange}
pattern="[0-9]*"
placeholder={placeholder}
type="text"
value={_value}
onChange={_onChange}
/>
);
};
Expand All @@ -156,7 +158,7 @@ const TextareaInput: FC<AutoFormFieldProps> = ({ field, id, inputClasses, onChan
);
const className = inputClasses.textarea ?? '';
const _value = value === null || value === undefined ? '' : value;
return <textarea className={className} id={id} name={field.name} value={_value} onChange={_onChange} />;
return <textarea className={className} id={id} name={field.name} onChange={_onChange} value={_value} />;
};
TextareaInput.displayName = 'TextareaInput';

Expand All @@ -168,12 +170,12 @@ const CheckboxInput: FC<AutoFormFieldProps> = ({ field, id, inputClasses, onChan
const className = inputClasses.checkbox ?? '';
return (
<input
checked={value === true}
className={className}
id={id}
name={field.name}
type="checkbox"
onChange={_onChange}
checked={value === true}
type="checkbox"
/>
);
};
Expand All @@ -192,7 +194,7 @@ const SelectInput: FC<AutoFormFieldProps> = ({ field, id, inputClasses, onChange
const className = inputClasses.select ?? '';
const hasPlaceholder = placeholder !== null && placeholder !== undefined;
return (
<select className={className} id={id} name={name} value={_value} onChange={_onChange}>
<select className={className} id={id} name={name} onChange={_onChange} value={_value}>
{hasPlaceholder && <option value="">{placeholder}</option>}
{options.map(option => (
<option key={option.value} value={option.value}>
Expand All @@ -213,11 +215,11 @@ const RadioInput: FC<AutoFormFieldProps> = ({ field, inputClasses, onChange, val
{options.map(option => (
<label className={className} key={option.value}>
<input
checked={value === option.value}
name={name}
type="radio"
onChange={_onChange}
type="radio"
value={option.value}
checked={value === option.value}
/>
{option.label}
</label>
Expand All @@ -227,19 +229,44 @@ const RadioInput: FC<AutoFormFieldProps> = ({ field, inputClasses, onChange, val
};
RadioInput.displayName = 'RadioInput';

/**
* A DateTime input component for AutoForm. Currently only supported by our Client, the server is not aware of this input
* type. Expects a Date object as the value, sends a Date object to the onChange callback. Does not use id or
* inputClasses props from AutoFormFieldProps because our underlying DateInput component does not support overriding the
* id or input className.
*/
const DateTimeInput: FC<AutoFormFieldProps<Date>> = ({ field, onChange, value }) => {
const { name, placeholder } = field;
const onDateChange = useCallback((date: Date) => onChange(name, date), [name, onChange]);
return (
<div className="auto-form-date-input">
<DateInput
dateFormat={getDateFNSDateTimeFormat()}
name={name}
onChange={onDateChange}
placeholderText={placeholder}
selected={value}
showTimeSelect
/>
</div>
);
};
DateTimeInput.displayName = 'DateTimeInput';

const AutoFormField: FC<AutoFormFieldProps> = props => {
const { field, id, inputWrapperCls, labelCls, labelWrapperCls, fieldWrapperCls } = props;
const { type } = field;
return (
<div className={'auto-form-field ' + fieldWrapperCls}>
<Label cls={labelCls} wrapperCls={labelWrapperCls} field={field} id={id} />
<Label cls={labelCls} field={field} id={id} wrapperCls={labelWrapperCls} />
<div className={inputWrapperCls}>
{type === 'text' && <TextInput {...props} />}
{type === 'textarea' && <TextareaInput {...props} />}
{type === 'number' && <NumberInput {...props} />}
{type === 'checkbox' && <CheckboxInput {...props} />}
{type === 'select' && <SelectInput {...props} />}
{type === 'radio' && <RadioInput {...props} />}
{type === 'datetime' && <DateTimeInput {...props} />}
</div>
</div>
);
Expand Down Expand Up @@ -280,15 +307,15 @@ export const AutoForm: FC<Props> = props => {
<div className={'auto-form ' + wrapperCls}>
{formSchema.fields.map(field => (
<AutoFormField
key={field.name}
field={field}
fieldWrapperCls={fieldWrapperCls}
id={`auto-form-${field.name}`}
inputClasses={inputClasses}
inputWrapperCls={inputWrapperCls}
key={field.name}
labelCls={labelCls}
labelWrapperCls={labelWrapperCls}
onChange={onChange}
fieldWrapperCls={fieldWrapperCls}
value={values[field.name]}
/>
))}
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/internal/components/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface DateInputProps {

export const DateInput: FC<DateInputProps & DatePickerProps> = memo(props => {
const { container, dateFormat, onSelect, timeFormat, ...pickerProps } = props;
const [id] = useState(() => generateId('date-input-'))
const [id] = useState(() => generateId('date-input-'));

const input = useRef<DatePicker>(undefined);
const formats = useMemo(() => {
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/internal/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export function createGridModelId(gridId: string, schemaQuery: SchemaQuery, keyV

return parts.join('|').toLowerCase();
}

export type Primitive = boolean | null | number | string | undefined;
9 changes: 9 additions & 0 deletions packages/components/src/public/QueryInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export enum QueryInfoStatus {
unknown,
}

export interface NamedParameter {
defaultValue: boolean | number | string;
isRequired: boolean;
jdbcType: string;
name: string;
}

const QUERY_INFO_DEFAULTS = {
disabledSystemFields: undefined,
// canEdit: false,
Expand All @@ -41,6 +48,7 @@ const QUERY_INFO_DEFAULTS = {
lastAction: undefined,
// lastUpdate: undefined,
name: undefined,
namedParameters: [],
pkCols: [],
schemaName: undefined,
status: QueryInfoStatus.unknown,
Expand Down Expand Up @@ -96,6 +104,7 @@ export class QueryInfo {
declare lastAction: LastActionStatus;
// declare lastUpdate: Date;
declare name: string;
declare namedParameters: NamedParameter[];
declare pkCols: string[];
declare plural: string;
declare queryLabel: string;
Expand Down
9 changes: 5 additions & 4 deletions packages/components/src/public/QueryModel/QueryModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { caseInsensitive } from '../../internal/util/utils';
import { naturalSortByProperty } from '../sort';
import { PaginationData } from '../../internal/components/pagination/Pagination';
import { SelectRowsMessage, SelectRowsOptions } from '../../internal/query/selectRows';
import { Primitive } from '../../internal/models';

export type QueryParameters = Record<string, Primitive>;

export function flattenValuesFromRow(
row: any,
Expand Down Expand Up @@ -186,8 +189,7 @@ export interface QueryConfig {
/**
* Query parameters used as input to a parameterized query.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
queryParameters?: Record<string, any>;
readonly queryParameters?: QueryParameters;
/**
* Array of column names to be explicitly included in the column list in the [[QueryModel]] data load.
*/
Expand Down Expand Up @@ -332,8 +334,7 @@ export class QueryModel {
/**
* Query parameters used as input to a parameterized query.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly queryParameters?: Record<string, any>;
readonly queryParameters?: QueryParameters;
/**
* Array of column names to be explicitly included from the column list in the QueryModel data load.
*/
Expand Down