Merge pull request #5502 from betagouv/dev

This commit is contained in:
Pierre de La Morinerie 2020-08-26 10:21:09 +02:00 committed by GitHub
commit 23be513b4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 192 additions and 44 deletions

View file

@ -147,6 +147,11 @@
margin-left: 0; margin-left: 0;
} }
&.blank-radio {
color: $dark-grey;
font-style: italic;
}
input[type=radio] { input[type=radio] {
margin-bottom: 0; margin-bottom: 0;
} }

View file

@ -163,7 +163,7 @@ module Users
respond_to do |format| respond_to do |format|
format.html { render :brouillon } format.html { render :brouillon }
format.json { render json: {}, status: :ok } format.js { render :brouillon }
end end
end end

View file

@ -62,7 +62,11 @@ module ApplicationHelper
script = "(function() {"; script = "(function() {";
script << "var el = document.querySelector('#{selector}');" script << "var el = document.querySelector('#{selector}');"
method = (inner ? "el.innerHTML = ''" : "el.parentNode.removeChild(el)") method = (inner ? "el.innerHTML = ''" : "el.parentNode.removeChild(el)")
script << "if (el) { setTimeout(function() { #{method}; }, #{timeout}); }"; if timeout.present? && timeout > 0
script << "if (el) { setTimeout(function() { #{method}; }, #{timeout}); }"
else
script << "if (el) { #{method} };"
end
script << "})();" script << "})();"
# rubocop:disable Rails/OutputSafety # rubocop:disable Rails/OutputSafety
raw(script); raw(script);

View file

@ -1,8 +1,9 @@
import { delegate } from '@utils'; import { delegate, fire } from '@utils';
const CHAMP_SELECTOR = '.editable-champ';
const BUTTON_SELECTOR = '.button.remove-row'; const BUTTON_SELECTOR = '.button.remove-row';
const DESTROY_INPUT_SELECTOR = 'input[type=hidden][name*=_destroy]'; const DESTROY_INPUT_SELECTOR = 'input[type=hidden][name*=_destroy]';
const CHAMP_SELECTOR = '.editable-champ'; const DOM_ID_INPUT_SELECTOR = 'input[type=hidden][name*=deleted_row_dom_ids]';
delegate('click', BUTTON_SELECTOR, (evt) => { delegate('click', BUTTON_SELECTOR, (evt) => {
evt.preventDefault(); evt.preventDefault();
@ -13,10 +14,20 @@ delegate('click', BUTTON_SELECTOR, (evt) => {
input.disabled = false; input.disabled = false;
input.value = true; input.value = true;
} }
row.querySelector(DOM_ID_INPUT_SELECTOR).disabled = false;
for (let champ of row.querySelectorAll(CHAMP_SELECTOR)) { for (let champ of row.querySelectorAll(CHAMP_SELECTOR)) {
champ.remove(); champ.remove();
} }
evt.target.remove(); evt.target.remove();
row.classList.remove('row'); row.classList.remove('row');
// We could debounce the autosave request, so that row removal would be batched
// with the next changes.
// However *adding* a new repetition row isn't debounced (changes are immediately
// effective server-side).
// So, to avoid ordering issues, enqueue an autosave request as soon as the row
// is removed.
fire(row, 'autosave:trigger');
}); });

View file

@ -1,4 +1,4 @@
import { fire, timeoutable } from '@utils'; import { ajax, fire, timeoutable } from '@utils';
// Manages a queue of Autosave operations, // Manages a queue of Autosave operations,
// and sends `autosave:*` events to indicate the state of the requests. // and sends `autosave:*` events to indicate the state of the requests.
@ -34,21 +34,20 @@ export default class AutoSaveController {
return reject(formDataError); return reject(formDataError);
} }
const fetchOptions = { const params = {
method: form.method, url: form.action,
body: formData, type: form.method,
credentials: 'same-origin', data: formData,
headers: { Accept: 'application/json' } dataType: 'script'
}; };
return window.fetch(form.action, fetchOptions).then((response) => { return ajax(params)
if (response.ok) { .then(({ response }) => {
resolve(response); resolve(response);
} else { })
const message = `Network request failed (${response.status}, "${response.statusText}")`; .catch((error) => {
reject(new Error(message)); reject(error);
} });
});
}); });
// Time out the request after a while, to avoid recent requests not starting // Time out the request after a while, to avoid recent requests not starting

