Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ETQ usager je ne veux pas pouvoir saisir un nombre invalide sans feedback #9516

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/assets/stylesheets/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@
input[type=email],
input[type=password],
input[type=number],
input[inputmode=numeric],
input[inputmode=decimal],
input[type=tel] {
max-width: 500px;
}
Expand All @@ -315,6 +317,8 @@
&[type='date'],
&[type='tel'],
&[type='number'],
&[inputmode='numeric'],
&[inputmode='decimal'],
&[type='datetime-local'] {
width: 33.33%;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
= @form.number_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, step: :any, required: @champ.required?))
= @form.text_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, required: @champ.required?, pattern: "-?[0-9]+([\.,][0-9]{1,3})?", inputmode: :decimal, data: { controller: 'format decimal-number-input', format: :decimal }))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autoriser les espaces ? (mm chose pour les nombres entiers)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes je ferai ça dans une autre PR, ça touche à d'autres aspects: le formatteur JS et le backend qui doit les supprimer pour parser le string en nombre.
Au passage j'avais aussi passé du temps à essayer de cabler un truc plus général en guise de séparateur de milliers (pour accepter aussi les , et .) mais ça mène très trop loin car ces caractères sont sont aussi possibles comme séparateur de décimales, et un usager peut saisir dans un format autre que la locale de son navigateur…

Original file line number Diff line number Diff line change
@@ -1 +1 @@
= @form.number_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, placeholder: 5, required: @champ.required?))
= @form.text_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, pattern: "[0-9]*", inputmode: :numeric, required: @champ.required?, data: { controller: 'format', format: :integer }))
Original file line number Diff line number Diff line change
@@ -1 +1 @@
= @form.number_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, placeholder: @champ.libelle, required: @champ.required?))
= @form.text_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, placeholder: @champ.libelle, required: @champ.required?, pattern: "[0-9]*", inputmode: :decimal))
2 changes: 2 additions & 0 deletions app/javascript/controllers/autosave_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ export class AutosaveController extends ApplicationController {
formData.append(input.name, input.value);
}
} else {
// NOTE: some type inputs (like number) have an empty input.value
// when the filled value is invalid (not a number) so we avoid them
formData.append(input.name, input.value);
}
}
Expand Down
35 changes: 35 additions & 0 deletions app/javascript/controllers/decimal_number_input_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ApplicationController } from './application_controller';

export class DecimalNumberInputController extends ApplicationController {
connect() {
const value = this.inputElement.value;

if (value) {
this.formatValue(value);
}
}

formatValue(value: string) {
const number = parseFloat(value);

if (isNaN(number)) {
return;
}

this.inputElement.value = number.toLocaleString();
this.emitInputEvent(); // trigger format controller
}

private get inputElement(): HTMLInputElement {
return this.element as HTMLInputElement;
}

private emitInputEvent() {
const event = new InputEvent('input', {
bubbles: true,
cancelable: true
});

this.inputElement.dispatchEvent(event);
}
}
17 changes: 17 additions & 0 deletions app/javascript/controllers/format_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export class FormatController extends ApplicationController {
const target = event.target as HTMLInputElement;
target.value = this.formatInteger(target.value);
});
break;
case 'decimal':
this.on('input', (event) => {
const target = event.target as HTMLInputElement;
target.value = this.formatDecimal(target.value);
});
break;
}
}

