diff --git a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml index 0f5edf612..0af3b8189 100644 --- a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml +++ b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml @@ -40,7 +40,7 @@ .cell = form.label :drop_down_list_value, "Options de la liste", for: dom_id(type_de_champ, :drop_down_list_value) = form.text_area :drop_down_list_value, class: 'small-margin small width-100', rows: 7, id: dom_id(type_de_champ, :drop_down_list_value) - - if type_de_champ.drop_down_list_with_other? + - if type_de_champ.simple_drop_down_list? .cell = form.label :drop_down_other, for: dom_id(type_de_champ, :drop_down_other) do Proposer une option « autre » avec un texte libre diff --git a/app/helpers/champ_helper.rb b/app/helpers/champ_helper.rb index 2cdd7da05..16e74c4bb 100644 --- a/app/helpers/champ_helper.rb +++ b/app/helpers/champ_helper.rb @@ -31,11 +31,30 @@ module ChampHelper champ.dossier&.brouillon? && !champ.repetition? end - def autosave_controller(champ) - if autosave_available?(champ) - { controller: 'autosave' } - elsif !champ.repetition? && champ.dossier&.en_construction? - { controller: 'check-conditions' } + def editable_champ_controller(champ) + if !champ.repetition? && !champ.non_fillable? + # This is an editable champ. Lets find what controllers it might need. + controllers = [] + + # This is a public champ – it can have an autosave controller. + if champ.public? + # This is a champ on dossier in draft state. Activate autosave. + if champ.dossier&.brouillon? + controllers << 'autosave' + # This is a champ on a dossier in en_construction state. Enable conditions checker. + elsif champ.public? && champ.dossier&.en_construction? + controllers << 'check-conditions' + end + end + + # This is a dropdown champ. Activate special behaviours it might have. + if champ.simple_drop_down_list? || champ.linked_drop_down_list? + controllers << 'champ-dropdown' + end + + if controllers.present? + { controller: controllers.join(' ') } + end end end diff --git a/app/javascript/controllers/autosave_controller.ts b/app/javascript/controllers/autosave_controller.ts index 296a0938b..99c915d86 100644 --- a/app/javascript/controllers/autosave_controller.ts +++ b/app/javascript/controllers/autosave_controller.ts @@ -4,13 +4,8 @@ import { isSelectElement, isCheckboxOrRadioInputElement, isTextInputElement, - show, - hide, - enable, - disable, getConfig } from '@utils'; -import { z } from 'zod'; import { ApplicationController } from './application_controller'; import { AutoUpload } from '../shared/activestorage/auto-upload'; @@ -87,8 +82,6 @@ export class AutosaveController extends ApplicationController { isSelectElement(target) || isCheckboxOrRadioInputElement(target) ) { - this.toggleOtherInput(target); - this.toggleLinkedSelect(target); this.enqueueAutosaveRequest(); } } @@ -206,77 +199,4 @@ export class AutosaveController extends ApplicationController { } return inputs.filter((element) => !element.disabled); } - - private toggleOtherInput(target: HTMLSelectElement | HTMLInputElement) { - const parent = target.closest('.editable-champ-drop_down_list'); - const inputGroup = parent?.querySelector('.drop_down_other'); - if (inputGroup) { - const input = inputGroup.querySelector('input'); - if (input) { - if (target.value == '__other__') { - show(inputGroup); - input.disabled = false; - } else { - hide(inputGroup); - input.disabled = true; - } - } - } - } - - private toggleLinkedSelect(target: HTMLSelectElement | HTMLInputElement) { - const secondaryOptions = target.dataset.secondaryOptions; - if (isSelectElement(target) && secondaryOptions) { - const parent = target.closest('.editable-champ-linked_drop_down_list'); - const secondary = parent?.querySelector( - 'select[data-secondary]' - ); - if (secondary) { - const options = parseOptions(secondaryOptions); - this.setSecondaryOptions(secondary, options[target.value]); - } - } - } - - private setSecondaryOptions( - secondarySelectElement: HTMLSelectElement, - options: string[] - ) { - const wrapper = secondarySelectElement.closest('.secondary'); - const hidden = wrapper?.nextElementSibling as HTMLInputElement | null; - - secondarySelectElement.innerHTML = ''; - - if (options.length) { - disable(hidden); - - if (secondarySelectElement.required) { - secondarySelectElement.appendChild(makeOption('')); - } - for (const option of options) { - secondarySelectElement.appendChild(makeOption(option)); - } - - secondarySelectElement.selectedIndex = 0; - enable(secondarySelectElement); - show(wrapper); - } else { - hide(wrapper); - disable(secondarySelectElement); - enable(hidden); - } - } -} - -const SecondaryOptions = z.record(z.string().array()); - -function parseOptions(options: string) { - return SecondaryOptions.parse(JSON.parse(options)); -} - -function makeOption(option: string) { - const element = document.createElement('option'); - element.textContent = option; - element.value = option; - return element; } diff --git a/app/javascript/controllers/champ_dropdown_controller.ts b/app/javascript/controllers/champ_dropdown_controller.ts new file mode 100644 index 000000000..1615439df --- /dev/null +++ b/app/javascript/controllers/champ_dropdown_controller.ts @@ -0,0 +1,100 @@ +import { + isSelectElement, + isCheckboxOrRadioInputElement, + show, + hide, + enable, + disable +} from '@utils'; +import { z } from 'zod'; + +import { ApplicationController } from './application_controller'; + +export class ChampDropdownController extends ApplicationController { + connect() { + this.on('change', (event) => this.onChange(event)); + } + + private onChange(event: Event) { + const target = event.target as HTMLInputElement; + if (!target.disabled) { + if (isSelectElement(target) || isCheckboxOrRadioInputElement(target)) { + this.toggleOtherInput(target); + this.toggleLinkedSelect(target); + } + } + } + + private toggleOtherInput(target: HTMLSelectElement | HTMLInputElement) { + const parent = target.closest('.editable-champ-drop_down_list'); + const inputGroup = parent?.querySelector('.drop_down_other'); + if (inputGroup) { + const input = inputGroup.querySelector('input'); + if (input) { + if (target.value == '__other__') { + show(inputGroup); + input.disabled = false; + } else { + hide(inputGroup); + input.disabled = true; + } + } + } + } + + private toggleLinkedSelect(target: HTMLSelectElement | HTMLInputElement) { + const secondaryOptions = target.dataset.secondaryOptions; + if (isSelectElement(target) && secondaryOptions) { + const parent = target.closest('.editable-champ-linked_drop_down_list'); + const secondary = parent?.querySelector( + 'select[data-secondary]' + ); + if (secondary) { + const options = parseOptions(secondaryOptions); + this.setSecondaryOptions(secondary, options[target.value]); + } + } + } + + private setSecondaryOptions( + secondarySelectElement: HTMLSelectElement, + options: string[] + ) { + const wrapper = secondarySelectElement.closest('.secondary'); + const hidden = wrapper?.nextElementSibling as HTMLInputElement | null; + + secondarySelectElement.innerHTML = ''; + + if (options.length) { + disable(hidden); + + if (secondarySelectElement.required) { + secondarySelectElement.appendChild(makeOption('')); + } + for (const option of options) { + secondarySelectElement.appendChild(makeOption(option)); + } + + secondarySelectElement.selectedIndex = 0; + enable(secondarySelectElement); + show(wrapper); + } else { + hide(wrapper); + disable(secondarySelectElement); + enable(hidden); + } + } +} + +const SecondaryOptions = z.record(z.string().array()); + +function parseOptions(options: string) { + return SecondaryOptions.parse(JSON.parse(options)); +} + +function makeOption(option: string) { + const element = document.createElement('option'); + element.textContent = option; + element.value = option; + return element; +} diff --git a/app/models/champ.rb b/app/models/champ.rb index 3a0558dce..cf39e2cfd 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -49,6 +49,9 @@ class Champ < ApplicationRecord :dossier_link?, :titre_identite?, :header_section?, + :simple_drop_down_list?, + :linked_drop_down_list?, + :non_fillable?, :cnaf?, :dgfip?, :pole_emploi?, diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index cde2c68a0..e5e965476 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -205,18 +205,18 @@ class TypeDeChamp < ApplicationRecord ]) end - def drop_down_list_with_other? + def simple_drop_down_list? type_champ == TypeDeChamp.type_champs.fetch(:drop_down_list) end - def header_section? - type_champ == TypeDeChamp.type_champs.fetch(:header_section) - end - def linked_drop_down_list? type_champ == TypeDeChamp.type_champs.fetch(:linked_drop_down_list) end + def header_section? + type_champ == TypeDeChamp.type_champs.fetch(:header_section) + end + def exclude_from_view? type_champ == TypeDeChamp.type_champs.fetch(:explication) end diff --git a/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml b/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml index 429eb948b..f14e0c9ad 100644 --- a/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml +++ b/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml @@ -1,4 +1,4 @@ -.editable-champ{ class: "editable-champ-#{champ.type_champ} #{champ.visible? ? '' : 'hidden'}", id: champ.input_group_id, data: autosave_controller(champ) } +.editable-champ{ class: "editable-champ-#{champ.type_champ} #{champ.visible? ? '' : 'hidden'}", id: champ.input_group_id, data: editable_champ_controller(champ) } - if champ.repetition? %h3.header-subsection= champ.libelle - if champ.description.present? diff --git a/spec/helpers/champ_helper_spec.rb b/spec/helpers/champ_helper_spec.rb new file mode 100644 index 000000000..cb7a62f8d --- /dev/null +++ b/spec/helpers/champ_helper_spec.rb @@ -0,0 +1,61 @@ +describe ChampHelper, type: :helper do + describe "editable_champ_controller" do + let(:dossier) { create(:dossier) } + let(:champ) { create(:champ, dossier: dossier) } + let(:controllers) { [] } + let(:data) { { controller: controllers.join(' ') } } + + context 'when an editable champ' do + let(:controllers) { ['autosave'] } + + it { expect(editable_champ_controller(champ)).to eq(data) } + end + + context 'when a repetition champ' do + let(:champ) { create(:champ_repetition, dossier: dossier) } + + it { expect(editable_champ_controller(champ)).to eq(nil) } + end + + context 'when a private champ' do + let(:champ) { create(:champ, dossier: dossier, private: true) } + + it { expect(editable_champ_controller(champ)).to eq(nil) } + end + + context 'when a dossier is en_construction' do + let(:controllers) { ['check-conditions'] } + let(:dossier) { create(:dossier, :en_construction) } + + it { expect(editable_champ_controller(champ)).to eq(data) } + + context 'when a public dropdown champ' do + let(:controllers) { ['check-conditions', 'champ-dropdown'] } + let(:champ) { create(:champ_drop_down_list, dossier: dossier) } + + it { expect(editable_champ_controller(champ)).to eq(data) } + end + + context 'when a private dropdown champ' do + let(:controllers) { ['champ-dropdown'] } + let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) } + + it { expect(editable_champ_controller(champ)).to eq(data) } + end + end + + context 'when a public dropdown champ' do + let(:controllers) { ['autosave', 'champ-dropdown'] } + let(:champ) { create(:champ_drop_down_list, dossier: dossier) } + + it { expect(editable_champ_controller(champ)).to eq(data) } + end + + context 'when a private dropdown champ' do + let(:controllers) { ['champ-dropdown'] } + let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) } + + it { expect(editable_champ_controller(champ)).to eq(data) } + end + end +end