Merge pull request #3308 from tchak/diaplay-champ-repetition

Bloc répétable 🎉🎉🎉
This commit is contained in:
Pierre de La Morinerie 2019-02-04 16:55:47 +01:00 committed by GitHub
commit d957cf4492
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 273 additions and 44 deletions

View file

@ -111,6 +111,10 @@
}
}
.add-row {
margin-bottom: 2 * $default-padding;
}
input[type=checkbox] {
&.small-margin {
margin-bottom: $default-padding / 2;
@ -246,6 +250,15 @@
.geo-areas {
margin-bottom: 2 * $default-padding;
}
&.editable-champ-repetition {
.row {
border-radius: 4px;
border: 1px solid $border-grey;
padding: $default-padding;
margin-bottom: 2 * $default-padding;
}
}
}
input.aa-input,

View file

@ -20,6 +20,10 @@
padding: (3 * $default-spacer) 2px;
}
th.padded {
padding-left: (2 * $default-spacer);
}
&.hoverable {
tbody tr:hover {
background: $light-grey;

View file

@ -0,0 +1,21 @@
class Champs::RepetitionController < ApplicationController
before_action :authenticate_logged_user!
def show
@champ = Champ
.joins(:dossier)
.where(dossiers: { user_id: logged_user_ids })
.find(params[:champ_id])
@position = params[:position]
row = (@champ.champs.empty? ? 0 : @champ.champs.last.row) + 1
@champ.add_row(row)
if @champ.private?
@attribute = "dossier[champs_private_attributes][#{@position}][champs_attributes]"
else
@attribute = "dossier[champs_attributes][#{@position}][champs_attributes]"
end
end
end

View file

@ -282,7 +282,8 @@ module NewUser
params.permit(dossier: {
champs_attributes: [
:id, :value, :primary_value, :secondary_value, :piece_justificative_file, value: [],
etablissement_attributes: Champs::SiretChamp::ETABLISSEMENT_ATTRIBUTES
etablissement_attributes: Champs::SiretChamp::ETABLISSEMENT_ATTRIBUTES,
champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, value: []]
]
})
end
@ -303,8 +304,7 @@ module NewUser
end
if !save_draft?
errors += @dossier.champs.select(&:mandatory_and_blank?)
.map { |c| "Le champ #{c.libelle.truncate(200)} doit être rempli." }
errors += @dossier.check_mandatory_champs
errors += PiecesJustificativesService.missing_pj_error_messages(@dossier)
end

View file

@ -31,6 +31,13 @@ module ApplicationHelper
# rubocop:enable Rails/OutputSafety
end
def append_to_element(selector, partial:, locals: {})
html = escape_javascript(render partial: partial, locals: locals)
# rubocop:disable Rails/OutputSafety
raw("document.querySelector('#{selector}').insertAdjacentHTML('beforeend', \"#{html}\");")
# rubocop:enable Rails/OutputSafety
end
def render_flash(timeout: false, sticky: false, fixed: false)
if flash.any?
html = render_to_element('#flash_messages', partial: 'layouts/flash_messages', locals: { sticky: sticky, fixed: fixed }, outer: true)

View file

@ -0,0 +1,24 @@
import { delegate } from '@utils';
const BUTTON_SELECTOR = '.button.remove-row';
const DESTROY_INPUT_SELECTOR = 'input[type=hidden][name*=_destroy]';
const CHAMP_SELECTOR = '.editable-champ';
addEventListener('turbolinks:load', () => {
delegate('click', BUTTON_SELECTOR, evt => {
evt.preventDefault();
const row = evt.target.closest('.row');
for (let input of row.querySelectorAll(DESTROY_INPUT_SELECTOR)) {
input.disabled = false;
input.value = true;
}
for (let champ of row.querySelectorAll(CHAMP_SELECTOR)) {
champ.remove();
}
evt.target.remove();
row.classList.remove('row');
});
});

View file

@ -21,6 +21,7 @@ import '../new_design/select2';
import '../new_design/champs/carte';
import '../new_design/champs/linked-drop-down-list';
import '../new_design/champs/repetition';
import '../new_design/administrateur/champs-editor';

View file

@ -10,7 +10,7 @@ class Champ < ApplicationRecord
has_many :geo_areas, dependent: :destroy
belongs_to :etablissement, dependent: :destroy
delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, :exclude_from_export?, :exclude_from_view?, to: :type_de_champ
delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, :exclude_from_export?, :exclude_from_view?, :repetition?, to: :type_de_champ
scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) }
scope :public_only, -> { where(private: false) }

View file

@ -9,10 +9,22 @@ class Champs::RepetitionChamp < Champ
champs.group_by(&:row).values
end
def add_row(row = 0)
type_de_champ.types_de_champ.each do |type_de_champ|
self.champs << type_de_champ.champ.build(row: row)
end
end
def mandatory_and_blank?
mandatory? && champs.empty?
end
def search_terms
# The user cannot enter any information here so it doesnt make much sense to search
end
private
def setup_dossier
champs.each do |champ|
champ.dossier = dossier

View file

