diff --git a/.rubocop.yml b/.rubocop.yml index cf883596..165bc73a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -68,6 +68,9 @@ RSpec/ExampleLength: RSpec/MultipleMemoizedHelpers: Max: 6 +Metrics/BlockNesting: + Max: 4 + Rails/I18nLocaleTexts: Enabled: false diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index d24c58cc..d05b31fc 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -97,7 +97,7 @@ module Api :name, :external_id, { - submitters: [%i[name uuid is_requester linked_to_uuid email]], + submitters: [%i[name uuid is_requester invite_by_uuid invite_by_uuid linked_to_uuid email]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index a1f3e91d..5d969a65 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -23,7 +23,7 @@ class StartFormController < ApplicationController if @submitter.completed_at? redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) else - if filter_undefined_submitters(@template).size > 2 && @submitter.new_record? + if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? @error_message = 'Not found' return render :show @@ -95,9 +95,7 @@ class StartFormController < ApplicationController end def filter_undefined_submitters(template) - template.submitters.select do |item| - item['linked_to_uuid'].blank? && item['is_requester'].blank? && item['email'].blank? - end + Templates.filter_undefined_submitters(template) end def submitter_params diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb new file mode 100644 index 00000000..2d32425d --- /dev/null +++ b/app/controllers/submit_form_invite_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class SubmitFormInviteController < ApplicationController + skip_before_action :authenticate_user! + skip_authorization_check + + def create + submitter = Submitter.find_by!(slug: params[:submit_form_slug]) + + return head :unprocessable_entity unless can_invite?(submitter) + + invite_submitters = filter_invite_submitters(submitter) + + ApplicationRecord.transaction do + invite_submitters.each do |item| + attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] } + + next unless attrs + + submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id) + + SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid }) + end + + submitter.submission.update!(submitters_order: :preserved) + end + + submitter.submission.submitters.reload + + if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } } + Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request) + + head :ok + else + head :unprocessable_entity + end + end + + private + + def can_invite?(submitter) + !submitter.declined_at? && + !submitter.completed_at? && + !submitter.submission.archived_at? && + !submitter.submission.expired? && + !submitter.submission.template.archived_at? + end + + def filter_invite_submitters(submitter) + (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'] } + end + end + + def submitters_attributes + params.require(:submission).permit(submitters: [%i[uuid email]]).fetch(:submitters, []) + end +end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 4118c48d..d998111d 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -103,7 +103,7 @@ class TemplatesController < ApplicationController params.require(:template).permit( :name, { schema: [%i[attachment_uuid name]], - submitters: [%i[name uuid is_requester linked_to_uuid email]], + submitters: [%i[name uuid is_requester linked_to_uuid 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 b07491be..745fee93 100644 --- a/app/controllers/templates_recipients_controller.rb +++ b/app/controllers/templates_recipients_controller.rb @@ -18,16 +18,18 @@ class TemplatesRecipientsController < ApplicationController def submitters_params params.require(:template).permit( - submitters: [%i[name uuid is_requester linked_to_uuid email option]] + submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email option]] ).fetch(:submitters, {}).values.filter_map do |s| next if s[:uuid].blank? - if s[:is_requester] == '1' + if s[:is_requester] == '1' && s[: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? + option = s.delete(:option) if option.present? @@ -38,8 +40,11 @@ class TemplatesRecipientsController < ApplicationController 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 diff --git a/app/javascript/application.js b/app/javascript/application.js index 090fef07..bdc4d93d 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -28,6 +28,7 @@ import PasswordInput from './elements/password_input' import SearchInput from './elements/search_input' import ToggleAttribute from './elements/toggle_attribute' import LinkedInput from './elements/linked_input' +import CheckboxGroup from './elements/checkbox_group' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -93,6 +94,7 @@ safeRegisterElement('password-input', PasswordInput) safeRegisterElement('search-input', SearchInput) safeRegisterElement('toggle-attribute', ToggleAttribute) safeRegisterElement('linked-input', LinkedInput) +safeRegisterElement('checkbox-group', CheckboxGroup) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { diff --git a/app/javascript/elements/checkbox_group.js b/app/javascript/elements/checkbox_group.js new file mode 100644 index 00000000..efa9bb78 --- /dev/null +++ b/app/javascript/elements/checkbox_group.js @@ -0,0 +1,15 @@ +export default class extends HTMLElement { + connectedCallback () { + this.items.forEach((item) => { + item.addEventListener('change', (e) => { + this.items.forEach((item) => { + item.checked = item === e.target && e.target.checked + }) + }) + }) + } + + get items () { + return this.querySelectorAll('input[type="checkbox"]') + } +} diff --git a/app/javascript/form.js b/app/javascript/form.js index 3ee6f373..eca075ab 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -14,6 +14,7 @@ safeRegisterElement('submission-form', class extends HTMLElement { this.app = createApp(Form, { submitter: JSON.parse(this.dataset.submitter), + inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters), canSendEmail: this.dataset.canSendEmail === 'true', previousSignatureValue: this.dataset.previousSignatureValue, goToLast: this.dataset.goToLast === 'true', diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index f8ca5c4e..4b7e6a20 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -436,6 +436,15 @@ + [] + }, withSignatureId: { type: Boolean, required: false, @@ -739,6 +755,7 @@ export default { data () { return { isCompleted: false, + isInvited: false, isFormVisible: this.expand !== false, showFillAllRequiredFields: false, currentStep: 0, @@ -1119,7 +1136,7 @@ export default { const formData = new FormData(this.$refs.form) const isLastStep = this.currentStep === this.stepFields.length - 1 - if (isLastStep && !emptyRequiredField) { + if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) { formData.append('completed', 'true') } diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js index d5c65791..25411d6b 100644 --- a/app/javascript/submission_form/i18n.js +++ b/app/javascript/submission_form/i18n.js @@ -10,6 +10,8 @@ const en = { reviewed: 'Reviewed', other: 'Other', authored_by_me: 'Authored by me', + invite: 'Invite', + email: 'Email', approved_by: 'Approved by', reviewed_by: 'Reviewed by', authored_by: 'Authored by', @@ -85,6 +87,8 @@ const en = { } const es = { + invite: 'Invitar', + email: 'Correo electrónico', approved: 'Aprobado', reviewed: 'Revisado', other: 'Otro', @@ -170,6 +174,8 @@ const es = { } const it = { + invite: 'Invita', + email: 'Email', approved: 'Approvato', reviewed: 'Revisionato', other: 'Altro', @@ -255,6 +261,8 @@ const it = { } const de = { + invite: 'Einladen', + email: 'E-Mail', approved: 'Genehmigt', reviewed: 'Überprüft', other: 'Andere', @@ -340,6 +348,8 @@ const de = { } const fr = { + invite: 'Inviter', + email: 'Courriel', approved: 'Approuvé', reviewed: 'Révisé', other: 'Autre', @@ -425,6 +435,8 @@ const fr = { } const pl = { + invite: 'Zaproś', + email: 'E-mail', approved: 'Zaakceptowany', reviewed: 'Przejrzany', other: 'Inny', @@ -510,6 +522,8 @@ const pl = { } const uk = { + invite: 'Запросити', + email: 'Електронна пошта', approved: 'Затверджено', reviewed: 'Переглянуто', other: 'Інше', @@ -595,6 +609,8 @@ const uk = { } const cs = { + invite: 'Pozvat', + email: 'E-mail', approved: 'Schváleno', reviewed: 'Zkontrolováno', other: 'Jiné', @@ -680,6 +696,8 @@ const cs = { } const pt = { + invite: 'Convidar', + email: 'E-mail', approved: 'Aprovado', reviewed: 'Revisado', other: 'Outro', @@ -765,6 +783,8 @@ const pt = { } const he = { + invite: 'הזמן', + email: 'דוא"ל', approved: 'מאושר', reviewed: 'נסקר', other: 'אחר', @@ -851,6 +871,8 @@ const he = { } const nl = { + invite: 'Uitnodigen', + email: 'E-mail', approved: 'Goedgekeurd', reviewed: 'Beoordeeld', other: 'Anders', @@ -937,6 +959,8 @@ const nl = { } const ar = { + invite: 'دعوة', + email: 'البريد الإلكتروني', approved: 'موافق عليه', reviewed: 'تمت مراجعته', other: 'آخر', @@ -1022,6 +1046,8 @@ const ar = { } const ko = { + invite: '초대하기', + email: '이메일', approved: '승인됨', reviewed: '검토됨', other: '기타', diff --git a/app/javascript/submission_form/invite_form.vue b/app/javascript/submission_form/invite_form.vue new file mode 100644 index 00000000..c32cbd53 --- /dev/null +++ b/app/javascript/submission_form/invite_form.vue @@ -0,0 +1,117 @@ + + + diff --git a/app/models/submission_event.rb b/app/models/submission_event.rb index 8cfa11c1..1a4777b7 100644 --- a/app/models/submission_event.rb +++ b/app/models/submission_event.rb @@ -47,6 +47,7 @@ class SubmissionEvent < ApplicationRecord phone_verified: 'phone_verified', start_form: 'start_form', view_form: 'view_form', + invite_party: 'invite_party', complete_form: 'complete_form', decline_form: 'decline_form', api_complete_form: 'api_complete_form' diff --git a/app/views/start_form/completed.html.erb b/app/views/start_form/completed.html.erb index 36110eb2..e3727074 100644 --- a/app/views/start_form/completed.html.erb +++ b/app/views/start_form/completed.html.erb @@ -24,7 +24,7 @@ <%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, class: 'base-button w-full' %> <% end %> - <% if @template.submitters.to_a.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %> + <% if Templates.filter_undefined_submitters(@template).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %> diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb index 597dfb70..a4b4b26f 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -1,17 +1,18 @@ <%= 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? } %>
-
- <% template.submitters.each_with_index do |item, index| %> +
+ <% submitters.each_with_index do |item, index| %> - <% if template.submitters.size > 1 %> + <% if submitters.size > 1 %> @@ -22,7 +23,7 @@ <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: (params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : '', dir: 'auto', id: "detailed_name_#{item['uuid']}" %> -
+
"> diff --git a/app/views/submissions/_email_form.html.erb b/app/views/submissions/_email_form.html.erb index 193e21dd..cd58298c 100644 --- a/app/views/submissions/_email_form.html.erb +++ b/app/views/submissions/_email_form.html.erb @@ -1,5 +1,6 @@ <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> - <% if template.submitters.size == 1 %> + <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? } %> + <% if submitters.size == 1 %> @@ -20,7 +21,7 @@
- <% template.submitters.each_with_index do |item, index| %> + <% submitters.each_with_index do |item, index| %>