diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 7d48ddf..e66bd66 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -36,9 +36,24 @@ @question = @answer.question @section = @plan.get_section(@question.section_id) - respond_to do |format| - format.js {} - end + render json: { + "question" => { + "id" => @question.id, + "answer_lock_version" => @answer.lock_version, + "locking" => @stale_answer ? + render_to_string(partial: 'answers/locking', locals: { question: @question, answer: @stale_answer, user: @answer.user }, formats: [:html]) : + nil, + "answer_status" => render_to_string(partial: 'answers/status', locals: { answer: @answer}, formats: [:html]) + }, + "section" => { + "id" => @section.id, + "progress" => render_to_string(partial: '/sections/progress', locals: { section: @section, plan: @plan }, formats: [:html]) + }, + "plan" => { + "id" => @plan.id, + "progress" => render_to_string(:partial => 'plans/progress', locals: { plan: @plan }, formats: [:html]) + } + }.to_json end # End update private diff --git a/app/controllers/plans_controller.rb b/app/controllers/plans_controller.rb index 93b9508..6031374 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -217,12 +217,12 @@ end end - def show_export + def download @plan = Plan.find(params[:id]) authorize @plan @phase_options = @plan.phases.order(:number).pluck(:title,:id) @export_settings = @plan.settings(:export) - render 'show_export' + render 'download' end @@ -277,7 +277,7 @@ end rescue ActiveRecord::RecordInvalid => e @phase_options = @plan.phases.order(:number).pluck(:title,:id) - redirect_to show_export_plan_path(@plan), alert: _('%{format} is not a valid exporting format. Available formats to export are %{available_formats}.') % + redirect_to download_plan_path(@plan), alert: _('%{format} is not a valid exporting format. Available formats to export are %{available_formats}.') % {format: params[:format], available_formats: ExportedPlan::VALID_FORMATS.to_s} end end diff --git a/app/controllers/settings/plans_controller.rb b/app/controllers/settings/plans_controller.rb index 07b2789..3117367 100644 --- a/app/controllers/settings/plans_controller.rb +++ b/app/controllers/settings/plans_controller.rb @@ -37,7 +37,7 @@ end respond_to do |format| @phase_options = @plan.phases.order(:number).pluck(:title,:id) - format.html { redirect_to(show_export_plan_path(@plan.id)) } + format.html { redirect_to(download_plan_path(@plan.id)) } # format.json { render json: settings_json } end end diff --git a/app/policies/plan_policy.rb b/app/policies/plan_policy.rb index d320fc2..b15c6e4 100644 --- a/app/policies/plan_policy.rb +++ b/app/policies/plan_policy.rb @@ -21,7 +21,7 @@ @plan.readable_by?(@user.id) && Role.find_by(user_id: @user.id, plan_id: @plan.id).active end - def show_export? + def download? @plan.readable_by?(@user.id) && Role.find_by(user_id: @user.id, plan_id: @plan.id).active end diff --git a/app/views/answers/_new_edit.html.erb b/app/views/answers/_new_edit.html.erb index 5706833..0aa8384 100644 --- a/app/views/answers/_new_edit.html.erb +++ b/app/views/answers/_new_edit.html.erb @@ -1,86 +1,22 @@ -<% q_format = question.question_format %> -<%= form_for answer, url: {controller: :answers, action: :update}, html: {method: :put, class: "roadmap-form answer", 'data-autosave': question.id}, remote: true do |f| %> -
- <% if !readonly %> - <%= f.hidden_field :id %> - <%= f.hidden_field :plan_id %> - <%= f.hidden_field :user_id %> - <%= f.hidden_field :question_id %> - <%= f.hidden_field :lock_version %> - <% end %> - -
- +<%= form_for answer, url: {controller: :answers, action: :update}, html: {method: :put, 'data-autosave': question.id, class: 'form-answer' } do |f| %> + <% if !readonly %> + <%= f.hidden_field :id %> + <%= f.hidden_field :plan_id %> + <%= f.hidden_field :user_id %> + <%= f.hidden_field :question_id %> + <%= f.hidden_field :lock_version %> + <% end %> +
<% if question.option_based? %> - <%= f.label raw(question.text), for: :question_option_ids %> - <% else %> - <%= f.label raw(question.text), for: :text %> + <%= render(partial: 'questions/new_edit_question_option_based', locals: { f: f, question: question, answer: answer, readonly: @readonly }) %> + <% elsif question.question_format.textfield?%> + <%= render(partial: 'questions/new_edit_question_textfield', locals: { f: f, question: question, answer: answer, readonly: @readonly }) %> + <% elsif question.question_format.textarea? %> + <%= render(partial: 'questions/new_edit_question_textarea', locals: { f: f, question: question, answer: answer, readonly: @readonly }) %> <% end %> - - <% if question.option_based? %> - <% options = question.question_options.by_number %> - <% if q_format.checkbox? %> -
    - <% options.each do |op| %> -
  1. - <%= f.check_box(:question_option_ids, { multiple: true, checked: answer.has_question_option(op.id), disabled: readonly }, op.id, nil) %> - <%= raw op.text %> -
  2. - <% end %> -
- <% elsif q_format.radiobuttons? %> -
    - <% options.each do |op| %> -
  1. - <%= f.radio_button :question_option_ids, op.id, { checked: answer.has_question_option(op.id), id: "answer_option_ids_#{op.id}", disabled: readonly } %> - <%= raw op.text %> -
  2. - <% end %> -
- <% elsif q_format.dropdown? || q_format.multiselectbox? %> - <% - options_html = "" - options.each do |op| - options_html += answer.has_question_option(op.id) ? - "" : - "" - end - %> - <%= select_tag('answer[question_option_ids]', raw(options_html), - {multiple: q_format.multiselectbox?, include_blank: q_format.dropdown?, disabled: readonly }) %> - <% end %> - - <% if question.option_comment_display == true %> - <%= label_tag('answer[text]', _('Comment')) %> - <% if readonly %> -

<%= raw(answer.text) %>

- <% else %> - <%= text_area_tag('answer[text]', answer.text, id: "answer-text-#{question.id}", class: "tinymce_answer") %> - <% end %> - <%end%> - - - <% elsif q_format.textfield? %> - <% if readonly %> -

<%= strip_tags(answer.text) %>

- <% else %> - <%= text_field_tag('answer[text]', strip_tags(answer.text)) %> - <% end %> - - <% elsif q_format.textarea? %> - <% if readonly %> -

<%= raw(answer.text) %>

- <% else %> - <%= text_area_tag('answer[text]', answer.text, id: "answer-text-#{question.id}", class: "tinymce_answer") %> - <% end %> - <% end %> - - <%= - raw("") - %> <% if !readonly && question.annotations.where(type: Annotation.types[:example_answer]).any? %> <% annotation = question.annotations.where(type: Annotation.types[:example_answer]).order(:created_at).first %> @@ -96,16 +32,8 @@
<% end %> <% end %> - -
- -
- <% if !readonly %> - - <% end %> -
" class="answer-status inline left-indent"> - <%= render(partial: 'answers/status', locals: { answer: answer }) %> -
-
+ <% if !readonly %> + <%= f.button(_('Save'), class: "btn btn-default", type: "submit") %> + <% end %> <% end %> \ No newline at end of file diff --git a/app/views/answers/_status.html.erb b/app/views/answers/_status.html.erb index c0fdfcc..4405d65 100644 --- a/app/views/answers/_status.html.erb +++ b/app/views/answers/_status.html.erb @@ -1,8 +1,13 @@ - -
+ <% if answer.updated_at.blank? %> - <%= _('Not answered yet') %> +

+ <%= _('Not answered yet') %> +

<% else %> - <%= _('Answered')%> <%= answer.updated_at.iso8601 %><%= _(' by')%> <%= answer.user.name %> +

+ <%= _('Answered')%> <%= answer.updated_at.iso8601 %><%= _(' by')%> <%= answer.user.name %> +

