Merge pull request #8416 from tchak/stimulus-autosubmit

refactor(autosubmit): split and improuve autosubmit and turbo controller
This commit is contained in:
Paul Chavard 2023-01-12 12:25:58 +01:00 committed by GitHub
commit 40befbccc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 138 additions and 75 deletions

View file

@ -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') = 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 %br
= label_tag :value, t('.value'), for: 'value' = label_tag :value, t('.value'), for: 'value'
- if field_type == :enum - 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 - 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 = hidden_field_tag :statut, statut
= submit_tag t('.add_filter'), class: 'fr-btn fr-btn--secondary fr-mt-2w' = submit_tag t('.add_filter'), class: 'fr-btn fr-btn--secondary fr-mt-2w'

View file

@ -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 .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' = 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' = submit_tag t('.show_notified_first'), data: {"checkbox-target": 'submit' }, class: 'visually-hidden'

View file

@ -131,19 +131,16 @@ module Instructeurs
end end
def add_filter def add_filter
respond_to do |format| procedure_presentation.add_filter(statut, params[:field], params[:value])
format.html do
procedure_presentation.add_filter(statut, params[:field], params[:value])
redirect_back(fallback_location: instructeur_procedure_url(procedure)) redirect_back(fallback_location: instructeur_procedure_url(procedure))
end end
format.turbo_stream do
@statut = statut def update_filter
@procedure = procedure @statut = statut
@procedure_presentation = procedure_presentation @procedure = procedure
@field = params[:field] @procedure_presentation = procedure_presentation
end @field = params[:field]
end
end end
def remove_filter def remove_filter

View file

