From 11b9876d4f114b2ae77bf055775a90b8201cbd35 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 11 Feb 2026 08:09:00 -0500 Subject: [PATCH 1/3] Removed unused controller_issues_new_before_save that was used for finding the 422 error --- lib/redmine_qbo/hooks/issues_hook_listener.rb | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/lib/redmine_qbo/hooks/issues_hook_listener.rb b/lib/redmine_qbo/hooks/issues_hook_listener.rb index 321bf0b..bac5c77 100644 --- a/lib/redmine_qbo/hooks/issues_hook_listener.rb +++ b/lib/redmine_qbo/hooks/issues_hook_listener.rb @@ -14,36 +14,6 @@ module RedmineQbo include IssuesHelper - # Check the new issue form for a valid project. - # This is added to help prevent 422 unprocessable entity errors when creating an issue - # See https://github.com/redmine/redmine/blob/84483d63828d0cb2efbf5bd786a2f0d22e34c93d/app/controllers/issues_controller.rb#L179 - def controller_issues_new_before_save(context={}) - - Rails.logger.debug "RedmineQbo::Hooks::IssuesHookListener.controller_issues_new_before_save: Checking for nil project or tracker" - Rails.logger.debug context[:params].inspect - Rails.logger.debug context[:issue].inspect - Rails.logger.debug context[:issue].project - Rails.logger.debug context[:issue].tracker - error = "" - if context[:issue].project.nil? - context[:issue].project ||= projects_for_select(context[:issue]).first - Rails.logger.error I18n.t(:notice_error_project_nil) + context[:issue].project.to_s - error = I18n.t(:notice_error_project_nil) + context[:issue].project.to_s - end - - if context[:issue].tracker.nil? - context[:issue].tracker ||= trackers_for_select(context[:issue]).first - Rails.logger.error I18n.t(:notice_error_tracker_nil) + context[:issue].tracker.to_s - error << "\n" - error << I18n.t(:notice_error_tracker_nil) + context[:issue].tracker.to_s - end - - context[:controller].flash[:error] = error unless error.blank? - Rails.logger.debug error unless error.blank? - - return context - end - # Edit Issue Form # Here we build the required form components before passing them to a partial view formatting. def view_issues_form_details_bottom(context={}) @@ -62,7 +32,6 @@ module RedmineQbo value: '#issue_customer' } - js_path = "updateIssueFrom('/issues/new.js', this)" js_path = "updateIssueFrom('#{escape_javascript update_issue_form_path(issue.project, issue)}', this)" unless issue.new_record? # This hidden field is used for the customer ID for the issue From 167385bb998f86c309ddf042765ccf7966c5c7b1 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 11 Feb 2026 19:31:01 -0500 Subject: [PATCH 2/3] override issues form to add passing @project to issues hook --- app/views/invoices/_form.html.erb | 89 +++++++++++++++++++ lib/redmine_qbo/hooks/issues_hook_listener.rb | 18 +++- 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 app/views/invoices/_form.html.erb diff --git a/app/views/invoices/_form.html.erb b/app/views/invoices/_form.html.erb new file mode 100644 index 0000000..2743ca2 --- /dev/null +++ b/app/views/invoices/_form.html.erb @@ -0,0 +1,89 @@ +<%= labelled_fields_for :issue, @issue do |f| %> +<%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %> +<%= hidden_field_tag 'form_update_triggered_by', '' %> +<%= hidden_field_tag 'back_url', params[:back_url], :id => nil if params[:back_url].present? %> + +<% if @issue.safe_attribute? 'is_private' %> +

+ <%= f.check_box :is_private, :no_label => true %> +

+<% end %> + +<% projects = projects_for_select(@issue) %> +<% if (@issue.safe_attribute?('project_id') || @issue.project_id_changed?) && (@project.nil? || projects.length > 1 || @issue.copy?) %> +

<%= f.select :project_id, project_tree_options_for_select(projects, :selected => @issue.project), {:required => true}, + :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)" %>

+<% end %> + +<% if @issue.safe_attribute?('tracker_id') || (@issue.persisted? && @issue.tracker_id_changed?) %> +

+ <%= f.select :tracker_id, trackers_options_for_select(@issue), {:required => true}, + :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)", + :title => @issue.tracker.description %> + <%= content_tag 'a', sprite_icon('help', l(:label_open_trackers_description)), :class => 'icon-only icon-help', :title => l(:label_open_trackers_description), :onclick => "showModal('trackers_description', '500px'); return false;", :href => '#' if trackers_for_select(@issue).any? {|t| t.description.present? } %> +

