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;
|
||||
}
|
||||
|
||||
&.blank-radio {
|
||||
color: $dark-grey;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
input[type=radio] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -163,7 +163,7 @@ module Users
|
|||
|
||||
respond_to do |format|
|
||||
format.html { render :brouillon }
|
||||
format.json { render json: {}, status: :ok }
|
||||
format.js { render :brouillon }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -62,7 +62,11 @@ module ApplicationHelper
|
|||
script = "(function() {";
|
||||
script << "var el = document.querySelector('#{selector}');"
|
||||
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 << "})();"
|
||||
# rubocop:disable Rails/OutputSafety
|
||||
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 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) => {
|
||||
evt.preventDefault();
|
||||
|
@ -13,10 +14,20 @@ delegate('click', BUTTON_SELECTOR, (evt) => {
|
|||
input.disabled = false;
|
||||
input.value = true;
|
||||
}
|
||||
row.querySelector(DOM_ID_INPUT_SELECTOR).disabled = false;
|
||||
|
||||
for (let champ of row.querySelectorAll(CHAMP_SELECTOR)) {
|
||||
champ.remove();
|
||||
}
|
||||
|
||||
evt.target.remove();
|
||||
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,
|
||||
// and sends `autosave:*` events to indicate the state of the requests.
|
||||
|
@ -34,21 +34,20 @@ export default class AutoSaveController {
|
|||
return reject(formDataError);
|
||||
}
|
||||
|
||||
const fetchOptions = {
|
||||
method: form.method,
|
||||
body: formData,
|
||||
credentials: 'same-origin',
|
||||
headers: { Accept: 'application/json' }
|
||||
const params = {
|
||||
url: form.action,
|
||||
type: form.method,
|
||||
data: formData,
|
||||
dataType: 'script'
|
||||
};
|
||||
|
||||
return window.fetch(form.action, fetchOptions).then((response) => {
|
||||
if (response.ok) {
|
||||
return ajax(params)
|
||||
.then(({ response }) => {
|
||||
resolve(response);
|
||||
} else {
|
||||
const message = `Network request failed (${response.status}, "${response.statusText}")`;
|
||||
reject(new Error(message));
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
// 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.
|
||||
const autoSaveController = new AutoSaveController();
|
||||
|
||||
// Whenever a 'change' event is triggered on one of the form inputs, try to autosave.
|
||||
|
||||
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);
|
||||
function enqueueAutosaveRequest() {
|
||||
const form = document.querySelector(FORM_SELECTOR);
|
||||
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
|
||||
//
|
||||
|
||||
addEventListener('autosave:enqueue', () => {
|
||||
disable(document.querySelector('button.autosave-retry'));
|
||||
|
|
|
@ -4,8 +4,9 @@ class FindDubiousProceduresJob < CronJob
|
|||
FORBIDDEN_KEYWORDS = [
|
||||
'NIR', 'NIRPP', 'race', 'religion',
|
||||
'carte bancaire', 'carte bleue', 'sécurité sociale',
|
||||
'agdref', 'handicap', 'syndicat', 'syndical',
|
||||
'parti politique', 'opinion politique', 'bord politique', 'courant politique'
|
||||
'agdref', 'syndicat', 'syndical',
|
||||
'parti politique', 'opinion politique', 'bord politique', 'courant politique',
|
||||
'médical', 'handicap', 'maladie', 'allergie', 'hospitalisé', 'RQTH', 'vaccin'
|
||||
]
|
||||
|
||||
def perform(*args)
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
- champs = champ.rows.last
|
||||
- if champs.present?
|
||||
- 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|
|
||||
= fields_for "#{attribute}[#{index}]", champ do |form|
|
||||
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form }
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
- if champ.options?
|
||||
- if champ.render_as_radios?
|
||||
%fieldset.radios
|
||||
%legend.mandatory-explanation
|
||||
Sélectionnez une des valeurs
|
||||
- champ.enabled_non_empty_options.each do |option|
|
||||
%label
|
||||
= form.radio_button :value, option
|
||||
= option
|
||||
- if !champ.mandatory?
|
||||
%label.blank-radio
|
||||
= form.radio_button :value, ''
|
||||
Non renseigné
|
||||
|
||||
- else
|
||||
= form.select :value,
|
||||
champ.options,
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
%div{ class: "repetition-#{form.index}" }
|
||||
- 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|
|
||||
= form.fields_for :champs, champ do |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",
|
||||
"dom4": "^2.1.5",
|
||||
"email-butler": "^1.0.13",
|
||||
"highcharts": "^8.1.0",
|
||||
"highcharts": "^8.1.1",
|
||||
"intersection-observer": "^0.10.0",
|
||||
"jquery": "^3.5.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"
|
||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||
|
||||
highcharts@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-8.1.0.tgz#f93adaf8d53b0f83c74c9854f0ad10baec010d97"
|
||||
integrity sha512-4KXq9t2/PU0cqKUtET7om9Kh5AyOinIn4vYi62oYVsb4ql5wyUYW06f9Si/ERG2Thoy/rcwNmR77upKjg8xhqQ==
|
||||
highcharts@^8.1.1:
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-8.1.1.tgz#7dc011e260289ab64d775807df0d13b85ed88338"
|
||||
integrity sha512-DSkI+fAqkqYDslOVLcEk8DX7W9itRIwzsdS0uVEOnVf0LF1hSKZtDINHP7ze/uBN9NdWQV9HydtiPTrkLx0lXg==
|
||||
|
||||
highlight-words-core@1.2.2:
|
||||
version "1.2.2"
|
||||
|
|
Loading…
Reference in a new issue