<% end %> \ No newline at end of file diff --git a/app/views/answers/update.js.erb b/app/views/answers/update.js.erb deleted file mode 100644 index d1dd1fe..0000000 --- a/app/views/answers/update.js.erb +++ /dev/null @@ -1,24 +0,0 @@ -// partial /answers/locking -<% if @stale_answer %> - $("#answer-locking-<%= @question.id%>") - .html("<%= escape_javascript(render partial: '/answers/locking', locals: { question: @question, answer: @stale_answer, user: @answer.user }) %>"); -<% else %> - $("#answer-locking-<%= @question.id%>").html(""); -<% end %> -// Destroys a tinymce editor whose id is defined below -dmproadmap.utils.tinymce.destroyEditorById('<%= "answer-text-#{@question.id}" %>'); -// partial /answer/new_edit -$("#answer-form-<%= @question.id%>") - .html("<%= escape_javascript(render partial: '/answers/new_edit', locals: { question: @question, answer: @answer, readonly: false }) %>"); -// Adds listeners for a tinyMCE editor with target element id passed -dmproadmap.answers.status.reloadEditorListeners('<%= "answer-text-#{@question.id}" %>'); -// partial /answer/status -$("#answer-status-<%= @question.id %>") - .html("<%= escape_javascript(render partial: '/answers/status', locals: { answer: @answer}) %>"); - -// partial /plans/progress -$(".progress").html("<%= escape_javascript(render :partial => '/plans/progress', locals: { plan: @plan }) %>"); - -// partial /sections/progress -$(".progress-bar-<%= @section.id %>") - .html("<%= escape_javascript(render partial: '/sections/progress', locals: { section: @section, plan: @plan }) %>"); \ No newline at end of file diff --git a/app/views/layouts/_es5_scripts.html.erb b/app/views/layouts/_es5_scripts.html.erb index f293a65..e575d54 100644 --- a/app/views/layouts/_es5_scripts.html.erb +++ b/app/views/layouts/_es5_scripts.html.erb @@ -4,12 +4,11 @@ <%= javascript_include_tag 'jquery.min.js' %> <%= javascript_include_tag 'rails.js' %> <%= javascript_include_tag 'jquery-ui.min.js' %> - <%= javascript_include_tag 'jquery-accessible-autocomplete-list-aria.js' %> <%= javascript_include_tag 'placeholder.min.js' %> <%= javascript_include_tag 'jquery.tablesorter.min.js' %> <%= javascript_include_tag 'jquery.tablesorter.widgets.min.js' %> <%= javascript_include_tag 'jquery.timeago.js' %> - <%= javascript_include_tag 'bootstrap.min.js' %> + <%#= javascript_include_tag 'bootstrap.min.js' %> @@ -30,7 +29,6 @@ - <%= javascript_include_tag 'views/answers/status.js' %> <%= javascript_include_tag 'views/guidances/admin_edit.js' %> <%= javascript_include_tag 'views/home/index.js' %> <%= javascript_include_tag 'views/notes/add.js' %> @@ -40,12 +38,8 @@ <%= javascript_include_tag 'views/orgs/admin_edit.js' %> <%= javascript_include_tag 'views/orgs/shibboleth_ds.js' %> <%= javascript_include_tag 'views/plans/available_templates.js' %> - <%= javascript_include_tag 'views/plans/export_configure.js' %> <%= javascript_include_tag 'views/plans/index.js' %> - <%= javascript_include_tag 'views/plans/new.js' %> <%= javascript_include_tag 'views/registrations/sign_in_sign_up.js' %> - <%#= javascript_include_tag 'views/shared/accessible_combobox.js' %> - <%#= javascript_include_tag 'views/shared/accessible_submit_button.js' %> <%= javascript_include_tag 'views/shared/login_form.js' %> <%= javascript_include_tag 'views/shared/register_form.js' %> <%= javascript_include_tag 'views/users/notification_preferences.js' %> diff --git a/app/views/phases/_edit_plan_answers.html.erb b/app/views/phases/_edit_plan_answers.html.erb new file mode 100644 index 0000000..1a9d52e --- /dev/null +++ b/app/views/phases/_edit_plan_answers.html.erb @@ -0,0 +1,90 @@ +
+
+
+ +
+
+ <%= render :partial => "/plans/progress", locals: { plan: @plan } %> +
+
+
+
+ <% @phase.sections.order(:number).each do |section| %> + <% sectionid = section.id %> +
+ +
+
+
+
<%= raw section.description %>
+ + + + <% section.questions.each do |question| %> + <% # Load the answer or create a new one + answers = question.plan_answers(@plan.id) + if answers.present? + answer = answers.first + else + answer = Answer.new({plan_id: @plan.id, question_id: question.id, user_id: current_user.id }) + if question.default_value.present? + answer.text = question.default_value + end + end + %> +
+
+ +
+
" class="answer-locking">
+ <%= render(partial: 'answers/new_edit', locals: { question: question, answer: answer, readonly: @readonly }) %> +
"> + <%= render(partial: 'answers/status', locals: { answer: answer }) %> +
+
+
+
+ + <%= render partial: 'guidance_section', locals: {plan: @plan, question: question, answer: answer, question_guidances: @question_guidances} %> +
+
+ <% end %> +
+
+ <% end %> +
+ +
+
\ No newline at end of file diff --git a/app/views/phases/edit.html.erb b/app/views/phases/edit.html.erb index 5b0e509..92b5bb6 100644 --- a/app/views/phases/edit.html.erb +++ b/app/views/phases/edit.html.erb @@ -1,149 +1,11 @@ - -
-

<%= @plan.title %>

-
- - - - -
-
- -
- - -
- <%= render :partial => "/plans/progress", locals: { plan: @plan } %> -
-
- -
- <% @phase.sections.order(:number).each do |section| %> - <% sectionid = section.id %> - -
- -
- -
-
- -
<%= raw section.description %>
- - - - - -
- <% section.questions.each do |question| %> - <% if question.id == session[:question_id_comments].to_i then id_css = "current_question" end %> -
- - <% - # Load the answer or create a new one - answers = question.plan_answers(@plan.id) - if answers.present? - answer = answers.first - else - answer = Answer.new({plan_id: @plan.id, - question_id: question.id, - user_id: current_user.id }) - if question.default_value.present? - answer.text = question.default_value - end - end - %> - - -
-
-
" class="answer-locking">
-
"> - <%= render(partial: 'answers/new_edit', - locals: { question: question, answer: answer, readonly: @readonly }) %> -
-
-
- - - -
- <%= render partial: 'guidance_section', - locals: {plan: @plan, question: question, answer: answer, - question_guidances: @question_guidances} %> -
-
- <% end %> - -
- -
-
- - <% end %> -
- - -
-
-
+ <%= render partial: 'edit_plan_answers', layout: 'shared/plan_navigation', locals: {} %>
-
\ No newline at end of file + \ No newline at end of file diff --git a/app/views/plans/_download.html.erb b/app/views/plans/_download.html.erb deleted file mode 100644 index e69de29..0000000 --- a/app/views/plans/_download.html.erb +++ /dev/null diff --git a/app/views/plans/_download_form.html.erb b/app/views/plans/_download_form.html.erb new file mode 100644 index 0000000..03449f9 --- /dev/null +++ b/app/views/plans/_download_form.html.erb @@ -0,0 +1,102 @@ +<%= form_tag( export_plan_path(@plan), method: :get) do |f| %> +

<%= _("Download Settings") %>

+ + <% if @phase_options.length > 1 %> +
+ <%= label_tag(:phase_id, _("Select phase to download")) %> + <%= select_tag(:phase_id, options_for_select(@phase_options, @phase_options[0])) %> +
+ <% else %> + <%= hidden_field_tag(:phase_id, @phase_options[0][1]) %> + <% end %> + +

<%= _("Optional plan components") %>

+
+ <%= label_tag 'export[project_details]', raw("#{check_box_tag 'export[project_details]', true, false} #{_('project details coversheet')}") %> +
+
+ <%= label_tag 'export[question_headings]', raw("#{check_box_tag 'export[question_headings]', true, true} #{_('question text and section headings')}") %> +
+
+ <%= label_tag 'export[unanswered_questions]', raw("#{check_box_tag 'export[unanswered_questions]', true, true} #{_('unanswered questions')}") %> +
+ <% if @plan.template.customization_of.present? %> +
+ <%= label_tag 'export[custom_sections]', raw("#{check_box_tag 'export[custom_sections]', true, false} #{_('supplementary section(s) not requested by funding organisation')}") %> +
+ <% end %> + +

<%= _('Format') %>