@ -121,7 +121,13 @@ class Dossier < ApplicationRecord
def build_default_champs
procedure.types_de_champ.each do |type_de_champ|
champs << type_de_champ.champ.build
champ = type_de_champ.champ.build
if type_de_champ.repetition?
champ.add_row
end
champs << champ
end
procedure.types_de_champ_private.each do |type_de_champ|
champs_private << type_de_champ.champ.build
@ -334,6 +340,14 @@ class Dossier < ApplicationRecord
log_dossier_operation(gestionnaire, :classer_sans_suite)
end
def check_mandatory_champs
(champs + champs.select(&:repetition?).flat_map(&:champs))
.select(&:mandatory_and_blank?)
.map do |champ|
"Le champ #{champ.libelle.truncate(200)} doit être rempli."
end
end
private
def log_dossier_operation(gestionnaire, operation, automatic_operation: false)

View file

@ -147,10 +147,11 @@ class TypeDeChamp < ApplicationRecord
end
def exclude_from_view?
type_champ.in?([
TypeDeChamp.type_champs.fetch(:explication),
TypeDeChamp.type_champs.fetch(:repetition)
])
type_champ == TypeDeChamp.type_champs.fetch(:explication)
end
def repetition?
type_champ == TypeDeChamp.type_champs.fetch(:repetition)
end
def public?

View file

@ -0,0 +1,10 @@
- champs = champ.rows.last
- index = (champ.rows.size - 1) * champs.size
%div{ class: "row row-#{champs.first.row}" }
- champs.each.with_index(index) do |champ, index|
= fields_for "#{attribute}[#{index}]", champ do |form|
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form }
= form.hidden_field :id
= form.hidden_field :_destroy, disabled: true
%button.button.danger.remove-row
Supprimer

View file

@ -0,0 +1,3 @@
<%= append_to_element(".repetition-#{@position}",
partial: 'champs/repetition/show',
locals: { champ: @champ, attribute: @attribute }) %>

View file

@ -0,0 +1,40 @@
- champs.reject(&:exclude_from_view?).each do |c|
- if c.type_champ == TypeDeChamp.type_champs.fetch(:repetition)
%tr
%th.libelle.repetition{ colspan: 3 }
= "#{c.libelle} :"
- c.rows.each do |champs|
= render partial: "shared/dossiers/champ_row", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile, repetition: true }
%tr
%th{ colspan: 4 }
- else
%tr
- if c.type_champ == TypeDeChamp.type_champs.fetch(:header_section)
%th.header-section{ colspan: 3 }
= c.libelle
- else
%th.libelle{ class: repetition ? 'padded' : '' }
= "#{c.libelle} :"
%td.rich-text
%span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) }
- case c.type_champ
- when TypeDeChamp.type_champs.fetch(:carte)
= render partial: "shared/champs/carte/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:dossier_link)
= render partial: "shared/champs/dossier_link/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:multiple_drop_down_list)
= render partial: "shared/champs/multiple_drop_down_list/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:piece_justificative)
= render partial: "shared/champs/piece_justificative/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:siret)
= render partial: "shared/champs/siret/show", locals: { champ: c, profile: profile }
- when TypeDeChamp.type_champs.fetch(:textarea)
= render partial: "shared/champs/textarea/show", locals: { champ: c }
- else
= sanitize(c.to_s)
- if c.type_champ != TypeDeChamp.type_champs.fetch(:header_section)
%td.updated-at
%span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) }
modifié le
= c.updated_at.strftime("%d/%m/%Y à %H:%M")

View file

@ -1,33 +1,3 @@
%table.table.vertical.dossier-champs
%tbody
- champs.reject(&:exclude_from_view?).each do |c|
%tr
- if c.type_champ == TypeDeChamp.type_champs.fetch(:header_section)
%th.header-section{ colspan: 3 }
= c.libelle
- else
%th.libelle
= "#{c.libelle} :"
%td.rich-text
%span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) }
- case c.type_champ
- when TypeDeChamp.type_champs.fetch(:carte)
= render partial: "shared/champs/carte/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:dossier_link)
= render partial: "shared/champs/dossier_link/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:multiple_drop_down_list)
= render partial: "shared/champs/multiple_drop_down_list/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:piece_justificative)
= render partial: "shared/champs/piece_justificative/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:siret)
= render partial: "shared/champs/siret/show", locals: { champ: c, profile: profile }
- when TypeDeChamp.type_champs.fetch(:textarea)
= render partial: "shared/champs/textarea/show", locals: { champ: c }
- else
= sanitize(c.to_s)
- if c.type_champ != TypeDeChamp.type_champs.fetch(:header_section)
%td.updated-at
%span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) }
modifié le
= c.updated_at.strftime("%d/%m/%Y à %H:%M")
= render partial: "shared/dossiers/champ_row", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile, repetition: false }

View file

@ -1,4 +1,4 @@
= form.label champ.main_value_name do
= form.label champ.main_value_name, { class: champ.repetition? ? 'header-section' : '' } do
#{champ.libelle}
- if champ.mandatory?
%span.mandatory *

