poc(batch_operation.ui): implement simple ui to trigger a batch of current page
This commit is contained in:
parent
7df86c50fb
commit
beb39027d0
13 changed files with 223 additions and 76 deletions
|
@ -51,3 +51,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.force-table-100{
|
||||
width: calc(100vw);
|
||||
}
|
||||
|
|
23
app/components/dossiers/batch_operation_component.rb
Normal file
23
app/components/dossiers/batch_operation_component.rb
Normal 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
|
|
@ -0,0 +1,3 @@
|
|||
fr:
|
||||
operations:
|
||||
archiver: 'Archive the selection'
|
|
@ -0,0 +1,3 @@
|
|||
fr:
|
||||
operations:
|
||||
archiver: 'Archiver la sélection'
|
|
@ -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]}
|
||||
|
48
app/javascript/controllers/batch_operation_controller.ts
Normal file
48
app/javascript/controllers/batch_operation_controller.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -3,4 +3,3 @@ class BatchOperationEnqueueAllJob < ApplicationJob
|
|||
batch_operation.enqueue_all
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
25
spec/components/dossiers/batch_operation_component_spec.rb
Normal file
25
spec/components/dossiers/batch_operation_component_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue