mirror of
https://github.com/status-im/spiff-arena.git
synced 2025-01-14 04:05:31 +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 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) {
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
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 && (
|
||||
|
Loading…
x
Reference in New Issue
Block a user