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')
= 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'

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
= 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'

View file

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

View file

@ -4,7 +4,7 @@ import debounce from 'debounce';
export type Detail = Record<string, unknown>;
export class ApplicationController extends Controller {
#debounced = new Map<() => void, () => void>();
#debounced = new Map<() => void, ReturnType<typeof debounce>>();
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<T = Detail>(type: string, detail?: T): void {
this.dispatch(type, {
detail: detail as object,

View file

@ -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<HTMLInputElement>();
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();
}
}
}
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();
}
});
this.onGlobal('turbo:submit-end', () => {
if (this.hasSpinnerTarget) {
hide(this.spinnerTarget);
}
});
}
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(
element: HTMLElement & { type?: string }
): element is HTMLInputElement {

View file

@ -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'

View file

@ -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'

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)

View file

@ -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' }

View file

@ -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"

View file

@ -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'

View file

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