Added vertical option to radio buttons and fixed the four problems with the compensation range field (#925)

* Fixed validation errors for numeric range field and added asterisk when field is required

* Removed unused import

* Added ability to make radio buttons vertical and fixed issues with compensation range field

* Radio Button Styling and Decimal Support in Compensation Fields

* Accepted suggestion to get rid of uneeded if statements

* Accepted suggestion about adding a comment to formatNumberString function

* fix npm run lint issues

* Fixed compensation range field

* Fixed compensation range field

* Fixed compensation range field and changed minimum and maximum to be required. Fixed some bugs

* Update spiffworkflow-frontend/src/index.css

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update spiffworkflow-frontend/src/rjsf/custom_widgets/CharacterCounterField/CharacterCounterField.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Added specific error messages to numeric range field and made character counter field

* Added specific error messages to numeric range field and made character counter field

* Fixed linting errors

* Revert "Fixed linting errors"

This reverts commit dd0c3253a0.

* Revert "Added specific error messages to numeric range field and made character counter field"

This reverts commit f9cb3979d8.

* Added check if min > max back to numeric range field

* removed old files

---------

Co-authored-by: burnettk <burnettk@users.noreply.github.com>
Co-authored-by: KyushuApp <160429351+KyushuApp@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Kevin Burnett <18027+burnettk@users.noreply.github.com>
Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
Kayvon-Martinez 2024-03-08 13:05:43 -06:00 committed by GitHub
parent e3f0758399
commit b418f6f7d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 278 additions and 41 deletions

View File

@ -9,6 +9,7 @@ import TypeaheadWidget from '../rjsf/custom_widgets/TypeaheadWidget/TypeaheadWid
import MarkDownFieldWidget from '../rjsf/custom_widgets/MarkDownFieldWidget/MarkDownFieldWidget';
import NumericRangeField from '../rjsf/custom_widgets/NumericRangeField/NumericRangeField';
import ObjectFieldRestrictedGridTemplate from '../rjsf/custom_templates/ObjectFieldRestrictGridTemplate';
import CharacterCounterField from '../rjsf/custom_widgets/CharacterCounterField/CharacterCounterField';
enum DateCheckType {
minimum = 'minimum',
@ -53,6 +54,7 @@ export default function CustomForm({
// set in uiSchema using the "ui:field" key for a property
const rjsfFields: RegistryFieldsType = {
'numeric-range': NumericRangeField,
'character-counter': CharacterCounterField,
};
const rjsfTemplates: any = {};
@ -250,12 +252,71 @@ export default function CustomForm({
formDataToCheck: any,
propertyKey: string,
errors: any,
_jsonSchema: any,
jsonSchema: any,
_uiSchemaPassedIn?: any
) => {
if (formDataToCheck[propertyKey].min > formDataToCheck[propertyKey].max) {
if (
jsonSchema.required &&
jsonSchema.required.includes(propertyKey) &&
(formDataToCheck[propertyKey].min === undefined ||
formDataToCheck[propertyKey].max === undefined)
) {
errors[propertyKey].addError(
`must have min less than max on numeric range`
`must have valid Minimum and Maximum on ${propertyKey}`
);
}
if (
formDataToCheck[propertyKey].min <
jsonSchema.properties[propertyKey].minimum
) {
errors[propertyKey].addError(
`must have min greater than or equal to ${jsonSchema.properties[propertyKey].minimum}`
);
}
if (
formDataToCheck[propertyKey].min >
jsonSchema.properties[propertyKey].maximum
) {
errors[propertyKey].addError(
`must have min less than or equal to ${jsonSchema.properties[propertyKey].maximum}`
);
}
if (
formDataToCheck[propertyKey].max <
jsonSchema.properties[propertyKey].minimum
) {
errors[propertyKey].addError(
`must have max greater than or equal to ${jsonSchema.properties[propertyKey].minimum}`
);
}
if (
formDataToCheck[propertyKey].max >
jsonSchema.properties[propertyKey].maximum
) {
errors[propertyKey].addError(
`must have max less than or equal to ${jsonSchema.properties[propertyKey].maximum}`
);
}
if (formDataToCheck[propertyKey].min > formDataToCheck[propertyKey].max) {
errors[propertyKey].addError(`must have min less than or equal to max`);
}
};
const checkCharacterCounter = (
formDataToCheck: any,
propertyKey: string,
errors: any,
jsonSchema: any,
_uiSchemaPassedIn?: any
) => {
if (
jsonSchema.required &&
jsonSchema.required.includes(propertyKey) &&
(formDataToCheck[propertyKey] === undefined ||
formDataToCheck[propertyKey] === '')
) {
errors[propertyKey].addError(
`must have required property '${propertyKey}'`
);
}
};
@ -332,6 +393,20 @@ export default function CustomForm({
);
}
if (
currentUiSchema &&
'ui:field' in currentUiSchema &&
currentUiSchema['ui:field'] === 'character-counter'
) {
checkCharacterCounter(
formDataToCheck,
propertyKey,
errors,
jsonSchemaToUse,
currentUiSchema
);
}
// recurse through all nested properties as well
let formDataToSend = formDataToCheck[propertyKey];
if (formDataToSend) {

View File

@ -966,6 +966,23 @@ div.onboarding {
line-height: 48px;
}
.radio-button-group-column {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-inline-end: 0;
}
.radio-button-group-column > * {
margin-bottom: 0.5rem;
}
.radio-button-group-row {
display: flex;
flex-direction: row;
align-items: flex-start;
}
/* Utility classes to create horizontally centered stacks (to align icons etc) */
.flex-align-horizontal-center {
display: flex;

View File

@ -28,6 +28,8 @@ function RadioWidget({
onChange(newValue);
}
};
const column = uiSchema?.['ui:layout']?.toString().toLowerCase() === 'column';
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, value);
const _onFocus = ({
@ -57,16 +59,18 @@ function RadioWidget({
onBlur={_onBlur}
onFocus={_onFocus}
>
{Array.isArray(enumOptions) &&
enumOptions.map((option) => {
return (
<RadioButton
id={`${id}-${option.value}`}
labelText={option.label}
value={`${option.value}`}
/>
);
})}
<div className={`radio-button-group-${column ? 'column' : 'row'}`}>
{Array.isArray(enumOptions) &&
enumOptions.map((option) => {
return (
<RadioButton
id={`${id}-${option.value}`}
labelText={option.label}
value={`${option.value}`}
/>
);
})}
</div>
</RadioButtonGroup>
);
}

View File

@ -0,0 +1,114 @@
import {
descriptionId,
FieldProps,
getTemplate,
getUiOptions,
} from '@rjsf/utils';
import { TextInput } from '@carbon/react';
import React from 'react';
import { getCommonAttributes } from '../../helpers';
// Example jsonSchema - NOTE: the "min" and "max" properties are special names and must be used:
// "name":{
// "title": "Name",
// "type": "string",
// "maxLength": 999999999999,
// }
//
// Example uiSchema:
// "name": {
// "ui:field": "character-counter",
// }
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function CharacterCounterField({
id,
schema,
uiSchema,
idSchema,
disabled,
readonly,
onChange,
autofocus,
label,
rawErrors = [],
formData,
registry,
required,
}: FieldProps) {
const commonAttributes = getCommonAttributes(
label,
schema,
uiSchema,
rawErrors
);
const description = schema?.description || uiSchema?.['ui:description'];
const uiOptions = getUiOptions(uiSchema || {});
const DescriptionFieldTemplate = getTemplate(
'DescriptionFieldTemplate',
registry,
uiOptions
);
if (schema.maxLength === undefined) {
throw new Error(
'CharacterCounterTextField requires a "maxLength" property to be specified in the schema'
);
}
const text = formData || '';
const onChangeLocal = (event: any) => {
event.preventDefault();
if (!disabled && !readonly) {
onChange(event.target.value);
}
};
return (
<div className="character---counter--text-field-wrapper">
<div className="character---counter--text-field-label">
<h5>
{required ? `${commonAttributes.label} *` : commonAttributes.label}
</h5>
{description && (
<div className="markdown-field-desc-text">
<DescriptionFieldTemplate
id={descriptionId(idSchema)}
description={description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
</div>
)}
</div>
<TextInput
id={id}
type="text"
disabled={disabled}
readonly={readonly}
value={text}
onChange={(event: any) => {
onChangeLocal(event);
}}
invalid={commonAttributes.invalid}
enableCounter
maxCount={schema.maxLength}
autoFocus={autofocus}
/>
{commonAttributes.errorMessageForField && (
<div className="error-message">
{commonAttributes.errorMessageForField}
</div>
)}
{commonAttributes.helperText && (
<p className="character---counter--text-field-help-text">
{commonAttributes.helperText}
</p>
)}
</div>
);
}

View File

@ -4,6 +4,7 @@ import {
getTemplate,
getUiOptions,
} from '@rjsf/utils';
import React from 'react';
import { TextInput } from '@carbon/react';
import { getCommonAttributes } from '../../helpers';
@ -11,6 +12,8 @@ import { getCommonAttributes } from '../../helpers';
// compensation":{
// "title": "Compensation (yearly), USD",
// "type": "object",
// "minimum": 0,
// "maximum": 999999999999,
// "properties": {
// "min": {
// "type": "number"
@ -59,44 +62,60 @@ export default function NumericRangeField({
);
const formatNumberString = (numberString: string): string => {
if (numberString) {
return numberString.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// this function will change the number string to a number with commas
// and a decimal point if needed. For example, 1000 will become 1,000
// or 1000.5 will become 1,000.5
const numberStringNoCommas = numberString.replace(/,/g, '');
if (numberStringNoCommas) {
const parts = numberStringNoCommas.split('.');
const integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.length > 1 ? `${integerPart}.${parts[1]}` : integerPart;
}
return '';
};
const parseNumberString = (numberString: string) =>
Number(numberString.replace(/,/g, ''));
const parseNumberString = (numberString: string) => {
if (
(numberString === '-' && numberString.length === 1) ||
numberString.endsWith('.')
) {
return null;
}
return Number(numberString.replace(/,/g, ''));
};
// create two number inputs for min and max compensation
const min = formData?.min || 0;
const max = formData?.max || 0;
if (schema.minimum === undefined || schema.maximum === undefined) {
throw new Error('minimum and maximum not defined');
}
const minNumber = schema.minimum;
const maxNumber = schema.maximum;
const min = formData?.min;
const [minValue, setMinValue] = React.useState(min?.toString() || '');
const max = formData?.max;
const [maxValue, setMaxValue] = React.useState(max?.toString() || '');
// the text input eventually breaks when the number gets too big.
// we are not sure what the cut off really is but seems unlikely
// people will need to go this high.
const maxNumber = 999_999_999_999;
const onChangeLocal = (nameToChange: any, event: any) => {
event.preventDefault();
const numberValue = parseNumberString(event.target.value);
if (numberValue > maxNumber) {
return;
// Validate and update the numeric range based on user input
if (nameToChange === 'min') {
setMinValue(formatNumberString(numberValue?.toString() || ''));
}
if (nameToChange === 'max') {
setMaxValue(formatNumberString(numberValue?.toString() || ''));
}
if (!disabled && !readonly) {
if (nameToChange === 'min' && numberValue > max) {
onChange({
...(formData || {}),
min: numberValue,
max: numberValue,
});
} else {
onChange({
...(formData || {}),
...{ max, min },
[nameToChange]: numberValue,
});
}
onChange({
...(formData || {}),
...{ max, min },
[nameToChange]: numberValue,
});
}
};
@ -121,24 +140,32 @@ export default function NumericRangeField({
<div className="numeric--range-field-inputs">
<TextInput
id={`${id}-min`}
type="text"
labelText={(schema as any).properties?.min?.title || `Minimum`}
disabled={disabled}
readonly={readonly}
value={formatNumberString(min)}
onChange={(values: any) => {
onChangeLocal('min', values);
value={formatNumberString(minValue)}
onChange={(event: any) => {
onChangeLocal('min', event);
setMinValue(event.target.value);
}}
invalid={commonAttributes.invalid}
helperText={`Min: ${formatNumberString(minNumber?.toString() || '')}`}
autofocus={autofocus}
/>
<TextInput
id={`${id}-max`}
type="text"
labelText={(schema as any).properties?.max?.title || `Maximum`}
disabled={disabled}
readonly={readonly}
value={formatNumberString(max)}
onChange={(values: any) => onChangeLocal('max', values)}
value={formatNumberString(maxValue)}
onChange={(event: any) => {
onChangeLocal('max', event);
setMaxValue(event.target.value);
}}
invalid={commonAttributes.invalid}
helperText={`Max: ${formatNumberString(maxNumber?.toString() || '')}`}
/>
</div>
{commonAttributes.errorMessageForField && (