From 88866d0413764eb96192eb48553a610c89e7b5b8 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 11 Jan 2023 21:45:41 +0100 Subject: [PATCH 1/4] refactor(autosubmit): split and improuve autosubmit and turbo controller --- .../controllers/application_controller.ts | 6 +- .../controllers/autosubmit_controller.ts | 91 ++++++++++++++----- .../controllers/turbo_controller.ts | 23 +++++ app/javascript/shared/utils.ts | 9 ++ 4 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 app/javascript/controllers/turbo_controller.ts diff --git a/app/javascript/controllers/application_controller.ts b/app/javascript/controllers/application_controller.ts index ad6733c97..41bbde30b 100644 --- a/app/javascript/controllers/application_controller.ts +++ b/app/javascript/controllers/application_controller.ts @@ -4,7 +4,7 @@ import debounce from 'debounce'; export type Detail = Record; export class ApplicationController extends Controller { - #debounced = new Map<() => void, () => void>(); + #debounced = new Map<() => void, ReturnType>(); protected debounce(fn: () => void, interval: number): void { this.globalDispatch('debounced:added'); @@ -26,6 +26,10 @@ export class ApplicationController extends Controller { debounced(); } + protected cancelDebounce(fn: () => void) { + this.#debounced.get(fn)?.clear(); + } + protected globalDispatch(type: string, detail?: T): void { this.dispatch(type, { detail: detail as object, diff --git a/app/javascript/controllers/autosubmit_controller.ts b/app/javascript/controllers/autosubmit_controller.ts index c5b0cbffe..848a3682a 100644 --- a/app/javascript/controllers/autosubmit_controller.ts +++ b/app/javascript/controllers/autosubmit_controller.ts @@ -1,32 +1,79 @@ +import { + isSelectElement, + isCheckboxOrRadioInputElement, + isTextInputElement, + isDateInputElement +} from '@utils'; import { ApplicationController } from './application_controller'; -import { show, hide } from '@utils'; -const AUTOSUBMIT_DEBOUNCE_DELAY = 5000; + +const AUTOSUBMIT_DEBOUNCE_DELAY = 500; +const AUTOSUBMIT_DATE_DEBOUNCE_DELAY = 5000; export class AutosubmitController extends ApplicationController { - static targets = ['form', 'spinner']; + static targets = ['submitter']; - declare readonly formTarget: HTMLFormElement; - declare readonly spinnerTarget: HTMLElement; - declare readonly hasSpinnerTarget: boolean; + declare readonly submitterTarget: HTMLButtonElement | HTMLInputElement; + declare readonly hasSubmitterTarget: boolean; - submit() { - this.formTarget.requestSubmit(); - } - - debouncedSubmit() { - this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY); - } + #dateTimeChangedInputs = new WeakSet(); connect() { - this.onGlobal('turbo:submit-start', () => { - if (this.hasSpinnerTarget) { - show(this.spinnerTarget); + this.on('input', (event) => this.onInput(event)); + this.on('change', (event) => this.onChange(event)); + this.on('blur', (event) => this.onBlur(event)); + } + + private onChange(event: Event) { + const target = event.target as HTMLInputElement; + if (target.disabled || target.hasAttribute('data-no-autosubmit')) return; + + if ( + isSelectElement(target) || + isCheckboxOrRadioInputElement(target) || + isTextInputElement(target) + ) { + if (isDateInputElement(target)) { + if (target.value.trim() == '' || !isNaN(Date.parse(target.value))) { + this.#dateTimeChangedInputs.add(target); + this.debounce(this.submit, AUTOSUBMIT_DATE_DEBOUNCE_DELAY); + } else { + this.#dateTimeChangedInputs.delete(target); + this.cancelDebounce(this.submit); + } + } else { + this.cancelDebounce(this.submit); + this.submit(); } - }); - this.onGlobal('turbo:submit-end', () => { - if (this.hasSpinnerTarget) { - hide(this.spinnerTarget); - } - }); + } + } + + private onInput(event: Event) { + const target = event.target as HTMLInputElement; + if (target.disabled || target.hasAttribute('data-no-autosubmit')) return; + + if (!isDateInputElement(target) && isTextInputElement(target)) { + this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY); + } + } + + private onBlur(event: Event) { + const target = event.target as HTMLInputElement; + if (target.disabled || target.hasAttribute('data-no-autosubmit')) return; + + if (isDateInputElement(target)) { + Promise.resolve().then(() => { + if (this.#dateTimeChangedInputs.has(target)) { + this.cancelDebounce(this.submit); + this.submit(); + } + }); + } + } + + private submit() { + const submitter = this.hasSubmitterTarget ? this.submitterTarget : null; + const form = + submitter?.form ?? this.element.closest('form'); + form?.requestSubmit(submitter); } } diff --git a/app/javascript/controllers/turbo_controller.ts b/app/javascript/controllers/turbo_controller.ts new file mode 100644 index 000000000..2c4e424ff --- /dev/null +++ b/app/javascript/controllers/turbo_controller.ts @@ -0,0 +1,23 @@ +import { show, hide } from '@utils'; + +import { ApplicationController } from './application_controller'; + +export class TurboController extends ApplicationController { + static targets = ['spinner']; + + declare readonly spinnerTarget: HTMLElement; + declare readonly hasSpinnerTarget: boolean; + + connect() { + this.onGlobal('turbo:submit-start', () => { + if (this.hasSpinnerTarget) { + show(this.spinnerTarget); + } + }); + this.onGlobal('turbo:submit-end', () => { + if (this.hasSpinnerTarget) { + hide(this.spinnerTarget); + } + }); + } +} diff --git a/app/javascript/shared/utils.ts b/app/javascript/shared/utils.ts index d9c800cc5..ca1b0eae3 100644 --- a/app/javascript/shared/utils.ts +++ b/app/javascript/shared/utils.ts @@ -290,6 +290,15 @@ export function isCheckboxOrRadioInputElement( ); } +export function isDateInputElement( + element: HTMLElement & { type?: string } +): element is HTMLInputElement { + return ( + element.tagName == 'INPUT' && + (element.type == 'date' || element.type == 'datetime-local') + ); +} + export function isTextInputElement( element: HTMLElement & { type?: string } ): element is HTMLInputElement { From 5d7284b8daf3551a9f20338fb4d318b68c29b854 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 11 Jan 2023 21:46:27 +0100 Subject: [PATCH 2/4] refactor(js): use autosubmit controller in notified_toggle_component --- .../notified_toggle_component.html.haml | 4 ++-- app/javascript/controllers/checkbox_controller.ts | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 app/javascript/controllers/checkbox_controller.ts diff --git a/app/components/dossiers/notified_toggle_component/notified_toggle_component.html.haml b/app/components/dossiers/notified_toggle_component/notified_toggle_component.html.haml index 2d7e75895..53da63089 100644 --- a/app/components/dossiers/notified_toggle_component/notified_toggle_component.html.haml +++ b/app/components/dossiers/notified_toggle_component/notified_toggle_component.html.haml @@ -1,5 +1,5 @@ -= form_tag update_sort_instructeur_procedure_path(procedure_id: @procedure.id, table: 'notifications', column: 'notifications', order: opposite_order), method: 'GET', data: {controller: 'checkbox'} do += form_tag update_sort_instructeur_procedure_path(procedure_id: @procedure.id, table: 'notifications', column: 'notifications', order: opposite_order), method: :get, data: { controller: 'autosubmit' } do .fr-toggle - = check_box_tag :order, opposite_order, active?, data: {action: 'change->checkbox#onChange'}, class: 'fr-toggle__input' + = check_box_tag :order, opposite_order, active?, class: 'fr-toggle__input' = label_tag :order, t('.show_notified_first'), class: 'fr-toggle__label fr-pl-1w' = submit_tag t('.show_notified_first'), data: {"checkbox-target": 'submit' }, class: 'visually-hidden' diff --git a/app/javascript/controllers/checkbox_controller.ts b/app/javascript/controllers/checkbox_controller.ts deleted file mode 100644 index 8b85bb51b..000000000 --- a/app/javascript/controllers/checkbox_controller.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApplicationController } from './application_controller'; - -export class CheckboxController extends ApplicationController { - onChange() { - const form = this.element as HTMLFormElement; - form.requestSubmit(); - } -} From 289d48f6970999da8e970a3e2299b06f8db8e4d3 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 11 Jan 2023 21:47:22 +0100 Subject: [PATCH 3/4] refactor(js): use autosubmit controller in filter_component --- .../filter_component.html.haml | 9 ++++---- .../instructeurs/procedures_controller.rb | 21 ++++++++----------- .../controllers/dossier_filter_controller.ts | 13 ------------ ...m.haml => update_filter.turbo_stream.haml} | 2 +- config/routes.rb | 3 ++- .../instructeurs/procedure_filters_spec.rb | 2 ++ 6 files changed, 19 insertions(+), 31 deletions(-) delete mode 100644 app/javascript/controllers/dossier_filter_controller.ts rename app/views/instructeurs/procedures/{add_filter.turbo_stream.haml => update_filter.turbo_stream.haml} (76%) diff --git a/app/components/dossiers/filter_component/filter_component.html.haml b/app/components/dossiers/filter_component/filter_component.html.haml index e58a95b54..e3b5342de 100644 --- a/app/components/dossiers/filter_component/filter_component.html.haml +++ b/app/components/dossiers/filter_component/filter_component.html.haml @@ -1,12 +1,13 @@ -= form_tag add_filter_instructeur_procedure_url(procedure), method: :post, class: 'dropdown-form large', id: 'filter-component', data: { controller: 'dossier-filter' } do += form_tag add_filter_instructeur_procedure_path(procedure), method: :post, class: 'dropdown-form large', id: 'filter-component', data: { turbo: true, controller: 'autosubmit' } do = label_tag :field, t('.column') - = select_tag :field, options_for_select(filterable_fields_for_select, field_id), include_blank: field_id.nil?, data: {action: "dossier-filter#onChange"} + = select_tag :field, options_for_select(filterable_fields_for_select, field_id), include_blank: field_id.nil? + %input.hidden{ type: 'submit', formaction: update_filter_instructeur_procedure_path(procedure), data: { autosubmit_target: 'submitter' } } %br = label_tag :value, t('.value'), for: 'value' - if field_type == :enum - = select_tag :value, options_for_select(options_for_select_of_field), id: 'value', name: 'value' + = select_tag :value, options_for_select(options_for_select_of_field), id: 'value', name: 'value', data: { no_autosubmit: true } - else - %input#value{ type: field_type, name: :value, maxlength: ProcedurePresentation::FILTERS_VALUE_MAX_LENGTH, disabled: field_id.nil? ? true : false } + %input#value{ type: field_type, name: :value, maxlength: ProcedurePresentation::FILTERS_VALUE_MAX_LENGTH, disabled: field_id.nil? ? true : false, data: { no_autosubmit: true } } = hidden_field_tag :statut, statut = submit_tag t('.add_filter'), class: 'fr-btn fr-btn--secondary fr-mt-2w' diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 3a1428604..7651f800c 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -131,19 +131,16 @@ module Instructeurs end def add_filter - respond_to do |format| - format.html do - procedure_presentation.add_filter(statut, params[:field], params[:value]) + procedure_presentation.add_filter(statut, params[:field], params[:value]) - redirect_back(fallback_location: instructeur_procedure_url(procedure)) - end - format.turbo_stream do - @statut = statut - @procedure = procedure - @procedure_presentation = procedure_presentation - @field = params[:field] - end - end + redirect_back(fallback_location: instructeur_procedure_url(procedure)) + end + + def update_filter + @statut = statut + @procedure = procedure + @procedure_presentation = procedure_presentation + @field = params[:field] end def remove_filter diff --git a/app/javascript/controllers/dossier_filter_controller.ts b/app/javascript/controllers/dossier_filter_controller.ts deleted file mode 100644 index 11b8b4113..000000000 --- a/app/javascript/controllers/dossier_filter_controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { httpRequest } from '@utils'; -import { ApplicationController } from './application_controller'; - -export class DossierFilterController extends ApplicationController { - onChange() { - const element = this.element as HTMLFormElement; - - httpRequest(element.action, { - method: element.getAttribute('method') ?? '', - body: new FormData(element) - }).turbo(); - } -} diff --git a/app/views/instructeurs/procedures/add_filter.turbo_stream.haml b/app/views/instructeurs/procedures/update_filter.turbo_stream.haml similarity index 76% rename from app/views/instructeurs/procedures/add_filter.turbo_stream.haml rename to app/views/instructeurs/procedures/update_filter.turbo_stream.haml index 05598d95b..55679210d 100644 --- a/app/views/instructeurs/procedures/add_filter.turbo_stream.haml +++ b/app/views/instructeurs/procedures/update_filter.turbo_stream.haml @@ -1,2 +1,2 @@ -= turbo_stream.replace 'filter-component' do += turbo_stream.morph 'filter-component' do = render Dossiers::FilterComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation, statut: @statut, field_id: @field) diff --git a/config/routes.rb b/config/routes.rb index 3ae74c592..dba9f617f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -394,7 +394,8 @@ Rails.application.routes.draw do patch 'update_displayed_fields' get 'update_sort/:table/:column' => 'procedures#update_sort', as: 'update_sort' post 'add_filter' - get 'remove_filter' => 'procedures#remove_filter', as: 'remove_filter' + post 'update_filter' + get 'remove_filter' get 'download_export' post 'download_export' get 'stats' diff --git a/spec/system/instructeurs/procedure_filters_spec.rb b/spec/system/instructeurs/procedure_filters_spec.rb index cf90566d4..44ef71eb9 100644 --- a/spec/system/instructeurs/procedure_filters_spec.rb +++ b/spec/system/instructeurs/procedure_filters_spec.rb @@ -84,6 +84,7 @@ describe "procedure filters" do find("input#value[type=date]", visible: true) fill_in "Valeur", with: "10/10/2010" click_button "Ajouter le filtre" + expect(page).to have_no_css("select#field", visible: true) # use enum filter click_on 'Sélectionner un filtre' @@ -134,6 +135,7 @@ describe "procedure filters" do select column_name, from: "Colonne" fill_in "Valeur", with: filter_value click_button "Ajouter le filtre" + expect(page).to have_no_css("select#field", visible: true) end def add_column(column_name) From c7c6d50df6e9d144c8d514eae2068cc16f03072c Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 11 Jan 2023 21:47:44 +0100 Subject: [PATCH 4/4] refactor(js): use autosubmit controller in procedure/all pages --- .../procedures/administrateurs.html.haml | 4 ++-- app/views/administrateurs/procedures/all.html.haml | 4 ++-- app/views/layouts/all.html.haml | 12 ++++++------ app/views/layouts/application.html.haml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/administrateurs/procedures/administrateurs.html.haml b/app/views/administrateurs/procedures/administrateurs.html.haml index 6595be513..fb49b445e 100644 --- a/app/views/administrateurs/procedures/administrateurs.html.haml +++ b/app/views/administrateurs/procedures/administrateurs.html.haml @@ -1,6 +1,6 @@ - content_for :results do .main-filter-header.fr-my-3w - = form_with(url: administrateurs_admin_procedures_path, method: :get, html: { 'data-autosubmit-target': 'form', 'data-turbo-frame': 'procedures', role: 'search' }) do |f| + = form_with(url: administrateurs_admin_procedures_path, method: :get, data: { turbo_frame: 'procedures' }, html: { role: 'search' }) do |f| - @filter.zone_ids&.each do |zone_id| = hidden_field_tag 'zone_ids[]', zone_id - @filter.statuses&.each do |status| @@ -14,7 +14,7 @@ %table#all-admins %caption = "#{@admins.total_count} administrateurs" - %span.hidden.fr-icon-ball-pen-fill{ 'aria-hidden': 'true', 'data-autosubmit-target': 'spinner' } + %span.hidden.fr-icon-ball-pen-fill{ 'aria-hidden': 'true', 'data-turbo-target': 'spinner' } - if @filter.email .selected-email.fr-mb-2w = link_to @filter.email, administrateurs_admin_procedures_path(@filter.without(:email)), class: 'fr-tag fr-tag--dismiss fr-mb-1w' diff --git a/app/views/administrateurs/procedures/all.html.haml b/app/views/administrateurs/procedures/all.html.haml index 268558792..70341a3ca 100644 --- a/app/views/administrateurs/procedures/all.html.haml +++ b/app/views/administrateurs/procedures/all.html.haml @@ -1,6 +1,6 @@ - content_for :results do .main-filter-header.fr-my-3w - = form_with(url: all_admin_procedures_path, method: :get, html: { 'data-autosubmit-target': 'form', 'data-turbo-frame': 'procedures', role: 'search', class: 'search' }) do |f| + = form_with(url: all_admin_procedures_path, method: :get, data: { turbo_frame: 'procedures' }, html: { role: 'search', class: 'search' }) do |f| - @filter.zone_ids&.each do |zone_id| = hidden_field_tag 'zone_ids[]', zone_id - @filter.statuses&.each do |status| @@ -16,7 +16,7 @@ %table#all-demarches %caption = "#{@procedures.total_count} démarches" - %span.hidden.fr-icon-ball-pen-fill{ 'aria-hidden': 'true', 'data-autosubmit-target': 'spinner' } + %span.hidden.fr-icon-ball-pen-fill{ 'aria-hidden': 'true', 'data-turbo-target': 'spinner' } - if @filter.libelle .selected-query.fr-mb-2w = link_to @filter.libelle, all_admin_procedures_path(@filter.without(:libelle)), class: 'fr-tag fr-tag--dismiss fr-mb-1w' diff --git a/app/views/layouts/all.html.haml b/app/views/layouts/all.html.haml index 2adc2765e..65a6cffb3 100644 --- a/app/views/layouts/all.html.haml +++ b/app/views/layouts/all.html.haml @@ -9,10 +9,10 @@ .fr-highlight.fr-mb-4w %p Ce tableau de bord permet de consulter les informations sur les démarches simplifiées pour toutes les zones. Filtrez par zone et statut. Consultez la liste des démarches et cliquez sur une démarche pour voir la zone et quels sont les administrateurs. - .fr-container--fluid{ 'data-turbo': 'true', 'data-controller': 'autosubmit' } + .fr-container--fluid{ data: { turbo: 'true' } } .fr-grid-row.fr-grid-row--gutters .fr-col-3 - = form_with(url: all_admin_procedures_path, method: :get, html: { 'data-autosubmit-target': 'form', 'data-turbo-frame': 'procedures' }) do |f| + = form_with(url: all_admin_procedures_path, method: :get, data: { controller: 'autosubmit', turbo_frame: 'procedures' }) do |f| %fieldset.sidebar-filter %legend @@ -31,7 +31,7 @@ .fr-ml-1w{ 'data-expand-target': 'content' } = f.collection_check_boxes :zone_ids, @filter.admin_zones, :id, :current_label, include_hidden: false do |b| .fr-checkbox-group.fr-ml-2w.fr-py-1w - = b.check_box(checked: @filter.zone_filtered?(b.value), 'data-action': 'autosubmit#submit') + = b.check_box(checked: @filter.zone_filtered?(b.value)) = b.label(class: 'fr-label') { b.text } %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" } .fr-mb-1w @@ -41,7 +41,7 @@ .fr-ml-1w.hidden{ 'data-expand-target': 'content' } = f.collection_check_boxes :zone_ids, @filter.other_zones, :id, :current_label, include_hidden: false do |b| .fr-checkbox-group.fr-ml-2w.fr-py-1w - = b.check_box(checked: @filter.zone_filtered?(b.value), 'data-action': 'autosubmit#submit') + = b.check_box(checked: @filter.zone_filtered?(b.value)) = b.label(class: 'fr-label') { b.text } %li.fr-py-2w{ 'data-controller': "expand" } .fr-mb-1w.fr-pl-2w @@ -51,7 +51,7 @@ .fr-input-group.hidden{ 'data-expand-target': 'content' } = f.label 'from_publication_date', 'Depuis', class: 'fr-label' .fr-input-wrap.fr-fi-calendar-line - = f.date_field 'from_publication_date', value: @filter.from_publication_date, class: 'fr-input', 'data-action': 'blur->autosubmit#submit change->autosubmit#debouncedSubmit' + = f.date_field 'from_publication_date', value: @filter.from_publication_date, class: 'fr-input' %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" } .fr-mb-1w @@ -61,7 +61,7 @@ .fr-ml-1w.hidden{ 'data-expand-target': 'content' } = f.collection_check_boxes :statuses, ['publiee', 'close'], :to_s, :to_s, include_hidden: false do |b| .fr-checkbox-group.fr-ml-2w.fr-py-1w - = b.check_box(checked: @filter.status_filtered?(b.value), 'data-action': 'autosubmit#submit') + = b.check_box(checked: @filter.status_filtered?(b.value)) = b.label(class: 'fr-label') { t b.text, scope: 'activerecord.attributes.procedure.aasm_state' } %turbo-frame#procedures.fr-col-9{ 'data-turbo-action': 'advance' } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index b181ba6e0..07624909a 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -35,7 +35,7 @@ = yield(:invisible_captcha_styles) - %body{ { id: content_for(:page_id), class: browser.platform.ios? ? 'ios' : nil }.compact } + %body{ { id: content_for(:page_id), class: browser.platform.ios? ? 'ios' : nil, data: { controller: 'turbo' } }.compact } = render partial: 'layouts/skiplinks' .page-wrapper = render partial: "layouts/outdated_browser_banner"