+
+
+ <%= select_tag :format, options_for_select(ExportedPlan::VALID_FORMATS, :pdf), + class: 'form-control' %> +
+
+ +

<%= _('PDF Formatting') %>

+
+
+

<%= _('Font') %>

+
+
+

<%= _('Margin (mm)') %>

+
+
+
+
+ <%= label_tag "export[formatting][font_face]", _('Face'), class: 'control-label' %> + <%= select_tag "export[formatting][font_face]", + options_for_select(Settings::Template::VALID_FONT_FACES, + @export_settings.formatting[:font_face]), + class: 'form-control', + "data-default": @plan.template.settings(:export).formatting[:font_face] %> +
+
+ <%= label_tag "export[formatting][font_size]", _('Size') + " (pt)", class: 'control-label' %> + <%= select_tag "export[formatting][font_size]", + options_for_select(Settings::Template::VALID_FONT_SIZE_RANGE.to_a, @export_settings.formatting[:font_size]), + class: 'form-control', + "data-default": @plan.template.settings(:export).formatting[:font_size] %> +
+ +
+ <%= label_tag "export[formatting][margin][top]", _('Top'), + class: 'control-label' %> + <%= select_tag "export[formatting][margin][top]", + options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, + @export_settings.formatting[:margin][:top]), + class: 'form-control', + "data-default": @plan.template.settings(:export).formatting[:margin][:top] %> +
+
+ <%= label_tag "export[formatting][margin][bottom]", _('Bottom'), + class: 'control-label' %> + <%= select_tag "export[formatting][margin][bottom]", + options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, + @export_settings.formatting[:margin][:bottom]), + class: 'form-control', + "data-default": @plan.template.settings(:export).formatting[:margin][:bottom] %> +
+
+ <%= label_tag "export[formatting][margin][left]", _('Left'), + class: 'control-label' %> + <%= select_tag "export[formatting][margin][left]", + options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, + @export_settings.formatting[:margin][:left]), + class: 'form-control', + "data-default": @plan.template.settings(:export).formatting[:margin][:left] %> +
+
+ <%= label_tag "export[formatting][margin][right]", _('Right'), + class: 'control-label' %> + <%= select_tag "export[formatting][margin][right]", + options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, + @export_settings.formatting[:margin][:right]), + class: 'form-control', + "data-default": @plan.template.settings(:export).formatting[:margin][:rigth] %> +
+
+ + <%= button_tag(_('Download Plan'), class: "btn btn-primary", type: "submit") %> +<% end %> diff --git a/app/views/plans/_progress.html.erb b/app/views/plans/_progress.html.erb index db2542a..e95f0b9 100644 --- a/app/views/plans/_progress.html.erb +++ b/app/views/plans/_progress.html.erb @@ -1,9 +1,11 @@ <% nanswers = plan.num_answered_questions() nquestions = plan.num_questions() + value=(nanswers.to_f/nquestions*100).round(2) %> -<% answered = %(#{nanswers}/#{nquestions})%> -
-
 
- <%= answered -%> <%= _(' answered')%> +
;"> + <%= "#{value} % #{_('answered')}" %>
diff --git a/app/views/plans/_share.html.erb b/app/views/plans/_share.html.erb deleted file mode 100644 index e69de29..0000000 --- a/app/views/plans/_share.html.erb +++ /dev/null diff --git a/app/views/plans/_share_form.html.erb b/app/views/plans/_share_form.html.erb index a4abdd2..a62fb16 100644 --- a/app/views/plans/_share_form.html.erb +++ b/app/views/plans/_share_form.html.erb @@ -80,5 +80,5 @@ <%= f.label :access_level, raw("#{f.radio_button :access_level, 3, "aria-required": true} #{_('Co-owner: can edit project details, change visibility, and add collaborators')}") %>
- <%= f.button(_('Submit'), class: "btn btn-default", type: "submit") %> + <%= f.button(_('Submit'), class: "btn btn-primary", type: "submit") %> <% end %> \ No newline at end of file diff --git a/app/views/plans/download.html.erb b/app/views/plans/download.html.erb new file mode 100644 index 0000000..f971efe --- /dev/null +++ b/app/views/plans/download.html.erb @@ -0,0 +1,12 @@ +
+
+ +

<%= @plan.title %>

+
+
+ +
+
+ <%= render partial: 'download_form', layout: '/shared/plan_navigation', locals: { plan: @plan } %> +
+
diff --git a/app/views/plans/index.html.erb b/app/views/plans/index.html.erb index fd90df3..9567967 100644 --- a/app/views/plans/index.html.erb +++ b/app/views/plans/index.html.erb @@ -76,14 +76,14 @@
  • <%= link_to _('Share'), share_plan_path(plan) %>
  • <% end %> -
  • <%= link_to _('Download'), show_export_plan_path(plan) %>
  • +
  • <%= link_to _('Download'), download_plan_path(plan) %>
  • <%= link_to _('Make a copy'), duplicate_plan_path(plan), method: :post, remote: true %>
  • <% else %>
  • <%= link_to _('View'), plan_path(plan) %>
  • -
  • <%= link_to _('Download'), show_export_plan_path(plan) %>
  • +
  • <%= link_to _('Download'), download_plan_path(plan) %>
  • <% end %> <% role = plan.roles.where(user_id: current_user.id).first %> <% conf = (role.creator? && plan.publicly_visible?) ? _("Are you sure you wish to remove this public plan? This will remove it from the Public DMPs page but any collaborators will still be able to access it.") : _("Are you sure you wish to remove this plan? Any collaborators will still be able to access it.") %> diff --git a/app/views/plans/new.html.erb b/app/views/plans/new.html.erb index 0f80120..515cd05 100644 --- a/app/views/plans/new.html.erb +++ b/app/views/plans/new.html.erb @@ -10,89 +10,71 @@
    - <%= form_for @plan, html: {method: :post, class: "roadmap-form padded bordered"}, remote: true do |f| %> -
    - - -
    - <%= _('What research project are you planning?') %> - -
    - <%= f.text_field(:title, class: 'left-indent input-large', + <%= form_for @plan, html: {method: :post, id: 'create-plan'}, remote: true do |f| %> + +

    <%= _('What research project are you planning?') %>

    +
    +
    + <%= f.text_field(:title, class: 'form-control', 'aria-describedby': 'project-title', 'aria-required': 'true', 'data-toggle': 'tooltip', 'data-content': _('If applying for funding, state the project title exactly as in the proposal.')) %> - <%= check_box_tag(:is_test, "1", false, class: 'left-indent') %> - <%= label_tag(:is_test, _('mock project for testing, practice, or educational purposes'), class: 'inline checkbox-label') %> -
    -
    +
    +
    + <%= label_tag(:is_test, raw("#{check_box_tag(:is_test, "1", false)} #{_('mock project for testing, practice, or educational purposes')}")) %> +
    +
    - -
    - <%= _('Select the primary research organisation') %> - -
    - <%= render partial: "shared/accessible_combobox", - locals: {name: 'plan[org_name]', - id: 'plan_org_name', - default_selection: @default_org, - models: @orgs, - attribute: 'name', - tooltip: _('Please select a valid research organisation from the list.'), - error: _('You must select a research organisation from the list.'), - classes: 'input-large'} %> -
    - -
    -

    - <%= _('or') %> -

    - <%= check_box_tag(:plan_no_org) %> - <%= label_tag(:plan_no_org, raw(" - #{_('My research organisation is not on the list')} - #{_(' or ')} - #{_('no research organisation is associated with this plan')} - "), class: 'checkbox-label') %> -
    -
    - - -
    - <%= _('Select the primary funding organisation') %> - -
    - <%= render partial: "shared/accessible_combobox", - locals: {name: 'plan[funder_name]', - id: 'plan_funder_name', - default_selection: nil, - models: @funders, - attribute: 'name', - tooltip: _('Please select a valid funding organisation from the list.'), - error: _('You must select a funding organisation from the list.'), - classes: 'input-large'} %> -
    -
    -

    - <%= _('or') %> -

    - <%= check_box_tag(:plan_no_funder) %> - <%= label_tag(:plan_no_funder, _('No funder associated with this plan'), class: 'checkbox-label') %> -
    -
    - - -
    -
    - + +

    <%= _('Select the primary research organisation') %>

    +
    +
    + <%= render partial: "shared/accessible_combobox", + locals: {name: 'plan[org_name]', + id: 'plan_org_name', + default_selection: @default_org, + models: @orgs, + attribute: 'name', + required: true, + error: _('You must select a research organisation from the list.'), + tooltip: _('Please select a valid research organisation from the list.')} %> +
    +
    + <%= label_tag(:plan_no_org, raw("#{check_box_tag(:plan_no_org)} #{_('My research organisation is not on the list')} #{_(' or ')} #{_('no research organisation is associated with this plan')}")) %> +
    -
    -
    - <%= f.hidden_field(:template_id) %> - <%= f.hidden_field(:visibility, value: @is_test ? 'is_test' : Rails.application.config.default_plan_visibility) %> - <%= render partial: 'shared/accessible_submit_button', - locals: {id: 'create_plan_submit', - val: 'Create Plan', - disabled_initially: true, - classes: "left-indent", - tooltip: _('You can not continue until you have filled in all of the required information.')} %> + +

    <%= _('Select the primary funding organisation') %>

    +
    +
    + <%= render partial: "shared/accessible_combobox", + locals: {name: 'plan[funder_name]', + id: 'plan_funder_name', + default_selection: nil, + models: @funders, + attribute: 'name', + required: true, + error: _('You must select a funding organisation from the list.'), + tooltip: _('Please select a valid funding organisation from the list.')} %>
    -
    +
    + <%= label_tag(:plan_no_funder, raw("#{check_box_tag(:plan_no_funder)} #{_('No funder associated with this plan')}")) %> +
    +
    + + +
    +
    + +
    +
    + +
    +
    + + <%= f.hidden_field(:template_id) %> + <%= f.hidden_field(:visibility, value: @is_test ? 'is_test' : Rails.application.config.default_plan_visibility) %> + <%= f.button(_('Create plan'), class: "btn btn-primary", type: "submit") %> <% end %> diff --git a/app/views/plans/show_export.html.erb b/app/views/plans/show_export.html.erb deleted file mode 100644 index 3a2253a..0000000 --- a/app/views/plans/show_export.html.erb +++ /dev/null @@ -1,124 +0,0 @@ -<%- model_class = Plan -%> - -
    -
    - -

    <%= @plan.title %>

    -
    -
    - -
    -
    -
    - - - - - -
    - -
    - -

    <%= _("Download Settings") %>

    -
    - <%= form_tag( export_plan_path(@plan), method: :get, html: {class: "roadmap-form"}) do |f| %> - <% if @phase_options.length > 1 %> -
    - <%= label_tag(:phase_id, _("Select phase to download")) %> - <%= select_tag(:phase_id, options_for_select(@phase_options, @phase_options[0])) %> -
    - <% else %> - <%= hidden_field_tag(:phase_id, @phase_options[0][1]) %> - <% end %> -

    <%= _("Optional plan components") %>

    -
    - <%= check_box_tag 'export[project_details]', true, false %> - <%= label_tag 'export[project_details]', _('project details coversheet'), class: 'checkbox-label' %> -
    -
    - <%= check_box_tag 'export[question_headings]', true, true, class: 'question-headings'%> - <%= label_tag 'export[question_headings]', _('question text and section headings'), class: 'checkbox-label' %> -
    -
    - <%= check_box_tag 'export[unanswered_questions]', true, true, class: 'unanswered-questions' %> - <%= label_tag 'export[unanswered_questions]', _('unanswered questions'), class: 'checkbox-label' %> -
    - <% if @plan.template.customization_of.present? %> -
    - <%= check_box_tag 'export[custom_sections]', true, false %> - <%= label_tag 'export[custom_sections]', _('supplementary section(s) not requested by funding organisation'), class: 'checkbox-label' %> -
    - <% end %> - - -
    -
    - <%= label_tag(:format, _('Format')) %> - <%= select_tag(:format, options_for_select(ExportedPlan::VALID_FORMATS, :pdf), class: 'export-format-selection') %> -
    - - -
    -
    -

    <%= _('PDF Formatting') %>

    -
    - <%= _('Font') -%> -
    - <%= label_tag("export[formatting][font_face]", _('Face')) %> - <%= select_tag("export[formatting][font_face]", options_for_select(Settings::Template::VALID_FONT_FACES, @export_settings.formatting[:font_face]), { "data-default" => @plan.template.settings(:export).formatting[:font_face] }) %> -
    -
    - <%= label_tag("export[formatting][font_size]", _('Size') + " (pt)") %> - <%= select_tag("export[formatting][font_size]", options_for_select(Settings::Template::VALID_FONT_SIZE_RANGE.to_a, @export_settings.formatting[:font_size]), { "data-default" => @plan.template.settings(:export).formatting[:font_size] }) %> -
    -
    -
    - <%= _('Margin') -%> (mm) -
    - <%= label_tag("export[formatting][margin][top]", _('Top')) %> - <%= select_tag("export[formatting][margin][top]", options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, @export_settings.formatting[:margin][:top]), { "data-default" => @plan.template.settings(:export).formatting[:margin][:top] }) %> -
    -
    - <%= label_tag("export[formatting][margin][bottom]", _('Bottom')) %> - <%= select_tag("export[formatting][margin][bottom]", options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, @export_settings.formatting[:margin][:bottom]), { "data-default" => @plan.template.settings(:export).formatting[:margin][:bottom] }) %> -
    -
    - <%= label_tag("export[formatting][margin][left]", _('Left')) %> - <%= select_tag("export[formatting][margin][left]", options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, @export_settings.formatting[:margin][:left]), { "data-default" => @plan.template.settings(:export).formatting[:margin][:left] }) %> -
    -
    - <%= label_tag("export[formatting][margin][right]", _('Right')) %> - <%= select_tag("export[formatting][margin][right]", options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, @export_settings.formatting[:margin][:right]), { "data-default" => @plan.template.settings(:export).formatting[:margin][:rigth] }) %> -
    -
    -
    - -
    -
     
    - <%= submit_tag _('Download Plan'), class: 'btn btn-primary', role: 'button' %> -
    - - <% end %> -
    -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/app/views/questions/_new_edit_question_option_based.html.erb b/app/views/questions/_new_edit_question_option_based.html.erb new file mode 100644 index 0000000..7a5a0c1 --- /dev/null +++ b/app/views/questions/_new_edit_question_option_based.html.erb @@ -0,0 +1,48 @@ +<% options = question.question_options.by_number %> +<% if question.question_format.checkbox? %> + <%= f.label(:question_option_ids, raw(question.text), class: 'control-label') %> + <% options.each do |op| %> +
    + +
    + <% end %> +<% elsif question.question_format.radiobuttons? %> + <%= f.label(:question_option_ids, raw(question.text), class: 'control-label') %> + <% options.each do |op| %> +
    + +
    + <% end %> +<% elsif question.question_format.dropdown? || question.question_format.multiselectbox? %> + <% + options_html = "" + options.each do |op| + options_html += answer.has_question_option(op.id) ? + "" : + "" + end + %> + <%= f.label(:question_option_ids, raw(question.text), class: 'control-label') %> + <%= select_tag('answer[question_option_ids]', + raw(options_html), + { multiple: question.question_format.multiselectbox?, + include_blank: question.question_format.dropdown?, + disabled: readonly, + class: 'form-control' }) %> +<% end %> +
    +<% if question.option_comment_display %> + <%= label_tag('answer[text]', _('Comment'), class: 'control-label') %> + <% if readonly %> +

    <%= raw(answer.text) %>

    + <% else %> + <%= text_area_tag('answer[text]', answer.text, id: "answer-text-#{question.id}", class: "form-control tinymce_answer") %> + <% end %> +<% end %> +
    diff --git a/app/views/questions/_new_edit_question_textarea.html.erb b/app/views/questions/_new_edit_question_textarea.html.erb new file mode 100644 index 0000000..c390574 --- /dev/null +++ b/app/views/questions/_new_edit_question_textarea.html.erb @@ -0,0 +1,8 @@ +
    + <%= f.label(:text, raw(question.text), class: 'control-label') %> + <% if readonly %> +

    <%= raw(answer.text) %>

    + <% else %> + <%= text_area_tag('answer[text]', answer.text, id: "answer-text-#{question.id}", class: "form-control tinymce_answer") %> + <% end %> +
    \ No newline at end of file diff --git a/app/views/questions/_new_edit_question_textfield.html.erb b/app/views/questions/_new_edit_question_textfield.html.erb new file mode 100644 index 0000000..b1dbd95 --- /dev/null +++ b/app/views/questions/_new_edit_question_textfield.html.erb @@ -0,0 +1,8 @@ +
    + <%= f.label(:text, raw(question.text), class: 'control-label') %> + <% if readonly %> +

    <%= strip_tags(answer.text) %>

    + <% else %> + <%= text_field_tag('answer[text]', strip_tags(answer.text), class: 'form-control') %> + <% end %> +
    \ No newline at end of file diff --git a/app/views/sections/_progress.html.erb b/app/views/sections/_progress.html.erb index 02c8d69..7eae314 100644 --- a/app/views/sections/_progress.html.erb +++ b/app/views/sections/_progress.html.erb @@ -3,6 +3,6 @@ <% num_section_answers = section.num_answered_questions(plan.id) %> - <%= num_section_answers %> / <%= num_section_questions %> + (<%= num_section_answers %> / <%= num_section_questions %>) \ No newline at end of file diff --git a/app/views/shared/_accessible_combobox.html.erb b/app/views/shared/_accessible_combobox.html.erb index 6b32337..f2ef99f 100644 --- a/app/views/shared/_accessible_combobox.html.erb +++ b/app/views/shared/_accessible_combobox.html.erb @@ -1,10 +1,13 @@ <% if !models.nil? %> + <% required = required ||= false %> + <% classes = classes ||= '' %> + <% error = error ||= _('Please select an item from the list.') %> + <% json = {} %> <% models.map{|m| json[m[attribute]] = m.id} %> - <% err_msg = error ||= _('Please select an item from the list.') %> + + + " name="<%= name.gsub("_#{attribute}]", "_id]") %>" - value="<%= default_selection.id unless default_selection.nil? %>" /> + value="<%= default_selection.id unless default_selection.nil? %>" aria-required="<%= required %>" + data-validation="js-combobox" data-validation-error="<%= error %>" /> - - <% else %> <%= _('No items available.') %> <% end %> \ No newline at end of file diff --git a/app/views/shared/_plan_navigation.html.erb b/app/views/shared/_plan_navigation.html.erb index 1c65b33..a6ffbd4 100644 --- a/app/views/shared/_plan_navigation.html.erb +++ b/app/views/shared/_plan_navigation.html.erb @@ -12,8 +12,8 @@ -
    diff --git a/config/routes.rb b/config/routes.rb index 343704b..218ec8f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -212,7 +212,7 @@ get 'warning' get 'section_answers' get 'share' - get 'show_export' + get 'download' post 'duplicate' get 'export' post 'invite' diff --git a/lib/assets/javascripts/application.js b/lib/assets/javascripts/application.js index 58a9708..9006f60 100644 --- a/lib/assets/javascripts/application.js +++ b/lib/assets/javascripts/application.js @@ -1,7 +1,10 @@ +import './views/answers/status'; import './views/contacts/new'; import './views/devise/invitations/edit'; import './views/devise/passwords/edit'; import './views/devise/passwords/new'; import './views/phases/edit'; +import './views/plans/download'; import './views/plans/edit_details'; +import './views/plans/new'; import './views/plans/share'; diff --git a/lib/assets/javascripts/constants.js b/lib/assets/javascripts/constants.js index 1ac6267..25721c0 100644 --- a/lib/assets/javascripts/constants.js +++ b/lib/assets/javascripts/constants.js @@ -7,6 +7,7 @@ export const VALIDATION_MESSAGE_PASSWORD = 'The password must be between 8 and 128 characters.'; export const VALIDATION_MESSAGE_PASSWORDS_MATCH = 'The passwords must match.'; export const VALIDATION_MESSAGE_RADIO = 'Please choose one of the options.'; +export const VALIDATION_MESSAGE_SELECT = "Please select a value from the list."; export const VALIDATION_MESSAGE_TEXT = 'This field is required.'; export const SHOW_PASSWORD_MESSAGE = 'Show password'; diff --git a/lib/assets/javascripts/spec/autoCompleteSpec.js b/lib/assets/javascripts/spec/autoCompleteSpec.js new file mode 100644 index 0000000..08965ea --- /dev/null +++ b/lib/assets/javascripts/spec/autoCompleteSpec.js @@ -0,0 +1,26 @@ +import initAutoComplete from '../utils/autoComplete'; + +describe('autoComplete test suite', () => { + beforeAll(() => fixture.setBase('javascripts/spec/fixtures')); + + beforeEach(() => { + $('body').html(fixture.load('autoComplete.html')); + initAutoComplete(); + // Override the form submission, we are just going to validate the ariatisation of the form + $('form').submit((e) => { e.preventDefault(); }); + }); + + afterEach(() => { + fixture.cleanup(); + $('body').html(''); + }); + + it('shows/hides the clear button correctly', () => { + + }); + + it('Selects the correct id based on the item selected in the combobox', () => { + + }); + +}); diff --git a/lib/assets/javascripts/spec/expandCollapseAllSpec.js b/lib/assets/javascripts/spec/expandCollapseAllSpec.js new file mode 100644 index 0000000..71c129a --- /dev/null +++ b/lib/assets/javascripts/spec/expandCollapseAllSpec.js @@ -0,0 +1,48 @@ +import expandCollapseAll from '../utils/expandCollapseAll'; + +describe('expandCollapseAll test suite', () => { + beforeAll(() => fixture.setBase('javascripts/spec/fixtures')); + + beforeEach(() => { + this.form = fixture.load('accordion.html'); + expandCollapseAll({ selector: '#accordion' }); + }); + + afterEach(() => { + fixture.cleanup(); + }); + + it('should be able to expand all sections when all are either expanded or collapsed', () => { + // Collapse all of the sections + // - click on 'collapse all' should have no effect + // - click on 'expand all' should expand all sections + $('#accordion div.panel-collapse').collapse('hide'); + expect($('.in').length === 0); + $('a[data-toggle-direction="hide"]').click(); + expect($('.in').length === 0); + $('a[data-toggle-direction="show"]').click(); + expect($('.in').length === 3); + + // Expand all of the sections + // - click on 'expand all' should have no effect + // - click on 'collapse all' should collapse all sections + $('#accordion div.panel-collapse').collapse('show'); + expect($('.in').length === 3); + $('a[data-toggle-direction="show"]').click(); + expect($('.in').length === 3); + $('a[data-toggle-direction="hide"]').click(); + expect($('.in').length === 0); + }); + + it('should be able to expand all sections when some are open and some collapsed', () => { + // Expand 2 of the 3 sections - click 'collapse all' - verify that all are collapsed + $('#collapseA, #collapseC').collapse('show'); + $('a[data-toggle-direction="hide"]').click(); + expect($('.in').length === 0); + + // Expand 2 of the 3 sections - click 'expand all' - verify that all are expanded + $('#collapseA, #collapseC').collapse('show'); + $('a[data-toggle-direction="show"]').click(); + expect($('.in').length === 3); + }); +}); diff --git a/lib/assets/javascripts/spec/fixtures/accordion.html b/lib/assets/javascripts/spec/fixtures/accordion.html new file mode 100644 index 0000000..78cb5aa --- /dev/null +++ b/lib/assets/javascripts/spec/fixtures/accordion.html @@ -0,0 +1,60 @@ +
    + + + + + +
    +
    + +
    +
    +
    + This is test section A. +
    +
    + +
    + +
    +
    +
    + This is test section B. +
    +
    + +
    + +
    +
    +
    + This is test section C. +
    +
    +
    +
    \ No newline at end of file diff --git a/lib/assets/javascripts/spec/fixtures/autoComplete.html b/lib/assets/javascripts/spec/fixtures/autoComplete.html new file mode 100644 index 0000000..7c3ce2f --- /dev/null +++ b/lib/assets/javascripts/spec/fixtures/autoComplete.html @@ -0,0 +1,35 @@ +
    + +

    JQuery Accessible Auto-complete Combobox

    +
    + + + + + + + + + + + + + +
    +
    \ No newline at end of file diff --git a/lib/assets/javascripts/utils/ariatiseForm.js b/lib/assets/javascripts/utils/ariatiseForm.js index fdd417a..2db61b3 100644 --- a/lib/assets/javascripts/utils/ariatiseForm.js +++ b/lib/assets/javascripts/utils/ariatiseForm.js @@ -103,12 +103,14 @@ return validator.isValidPassword(value); case 'radio': return validator.isValidText(value); + case 'js-combobox': + return validator.isValidText(value); default: return false; } }; -const getValidationMessage = (type) => { +const getDefaultValidationMessage = (type) => { switch (type) { case 'text': return constants.VALIDATION_MESSAGE_TEXT; @@ -120,11 +122,21 @@ return constants.VALIDATION_MESSAGE_PASSWORD; case 'radio': return constants.VALIDATION_MESSAGE_RADIO; + case 'js-combobox': + return constants.VALIDATION_MESSAGE_SELECT; default: return constants.VALIDATION_MESSAGE_DEFAULT; } }; +const getValidationMessage = (el) => { + if ($(el).attr('data-validation-error')) { + return $(el).attr('data-validation-error'); + } + // Use the default validation error message if none was specified + return getDefaultValidationMessage(getValidationTypeForElement(el)); +}; + const valid = (el) => { $(el).parent().removeClass(validationStates.hasError); $(el).attr(ariaInvalid(false)); @@ -142,9 +154,8 @@ // Add validation error message sections for each validatable input element validatable.each((i, el) => { - const type = getValidationTypeForElement(el); $(el).attr(ariaDescribedBy(`help${i}`)); - $(el).after(blockHelp(`help${i}`, getValidationMessage(type))); + $(el).after(blockHelp(`help${i}`, getValidationMessage(el))); }); // Bind validations to the form's submit button @@ -160,7 +171,7 @@ valid(el); } else { anyInvalid = true; - invalid(el, getValidationMessage(type)); + invalid(el); } } }); diff --git a/lib/assets/javascripts/utils/autoComplete.js b/lib/assets/javascripts/utils/autoComplete.js new file mode 100644 index 0000000..3660dba --- /dev/null +++ b/lib/assets/javascripts/utils/autoComplete.js @@ -0,0 +1,59 @@ +import 'jquery-accessible-autocomplete-list-aria/jquery-accessible-autocomplete-list-aria'; +import debounce from '../utils/debounce'; + +/* + * Looks up the id for the text selected by the user in the jquery autocomplete combobox and + * then sets updates the hidden id field with the id value so that its available on form submit. + * The id-text mappings are stored as JSON in the corresponding hidden crosswalk field + * @param the combobox element + */ +const updateIdField = (el) => { + const crosswalk = $(`#${$(el).attr('id')}_crosswalk`); + const idField = $(el).attr('id').replace(/_name/, '_id'); + + if (crosswalk && idField) { + const json = JSON.parse(`${$(crosswalk).val().replace(/\\"/g, '"').replace(/\\'/g, '\'')}`); + const selection = json[$(el).val()]; + $(idField).val(selection === 'undefined' ? '' : selection).change(); + } +}; + +/* + * Shows/hides the combobox's clear button based on whether or not text is present + * @param the combobox id + */ +const toggleClearButton = (el) => { + const clearButton = $(el).parent().find('.combobox-clear-button'); + if ($(el).val().trim().length <= 0) { + $(clearButton).addClass('hidden'); + } else { + $(clearButton).removeClass('hidden'); + } +}; + +/* + * Wires up the jquery autocomplete combobox so that it calls the above 2 functions when the + * user changes the text values in the combobox by typing or selecting a value + */ +export default () => { + $('.js-combobox').each((idx, el) => { + const debounced = debounce((e) => { + toggleClearButton(e); + updateIdField(e); + }, 500); + + // When the value in the combobox changes update the hidden id field + $(el).on('keyup', (e) => { + debounced($(e.currentTarget)); + }); + + // Clear the text and hide the button when the user clicks the clear button + $(el).parent().find('.combobox-clear-button').on('click', () => { + $(el).val('').focus(); + debounced($(el)); + }); + + // Show/hide the clear button on page load + toggleClearButton(el); + }); +}; \ No newline at end of file diff --git a/lib/assets/javascripts/views/answers/status.js b/lib/assets/javascripts/views/answers/status.js index ae9b12d..55e3aa0 100644 --- a/lib/assets/javascripts/views/answers/status.js +++ b/lib/assets/javascripts/views/answers/status.js @@ -1,92 +1,130 @@ -$(document).ready(function(){ - /*-------------- - START Autosaving - ----------------*/ - // debounced object holds a set of debounced functions, one for each form present in the page. Note, - // each debounced function stored at funcs is created on demand, i.e. once the user changes any element of a form - var debounced = (function(){ - var funcs = {}; - return { - has: function(id){ - return funcs[id] !== undefined; - }, - get: function(id){ +import { + isObject, + isNumber, + isString } from '../../utils/isType'; +import { Tinymce } from '../../utils/tinymce'; +import debounce from '../../utils/debounce'; - return funcs[id]; - }, - set: function(id, func){ - funcs[id] = dmproadmap.utils.debounce(func); - } - } - })(); - // This function triggers a form submit, if and only if the answer has not been optimistically locked - var autoSaving=function(){ - if($(this).closest('.question-form').find('.answer-locking').children().length === 0){ - $(this).closest('form.answer').submit(); - } - }; - var listenersForEditor=function(editor){ - editor.on('change', function(){ - var notAnswered = $('#'+editor.id).closest('.question-form').find('.not-answered'); - notAnswered.hide(); - }); - editor.on('blur', function(){ - var id = $('#'+editor.id).closest('form.answer').attr('data-autosave'); - $('#'+editor.id).val(editor.getContent()); //Updates target element of tinyMCE.editor with its content - if(!debounced.has(id)){ - debounced.set(id, autoSaving); - } - debounced.get(id).apply($('#'+editor.id),[id]); - }); - editor.on('focus', function(){ - var id = $('#'+editor.id).closest('form.answer').attr('data-autosave'); - if(debounced.has(id)){ - debounced.get(id).cancel(); //Cancels the execution of its debounced function either because user transitioned from question with options - // to the comments or because textarea lost focus and gained again before the delay being met - } - }); +$(() => { + /* + * Shows the closest saving-message HTML element within a question-form + * @param { Strin } selector - A valid CSS selector to look for + * @return { jQuery } + */ + const showSavingMessage = selector => $(selector).closest('.question-form').find('.saving-message').show(); + /* + * Hides the closest not-answered HTML element within a question-form + * @param { String } selector - A valid CSS selector to look for + * @return { jQuery } + */ + const hideNotAnswered = selector => $(selector).closest('.question-form').find('.not-answered').hide(); + /* + * Retrieves the question id for the closest form-answer + * @param { String } selector - A valid CSS selector to look for + * @return { String } representing the question id for a given answer, otherwise undefined + */ + const questionId = selector => $(selector).closest('.form-answer').attr('data-autosave'); + /* + * A map of debounced functions, one for each input, textarea or select change at any + * form with class form-answer. The key represents a question id and the value holds + * the debounced function for a given input, textarea or select. Note, this map is + * populated on demand, i.e. the first time a change is made at a given input, textarea + * or select within the form, a new key-value should be created. Succesive times, the + * debounced function should be retrieved instead. + */ + const debounceMap = {}; + const autoSaving = (selector) => { + if ($(selector).closest('.question-form').find('.answer-locking').html().length === 0) { + $(selector).closest('.form-answer').trigger('submit'); } - /*-------------- - END Autosaving - ----------------*/ - // Listener for submit event triggered - $('.question-form').on('submit', 'form.answer', function(){ - var id = $(this).attr('data-autosave'); - if(debounced.has(id)){ - debounced.get(id).cancel(); //Cancels the execution of its debounced function, if not already, since submit() could have been trigerred through Save button - } - var container = $(this).closest('.question-form'); - var saving = container.find('.saving-message'); - saving.show(); + }; + // Initialises tinymce for any target element with class tinymce_answer + Tinymce.init({ selector: '.tinymce_answer' }); + // Listeners for change, blur and focus at any target element with class tinymce_answer + Tinymce.findEditorsByClassName('tinymce_answer').forEach((editor) => { + editor.on('Change', () => { + hideNotAnswered(`#${editor.id}`); }); - // Listener for changes at any element value from question-form - $('.question-form').on('change', 'form.answer fieldset input, form.answer fieldset select', function(){ - var notAnswered = $(this).closest('.question-form').find('.not-answered'); - notAnswered.hide(); + editor.on('Blur', () => { + const id = questionId(`#${editor.id}`); + $(`#${editor.id}`).val(editor.getContent()); // Updates target element of editor with its content + if (!debounceMap[id]) { + debounceMap[id] = debounce(autoSaving); + } + debounceMap[id]($(`#${editor.id}`)); }); - // Listener for changes at any element value from question-form. This triggers the debounced function - $('.question-form').on('change', 'form.answer fieldset input, form.answer fieldset select', function(){ - var id = $(this).closest('form.answer').attr('data-autosave'); - if(!debounced.has(id)){ - debounced.set(id, autoSaving); - } - debounced.get(id).apply($(this),[id]); + editor.on('Focus', () => { + const id = questionId(`#${editor.id}`); + if (debounceMap[id]) { + /* Cancels the delayed execution of autoSaving, either because user + * transitioned from an option_based question to the comment or + * because the target element triggered blur and focus before + * the delayed execution of autoSaving. + */ + debounceMap[id].cancel(); + } }); - // Init function to add listeners for every tinyMCE editor whose target element class is tinymce_answer - (function(){ - var editors = dmproadmap.utils.tinymce.findEditorsByClassName('tinymce_answer'); - editors.forEach(listenersForEditor); - // Initialises timeago for each element abbr with class timeago - $('abbr.timeago').timeago(); - })(); - (function(ctx){ - // function to add listeners for a tinyMCE editor with target element id passed - ctx.reloadEditorListeners = ctx.reloadEditorListeners || (function(id){ - var editor = dmproadmap.utils.tinymce.findEditorById(id); - if(editor){ - listenersForEditor(editor); - $('abbr.timeago').timeago(); + }); + // Listener for input or select field + $('.question-form').on('change', 'form.answer fieldset input, form.answer fieldset select', (e) => { + hideNotAnswered(e.target); + const id = questionId(e.target); + if (!debounceMap[id]) { + debounceMap[id] = debounce(autoSaving); + } + debounceMap[id]($(e.target)); + }); + // Listener for submit button + $('.form-answer').on('submit', (e) => { + e.preventDefault(); + const id = questionId(e.target); + if (debounceMap[id]) { + // Cancels the delated execution of autoSaving + // (e.g. user clicks the button before the delay is met) + debounceMap[id].cancel(); + } + showSavingMessage(e.target); + const formElements = $(e.target).closest('.form-answer').serializeArray(); + const answerId = formElements.find(el => el.name === 'answer[id]'); + if (answerId) { + // TODO centralise AJAX calls + $.ajax({ + method: 'PUT', + url: `/answers/${answerId}`, + data: formElements, + }).done((data) => { + // Validation for the data object received + if (isObject(data)) { + if (isObject(data.question)) { // Object related to question within data received + if (isNumber(data.question.id)) { + if (isString(data.question.answer_status)) { + $(`#answer-status-${data.question.id}`).html(data.question.answer_status); // TODO check partial render of this view on the server + $('abbr.timeago').timeago(); + } + if (isString(data.question.locking)) { + $(`#answer-locking-${data.question.id}`).html(data.question.locking); + } + if (isNumber(data.question.answer_lock_version)) { + $(e.target).closest('.form-answer').find('#answer_lock_version').val(data.question.answer_lock_version); + } } - }); - })(define('dmproadmap.answers.status')); -}); \ No newline at end of file + } + if (isObject(data.plan)) { // Object related to plan within data received + if (isString(data.plan.progress)) { + $('.progress').html(data.plan.progress); + } + } + if (isObject(data.section)) { // Object related to section within data received + if (isNumber(data.section.id)) { + if (isString(data.section.progress)) { + $(`.section-progress-${data.section.id}`).html(data.section.progress); + } + } + } + } + }, () => { + // TODO adequate error handling for network error + }); + } + }); +}); diff --git a/lib/assets/javascripts/views/phases/edit.js b/lib/assets/javascripts/views/phases/edit.js index ffab5ac..f771160 100644 --- a/lib/assets/javascripts/views/phases/edit.js +++ b/lib/assets/javascripts/views/phases/edit.js @@ -1,8 +1,16 @@ +import 'bootstrap-sass/assets/javascripts/bootstrap/collapse'; import expandCollapseAll from '../../utils/expandCollapseAll'; $(() => { // Attach handlers for the expand/collapse all accordions expandCollapseAll(); + $('a[data-toggle="collapse"').click((e) => { + if ($(e.target).hasClass('fa-plus')) { + $(e.target).removeClass('fa-plus').addClass('fa-minus'); + } else { + $(e.target).removeClass('fa-minus').addClass('fa-plus'); + } + }); }); /* $(document).ready(function(){ diff --git a/lib/assets/javascripts/views/plans/download.js b/lib/assets/javascripts/views/plans/download.js new file mode 100644 index 0000000..2f5af98 --- /dev/null +++ b/lib/assets/javascripts/views/plans/download.js @@ -0,0 +1,10 @@ +$(() => { + // Hide the PDF Formatting section if 'pdf' is not the desired format + $('select#format').on('change', (e) => { + if ($(e.currentTarget).val() === 'pdf') { + $('#pdf-formatting').show(); + } else { + $('#pdf-formatting').hide(); + } + }); +}); diff --git a/lib/assets/javascripts/views/plans/export_configure.js b/lib/assets/javascripts/views/plans/export_configure.js deleted file mode 100644 index 5e7bf31..0000000 --- a/lib/assets/javascripts/views/plans/export_configure.js +++ /dev/null @@ -1,131 +0,0 @@ -$(document).ready(function() { - - // Prevent the click handler from being registered multiple times. - // This is due to the buggy way this is included. - if (window['has_export_js']) - return; - - window['has_export_js'] = true; - - $.expr.filters.indeterminate = function(element) { - return $(element).prop('indeterminate'); - }; - - $("select#format").change(function(){ - if ($(this).val() == 'pdf') { - $("#pdf-format-options").show(); - $("#settings-toggle > small").show(); - } - else { - $("#pdf-format-options").hide(); - $("#settings-toggle > small").hide(); - } - }); - - $("input:checkbox, select:not(#format)").change(function(){ - $(".unsaved_changes_alert").show(); - }); - - $("select:not(#format)").change(function(){ - $(".unsaved_changes_alert").show(); - }); - - $('.check_select > legend').append(''); - - $('.resetbutton').click(function(){ - $('input:checkbox').prop('checked',true); - $("select:not(#format)").each(function(){ - $(this).val($(this).data("default")); - }); - $(".unsaved_changes_alert").hide(); - $("#settings-toggle > small").text(__('(Using template PDF formatting values)')); - }); - - $('.savebutton').click(function(){ - var custom = false; - $("select:not(#format)").each(function(){ - if ($(this).val() != $(this).data("default")) { - custom = true; - } - }); - if (custom) { - $("#settings-toggle > small").text(__('(Using custom PDF formatting values)')); - } - else { - $("#settings-toggle > small").text(__('(Using template PDF formatting values)')); - } - $(".unsaved_changes_alert").hide(); - }); - - $('.check_select').each(function() { - var container = $(this), - toggle = container.find('> legend > .toggle'), - checks = container.find('> ol > li > input[type=checkbox], li > fieldset > legend > input[type=checkbox]'); - - - function checked(toggle) { - var checks = toggle.prop('checks'), - checked = checks.filter(':checked').length, - indeterminate = checks.filter(':indeterminate').length; - - return { - 'indeterminate' : ((checked > 0 && checked < checks.length) || indeterminate > 0), - 'checked' : (checked == checks.length) - }; - } - - function toggleParent(toggle) { - var parent_toggle = toggle.prop('toggle'); - - if (parent_toggle) - parent_toggle.prop(checked(parent_toggle)); - } - - checks.prop('toggle', toggle); - toggle.prop('checks', checks); - toggle.prop('id', container.find('> legend > label').prop('for')); - toggle.prop(checked(toggle)); - toggleParent(toggle); - - checks.change(function() { - toggle.prop(checked(toggle)); - toggleParent(toggle); - }); - - toggle.change(function() { - $(".unsaved_changes_alert").show(); - checks.prop({ 'checked': toggle.is(':checked'), 'indeterminate': false}); - - checks.each(function() { - var child_checks = $(this).prop('checks'); - - if (child_checks) - child_checks.prop({ 'checked': toggle.is(':checked'), 'indeterminate': toggle.is(':indeterminate') }); - - }); - }); - }); - /*---------------- - Listener for select that displays the formatting options (e.g. csv, html, pdf, txt, etc.) - ------------------*/ - $('.export-format-selection').click(function(e){ - e.preventDefault(); - if($(this).val() === 'pdf'){ - $('#pdf-format-options').show(); - }else{ - $('#pdf-format-options').hide(); - } - }); - - /*---------------- - Listener for select that disables the unanswered questions - ------------------*/ - $('.question-headings').click(function(e){ - if($(this).is(':checked')){ - $('.unanswered-questions').removeAttr("disabled"); - }else{ - $('.unanswered-questions').prop("checked", false); - $('.unanswered-questions').prop("disabled", true); - } - }); -}); \ No newline at end of file diff --git a/lib/assets/javascripts/views/plans/new.js b/lib/assets/javascripts/views/plans/new.js index 05ea6c6..dd7c547 100644 --- a/lib/assets/javascripts/views/plans/new.js +++ b/lib/assets/javascripts/views/plans/new.js @@ -1,3 +1,12 @@ +import ariatiseForm from '../../utils/ariatiseForm'; +import initAutoComplete from '../../utils/autoComplete'; + +$().ready(() => { + initAutoComplete(); + ariatiseForm({ selector: '#create-plan' }); +}); + +/* $(document).ready(function(){ $("#available-templates").hide(); @@ -73,3 +82,4 @@ $("#plan_" + name + "_name").siblings(".combobox-clear-button").hide(); } } +*/ diff --git a/lib/assets/javascripts/views/plans/share.js b/lib/assets/javascripts/views/plans/share.js index 7b54f68..e85491e 100644 --- a/lib/assets/javascripts/views/plans/share.js +++ b/lib/assets/javascripts/views/plans/share.js @@ -1,6 +1,6 @@ import ariatiseForm from '../../utils/ariatiseForm'; -$().ready(() => { +$(() => { // Invite Collaborators form on the Share page ariatiseForm({ selector: '#new_role' }); }); diff --git a/lib/assets/package-lock.json b/lib/assets/package-lock.json index 10cb86b..d43dad4 100644 --- a/lib/assets/package-lock.json +++ b/lib/assets/package-lock.json @@ -4652,9 +4652,7 @@ "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=" }, "jquery-accessible-autocomplete-list-aria": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/jquery-accessible-autocomplete-list-aria/-/jquery-accessible-autocomplete-list-aria-1.5.5.tgz", - "integrity": "sha1-2EG0wfBwSQaGcri1FsIkhUaPi4s=", + "version": "github:nico3333fr/jquery-accessible-autocomplete-list-aria#38a057140ccafa9a1c8a948b1bbf4a410c0181c6", "requires": { "jquery": "3.2.1" } diff --git a/lib/assets/stylesheets/application.scss b/lib/assets/stylesheets/application.scss index 420a724..1df72cd 100644 --- a/lib/assets/stylesheets/application.scss +++ b/lib/assets/stylesheets/application.scss @@ -19,3 +19,7 @@ @import "dmproadmap/tables"; @import "dmproadmap/forms"; */ + +[class^="bg-"] { + padding: 15px; +} diff --git a/lib/assets/webpack.config.js b/lib/assets/webpack.config.js index 71f1ec7..6e20570 100644 --- a/lib/assets/webpack.config.js +++ b/lib/assets/webpack.config.js @@ -15,7 +15,7 @@ context: __dirname, entry: { - vendor: ['jquery'], + vendor: ['jquery', 'timeago/jquery.timeago'], application: ['./javascripts/application.js', './stylesheets/application.scss'], }, diff --git a/test/functional/answers_controller_test.rb b/test/functional/answers_controller_test.rb index 6ed2db9..372ed47 100644 --- a/test/functional/answers_controller_test.rb +++ b/test/functional/answers_controller_test.rb @@ -69,11 +69,9 @@ private def put_answer(answer, attributes, referrer) - put answer_path(FastGettext.locale, answer, format: "js"), attributes, {'HTTP_REFERER': referrer} + put answer_path(FastGettext.locale, answer, format: "json"), attributes, {'HTTP_REFERER': referrer} assert_response :success - assert_equal "text/javascript", @response.content_type - -# assert_match(/[^\$]*\$\("#answer-locking-[0-9]+"\).html\(""\);[^\$]*\$\("#answer-form-[0-9]+"\)[^\.]*.html\(".+"\);[^\$]*\$\("#answer-status-[0-9]+"\)[^.]*.html\(".+"\);[^\$]*\$.[^$]*\$.[^\$]*\$\(".progress"\).html\(".+"\);[^\$]*\$\("#section-progress-[0-9]+"\)[^.]*.html\(".+"\);/, @response.body) + assert_equal "application/json", @response.content_type end end diff --git a/test/functional/plans_controller_test.rb b/test/functional/plans_controller_test.rb index 6de8db2..a17531e 100644 --- a/test/functional/plans_controller_test.rb +++ b/test/functional/plans_controller_test.rb @@ -290,14 +290,14 @@ # TODO: We need some better tests here to check the different formats! end - # GET /plans/:id/show_export (show_export_plan_path) + # GET /plans/:id/download (download_plan_path) # ---------------------------------------------------------- - test "show the export the plan page" do + test "show the download plan page" do # Should redirect user to the root path if they are not logged in! - try_no_user_and_unauthorized(show_export_plan_path(@plan)) + try_no_user_and_unauthorized(download_plan_path(@plan)) sign_in @user - get show_export_plan_path(@plan) + get download_plan_path(@plan) assert_response :success assert assigns(:plan) end diff --git a/test/integration/answer_locking_test.rb b/test/integration/answer_locking_test.rb index 0c0bb0f..6dfe674 100644 --- a/test/integration/answer_locking_test.rb +++ b/test/integration/answer_locking_test.rb @@ -26,9 +26,9 @@ # Signin as UserA and insert the new answer sign_in @plan.owner - put answer_path(FastGettext.locale, userA, format: "js"), obj_to_params(userA.attributes) + put answer_path(FastGettext.locale, userA, format: "json"), obj_to_params(userA.attributes) assert_response :success - assert_equal "text/javascript", @response.content_type + assert_equal "application/json", @response.content_type updated = Answer.find_by(plan: @plan, question: @question) assert_equal "Initial answer - by UserA", updated.text assert_equal @plan.owner.id, updated.user_id @@ -40,9 +40,9 @@ # Signin as UserB and try to insert the new answer but fail sign_in @collaborator - put answer_path(FastGettext.locale, userB, format: "js"), obj_to_params(userB.attributes) + put answer_path(FastGettext.locale, userB, format: "json"), obj_to_params(userB.attributes) assert_response :success - assert_equal "text/javascript", @response.content_type + assert_equal "application/json", @response.content_type updated = Answer.find_by(plan: @plan, question: @question) assert_equal "Initial answer - by UserA", updated.text assert_equal @plan.owner.id, updated.user_id @@ -63,9 +63,9 @@ sign_in @plan.owner userA['text'] += " - Updated by userA" - put answer_path(FastGettext.locale, userA['id'], format: "js"), obj_to_params(userA) + put answer_path(FastGettext.locale, userA['id'], format: "json"), obj_to_params(userA) assert_response :success - assert_equal "text/javascript", @response.content_type + assert_equal "application/json", @response.content_type updated = Answer.find_by(plan: @plan, question: @question) assert_equal "Initial answer - by UserA - Updated by userA", updated.text assert_equal @plan.owner.id, updated.user_id @@ -79,9 +79,9 @@ sign_in @collaborator userB['text'] += " - Updated by userB" - put answer_path(FastGettext.locale, userB['id'], format: "js"), obj_to_params(userB) + put answer_path(FastGettext.locale, userB['id'], format: "json"), obj_to_params(userB) assert_response :success - assert_equal "text/javascript", @response.content_type + assert_equal "application/json", @response.content_type updated = Answer.find_by(plan: @plan, question: @question) assert_equal "Initial answer - by UserA - Updated by userA", updated.text assert_equal @plan.owner.id, updated.user_id