+ <%= render partial: 'issues/trackers_description', locals: {trackers: trackers_for_select(@issue)} %> +<% end %> + +<% if @issue.safe_attribute? 'subject' %> +

<%= f.text_field :subject, :size => 80, :maxlength => 255, :required => true %>

+<% end %> + +<% if @issue.safe_attribute? 'description' %> +

+ <%= f.label_for_field :description, :required => @issue.required_attribute?('description') %> + <%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %> + <%= f.textarea :description, :cols => 60, :accesskey => accesskey(:edit), :class => 'wiki-edit', + :rows => [[10, @issue.description.to_s.length / 50].max, 20].min, + :data => { + :auto_complete => true + }.merge(list_autofill_data_attributes), + :no_label => true %> + <% end %> + <%= link_to_function content_tag(:span, sprite_icon('edit', l(:button_edit))), '$(this).hide(); $("#issue_description_and_toolbar").show()', :class => 'icon icon-edit' unless @issue.new_record? %> +

+<%= wikitoolbar_for 'issue_description', preview_issue_path(:project_id => @issue.project, :issue_id => @issue.id) %> +<% end %> + +
+ <%= render :partial => 'issues/attributes' %> +
+ +<%= call_hook(:view_issues_form_details_bottom, { :project => @project, :issue => @issue, :form => f }) %> +<% end %> + +<% heads_for_wiki_formatter %> +<%= heads_for_auto_complete(@issue.project) %> + +<% if User.current.allowed_to?(:add_issue_watchers, @issue.project)%> + <%= update_data_sources_for_auto_complete({users: watchers_autocomplete_for_mention_path(project_id: @issue.project, q: '', object_type: 'issue', + object_id: @issue.id)}) %> +<% end %> + +<%= javascript_tag do %> +$(document).ready(function(){ + $("#issue_tracker_id, #issue_status_id").each(function(){ + $(this).val($(this).find("option[selected=selected]").val()); + }); + $(".assign-to-me-link").click(function(event){ + event.preventDefault(); + var element = $(event.target); + $('#issue_assigned_to_id').val(element.data('id')); + element.hide(); + }); + $('#issue_assigned_to_id').change(function(event){ + var assign_to_me_link = $(".assign-to-me-link"); + + if (assign_to_me_link.length > 0) { + var user_id = $(event.target).val(); + var current_user_id = assign_to_me_link.data('id'); + + if (user_id == current_user_id) { + assign_to_me_link.hide(); + } else { + assign_to_me_link.show(); + } + } + }); +}); +<% end %> \ No newline at end of file diff --git a/lib/redmine_qbo/hooks/issues_hook_listener.rb b/lib/redmine_qbo/hooks/issues_hook_listener.rb index bac5c77..65308e9 100644 --- a/lib/redmine_qbo/hooks/issues_hook_listener.rb +++ b/lib/redmine_qbo/hooks/issues_hook_listener.rb @@ -18,9 +18,11 @@ module RedmineQbo # Here we build the required form components before passing them to a partial view formatting. def view_issues_form_details_bottom(context={}) Rails.logger.debug "RedmineQbo::Hooks::IssuesHookListener.view_issues_form_details_bottom: Building form components for quickbooks customer, estimate, and invoice data" - Rails.logger.debug context[:issue].inspect f = context[:form] issue = context[:issue] + project = context[:project] + Rails.logger.debug issue.inspect + Rails.logger.debug project.inspect # Customer Name Text Box with database backed autocomplete # onchange event will update the hidden customer_id field @@ -32,8 +34,18 @@ module RedmineQbo value: '#issue_customer' } - js_path = "updateIssueFrom('/issues/new.js', this)" - js_path = "updateIssueFrom('#{escape_javascript update_issue_form_path(issue.project, issue)}', this)" unless issue.new_record? + # We need to handle 3 cases for the onchange event of the customer name field: + # 1. New issue Withough project: /issues/new.js + # 2. New issue With project: /projects/rmt/issues/new.js + # 3. Existing issue: /issues//edit.js + # The built in helper update_issue_form_path requires a project object to determine the correct path for new vs existing issues, + # but it doesn't work for issue.project when creating new issues not in a project i.e. http://redmine.domain.com/issues/new . + # So we need to figure out how to get a the @project from the controller calling the hook. + # + # If this is not handled correctly, it leads to a 422 error when creating a new issue and selecting a customer. + js_path = "updateIssueFrom('#{escape_javascript update_issue_form_path(project, issue)}', this)" + Rails.logger.debug js_path + # This hidden field is used for the customer ID for the issue # the onchange event will reload the issue form via ajax to update the available estimates customer_id = f.hidden_field :customer_id, From 331c1eabebb2cea51b26a0467067bcdd2a7e7cca Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 11 Feb 2026 19:59:41 -0500 Subject: [PATCH 3/3] seems to work without overiding the main issues _form --- app/views/invoices/_form.html.erb | 89 ------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 app/views/invoices/_form.html.erb diff --git a/app/views/invoices/_form.html.erb b/app/views/invoices/_form.html.erb deleted file mode 100644 index 2743ca2..0000000 --- a/app/views/invoices/_form.html.erb +++ /dev/null @@ -1,89 +0,0 @@ -<%= labelled_fields_for :issue, @issue do |f| %> -<%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %> -<%= hidden_field_tag 'form_update_triggered_by', '' %> -<%= hidden_field_tag 'back_url', params[:back_url], :id => nil if params[:back_url].present? %> - -<% if @issue.safe_attribute? 'is_private' %> -