@ -4,7 +4,7 @@ import debounce from 'debounce';
export type Detail = Record<string, unknown>; export type Detail = Record<string, unknown>;
export class ApplicationController extends Controller { export class ApplicationController extends Controller {
#debounced = new Map<() => void, () => void>(); #debounced = new Map<() => void, ReturnType<typeof debounce>>();
protected debounce(fn: () => void, interval: number): void { protected debounce(fn: () => void, interval: number): void {
this.globalDispatch('debounced:added'); this.globalDispatch('debounced:added');
@ -26,6 +26,10 @@ export class ApplicationController extends Controller {
debounced(); debounced();
} }
protected cancelDebounce(fn: () => void) {
this.#debounced.get(fn)?.clear();
}
protected globalDispatch<T = Detail>(type: string, detail?: T): void { protected globalDispatch<T = Detail>(type: string, detail?: T): void {
this.dispatch(type, { this.dispatch(type, {
detail: detail as object, detail: detail as object,

View file

@ -1,32 +1,79 @@
import {
isSelectElement,
isCheckboxOrRadioInputElement,
isTextInputElement,
isDateInputElement
} from '@utils';
import { ApplicationController } from './application_controller'; 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 { export class AutosubmitController extends ApplicationController {
static targets = ['form', 'spinner']; static targets = ['submitter'];
declare readonly formTarget: HTMLFormElement; declare readonly submitterTarget: HTMLButtonElement | HTMLInputElement;
declare readonly spinnerTarget: HTMLElement; declare readonly hasSubmitterTarget: boolean;
declare readonly hasSpinnerTarget: boolean;
submit() { #dateTimeChangedInputs = new WeakSet<HTMLInputElement>();
this.formTarget.requestSubmit();
}
debouncedSubmit() {
this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY);
}
connect() { connect() {
this.onGlobal('turbo:submit-start', () => { this.on('input', (event) => this.onInput(event));
if (this.hasSpinnerTarget) { this.on('change', (event) => this.onChange(event));
show(this.spinnerTarget); 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<HTMLFormElement>('form');
form?.requestSubmit(submitter);
} }
} }

View file

@ -1,8 +0,0 @@
import { ApplicationController } from './application_controller';
export class CheckboxController extends ApplicationController {
onChange() {
const form = this.element as HTMLFormElement;
form.requestSubmit();
}
}

View file

@ -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();
}
}

View file

@ -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);
}
});
}
}

View file

@ -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( export function isTextInputElement(
element: HTMLElement & { type?: string } element: HTMLElement & { type?: string }
): element is HTMLInputElement { ): element is HTMLInputElement {

View file

@ -1,6 +1,6 @@
- content_for :results do - content_for :results do
.main-filter-header.fr-my-3w .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| - @filter.zone_ids&.each do |zone_id|
= hidden_field_tag 'zone_ids[]', zone_id = hidden_field_tag 'zone_ids[]', zone_id
- @filter.statuses&.each do |status| - @filter.statuses&.each do |status|
@ -14,7 +14,7 @@
%table#all-admins %table#all-admins
%caption %caption
= "#{@admins.total_count} administrateurs" = "#{@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 - if @filter.email
.selected-email.fr-mb-2w .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' = link_to @filter.email, administrateurs_admin_procedures_path(@filter.without(:email)), class: 'fr-tag fr-tag--dismiss fr-mb-1w'

View file

@ -1,6 +1,6 @@
- content_for :results do - content_for :results do
.main-filter-header.fr-my-3w .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| - @filter.zone_ids&.each do |zone_id|
= hidden_field_tag 'zone_ids[]', zone_id = hidden_field_tag 'zone_ids[]', zone_id
- @filter.statuses&.each do |status| - @filter.statuses&.each do |status|
@ -16,7 +16,7 @@
%table#all-demarches %table#all-demarches
%caption %caption
= "#{@procedures.total_count} démarches" = "#{@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 - if @filter.libelle
.selected-query.fr-mb-2w .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' = link_to @filter.libelle, all_admin_procedures_path(@filter.without(:libelle)), class: 'fr-tag fr-tag--dismiss fr-mb-1w'

View file

@ -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) = render Dossiers::FilterComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation, statut: @statut, field_id: @field)

View file

@ -9,10 +9,10 @@
.fr-highlight.fr-mb-4w .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. %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-grid-row.fr-grid-row--gutters
.fr-col-3 .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 %fieldset.sidebar-filter
%legend %legend
@ -31,7 +31,7 @@
.fr-ml-1w{ 'data-expand-target': 'content' } .fr-ml-1w{ 'data-expand-target': 'content' }
= f.collection_check_boxes :zone_ids, @filter.admin_zones, :id, :current_label, include_hidden: false do |b| = 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 .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 } = b.label(class: 'fr-label') { b.text }
%li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" } %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
.fr-mb-1w .fr-mb-1w
@ -41,7 +41,7 @@
.fr-ml-1w.hidden{ 'data-expand-target': 'content' } .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| = 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 .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 } = b.label(class: 'fr-label') { b.text }
%li.fr-py-2w{ 'data-controller': "expand" } %li.fr-py-2w{ 'data-controller': "expand" }
.fr-mb-1w.fr-pl-2w .fr-mb-1w.fr-pl-2w
@ -51,7 +51,7 @@
.fr-input-group.hidden{ 'data-expand-target': 'content' } .fr-input-group.hidden{ 'data-expand-target': 'content' }
= f.label 'from_publication_date', 'Depuis', class: 'fr-label' = f.label 'from_publication_date', 'Depuis', class: 'fr-label'
.fr-input-wrap.fr-fi-calendar-line .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" } %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
.fr-mb-1w .fr-mb-1w
@ -61,7 +61,7 @@
.fr-ml-1w.hidden{ 'data-expand-target': 'content' } .fr-ml-1w.hidden{ 'data-expand-target': 'content' }
= f.collection_check_boxes :statuses, ['publiee', 'close'], :to_s, :to_s, include_hidden: false do |b| = 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 .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' } = b.label(class: 'fr-label') { t b.text, scope: 'activerecord.attributes.procedure.aasm_state' }
%turbo-frame#procedures.fr-col-9{ 'data-turbo-action': 'advance' } %turbo-frame#procedures.fr-col-9{ 'data-turbo-action': 'advance' }

View file

@ -35,7 +35,7 @@
= yield(:invisible_captcha_styles) = 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' = render partial: 'layouts/skiplinks'
.page-wrapper .page-wrapper
= render partial: "layouts/outdated_browser_banner" = render partial: "layouts/outdated_browser_banner"

View file

@ -394,7 +394,8 @@ Rails.application.routes.draw do
patch 'update_displayed_fields' patch 'update_displayed_fields'
get 'update_sort/:table/:column' => 'procedures#update_sort', as: 'update_sort' get 'update_sort/:table/:column' => 'procedures#update_sort', as: 'update_sort'
post 'add_filter' post 'add_filter'
get 'remove_filter' => 'procedures#remove_filter', as: 'remove_filter' post 'update_filter'
get 'remove_filter'
get 'download_export' get 'download_export'
post 'download_export' post 'download_export'
get 'stats' get 'stats'

View file

@ -84,6 +84,7 @@ describe "procedure filters" do
find("input#value[type=date]", visible: true) find("input#value[type=date]", visible: true)
fill_in "Valeur", with: "10/10/2010" fill_in "Valeur", with: "10/10/2010"
click_button "Ajouter le filtre" click_button "Ajouter le filtre"
expect(page).to have_no_css("select#field", visible: true)
# use enum filter # use enum filter
click_on 'Sélectionner un filtre' click_on 'Sélectionner un filtre'
@ -134,6 +135,7 @@ describe "procedure filters" do
select column_name, from: "Colonne" select column_name, from: "Colonne"
fill_in "Valeur", with: filter_value fill_in "Valeur", with: filter_value
click_button "Ajouter le filtre" click_button "Ajouter le filtre"
expect(page).to have_no_css("select#field", visible: true)
end end
def add_column(column_name) def add_column(column_name)