View file

@ -16,26 +16,40 @@ const AUTOSAVE_STATUS_VISIBLE_DURATION = gon.autosave.status_visible_duration;
// Create a controller responsible for queuing autosave operations. // Create a controller responsible for queuing autosave operations.
const autoSaveController = new AutoSaveController(); const autoSaveController = new AutoSaveController();
// Whenever a 'change' event is triggered on one of the form inputs, try to autosave. function enqueueAutosaveRequest() {
const form = document.querySelector(FORM_SELECTOR);
const formSelector = 'form#dossier-edit-form.autosave-enabled';
const formInputsSelector = `${formSelector} input:not([type=file]), ${formSelector} select, ${formSelector} textarea`;
delegate(
'change',
formInputsSelector,
debounce(() => {
const form = document.querySelector(formSelector);
autoSaveController.enqueueAutosaveRequest(form);
}, AUTOSAVE_DEBOUNCE_DELAY)
);
delegate('click', '.autosave-retry', () => {
const form = document.querySelector(formSelector);
autoSaveController.enqueueAutosaveRequest(form); autoSaveController.enqueueAutosaveRequest(form);
}
//
// Whenever a 'change' event is triggered on one of the form inputs, try to autosave.
//
const FORM_SELECTOR = 'form#dossier-edit-form.autosave-enabled';
const INPUTS_SELECTOR = `${FORM_SELECTOR} input:not([type=file]), ${FORM_SELECTOR} select, ${FORM_SELECTOR} textarea`;
const RETRY_BUTTON_SELECTOR = '.autosave-retry';
// When an autosave is requested programatically, auto-save the form immediately
addEventListener('autosave:trigger', (event) => {
const form = event.target.closest('form');
if (form && form.classList.contains('autosave-enabled')) {
enqueueAutosaveRequest();
}
}); });
// When the "Retry" button is clicked, auto-save the form immediately
delegate('click', RETRY_BUTTON_SELECTOR, enqueueAutosaveRequest);
// When an input changes, batches changes for N seconds, then auto-save the form
delegate(
'change',
INPUTS_SELECTOR,
debounce(enqueueAutosaveRequest, AUTOSAVE_DEBOUNCE_DELAY)
);
//
// Display some UI during the autosave // Display some UI during the autosave
//
addEventListener('autosave:enqueue', () => { addEventListener('autosave:enqueue', () => {
disable(document.querySelector('button.autosave-retry')); disable(document.querySelector('button.autosave-retry'));

View file

@ -4,8 +4,9 @@ class FindDubiousProceduresJob < CronJob
FORBIDDEN_KEYWORDS = [ FORBIDDEN_KEYWORDS = [
'NIR', 'NIRPP', 'race', 'religion', 'NIR', 'NIRPP', 'race', 'religion',
'carte bancaire', 'carte bleue', 'sécurité sociale', 'carte bancaire', 'carte bleue', 'sécurité sociale',
'agdref', 'handicap', 'syndicat', 'syndical', 'agdref', 'syndicat', 'syndical',
'parti politique', 'opinion politique', 'bord politique', 'courant politique' 'parti politique', 'opinion politique', 'bord politique', 'courant politique',
'médical', 'handicap', 'maladie', 'allergie', 'hospitalisé', 'RQTH', 'vaccin'
] ]
def perform(*args) def perform(*args)

View file

@ -1,7 +1,11 @@
- champs = champ.rows.last - champs = champ.rows.last
- if champs.present? - if champs.present?
- index = (champ.rows.size - 1) * champs.size - index = (champ.rows.size - 1) * champs.size
%div{ class: "row row-#{champs.first.row}" } - row_dom_id = "row-#{SecureRandom.hex(4)}"
%div{ class: "row row-#{champs.first.row}", id: row_dom_id }
-# Tell the controller which DOM element should be removed once the row deletion is successful
= hidden_field_tag 'deleted_row_dom_ids[]', row_dom_id, disabled: true
- champs.each.with_index(index) do |champ, index| - champs.each.with_index(index) do |champ, index|
= fields_for "#{attribute}[#{index}]", champ do |form| = fields_for "#{attribute}[#{index}]", champ do |form|
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form } = render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form }

View file

@ -1,12 +1,15 @@
- if champ.options? - if champ.options?
- if champ.render_as_radios? - if champ.render_as_radios?
%fieldset.radios %fieldset.radios
%legend.mandatory-explanation
Sélectionnez une des valeurs
- champ.enabled_non_empty_options.each do |option| - champ.enabled_non_empty_options.each do |option|
%label %label
= form.radio_button :value, option = form.radio_button :value, option
= option = option
- if !champ.mandatory?
%label.blank-radio
= form.radio_button :value, ''
Non renseigné
- else - else
= form.select :value, = form.select :value,
champ.options, champ.options,

View file

@ -1,6 +1,10 @@
%div{ class: "repetition-#{form.index}" } %div{ class: "repetition-#{form.index}" }
- champ.rows.each do |champs| - champ.rows.each do |champs|
%div{ class: "row row-#{champs.first.row}" } - row_dom_id = "row-#{SecureRandom.hex(4)}"
%div{ class: "row row-#{champs.first.row}", id: row_dom_id }
-# Tell the controller which DOM element should be removed once the row deletion is successful
= hidden_field_tag 'deleted_row_dom_ids[]', row_dom_id, disabled: true
- champs.each do |champ| - champs.each do |champ|
= form.fields_for :champs, champ do |form| = form.fields_for :champs, champ do |form|
= render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form } = render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form }