Expand All @@ -39,4 +46,14 @@ export class FormatController extends ApplicationController {
private formatInteger(value: string) {
return value.replace(/[^\d]/g, '');
}

private formatDecimal(value: string) {
// Le séparateur de décimales est toujours après le séparateur de milliers (un point ou une virgule).
// S'il n'y a qu'un seul séparateur, on considère que c'est celui des décimales.
// S'il n'y en a pas, ça n'a pas d'effet.
const decimalSeparator =
value.lastIndexOf(',') > value.lastIndexOf('.') ? ',' : '.';

return value.replace(new RegExp(`[^\\d${decimalSeparator}]`, 'g'), '');
}
}
9 changes: 9 additions & 0 deletions app/models/champs/decimal_number_champ.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class Champs::DecimalNumberChamp < Champ
before_validation :format_value
validates :value, numericality: {
allow_nil: true,
allow_blank: true,
Expand All @@ -17,7 +18,15 @@ def for_api

private

def format_value
return if value.blank?

self.value = value.tr(",", ".")
end

def processed_value
return if invalid?

value&.to_f
end
end
2 changes: 2 additions & 0 deletions app/models/champs/integer_number_champ.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def for_api
private

def processed_value
return if invalid?

value&.to_i
end
end
2 changes: 1 addition & 1 deletion config/locales/models/champs/decimal_number_champ/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ en:
attributes:
champs/decimal_number_champ:
hints:
value: "You can enter up to 3 decimal places after the decimal point. Exemple: 3.14"
value: "You can enter up to 3 decimal places after the decimal point. Exemple: 3.141"
2 changes: 1 addition & 1 deletion config/locales/models/champs/decimal_number_champ/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ fr:
attributes:
champs/decimal_number_champ:
hints:
value: "Vous pouvez saisir jusqu’à 3 décimales après la virgule. Exemple: 3,14"
value: "Vous pouvez saisir jusqu’à 3 décimales après la virgule. Exemple: 3,141"
30 changes: 30 additions & 0 deletions spec/controllers/users/dossiers_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,36 @@
it { expect(first_champ.reload.value).to eq('beautiful value') }
it { expect(response).to have_http_status(:ok) }
end

context 'decimal number champ separator' do
let (:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :decimal_number }]) }
let (:submit_payload) do
{
id: dossier.id,
dossier: {
champs_public_attributes: { first_champ.id => { id: first_champ.id, value: value } }
}
}
end

context 'when spearator is dot' do
let(:value) { '3.14' }

it "saves the value" do
subject
expect(first_champ.reload.value).to eq('3.14')
end
end

context 'when spearator is comma' do
let(:value) { '3,14' }

it "saves the value" do
subject
expect(first_champ.reload.value).to eq('3.14')
end
end
end
end

describe '#update en_construction' do
Expand Down
12 changes: 12 additions & 0 deletions spec/models/logic/champ_value_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,25 @@

it { is_expected.to be nil }
end

context 'with invalid value' do
before { champ.value = 'environ 300' }

it { is_expected.to be nil }
end
end

context 'decimal tdc' do
let(:champ) { create(:champ_decimal_number, value: '42.01') }

it { expect(champ_value(champ.stable_id).type([champ.type_de_champ])).to eq(:number) }
it { is_expected.to eq(42.01) }

context 'with invalid value' do
before { champ.value = 'racine de 2' }

it { is_expected.to be nil }
end
end

context 'dropdown tdc' do
Expand Down
28 changes: 27 additions & 1 deletion spec/system/users/brouillon_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,13 @@
end.to change { Champ.count }
end

let(:simple_procedure) { create(:procedure, :published, :for_individual, types_de_champ_public: [{ mandatory: true, libelle: 'texte obligatoire' }, { mandatory: false, libelle: 'texte optionnel' }]) }
let(:simple_procedure) {
create(:procedure, :published, :for_individual, types_de_champ_public: [
{ mandatory: true, libelle: 'texte obligatoire' }, { mandatory: false, libelle: 'texte optionnel' },
{ mandatory: false, libelle: "nombre entier", type: :integer_number },
{ mandatory: false, libelle: "nombre décimal", type: :decimal_number }
])
}

scenario 'save an incomplete dossier as draft but cannot not submit it', js: true, retry: 3 do
log_in(user, simple_procedure)
Expand All @@ -170,6 +176,26 @@
expect(page).to have_current_path(merci_dossier_path(user_dossier))
end

scenario 'numbers champs formatting', js: true, retry: 3 do
log_in(user, simple_procedure)
fill_individual

fill_in('nombre entier', with: '300 environ')
wait_until {
champ_value_for('nombre entier') == '300'
}

fill_in('nombre décimal', with: '123 456,78')
wait_until {
champ_value_for('nombre décimal') == '123456.78'
}

fill_in('nombre décimal', with: '1,234.56')
wait_until {
champ_value_for('nombre décimal') == '1234.56'
}
end

scenario 'extends dossier experation date more than one time, ', js: true, retry: 3 do
simple_procedure.update(procedure_expires_when_termine_enabled: true)
user_old_dossier = create(:dossier,
Expand Down