View file

@ -1 +1,15 @@
%h2.repetition-libelle= champ.libelle
%div{ class: "repetition-#{form.index}" }
- champ.rows.each do |champs|
%div{ class: "row row-#{champs.first.row}" }
- champs.each do |champ|
= form.fields_for :champs, champ do |form|
= render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form }
= form.hidden_field :_destroy, disabled: true
%button.button.danger.remove-row
Supprimer
- if champ.persisted?
= link_to "Ajouter une ligne pour « #{champ.libelle} »", champs_repetition_path(form.index), class: 'button add-row', data: { remote: true, method: 'POST', params: { champ_id: champ&.id }.to_query }
- else
%button.button.add-row{ disabled: true }
= "Ajouter une ligne pour « #{champ.libelle} »"

View file

@ -10,7 +10,7 @@ Flipflop.configure do
feature :champ_integer_number,
title: "Champ nombre entier"
feature :champ_repetition,
title: "Bloc répétable (NE MARCHE PAS NE PAS ACTIVER)"
title: "Bloc répétable"
end
feature :web_hook

View file

@ -127,6 +127,7 @@ Rails.application.routes.draw do
get ':position/siret', to: 'siret#show', as: :siret
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
post ':position/carte', to: 'carte#show', as: :carte
post ':position/repetition', to: 'repetition#show', as: :repetition
end
get 'tour-de-france' => 'root#tour_de_france'

View file

@ -83,6 +83,41 @@ feature 'The user' do
expect(page).to have_field('dossier_link', with: '123')
end
let(:procedure_with_repetition) do
tdc = create(:type_de_champ_repetition, libelle: 'repetition')
tdc.types_de_champ << create(:type_de_champ_text, libelle: 'text')
create(:procedure, :published, :for_individual, types_de_champ: [tdc])
end
scenario 'fill a dossier with repetition', js: true do
log_in(user.email, password, procedure_with_repetition)
fill_individual
fill_in('text', with: 'super texte')
expect(page).to have_field('text', with: 'super texte')
click_on 'Ajouter une ligne pour'
within '.row-1' do
fill_in('text', with: 'un autre texte')
end
expect(page).to have_content('Supprimer', count: 2)
click_on 'Enregistrer le brouillon'
expect(page).to have_content('Supprimer', count: 2)
within '.row-1' do
click_on 'Supprimer'
end
click_on 'Enregistrer le brouillon'
expect(page).to have_content('Supprimer', count: 1)
end
let(:simple_procedure) do
tdcs = [create(:type_de_champ, mandatory: true, libelle: 'texte obligatoire')]
create(:procedure, :published, :for_individual, types_de_champ: tdcs)

View file

@ -832,4 +832,63 @@ describe Dossier do
it { expect(dossier.followers_gestionnaires).not_to include(gestionnaire) }
it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'passer_en_instruction', true]]) }
end
describe "#check_mandatory_champs" do
let(:procedure) { create(:procedure, :with_type_de_champ) }
let(:dossier) { create(:dossier, :with_all_champs, procedure: procedure) }
it 'no mandatory champs' do
expect(dossier.check_mandatory_champs).to be_empty
end
context "with mandatory champs" do
let(:procedure) { create(:procedure, :with_type_de_champ_mandatory) }
let(:champ_with_error) { dossier.champs.first }
before do
champ_with_error.value = nil
champ_with_error.save
end
it 'should have errors' do
errors = dossier.check_mandatory_champs
expect(errors).not_to be_empty
expect(errors.first).to eq("Le champ #{champ_with_error.libelle} doit être rempli.")
end
end
context "with champ repetition" do
let(:procedure) { create(:procedure) }
let(:type_de_champ_repetition) { create(:type_de_champ_repetition, mandatory: true) }
before do
procedure.types_de_champ << type_de_champ_repetition
type_de_champ_repetition.types_de_champ << create(:type_de_champ_text, mandatory: true)
end
context "when no champs" do
let(:champ_with_error) { dossier.champs.first }
it 'should have errors' do
errors = dossier.check_mandatory_champs
expect(errors).not_to be_empty
expect(errors.first).to eq("Le champ #{champ_with_error.libelle} doit être rempli.")
end
end
context "when mandatory champ inside repetition" do
let(:champ_with_error) { dossier.champs.first.champs.first }
before do
dossier.champs.first.add_row
end
it 'should have errors' do
errors = dossier.check_mandatory_champs
expect(errors).not_to be_empty
expect(errors.first).to eq("Le champ #{champ_with_error.libelle} doit être rempli.")
end
end
end
end
end

View file

@ -8,7 +8,7 @@ describe 'shared/dossiers/champs.html.haml', type: :view do
allow(view).to receive(:current_gestionnaire).and_return(gestionnaire)
end
subject { render 'shared/dossiers/champs.html.haml', champs: champs, demande_seen_at: demande_seen_at }
subject { render 'shared/dossiers/champs.html.haml', champs: champs, demande_seen_at: demande_seen_at, profile: nil }
context "there are some champs" do
let(:dossier) { create(:dossier) }