View file

@ -0,0 +1,3 @@
<% (params['deleted_row_dom_ids'] || []).each do |deleted_row_dom_id| %>
<%= remove_element('#' + deleted_row_dom_id) %>
<% end %>

View file

@ -19,7 +19,7 @@
"debounce": "^1.2.0", "debounce": "^1.2.0",
"dom4": "^2.1.5", "dom4": "^2.1.5",
"email-butler": "^1.0.13", "email-butler": "^1.0.13",
"highcharts": "^8.1.0", "highcharts": "^8.1.1",
"intersection-observer": "^0.10.0", "intersection-observer": "^0.10.0",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"mapbox-gl": "^1.11.1", "mapbox-gl": "^1.11.1",

View file

@ -0,0 +1,100 @@
describe 'shared/dossiers/edit.html.haml', type: :view do
before do
allow(controller).to receive(:current_user).and_return(dossier.user)
end
subject { render 'shared/dossiers/edit.html.haml', dossier: dossier, apercu: false }
context 'when there are some champs' do
let(:dossier) { create(:dossier) }
let(:champ_checkbox) { create(:champ_checkbox, dossier: dossier, value: 'on') }
let(:champ_header_section) { create(:champ_header_section, dossier: dossier, value: 'Section') }
let(:champ_explication) { create(:champ_explication, dossier: dossier, value: 'mazette') }
let(:champ_dossier_link) { create(:champ_dossier_link, dossier: dossier, value: dossier.id) }
let(:champ_textarea) { create(:champ_textarea, dossier: dossier, value: 'Some long text in a textarea.') }
let(:champs) { [champ_checkbox, champ_header_section, champ_explication, champ_dossier_link, champ_textarea] }
before { dossier.champs << champs }
it 'renders labels and editable values of champs' do
expect(subject).to have_field(champ_checkbox.libelle, checked: true)
expect(subject).to have_css(".header-section", text: champ_header_section.libelle)
expect(subject).to have_text(champ_explication.libelle)
expect(subject).to have_field(champ_dossier_link.libelle, with: champ_dossier_link.value)
expect(subject).to have_field(champ_textarea.libelle, with: champ_textarea.value)
end
end
context 'with a single-value list' do
let(:dossier) { create(:dossier) }
let(:type_de_champ) { create(:type_de_champ_drop_down_list, mandatory: mandatory, procedure: dossier.procedure) }
let(:champ) { create(:champ_drop_down_list, dossier: dossier, type_de_champ: type_de_champ) }
let(:options) { type_de_champ.drop_down_list_options }
let(:enabled_options) { type_de_champ.drop_down_list_enabled_non_empty_options }
let(:mandatory) { true }
before { dossier.champs << champ }
context 'when the list is short' do
it 'renders the list as radio buttons' do
expect(subject).to have_selector('input[type=radio]', count: enabled_options.count)
end
context 'when the champ is optional' do
let(:mandatory) { false }
it 'allows unselecting a previously selected value' do
expect(subject).to have_selector('input[type=radio]', count: enabled_options.count + 1)
expect(subject).to have_unchecked_field('Non renseigné', count: 1)
end
end
end
context 'when the list is long' do
let(:type_de_champ) { create(:type_de_champ_drop_down_list, :long, procedure: dossier.procedure) }
it 'renders the list as a dropdown' do
expect(subject).to have_select(type_de_champ.libelle, options: options)
end
end
end
context 'with a multiple-values list' do
let(:dossier) { create(:dossier) }
let(:type_de_champ) { create(:type_de_champ_multiple_drop_down_list, procedure: dossier.procedure) }
let(:champ) { create(:champ_multiple_drop_down_list, dossier: dossier, type_de_champ: type_de_champ) }
let(:options) { type_de_champ.drop_down_list_options }
let(:enabled_options) { type_de_champ.drop_down_list_enabled_non_empty_options }
before { dossier.champs << champ }
context 'when the list is short' do
it 'renders the list as checkboxes' do
expect(subject).to have_selector('input[type=checkbox]', count: enabled_options.count)
end
end
context 'when the list is long' do
let(:type_de_champ) { create(:type_de_champ_multiple_drop_down_list, :long, procedure: dossier.procedure) }
it 'renders the list as a multiple-selection dropdown' do
expect(subject).to have_selector('select.select2')
end
end
end
context 'with a routed procedure' do
let(:procedure) do
create(:procedure,
:routee,
routing_criteria_name: 'departement')
end
let(:dossier) { create(:dossier, procedure: procedure) }
let(:champs) { [] }
it 'renders the routing criteria name and its value' do
expect(subject).to have_field(procedure.routing_criteria_name)
expect(subject).to include(dossier.groupe_instructeur.label)
end
end
end

View file

@ -4674,10 +4674,10 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
highcharts@^8.1.0: highcharts@^8.1.1:
version "8.1.0" version "8.1.1"
resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-8.1.0.tgz#f93adaf8d53b0f83c74c9854f0ad10baec010d97" resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-8.1.1.tgz#7dc011e260289ab64d775807df0d13b85ed88338"
integrity sha512-4KXq9t2/PU0cqKUtET7om9Kh5AyOinIn4vYi62oYVsb4ql5wyUYW06f9Si/ERG2Thoy/rcwNmR77upKjg8xhqQ== integrity sha512-DSkI+fAqkqYDslOVLcEk8DX7W9itRIwzsdS0uVEOnVf0LF1hSKZtDINHP7ze/uBN9NdWQV9HydtiPTrkLx0lXg==
highlight-words-core@1.2.2: highlight-words-core@1.2.2:
version "1.2.2" version "1.2.2"