poc(batch_operation.ui): implement simple ui to trigger a batch of current page

This commit is contained in:
Martin 2022-11-21 16:32:17 +01:00 committed by mfo
parent 7df86c50fb
commit beb39027d0
13 changed files with 223 additions and 76 deletions

View file

@ -51,3 +51,6 @@
}
}
}
.force-table-100{
width: calc(100vw);
}

View file

@ -0,0 +1,23 @@
class Dossiers::BatchOperationComponent < ApplicationComponent
attr_reader :statut, :procedure
def initialize(statut:, procedure:)
@statut = statut
@procedure = procedure
end
def render?
@statut == 'traites'
end
def available_operations
options = []
case @statut
when 'traites' then
options.push [t(".operations.archiver"), BatchOperation.operations.fetch(:archiver)]
else
end
options
end
end

View file

@ -0,0 +1,3 @@
fr:
operations:
archiver: 'Archive the selection'

View file

@ -0,0 +1,3 @@
fr:
operations:
archiver: 'Archiver la sélection'

View file

@ -0,0 +1,5 @@
= form_for(BatchOperation.new, url: Rails.application.routes.url_helpers.instructeur_batch_operations_path(procedure_id: procedure.id), method: :post, id: dom_id(BatchOperation.new), html: { class: 'flex justify-end' }, data: { "batch-operation-target" => "form"}) do |form|
.flex.align-center
- available_operations.each do |opt|
= form.submit opt[0], class: "fr-btn", disabled: :disabled, name: "#{form.object_name}[operation]", data: { "batch-operation-target" => "submit", "submitter-operation" => opt[1]}

View file

@ -0,0 +1,48 @@
import { ApplicationController } from './application_controller';
export class BatchOperationController extends ApplicationController {
static targets = ['input', 'all', 'submit', 'form'];
declare readonly submit: HTMLFormElement;
declare readonly submit: HTMLInputElement;
declare readonly allTarget: HTMLInputElement;
declare readonly inputTargets: HTMLInputElement[];
connect() {
this.formTarget.addEventListener(
'submit',
this.interceptFormSubmit.bind(this)
);
}
// DSFR recommends a <input type="submit" /> or <button type="submit" /> a form (not a <select>)
// but we have many actions on the same form (archive all, accept all, ...)
// so we intercept the form submit, and set the BatchOperation.operation by hand using the Event.submitter
interceptFormSubmit(event: Event) {
const { submitter } = event;
submitter.setAttribute('value', submitter.dataset.submitterOperation);
return event;
}
onCheckOne(event: Event) {
this.toggleSubmitButtonWhenNeeded();
return event;
}
onCheckAll(event: Event) {
this.inputTargets.forEach((e) => (e.checked = event.target.checked));
this.toggleSubmitButtonWhenNeeded();
return event;
}
toggleSubmitButtonWhenNeeded() {
const available = this.inputTargets.some((e) => e.checked);
if (available) {
this.submitTarget.removeAttribute('disabled');
} else {
this.submitTarget.setAttribute('disabled', 'disabled');
}
}
}

View file

@ -5,6 +5,7 @@
@import '@gouvfr/dsfr/dist/component/link/link.css';
@import '@gouvfr/dsfr/dist/component/form/form.css';
@import '@gouvfr/dsfr/dist/component/button/button.css';
@import '@gouvfr/dsfr/dist/component/select/select.css';
/* Verify README of each component to insert them in the expected order. */
@import '@gouvfr/dsfr/dist/component/alert/alert.css';

View file

@ -3,4 +3,3 @@ class BatchOperationEnqueueAllJob < ApplicationJob
batch_operation.enqueue_all
end
end

View file

@ -46,4 +46,20 @@ class BatchOperation < ApplicationRecord
def called_for_last_time? # beware, must be reloaded first
dossiers.count.zero?
end
private
# safer enqueue, in case instructeur kept the page for some time and their is a Dossier.id which does not fit current transaction
def dossiers_safe_scope
query = Dossier.joins(:procedure)
.where(procedure: { id: instructeur.procedures.ids })
.where(id: dossiers.ids)
.visible_by_administration
# case operation
# when BatchOperation.operations.fetch(:archiver) then
# query.not_archived
# when BatchOperation.operations.fetch(:accepter) then
# query.state_en_instruction
# end
end
end

View file

