From 32596d738c9352fa90ff2615bfda19ebf0637322 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 19 Dec 2024 17:17:34 +0200 Subject: [PATCH] optional invite --- .rubocop.yml | 2 +- app/controllers/api/templates_controller.rb | 2 +- .../submit_form_invite_controller.rb | 10 ++-- app/controllers/templates_controller.rb | 2 +- .../templates_recipients_controller.rb | 53 +++++++++++-------- app/javascript/application.js | 2 + .../elements/indeterminate_checkbox.js | 35 ++++++++++++ app/javascript/form.js | 1 + app/javascript/submission_form/form.vue | 10 +++- .../submission_form/invite_form.vue | 13 +++-- app/views/submissions/_detailed_form.html.erb | 2 +- app/views/submissions/_email_form.html.erb | 2 +- app/views/submissions/_phone_form.html.erb | 2 +- .../submit_form/_submission_form.html.erb | 3 +- app/views/templates_preferences/show.html.erb | 14 ++--- lib/submissions/create_from_submitters.rb | 4 +- lib/templates.rb | 4 +- 17 files changed, 112 insertions(+), 49 deletions(-) create mode 100644 app/javascript/elements/indeterminate_checkbox.js diff --git a/.rubocop.yml b/.rubocop.yml index 499a63e6..a679c86a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -69,7 +69,7 @@ RSpec/MultipleMemoizedHelpers: Max: 9 Metrics/BlockNesting: - Max: 4 + Max: 5 Rails/I18nLocaleTexts: Enabled: false diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index 94d2c8f3..a695e728 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -100,7 +100,7 @@ module Api :name, :external_id, { - submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email]], + submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index 2d32425d..1d42779c 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -9,13 +9,15 @@ class SubmitFormInviteController < ApplicationController return head :unprocessable_entity unless can_invite?(submitter) - invite_submitters = filter_invite_submitters(submitter) + invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid') + optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid') ApplicationRecord.transaction do - invite_submitters.each do |item| + (invite_submitters + optional_invite_submitters).each do |item| attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] } next unless attrs + next if attrs[:email].blank? submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id) @@ -46,9 +48,9 @@ class SubmitFormInviteController < ApplicationController !submitter.submission.template.archived_at? end - def filter_invite_submitters(submitter) + def filter_invite_submitters(submitter, key = 'invite_by_uuid') (submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s| - s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } + s[key] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } end end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index c023a5e7..3010d4ab 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -111,7 +111,7 @@ class TemplatesController < ApplicationController params.require(:template).permit( :name, { schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]], - submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid email]], + submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, diff --git a/app/controllers/templates_recipients_controller.rb b/app/controllers/templates_recipients_controller.rb index 745fee93..a1298094 100644 --- a/app/controllers/templates_recipients_controller.rb +++ b/app/controllers/templates_recipients_controller.rb @@ -17,38 +17,47 @@ class TemplatesRecipientsController < ApplicationController private def submitters_params - params.require(:template).permit( - submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email option]] - ).fetch(:submitters, {}).values.filter_map do |s| + permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid + invite_by_uuid linked_to_uuid email option]] } + + params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s| next if s[:uuid].blank? - if s[:is_requester] == '1' && s[:invite_by_uuid].blank? + if s[:is_requester] == '1' && s[:invite_by_uuid].blank? && s[:optional_invite_by_uuid].blank? s[:is_requester] = true else s.delete(:is_requester) end s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank? + s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank? - option = s.delete(:option) - - if option.present? - case option - when 'is_requester' - s[:is_requester] = true - when 'not_set' - s.delete(:is_requester) - s.delete(:email) - s.delete(:linked_to_uuid) - s.delete(:invite_by_uuid) - when /\Alinked_to_(.*)\z/ - s[:linked_to_uuid] = ::Regexp.last_match(-1) - when /\Ainvite_by_(.*)\z/ - s[:invite_by_uuid] = ::Regexp.last_match(-1) - end - end + normalize_option_value(s) + end + end - s + def normalize_option_value(attrs) + option = attrs.delete(:option) + + if option.present? + case option + when 'is_requester' + attrs[:is_requester] = true + when 'not_set' + attrs.delete(:is_requester) + attrs.delete(:email) + attrs.delete(:linked_to_uuid) + attrs.delete(:invite_by_uuid) + attrs.delete(:optional_invite_by_uuid) + when /\Alinked_to_(.*)\z/ + attrs[:linked_to_uuid] = ::Regexp.last_match(-1) + when /\Aoptional_invite_by_(.*)\z/ + attrs[:optional_invite_by_uuid] = ::Regexp.last_match(-1) + when /\Ainvite_by_(.*)\z/ + attrs[:invite_by_uuid] = ::Regexp.last_match(-1) + end end + + attrs end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 2239d638..d35641ad 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -31,6 +31,7 @@ import LinkedInput from './elements/linked_input' import CheckboxGroup from './elements/checkbox_group' import MaskedInput from './elements/masked_input' import SetDateButton from './elements/set_date_button' +import IndeterminateCheckbox from './elements/indeterminate_checkbox' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -99,6 +100,7 @@ safeRegisterElement('linked-input', LinkedInput) safeRegisterElement('checkbox-group', CheckboxGroup) safeRegisterElement('masked-input', MaskedInput) safeRegisterElement('set-date-button', SetDateButton) +safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { diff --git a/app/javascript/elements/indeterminate_checkbox.js b/app/javascript/elements/indeterminate_checkbox.js new file mode 100644 index 00000000..24648943 --- /dev/null +++ b/app/javascript/elements/indeterminate_checkbox.js @@ -0,0 +1,35 @@ +export default class extends HTMLElement { + connectedCallback () { + if (this.dataset.indeterminate === 'true') { + this.checkbox.indeterminate = true + this.checkbox.readOnly = true + } + + this.checkbox.addEventListener('click', () => { + this.checkbox.setAttribute('name', this.dataset.name) + + if (this.showIndeterminateEl) { + this.showIndeterminateEl.classList.add('hidden') + } + + if (this.checkbox.readOnly) { + this.checkbox.checked = this.checkbox.readOnly = false + } else if (!this.checkbox.checked) { + if (this.showIndeterminateEl) { + this.showIndeterminateEl.classList.remove('hidden') + } + + this.checkbox.setAttribute('name', this.dataset.indeterminateName) + this.checkbox.checked = this.checkbox.readOnly = this.checkbox.indeterminate = true + } + }) + } + + get checkbox () { + return this.querySelector('input[type="checkbox"]') + } + + get showIndeterminateEl () { + return document.getElementById(this.dataset.showIndeterminateId) + } +} diff --git a/app/javascript/form.js b/app/javascript/form.js index f9583da6..2e07f033 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -15,6 +15,7 @@ safeRegisterElement('submission-form', class extends HTMLElement { this.app = createApp(Form, { submitter: JSON.parse(this.dataset.submitter), inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters), + optionalInviteSubmitters: JSON.parse(this.dataset.optionalInviteSubmitters), schema: JSON.parse(this.dataset.schema), canSendEmail: this.dataset.canSendEmail === 'true', previousSignatureValue: this.dataset.previousSignatureValue, diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index f8c4031d..4ac1b56e 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -498,6 +498,7 @@ [] }, + optionalInviteSubmitters: { + type: Array, + required: false, + default: () => [] + }, withSignatureId: { type: Boolean, required: false, @@ -1320,7 +1326,7 @@ export default { const formData = new FormData(this.$refs.form) const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete - if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) { + if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) { formData.append('completed', 'true') } @@ -1359,7 +1365,7 @@ export default { if (emptyRequiredField === nextStep) { this.showFillAllRequiredFields = true } - } else if (this.inviteSubmitters.length) { + } else if (this.inviteSubmitters.length || this.optionalInviteSubmitters.length) { this.isInvite = true } else { this.performComplete(response) diff --git a/app/javascript/submission_form/invite_form.vue b/app/javascript/submission_form/invite_form.vue index c32cbd53..eefe7661 100644 --- a/app/javascript/submission_form/invite_form.vue +++ b/app/javascript/submission_form/invite_form.vue @@ -12,7 +12,7 @@ :value="authenticityToken" >
@@ -26,7 +26,7 @@ dir="auto" class="label text-2xl" > - {{ t('invite') }} {{ submitter.name }} + {{ t('invite') }} {{ submitter.name }} @@ -53,7 +53,7 @@ class="mr-1 animate-spin" /> - {{ t('submit') }} + {{ t('complete') }} [] + }, url: { type: String, required: true diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb index e841a815..23a7ca28 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -1,5 +1,5 @@ <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> - <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? } %> + <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %>
diff --git a/app/views/submissions/_email_form.html.erb b/app/views/submissions/_email_form.html.erb index f9534c15..e2e90aa6 100644 --- a/app/views/submissions/_email_form.html.erb +++ b/app/views/submissions/_email_form.html.erb @@ -1,5 +1,5 @@ <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> - <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? } %> + <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %> <% if submitters.size == 1 %> diff --git a/app/views/submissions/_phone_form.html.erb b/app/views/submissions/_phone_form.html.erb index 68bcb8da..1034eb27 100644 --- a/app/views/submissions/_phone_form.html.erb +++ b/app/views/submissions/_phone_form.html.erb @@ -1,5 +1,5 @@ <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> - <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? } %> + <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %>
diff --git a/app/views/submit_form/_submission_form.html.erb b/app/views/submit_form/_submission_form.html.erb index 37b8eacc..9ddb02c6 100644 --- a/app/views/submit_form/_submission_form.html.erb +++ b/app/views/submit_form/_submission_form.html.erb @@ -1,4 +1,5 @@ <% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid created_at], methods: %i[url filename content_type]) %> <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> <% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %> - +<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %> + diff --git a/app/views/templates_preferences/show.html.erb b/app/views/templates_preferences/show.html.erb index 1059bcca..e5e7ca2c 100644 --- a/app/views/templates_preferences/show.html.erb +++ b/app/views/templates_preferences/show.html.erb @@ -223,16 +223,16 @@
<% @template.submitters.each_with_index do |submitter, index| %>
- <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'linked_to_uuid')), index: do |ff| %> - <% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : ''))) %> + <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %> + <% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %> <%= ff.hidden_field :uuid %>
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %> <% if @template.submitters.size == 2 %> - <%= ff.email_field :email, class: 'base-input', autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %> + <%= ff.email_field :email, class: 'base-input', autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %> <% else %> - <%= ff.select :option, [[t('not_specified'), 'not_set'], [t('submission_requester'), 'is_requester'], [t('specified_email'), 'email'], *(@template.submitters - [submitter]).map { |e| [t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"] }, *(@template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }], {}, class: 'base-select mb-3' %> + <%= ff.select :option, [[t('not_specified'), 'not_set'], [t('submission_requester'), 'is_requester'], [t('specified_email'), 'email'], *(@template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(@template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }], {}, class: 'base-select mb-3' %> <%= ff.email_field :email, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %> <% end %> @@ -250,10 +250,12 @@ <% if index == 1 %> <% end %> diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index ea4c5fe7..30f3addf 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -55,8 +55,8 @@ module Submissions def maybe_add_invite_submitters(submission, template) template.submitters.each do |item| - next if item['invite_by_uuid'].blank? || - submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] } + next if item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank? + next if submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] } submission.template_submitters << item end diff --git a/lib/templates.rb b/lib/templates.rb index 54ecd842..48407d51 100644 --- a/lib/templates.rb +++ b/lib/templates.rb @@ -26,8 +26,8 @@ module Templates def filter_undefined_submitters(template) template.submitters.to_a.select do |item| - item['invite_by_uuid'].blank? && item['linked_to_uuid'].blank? && - item['is_requester'].blank? && item['email'].blank? + item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank? && + item['linked_to_uuid'].blank? && item['is_requester'].blank? && item['email'].blank? end end end