Merge pull request #5502 from betagouv/dev
This commit is contained in:
commit
23be513b4f
14 changed files with 192 additions and 44 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'));
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
3
app/views/users/dossiers/brouillon.js.erb
Normal file
3
app/views/users/dossiers/brouillon.js.erb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<% (params['deleted_row_dom_ids'] || []).each do |deleted_row_dom_id| %>
|
||||||
|
<%= remove_element('#' + deleted_row_dom_id) %>
|
||||||
|
<% end %>
|
|
@ -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",
|
||||||
|
|
100
spec/views/shared/dossiers/_edit.html.haml_spec.rb
Normal file
100
spec/views/shared/dossiers/_edit.html.haml_spec.rb
Normal 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
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue