Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a1b40a2
Enhance input visibility logic and add new helper functions
projkov Jan 31, 2026
c3bb5f9
Refactor input handling and enhance test coverage
projkov Jan 31, 2026
0b17e40
Enhance input serializer and entity to support conditional display
projkov Jan 31, 2026
8d004a6
Add conditional input group to demo suite
projkov Jan 31, 2026
098bda9
Initialize Bundler in inferno script
projkov Jan 31, 2026
87e3a29
Remove yarn.lock file from the project
projkov Jan 31, 2026
983a165
Fix lint errors in the client
projkov Feb 1, 2026
01b1a04
Refactor input visibility logic to use `enable_when` instead of `show…
projkov Feb 2, 2026
bbf9490
Change conditional input group in demo suite
projkov Feb 2, 2026
e1913e4
Refactor input handling and enhance conditional input tests
projkov Feb 6, 2026
1de06b6
Remove Bundler setup requirement from the inferno script
projkov Feb 6, 2026
77fff3b
Update normalizeValue function and add comprehensive tests
projkov Feb 6, 2026
ebf97db
Merge branch 'main' into conditional-rendering-of-modal-inputs
projkov May 13, 2026
4cf6082
Remove unused sortAndNormalizeArray function from InputHelpers.ts
projkov May 13, 2026
f81cbaf
Add test to cover dependent visibility for checkbox
projkov May 13, 2026
a3cd415
Update conditionalShowInput documentation for clarity and accuracy
projkov May 13, 2026
7810955
Remove code duplication in Inputs.test.tsx
projkov May 13, 2026
33b30e3
Enhance InputHelpers with array sorting in normalization and add cons…
projkov May 13, 2026
a27cff2
Lint auto fixes
projkov May 20, 2026
aaaf1e2
Lint fixes
projkov May 20, 2026
323298d
Remove console warnings for missing inputs in conditionalShowInput an…
projkov May 20, 2026
73af7d3
Update conditionalShowInput to account for checkbox type and remove u…
projkov May 20, 2026
e7fba3c
Remove console warning check for missing inputs in conditionalShowInp…
projkov May 20, 2026
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
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ PLATFORMS
arm64-darwin-22
arm64-darwin-23
arm64-darwin-24
arm64-darwin-25
x86_64-darwin-20
x86_64-darwin-22
x86_64-darwin-23
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/InputsModal/InputFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import InputOAuthCredentials from '~/components/InputsModal/InputOAuthCredential
import InputRadioGroup from '~/components/InputsModal/InputRadioGroup';
import InputSingleCheckbox from '~/components/InputsModal/InputSingleCheckbox';
import InputTextField from '~/components/InputsModal/InputTextField';
import { showInput } from './InputHelpers';

export interface InputFieldsProps {
inputs: TestInput[];
Expand All @@ -19,7 +20,7 @@ const InputFields: FC<InputFieldsProps> = ({ inputs, inputsMap, setInputsMap })
return (
<List>
{inputs.map((input: TestInput, index: number) => {
if (!input.hidden) {
if (showInput(input, inputsMap)) {
switch (input.type) {
case 'auth_info':
if (input.options?.mode === 'auth') {
Expand Down
65 changes: 65 additions & 0 deletions client/src/components/InputsModal/InputHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,68 @@ export const isJsonString = (str: unknown) => {
}
return !!value && typeof value === 'object';
};

/**
* Converts a value to its string representation.
* - If value is null or undefined, returns an empty string.
* - Otherwise, returns the string representation of the value.
*
* @param value - The value to normalize.
* @returns The normalized string value.
*/
export const normalizeValue = (value: unknown): string => {
if (value === null) {
return '';
}
switch (typeof value) {
case 'string':
return value;
case 'number':
case 'boolean':
case 'bigint':
case 'symbol':
return String(value);
case 'object':
return JSON.stringify(value);
default:
return '';
}
};

/**
* Sorts and normalizes an array of values.
* - Sorts the array and returns a normalized string representation of each element.
*
* @param value - The value to sort and normalize.
* @returns The sorted and normalized array of strings.
*/
const sortAndNormalizeArray = (value: unknown[]): string[] => {
return value.sort().map((item) => normalizeValue(item));
};
Comment thread
projkov marked this conversation as resolved.
Outdated

/**
* Returns true if the input field should be displayed based on its `enable_when`
* condition and the values in inputsMap.
* - No `enable_when` → always show.
* - Referenced input missing or no `input_name` → hide.
* - When `enable_when.value` is a string: show when referenced value equals it.
* - When `enable_when.value` is string[]: show when referenced value equals any element.
*/
export const conditionalShowInput = (
input: TestInput,
inputsMap: Map<string, unknown>,
): boolean => {
const enableWhen = input.enable_when;
if (!enableWhen?.input_name) {
return true;
}
const inputValue = inputsMap.get(enableWhen.input_name);
if (inputValue === undefined) {
return false;
}
return normalizeValue(inputValue) === normalizeValue(enableWhen.value);
};

export const showInput = (input: TestInput, inputsMap: Map<string, unknown>): boolean => {
return !input.hidden && conditionalShowInput(input, inputsMap);
};
54 changes: 54 additions & 0 deletions client/src/components/InputsModal/__tests__/InputHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import { normalizeValue } from '~/components/InputsModal/InputHelpers';

describe('normalizeValue', () => {
it('returns empty string for null', () => {
expect(normalizeValue(null)).toBe('');
});

it('returns empty string for undefined', () => {
expect(normalizeValue(undefined)).toBe('');
});

it('returns string unchanged', () => {
expect(normalizeValue('')).toBe('');
expect(normalizeValue('hello')).toBe('hello');
expect(normalizeValue('4.0')).toBe('4.0');
});

it('converts number to string', () => {
expect(normalizeValue(0)).toBe('0');
expect(normalizeValue(42)).toBe('42');
expect(normalizeValue(-1)).toBe('-1');
expect(normalizeValue(3.14)).toBe('3.14');
});

it('converts boolean to string', () => {
expect(normalizeValue(true)).toBe('true');
expect(normalizeValue(false)).toBe('false');
});

it('converts bigint to string', () => {
expect(normalizeValue(BigInt(0))).toBe('0');
expect(normalizeValue(BigInt(9007199254740991))).toBe('9007199254740991');
});

it('converts symbol to string', () => {
const sym = Symbol('test');
expect(normalizeValue(sym)).toBe(sym.toString());
});

it('JSON-stringifies plain objects', () => {
expect(normalizeValue({})).toBe('{}');
expect(normalizeValue({ a: 1, b: 'x' })).toBe('{"a":1,"b":"x"}');
});

it('JSON-stringifies arrays', () => {
expect(normalizeValue([])).toBe('[]');
expect(normalizeValue([1, 'a', true])).toBe('[1,"a",true]');
});

it('returns empty string for function (default case)', () => {
expect(normalizeValue(() => {})).toBe('');
});
});
138 changes: 138 additions & 0 deletions client/src/components/InputsModal/__tests__/Inputs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import InputTextField from '~/components/InputsModal/InputTextField';
import { isJsonString } from '~/components/InputsModal/InputHelpers';
import ThemeProvider from '~/components/ThemeProvider';
import InputFields from '../InputFields';

describe('Input Components', () => {
it('renders InputCheckboxGroup', () => {
Expand Down Expand Up @@ -180,4 +181,141 @@
expect(isJsonString(string)).toEqual(true);
});
});

describe('renders conditional fields correctly', () => {
const constructInput = (props: Partial<TestInput>): TestInput => {
return {
name: props?.name || '',
type: (props?.type || 'text') as TestInput['type'],
title: props?.title,
optional: props?.optional,
enable_when: props?.enable_when,
hidden: props?.hidden,
value: props?.value,
};
};

const constructInputsMap = (inputs: TestInput[]): Map<string, string> => {
return inputs.reduce((acc, input) => {
acc.set(input.name, input.value as string);
return acc;
}, new Map<string, string>());
};

const renderInputFields = (inputs: TestInput[], inputsMap: Map<string, string>) => {
render(
<ThemeProvider>
<SnackbarProvider>
<InputFields inputs={inputs} inputsMap={inputsMap} setInputsMap={() => {}} />
</SnackbarProvider>
</ThemeProvider>,
);
};

const assertInputVisibility = (inputs: Partial<TestInput>[], expectedVisible: boolean[]) => {
const constructedInputs: TestInput[] = inputs.map((input) => constructInput(input));
const inputsMap = constructInputsMap(constructedInputs);
renderInputFields(constructedInputs, inputsMap);

constructedInputs.forEach((input, index) => {
const label = input.title as string;
const shouldBeVisible = expectedVisible[index];
if (shouldBeVisible) {
expect(screen.getByLabelText(label, { exact: false })).toBeVisible();
} else {
expect(screen.queryByLabelText(label, { exact: false })).not.toBeInTheDocument();
}
});
};

it('renders field when it has no enable_when (always visible)', () => {
assertInputVisibility([{ name: 'standalone', title: 'Standalone field' }], [true]);
});

it('skips rendering dependent field when controlling value is undefined', () => {
assertInputVisibility(
[
{ name: 'trigger', title: 'Trigger' },
{
name: 'dependent',
title: 'Dependent',
enable_when: { input_name: 'trigger', value: 'yes' },
},
],
[true, false],
);
});

it('skips rendering when controlling value does not match enable_when', () => {
assertInputVisibility(
[
{ name: 'trigger', title: 'Trigger', value: 'no' },
{
name: 'dependent',
title: 'Dependent',
enable_when: { input_name: 'trigger', value: 'yes' },
},
],
[true, false],
);
});

it('renders dependent field when controlling value matches enable_when', () => {
assertInputVisibility(
[
{ name: 'trigger', title: 'Trigger', value: 'yes' },
{
name: 'dependent',
title: 'Dependent',
enable_when: { input_name: 'trigger', value: 'yes' },
},
],
[true, true],
);
});

it('hides field when hidden is true even if enable_when would pass', () => {
assertInputVisibility(
[
{ name: 'trigger', title: 'Trigger', value: 'yes' },
{
name: 'dependent',
title: 'Dependent',
enable_when: { input_name: 'trigger', value: 'yes' },
hidden: true,
},
],
[true, false],
);
});

it('renders dependent field when controlling value (array) equals enable_when array value', () => {
const refValue = ['a', 'b'];
const inputs: TestInput[] = [
constructInput({
name: 'trigger',
title: 'Trigger',
type: 'checkbox',
value: refValue,
}),
constructInput({
name: 'dependent',
title: 'Dependent',
enable_when: { input_name: 'trigger', value: ['a', 'b'] },

Check failure on line 304 in client/src/components/InputsModal/__tests__/Inputs.test.tsx

View workflow job for this annotation

GitHub Actions / lint (20.x)

Type 'string[]' is not assignable to type 'string'.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the value be a string rep of the Array? Or is use with checkboxes disallowed?

}),
];
const inputsMap = new Map<string, unknown>();
inputsMap.set('trigger', refValue);
render(
<ThemeProvider>
<SnackbarProvider>
<InputFields inputs={inputs} inputsMap={inputsMap} setInputsMap={() => {}} />
</SnackbarProvider>
</ThemeProvider>,
);

expect(screen.getByRole('checkbox', { name: /Trigger/i })).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: /Dependent/i })).toBeVisible();
});
});
});
4 changes: 4 additions & 0 deletions client/src/models/testSuiteModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ export interface TestInput {
list_options?: InputOption[];
mode?: string;
};
enable_when?: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please propose updated Inferno documentation in this section (rendered here) for this input feature.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input_name: string;
value: string;
};
}

export interface TestOutput {
Expand Down
45 changes: 45 additions & 0 deletions dev_suites/dev_demo_ig_stu1/demo_suite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -417,5 +417,50 @@ class DemoSuite < Inferno::TestSuite
end
end
end

group do
id 'conditional_group'
title 'Conditional Inputs Group'
optional

def list_options
[
{ value: 'copy_paste', label: 'Paste JSON' },
{ value: 'url', label: 'URL to FHIR Bundle' },
{ value: 'summary_op', label: '$summary Operation' }
]
end

test 'Conditional, optional, empty input test (Radio)' do
input :get_type_radio, title: 'How to get Bundle (Radio)', type: 'radio', options: {
list_options: list_options
}
input :bundle_copy_paste_radio, title: 'Paste JSON (Radio)', type: 'textarea', optional: true,
enable_when: { input_name: 'get_type_radio', value: 'copy_paste' }
input :bundle_url_radio, title: 'URL to FHIR Bundle (Radio)', type: 'text', optional: true,
enable_when: { input_name: 'get_type_radio', value: 'url' }
input :fhir_server_url_radio, title: 'FHIR Server URL (Radio)', type: 'text', optional: true,
enable_when: { input_name: 'get_type_radio', value: 'summary_op' }
input :patient_identifier_radio, title: 'Patient ID (Radio)', type: 'text', optional: true,
enable_when: { input_name: 'get_type_radio', value: 'summary_op' }

run { pass }
end

test 'Conditional, optional, empty input test (Select)' do
input :get_type_select, title: 'How to get Bundle (Select)', type: 'select', default: 'copy_paste', options: {
list_options: list_options
}
input :bundle_copy_paste_select, title: 'Paste JSON (Select)', type: 'textarea', optional: true,
enable_when: { input_name: 'get_type_select', value: 'copy_paste' }
input :bundle_url_select, title: 'URL to FHIR Bundle (Select)', type: 'text', optional: true,
enable_when: { input_name: 'get_type_select', value: 'url' }
input :fhir_server_url_select, title: 'FHIR Server URL (Select)', type: 'text', optional: true,
enable_when: { input_name: 'get_type_select', value: 'summary_op' }
input :patient_identifier_select, title: 'Patient ID (Select)', type: 'text', optional: true,
enable_when: { input_name: 'get_type_select', value: 'summary_op' }
run { pass }
end
end
end
end
1 change: 1 addition & 0 deletions lib/inferno/apps/web/serializers/input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Input < Serializer
field :locked, if: :field_present?
field :hidden, if: :field_present?
field :value, if: :field_present?
field :enable_when, if: :field_present?
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/inferno/dsl/input_output_handling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module InputOutputHandling
# @option input_params [Hash] :options Possible input option formats based on input type
# @option options [Array] :list_options Array of options for input formats
# that require a list of possible values (radio and checkbox)
# @option input_params [Hash] :enable_when Conditions for showing the input
# @return [void]
# @example
# input :patient_id, title: 'Patient ID', description: 'The ID of the patient being searched for',
Expand Down
Loading
Loading