mirror of
https://github.com/status-im/spiff-arena.git
synced 2025-01-28 02:35:25 +00:00
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 dd0c3253a0f540e0fad502c2af1428372e13efc9. * Revert "Added specific error messages to numeric range field and made character counter field" This reverts commit f9cb3979d85e2e4952266c637cebf742473fce2a. * 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:
parent
e3f0758399
commit
b418f6f7d8
@ -9,6 +9,7 @@ import TypeaheadWidget from '../rjsf/custom_widgets/TypeaheadWidget/TypeaheadWid
|
|||||||
import MarkDownFieldWidget from '../rjsf/custom_widgets/MarkDownFieldWidget/MarkDownFieldWidget';
|
import MarkDownFieldWidget from '../rjsf/custom_widgets/MarkDownFieldWidget/MarkDownFieldWidget';
|
||||||
import NumericRangeField from '../rjsf/custom_widgets/NumericRangeField/NumericRangeField';
|
import NumericRangeField from '../rjsf/custom_widgets/NumericRangeField/NumericRangeField';
|
||||||
import ObjectFieldRestrictedGridTemplate from '../rjsf/custom_templates/ObjectFieldRestrictGridTemplate';
|
import ObjectFieldRestrictedGridTemplate from '../rjsf/custom_templates/ObjectFieldRestrictGridTemplate';
|
||||||
|
import CharacterCounterField from '../rjsf/custom_widgets/CharacterCounterField/CharacterCounterField';
|
||||||
|
|
||||||
enum DateCheckType {
|
enum DateCheckType {
|
||||||
minimum = 'minimum',
|
minimum = 'minimum',
|
||||||
@ -53,6 +54,7 @@ export default function CustomForm({
|
|||||||
// set in uiSchema using the "ui:field" key for a property
|
// set in uiSchema using the "ui:field" key for a property
|
||||||
const rjsfFields: RegistryFieldsType = {
|
const rjsfFields: RegistryFieldsType = {
|
||||||
'numeric-range': NumericRangeField,
|
'numeric-range': NumericRangeField,
|
||||||
|
'character-counter': CharacterCounterField,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rjsfTemplates: any = {};
|
const rjsfTemplates: any = {};
|
||||||
@ -250,12 +252,71 @@ export default function CustomForm({
|
|||||||
formDataToCheck: any,
|
formDataToCheck: any,
|
||||||
propertyKey: string,
|
propertyKey: string,
|
||||||
errors: any,
|
errors: any,
|
||||||
_jsonSchema: any,
|
jsonSchema: any,
|
||||||
_uiSchemaPassedIn?: 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(
|
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
|
// recurse through all nested properties as well
|
||||||
let formDataToSend = formDataToCheck[propertyKey];
|
let formDataToSend = formDataToCheck[propertyKey];
|
||||||
if (formDataToSend) {
|
if (formDataToSend) {
|
||||||
|
@ -966,6 +966,23 @@ div.onboarding {
|
|||||||
line-height: 48px;
|
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) */
|
/* Utility classes to create horizontally centered stacks (to align icons etc) */
|
||||||
.flex-align-horizontal-center {
|
.flex-align-horizontal-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -28,6 +28,8 @@ function RadioWidget({
|
|||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const column = uiSchema?.['ui:layout']?.toString().toLowerCase() === 'column';
|
||||||
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
|
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
|
||||||
onBlur(id, value);
|
onBlur(id, value);
|
||||||
const _onFocus = ({
|
const _onFocus = ({
|
||||||
@ -57,16 +59,18 @@ function RadioWidget({
|
|||||||
onBlur={_onBlur}
|
onBlur={_onBlur}
|
||||||
onFocus={_onFocus}
|
onFocus={_onFocus}
|
||||||
>
|
>
|
||||||
{Array.isArray(enumOptions) &&
|
<div className={`radio-button-group-${column ? 'column' : 'row'}`}>
|
||||||
enumOptions.map((option) => {
|
{Array.isArray(enumOptions) &&
|
||||||
return (
|
enumOptions.map((option) => {
|
||||||
<RadioButton
|
return (
|
||||||
id={`${id}-${option.value}`}
|
<RadioButton
|
||||||
labelText={option.label}
|
id={`${id}-${option.value}`}
|
||||||
value={`${option.value}`}
|
labelText={option.label}
|
||||||
/>
|
value={`${option.value}`}
|
||||||
);
|
/>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</RadioButtonGroup>
|
</RadioButtonGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -4,6 +4,7 @@ import {
|
|||||||
getTemplate,
|
getTemplate,
|
||||||
getUiOptions,
|
getUiOptions,
|
||||||
} from '@rjsf/utils';
|
} from '@rjsf/utils';
|
||||||
|
import React from 'react';
|
||||||
import { TextInput } from '@carbon/react';
|
import { TextInput } from '@carbon/react';
|
||||||
import { getCommonAttributes } from '../../helpers';
|
import { getCommonAttributes } from '../../helpers';
|
||||||
|
|
||||||
@ -11,6 +12,8 @@ import { getCommonAttributes } from '../../helpers';
|
|||||||
// compensation":{
|
// compensation":{
|
||||||
// "title": "Compensation (yearly), USD",
|
// "title": "Compensation (yearly), USD",
|
||||||
// "type": "object",
|
// "type": "object",
|
||||||
|
// "minimum": 0,
|
||||||
|
// "maximum": 999999999999,
|
||||||
// "properties": {
|
// "properties": {
|
||||||
// "min": {
|
// "min": {
|
||||||
// "type": "number"
|
// "type": "number"
|
||||||
@ -59,44 +62,60 @@ export default function NumericRangeField({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const formatNumberString = (numberString: string): string => {
|
const formatNumberString = (numberString: string): string => {
|
||||||
if (numberString) {
|
// this function will change the number string to a number with commas
|
||||||
return numberString.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
// 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 '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseNumberString = (numberString: string) =>
|
const parseNumberString = (numberString: string) => {
|
||||||
Number(numberString.replace(/,/g, ''));
|
if (
|
||||||
|
(numberString === '-' && numberString.length === 1) ||
|
||||||
|
numberString.endsWith('.')
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Number(numberString.replace(/,/g, ''));
|
||||||
|
};
|
||||||
|
|
||||||
// create two number inputs for min and max compensation
|
if (schema.minimum === undefined || schema.maximum === undefined) {
|
||||||
const min = formData?.min || 0;
|
throw new Error('minimum and maximum not defined');
|
||||||
const max = formData?.max || 0;
|
}
|
||||||
|
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.
|
// the text input eventually breaks when the number gets too big.
|
||||||
// we are not sure what the cut off really is but seems unlikely
|
// we are not sure what the cut off really is but seems unlikely
|
||||||
// people will need to go this high.
|
// people will need to go this high.
|
||||||
const maxNumber = 999_999_999_999;
|
|
||||||
|
|
||||||
const onChangeLocal = (nameToChange: any, event: any) => {
|
const onChangeLocal = (nameToChange: any, event: any) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const numberValue = parseNumberString(event.target.value);
|
const numberValue = parseNumberString(event.target.value);
|
||||||
if (numberValue > maxNumber) {
|
// Validate and update the numeric range based on user input
|
||||||
return;
|
if (nameToChange === 'min') {
|
||||||
|
setMinValue(formatNumberString(numberValue?.toString() || ''));
|
||||||
|
}
|
||||||
|
if (nameToChange === 'max') {
|
||||||
|
setMaxValue(formatNumberString(numberValue?.toString() || ''));
|
||||||
}
|
}
|
||||||
if (!disabled && !readonly) {
|
if (!disabled && !readonly) {
|
||||||
if (nameToChange === 'min' && numberValue > max) {
|
onChange({
|
||||||
onChange({
|
...(formData || {}),
|
||||||
...(formData || {}),
|
...{ max, min },
|
||||||
min: numberValue,
|
[nameToChange]: numberValue,
|
||||||
max: numberValue,
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
onChange({
|
|
||||||
...(formData || {}),
|
|
||||||
...{ max, min },
|
|
||||||
[nameToChange]: numberValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -121,24 +140,32 @@ export default function NumericRangeField({
|
|||||||
<div className="numeric--range-field-inputs">
|
<div className="numeric--range-field-inputs">
|
||||||
<TextInput
|
<TextInput
|
||||||
id={`${id}-min`}
|
id={`${id}-min`}
|
||||||
|
type="text"
|
||||||
labelText={(schema as any).properties?.min?.title || `Minimum`}
|
labelText={(schema as any).properties?.min?.title || `Minimum`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
value={formatNumberString(min)}
|
value={formatNumberString(minValue)}
|
||||||
onChange={(values: any) => {
|
onChange={(event: any) => {
|
||||||
onChangeLocal('min', values);
|
onChangeLocal('min', event);
|
||||||
|
setMinValue(event.target.value);
|
||||||
}}
|
}}
|
||||||
invalid={commonAttributes.invalid}
|
invalid={commonAttributes.invalid}
|
||||||
|
helperText={`Min: ${formatNumberString(minNumber?.toString() || '')}`}
|
||||||
autofocus={autofocus}
|
autofocus={autofocus}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id={`${id}-max`}
|
id={`${id}-max`}
|
||||||
|
type="text"
|
||||||
labelText={(schema as any).properties?.max?.title || `Maximum`}
|
labelText={(schema as any).properties?.max?.title || `Maximum`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
value={formatNumberString(max)}
|
value={formatNumberString(maxValue)}
|
||||||
onChange={(values: any) => onChangeLocal('max', values)}
|
onChange={(event: any) => {
|
||||||
|
onChangeLocal('max', event);
|
||||||
|
setMaxValue(event.target.value);
|
||||||
|
}}
|
||||||
invalid={commonAttributes.invalid}
|
invalid={commonAttributes.invalid}
|
||||||
|
helperText={`Max: ${formatNumberString(maxNumber?.toString() || '')}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{commonAttributes.errorMessageForField && (
|
{commonAttributes.errorMessageForField && (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user