8000 inputs: let the checkbox and multiselect parse arrays by tahini · Pull Request #1039 · chairemobilite/evolution · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

inputs: let the checkbox and multiselect parse arrays #1039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 17, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,10 @@ export type QuestionWidgetConfig =
| InputSelectType
| InputRadioNumberType;

const inputTypesWithArrayValue: QuestionWidgetConfig['inputType'][] = ['checkbox', 'multiselect'];
export const isInputTypeWithArrayValue = (inputType: QuestionWidgetConfig['inputType']): boolean =>
inputTypesWithArrayValue.includes(inputType);

export type TextWidgetConfig = {
type: 'text';
align?: WidgetDirectionAlign;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This file is licensed under the MIT License.
* License text available at https://opensource.org/licenses/MIT
*/
import { WidgetConfig } from '../WidgetConfig';
import { WidgetConfig, isInputTypeWithArrayValue } from '../WidgetConfig';

// Simply make sure various objects are recognize as WidgetConfig object. Any fault should result in compilation error.
test('Test text assignation', () => {
Expand Down Expand Up @@ -80,3 +80,24 @@ test('Test input text assignation', () => {

expect(widgetConfig).toBeDefined();
});

describe('isQuestionAnswerAnArray', () => {
test('should return true for array input types', () => {
expect(isInputTypeWithArrayValue('checkbox')).toBe(true);
expect(isInputTypeWithArrayValue('multiselect')).toBe(true);
});

test('should return false for non-array input types', () => {
expect(isInputTypeWithArrayValue('string')).toBe(false);
expect(isInputTypeWithArrayValue('text')).toBe(false);
expect(isInputTypeWithArrayValue('radio')).toBe(false);
expect(isInputTypeWithArrayValue('radioNumber')).toBe(false);
expect(isInputTypeWithArrayValue('select')).toBe(false);
expect(isInputTypeWithArrayValue('button')).toBe(false);
expect(isInputTypeWithArrayValue('time')).toBe(false);
expect(isInputTypeWithArrayValue('slider')).toBe(false);
expect(isInputTypeWithArrayValue('datePicker')).toBe(false);
expect(isInputTypeWithArrayValue('mapPoint')).toBe(false);
expect(isInputTypeWithArrayValue('mapFindPlace')).toBe(false);
});
});
27 changes: 21 additions & 6 deletions packages/evolution-common/src/utils/__tests__/helpers.test.ts
10000
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,20 @@ each([
[3.4, 'integer', 3],
[null, 'integer', null],
[undefined, 'integer', undefined],
[[3], 'integer', undefined],
[[3], 'integer', 3],
[{ test: 3 }, 'integer', undefined],
[['a', 'b'], 'integer', null, null],
['', 'integer', null],
['3', 'float', 3],
['3.4', 'float', 3.4],
[3, 'float', 3],
[3.4, 'float', 3.4],
[null, 'float', null],
[undefined, 'float', undefined],
[[3], 'float', undefined],
[[3], 'float', 3],
[{ test: 3 }, 'float', undefined],
['', 'float', null],
[['a', 'b'], 'float', null, null],
['true', 'boolean', true],
[true, 'boolean', true],
['f', 'boolean', false],
Expand All @@ -147,20 +149,33 @@ each([
[3, 'string', '3'],
[null, 'string', null],
[undefined, 'string', undefined],
[['str1', 'str2'], 'string', 'str1', ['str1', 'str2']],
[{ type: 'Feature', geometry: { type: 'Point', coordinates: [0,0] }, properties: {} }, 'geojson', { type: 'Feature', geometry: { type: 'Point', coordinates: [0,0] }, properties: {} }],
// Should add the properties to the feature
[{ type: 'Feature', geometry: { type: 'Point', coordinates: [0,0] } }, 'geojson', { type: 'Feature', geometry: { type: 'Point', coordinates: [0,0] }, properties: {} }],
[{ type: 'Feature', geometry: 'not a geometry' }, 'geojson', null],
[3, 'geojson', null],
['not a feature', 'geojson', null],
[null, 'geojson', null],
[[3, 4], undefined, [3, 4]],
[[3, 4], undefined, 3, [3, 4]],
[{ test: 3 }, undefined, { test: 3 }],
// TODO What about other data types? They are simply converted to string, should something else be done?
[[3, 4], 'string', String([3, 4])],
[[3, 4], 'string', '3', ['3', '4']],
[{ test: 3 }, 'string', String({ test: 3 })]
]).test('parseValue: %s %s', (value, type, expected) => {
expect(Helpers.parseValue(value, type)).toEqual(expected);
]).describe('parseValue: %s %s', (value, type, expected, expectedAsArray) => {
test('asArray false', () => {
expect(Helpers.parseValue(value, type, false)).toEqual(expected);
});

test('asArray true', () => {
let expectedArray = expectedAsArray;
if (expectedAsArray === undefined && expected !== null && expected !== undefined) {
expectedArray = [expected];
} else if (expectedAsArray === undefined && expected === null) {
expectedArray = null;
}
expect(Helpers.parseValue(value, type, true)).toEqual(expectedArray);
});
});

each([
Expand Down
77 changes: 55 additions & 22 deletions packages/evolution-common/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,37 +217,23 @@ export const getPath = function (path: string | null | undefined, relativePath?:
return path;
};

/**
* Convert a value to a specified data type
*
* TODO Try to force type return value
*
* TODO2 In float and integer, is the undefined returned value ok?
*
* TODO3 What about other types, if datatype is string and we get an array, it
* returns an array... not as humanly expected. But this is not documented and
* it is used in the wild in many survey. Let's revisit when old usages can
* version evolution to its current state and we can evolve the API
*
* @param value The value to parse
* @param datatype The type of data
* @returns The parsed value converted to the specified data type, or the original value if no conversion is applied.
*/
export const parseValue = function (
value: any,
// inner function to parse a single value based on its datatype
// This is used by the parseValue function to handle both single values and arrays
const _parseSingleValue = (
value: unknown,
datatype: 'integer' | 'float' | 'boolean' | 'string' | 'geojson' | undefined
) {
) => {
// If datatype is undefined, just return the value
if (datatype === undefined) {
return value;
}
if (datatype === 'integer') {
return value === undefined || (value !== null && typeof value === 'object') || Array.isArray(value)
return !(typeof value === 'string' || typeof value === 'number' || value === null)
? undefined
: LE._toInteger(value);
}
if (datatype === 'float') {
return value === undefined || (value !== null && typeof value === 'object') || Array.isArray(value)
return !(typeof value === 'string' || typeof value === 'number' || value === null)
? undefined
: LE._toFloat(value);
}
Expand All @@ -257,7 +243,7 @@ export const parseValue = function (
if (datatype === 'geojson') {
// Add the properties to the value if it is an object and it does not have properties
if (value !== null && typeof value === 'object' && value['properties'] === undefined) {
value.properties = {};
(value as any).properties = {};
}
// For geojson, we only accept valid geojson objects
return value && isFeature(value) ? value : null;
Expand All @@ -269,6 +255,53 @@ export const parseValue = function (
return value === undefined || value === null ? value : String(value);
};

/**
* Convert a value to a specified data type
*
* TODO Try to force type return value
*
* TODO2 In float and integer, is the undefined returned value ok?
*
* TODO3 What about other types, if datatype is string and we get an array, it
* returns an array... not as humanly expected. But this is not documented and
* it is used in the wild in many survey. Let's revisit when old usages can
* version evolution to its current state and we can evolve the API
*
* @param value The value to parse
* @param datatype The type of data
* @param asArray whether this datatype is expected to be an array (for example,
* it would be true for checkboxes or multiselect)
* @returns The parsed value converted to the specified data type, or the original value if no conversion is applied.
*/
export const parseValue = function (
value: any,
datatype: 'integer' | 'float' | 'boolean' | 'string' | 'geojson' | undefined,
asArray: boolean = false
) {
if (!asArray && !Array.isArray(value)) {
// If the value is not an array, and the value is not expected as array, return the parsed value
return _parseSingleValue(value, datatype);
} else if (!asArray && Array.isArray(value)) {
// If the value is an array, but not expected as array, return the first value parsed
if (value.length > 0) {
return _parseSingleValue(value[0], datatype);
} else {
return null;
}
} else if (asArray && Array.isArray(value)) {
// If the value is an array, and expected as array, we will parse each value in the array
const parsedValues = value
.map((v) => _parseSingleValue(v, datatype))
.filter((v) => v !== undefined && v !== null);
// If the parsed values are empty, return null
return parsedValues.length === 0 ? null : parsedValues;
} else {
// If the value is not an array, but expected as array, we will return an array with the parsed value
const parsedValue = _parseSingleValue(value, datatype);
return parsedValue === undefined || parsedValue === null ? parsedValue : [parsedValue];
}
};

/**
* Get a response object or value for a specific path in the interview
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import * as surveyHelper from 'evolution-common/lib/utils/helpers';
import applicationConfiguration from '../../config/application.config';
import { checkConditional, checkChoicesConditional } from './Conditional';
import { checkValidations } from './Validation';
import { UserRuntimeInterviewAttributes } from 'evolution-common/lib/services/questionnaire/types';
import {
isInputTypeWithArrayValue,
UserRuntimeInterviewAttributes
} from 'evolution-common/lib/services/questionnaire/types';
import { CliUser } from 'chaire-lib-common/lib/services/user/userType';
import { GroupConfig } from 'evolution-common/lib/services/questionnaire/types';

Expand Down Expand Up @@ -143,7 +146,7 @@ const prepareSimpleWidget = (
let customValue = customPath ? surveyHelper.getResponse(data.interview, customPath, undefined) : undefined;

// convert values to number or boolean if needed:
value = surveyHelper.parseValue(value, widgetConfig.datatype);
value = surveyHelper.parseValue(value, widgetConfig.datatype, isInputTypeWithArrayValue(widgetConfig.inputType));
let visibleValueUpdated = false;
customValue = surveyHelper.parseValue(customValue, widgetConfig.customDatatype);

Expand Down
8 changes: 6 additions & 2 deletions packages/evolution-frontend/src/components/survey/Question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
UserInterviewAttributes
} from 'evolution-common/lib/services/questionnaire/types';
import { CliUser } from 'chaire-lib-common/lib/services/user/userType';
import { QuestionWidgetConfig } from 'evolution-common/lib/services/questionnaire/types';
import { QuestionWidgetConfig, isInputTypeWithArrayValue } from 'evolution-common/lib/services/questionnaire/types';
import { WidgetStatus } from 'evolution-common/lib/services/questionnaire/types';
import InputWidgetWrapper from './widgets/InputWidgetWrapper';

Expand Down Expand Up @@ -90,7 +90,11 @@ export class Question extends React.Component<QuestionProps & WithSurveyContextP
const previousValue = widgetStatus.value;
const previousCustomValue = widgetStatus.customValue;
const value = e.target ? e.target.value : e; //InputDatePicker call onValueChange with e=value
const parsedValue = surveyHelper.parseValue(value, (widgetConfig as any).datatype);
const parsedValue = surveyHelper.parseValue(
value,
(widgetConfig as any).datatype,
isInputTypeWithArrayValue(widgetConfig.inputType)
);
const parsedCustomValue = surveyHelper.parseValue(customValue, (widgetConfig as any).customDatatype);
const [isValid] = checkValidations(
widgetConfig.validations,
Expand Down
Loading
0