From 6b5a034450b7aad671f36dde73a0ae0be8986760 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 17 Feb 2025 16:23:07 +0100 Subject: [PATCH 01/40] WIP support new sentiment_linear_scale custom field type in BE --- back/app/models/custom_field.rb | 5 +++-- back/app/services/field_visitor_service.rb | 4 ++++ back/app/services/idea_custom_fields_service.rb | 5 +++-- back/app/services/json_schema_generator_service.rb | 8 ++++++++ back/app/services/survey_results_generator_service.rb | 4 ++++ back/app/services/ui_schema_generator_service.rb | 10 ++++++++++ .../20250217295025_add_follow_up_to_custom_fields.rb | 10 ++++++++++ back/db/structure.sql | 8 ++++++-- back/lib/participation_method/native_survey.rb | 2 +- back/spec/models/custom_field_spec.rb | 4 ++++ .../services/json_schema_generator_service_spec.rb | 2 ++ back/spec/services/ui_schema_generator_service_spec.rb | 2 ++ 12 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb diff --git a/back/app/models/custom_field.rb b/back/app/models/custom_field.rb index 050fb13a5313..8bf80a76ba75 100644 --- a/back/app/models/custom_field.rb +++ b/back/app/models/custom_field.rb @@ -38,6 +38,7 @@ # linear_scale_label_9_multiloc :jsonb not null # linear_scale_label_10_multiloc :jsonb not null # linear_scale_label_11_multiloc :jsonb not null +# ask_follow_up :boolean default(FALSE), not null # # Indexes # @@ -64,7 +65,7 @@ class CustomField < ApplicationRecord INPUT_TYPES = %w[ checkbox date file_upload files html html_multiloc image_files linear_scale rating multiline_text multiline_text_multiloc multiselect multiselect_image number page point line polygon select select_image shapefile_upload text text_multiloc - topic_ids section cosponsor_ids ranking matrix_linear_scale + topic_ids section cosponsor_ids ranking matrix_linear_scale sentiment_linear_scale ].freeze CODES = %w[ author_id birthyear body_multiloc budget domicile gender idea_files_attributes idea_images_attributes @@ -141,7 +142,7 @@ def supports_geojson? end def supports_linear_scale? - %w[linear_scale matrix_linear_scale rating].include?(input_type) + %w[linear_scale matrix_linear_scale sentiment_linear_scale rating].include?(input_type) end def supports_matrix_statements? diff --git a/back/app/services/field_visitor_service.rb b/back/app/services/field_visitor_service.rb index 527fbb7160b2..5e8f420fc9c6 100644 --- a/back/app/services/field_visitor_service.rb +++ b/back/app/services/field_visitor_service.rb @@ -109,6 +109,10 @@ def visit_ranking(field) default(field) end + def visit_sentiment_linear_scale(field) + default(field) + end + def visit_page(field) default(field) end diff --git a/back/app/services/idea_custom_fields_service.rb b/back/app/services/idea_custom_fields_service.rb index 5e8732bcdcec..7c7f1bbd2281 100644 --- a/back/app/services/idea_custom_fields_service.rb +++ b/back/app/services/idea_custom_fields_service.rb @@ -40,13 +40,14 @@ def submittable_fields_with_other_options # Used in the printable PDF export def printable_fields - ignore_field_types = %w[section page date files image_files point file_upload shapefile_upload topic_ids cosponsor_ids ranking matrix_linear_scale] + ignore_field_types = %w[section page date files image_files point file_upload shapefile_upload topic_ids cosponsor_ids ranking matrix_linear_scale sentiment_linear_scale] fields = enabled_fields.reject { |field| ignore_field_types.include? field.input_type } insert_other_option_text_fields(fields) end def importable_fields - ignore_field_types = %w[page section date files image_files file_upload shapefile_upload point line polygon cosponsor_ids ranking matrix_linear_scale] + # TODO: Decide if sentiment_lienar_scale can be imported? + ignore_field_types = %w[page section date files image_files file_upload shapefile_upload point line polygon cosponsor_ids ranking matrix_linear_scale sentiment_linear_scale] enabled_fields_with_other_options.reject { |field| ignore_field_types.include? field.input_type } end diff --git a/back/app/services/json_schema_generator_service.rb b/back/app/services/json_schema_generator_service.rb index c3ab06ae0b71..7a3b9ccc1dcb 100644 --- a/back/app/services/json_schema_generator_service.rb +++ b/back/app/services/json_schema_generator_service.rb @@ -262,6 +262,14 @@ def visit_linear_scale(field) } end + def visit_sentiment_linear_scale(field) + { + type: 'number', + minimum: 1, + maximum: 5 + } + end + def visit_rating(field) { type: 'number', diff --git a/back/app/services/survey_results_generator_service.rb b/back/app/services/survey_results_generator_service.rb index 01d791b3100f..0da26dc06651 100644 --- a/back/app/services/survey_results_generator_service.rb +++ b/back/app/services/survey_results_generator_service.rb @@ -80,6 +80,10 @@ def visit_linear_scale(field) visit_select_base(field) end + def visit_sentiment_linear_scale(field) + visit_select_base(field) + end + def visit_matrix_linear_scale(field) core_field_attributes(field).merge({ multilocs: { answer: build_scaled_input_multilocs(field) }, diff --git a/back/app/services/ui_schema_generator_service.rb b/back/app/services/ui_schema_generator_service.rb index e7b94be6aeac..db0e8a4a798f 100644 --- a/back/app/services/ui_schema_generator_service.rb +++ b/back/app/services/ui_schema_generator_service.rb @@ -95,6 +95,16 @@ def visit_linear_scale(field) end end + def visit_sentiment_linear_scale(field) + default(field).tap do |ui_field| + ui_field[:options][:linear_scale_label1] = multiloc_service.t(field.linear_scale_label_1_multiloc) + ui_field[:options][:linear_scale_label2] = multiloc_service.t(field.linear_scale_label_2_multiloc) + ui_field[:options][:linear_scale_label3] = multiloc_service.t(field.linear_scale_label_3_multiloc) + ui_field[:options][:linear_scale_label4] = multiloc_service.t(field.linear_scale_label_4_multiloc) + ui_field[:options][:linear_scale_label5] = multiloc_service.t(field.linear_scale_label_5_multiloc) + end + end + def visit_matrix_linear_scale(field) visit_linear_scale(field).tap do |ui_field| ui_field[:options][:statements] = field.matrix_statements.map do |statement| diff --git a/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb b/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb new file mode 100644 index 000000000000..3fa8fbef5053 --- /dev/null +++ b/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb @@ -0,0 +1,10 @@ +class AddFollowUpToCustomFields < ActiveRecord::Migration[7.0] + class StubCustomField < ApplicationRecord + self.table_name = 'custom_fields' + end + + def change + add_column :custom_fields, :ask_follow_up, :boolean, default: false, null: false + end +end + \ No newline at end of file diff --git a/back/db/structure.sql b/back/db/structure.sql index 30223523daa0..fd0db685c842 100644 --- a/back/db/structure.sql +++ b/back/db/structure.sql @@ -1564,7 +1564,8 @@ CREATE TABLE public.phases ( manual_votes_count integer DEFAULT 0 NOT NULL, manual_voters_amount integer, manual_voters_last_updated_by_id uuid, - manual_voters_last_updated_at timestamp(6) without time zone + manual_voters_last_updated_at timestamp(6) without time zone, + native_survey_method character varying ); @@ -2097,7 +2098,8 @@ CREATE TABLE public.custom_fields ( linear_scale_label_8_multiloc jsonb DEFAULT '{}'::jsonb NOT NULL, linear_scale_label_9_multiloc jsonb DEFAULT '{}'::jsonb NOT NULL, linear_scale_label_10_multiloc jsonb DEFAULT '{}'::jsonb NOT NULL, - linear_scale_label_11_multiloc jsonb DEFAULT '{}'::jsonb NOT NULL + linear_scale_label_11_multiloc jsonb DEFAULT '{}'::jsonb NOT NULL, + ask_follow_up boolean DEFAULT false NOT NULL ); @@ -6826,6 +6828,8 @@ ALTER TABLE ONLY public.ideas_topics SET search_path TO public,shared_extensions; INSERT INTO "schema_migrations" (version) VALUES +('20250217295025'), +('20250211103910'), ('20250204143605'), ('20250120125531'), ('20250117121004'), diff --git a/back/lib/participation_method/native_survey.rb b/back/lib/participation_method/native_survey.rb index 099a7b4e0e85..5b272b61fe9e 100644 --- a/back/lib/participation_method/native_survey.rb +++ b/back/lib/participation_method/native_survey.rb @@ -9,7 +9,7 @@ def self.method_str def allowed_extra_field_input_types %w[page number linear_scale rating text multiline_text select multiselect multiselect_image file_upload shapefile_upload point line polygon - ranking matrix_linear_scale] + ranking matrix_linear_scale sentiment_linear_scale] end def assign_defaults(input) diff --git a/back/spec/models/custom_field_spec.rb b/back/spec/models/custom_field_spec.rb index b44ddf330f4f..3a34530a7d40 100644 --- a/back/spec/models/custom_field_spec.rb +++ b/back/spec/models/custom_field_spec.rb @@ -114,6 +114,10 @@ def visit_shapefile_upload(_field) def visit_ranking(_field) 'ranking from visitor' end + + def visit_sentiment_linear_scale(_field) + 'sentiment_linear_scale from visitor' + end end RSpec.describe CustomField do diff --git a/back/spec/services/json_schema_generator_service_spec.rb b/back/spec/services/json_schema_generator_service_spec.rb index 6b4d8f70b8a6..bd860730e38a 100644 --- a/back/spec/services/json_schema_generator_service_spec.rb +++ b/back/spec/services/json_schema_generator_service_spec.rb @@ -565,6 +565,8 @@ end end + # TODO: Add test for sentiment_linear_scale + describe '#visit_rating' do let(:field) { create(:custom_field_linear_scale, key: field_key) } diff --git a/back/spec/services/ui_schema_generator_service_spec.rb b/back/spec/services/ui_schema_generator_service_spec.rb index 5054f93d7c30..b3b40e20f40b 100644 --- a/back/spec/services/ui_schema_generator_service_spec.rb +++ b/back/spec/services/ui_schema_generator_service_spec.rb @@ -635,6 +635,8 @@ def generate_for_current_locale(fields) ) end + # TODO: Add test for sentiment linear scale + it 'returns the schema for the given field' do expect(generator.visit_linear_scale(field)).to eq({ type: 'Control', From dc4883444972bc94ea64481a1d30d57f1441aa9a Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Tue, 18 Feb 2025 13:39:45 +0100 Subject: [PATCH 02/40] Fix lint --- .../app/services/json_schema_generator_service.rb | 2 +- ...250217295025_add_follow_up_to_custom_fields.rb | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/back/app/services/json_schema_generator_service.rb b/back/app/services/json_schema_generator_service.rb index 7a3b9ccc1dcb..e31d07b839f7 100644 --- a/back/app/services/json_schema_generator_service.rb +++ b/back/app/services/json_schema_generator_service.rb @@ -262,7 +262,7 @@ def visit_linear_scale(field) } end - def visit_sentiment_linear_scale(field) + def visit_sentiment_linear_scale { type: 'number', minimum: 1, diff --git a/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb b/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb index 3fa8fbef5053..ebc4cd6e6e2b 100644 --- a/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb +++ b/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb @@ -1,10 +1,9 @@ class AddFollowUpToCustomFields < ActiveRecord::Migration[7.0] - class StubCustomField < ApplicationRecord - self.table_name = 'custom_fields' - end - - def change - add_column :custom_fields, :ask_follow_up, :boolean, default: false, null: false - end + class StubCustomField < ApplicationRecord + self.table_name = 'custom_fields' + end + + def change + add_column :custom_fields, :ask_follow_up, :boolean, default: false, null: false + end end - \ No newline at end of file From 0ab43bddd5dd3588b7f30d8913c108b047f84f1b Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Tue, 18 Feb 2025 14:03:00 +0100 Subject: [PATCH 03/40] Add spec and remove ToDo comments --- back/app/services/idea_custom_fields_service.rb | 1 - .../services/json_schema_generator_service_spec.rb | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/back/app/services/idea_custom_fields_service.rb b/back/app/services/idea_custom_fields_service.rb index 7c7f1bbd2281..598c9f3666ba 100644 --- a/back/app/services/idea_custom_fields_service.rb +++ b/back/app/services/idea_custom_fields_service.rb @@ -46,7 +46,6 @@ def printable_fields end def importable_fields - # TODO: Decide if sentiment_lienar_scale can be imported? ignore_field_types = %w[page section date files image_files file_upload shapefile_upload point line polygon cosponsor_ids ranking matrix_linear_scale sentiment_linear_scale] enabled_fields_with_other_options.reject { |field| ignore_field_types.include? field.input_type } end diff --git a/back/spec/services/json_schema_generator_service_spec.rb b/back/spec/services/json_schema_generator_service_spec.rb index bd860730e38a..d11d2541e0fd 100644 --- a/back/spec/services/json_schema_generator_service_spec.rb +++ b/back/spec/services/json_schema_generator_service_spec.rb @@ -565,8 +565,6 @@ end end - # TODO: Add test for sentiment_linear_scale - describe '#visit_rating' do let(:field) { create(:custom_field_linear_scale, key: field_key) } @@ -629,6 +627,18 @@ end end + describe '#visit_sentiment_linear_scale' do + let(:field) { create(:custom_field_sentiment_linear_scale, key: field_key) } + + it 'returns the schema for the given field' do + expect(generator.visit_sentiment_linear_scale).to eq({ + type: 'number', + minimum: 1, + maximum: 5 + }) + end + end + describe '#visit_matrix_linear_scale' do let(:field) do create(:custom_field_matrix_linear_scale, required: true, maximum: 5, key: field_key) From 9ff7fad1f07ca65c472feae4b29902bd44dfafed Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Tue, 18 Feb 2025 14:13:47 +0100 Subject: [PATCH 04/40] Add missing BE specs for sentiment_linear_scale --- .../services/json_schema_generator_service.rb | 2 +- back/spec/factories/custom_fields.rb | 40 +++++++++++++++++++ .../custom_field_params_service_spec.rb | 1 + .../json_schema_generator_service_spec.rb | 2 +- .../ui_schema_generator_service_spec.rb | 28 ++++++++++++- 5 files changed, 69 insertions(+), 4 deletions(-) diff --git a/back/app/services/json_schema_generator_service.rb b/back/app/services/json_schema_generator_service.rb index e31d07b839f7..2769336595dc 100644 --- a/back/app/services/json_schema_generator_service.rb +++ b/back/app/services/json_schema_generator_service.rb @@ -262,7 +262,7 @@ def visit_linear_scale(field) } end - def visit_sentiment_linear_scale + def visit_sentiment_linear_scale(_field) { type: 'number', minimum: 1, diff --git a/back/spec/factories/custom_fields.rb b/back/spec/factories/custom_fields.rb index edf8d5874bad..d071ccd13a9c 100644 --- a/back/spec/factories/custom_fields.rb +++ b/back/spec/factories/custom_fields.rb @@ -165,6 +165,46 @@ linear_scale_label_11_multiloc { {} } end + factory :custom_field_sentiment_linear_scale do + title_multiloc do + { + 'en' => 'We need a swimming pool.' + } + end + description_multiloc do + { + 'en' => 'Please indicate how strong you agree or disagree.' + } + end + input_type { 'linear_scale' } + maximum { 5 } + linear_scale_label_1_multiloc do + { + 'en' => 'Strongly disagree' + } + end + linear_scale_label_2_multiloc do + { + 'en' => 'Disagree' + } + end + linear_scale_label_3_multiloc do + { + 'en' => 'Neutral' + } + end + linear_scale_label_4_multiloc do + { + 'en' => 'Agree' + } + end + linear_scale_label_5_multiloc do + { + 'en' => 'Strongly agree' + } + end + end + factory :custom_field_rating do title_multiloc do { diff --git a/back/spec/services/custom_field_params_service_spec.rb b/back/spec/services/custom_field_params_service_spec.rb index c9d4187c2453..7ea964492e1f 100644 --- a/back/spec/services/custom_field_params_service_spec.rb +++ b/back/spec/services/custom_field_params_service_spec.rb @@ -13,6 +13,7 @@ create(:custom_field_shapefile_upload, key: 'shapefile_upload_field'), create(:custom_field_html_multiloc, key: 'html_multiloc_field'), create(:custom_field_linear_scale, key: 'linear_scale_field') + ] end diff --git a/back/spec/services/json_schema_generator_service_spec.rb b/back/spec/services/json_schema_generator_service_spec.rb index d11d2541e0fd..63b85e94aab3 100644 --- a/back/spec/services/json_schema_generator_service_spec.rb +++ b/back/spec/services/json_schema_generator_service_spec.rb @@ -631,7 +631,7 @@ let(:field) { create(:custom_field_sentiment_linear_scale, key: field_key) } it 'returns the schema for the given field' do - expect(generator.visit_sentiment_linear_scale).to eq({ + expect(generator.visit_sentiment_linear_scale(field)).to eq({ type: 'number', minimum: 1, maximum: 5 diff --git a/back/spec/services/ui_schema_generator_service_spec.rb b/back/spec/services/ui_schema_generator_service_spec.rb index b3b40e20f40b..bd866889ac54 100644 --- a/back/spec/services/ui_schema_generator_service_spec.rb +++ b/back/spec/services/ui_schema_generator_service_spec.rb @@ -635,8 +635,6 @@ def generate_for_current_locale(fields) ) end - # TODO: Add test for sentiment linear scale - it 'returns the schema for the given field' do expect(generator.visit_linear_scale(field)).to eq({ type: 'Control', @@ -661,6 +659,32 @@ def generate_for_current_locale(fields) end end + describe '#visit_sentiment_linear_scale' do + let(:field) do + create( + :custom_field_sentiment_linear_scale, + key: field_key + ) + end + + it 'returns the schema for the given field' do + expect(generator.visit_sentiment_linear_scale(field)).to eq({ + type: 'Control', + scope: "#/properties/#{field_key}", + label: 'We need a swimming pool.', + options: { + input_type: field.input_type, + description: 'Please indicate how strong you agree or disagree.', + linear_scale_label1: 'Strongly disagree', + linear_scale_label2: 'Disagree', + linear_scale_label3: 'Neutral', + linear_scale_label4: 'Agree', + linear_scale_label5: 'Strongly agree' + } + }) + end +end + describe '#visit_rating' do let(:field) do create( From 737963549e3dd1f51224a9048581ad42df4c7602 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Tue, 18 Feb 2025 14:17:39 +0100 Subject: [PATCH 05/40] Include ask_follow_up in json schema response and spens --- back/app/services/ui_schema_generator_service.rb | 1 + back/spec/factories/custom_fields.rb | 1 + back/spec/services/ui_schema_generator_service_spec.rb | 1 + 3 files changed, 3 insertions(+) diff --git a/back/app/services/ui_schema_generator_service.rb b/back/app/services/ui_schema_generator_service.rb index db0e8a4a798f..42726e4cf55c 100644 --- a/back/app/services/ui_schema_generator_service.rb +++ b/back/app/services/ui_schema_generator_service.rb @@ -97,6 +97,7 @@ def visit_linear_scale(field) def visit_sentiment_linear_scale(field) default(field).tap do |ui_field| + ui_field[:options][:ask_follow_up] = field.ask_follow_up ui_field[:options][:linear_scale_label1] = multiloc_service.t(field.linear_scale_label_1_multiloc) ui_field[:options][:linear_scale_label2] = multiloc_service.t(field.linear_scale_label_2_multiloc) ui_field[:options][:linear_scale_label3] = multiloc_service.t(field.linear_scale_label_3_multiloc) diff --git a/back/spec/factories/custom_fields.rb b/back/spec/factories/custom_fields.rb index d071ccd13a9c..b4d236a0545c 100644 --- a/back/spec/factories/custom_fields.rb +++ b/back/spec/factories/custom_fields.rb @@ -166,6 +166,7 @@ end factory :custom_field_sentiment_linear_scale do + ask_follow_up { false } title_multiloc do { 'en' => 'We need a swimming pool.' diff --git a/back/spec/services/ui_schema_generator_service_spec.rb b/back/spec/services/ui_schema_generator_service_spec.rb index bd866889ac54..c3177522fcea 100644 --- a/back/spec/services/ui_schema_generator_service_spec.rb +++ b/back/spec/services/ui_schema_generator_service_spec.rb @@ -675,6 +675,7 @@ def generate_for_current_locale(fields) options: { input_type: field.input_type, description: 'Please indicate how strong you agree or disagree.', + ask_follow_up: false, linear_scale_label1: 'Strongly disagree', linear_scale_label2: 'Disagree', linear_scale_label3: 'Neutral', From f9d6bd8d6f5c21a79b709c2480b3fe775a2bafd5 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Tue, 18 Feb 2025 16:22:49 +0100 Subject: [PATCH 06/40] Update specs and add missing support for ask_follow_up attribute --- .../web_api/v1/custom_field_serializer.rb | 18 ++++-- back/app/services/project_copy_service.rb | 1 + .../v1/admin/idea_custom_fields_controller.rb | 1 + .../update_all_native_survey_spec.rb | 60 +++++++++++++++++++ .../templates/serializers/custom_field.rb | 1 + back/spec/factories/custom_fields.rb | 4 +- .../v1/custom_field_serializer_spec.rb | 31 ++++++++++ 7 files changed, 109 insertions(+), 7 deletions(-) diff --git a/back/app/serializers/web_api/v1/custom_field_serializer.rb b/back/app/serializers/web_api/v1/custom_field_serializer.rb index 9e107fe1f0d1..0cb418817d73 100644 --- a/back/app/serializers/web_api/v1/custom_field_serializer.rb +++ b/back/app/serializers/web_api/v1/custom_field_serializer.rb @@ -24,6 +24,10 @@ class WebApi::V1::CustomFieldSerializer < WebApi::V1::BaseSerializer object.dropdown_layout_type? } + attribute :ask_follow_up, if: proc { |object, _params| + object.input_type == 'sentiment_linear_scale' + } + attribute :constraints do |object, params| if params[:constraints] params[:constraints][object.code&.to_sym] || {} @@ -35,11 +39,13 @@ class WebApi::V1::CustomFieldSerializer < WebApi::V1::BaseSerializer attributes :maximum, if: proc { |object, _params| object.supports_linear_scale? } attributes :linear_scale_label_1_multiloc, - :linear_scale_label_2_multiloc, - :linear_scale_label_3_multiloc, - :linear_scale_label_4_multiloc, - :linear_scale_label_5_multiloc, - :linear_scale_label_6_multiloc, + :linear_scale_label_2_multiloc, + :linear_scale_label_3_multiloc, + :linear_scale_label_4_multiloc, + :linear_scale_label_5_multiloc, + if: proc { |object, _params| object.input_type == 'sentiment_linear_scale' || object.linear_scale? || object.supports_matrix_statements? } + + attributes :linear_scale_label_6_multiloc, :linear_scale_label_7_multiloc, :linear_scale_label_8_multiloc, :linear_scale_label_9_multiloc, @@ -47,6 +53,8 @@ class WebApi::V1::CustomFieldSerializer < WebApi::V1::BaseSerializer :linear_scale_label_11_multiloc, if: proc { |object, _params| object.linear_scale? || object.supports_matrix_statements? } + + attributes :select_count_enabled, :maximum_select_count, :minimum_select_count, if: proc { |object, _params| object.multiselect? } diff --git a/back/app/services/project_copy_service.rb b/back/app/services/project_copy_service.rb index d9b360a70ef9..9556911d33a9 100644 --- a/back/app/services/project_copy_service.rb +++ b/back/app/services/project_copy_service.rb @@ -160,6 +160,7 @@ def yml_custom_fields(shift_timestamps: 0) 'answer_visible_to' => field.answer_visible_to, 'hidden' => field.hidden, 'maximum' => field.maximum, + 'ask_follow_up' => field.ask_follow_up, 'linear_scale_label_1_multiloc' => field.linear_scale_label_1_multiloc, 'linear_scale_label_2_multiloc' => field.linear_scale_label_2_multiloc, 'linear_scale_label_3_multiloc' => field.linear_scale_label_3_multiloc, diff --git a/back/engines/commercial/idea_custom_fields/app/controllers/idea_custom_fields/web_api/v1/admin/idea_custom_fields_controller.rb b/back/engines/commercial/idea_custom_fields/app/controllers/idea_custom_fields/web_api/v1/admin/idea_custom_fields_controller.rb index 3d3a8af5c122..fcab4f0f2071 100644 --- a/back/engines/commercial/idea_custom_fields/app/controllers/idea_custom_fields/web_api/v1/admin/idea_custom_fields_controller.rb +++ b/back/engines/commercial/idea_custom_fields/app/controllers/idea_custom_fields/web_api/v1/admin/idea_custom_fields_controller.rb @@ -418,6 +418,7 @@ def update_all_params :dropdown_layout, :page_layout, :map_config_id, + :ask_follow_up, { title_multiloc: CL2_SUPPORTED_LOCALES, description_multiloc: CL2_SUPPORTED_LOCALES, linear_scale_label_1_multiloc: CL2_SUPPORTED_LOCALES, diff --git a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb index 244f95cab84b..6d5df2b1fba8 100644 --- a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb +++ b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb @@ -745,6 +745,66 @@ }) end + example 'Update sentiment_linear_scale field' do + field_to_update = create(:custom_field_sentiment_linear_scale, resource: custom_form) + create(:custom_field, resource: custom_form) # field to destroy + request = { + custom_fields: [ + { + input_type: 'page', + page_layout: 'default' + }, + { + id: field_to_update.id, + title_multiloc: { 'en' => 'Select a value from the scale' }, + description_multiloc: { 'en' => 'Description of question' }, + required: true, + enabled: true, + maximum: 5, + ask_follow_up: true, + linear_scale_label_1_multiloc: { 'en' => 'Lowest' }, + linear_scale_label_2_multiloc: { 'en' => 'Low' }, + linear_scale_label_3_multiloc: { 'en' => 'Neutral' }, + linear_scale_label_4_multiloc: { 'en' => 'High' }, + linear_scale_label_5_multiloc: { 'en' => 'Highest' }, + }, + final_page + ] + } + do_request request + + assert_status 200 + json_response = json_parse(response_body) + expect(json_response[:data].size).to eq 3 + expect(json_response[:data][1]).to match({ + attributes: { + code: nil, + created_at: an_instance_of(String), + description_multiloc: { en: 'Description of question' }, + enabled: true, + input_type: 'sentiment_linear_scale', + key: an_instance_of(String), + ordering: 1, + required: true, + title_multiloc: { en: 'Select a value from the scale' }, + updated_at: an_instance_of(String), + maximum: 5, + ask_follow_up: true, + linear_scale_label_1_multiloc: { en: 'Lowest' }, + linear_scale_label_2_multiloc: { en: 'Low' }, + linear_scale_label_3_multiloc: { en: 'Neutral' }, + linear_scale_label_4_multiloc: { en: 'High' }, + linear_scale_label_5_multiloc: { en: 'Highest' }, + logic: {}, + random_option_ordering: false, + constraints: {} + }, + id: an_instance_of(String), + type: 'custom_field', + relationships: { options: { data: [] }, resource: { data: { id: custom_form.id, type: 'custom_form' } } } + }) + end + example 'Update select field with logic' do field_to_update = create(:custom_field_select, :with_options, resource: custom_form) survey_end_page = create(:custom_field_page, key: 'survey_end', resource: custom_form) diff --git a/back/engines/commercial/multi_tenancy/app/services/multi_tenancy/templates/serializers/custom_field.rb b/back/engines/commercial/multi_tenancy/app/services/multi_tenancy/templates/serializers/custom_field.rb index fc227301e93f..34cfbc4c3a99 100644 --- a/back/engines/commercial/multi_tenancy/app/services/multi_tenancy/templates/serializers/custom_field.rb +++ b/back/engines/commercial/multi_tenancy/app/services/multi_tenancy/templates/serializers/custom_field.rb @@ -33,6 +33,7 @@ class CustomField < Base random_option_ordering dropdown_layout page_layout + ask_follow_up ] # Enigmatic comment from the previous implementation: diff --git a/back/spec/factories/custom_fields.rb b/back/spec/factories/custom_fields.rb index b4d236a0545c..a33f9b260de8 100644 --- a/back/spec/factories/custom_fields.rb +++ b/back/spec/factories/custom_fields.rb @@ -166,7 +166,6 @@ end factory :custom_field_sentiment_linear_scale do - ask_follow_up { false } title_multiloc do { 'en' => 'We need a swimming pool.' @@ -177,8 +176,9 @@ 'en' => 'Please indicate how strong you agree or disagree.' } end - input_type { 'linear_scale' } + input_type { 'sentiment_linear_scale' } maximum { 5 } + ask_follow_up { false } linear_scale_label_1_multiloc do { 'en' => 'Strongly disagree' diff --git a/back/spec/serializers/web_api/v1/custom_field_serializer_spec.rb b/back/spec/serializers/web_api/v1/custom_field_serializer_spec.rb index 3948a59b9a48..5c8590abbcb5 100644 --- a/back/spec/serializers/web_api/v1/custom_field_serializer_spec.rb +++ b/back/spec/serializers/web_api/v1/custom_field_serializer_spec.rb @@ -100,6 +100,37 @@ end end + context 'sentiment_linear_scale field' do + let(:field) { create(:custom_field_sentiment_linear_scale, :for_custom_form, key: 'scale') } + + it 'includes maximum and scale value labels' do + serialized_field = described_class.new(field).serializable_hash + attributes = serialized_field[:data][:attributes] + expect(attributes).to match({ + code: nil, + created_at: an_instance_of(ActiveSupport::TimeWithZone), + description_multiloc: { 'en' => 'Please indicate how strong you agree or disagree.' }, + enabled: true, + input_type: 'sentiment_linear_scale', + key: 'scale', + maximum: 5, + ask_follow_up: false, + linear_scale_label_1_multiloc: { 'en' => 'Strongly disagree' }, + linear_scale_label_2_multiloc: { 'en' => 'Disagree' }, + linear_scale_label_3_multiloc: { 'en' => 'Neutral' }, + linear_scale_label_4_multiloc: { 'en' => 'Agree' }, + linear_scale_label_5_multiloc: { 'en' => 'Strongly agree' }, + ordering: 0, + required: false, + title_multiloc: { 'en' => 'We need a swimming pool.' }, + updated_at: an_instance_of(ActiveSupport::TimeWithZone), + logic: {}, + constraints: {}, + random_option_ordering: false + }) + end + end + context 'rating field' do let(:field) { create(:custom_field_rating, :for_custom_form, key: 'scale') } From 33a0049017ef1618bc49d80f95ca62232445e4d7 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Tue, 18 Feb 2025 16:33:02 +0100 Subject: [PATCH 07/40] Fix lint issues --- .../web_api/v1/custom_field_serializer.rb | 12 ++--- .../update_all_native_survey_spec.rb | 2 +- .../ui_schema_generator_service_spec.rb | 46 +++++++++---------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/back/app/serializers/web_api/v1/custom_field_serializer.rb b/back/app/serializers/web_api/v1/custom_field_serializer.rb index 0cb418817d73..6d8843d7233c 100644 --- a/back/app/serializers/web_api/v1/custom_field_serializer.rb +++ b/back/app/serializers/web_api/v1/custom_field_serializer.rb @@ -39,11 +39,11 @@ class WebApi::V1::CustomFieldSerializer < WebApi::V1::BaseSerializer attributes :maximum, if: proc { |object, _params| object.supports_linear_scale? } attributes :linear_scale_label_1_multiloc, - :linear_scale_label_2_multiloc, - :linear_scale_label_3_multiloc, - :linear_scale_label_4_multiloc, - :linear_scale_label_5_multiloc, - if: proc { |object, _params| object.input_type == 'sentiment_linear_scale' || object.linear_scale? || object.supports_matrix_statements? } + :linear_scale_label_2_multiloc, + :linear_scale_label_3_multiloc, + :linear_scale_label_4_multiloc, + :linear_scale_label_5_multiloc, + if: proc { |object, _params| object.input_type == 'sentiment_linear_scale' || object.linear_scale? || object.supports_matrix_statements? } attributes :linear_scale_label_6_multiloc, :linear_scale_label_7_multiloc, @@ -53,8 +53,6 @@ class WebApi::V1::CustomFieldSerializer < WebApi::V1::BaseSerializer :linear_scale_label_11_multiloc, if: proc { |object, _params| object.linear_scale? || object.supports_matrix_statements? } - - attributes :select_count_enabled, :maximum_select_count, :minimum_select_count, if: proc { |object, _params| object.multiselect? } diff --git a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb index 6d5df2b1fba8..a12ce161af4e 100644 --- a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb +++ b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb @@ -766,7 +766,7 @@ linear_scale_label_2_multiloc: { 'en' => 'Low' }, linear_scale_label_3_multiloc: { 'en' => 'Neutral' }, linear_scale_label_4_multiloc: { 'en' => 'High' }, - linear_scale_label_5_multiloc: { 'en' => 'Highest' }, + linear_scale_label_5_multiloc: { 'en' => 'Highest' } }, final_page ] diff --git a/back/spec/services/ui_schema_generator_service_spec.rb b/back/spec/services/ui_schema_generator_service_spec.rb index c3177522fcea..6bc3bb9b95e4 100644 --- a/back/spec/services/ui_schema_generator_service_spec.rb +++ b/back/spec/services/ui_schema_generator_service_spec.rb @@ -660,31 +660,31 @@ def generate_for_current_locale(fields) end describe '#visit_sentiment_linear_scale' do - let(:field) do - create( - :custom_field_sentiment_linear_scale, - key: field_key - ) - end + let(:field) do + create( + :custom_field_sentiment_linear_scale, + key: field_key + ) + end - it 'returns the schema for the given field' do - expect(generator.visit_sentiment_linear_scale(field)).to eq({ - type: 'Control', - scope: "#/properties/#{field_key}", - label: 'We need a swimming pool.', - options: { - input_type: field.input_type, - description: 'Please indicate how strong you agree or disagree.', - ask_follow_up: false, - linear_scale_label1: 'Strongly disagree', - linear_scale_label2: 'Disagree', - linear_scale_label3: 'Neutral', - linear_scale_label4: 'Agree', - linear_scale_label5: 'Strongly agree' - } - }) + it 'returns the schema for the given field' do + expect(generator.visit_sentiment_linear_scale(field)).to eq({ + type: 'Control', + scope: "#/properties/#{field_key}", + label: 'We need a swimming pool.', + options: { + input_type: field.input_type, + description: 'Please indicate how strong you agree or disagree.', + ask_follow_up: false, + linear_scale_label1: 'Strongly disagree', + linear_scale_label2: 'Disagree', + linear_scale_label3: 'Neutral', + linear_scale_label4: 'Agree', + linear_scale_label5: 'Strongly agree' + } + }) + end end -end describe '#visit_rating' do let(:field) do From 15d480eb809140ebafbf8c4e40c3eb024394d7ff Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Thu, 20 Feb 2025 10:21:26 +0100 Subject: [PATCH 08/40] WIP initial support for adding new sentiment question to form builder toolbox --- front/app/api/custom_fields/types.ts | 4 +++- front/app/component-library/components/Icon/index.tsx | 9 +++++++++ .../components/FormBuilderToolbox/index.tsx | 10 ++++++++++ .../app/components/FormBuilder/components/messages.ts | 4 ++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/front/app/api/custom_fields/types.ts b/front/app/api/custom_fields/types.ts index fef16afb255d..71f0d1cff7d9 100644 --- a/front/app/api/custom_fields/types.ts +++ b/front/app/api/custom_fields/types.ts @@ -38,7 +38,8 @@ export type ICustomFieldInputType = | 'point' | 'line' | 'polygon' - | 'cosponsor_ids'; + | 'cosponsor_ids' + | 'sentiment_linear_scale'; export type IOptionsType = { id?: string; @@ -76,6 +77,7 @@ export interface IAttributes { isEnabledEditable?: boolean; isTitleEditable?: boolean; isDeleteEnabled?: boolean; + ask_follow_up?: boolean; constraints?: { locks: { title_multiloc?: boolean; diff --git a/front/app/component-library/components/Icon/index.tsx b/front/app/component-library/components/Icon/index.tsx index 0f0151f33c24..943756bd2078 100644 --- a/front/app/component-library/components/Icon/index.tsx +++ b/front/app/component-library/components/Icon/index.tsx @@ -2704,6 +2704,15 @@ export const icons = { ), + 'survey-sentiment': (props: IconPropsWithoutName) => ( + + + + ), 'survey-multiple-choice': (props: IconPropsWithoutName) => ( + addField('sentiment_linear_scale')} + data-cy="e2e-sentiment" + fieldsToExclude={builderConfig.toolboxFieldsToExclude} + inputType="sentiment_linear_scale" + disabled={isCustomFieldsDisabled} + /> Date: Thu, 20 Feb 2025 10:21:55 +0100 Subject: [PATCH 09/40] Tweaks to BE code --- back/app/services/survey_results_generator_service.rb | 8 ++++---- .../20250217295025_add_follow_up_to_custom_fields.rb | 6 +----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/back/app/services/survey_results_generator_service.rb b/back/app/services/survey_results_generator_service.rb index 0da26dc06651..b7901bc2af61 100644 --- a/back/app/services/survey_results_generator_service.rb +++ b/back/app/services/survey_results_generator_service.rb @@ -169,8 +169,8 @@ def visit_select_base(field) query = inputs query = query.joins(:author) if group_mode == 'user_field' if group_field - raise "Unsupported group field type: #{group_field.input_type}" unless %w[select linear_scale rating].include?(group_field.input_type) - raise "Unsupported question type: #{field.input_type}" unless %w[select multiselect linear_scale rating multiselect_image].include?(field.input_type) + raise "Unsupported group field type: #{group_field.input_type}" unless %w[select linear_scale sentiment_linear_scale rating].include?(group_field.input_type) + raise "Unsupported question type: #{field.input_type}" unless %w[select multiselect linear_scale sentiment_linear_scale rating multiselect_image].include?(field.input_type) query = query.select( select_field_query(field, as: 'answer'), @@ -185,7 +185,7 @@ def visit_select_base(field) end # Sort correctly - answers = answers.sort_by { |a| -a[:count] } unless %w[linear_scale rating].include?(field.input_type) + answers = answers.sort_by { |a| -a[:count] } unless %w[linear_scale sentiment_linear_scale rating].include?(field.input_type) answers = answers.sort_by { |a| a[:answer] == 'other' ? 1 : 0 } # other should always be last # Build response @@ -195,7 +195,7 @@ def visit_select_base(field) def select_field_query(field, as: 'answer') table = field.resource_type == 'User' ? 'users' : 'ideas' - if %w[select linear_scale rating].include? field.input_type + if %w[select linear_scale sentiment_linear_scale rating].include? field.input_type "COALESCE(#{table}.custom_field_values->'#{field.key}', 'null') as #{as}" elsif %w[multiselect multiselect_image].include? field.input_type %{ diff --git a/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb b/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb index ebc4cd6e6e2b..efb50c961a54 100644 --- a/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb +++ b/back/db/migrate/20250217295025_add_follow_up_to_custom_fields.rb @@ -1,8 +1,4 @@ -class AddFollowUpToCustomFields < ActiveRecord::Migration[7.0] - class StubCustomField < ApplicationRecord - self.table_name = 'custom_fields' - end - +class AddFollowUpToCustomFields < ActiveRecord::Migration[7.1] def change add_column :custom_fields, :ask_follow_up, :boolean, default: false, null: false end From b62aeca43b5987dc3971f39564d31959759566fb Mon Sep 17 00:00:00 2001 From: CircleCI Date: Thu, 20 Feb 2025 09:24:30 +0000 Subject: [PATCH 10/40] Translations updated by CI (extract-intl) --- front/app/translations/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/front/app/translations/en.json b/front/app/translations/en.json index 27266cbea0ff..3d69e83bce20 100644 --- a/front/app/translations/en.json +++ b/front/app/translations/en.json @@ -791,6 +791,7 @@ "app.components.formBuilder.section": "Section", "app.components.formBuilder.sectionCannotBeDeleted": "This section can't be deleted.", "app.components.formBuilder.selectRangeTooltip": "Choose the maximum value for your scale.", + "app.components.formBuilder.sentiment": "Sentiment scale", "app.components.formBuilder.shapefileUpload": "Esri shapefile upload", "app.components.formBuilder.shortAnswer": "Short answer", "app.components.formBuilder.showResponseToUsersToggleLabel": "Show response to users", From 19d433e5e5b5dbe19d182358617460d30455c4cf Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Thu, 20 Feb 2025 11:07:51 +0100 Subject: [PATCH 11/40] Simplify serializer for linear scale labels --- back/app/models/custom_field.rb | 4 ++++ back/app/serializers/web_api/v1/custom_field_serializer.rb | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/back/app/models/custom_field.rb b/back/app/models/custom_field.rb index 8bf80a76ba75..1be2734d519b 100644 --- a/back/app/models/custom_field.rb +++ b/back/app/models/custom_field.rb @@ -223,6 +223,10 @@ def linear_scale? input_type == 'linear_scale' end + def sentiment_linear_scale? + input_type == 'sentiment_linear_scale' + end + def rating? input_type == 'rating' end diff --git a/back/app/serializers/web_api/v1/custom_field_serializer.rb b/back/app/serializers/web_api/v1/custom_field_serializer.rb index 6d8843d7233c..40fb2f831c53 100644 --- a/back/app/serializers/web_api/v1/custom_field_serializer.rb +++ b/back/app/serializers/web_api/v1/custom_field_serializer.rb @@ -43,15 +43,13 @@ class WebApi::V1::CustomFieldSerializer < WebApi::V1::BaseSerializer :linear_scale_label_3_multiloc, :linear_scale_label_4_multiloc, :linear_scale_label_5_multiloc, - if: proc { |object, _params| object.input_type == 'sentiment_linear_scale' || object.linear_scale? || object.supports_matrix_statements? } - - attributes :linear_scale_label_6_multiloc, + :linear_scale_label_6_multiloc, :linear_scale_label_7_multiloc, :linear_scale_label_8_multiloc, :linear_scale_label_9_multiloc, :linear_scale_label_10_multiloc, :linear_scale_label_11_multiloc, - if: proc { |object, _params| object.linear_scale? || object.supports_matrix_statements? } + if: proc { |object, _params| object.linear_scale? || object?.sentiment_linear_scale? || object.supports_matrix_statements? } attributes :select_count_enabled, :maximum_select_count, :minimum_select_count, if: proc { |object, _params| object.multiselect? From bd29e33e3f94a96546540209b9288b33465ebc69 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Thu, 20 Feb 2025 11:30:20 +0100 Subject: [PATCH 12/40] Fix syntax --- back/app/serializers/web_api/v1/custom_field_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/app/serializers/web_api/v1/custom_field_serializer.rb b/back/app/serializers/web_api/v1/custom_field_serializer.rb index 40fb2f831c53..537e6a093f9b 100644 --- a/back/app/serializers/web_api/v1/custom_field_serializer.rb +++ b/back/app/serializers/web_api/v1/custom_field_serializer.rb @@ -49,7 +49,7 @@ class WebApi::V1::CustomFieldSerializer < WebApi::V1::BaseSerializer :linear_scale_label_9_multiloc, :linear_scale_label_10_multiloc, :linear_scale_label_11_multiloc, - if: proc { |object, _params| object.linear_scale? || object?.sentiment_linear_scale? || object.supports_matrix_statements? } + if: proc { |object, _params| object.linear_scale? || object.sentiment_linear_scale? || object.supports_matrix_statements? } attributes :select_count_enabled, :maximum_select_count, :minimum_select_count, if: proc { |object, _params| object.multiselect? From 7e5b1e16da7c52b11113cc41d3afc18c4b4cfe43 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Thu, 20 Feb 2025 12:36:38 +0100 Subject: [PATCH 13/40] Update specs --- .../phase_context/update_all_native_survey_spec.rb | 8 +++++++- .../web_api/v1/custom_field_serializer_spec.rb | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb index a12ce161af4e..bd2c77ff6210 100644 --- a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb +++ b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb @@ -766,7 +766,13 @@ linear_scale_label_2_multiloc: { 'en' => 'Low' }, linear_scale_label_3_multiloc: { 'en' => 'Neutral' }, linear_scale_label_4_multiloc: { 'en' => 'High' }, - linear_scale_label_5_multiloc: { 'en' => 'Highest' } + linear_scale_label_5_multiloc: { 'en' => 'Highest' }, + linear_scale_label_6_multiloc: {}, + linear_scale_label_7_multiloc: {}, + linear_scale_label_8_multiloc: {}, + linear_scale_label_9_multiloc: {}, + linear_scale_label_10_multiloc: {}, + linear_scale_label_11_multiloc: {} }, final_page ] diff --git a/back/spec/serializers/web_api/v1/custom_field_serializer_spec.rb b/back/spec/serializers/web_api/v1/custom_field_serializer_spec.rb index 5c8590abbcb5..f940e4c9254b 100644 --- a/back/spec/serializers/web_api/v1/custom_field_serializer_spec.rb +++ b/back/spec/serializers/web_api/v1/custom_field_serializer_spec.rb @@ -120,6 +120,12 @@ linear_scale_label_3_multiloc: { 'en' => 'Neutral' }, linear_scale_label_4_multiloc: { 'en' => 'Agree' }, linear_scale_label_5_multiloc: { 'en' => 'Strongly agree' }, + linear_scale_label_6_multiloc: {}, + linear_scale_label_7_multiloc: {}, + linear_scale_label_8_multiloc: {}, + linear_scale_label_9_multiloc: {}, + linear_scale_label_10_multiloc: {}, + linear_scale_label_11_multiloc: {}, ordering: 0, required: false, title_multiloc: { 'en' => 'We need a swimming pool.' }, From 6f8f344c41365a05ba40350f4fcdfaa49d9a8dc4 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Thu, 20 Feb 2025 12:51:47 +0100 Subject: [PATCH 14/40] Fix failing spec --- .../phase_context/update_all_native_survey_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb index bd2c77ff6210..41e7905e1d17 100644 --- a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb +++ b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb @@ -801,6 +801,12 @@ linear_scale_label_3_multiloc: { en: 'Neutral' }, linear_scale_label_4_multiloc: { en: 'High' }, linear_scale_label_5_multiloc: { en: 'Highest' }, + linear_scale_label_6_multiloc: {}, + linear_scale_label_7_multiloc: {}, + linear_scale_label_8_multiloc: {}, + linear_scale_label_9_multiloc: {}, + linear_scale_label_10_multiloc: {}, + linear_scale_label_11_multiloc: {}. logic: {}, random_option_ordering: false, constraints: {} From b38e70a06e187497060fa32d3db79adeecc86c72 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Thu, 20 Feb 2025 13:01:26 +0100 Subject: [PATCH 15/40] Fix spec --- .../phase_context/update_all_native_survey_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb index 41e7905e1d17..5c0d7a8c96c9 100644 --- a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb +++ b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb @@ -806,7 +806,7 @@ linear_scale_label_8_multiloc: {}, linear_scale_label_9_multiloc: {}, linear_scale_label_10_multiloc: {}, - linear_scale_label_11_multiloc: {}. + linear_scale_label_11_multiloc: {}, logic: {}, random_option_ordering: false, constraints: {} From c0909b38786240854cc03f06b59131433b65a8a3 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Thu, 20 Feb 2025 13:03:17 +0100 Subject: [PATCH 16/40] Include sentiment_linear_scale in importing list --- back/app/services/idea_custom_fields_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/app/services/idea_custom_fields_service.rb b/back/app/services/idea_custom_fields_service.rb index 598c9f3666ba..b909d1212d95 100644 --- a/back/app/services/idea_custom_fields_service.rb +++ b/back/app/services/idea_custom_fields_service.rb @@ -46,7 +46,7 @@ def printable_fields end def importable_fields - ignore_field_types = %w[page section date files image_files file_upload shapefile_upload point line polygon cosponsor_ids ranking matrix_linear_scale sentiment_linear_scale] + ignore_field_types = %w[page section date files image_files file_upload shapefile_upload point line polygon cosponsor_ids ranking matrix_linear_scale] enabled_fields_with_other_options.reject { |field| ignore_field_types.include? field.input_type } end From 338c9883594fce089fa303c3b1b1370c6b6e6ba6 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Thu, 20 Feb 2025 13:13:30 +0100 Subject: [PATCH 17/40] Do not allow sentiment question in pdf print --- back/app/services/idea_custom_fields_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/app/services/idea_custom_fields_service.rb b/back/app/services/idea_custom_fields_service.rb index b909d1212d95..5e8732bcdcec 100644 --- a/back/app/services/idea_custom_fields_service.rb +++ b/back/app/services/idea_custom_fields_service.rb @@ -40,7 +40,7 @@ def submittable_fields_with_other_options # Used in the printable PDF export def printable_fields - ignore_field_types = %w[section page date files image_files point file_upload shapefile_upload topic_ids cosponsor_ids ranking matrix_linear_scale sentiment_linear_scale] + ignore_field_types = %w[section page date files image_files point file_upload shapefile_upload topic_ids cosponsor_ids ranking matrix_linear_scale] fields = enabled_fields.reject { |field| ignore_field_types.include? field.input_type } insert_other_option_text_fields(fields) end From 40fba1607f82454e88ec6c6d9f56eb02af1c047a Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Thu, 20 Feb 2025 17:15:25 +0100 Subject: [PATCH 18/40] Support sentiment question in form builder --- .../ScaleLabelsInput.tsx | 9 ++- .../LinearAndRatingSettings/index.tsx | 1 + .../LinearAndRatingSettings/utils.ts | 11 ++++ .../SentimentLinearScaleSettings/index.tsx | 58 +++++++++++++++++++ .../SentimentLinearScaleSettings/messages.ts | 8 +++ .../components/FormBuilderToolbox/index.tsx | 2 +- .../components/FormFields/utils.ts | 2 + .../app/components/FormBuilder/edit/index.tsx | 8 ++- .../app/components/FormBuilder/edit/utils.ts | 13 ++++- front/app/components/FormBuilder/utils.tsx | 14 +++++ .../projects/project/nativeSurvey/utils.tsx | 1 + 11 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts create mode 100644 front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/index.tsx create mode 100644 front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/messages.ts diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/ScaleLabelsInput.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/ScaleLabelsInput.tsx index 3e5d63e14eef..000a04ab2a3b 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/ScaleLabelsInput.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/ScaleLabelsInput.tsx @@ -11,10 +11,13 @@ import { useFormContext } from 'react-hook-form'; import styled from 'styled-components'; import { SupportedLocale } from 'typings'; +import { ICustomFieldInputType } from 'api/custom_fields/types'; + import { useIntl } from 'utils/cl-intl'; import { isNilOrError } from 'utils/helperUtils'; import messages from './messages'; +import { getNumberLabelForIndex } from './utils'; const StyledLabel = styled(Label)` margin-top: auto; @@ -26,6 +29,7 @@ interface Props { onSelectedLocaleChange?: (locale: SupportedLocale) => void; locales: SupportedLocale[]; platformLocale: SupportedLocale; + inputType: ICustomFieldInputType; } const ScaleLabelsInput = ({ @@ -34,6 +38,7 @@ const ScaleLabelsInput = ({ onSelectedLocaleChange, locales, platformLocale, + inputType, }: Props) => { const { setValue, getValues } = useFormContext(); const [selectedLocale, setSelectedLocale] = useState( @@ -98,7 +103,9 @@ const ScaleLabelsInput = ({ return ( - )} diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts new file mode 100644 index 000000000000..12be2bf98a7c --- /dev/null +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts @@ -0,0 +1,11 @@ +import { ICustomFieldInputType } from 'api/custom_fields/types'; + +export const getNumberLabelForIndex = ( + inputType: ICustomFieldInputType, + index: number +) => { + if (inputType === 'sentiment_linear_scale') { + return index - 2; // We show a scale of -2 to 2 in the UI for sentiment scales. + } + return index + 1; // Show a scale of 1 to 5 in the UI for other field types. +}; diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/index.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/index.tsx new file mode 100644 index 000000000000..8b33c72b7d67 --- /dev/null +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/index.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import { Box, Toggle } from '@citizenlab/cl2-component-library'; +import { useFormContext } from 'react-hook-form'; +import { SupportedLocale } from 'typings'; + +import { useIntl } from 'utils/cl-intl'; + +import ScaleLabelInput from '../LinearAndRatingSettings/ScaleLabelsInput'; + +import messages from './messages'; + +interface Props { + maximumName: string; + askFollowUpName: string; + labelBaseName: string; + onSelectedLocaleChange?: (locale: SupportedLocale) => void; + locales: SupportedLocale[]; + platformLocale: SupportedLocale; +} + +const SentimentLinearScaleSettings = ({ + onSelectedLocaleChange, + maximumName, + askFollowUpName, + labelBaseName, + platformLocale, + locales, +}: Props) => { + const { formatMessage } = useIntl(); + const { setValue, getValues } = useFormContext(); + + return ( + <> + + + + { + setValue(askFollowUpName, !getValues(askFollowUpName)); + }} + label={formatMessage(messages.askFollowUpToggleLabel)} + /> + + + + ); +}; + +export default SentimentLinearScaleSettings; diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/messages.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/messages.ts new file mode 100644 index 000000000000..5610c431f62a --- /dev/null +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/messages.ts @@ -0,0 +1,8 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + askFollowUpToggleLabel: { + id: 'app.components.formBuilder.askFollowUpToggleLabel', + defaultMessage: 'Ask follow up', + }, +}); diff --git a/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx b/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx index d2de4fa0e743..81a9b0911dd0 100644 --- a/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx @@ -279,7 +279,7 @@ const FormBuilderToolbox = ({ label={formatMessage(messages.sentiment)} onClick={() => addField('sentiment_linear_scale')} data-cy="e2e-sentiment" - fieldsToExclude={builderConfig.toolboxFieldsToExclude} + fieldsToInclude={builderConfig.toolboxFieldsToInclude} inputType="sentiment_linear_scale" disabled={isCustomFieldsDisabled} /> diff --git a/front/app/components/FormBuilder/components/FormFields/utils.ts b/front/app/components/FormBuilder/components/FormFields/utils.ts index 5924fc968bb3..fa8ed2a9ce58 100644 --- a/front/app/components/FormBuilder/components/FormFields/utils.ts +++ b/front/app/components/FormBuilder/components/FormFields/utils.ts @@ -94,6 +94,8 @@ const getCustomFieldBadgeLabel = ( return messages.ranking; case 'matrix_linear_scale': return messages.matrix; + case 'sentiment_linear_scale': + return messages.sentiment; default: return messages.default; } diff --git a/front/app/components/FormBuilder/edit/index.tsx b/front/app/components/FormBuilder/edit/index.tsx index 405036bdc1e8..8d1150c82541 100644 --- a/front/app/components/FormBuilder/edit/index.tsx +++ b/front/app/components/FormBuilder/edit/index.tsx @@ -50,6 +50,7 @@ import { NestedGroupingStructure, getReorderedFields, DragAndDropResult, + supportsLinearScaleLabels, } from './utils'; interface FormValues { @@ -135,6 +136,7 @@ const FormEdit = ({ linear_scale_label_10_multiloc: object(), linear_scale_label_11_multiloc: object(), required: boolean(), + ask_follow_up: boolean(), temp_id: string(), logic: validateLogic(formatMessage(messages.logicValidationError)), }) @@ -295,8 +297,10 @@ const FormEdit = ({ ...(field.input_type === 'matrix_linear_scale' && { matrix_statements: field.matrix_statements || {}, }), - ...((field.input_type === 'linear_scale' || - field.input_type === 'matrix_linear_scale') && { + ...(field.input_type === 'sentiment_linear_scale' && { + ask_follow_up: field.ask_follow_up || false, + }), + ...(supportsLinearScaleLabels(field.input_type) && { linear_scale_label_1_multiloc: field.linear_scale_label_1_multiloc || {}, linear_scale_label_2_multiloc: diff --git a/front/app/components/FormBuilder/edit/utils.ts b/front/app/components/FormBuilder/edit/utils.ts index 86eb786c8d3d..ff9366b0c73e 100644 --- a/front/app/components/FormBuilder/edit/utils.ts +++ b/front/app/components/FormBuilder/edit/utils.ts @@ -1,4 +1,7 @@ -import { IFlatCustomField } from 'api/custom_fields/types'; +import { + ICustomFieldInputType, + IFlatCustomField, +} from 'api/custom_fields/types'; import { questionDNDType } from 'components/FormBuilder/components/FormFields/constants'; @@ -14,6 +17,14 @@ const reorder = ( return result; }; +export const supportsLinearScaleLabels = (inputType: ICustomFieldInputType) => { + return [ + 'linear_scale', + 'sentiment_linear_scale', + 'matrix_linear_scale', + ].includes(inputType); +}; + export type NestedGroupingStructure = { questions: IFlatCustomField[]; groupElement: IFlatCustomField; diff --git a/front/app/components/FormBuilder/utils.tsx b/front/app/components/FormBuilder/utils.tsx index f181cd86761f..6b830e4f9b6d 100644 --- a/front/app/components/FormBuilder/utils.tsx +++ b/front/app/components/FormBuilder/utils.tsx @@ -21,6 +21,7 @@ import MultiselectSettings from './components/FormBuilderSettings/MultiselectSet import OptionsSettings from './components/FormBuilderSettings/OptionsSettings'; import PageLayoutSettings from './components/FormBuilderSettings/PageLayoutSettings'; import PointSettings from './components/FormBuilderSettings/PointSettings'; +import SentimentLinearScaleSettings from './components/FormBuilderSettings/SentimentLinearScaleSettings'; import messages from './components/messages'; export const builtInFieldKeys = [ @@ -94,6 +95,16 @@ export function getAdditionalSettings( } switch (inputType) { + case 'sentiment_linear_scale': + return ( + + ); case 'matrix_linear_scale': return ( Date: Thu, 20 Feb 2025 17:16:18 +0100 Subject: [PATCH 19/40] Fix comment --- .../FormBuilderSettings/LinearAndRatingSettings/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts index 12be2bf98a7c..daf93da75b09 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts @@ -7,5 +7,5 @@ export const getNumberLabelForIndex = ( if (inputType === 'sentiment_linear_scale') { return index - 2; // We show a scale of -2 to 2 in the UI for sentiment scales. } - return index + 1; // Show a scale of 1 to 5 in the UI for other field types. + return index + 1; // Show a scale of 1 to 11 in the UI for other field types. }; From 54b3466a65897af8658e90d0a30eb356731b2cfe Mon Sep 17 00:00:00 2001 From: CircleCI Date: Thu, 20 Feb 2025 16:18:33 +0000 Subject: [PATCH 20/40] Translations updated by CI (extract-intl) --- front/app/translations/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/front/app/translations/en.json b/front/app/translations/en.json index d3b2003b3431..6076edac53e2 100644 --- a/front/app/translations/en.json +++ b/front/app/translations/en.json @@ -668,6 +668,7 @@ "app.components.formBuilder.agree": "Agree", "app.components.formBuilder.ai1": "AI", "app.components.formBuilder.aiUpsellText1": "If you have access to our AI package, you will be able to summarise and categorise text responses with AI", + "app.components.formBuilder.askFollowUpToggleLabel": "Ask follow up", "app.components.formBuilder.cancelLeaveBuilderButtonText": "Cancel", "app.components.formBuilder.chooseMany": "Choose many", "app.components.formBuilder.chooseOne": "Choose one", From 69d5d2902eac1f2375b5b01c991bdf518f2b461a Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Fri, 21 Feb 2025 10:00:50 +0100 Subject: [PATCH 21/40] WIP Initial support for the follow_up custom field for sentiment questions --- back/app/models/custom_field.rb | 22 +++++++++++++++++++ .../services/custom_field_params_service.rb | 5 +++++ .../services/json_schema_generator_service.rb | 1 + .../survey_results_generator_service.rb | 1 + 4 files changed, 29 insertions(+) diff --git a/back/app/models/custom_field.rb b/back/app/models/custom_field.rb index 35fd71b9f7b8..5760275c17e1 100644 --- a/back/app/models/custom_field.rb +++ b/back/app/models/custom_field.rb @@ -123,6 +123,10 @@ def includes_other_option? options.any?(&:other) end + def includes_follow_up? + ask_follow_up == true + end + def support_free_text_value? %w[text multiline_text text_multiloc multiline_text_multiloc html_multiloc].include?(input_type) || (support_options? && includes_other_option?) end @@ -308,6 +312,24 @@ def other_option_text_field ) end + def follow_up_text_field + return if !includes_follow_up? + + follow_up_field_key = "#{key}_follow_up" + title_multiloc = MultilocService.new.i18n_to_multiloc( + 'custom_fields.ideas.other_input_field.title', + locales: CL2_SUPPORTED_LOCALES + ) + + CustomField.new( + key: follow_up_field_key, + input_type: 'text', + title_multiloc: title_multiloc, + required: true, + enabled: true + ) + end + def ordered_options @ordered_options ||= if random_option_ordering options.shuffle.sort_by { |o| o.other ? 1 : 0 } diff --git a/back/app/services/custom_field_params_service.rb b/back/app/services/custom_field_params_service.rb index deffb8ed608f..f449077a5d9e 100644 --- a/back/app/services/custom_field_params_service.rb +++ b/back/app/services/custom_field_params_service.rb @@ -72,6 +72,11 @@ def reject_other_text_values(extra_field_values) extra_field_values.delete key end end + + if key.end_with? '_follow_up' + parent_field_key = key.delete_suffix '_follow_up' + extra_field_values.delete key # TODO - Figure out how to accept follow-up values + end end end end diff --git a/back/app/services/json_schema_generator_service.rb b/back/app/services/json_schema_generator_service.rb index 2769336595dc..fc66f89ebfaf 100644 --- a/back/app/services/json_schema_generator_service.rb +++ b/back/app/services/json_schema_generator_service.rb @@ -325,6 +325,7 @@ def generate_for_current_locale(fields) accu[field.key] = field_schema accu[field.other_option_text_field.key] = visit(field.other_option_text_field) if field.other_option_text_field + accu[field.follow_up_text_field.key] = visit(field.follow_up_text_field) if field.follow_up_text_field end { type: 'object', diff --git a/back/app/services/survey_results_generator_service.rb b/back/app/services/survey_results_generator_service.rb index b7901bc2af61..e36812d4bcf8 100644 --- a/back/app/services/survey_results_generator_service.rb +++ b/back/app/services/survey_results_generator_service.rb @@ -250,6 +250,7 @@ def build_select_response(answers, field) }) attributes[:textResponses] = get_text_responses("#{field.key}_other") if field.other_option_text_field + # TODO: Get text responses for sentiment questions with follow up questions. attributes[:legend] = generate_answer_keys(group_field) if group_field attributes From 6a4e283d5398117f6fc06fda7a1fc9adce21659e Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 24 Feb 2025 09:00:01 +0100 Subject: [PATCH 22/40] WIP Support sentiment linear scale in front office --- .../Form/Components/Controls/InputControl.tsx | 16 +- .../SentimentLinearScaleControl.tsx | 226 ++++++++++++++++++ .../assets/sentiment_1.svg | 4 + .../assets/sentiment_2.svg | 4 + .../assets/sentiment_3.svg | 4 + .../assets/sentiment_4.svg | 4 + .../assets/sentiment_5.svg | 4 + .../SentimentLinearScaleControl/utils.tsx | 74 ++++++ .../Components/Controls/TextAreaControl.tsx | 39 +-- .../Form/Components/Controls/index.tsx | 5 + .../Form/Components/Fields/formConfig.tsx | 6 + .../Components/Layouts/CLSurveyPageLayout.tsx | 6 +- .../utils/extractElementsByFollowUpLogic.ts | 38 +++ .../Form/utils/getFollowUpControlKey.ts | 18 ++ 14 files changed, 418 insertions(+), 30 deletions(-) create mode 100644 front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx create mode 100644 front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_1.svg create mode 100644 front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_2.svg create mode 100644 front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_3.svg create mode 100644 front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_4.svg create mode 100644 front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_5.svg create mode 100644 front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx create mode 100644 front/app/components/Form/utils/extractElementsByFollowUpLogic.ts create mode 100644 front/app/components/Form/utils/getFollowUpControlKey.ts diff --git a/front/app/components/Form/Components/Controls/InputControl.tsx b/front/app/components/Form/Components/Controls/InputControl.tsx index 131ff3553c34..e859074ada5e 100644 --- a/front/app/components/Form/Components/Controls/InputControl.tsx +++ b/front/app/components/Form/Components/Controls/InputControl.tsx @@ -111,26 +111,18 @@ export const InputControl = ({ type={schema.type === 'number' ? 'number' : 'text'} value={data} onChange={onChange} - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - maxCharCount={schema?.maxLength} + maxCharCount={schema.maxLength} onBlur={() => { - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - uischema?.options?.transform === 'trim_on_blur' && + uischema.options?.transform === 'trim_on_blur' && isString(data) && onChange(data.trim()); setDidBlur(true); }} - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - disabled={uischema?.options?.readonly} + disabled={uischema.options?.readonly} placeholder={isOtherField ? label : undefined} onKeyDown={handleKeyDown} /> - {/* TODO: Fix this the next time the file is edited. */} - {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */} - + theme.colors.coolGrey600}; + } + } +`; + +const SentimentLinearScaleControl = ({ + data, + path, + errors, + schema, + uischema, + required, + handleChange, + id, + visible, +}: ControlProps) => { + const theme = useTheme(); + const { formatMessage } = useIntl(); + + const minimum = 1; + const maximum = 5; + + const answerNotPublic = uischema.options?.answer_visible_to === 'admins'; + + const sliderRef = useRef(null); + + const getAriaLabel = useCallback( + (value: number, total: number) => { + return getAriaValueText({ + value, + total, + uischema, + formatMessage, + }); + }, + [uischema, formatMessage] + ); + + // Set the aria-valuenow and aria-valuetext attributes on the slider when the data changes + useEffect(() => { + if (sliderRef.current) { + sliderRef.current.setAttribute('aria-valuenow', String(data || minimum)); + sliderRef.current.setAttribute( + 'aria-valuetext', + getAriaLabel(data || minimum, 5) + ); + } + }, [data, getAriaLabel, minimum, maximum]); + + // Handle keyboard input for the slider + const handleKeyDown = (event: React.KeyboardEvent) => { + const value = data || minimum; + let newValue = value; + newValue = handleKeyboardKeyChange(event, value); + + handleChange(path, newValue); + if (sliderRef.current) { + sliderRef.current.setAttribute('aria-valuenow', String(newValue)); + sliderRef.current.setAttribute( + 'aria-valuetext', + getAriaLabel(newValue, 5) + ); + } + event.preventDefault(); + }; + + // Put all labels from the UI Schema in an array so we can easily access them + const labelsFromSchema = Array.from({ length: maximum }, (_, index) => { + return uischema.options?.[`linear_scale_label${index + 1}`]; + }); + + // If the control is not visible, don't render anything + if (!visible) { + return null; + } + + return ( + <> + + {answerNotPublic && ( + + + + )} + + + + {[...Array(maximum).keys()].map((i) => { + const visualIndex = i + 1; + return ( + + ); + })} + + + {[...Array(maximum).keys()].map((i) => { + const visualIndex = i + 1; + return ( + + ); + })} + +
+ + {labelsFromSchema[visualIndex - 1]} + +
+ + + +
+ +
+ + + ); +}; + +export default withJsonFormsControlProps(SentimentLinearScaleControl); + +export const sentimentLinearScaleControlTester = (schema: UiSchema) => { + if (schema.options?.input_type === 'sentiment_linear_scale') { + return 200; + } + return -1; +}; diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_1.svg b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_1.svg new file mode 100644 index 000000000000..0657ab956cda --- /dev/null +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_2.svg b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_2.svg new file mode 100644 index 000000000000..ec4b7d46fcd6 --- /dev/null +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_3.svg b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_3.svg new file mode 100644 index 000000000000..73f7efacaa75 --- /dev/null +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_4.svg b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_4.svg new file mode 100644 index 000000000000..b395cf65bdc6 --- /dev/null +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_4.svg @@ -0,0 +1,4 @@ + + + + diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_5.svg b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_5.svg new file mode 100644 index 000000000000..a590078dc02d --- /dev/null +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/assets/sentiment_5.svg @@ -0,0 +1,4 @@ + + + + diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx new file mode 100644 index 000000000000..1300fb614334 --- /dev/null +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx @@ -0,0 +1,74 @@ +import messages from '../messages'; + +import Sentiment1Svg from './assets/sentiment_1.svg'; +import Sentiment2Svg from './assets/sentiment_2.svg'; +import Sentiment3Svg from './assets/sentiment_3.svg'; +import Sentiment4Svg from './assets/sentiment_4.svg'; +import Sentiment5Svg from './assets/sentiment_5.svg'; + +export const handleKeyboardKeyChange = (event, value) => { + switch (event.key) { + case 'ArrowLeft': + case 'ArrowDown': + return Math.max(1, value - 1); + case 'ArrowRight': + case 'ArrowUp': + return Math.min(5, value + 1); + case 'Home': + return 1; + case 'End': + return 5; + default: + return; + } +}; + +export const getSentimentEmoji = (index: number) => { + switch (index) { + case 1: + return Sentiment1Svg; + case 2: + return Sentiment2Svg; + case 3: + return Sentiment3Svg; + case 4: + return Sentiment4Svg; + case 5: + return Sentiment5Svg; + } + return Sentiment1Svg; +}; + +type GetAriaValueTextProps = { + uischema: any; + formatMessage: any; + value: number; + total: number; +}; + +export const getAriaValueText = ({ + uischema, + formatMessage, + value, + total, +}: GetAriaValueTextProps) => { + // If the value has a label, read it out + if (uischema.options?.[`linear_scale_label${value}`]) { + return formatMessage(messages.valueOutOfTotalWithLabel, { + value, + total, + label: uischema.options[`linear_scale_label${value}`], + }); + } + // If we don't have a label but we do have a maximum, read out the current value & maximum label + else if (uischema.options?.[`linear_scale_label${5}`]) { + return formatMessage(messages.valueOutOfTotalWithMaxExplanation, { + value, + total, + maxValue: 5, + maxLabel: uischema.options[`linear_scale_label${5}`], + }); + } + // Otherwise, just read out the value and the maximum value + return formatMessage(messages.valueOutOfTotal, { value, total }); +}; diff --git a/front/app/components/Form/Components/Controls/TextAreaControl.tsx b/front/app/components/Form/Components/Controls/TextAreaControl.tsx index 2ce157c8bbf5..5d1f968e443e 100644 --- a/front/app/components/Form/Components/Controls/TextAreaControl.tsx +++ b/front/app/components/Form/Components/Controls/TextAreaControl.tsx @@ -10,6 +10,7 @@ import { import { withJsonFormsControlProps } from '@jsonforms/react'; import styled from 'styled-components'; +import getFollowUpControlKey from 'components/Form/utils/getFollowUpControlKey'; import { FormLabel } from 'components/UI/FormComponents'; import TextArea from 'components/UI/TextArea'; @@ -45,41 +46,45 @@ const TextAreaControl = ({ return null; } + const isFollowUpField = !!getFollowUpControlKey(uischema.scope); + return ( <> - + {!isFollowUpField && ( + + )} + {answerNotPublic && ( )} - + handleChange(path, value)} rows={6} value={data} id={sanitizeForClassname(id)} onBlur={() => { - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - uischema?.options?.transform === 'trim_on_blur' && + uischema.options?.transform === 'trim_on_blur' && isString(data) && handleChange(path, data.trim()); setDidBlur(true); }} - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - disabled={uischema?.options?.readonly} + disabled={uischema.options?.readonly} + placeholder={isFollowUpField ? getLabel(uischema, schema, path) : ''} /> - {/* TODO: Fix this the next time the file is edited. */} - {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */} - + { diff --git a/front/app/components/Form/Components/Layouts/CLSurveyPageLayout.tsx b/front/app/components/Form/Components/Layouts/CLSurveyPageLayout.tsx index 220c4b681a80..502ce3903c3b 100644 --- a/front/app/components/Form/Components/Layouts/CLSurveyPageLayout.tsx +++ b/front/app/components/Form/Components/Layouts/CLSurveyPageLayout.tsx @@ -40,6 +40,7 @@ import { parseLayers } from 'components/EsriMap/utils'; import { FormContext } from 'components/Form/contexts'; import { PageCategorization, PageType } from 'components/Form/typings'; import customAjv from 'components/Form/utils/customAjv'; +import extractElementsByFollowUpLogic from 'components/Form/utils/extractElementsByFollowUpLogic'; import extractElementsByOtherOptionLogic from 'components/Form/utils/extractElementsByOtherOptionLogic'; import getFormCompletionPercentage from 'components/Form/utils/getFormCompletionPercentage'; import getPageVariant from 'components/Form/utils/getPageVariant'; @@ -308,7 +309,10 @@ const CLSurveyPageLayout = memo( ); } - const pageElements = extractElementsByOtherOptionLogic(currentPage, data); + // Extract elements depending on other option logic and follow-up logic + // E.g. If a user selects 'other' in a multiple choice question, we show a text field, if they don't we should not show it. + let pageElements = extractElementsByOtherOptionLogic(currentPage, data); + pageElements = extractElementsByFollowUpLogic(pageElements, data); // This is the index of the current page in the pageTypeElements array, // which also includes non-visible pages. diff --git a/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts b/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts new file mode 100644 index 000000000000..693ea2eae53a --- /dev/null +++ b/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts @@ -0,0 +1,38 @@ +import { ExtendedUISchema, FormValues } from '../typings'; + +// This returns the elements on a page that are visible based on the data and the Sentiment Linear Scale selection. +// You can pass returnHidden as true to get the hidden elements +const extractElementsByFollowUpLogic = ( + pageElements: any, + data: FormValues +): ExtendedUISchema[] => { + // Get a list of any "Follow-up" custom fields in the page. + const followUpFieldValues = pageElements + ?.filter((element) => element.options?.ask_follow_up) + .map((element) => { + const parentFieldKey = element.scope?.split('/').pop(); + return { + followUpFieldKey: `${parentFieldKey}_follow_up`, + parentFieldKey, + }; + }); + + // Filter out any elements that are hidden based on the current form data + // E.g. If the user has selected a value for a sentiment linear scale field + // and there is a follow-up question, include the follow-up field in the page. + return pageElements?.filter((element) => { + const key = element.scope?.split('/').pop(); + const followUpField = followUpFieldValues.find( + (item) => item.followUpFieldKey === key + ); + + return ( + !followUpField || + data[followUpField.parentFieldKey] || + (data[followUpField.parentFieldKey] && + !element.scope?.includes('follow_up')) + ); + }); +}; + +export default extractElementsByFollowUpLogic; diff --git a/front/app/components/Form/utils/getFollowUpControlKey.ts b/front/app/components/Form/utils/getFollowUpControlKey.ts new file mode 100644 index 000000000000..c20eeda29502 --- /dev/null +++ b/front/app/components/Form/utils/getFollowUpControlKey.ts @@ -0,0 +1,18 @@ +// This function is used for fields that have a "follow up" text field, +// which is currently only used for sentiment linear scale questions. +// The 'main' control will have a key like 'sentiment_linear_scale_o0b'. +// The 'follow up' control is modeled as a separate field and it will have +// a key like 'sentiment_linear_scale_o0b_follow_up'. +// This function extracts the 'main' control key from the 'follow up' control key. +function getFollowUpControlKey(scope: string = ''): string | undefined { + const regex = /^#\/properties\/(\w+)_follow_up$/; + const match = scope.match(regex); + + if (match) { + return match[1]; + } + + return undefined; +} + +export default getFollowUpControlKey; From 7e932e142a8f2aedd6866f3501bef15e3de39447 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 24 Feb 2025 09:02:14 +0100 Subject: [PATCH 23/40] Make follow up field a text area and use correct label value --- back/app/models/custom_field.rb | 6 +++--- back/app/services/idea_custom_fields_service.rb | 1 + back/config/locales/en.yml | 2 ++ back/spec/fixtures/locales/en.yml | 2 ++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/back/app/models/custom_field.rb b/back/app/models/custom_field.rb index 5760275c17e1..32606d27bba1 100644 --- a/back/app/models/custom_field.rb +++ b/back/app/models/custom_field.rb @@ -317,15 +317,15 @@ def follow_up_text_field follow_up_field_key = "#{key}_follow_up" title_multiloc = MultilocService.new.i18n_to_multiloc( - 'custom_fields.ideas.other_input_field.title', + 'custom_fields.ideas.ask_follow_up_field.title', locales: CL2_SUPPORTED_LOCALES ) CustomField.new( key: follow_up_field_key, - input_type: 'text', + input_type: 'multiline_text', title_multiloc: title_multiloc, - required: true, + required: false, enabled: true ) end diff --git a/back/app/services/idea_custom_fields_service.rb b/back/app/services/idea_custom_fields_service.rb index 5e8732bcdcec..95887e33b8a5 100644 --- a/back/app/services/idea_custom_fields_service.rb +++ b/back/app/services/idea_custom_fields_service.rb @@ -188,6 +188,7 @@ def insert_other_option_text_fields(fields) fields.each do |field| all_fields << field all_fields << field.other_option_text_field if field.other_option_text_field + all_fields << field.follow_up_text_field if field.follow_up_text_field end all_fields end diff --git a/back/config/locales/en.yml b/back/config/locales/en.yml index a9dccf5f1cbc..be9dc0b70d16 100644 --- a/back/config/locales/en.yml +++ b/back/config/locales/en.yml @@ -177,6 +177,8 @@ en: description: other_input_field: title: Type your answer + ask_follow_up_field: + title: Tell us why (optional) custom_forms: categories: main_content: diff --git a/back/spec/fixtures/locales/en.yml b/back/spec/fixtures/locales/en.yml index c617bd54d079..9a70255193ed 100644 --- a/back/spec/fixtures/locales/en.yml +++ b/back/spec/fixtures/locales/en.yml @@ -62,6 +62,8 @@ en: description: other_input_field: title: Type your answer + ask_follow_up_field: + title: Tell us why (optional) custom_forms: categories: main_content: From 5e750ebca106563e076074d87ebeca52b44ff12f Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 24 Feb 2025 09:03:41 +0100 Subject: [PATCH 24/40] Fix from review --- back/app/models/custom_field.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/back/app/models/custom_field.rb b/back/app/models/custom_field.rb index 32606d27bba1..9fb04e588e2f 100644 --- a/back/app/models/custom_field.rb +++ b/back/app/models/custom_field.rb @@ -123,8 +123,8 @@ def includes_other_option? options.any?(&:other) end - def includes_follow_up? - ask_follow_up == true + def ask_follow_up? + ask_follow_up end def support_free_text_value? @@ -313,7 +313,7 @@ def other_option_text_field end def follow_up_text_field - return if !includes_follow_up? + return unless ask_follow_up? follow_up_field_key = "#{key}_follow_up" title_multiloc = MultilocService.new.i18n_to_multiloc( From 9caae4869723a54be472325c646034373502e401 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 24 Feb 2025 09:39:49 +0100 Subject: [PATCH 25/40] Fix rubocop issue --- back/app/services/custom_field_params_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/back/app/services/custom_field_params_service.rb b/back/app/services/custom_field_params_service.rb index f449077a5d9e..35c344068da5 100644 --- a/back/app/services/custom_field_params_service.rb +++ b/back/app/services/custom_field_params_service.rb @@ -74,8 +74,8 @@ def reject_other_text_values(extra_field_values) end if key.end_with? '_follow_up' - parent_field_key = key.delete_suffix '_follow_up' - extra_field_values.delete key # TODO - Figure out how to accept follow-up values + key.delete_suffix '_follow_up' + extra_field_values.delete key # TODO: - Figure out how to accept follow-up values end end end From 10676b6a864d91cfa8beeed87ebbd9bc99d95763 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 24 Feb 2025 11:47:00 +0100 Subject: [PATCH 26/40] Improve accessibility --- .../SentimentLinearScaleControl.tsx | 160 ++++++++++-------- .../SentimentLinearScaleControl/utils.tsx | 2 + .../components/FormBuilderToolbox/index.tsx | 19 ++- 3 files changed, 105 insertions(+), 76 deletions(-) diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx index f04b8e7a6d3d..6bef42d6ac23 100644 --- a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx @@ -6,6 +6,7 @@ import { Table, Td, Text, + Th, Tr, } from '@citizenlab/cl2-component-library'; import { ControlProps } from '@jsonforms/core'; @@ -15,6 +16,7 @@ import styled, { useTheme } from 'styled-components'; import { FormLabel } from 'components/UI/FormComponents'; +import { ScreenReaderOnly } from 'utils/a11y'; import { FormattedMessage, useIntl } from 'utils/cl-intl'; import { getLabel, sanitizeForClassname } from 'utils/JSONFormUtils'; @@ -79,7 +81,7 @@ const SentimentLinearScaleControl = ({ sliderRef.current.setAttribute('aria-valuenow', String(data || minimum)); sliderRef.current.setAttribute( 'aria-valuetext', - getAriaLabel(data || minimum, 5) + getAriaLabel(data || minimum, maximum) ); } }, [data, getAriaLabel, minimum, maximum]); @@ -114,7 +116,7 @@ const SentimentLinearScaleControl = ({ return ( <> { + if (event.key !== 'Tab') { + handleKeyDown(event); + } + }} tabIndex={0} - onKeyDown={handleKeyDown} pb="0px" > - - - {[...Array(maximum).keys()].map((i) => { - const visualIndex = i + 1; - return ( - + ); + })} + +
- + + + {[...Array(maximum).keys()].map((i) => { + const visualIndex = i + 1; + return ( + - - {[...Array(maximum).keys()].map((i) => { - const visualIndex = i + 1; - return ( - + + {[...Array(maximum).keys()].map((i) => { + const visualIndex = i + 1; + return ( + - ); - })} - -
- {labelsFromSchema[visualIndex - 1]} - - - ); - })} -
- -
+ - { + if (data === visualIndex) { + // Clear data from this question and any follow-up question + handleChange(path, undefined); + handleChange(`${path}_follow_up`, undefined); + } else { + handleChange(path, visualIndex); + } }} - /> - - -
+ buttonStyle="text" + > + + {getAriaLabel(visualIndex, maximum)} + + + + +
+
+ { return 1; case 'End': return 5; + case 'Enter': + return value; default: return; } diff --git a/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx b/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx index 81a9b0911dd0..c5b88f175826 100644 --- a/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx @@ -265,15 +265,6 @@ const FormBuilderToolbox = ({ inputType="rating" disabled={isCustomFieldsDisabled} /> - addField('matrix_linear_scale')} - data-cy="e2e-matrix" - fieldsToInclude={builderConfig.toolboxFieldsToInclude} - inputType="matrix_linear_scale" - disabled={isCustomFieldsDisabled} - /> + addField('matrix_linear_scale')} + data-cy="e2e-matrix" + fieldsToInclude={builderConfig.toolboxFieldsToInclude} + inputType="matrix_linear_scale" + disabled={isCustomFieldsDisabled} + /> + Date: Mon, 24 Feb 2025 12:11:36 +0100 Subject: [PATCH 27/40] Code cleanup --- .../SentimentLinearScaleControl.tsx | 46 ++++++------------- .../SentimentLinearScaleControl/utils.tsx | 31 +++++++++++++ 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx index 6bef42d6ac23..0009bbd80834 100644 --- a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx @@ -12,7 +12,6 @@ import { import { ControlProps } from '@jsonforms/core'; import { withJsonFormsControlProps } from '@jsonforms/react'; import { UiSchema } from 'react-jsonschema-form'; -import styled, { useTheme } from 'styled-components'; import { FormLabel } from 'components/UI/FormComponents'; @@ -27,21 +26,12 @@ import messages from '../messages'; import { getAriaValueText, + getClassNameSentimentImage, getSentimentEmoji, handleKeyboardKeyChange, + StyledImg, } from './utils'; -const StyledImg = styled.img` - padding: 4px; - border: 3px solid white; - border-radius: 4px; - { - &:hover { - border: 3px solid ${({ theme }) => theme.colors.coolGrey600}; - } - } -`; - const SentimentLinearScaleControl = ({ data, path, @@ -53,7 +43,6 @@ const SentimentLinearScaleControl = ({ id, visible, }: ControlProps) => { - const theme = useTheme(); const { formatMessage } = useIntl(); const minimum = 1; @@ -63,6 +52,12 @@ const SentimentLinearScaleControl = ({ const sliderRef = useRef(null); + // Put all labels from the UI Schema in an array so we can easily access them + const labelsFromSchema = Array.from({ length: maximum }, (_, index) => { + return uischema.options?.[`linear_scale_label${index + 1}`]; + }); + + // Get the aria-label for the slider const getAriaLabel = useCallback( (value: number, total: number) => { return getAriaValueText({ @@ -103,11 +98,6 @@ const SentimentLinearScaleControl = ({ event.preventDefault(); }; - // Put all labels from the UI Schema in an array so we can easily access them - const labelsFromSchema = Array.from({ length: maximum }, (_, index) => { - return uischema.options?.[`linear_scale_label${index + 1}`]; - }); - // If the control is not visible, don't render anything if (!visible) { return null; @@ -136,11 +126,11 @@ const SentimentLinearScaleControl = ({ aria-labelledby={sanitizeForClassname(id)} onKeyDown={(event) => { if (event.key !== 'Tab') { + // Don't override the default tab behaviour handleKeyDown(event); } }} tabIndex={0} - pb="0px" > @@ -207,19 +197,11 @@ const SentimentLinearScaleControl = ({ diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx index 4981eccd5f33..06c52efcd01b 100644 --- a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx @@ -1,3 +1,5 @@ +import styled from 'styled-components'; + import messages from '../messages'; import Sentiment1Svg from './assets/sentiment_1.svg'; @@ -6,6 +8,35 @@ import Sentiment3Svg from './assets/sentiment_3.svg'; import Sentiment4Svg from './assets/sentiment_4.svg'; import Sentiment5Svg from './assets/sentiment_5.svg'; +export const StyledImg = styled.img` + padding: 4px; + border: 3px solid white; + border-radius: 4px; + height: 50px; + + &.anotherValueSelected { + filter: grayscale(1); + opacity: 0.8; + } + + &.isSelected { + border: 3px solid ${({ theme }) => theme.colors.coolGrey600}; + } + + &:hover { + border: 3px solid ${({ theme }) => theme.colors.coolGrey600}; + } +`; + +export const getClassNameSentimentImage = (data, visualIndex) => { + if (data === visualIndex) { + return 'isSelected'; + } else if (data) { + return 'anotherValueSelected'; + } + return ''; +}; + export const handleKeyboardKeyChange = (event, value) => { switch (event.key) { case 'ArrowLeft': From 9dbf8b7baf4035a662411c9c5334a76f6c1d0214 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 24 Feb 2025 12:14:22 +0100 Subject: [PATCH 28/40] Code cleanup --- .../SentimentLinearScaleControl.tsx | 7 +++--- .../components.tsx | 21 ++++++++++++++++++ .../SentimentLinearScaleControl/utils.tsx | 22 ------------------- 3 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 front/app/components/Form/Components/Controls/SentimentLinearScaleControl/components.tsx diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx index 0009bbd80834..9f3b2aca39e0 100644 --- a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/SentimentLinearScaleControl.tsx @@ -24,12 +24,12 @@ import VerificationIcon from '../../VerificationIcon'; import { getSubtextElement } from '../controlUtils'; import messages from '../messages'; +import { StyledImg } from './components'; import { getAriaValueText, getClassNameSentimentImage, getSentimentEmoji, handleKeyboardKeyChange, - StyledImg, } from './utils'; const SentimentLinearScaleControl = ({ @@ -154,9 +154,8 @@ const SentimentLinearScaleControl = ({ > {labelsFromSchema[visualIndex - 1]} - {labelsFromSchema[visualIndex - 1] - ? labelsFromSchema[visualIndex - 1] - : getAriaLabel(visualIndex, maximum)} + {!labelsFromSchema[visualIndex - 1] && + getAriaLabel(visualIndex, maximum)} diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/components.tsx b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/components.tsx new file mode 100644 index 000000000000..fd1e1d221f2f --- /dev/null +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/components.tsx @@ -0,0 +1,21 @@ +import styled from 'styled-components'; + +export const StyledImg = styled.img` + padding: 4px; + border: 3px solid white; + border-radius: 4px; + height: 50px; + + &.anotherValueSelected { + filter: grayscale(1); + opacity: 0.8; + } + + &.isSelected { + border: 3px solid ${({ theme }) => theme.colors.coolGrey600}; + } + + &:hover { + border: 3px solid ${({ theme }) => theme.colors.coolGrey600}; + } +`; diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx index 06c52efcd01b..a4cc2eea181d 100644 --- a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx @@ -1,5 +1,3 @@ -import styled from 'styled-components'; - import messages from '../messages'; import Sentiment1Svg from './assets/sentiment_1.svg'; @@ -8,26 +6,6 @@ import Sentiment3Svg from './assets/sentiment_3.svg'; import Sentiment4Svg from './assets/sentiment_4.svg'; import Sentiment5Svg from './assets/sentiment_5.svg'; -export const StyledImg = styled.img` - padding: 4px; - border: 3px solid white; - border-radius: 4px; - height: 50px; - - &.anotherValueSelected { - filter: grayscale(1); - opacity: 0.8; - } - - &.isSelected { - border: 3px solid ${({ theme }) => theme.colors.coolGrey600}; - } - - &:hover { - border: 3px solid ${({ theme }) => theme.colors.coolGrey600}; - } -`; - export const getClassNameSentimentImage = (data, visualIndex) => { if (data === visualIndex) { return 'isSelected'; From 723eabda35261a225221bfeb92b57e48577ad787 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 24 Feb 2025 14:42:11 +0100 Subject: [PATCH 29/40] Remove part of comment --- .../app/components/Form/utils/extractElementsByFollowUpLogic.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts b/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts index 693ea2eae53a..0c7b6931bee4 100644 --- a/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts +++ b/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts @@ -1,7 +1,6 @@ import { ExtendedUISchema, FormValues } from '../typings'; // This returns the elements on a page that are visible based on the data and the Sentiment Linear Scale selection. -// You can pass returnHidden as true to get the hidden elements const extractElementsByFollowUpLogic = ( pageElements: any, data: FormValues From 01903bed0daaa057e4b1d812f70212f99adf65bf Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 24 Feb 2025 14:52:28 +0100 Subject: [PATCH 30/40] Basic support for initial results view --- back/app/services/survey_results_generator_service.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/back/app/services/survey_results_generator_service.rb b/back/app/services/survey_results_generator_service.rb index e36812d4bcf8..4431294f1b60 100644 --- a/back/app/services/survey_results_generator_service.rb +++ b/back/app/services/survey_results_generator_service.rb @@ -263,7 +263,7 @@ def get_multilocs(field) end def get_option_multilocs(field) - if %w[linear_scale rating].include?(field.input_type) + if %w[linear_scale sentiment_linear_scale rating].include?(field.input_type) return build_scaled_input_multilocs(field) end @@ -279,7 +279,7 @@ def build_scaled_input_multilocs(field) { title_multiloc: locales.index_with { |_locale| value.to_s } } end - format_labels = %w[linear_scale matrix_linear_scale].include?(field.input_type) + format_labels = %w[linear_scale sentiment_linear_scale matrix_linear_scale].include?(field.input_type) answer_multilocs.each_key do |value| labels = field.nth_linear_scale_multiloc(value).transform_values do |label| @@ -295,7 +295,7 @@ def build_scaled_input_multilocs(field) def get_option_logic(field) return {} if field.logic.blank? - is_linear_or_rating = %w[linear_scale rating].include?(field.input_type) + is_linear_or_rating = %w[linear_scale sentiment_linear_scale rating].include?(field.input_type) options = if is_linear_or_rating # Create a unique ID for this linear scale option in the full results so we can filter logic (1..field.maximum).map { |value| { id: "#{field.id}_#{value}", key: value } } @@ -395,7 +395,7 @@ def group_query(query, group: false) end def generate_answer_keys(field) - (%w[linear_scale rating].include?(field.input_type) ? (1..field.maximum).to_a : field.options.map(&:key)) + [nil] + (%w[linear_scale sentiment_linear_scale rating].include?(field.input_type) ? (1..field.maximum).to_a : field.options.map(&:key)) + [nil] end # Convert stored user keys for domicile field to match the options keys eg "f6319053-d521-4b28-9d71-a3693ec95f45" => "north_london_8rg" From 7a4c911de15fb9e936a81885015bf946866a7221 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Mon, 24 Feb 2025 15:20:50 +0100 Subject: [PATCH 31/40] Use correct initial label values for sentiment_linear_scale fields --- .../components/FormBuilderToolbox/index.tsx | 61 +++++++-------- .../components/FormBuilderToolbox/utils.ts | 75 +++++++++++++++++++ .../FormBuilder/components/messages.ts | 20 +++++ 3 files changed, 126 insertions(+), 30 deletions(-) diff --git a/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx b/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx index 81a9b0911dd0..dd2c39d6dede 100644 --- a/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx @@ -29,6 +29,7 @@ import messages from '../messages'; import BuiltInFields from './BuiltInFields'; import LayoutFields from './LayoutFields'; import ToolboxItem from './ToolboxItem'; +import { getInitialLinearScaleLabel } from './utils'; interface FormBuilderToolboxProps { onAddField: (field: IFlatCreateCustomField, index: number) => void; @@ -87,36 +88,36 @@ const FormBuilderToolbox = ({ title_multiloc: { [locale]: '', }, - linear_scale_label_1_multiloc: { - [locale]: - inputType === 'matrix_linear_scale' - ? formatMessage(messages.stronglyDisagree) - : '', - }, - linear_scale_label_2_multiloc: { - [locale]: - inputType === 'matrix_linear_scale' - ? formatMessage(messages.disagree) - : '', - }, - linear_scale_label_3_multiloc: { - [locale]: - inputType === 'matrix_linear_scale' - ? formatMessage(messages.neutral) - : '', - }, - linear_scale_label_4_multiloc: { - [locale]: - inputType === 'matrix_linear_scale' - ? formatMessage(messages.agree) - : '', - }, - linear_scale_label_5_multiloc: { - [locale]: - inputType === 'matrix_linear_scale' - ? formatMessage(messages.stronglyAgree) - : '', - }, + linear_scale_label_1_multiloc: getInitialLinearScaleLabel({ + value: 1, + inputType, + formatMessage, + locale, + }), + linear_scale_label_2_multiloc: getInitialLinearScaleLabel({ + value: 2, + inputType, + formatMessage, + locale, + }), + linear_scale_label_3_multiloc: getInitialLinearScaleLabel({ + value: 3, + inputType, + formatMessage, + locale, + }), + linear_scale_label_4_multiloc: getInitialLinearScaleLabel({ + value: 4, + inputType, + formatMessage, + locale, + }), + linear_scale_label_5_multiloc: getInitialLinearScaleLabel({ + value: 5, + inputType, + formatMessage, + locale, + }), linear_scale_label_6_multiloc: {}, linear_scale_label_7_multiloc: {}, linear_scale_label_8_multiloc: {}, diff --git a/front/app/components/FormBuilder/components/FormBuilderToolbox/utils.ts b/front/app/components/FormBuilder/components/FormBuilderToolbox/utils.ts index b13acf151d65..021a35d6fb18 100644 --- a/front/app/components/FormBuilder/components/FormBuilderToolbox/utils.ts +++ b/front/app/components/FormBuilder/components/FormBuilderToolbox/utils.ts @@ -1,5 +1,80 @@ import styled from 'styled-components'; +import { ICustomFieldInputType } from 'api/custom_fields/types'; + +import { MessageDescriptor } from 'utils/cl-intl'; +import { FormatMessageValues } from 'utils/cl-intl/useIntl'; + +import messages from '../messages'; + export const DraggableElement = styled.div` cursor: move; `; + +type InitialLinearScaleLabelProps = { + value: number; + inputType: ICustomFieldInputType; + locale: string; + formatMessage: ( + messageDescriptor: MessageDescriptor, + values?: FormatMessageValues + ) => string; +}; + +// Function to get the initial default labels for a linear scale field +export const getInitialLinearScaleLabel = ({ + value, + inputType, + locale, + formatMessage, +}: InitialLinearScaleLabelProps) => { + if (inputType === 'matrix_linear_scale') { + switch (value) { + case 1: + return { + [locale]: formatMessage(messages.stronglyDisagree), + }; + case 2: + return { + [locale]: formatMessage(messages.disagree), + }; + case 3: + return { + [locale]: formatMessage(messages.neutral), + }; + case 4: + return { + [locale]: formatMessage(messages.agree), + }; + case 5: + return { + [locale]: formatMessage(messages.stronglyAgree), + }; + } + } else if (inputType === 'sentiment_linear_scale') { + switch (value) { + case 1: + return { + [locale]: formatMessage(messages.veryBad), + }; + case 2: + return { + [locale]: formatMessage(messages.bad), + }; + case 3: + return { + [locale]: formatMessage(messages.ok), + }; + case 4: + return { + [locale]: formatMessage(messages.good), + }; + case 5: + return { + [locale]: formatMessage(messages.veryGood), + }; + } + } + + return {}; +}; diff --git a/front/app/components/FormBuilder/components/messages.ts b/front/app/components/FormBuilder/components/messages.ts index f8477a5105d0..8491c79a1f79 100644 --- a/front/app/components/FormBuilder/components/messages.ts +++ b/front/app/components/FormBuilder/components/messages.ts @@ -342,4 +342,24 @@ export default defineMessages({ id: 'app.components.formBuilder.lastPage', defaultMessage: 'Ending', }, + veryBad: { + id: 'app.components.formBuilder.veryBad', + defaultMessage: 'Very bad', + }, + bad: { + id: 'app.components.formBuilder.bad', + defaultMessage: 'Bad', + }, + ok: { + id: 'app.components.formBuilder.ok', + defaultMessage: 'Ok', + }, + good: { + id: 'app.components.formBuilder.good', + defaultMessage: 'Good', + }, + veryGood: { + id: 'app.components.formBuilder.veryGood', + defaultMessage: 'Very good', + }, }); From ebd985447ce56a06e6eba296e0ef8fc7c083deb5 Mon Sep 17 00:00:00 2001 From: CircleCI Date: Mon, 24 Feb 2025 14:24:05 +0000 Subject: [PATCH 32/40] Translations updated by CI (extract-intl) --- front/app/translations/en.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/front/app/translations/en.json b/front/app/translations/en.json index 6076edac53e2..2a53b1239dd2 100644 --- a/front/app/translations/en.json +++ b/front/app/translations/en.json @@ -669,6 +669,7 @@ "app.components.formBuilder.ai1": "AI", "app.components.formBuilder.aiUpsellText1": "If you have access to our AI package, you will be able to summarise and categorise text responses with AI", "app.components.formBuilder.askFollowUpToggleLabel": "Ask follow up", + "app.components.formBuilder.bad": "Bad", "app.components.formBuilder.cancelLeaveBuilderButtonText": "Cancel", "app.components.formBuilder.chooseMany": "Choose many", "app.components.formBuilder.chooseOne": "Choose one", @@ -723,6 +724,7 @@ "app.components.formBuilder.formField.deleteFieldWithLogicConfirmationQuestion": "Deleting this page will also delete the logic associated with it. Are you sure you want to delete it?", "app.components.formBuilder.formField.deleteResultsInfo": "This cannot be undone", "app.components.formBuilder.goToPageInputLabel": "Then next page is:", + "app.components.formBuilder.good": "Good", "app.components.formBuilder.helmetTitle": "Form builder", "app.components.formBuilder.imageFileUpload": "Image upload", "app.components.formBuilder.invalidLogicBadgeMessage": "Invalid logic", @@ -766,6 +768,7 @@ "app.components.formBuilder.neutral": "Neutral", "app.components.formBuilder.newField": "New field", "app.components.formBuilder.number": "Number", + "app.components.formBuilder.ok": "Ok", "app.components.formBuilder.open": "Open", "app.components.formBuilder.optional": "Optional", "app.components.formBuilder.other": "Other", @@ -806,6 +809,8 @@ "app.components.formBuilder.title": "Title", "app.components.formBuilder.toLabel": "to", "app.components.formBuilder.unsavedChanges": "You have unsaved changes", + "app.components.formBuilder.veryBad": "Very bad", + "app.components.formBuilder.veryGood": "Very good", "app.components.phaseTimeLeft.xDayLeft": "{timeLeft, plural, =0 {Less than a day} one {# day} other {# days}} left", "app.components.phaseTimeLeft.xWeeksLeft": "{timeLeft} weeks left", "app.components.screenReaderCurrency.AED": "United Arab Emirates Dirham", From e7e8e9acfe7cd918faf4c72e99b13927717ab7ce Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Wed, 26 Feb 2025 09:05:15 +0100 Subject: [PATCH 33/40] Fixes from code review --- back/app/models/custom_field.rb | 5 +++++ .../app/serializers/web_api/v1/custom_field_serializer.rb | 2 +- back/config/locales/en.yml | 2 +- .../phase_context/update_all_native_survey_spec.rb | 8 +------- back/spec/fixtures/locales/en.yml | 2 +- back/spec/services/custom_field_params_service_spec.rb | 1 - 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/back/app/models/custom_field.rb b/back/app/models/custom_field.rb index 9fb04e588e2f..e019abf7e181 100644 --- a/back/app/models/custom_field.rb +++ b/back/app/models/custom_field.rb @@ -155,6 +155,11 @@ def supports_matrix_statements? input_type == 'matrix_linear_scale' end + + def supports_linear_scale_labels? + %w[linear_scale matrix_linear_scale sentiment_linear_scale].include?(input_type) + end + def average_rankings(scope) # This basically starts from all combinations of scope ID, option key (value) # and position (ordinality) and then calculates the average position for each diff --git a/back/app/serializers/web_api/v1/custom_field_serializer.rb b/back/app/serializers/web_api/v1/custom_field_serializer.rb index 537e6a093f9b..b7bd9b75d220 100644 --- a/back/app/serializers/web_api/v1/custom_field_serializer.rb +++ b/back/app/serializers/web_api/v1/custom_field_serializer.rb @@ -49,7 +49,7 @@ class WebApi::V1::CustomFieldSerializer < WebApi::V1::BaseSerializer :linear_scale_label_9_multiloc, :linear_scale_label_10_multiloc, :linear_scale_label_11_multiloc, - if: proc { |object, _params| object.linear_scale? || object.sentiment_linear_scale? || object.supports_matrix_statements? } + if: proc { |object, _params| object.supports_linear_scale_labels? } attributes :select_count_enabled, :maximum_select_count, :minimum_select_count, if: proc { |object, _params| object.multiselect? diff --git a/back/config/locales/en.yml b/back/config/locales/en.yml index be9dc0b70d16..91f48747fec2 100644 --- a/back/config/locales/en.yml +++ b/back/config/locales/en.yml @@ -178,7 +178,7 @@ en: other_input_field: title: Type your answer ask_follow_up_field: - title: Tell us why (optional) + title: Tell us why custom_forms: categories: main_content: diff --git a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb index 5c0d7a8c96c9..b20b991195eb 100644 --- a/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb +++ b/back/engines/commercial/idea_custom_fields/spec/acceptance/idea_custom_fields/phase_context/update_all_native_survey_spec.rb @@ -766,13 +766,7 @@ linear_scale_label_2_multiloc: { 'en' => 'Low' }, linear_scale_label_3_multiloc: { 'en' => 'Neutral' }, linear_scale_label_4_multiloc: { 'en' => 'High' }, - linear_scale_label_5_multiloc: { 'en' => 'Highest' }, - linear_scale_label_6_multiloc: {}, - linear_scale_label_7_multiloc: {}, - linear_scale_label_8_multiloc: {}, - linear_scale_label_9_multiloc: {}, - linear_scale_label_10_multiloc: {}, - linear_scale_label_11_multiloc: {} + linear_scale_label_5_multiloc: { 'en' => 'Highest' } }, final_page ] diff --git a/back/spec/fixtures/locales/en.yml b/back/spec/fixtures/locales/en.yml index 9a70255193ed..0b89ae848f42 100644 --- a/back/spec/fixtures/locales/en.yml +++ b/back/spec/fixtures/locales/en.yml @@ -63,7 +63,7 @@ en: other_input_field: title: Type your answer ask_follow_up_field: - title: Tell us why (optional) + title: Tell us why custom_forms: categories: main_content: diff --git a/back/spec/services/custom_field_params_service_spec.rb b/back/spec/services/custom_field_params_service_spec.rb index 7ea964492e1f..c9d4187c2453 100644 --- a/back/spec/services/custom_field_params_service_spec.rb +++ b/back/spec/services/custom_field_params_service_spec.rb @@ -13,7 +13,6 @@ create(:custom_field_shapefile_upload, key: 'shapefile_upload_field'), create(:custom_field_html_multiloc, key: 'html_multiloc_field'), create(:custom_field_linear_scale, key: 'linear_scale_field') - ] end From 219a53d52297cdd60cdf93d5b9934e12920f38a6 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Wed, 26 Feb 2025 09:12:25 +0100 Subject: [PATCH 34/40] Fixes from code review --- back/app/services/custom_field_params_service.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/back/app/services/custom_field_params_service.rb b/back/app/services/custom_field_params_service.rb index 35c344068da5..deffb8ed608f 100644 --- a/back/app/services/custom_field_params_service.rb +++ b/back/app/services/custom_field_params_service.rb @@ -72,11 +72,6 @@ def reject_other_text_values(extra_field_values) extra_field_values.delete key end end - - if key.end_with? '_follow_up' - key.delete_suffix '_follow_up' - extra_field_values.delete key # TODO: - Figure out how to accept follow-up values - end end end end From 74a3880f20f7b73a5d90ed5226029dfd7dca9517 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Wed, 26 Feb 2025 09:21:38 +0100 Subject: [PATCH 35/40] Fix lint issue --- back/app/models/custom_field.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/back/app/models/custom_field.rb b/back/app/models/custom_field.rb index e019abf7e181..6ffc7ec34f2c 100644 --- a/back/app/models/custom_field.rb +++ b/back/app/models/custom_field.rb @@ -155,7 +155,6 @@ def supports_matrix_statements? input_type == 'matrix_linear_scale' end - def supports_linear_scale_labels? %w[linear_scale matrix_linear_scale sentiment_linear_scale].include?(input_type) end From 0df400efa2291b37be1b53b744ca2d60a263671f Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Wed, 26 Feb 2025 09:58:00 +0100 Subject: [PATCH 36/40] Code cleanup from code review --- .../LinearAndRatingSettings/utils.ts | 9 +-- .../SentimentLinearScaleSettings/index.tsx | 38 +++++----- .../components/FormBuilderToolbox/utils.ts | 70 ++++++------------- 3 files changed, 46 insertions(+), 71 deletions(-) diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts index daf93da75b09..290693bea3a9 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/LinearAndRatingSettings/utils.ts @@ -1,11 +1,12 @@ import { ICustomFieldInputType } from 'api/custom_fields/types'; +const SCALE_OFFSETS: Partial> = { + sentiment_linear_scale: -2, // We show a scale of from -2 in the UI for sentiment scales. +}; + export const getNumberLabelForIndex = ( inputType: ICustomFieldInputType, index: number ) => { - if (inputType === 'sentiment_linear_scale') { - return index - 2; // We show a scale of -2 to 2 in the UI for sentiment scales. - } - return index + 1; // Show a scale of 1 to 11 in the UI for other field types. + return (SCALE_OFFSETS[inputType] ?? 1) + index; }; diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/index.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/index.tsx index 8b33c72b7d67..940e5c14a81e 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/SentimentLinearScaleSettings/index.tsx @@ -31,27 +31,27 @@ const SentimentLinearScaleSettings = ({ const { setValue, getValues } = useFormContext(); return ( - <> - - + + + { + setValue(askFollowUpName, !getValues(askFollowUpName), { + shouldDirty: true, + }); + }} + label={formatMessage(messages.askFollowUpToggleLabel)} /> - - { - setValue(askFollowUpName, !getValues(askFollowUpName)); - }} - label={formatMessage(messages.askFollowUpToggleLabel)} - /> - - + ); }; diff --git a/front/app/components/FormBuilder/components/FormBuilderToolbox/utils.ts b/front/app/components/FormBuilder/components/FormBuilderToolbox/utils.ts index 021a35d6fb18..7bffb3bf3c54 100644 --- a/front/app/components/FormBuilder/components/FormBuilderToolbox/utils.ts +++ b/front/app/components/FormBuilder/components/FormBuilderToolbox/utils.ts @@ -28,53 +28,27 @@ export const getInitialLinearScaleLabel = ({ locale, formatMessage, }: InitialLinearScaleLabelProps) => { - if (inputType === 'matrix_linear_scale') { - switch (value) { - case 1: - return { - [locale]: formatMessage(messages.stronglyDisagree), - }; - case 2: - return { - [locale]: formatMessage(messages.disagree), - }; - case 3: - return { - [locale]: formatMessage(messages.neutral), - }; - case 4: - return { - [locale]: formatMessage(messages.agree), - }; - case 5: - return { - [locale]: formatMessage(messages.stronglyAgree), - }; - } - } else if (inputType === 'sentiment_linear_scale') { - switch (value) { - case 1: - return { - [locale]: formatMessage(messages.veryBad), - }; - case 2: - return { - [locale]: formatMessage(messages.bad), - }; - case 3: - return { - [locale]: formatMessage(messages.ok), - }; - case 4: - return { - [locale]: formatMessage(messages.good), - }; - case 5: - return { - [locale]: formatMessage(messages.veryGood), - }; - } - } + const SCALE_LABELS: Record< + 'matrix_linear_scale' | 'sentiment_linear_scale', + Record + > = { + matrix_linear_scale: { + 1: messages.stronglyDisagree, + 2: messages.disagree, + 3: messages.neutral, + 4: messages.agree, + 5: messages.stronglyAgree, + }, + sentiment_linear_scale: { + 1: messages.veryBad, + 2: messages.bad, + 3: messages.ok, + 4: messages.good, + 5: messages.veryGood, + }, + }; - return {}; + return SCALE_LABELS[inputType]?.[value] + ? { [locale]: formatMessage(SCALE_LABELS[inputType][value]) } + : {}; }; From d48af9ebe43d44580f7bef288e003b556b1ddc2c Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Wed, 26 Feb 2025 10:41:01 +0100 Subject: [PATCH 37/40] Code cleanup and tests after code review --- .../SentimentLinearScaleControl/utils.tsx | 35 ++++---- .../extractElementsByFollowUpLogic.test.ts | 79 +++++++++++++++++++ .../utils/extractElementsByFollowUpLogic.ts | 30 ++++--- .../Form/utils/getFollowUpControlKey.test.ts | 9 +++ 4 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 front/app/components/Form/utils/extractElementsByFollowUpLogic.test.ts create mode 100644 front/app/components/Form/utils/getFollowUpControlKey.test.ts diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx index a4cc2eea181d..335c55f7052f 100644 --- a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx @@ -1,3 +1,8 @@ +import { ControlElement } from '@jsonforms/core'; +import { MessageDescriptor } from 'react-intl'; + +import { FormatMessageValues } from 'utils/cl-intl/useIntl'; + import messages from '../messages'; import Sentiment1Svg from './assets/sentiment_1.svg'; @@ -34,25 +39,23 @@ export const handleKeyboardKeyChange = (event, value) => { } }; -export const getSentimentEmoji = (index: number) => { - switch (index) { - case 1: - return Sentiment1Svg; - case 2: - return Sentiment2Svg; - case 3: - return Sentiment3Svg; - case 4: - return Sentiment4Svg; - case 5: - return Sentiment5Svg; - } - return Sentiment1Svg; +const sentimentEmojis: Record<1 | 2 | 3 | 4 | 5, string> = { + 1: Sentiment1Svg, + 2: Sentiment2Svg, + 3: Sentiment3Svg, + 4: Sentiment4Svg, + 5: Sentiment5Svg, }; +export const getSentimentEmoji = (index: number) => + sentimentEmojis[index as keyof typeof sentimentEmojis]; + type GetAriaValueTextProps = { - uischema: any; - formatMessage: any; + uischema: ControlElement; + formatMessage: ( + messageDescriptor: MessageDescriptor, + values?: FormatMessageValues + ) => string; value: number; total: number; }; diff --git a/front/app/components/Form/utils/extractElementsByFollowUpLogic.test.ts b/front/app/components/Form/utils/extractElementsByFollowUpLogic.test.ts new file mode 100644 index 000000000000..4fa9604384e9 --- /dev/null +++ b/front/app/components/Form/utils/extractElementsByFollowUpLogic.test.ts @@ -0,0 +1,79 @@ +import extractElementsByFollowUpLogic from './extractElementsByFollowUpLogic'; + +describe('extractElementsByFollowUpLogic', () => { + it('works', () => { + expect( + extractElementsByFollowUpLogic(pageElementsWithFollowUp, { + // Parent field has NOT been answered yet + sentiment_linear_scale_question_drs: undefined, + }) + ).toHaveLength(1); // Only the parent field is in the page elements + + expect( + extractElementsByFollowUpLogic(pageElementsWithFollowUp, { + // Parent field HAS been answered + sentiment_linear_scale_question_drs: 3, + }) + ).toHaveLength(2); // Both parent field and follow-up field should be in the page elements + + expect( + extractElementsByFollowUpLogic(pageElementsWithoutFollowUp, { + // Parent field HAS been answered + sentiment_linear_scale_question_drs: 3, + }) + ).toHaveLength(1); // Only the parent field is in the page elements + }); +}); + +const pageElementsWithFollowUp = [ + { + type: 'Control', // Parent field + scope: '#/properties/sentiment_linear_scale_question_drs', + label: 'How do you feel about living in Gothenburg?', + options: { + description: '', + input_type: 'sentiment_linear_scale', + isAdminField: false, + hasRule: false, + ask_follow_up: true, + linear_scale_label1: 'Very bad', + linear_scale_label2: 'Bad', + linear_scale_label3: 'Ok', + linear_scale_label4: 'Good', + linear_scale_label5: 'Very good', + }, + }, + { + type: 'Control', // Follow-up field + scope: '#/properties/sentiment_linear_scale_question_drs_follow_up', + label: 'Tell us why', + options: { + description: '', + input_type: 'multiline_text', + isAdminField: false, + hasRule: false, + textarea: true, + transform: 'trim_on_blur', + }, + }, +]; + +const pageElementsWithoutFollowUp = [ + { + type: 'Control', // Parent field + scope: '#/properties/sentiment_linear_scale_question_drs', + label: 'How do you feel about living in Gothenburg?', + options: { + description: '', + input_type: 'sentiment_linear_scale', + isAdminField: false, + hasRule: false, + ask_follow_up: false, + linear_scale_label1: 'Very bad', + linear_scale_label2: 'Bad', + linear_scale_label3: 'Ok', + linear_scale_label4: 'Good', + linear_scale_label5: 'Very good', + }, + }, +]; diff --git a/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts b/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts index 0c7b6931bee4..ef839fc4267a 100644 --- a/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts +++ b/front/app/components/Form/utils/extractElementsByFollowUpLogic.ts @@ -2,14 +2,14 @@ import { ExtendedUISchema, FormValues } from '../typings'; // This returns the elements on a page that are visible based on the data and the Sentiment Linear Scale selection. const extractElementsByFollowUpLogic = ( - pageElements: any, + pageElements: ExtendedUISchema[], data: FormValues ): ExtendedUISchema[] => { // Get a list of any "Follow-up" custom fields in the page. const followUpFieldValues = pageElements - ?.filter((element) => element.options?.ask_follow_up) + .filter((element) => element.options?.ask_follow_up) .map((element) => { - const parentFieldKey = element.scope?.split('/').pop(); + const parentFieldKey = element.scope.split('/').pop(); return { followUpFieldKey: `${parentFieldKey}_follow_up`, parentFieldKey, @@ -19,17 +19,29 @@ const extractElementsByFollowUpLogic = ( // Filter out any elements that are hidden based on the current form data // E.g. If the user has selected a value for a sentiment linear scale field // and there is a follow-up question, include the follow-up field in the page. - return pageElements?.filter((element) => { - const key = element.scope?.split('/').pop(); + return pageElements.filter((element) => { + const key = element.scope.split('/').pop(); + + // Check if the element is a follow-up field. const followUpField = followUpFieldValues.find( (item) => item.followUpFieldKey === key ); + // If the element is NOT a follow-up field, keep it in the page. + const isNotFollowUpField = !followUpField; + + // If the element IS a follow up field & the parent field DOES NOT have data yet, remove it from the page. + const noCurrentAnswerToParentQuestion = + followUpField?.parentFieldKey && !element.scope.includes('follow'); + + // If the element IS a follow up field & the parent field DOES have data, keep it in the page. + const parentFieldHasAnswer = + followUpField?.parentFieldKey && data[followUpField.parentFieldKey]; + return ( - !followUpField || - data[followUpField.parentFieldKey] || - (data[followUpField.parentFieldKey] && - !element.scope?.includes('follow_up')) + isNotFollowUpField || + parentFieldHasAnswer || + noCurrentAnswerToParentQuestion ); }); }; diff --git a/front/app/components/Form/utils/getFollowUpControlKey.test.ts b/front/app/components/Form/utils/getFollowUpControlKey.test.ts new file mode 100644 index 000000000000..eed4f6b8bdf2 --- /dev/null +++ b/front/app/components/Form/utils/getFollowUpControlKey.test.ts @@ -0,0 +1,9 @@ +import getFollowUpControlKey from './getFollowUpControlKey'; + +describe('getFollowUpControlKey', () => { + it('works', () => { + expect( + getFollowUpControlKey('#/properties/sentiment_linear_scale_o0b_follow_up') + ).toBe('sentiment_linear_scale_o0b'); + }); +}); From 235ad965594db66347fd9d9c556b797af106016e Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Wed, 26 Feb 2025 10:42:02 +0100 Subject: [PATCH 38/40] Add types --- .../Controls/SentimentLinearScaleControl/utils.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx index 335c55f7052f..6ae512e3d147 100644 --- a/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx +++ b/front/app/components/Form/Components/Controls/SentimentLinearScaleControl/utils.tsx @@ -11,7 +11,10 @@ import Sentiment3Svg from './assets/sentiment_3.svg'; import Sentiment4Svg from './assets/sentiment_4.svg'; import Sentiment5Svg from './assets/sentiment_5.svg'; -export const getClassNameSentimentImage = (data, visualIndex) => { +export const getClassNameSentimentImage = ( + data: number, + visualIndex: number +) => { if (data === visualIndex) { return 'isSelected'; } else if (data) { From fb013a07e76edc6e13df314f4f634df3e1133845 Mon Sep 17 00:00:00 2001 From: Amanda Anderson Date: Wed, 26 Feb 2025 11:53:14 +0100 Subject: [PATCH 39/40] Add optional label in to placeholder for follow up field --- .../Form/Components/Controls/TextAreaControl.tsx | 11 +++++++++-- .../components/Form/Components/Controls/messages.ts | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/front/app/components/Form/Components/Controls/TextAreaControl.tsx b/front/app/components/Form/Components/Controls/TextAreaControl.tsx index 5d1f968e443e..6b65dfcc640e 100644 --- a/front/app/components/Form/Components/Controls/TextAreaControl.tsx +++ b/front/app/components/Form/Components/Controls/TextAreaControl.tsx @@ -14,7 +14,7 @@ import getFollowUpControlKey from 'components/Form/utils/getFollowUpControlKey'; import { FormLabel } from 'components/UI/FormComponents'; import TextArea from 'components/UI/TextArea'; -import { FormattedMessage } from 'utils/cl-intl'; +import { FormattedMessage, useIntl } from 'utils/cl-intl'; import { isString } from 'utils/helperUtils'; import { getLabel, sanitizeForClassname } from 'utils/JSONFormUtils'; @@ -39,6 +39,7 @@ const TextAreaControl = ({ uischema, visible, }: ControlProps) => { + const { formatMessage } = useIntl(); const [didBlur, setDidBlur] = useState(false); const answerNotPublic = uischema.options?.answer_visible_to === 'admins'; @@ -82,7 +83,13 @@ const TextAreaControl = ({ setDidBlur(true); }} disabled={uischema.options?.readonly} - placeholder={isFollowUpField ? getLabel(uischema, schema, path) : ''} + placeholder={ + isFollowUpField + ? `${getLabel(uischema, schema, path)} ${formatMessage( + messages.optionalParentheses + )}` + : '' + } /> diff --git a/front/app/components/Form/Components/Controls/messages.ts b/front/app/components/Form/Components/Controls/messages.ts index e3cf5b41295e..965024c2fcf0 100644 --- a/front/app/components/Form/Components/Controls/messages.ts +++ b/front/app/components/Form/Components/Controls/messages.ts @@ -136,4 +136,8 @@ export default defineMessages({ id: 'app.components.form.controls.allStatementsError', defaultMessage: 'An answer must be selected for all statements.', }, + optionalParentheses: { + id: 'app.components.form.controls.optionalParentheses', + defaultMessage: '(optional)', + }, }); From 89370a9d37b5cb77ed4ad14c88eb7e6bce47520d Mon Sep 17 00:00:00 2001 From: CircleCI Date: Wed, 26 Feb 2025 10:56:58 +0000 Subject: [PATCH 40/40] Translations updated by CI (extract-intl) --- front/app/translations/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/front/app/translations/en.json b/front/app/translations/en.json index f1f52c96a414..04af5f3cae91 100644 --- a/front/app/translations/en.json +++ b/front/app/translations/en.json @@ -637,6 +637,7 @@ "app.components.form.controls.minimumCoordinates2": "A minimum of {numPoints} map points is required.", "app.components.form.controls.noRankSelected": "No rank selected", "app.components.form.controls.notPublic1": "*This answer will only be shared with project managers, and not to the public.", + "app.components.form.controls.optionalParentheses": "(optional)", "app.components.form.controls.rankingInstructions": "Drag and drop to rank options.", "app.components.form.controls.selectAsManyAsYouLike": "*Select as many as you like", "app.components.form.controls.selectBetween": "*Select between {minItems} and {maxItems} options",