- <%= f.check_box :is_private, :no_label => true %> -

-<% end %> - -<% projects = projects_for_select(@issue) %> -<% if (@issue.safe_attribute?('project_id') || @issue.project_id_changed?) && (@project.nil? || projects.length > 1 || @issue.copy?) %> -

<%= f.select :project_id, project_tree_options_for_select(projects, :selected => @issue.project), {:required => true}, - :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)" %>

-<% end %> - -<% if @issue.safe_attribute?('tracker_id') || (@issue.persisted? && @issue.tracker_id_changed?) %> -

- <%= f.select :tracker_id, trackers_options_for_select(@issue), {:required => true}, - :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)", - :title => @issue.tracker.description %> - <%= content_tag 'a', sprite_icon('help', l(:label_open_trackers_description)), :class => 'icon-only icon-help', :title => l(:label_open_trackers_description), :onclick => "showModal('trackers_description', '500px'); return false;", :href => '#' if trackers_for_select(@issue).any? {|t| t.description.present? } %> -

- <%= render partial: 'issues/trackers_description', locals: {trackers: trackers_for_select(@issue)} %> -<% end %> - -<% if @issue.safe_attribute? 'subject' %> -

<%= f.text_field :subject, :size => 80, :maxlength => 255, :required => true %>

-<% end %> - -<% if @issue.safe_attribute? 'description' %> -

- <%= f.label_for_field :description, :required => @issue.required_attribute?('description') %> - <%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %> - <%= f.textarea :description, :cols => 60, :accesskey => accesskey(:edit), :class => 'wiki-edit', - :rows => [[10, @issue.description.to_s.length / 50].max, 20].min, - :data => { - :auto_complete => true - }.merge(list_autofill_data_attributes), - :no_label => true %> - <% end %> - <%= link_to_function content_tag(:span, sprite_icon('edit', l(:button_edit))), '$(this).hide(); $("#issue_description_and_toolbar").show()', :class => 'icon icon-edit' unless @issue.new_record? %> -

-<%= wikitoolbar_for 'issue_description', preview_issue_path(:project_id => @issue.project, :issue_id => @issue.id) %> -<% end %> - -
- <%= render :partial => 'issues/attributes' %> -
- -<%= call_hook(:view_issues_form_details_bottom, { :project => @project, :issue => @issue, :form => f }) %> -<% end %> - -<% heads_for_wiki_formatter %> -<%= heads_for_auto_complete(@issue.project) %> - -<% if User.current.allowed_to?(:add_issue_watchers, @issue.project)%> - <%= update_data_sources_for_auto_complete({users: watchers_autocomplete_for_mention_path(project_id: @issue.project, q: '', object_type: 'issue', - object_id: @issue.id)}) %> -<% end %> - -<%= javascript_tag do %> -$(document).ready(function(){ - $("#issue_tracker_id, #issue_status_id").each(function(){ - $(this).val($(this).find("option[selected=selected]").val()); - }); - $(".assign-to-me-link").click(function(event){ - event.preventDefault(); - var element = $(event.target); - $('#issue_assigned_to_id').val(element.data('id')); - element.hide(); - }); - $('#issue_assigned_to_id').change(function(event){ - var assign_to_me_link = $(".assign-to-me-link"); - - if (assign_to_me_link.length > 0) { - var user_id = $(event.target).val(); - var current_user_id = assign_to_me_link.data('id'); - - if (user_id == current_user_id) { - assign_to_me_link.hide(); - } else { - assign_to_me_link.show(); - } - } - }); -}); -<% end %> \ No newline at end of file