From e48767eb95a7d1740c70095b07bd14b7f27a4f59 Mon Sep 17 00:00:00 2001 From: Quentin Ruhier Date: Mon, 20 Apr 2026 14:21:01 +0200 Subject: [PATCH 1/5] feat: add filter for ucq based on a variable --- CHANGELOG.md | 4 ++ legacy/src/constants/dictionary/codes.ts | 4 ++ .../component-new-edit/collected-variable.jsx | 6 +++ .../response-format-table.jsx | 6 ++- legacy/src/model/formToState/lists/utils.js | 8 ++-- .../src/model/formToState/lists/variables.js | 6 ++- .../response-format-single.spec.ts | 6 ++- .../transformations/response-format-single.ts | 12 ++++- .../transformations/response-format-table.jsx | 12 ++++- .../response-format-table.spec.jsx | 44 ++++++++++++++++--- .../model/transformations/response.spec.ts | 16 +++++++ legacy/src/model/transformations/response.ts | 2 + legacy/src/model/transformations/types.ts | 2 + .../variables/collected-variables-single.js | 1 + .../collected-variables-single.spec.js | 2 + .../variables/collected-variables-table.js | 4 ++ .../collected-variables-table.spec.js | 4 ++ .../response-format-single-variable.jsx | 11 +++++ .../response-format-single-variable.spec.jsx | 26 ++++++++++- 19 files changed, 158 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80bac3d44..05a3007ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.1.0](https://github.com/InseeFr/Pogues/releases/tag/3.1.0) - 2026-05-13 +### Added + +- In a unique choice question with modalities computed from a variable, you can now add a VTL expression for filtering modalities. + ### Fixed - Questionnaire loading no longer crash when a question uses a code list (or nomenclature) that does not exist anymore. It could happen when an existing question used a previous version of a nomenclature. diff --git a/legacy/src/constants/dictionary/codes.ts b/legacy/src/constants/dictionary/codes.ts index 61ff920df..576a08f25 100644 --- a/legacy/src/constants/dictionary/codes.ts +++ b/legacy/src/constants/dictionary/codes.ts @@ -46,4 +46,8 @@ export const codesDictionary: Dictionary = { fr: `Modifier le filtre`, en: `Modify filter`, }, + modalityFilter: { + fr: `Filtre de modalités`, + en: `Modality filter`, + }, }; diff --git a/legacy/src/model/formToState/component-new-edit/collected-variable.jsx b/legacy/src/model/formToState/component-new-edit/collected-variable.jsx index 9179bf5af..6b7d426a7 100644 --- a/legacy/src/model/formToState/component-new-edit/collected-variable.jsx +++ b/legacy/src/model/formToState/component-new-edit/collected-variable.jsx @@ -15,6 +15,7 @@ export const defaultState = { codeListReferenceLabel: '', variableReference: '', variableReferenceLabel: '', + optionFilter: '', choiceType: '', isCollected: '1', alternativeLabel: '', @@ -33,6 +34,7 @@ export const defaultForm = { codeListReferenceLabel: '', variableReference: '', variableReferenceLabel: '', + optionFilter: '', choiceType: '', isCollected: '1', alternativeLabel: '', @@ -58,6 +60,7 @@ export function formToState(form, codesListsStore = {}) { codeListReferenceLabel, variableReference, variableReferenceLabel, + optionFilter, choiceType, isCollected, alternativeLabel, @@ -83,6 +86,7 @@ export function formToState(form, codesListsStore = {}) { codeListReferenceLabel, variableReference, variableReferenceLabel: resolvedVariableReferenceLabel, + optionFilter, choiceType, }; } @@ -119,6 +123,7 @@ export function storeToForm(currentStore) { codeListReferenceLabel, variableReference, variableReferenceLabel, + optionFilter, choiceType, } = currentStore[key]; return { @@ -137,6 +142,7 @@ export function storeToForm(currentStore) { codeListReferenceLabel, variableReference, variableReferenceLabel, + optionFilter, choiceType, }; }); diff --git a/legacy/src/model/formToState/component-new-edit/response-format-table.jsx b/legacy/src/model/formToState/component-new-edit/response-format-table.jsx index ec1c844f5..a4aa4245c 100644 --- a/legacy/src/model/formToState/component-new-edit/response-format-table.jsx +++ b/legacy/src/model/formToState/component-new-edit/response-format-table.jsx @@ -114,6 +114,7 @@ export const defaultMeasureForm = { name: '', label: '', scope: '', + optionFilter: '', }, visHint: RADIO, choiceType: CHOICE_TYPE_CODES_LIST, @@ -506,7 +507,10 @@ const Factory = (initialState = {}, codesListsStore) => { } state[SINGLE_CHOICE] = { - [listPath]: codesListsStore[measureState[listPath].id], + [listPath]: { + ...codesListsStore[measureState[listPath].id], + optionFilter: measureState[listPath].optionFilter, + }, visHint: measureState.visHint, choiceType: measureState.choiceType, }; diff --git a/legacy/src/model/formToState/lists/utils.js b/legacy/src/model/formToState/lists/utils.js index fe3d5aec5..dbb91dea4 100644 --- a/legacy/src/model/formToState/lists/utils.js +++ b/legacy/src/model/formToState/lists/utils.js @@ -9,10 +9,10 @@ export function createListFactory( currentState = {}, ) { if (codesListsStore && currentState.id) { - currentState = merge( - cloneDeep(defaultState), - codesListsStore[currentState.id], - ); + currentState = merge(cloneDeep(defaultState), { + ...codesListsStore[currentState.id], + optionFilter: currentState.optionFilter, + }); } else { currentState = cloneDeep(defaultState); } diff --git a/legacy/src/model/formToState/lists/variables.js b/legacy/src/model/formToState/lists/variables.js index ae4588dc1..9120ed3c5 100644 --- a/legacy/src/model/formToState/lists/variables.js +++ b/legacy/src/model/formToState/lists/variables.js @@ -5,16 +5,18 @@ export const defaultState = { label: '', name: '', scope: '', + optionFilter: '', }; export function formToState(form) { - const { id, label, name, scope } = form; + const { id, label, name, scope, optionFilter } = form; return { id, name, label, scope, + optionFilter, }; } @@ -23,12 +25,14 @@ export function stateComponentToForm({ name = '', label = '', scope = '', + optionFilter = '', }) { return { id, name, label, scope, + optionFilter, }; } diff --git a/legacy/src/model/transformations/response-format-single.spec.ts b/legacy/src/model/transformations/response-format-single.spec.ts index 22ca52ba4..6d5941e5b 100644 --- a/legacy/src/model/transformations/response-format-single.spec.ts +++ b/legacy/src/model/transformations/response-format-single.spec.ts @@ -82,6 +82,7 @@ describe('response format single', () => { }, CollectedVariableReference: 'my-var-id', VariableReference: 'my-variable-id', + optionFilter: 'nice filter', mandatory: true, choiceType: CHOICE_TYPE.VARIABLE, }, @@ -89,7 +90,7 @@ describe('response format single', () => { }; const output = remoteToState(remote); const expected = { - Variable: { id: 'my-variable-id' }, + Variable: { id: 'my-variable-id', optionFilter: 'nice filter' }, id: 'my-response-id', mandatory: true, choiceType: 'VARIABLE', @@ -167,7 +168,7 @@ describe('stateToRemote', () => { mandatory: true, visHint: DATATYPE_VIS_HINT.RADIO, choiceType: CHOICE_TYPE.VARIABLE, - Variable: { id: 'my-variable-id' }, + Variable: { id: 'my-variable-id', optionFilter: 'nice filter' }, }; const collectedVariables = ['my-var-id']; const output = stateToRemote(state, collectedVariables); @@ -182,6 +183,7 @@ describe('stateToRemote', () => { typeName: 'TEXT', visualizationHint: 'RADIO', }, + optionFilter: 'nice filter', choiceType: 'VARIABLE', id: 'my-response-id', mandatory: true, diff --git a/legacy/src/model/transformations/response-format-single.ts b/legacy/src/model/transformations/response-format-single.ts index 052fe8e2d..2deb003d0 100644 --- a/legacy/src/model/transformations/response-format-single.ts +++ b/legacy/src/model/transformations/response-format-single.ts @@ -14,6 +14,7 @@ type RemoteResponseFormatSingle = { id: string; CodeListReference?: unknown; VariableReference?: unknown; + optionFilter?: string; choiceType?: | CHOICE_TYPE.CODE_LIST | CHOICE_TYPE.SUGGESTER @@ -50,7 +51,7 @@ export type StateResponseFormatSingle = { | DATATYPE_VIS_HINT.CHECKBOX | DATATYPE_VIS_HINT.RADIO | DATATYPE_VIS_HINT.DROPDOWN; - [DEFAULT_VARIABLE_SELECTOR_PATH]: { id: string }; + [DEFAULT_VARIABLE_SELECTOR_PATH]: { id: string; optionFilter?: string }; } ); @@ -66,6 +67,7 @@ export function remoteToState(remote: { CodeListReference, VariableReference, id, + optionFilter, }, ], } = remote; @@ -94,7 +96,10 @@ export function remoteToState(remote: { if (choiceType === CHOICE_TYPE.VARIABLE) { return { ...baseState, - [DEFAULT_VARIABLE_SELECTOR_PATH]: { id: VariableReference as string }, + [DEFAULT_VARIABLE_SELECTOR_PATH]: { + id: VariableReference as string, + optionFilter: optionFilter, + }, visHint, }; } @@ -117,6 +122,7 @@ export function stateToRemote( let nomenclatureId; let codesListId; let variableReferenceId; + let optionFilter; if ( choiceType === CHOICE_TYPE.SUGGESTER && @@ -128,6 +134,7 @@ export function stateToRemote( DEFAULT_VARIABLE_SELECTOR_PATH in state ) { variableReferenceId = state[DEFAULT_VARIABLE_SELECTOR_PATH]?.id; + optionFilter = state[DEFAULT_VARIABLE_SELECTOR_PATH]?.optionFilter; } else if ( choiceType === CHOICE_TYPE.CODE_LIST && DEFAULT_CODES_LIST_SELECTOR_PATH in state @@ -145,6 +152,7 @@ export function stateToRemote( codesListId, nomenclatureId, variableReferenceId, + optionFilter, typeName: DATATYPE_NAME.TEXT, maxLength: 249, collectedVariable: collectedVariables[0], diff --git a/legacy/src/model/transformations/response-format-table.jsx b/legacy/src/model/transformations/response-format-table.jsx index 8655b08c8..c689ed92d 100644 --- a/legacy/src/model/transformations/response-format-table.jsx +++ b/legacy/src/model/transformations/response-format-table.jsx @@ -211,6 +211,7 @@ function remoteToStateMeasure(remote) { response: { CodeListReference, VariableReference, + optionFilter, Datatype, conditionFilter, conditionReadOnly, @@ -232,7 +233,13 @@ function remoteToStateMeasure(remote) { state.type = SINGLE_CHOICE; state[SINGLE_CHOICE] = ResponseFormatSingle.remoteToState({ responses: [ - { Datatype, VariableReference, choiceType, CollectedVariableReference }, + { + Datatype, + VariableReference, + choiceType, + optionFilter, + CollectedVariableReference, + }, ], }); } else { @@ -370,6 +377,8 @@ function stateToResponseState(state, primaryType) { measureTypeState[DEFAULT_NOMENCLATURE_SELECTOR_PATH]?.id; const variableReferenceId = measureTypeState[DEFAULT_VARIABLE_SELECTOR_PATH]?.id; + const optionFilter = + measureTypeState[DEFAULT_VARIABLE_SELECTOR_PATH]?.optionFilter; responseState = { ...responseState, @@ -377,6 +386,7 @@ function stateToResponseState(state, primaryType) { codesListId, nomenclatureId, variableReferenceId, + optionFilter, typeName: TEXT, maxLength: 249, visHint, diff --git a/legacy/src/model/transformations/response-format-table.spec.jsx b/legacy/src/model/transformations/response-format-table.spec.jsx index 9f3837cd4..4371fb733 100644 --- a/legacy/src/model/transformations/response-format-table.spec.jsx +++ b/legacy/src/model/transformations/response-format-table.spec.jsx @@ -1,7 +1,11 @@ import { describe, expect, it } from 'vitest'; +import { CHOICE_TYPE } from '@/constants/pogues-constants'; + import { remoteToState, stateToRemote } from './response-format-table'; +const { CODE_LIST, SUGGESTER, VARIABLE } = CHOICE_TYPE; + describe('remoteToState', () => { it('should use an offset equal to 1', () => { const remote = { @@ -230,7 +234,7 @@ describe('remoteToState', () => { type: 'TextDatatypeType', MaxLength: 1, }, - choiceType: 'SUGGESTER', + choiceType: SUGGESTER, CollectedVariableReference: 'mbz5jjxv', }, { @@ -242,7 +246,20 @@ describe('remoteToState', () => { type: 'TextDatatypeType', MaxLength: 1, }, - choiceType: 'CODE_LIST', + choiceType: CODE_LIST, + CollectedVariableReference: 'mbz5crwv', + }, + { + id: 'mbh8ke14', + VariableReference: 'kdyg5y7', + optionFilter: 'nice filter', + Datatype: { + typeName: 'TEXT', + visualizationHint: 'RADIO', + type: 'TextDatatypeType', + MaxLength: 1, + }, + choiceType: VARIABLE, CollectedVariableReference: 'mbz5crwv', }, ], @@ -260,7 +277,11 @@ describe('remoteToState', () => { }, { dimensionType: 'MEASURE', - Label: 'measure-radio', + Label: 'measure-codeList-radio', + }, + { + dimensionType: 'MEASURE', + Label: 'measure-variable-radio', }, ], }; @@ -283,7 +304,7 @@ describe('remoteToState', () => { id: undefined, mandatory: undefined, visHint: 'SUGGESTER', - choiceType: 'SUGGESTER', + choiceType: SUGGESTER, }, label: 'measure-suggester', type: 'SINGLE_CHOICE', @@ -294,9 +315,20 @@ describe('remoteToState', () => { id: undefined, mandatory: undefined, visHint: 'RADIO', - choiceType: 'CODE_LIST', + choiceType: CODE_LIST, + }, + label: 'measure-codeList-radio', + type: 'SINGLE_CHOICE', + }, + { + SINGLE_CHOICE: { + Variable: { id: 'kdyg5y7', optionFilter: 'nice filter' }, + id: undefined, + mandatory: undefined, + visHint: 'RADIO', + choiceType: VARIABLE, }, - label: 'measure-radio', + label: 'measure-variable-radio', type: 'SINGLE_CHOICE', }, ], diff --git a/legacy/src/model/transformations/response.spec.ts b/legacy/src/model/transformations/response.spec.ts index 8febe53ee..9c286524f 100644 --- a/legacy/src/model/transformations/response.spec.ts +++ b/legacy/src/model/transformations/response.spec.ts @@ -81,6 +81,22 @@ describe('response tranformations', () => { expect(result.VariableReference).toEqual(variableReferenceId); }); + test('when choiceType is a variable, with a defined optionFilter', () => { + const typeName = DATATYPE_NAME.TEXT; + const optionFilter = 'nice filter'; + + const result = stateToRemote({ + id: '1', + typeName, + variableReferenceId: 'variableId', + visHint: DATATYPE_VIS_HINT.RADIO, + choiceType: CHOICE_TYPE.VARIABLE, + optionFilter: optionFilter, + }); + + expect(result.optionFilter).toEqual(optionFilter); + }); + test('when visHint is a suggester, with a defined nomenclatureId', () => { const typeName = DATATYPE_NAME.DATE; const nomenclatureId = 'nomenclatureId'; diff --git a/legacy/src/model/transformations/response.ts b/legacy/src/model/transformations/response.ts index b5ea5e2fd..4cda4be97 100644 --- a/legacy/src/model/transformations/response.ts +++ b/legacy/src/model/transformations/response.ts @@ -38,6 +38,7 @@ export function stateToRemote( codesListId, nomenclatureId, variableReferenceId, + optionFilter, choiceType, allowArbitraryResponse, visHint: visualizationHint, @@ -71,6 +72,7 @@ export function stateToRemote( case CHOICE_TYPE.VARIABLE: model.VariableReference = variableReferenceId; + model.optionFilter = optionFilter; break; default: diff --git a/legacy/src/model/transformations/types.ts b/legacy/src/model/transformations/types.ts index c916c9bc8..7e16c6d9c 100644 --- a/legacy/src/model/transformations/types.ts +++ b/legacy/src/model/transformations/types.ts @@ -30,6 +30,7 @@ export type StateResponse = { codesListId?: string; nomenclatureId?: string; variableReferenceId?: string; + optionFilter?: string; allowArbitraryResponse?: unknown; visHint?: DATATYPE_VIS_HINT; choiceType?: @@ -67,6 +68,7 @@ export type RemoteResponse = { CollectedVariableReference?: string; CodeListReference?: unknown; VariableReference?: unknown; + optionFilter?: string; choiceType?: | CHOICE_TYPE.CODE_LIST | CHOICE_TYPE.VARIABLE diff --git a/legacy/src/utils/variables/collected-variables-single.js b/legacy/src/utils/variables/collected-variables-single.js index 25b45343a..c1852721a 100644 --- a/legacy/src/utils/variables/collected-variables-single.js +++ b/legacy/src/utils/variables/collected-variables-single.js @@ -36,6 +36,7 @@ export function getCollectedVariablesSingle( variableReference: form.Variable.id, // We need to dynamically get the label of the variable reference, as it can be changed by the user variableReferenceLabel: desiredVarLabel, + optionFilter: form.Variable.optionFilter, type: TEXT, choiceType: form.choiceType, [TEXT]: { maxLength: 249 }, diff --git a/legacy/src/utils/variables/collected-variables-single.spec.js b/legacy/src/utils/variables/collected-variables-single.spec.js index 5f81b841e..0387c2bd3 100644 --- a/legacy/src/utils/variables/collected-variables-single.spec.js +++ b/legacy/src/utils/variables/collected-variables-single.spec.js @@ -314,6 +314,7 @@ describe('getCollectedVariablesSingle', () => { label: 'my-variableRef', name: 'my-variableRef-id', scope: 'my-variableScope', + optionFilter: 'nice filter', }, visHint: 'VARIABLE', choiceType: 'VARIABLE', @@ -327,6 +328,7 @@ describe('getCollectedVariablesSingle', () => { label: 'MY_Q label', variableReference: 'my-variableRef-id', variableReferenceLabel: 'my-variableRef', + optionFilter: 'nice filter', choiceType: 'VARIABLE', type: 'TEXT', TEXT: { maxLength: 249 }, diff --git a/legacy/src/utils/variables/collected-variables-table.js b/legacy/src/utils/variables/collected-variables-table.js index ed156f413..45cf0b2e7 100644 --- a/legacy/src/utils/variables/collected-variables-table.js +++ b/legacy/src/utils/variables/collected-variables-table.js @@ -171,6 +171,10 @@ export function getReponsesValues(measure) { choiceType === CHOICE_TYPE.VARIABLE ? measure[SINGLE_CHOICE][listPath].label : '', + optionFilter: + choiceType === CHOICE_TYPE.VARIABLE + ? measure[SINGLE_CHOICE][listPath].optionFilter + : '', }; } default: diff --git a/legacy/src/utils/variables/collected-variables-table.spec.js b/legacy/src/utils/variables/collected-variables-table.spec.js index a23140583..adbf34d2c 100644 --- a/legacy/src/utils/variables/collected-variables-table.spec.js +++ b/legacy/src/utils/variables/collected-variables-table.spec.js @@ -391,6 +391,7 @@ describe('getReponsesValues', () => { codeListReference: 'mbqae1u1', codeListReferenceLabel: 'oui_non', variableReferenceLabel: '', + optionFilter: '', choiceType: 'CODE_LIST', type: 'TEXT', TEXT: { maxLength: 249 }, @@ -423,6 +424,7 @@ describe('getReponsesValues', () => { label: 'my-variable-label', name: 'my-variable-name', scope: 'my-variable-scope', + optionFilter: 'nice filter', }, }, }; @@ -431,6 +433,7 @@ describe('getReponsesValues', () => { codeListReferenceLabel: '', variableReference: 'my-variable-id', variableReferenceLabel: 'my-variable-label', + optionFilter: 'nice filter', choiceType: 'VARIABLE', type: 'TEXT', TEXT: { maxLength: 249 }, @@ -465,6 +468,7 @@ describe('getReponsesValues', () => { codeListReference: 'mbqae1u1', codeListReferenceLabel: 'oui_non', variableReferenceLabel: '', + optionFilter: '', choiceType: 'CODE_LIST', type: 'TEXT', TEXT: { maxLength: 249 }, diff --git a/legacy/src/widgets/component-new-edit/components/response-format/single/response-format-single-variable.jsx b/legacy/src/widgets/component-new-edit/components/response-format/single/response-format-single-variable.jsx index 52da74067..23c61c9ee 100644 --- a/legacy/src/widgets/component-new-edit/components/response-format/single/response-format-single-variable.jsx +++ b/legacy/src/widgets/component-new-edit/components/response-format/single/response-format-single-variable.jsx @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { Field, FormSection } from 'redux-form'; import { formValueSelector } from 'redux-form'; +import { InputWithVariableAutoCompletion } from '@/forms/controls/control-with-suggestions'; import ListRadios from '@/forms/controls/list-radios'; import Select from '@/forms/controls/select'; @@ -58,6 +59,16 @@ function ResponseFormatSingleVariable({ scope={selectedScope} /> + + + + ({ + InputWithVariableAutoCompletion: (props) => , +})); + vi.mock('@/forms/controls/select', () => ({ default: ({ children, label, name, onChange }) => (