Merge pull request #8416 from tchak/stimulus-autosubmit
refactor(autosubmit): split and improuve autosubmit and turbo controller
This commit is contained in:
commit
40befbccc3
16 changed files with 138 additions and 75 deletions
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { ApplicationController } from './application_controller';
|
||||
|
||||
export class CheckboxController extends ApplicationController {
|
||||
onChange() {
|
||||
const form = this.element as HTMLFormElement;
|
||||
form.requestSubmit();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
23
app/javascript/controllers/turbo_controller.ts
Normal file
23
app/javascript/controllers/turbo_controller.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
|
@ -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' }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue