diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a6f73b90..64fdfda13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -76,6 +76,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/component.jsx b/legacy/src/model/formToState/component-new-edit/component.jsx
index 007b01ff5..6b836daf7 100644
--- a/legacy/src/model/formToState/component-new-edit/component.jsx
+++ b/legacy/src/model/formToState/component-new-edit/component.jsx
@@ -58,6 +58,7 @@ export function formToState(form, transformers) {
occurrenceDescription,
locked,
codeFilters = [],
+ optionFilter,
} = form;
let newName;
@@ -107,6 +108,7 @@ export function formToState(form, transformers) {
occurrenceDescription: occurrenceDescription,
locked: locked,
codeFilters,
+ optionFilter,
};
if (res.responseFormat.type) {
transformers.codesList.formToComponentState(
@@ -140,6 +142,7 @@ export function stateToForm(currentState, transformers, activeQuestionnaire) {
locked,
selectedComponent,
codeFilters = [],
+ optionFilter,
} = currentState;
let target = '';
@@ -176,6 +179,7 @@ export function stateToForm(currentState, transformers, activeQuestionnaire) {
occurrenceDescription: occurrenceDescription || '',
locked: locked,
codeFilters,
+ optionFilter,
};
if (type === QUESTION) {
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/component.jsx b/legacy/src/model/transformations/component.jsx
index 47aa02622..6ab979d0d 100644
--- a/legacy/src/model/transformations/component.jsx
+++ b/legacy/src/model/transformations/component.jsx
@@ -352,6 +352,7 @@ function remoteToState(remote, componentGroup, codesListsStore) {
Locked: locked,
Loop: loop,
codeFilters: codeFilters,
+ OptionFilter: optionFilter,
sourceVariableReferences,
} = remote;
const redirectionClar =
@@ -387,6 +388,7 @@ function remoteToState(remote, componentGroup, codesListsStore) {
dynamiqueSpecified:
flowLogic && flowLogic === FILTER ? Filtres : Redirections,
codeFilters,
+ optionFilter,
};
if (genericName) {
state.label = label;
@@ -422,6 +424,7 @@ function remoteToState(remote, componentGroup, codesListsStore) {
scope,
mandatory,
sourceVariableReferences,
+ optionFilter,
);
state.collectedVariables =
CollectedVariable.remoteToComponentState(responseFinal);
diff --git a/legacy/src/model/transformations/response-format-single.spec.ts b/legacy/src/model/transformations/response-format-single.spec.ts
index 22ca52ba4..267f8b785 100644
--- a/legacy/src/model/transformations/response-format-single.spec.ts
+++ b/legacy/src/model/transformations/response-format-single.spec.ts
@@ -86,10 +86,11 @@ describe('response format single', () => {
choiceType: CHOICE_TYPE.VARIABLE,
},
],
+ optionFilter: 'nice filter',
};
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);
@@ -187,6 +188,7 @@ describe('stateToRemote', () => {
mandatory: true,
},
],
+ optionFilter: 'nice filter',
};
expect(output).toEqual(expected);
});
diff --git a/legacy/src/model/transformations/response-format-single.ts b/legacy/src/model/transformations/response-format-single.ts
index 052fe8e2d..6c6ca6dd8 100644
--- a/legacy/src/model/transformations/response-format-single.ts
+++ b/legacy/src/model/transformations/response-format-single.ts
@@ -50,12 +50,13 @@ 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 };
}
);
export function remoteToState(remote: {
responses: RemoteResponseFormatSingle;
+ optionFilter?: string;
}): StateResponseFormatSingle {
const {
responses: [
@@ -68,6 +69,7 @@ export function remoteToState(remote: {
id,
},
],
+ optionFilter,
} = remote;
const baseState = {
@@ -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,
+ },
visHint,
};
}
@@ -111,12 +116,13 @@ export function remoteToState(remote: {
export function stateToRemote(
state: StateResponseFormatSingle,
collectedVariables: string[],
-): { Response: RemoteResponseFormatSingle } {
+): { Response: RemoteResponseFormatSingle; optionFilter?: string } {
const { allowArbitraryResponse, visHint, mandatory, id, choiceType } = state;
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
@@ -150,5 +157,6 @@ export function stateToRemote(
collectedVariable: collectedVariables[0],
}),
],
+ optionFilter,
};
}
diff --git a/legacy/src/model/transformations/response-format-table.jsx b/legacy/src/model/transformations/response-format-table.jsx
index 8655b08c8..38b6de3e6 100644
--- a/legacy/src/model/transformations/response-format-table.jsx
+++ b/legacy/src/model/transformations/response-format-table.jsx
@@ -232,7 +232,12 @@ function remoteToStateMeasure(remote) {
state.type = SINGLE_CHOICE;
state[SINGLE_CHOICE] = ResponseFormatSingle.remoteToState({
responses: [
- { Datatype, VariableReference, choiceType, CollectedVariableReference },
+ {
+ Datatype,
+ VariableReference,
+ choiceType,
+ CollectedVariableReference,
+ },
],
});
} else {
diff --git a/legacy/src/model/transformations/response-format-table.spec.jsx b/legacy/src/model/transformations/response-format-table.spec.jsx
index 9f3837cd4..5ea3cb8eb 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,19 @@ describe('remoteToState', () => {
type: 'TextDatatypeType',
MaxLength: 1,
},
- choiceType: 'CODE_LIST',
+ choiceType: CODE_LIST,
+ CollectedVariableReference: 'mbz5crwv',
+ },
+ {
+ id: 'mbh8ke14',
+ VariableReference: 'kdyg5y7',
+ Datatype: {
+ typeName: 'TEXT',
+ visualizationHint: 'RADIO',
+ type: 'TextDatatypeType',
+ MaxLength: 1,
+ },
+ choiceType: VARIABLE,
CollectedVariableReference: 'mbz5crwv',
},
],
@@ -260,7 +276,11 @@ describe('remoteToState', () => {
},
{
dimensionType: 'MEASURE',
- Label: 'measure-radio',
+ Label: 'measure-codeList-radio',
+ },
+ {
+ dimensionType: 'MEASURE',
+ Label: 'measure-variable-radio',
},
],
};
@@ -283,7 +303,7 @@ describe('remoteToState', () => {
id: undefined,
mandatory: undefined,
visHint: 'SUGGESTER',
- choiceType: 'SUGGESTER',
+ choiceType: SUGGESTER,
},
label: 'measure-suggester',
type: 'SINGLE_CHOICE',
@@ -294,9 +314,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' },
+ 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-format.jsx b/legacy/src/model/transformations/response-format.jsx
index 8022819a5..e3201e099 100644
--- a/legacy/src/model/transformations/response-format.jsx
+++ b/legacy/src/model/transformations/response-format.jsx
@@ -18,13 +18,18 @@ export function remoteToState(
mandatory,
/** Add source variable references to response format if the response format is pairing. */
sourceVariableReferences,
+ /** filter for modalities based on a variable. Only for unique choice questions */
+ optionFilter = '',
) {
let datatypeState = {};
if (type === SIMPLE) {
datatypeState = ResponseFormatSimple.remoteToState({ responses });
} else if (type === SINGLE_CHOICE) {
- datatypeState = ResponseFormatSingle.remoteToState({ responses });
+ datatypeState = ResponseFormatSingle.remoteToState({
+ responses,
+ optionFilter,
+ });
} else if (type === MULTIPLE_CHOICE) {
datatypeState = ResponseFormatMultiple.remoteToState({
responses,
@@ -72,6 +77,7 @@ export function stateToRemote(
collectedVariables,
);
remote.Response = dataTypeRemote.Response;
+ remote.OptionFilter = dataTypeRemote.optionFilter;
} else if (type === MULTIPLE_CHOICE) {
dataTypeRemote = ResponseFormatMultiple.stateToRemote(
responseFormatState,
diff --git a/legacy/src/model/transformations/types.ts b/legacy/src/model/transformations/types.ts
index c916c9bc8..02af07677 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?:
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/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..6bc78858a 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';
@@ -28,6 +29,7 @@ function ResponseFormatSingleVariable({
selectedScope,
componentsStore,
externalLoopsStore,
+ allowFilter = false,
}) {
const scopes = getQuestionnaireScope(componentsStore, externalLoopsStore);
@@ -58,6 +60,18 @@ function ResponseFormatSingleVariable({
scope={selectedScope}
/>
+ {allowFilter && (
+
+
+
+ )}
+
{
diff --git a/legacy/src/widgets/component-new-edit/components/response-format/single/response-format-single-variable.spec.jsx b/legacy/src/widgets/component-new-edit/components/response-format/single/response-format-single-variable.spec.jsx
index f6266c962..5baeeacb3 100644
--- a/legacy/src/widgets/component-new-edit/components/response-format/single/response-format-single-variable.spec.jsx
+++ b/legacy/src/widgets/component-new-edit/components/response-format/single/response-format-single-variable.spec.jsx
@@ -6,9 +6,19 @@ import { combineReducers, createStore } from 'redux';
import { reducer as formReducer, reduxForm } from 'redux-form';
import { beforeEach, describe, expect, it, vi } from 'vitest';
+import Input from '@/forms/controls/input';
+
import Dictionary from '../../../../../utils/dictionary/dictionary';
import ResponseFormatSingleVariable from './response-format-single-variable';
+/*
+ Mock InputWithVariableAutoCompletion as the classic Input,
+ since there are errors when importing antlr-editor in tests
+ */
+vi.mock('@/forms/controls/control-with-suggestions', () => ({
+ InputWithVariableAutoCompletion: (props) => ,
+}));
+
vi.mock('@/forms/controls/select', () => ({
default: ({ children, label, name, onChange }) => (