@ -3,7 +3,7 @@
= t('views.instructeurs.dossiers.restore')
- elsif close_to_expiration || Dossier::TERMINE.include?(state)
.dropdown.user-dossier-actions{ data: { controller: 'menu-button' } }
%button.fr-btn.dropdown-button{ data: { menu_button_target: 'button' } }
%button.fr-btn.fr-mb-0.dropdown-button{ data: { menu_button_target: 'button' } }
Actions
.dropdown-content.fade-in-down{ data: { menu_button_target: 'menu' }, id: "dossier_#{dossier_id}_actions_menu" }
%ul.dropdown-items

View file

@ -58,8 +58,6 @@
%p.explication-onglet
= t('views.instructeurs.dossiers.tab_explainations.expirant')
- if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0
- pagination = paginate @filtered_sorted_paginated_ids
= pagination
.flex
.flex-grow
= render partial: "dossiers_filter", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut, filterable_fields_for_select: @filterable_fields_for_select }
@ -68,83 +66,106 @@
- if @dossiers_count > 0
.dossiers-export
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count, export_url: method(:download_export_instructeur_procedure_path))
%hr.fr-mt-5v
%table.table.dossiers-table.hoverable
%thead
%tr
- if @statut.in? %w(suivis traites tous)
= render partial: "header_field", locals: { field: { "label" => "●", "table" => "notifications", "column" => "notifications" }, classname: "notification-col" }
- else
%th.notification-col
%div{ data: { controller: 'batch-operation' } }
- batch_operation_component = Dossiers::BatchOperationComponent.new(statut: @statut, procedure: @procedure)
- @procedure_presentation.displayed_fields_for_headers.each do |field|
= render partial: "header_field", locals: { field: field, classname: field['classname'] }
%th.action-col.follow-col
%span.dropdown{ data: { controller: 'menu-button', popover: 'true' } }
%button.button.dropdown-button{ data: { menu_button_target: 'button' } }
= t('views.instructeurs.dossiers.personalize')
#custom-menu.dropdown-content.fade-in-down{ data: { menu_button_target: 'menu' } }
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do
= hidden_field_tag :values, nil
= react_component("ComboMultiple",
options: @displayable_fields_for_select,
selected: @displayable_fields_selected,
disabled: [],
label: 'Colonne à afficher',
group: '.columns-form',
name: 'values')
= submit_tag t('views.instructeurs.dossiers.save'), class: 'button'
%tbody
- @projected_dossiers.each do |p|
- path = instructeur_dossier_path(@procedure, p.dossier_id)
%tr{ class: [p.hidden_by_user_at.present? && "file-hidden-by-user"] }
%td.folder-col
- if p.hidden_by_administration_at.present?
%span.cell-link
%span.icon.folder
.flex
.flex-grow= render batch_operation_component
.fr-table.fr-table--bordered
%table
%thead
%tr
- if batch_operation_component.render?
%th
%input{ type: "checkbox", data: { "batch-operation-target" => "all","action" => "batch-operation#onCheckAll"} }
- else
%a.cell-link{ href: path }
%span.icon.folder
- if @not_archived_notifications_dossier_ids.include?(p.dossier_id)
%span.notifications{ 'aria-label': 'notifications' }
%td.number-col
- if p.hidden_by_administration_at.present?
%span.cell-link= p.dossier_id
- else
%a.cell-link{ href: path }= p.dossier_id
- p.columns.each do |column|
%td
- if p.hidden_by_administration_at.present?
%span.cell-link
= column
= "- #{t('views.instructeurs.dossiers.deleted_by_user')}" if p.hidden_by_user_at.present?
- if @statut.in? %w(suivis traites tous)
= render partial: "header_field", locals: { field: { "label" => "●", "table" => "notifications", "column" => "notifications" }, classname: "notification-col" }
- else
%a.cell-link{ href: path }
= column
= "- #{t('views.instructeurs.dossiers.deleted_by_user')}" if p.hidden_by_user_at.present?
%th.notification-col
%td.status-col
- if p.hidden_by_administration_at.present?
%span.cell-link= status_badge(p.state)
- else
%a.cell-link{ href: path }= status_badge(p.state)
- @procedure_presentation.displayed_fields_for_headers.each do |field|
= render partial: "header_field", locals: { field: field, classname: field['classname'] }
%td.action-col.follow-col
%ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right
= render partial: 'dossier_actions', locals: { procedure_id: @procedure.id,
dossier_id: p.dossier_id,
state: p.state,
archived: p.archived,
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
close_to_expiration: @statut == 'expirant',
hidden_by_administration: @statut == 'supprimes_recemment' }
%th.action-col.follow-col
%span.dropdown{ data: { controller: 'menu-button', popover: 'true' } }
%button.button.dropdown-button{ data: { menu_button_target: 'button' } }
= t('views.instructeurs.dossiers.personalize')
#custom-menu.dropdown-content.fade-in-down{ data: { menu_button_target: 'menu' } }
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do
= hidden_field_tag :values, nil
= react_component("ComboMultiple",
options: @displayable_fields_for_select,
selected: @displayable_fields_selected,
disabled: [],
label: 'Colonne à afficher',
group: '.columns-form',
name: 'values')
= submit_tag t('views.instructeurs.dossiers.save'), class: 'button'
%tr
%tbody
- @projected_dossiers.each do |p|
- path = instructeur_dossier_path(@procedure, p.dossier_id)
%tr{ class: [p.hidden_by_user_at.present? && "file-hidden-by-user"] }
%td.folder-col
- if batch_operation_component.render?
- if p.batch_operation_id.present?
%span.cell-link
%span.fr-icon-lock-line{ aria_hidden: "true" }
- else
= check_box_tag :"batch_operation[dossier_ids][]", p.dossier_id, false, data: { "batch-operation-target" => "input", "action" => "batch-operation#onCheckOne"}, form: dom_id(BatchOperation.new)
- else
- if p.hidden_by_administration_at.present?
%span.cell-link
%span.icon.folder
- else
%a.cell-link{ href: path }
%span.icon.folder
- if @not_archived_notifications_dossier_ids.include?(p.dossier_id)
%span.notifications{ 'aria-label': 'notifications' }
%td.number-col
- if p.hidden_by_administration_at.present?
%span.cell-link= p.dossier_id
- else
%a.cell-link{ href: path }= p.dossier_id
- p.columns.each do |column|
%td
- if p.hidden_by_administration_at.present?
%span.cell-link
= column
= "- #{t('views.instructeurs.dossiers.deleted_by_user')}" if p.hidden_by_user_at.present?
- else
%a.cell-link{ href: path }
= column
= "- #{t('views.instructeurs.dossiers.deleted_by_user')}" if p.hidden_by_user_at.present?
%td.status-col
- if p.hidden_by_administration_at.present?
%span.cell-link= status_badge(p.state)
- else
%a.cell-link{ href: path }= status_badge(p.state)
%td.action-col.follow-col
%ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right
= render partial: 'dossier_actions', locals: { procedure_id: @procedure.id,
dossier_id: p.dossier_id,
state: p.state,
archived: p.archived,
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
close_to_expiration: @statut == 'expirant',
hidden_by_administration: @statut == 'supprimes_recemment' }
%tfoot
%tr
%td.force-table-100{ colspan: @procedure_presentation.displayed_fields_for_headers.size + 2 }= paginate @filtered_sorted_paginated_ids
= pagination
- else
%h2.empty-text
= t('views.instructeurs.dossiers.no_file')

View file

@ -0,0 +1,25 @@
RSpec.describe Dossiers::BatchOperationComponent, type: :component do
include ActionView::Context
include ActionView::Helpers::FormHelper
include ActionView::Helpers::FormOptionsHelper
let(:component) do
cmp = nil
form_for(BatchOperation.new, url: Rails.application.routes.url_helpers.instructeur_batch_operations_path(procedure_id: 1), method: :post, data: { controller: 'batch-operation' }) do |form|
cmp = described_class.new(statut: statut, form: form)
end
cmp
end
subject { render_inline(component).to_html }
context 'statut traite' do
let(:statut) { 'traites' }
it { is_expected.to have_selector('.fr-select-group') }
it { is_expected.to have_selector('option', text: "Archiver") }
end
context 'statut tous' do
let(:statut) { 'tous' }
it { is_expected.not_to have_selector('.fr-select-group') }
end
end

View file

@ -22,7 +22,7 @@ describe Instructeurs::BatchOperationsController, type: :controller do
procedure_id: procedure.id,
batch_operation: {
operation: BatchOperation.operations.fetch(:archiver),
dossier_ids: [ dossier.id ]
dossier_ids: [dossier.id]
}
}
end
@ -37,7 +37,7 @@ describe Instructeurs::BatchOperationsController, type: :controller do
expect(BatchOperation.first.dossiers).to include(dossier)
end
it 'enqueues a BatchOperationJob' do
expect {subject}.to have_enqueued_job(BatchOperationEnqueueAllJob).with(BatchOperation.last)
expect { subject }.to have_enqueued_job(BatchOperationEnqueueAllJob).with(BatchOperation.last)
end
end
end