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/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 88601355..908a04b4 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -80,7 +80,7 @@ module Api end render json: build_create_json(submissions) - rescue Submitters::NormalizeValues::BaseError => e + rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e Rollbar.warning(e) if defined?(Rollbar) render json: { error: e.message }, status: :unprocessable_entity @@ -158,7 +158,7 @@ module Api :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], fields: [:name, :uuid, :default_value, :value, :title, :description, - :readonly, :redacted, :validation_pattern, :invalid_message, + :readonly, :validation_pattern, :invalid_message, { default_value: [], value: [], preferences: {} }] }]] } ] diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index 4a73a96d..e9fc496b 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -77,7 +77,7 @@ module Api with_urls: true, with_events: false, params:) - rescue Submitters::NormalizeValues::BaseError => e + rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e Rollbar.warning(e) if defined?(Rollbar) render json: { error: e.message }, status: :unprocessable_entity @@ -91,7 +91,7 @@ module Api :completed, :phone, :application_key, :external_id, :go_to_last, { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], fields: [[:name, :uuid, :default_value, :value, - :readonly, :redacted, :validation_pattern, :invalid_message, + :readonly, :validation_pattern, :invalid_message, { default_value: [], value: [], preferences: {} }]] } ) end diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index 7341b916..14d45862 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -18,7 +18,7 @@ module Api preview_image_attachments = ActiveStorage::Attachment.joins(:blob) - .where(blob: { filename: '0.jpg' }) + .where(blob: { filename: ['0.png', '0.jpg'] }) .where(record_id: schema_documents.map(&:id), record_type: 'ActiveStorage::Attachment', name: :preview_images) @@ -97,7 +97,7 @@ module Api :name, :external_id, { - submitters: [%i[name uuid]], + 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 929767f7..5d969a65 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -11,7 +11,8 @@ class StartFormController < ApplicationController def show @submitter = @template.submissions.new(account_id: @template.account_id) - .submitters.new(uuid: @template.submitters.first['uuid']) + .submitters.new(uuid: (filter_undefined_submitters(@template).first || + @template.submitters.first)['uuid']) end def update @@ -22,15 +23,17 @@ class StartFormController < ApplicationController if @submitter.completed_at? redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) else - if @template.submitters.to_a.size > 2 && @submitter.new_record? + if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? @error_message = 'Not found' return render :show end - assign_submission_attributes(@submitter, @template) if @submitter.new_record? + if (is_new_record = @submitter.new_record?) + assign_submission_attributes(@submitter, @template) - is_new_record = @submitter.new_record? + Submissions::AssignDefinedSubmitters.call(@submitter.submission) + end if @submitter.save if is_new_record @@ -66,7 +69,7 @@ class StartFormController < ApplicationController (Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?) submitter.assign_attributes( - uuid: template.submitters.first['uuid'], + uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'], ip: request.remote_ip, ua: request.user_agent, values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {}, @@ -83,25 +86,16 @@ class StartFormController < ApplicationController submitter.submission ||= Submission.new(template:, account_id: template.account_id, template_submitters: template.submitters, + submitters: [submitter], source: :link) - maybe_assign_default_second_submitter(submitter.submission) - submitter.account_id = submitter.submission.account_id submitter end - def maybe_assign_default_second_submitter(submission) - return unless submission.new_record? - return if submission.template.submitters.to_a.size != 2 - - submission.submitters_order = 'preserved' - submission.submitters.new( - account_id: submission.account_id, - uuid: submission.template.submitters.second['uuid'], - email: submission.template.author.email - ) + def filter_undefined_submitters(template) + 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 f973831a..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]], + 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_preferences_controller.rb b/app/controllers/templates_preferences_controller.rb index ca21a52e..30f1c3d6 100644 --- a/app/controllers/templates_preferences_controller.rb +++ b/app/controllers/templates_preferences_controller.rb @@ -9,7 +9,7 @@ class TemplatesPreferencesController < ApplicationController authorize!(:update, @template) @template.preferences = @template.preferences.merge(template_params[:preferences]) - @template.preferences = @template.preferences.reject { |_, v| v.is_a?(String) && v.blank? } + @template.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? } @template.save! head :ok @@ -23,12 +23,16 @@ class TemplatesPreferencesController < ApplicationController documents_copy_email_subject documents_copy_email_body documents_copy_email_enabled documents_copy_email_attach_audit completed_notification_email_attach_documents + completed_redirect_url completed_notification_email_subject completed_notification_email_body - completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit] + + [completed_message: %i[title body]] ).tap do |attrs| attrs[:preferences] = attrs[:preferences].transform_values do |value| if %w[true false].include?(value) value == 'true' + elsif value.respond_to?(:compact_blank) + value.compact_blank else value end diff --git a/app/controllers/templates_recipients_controller.rb b/app/controllers/templates_recipients_controller.rb new file mode 100644 index 00000000..745fee93 --- /dev/null +++ b/app/controllers/templates_recipients_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class TemplatesRecipientsController < ApplicationController + load_and_authorize_resource :template + + def create + authorize!(:update, @template) + + @template.submitters = + submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } } + + @template.save! + + render json: { submitters: @template.submitters } + end + + 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| + next if s[:uuid].blank? + + 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? + 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 + + s + end + end +end diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index 7d106675..620860cb 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -52,7 +52,7 @@ class TemplatesUploadsController < ApplicationController def create_file_params_from_url tempfile = Tempfile.new tempfile.binmode - tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body) + tempfile.write(DownloadUtils.call(params[:url]).body) tempfile.rewind file = ActionDispatch::Http::UploadedFile.new( @@ -65,10 +65,4 @@ class TemplatesUploadsController < ApplicationController { files: [file] } end - - def conn - Faraday.new do |faraday| - faraday.response :follow_redirects - end - end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 098ea339..bdc4d93d 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -26,6 +26,9 @@ import EmailsTextarea from './elements/emails_textarea' import ToggleOnSubmit from './elements/toggle_on_submit' 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' @@ -89,9 +92,14 @@ safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-on-submit', ToggleOnSubmit) 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 () { + document.addEventListener('turbo:submit-end', this.onSubmit) + this.appElem = document.createElement('div') this.appElem.classList.add('md:h-screen') @@ -114,12 +122,22 @@ safeRegisterElement('template-builder', class extends HTMLElement { acceptFileTypes: this.dataset.acceptFileTypes }) - this.app.mount(this.appElem) + this.component = this.app.mount(this.appElem) this.appendChild(this.appElem) } + onSubmit = (e) => { + if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') { + e.detail.fetchResponse.response.json().then((data) => { + this.component.template.submitters = data.submitters + }) + } + } + disconnectedCallback () { + document.removeEventListener('turbo:submit-end', this.onSubmit) + this.app?.unmount() this.appElem?.remove() } diff --git a/app/javascript/application.scss b/app/javascript/application.scss index 26ad688a..b29a6bf8 100644 --- a/app/javascript/application.scss +++ b/app/javascript/application.scss @@ -51,6 +51,10 @@ button[disabled] .enabled { @apply input input-bordered bg-white; } +.base-input-slim { + @apply input input-bordered bg-white h-10; +} + .base-textarea { @apply textarea textarea-bordered bg-white rounded-3xl; } @@ -100,7 +104,7 @@ button[disabled] .enabled { .autocomplete { background: white; z-index: 1000; - font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font: 16px/25px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; overflow: auto; box-sizing: border-box; @apply border border-base-300 mt-1 rounded-md; @@ -111,7 +115,7 @@ button[disabled] .enabled { } .autocomplete > div { - @apply px-2 py-1 font-normal text-sm; + @apply px-2 py-1.5 font-normal; } .autocomplete .group { 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/elements/linked_input.js b/app/javascript/elements/linked_input.js new file mode 100644 index 00000000..0c0cfe7f --- /dev/null +++ b/app/javascript/elements/linked_input.js @@ -0,0 +1,31 @@ +export default class extends HTMLElement { + connectedCallback () { + if (this.target) { + this.input.value = this.target.value + + this.target.addEventListener('input', (e) => { + this.input.value = e.target.value + }) + + this.target.addEventListener('linked-input.update', (e) => { + this.input.value = e.target.value + }) + } + } + + get input () { + return this.querySelector('input') + } + + get target () { + if (this.dataset.targetId) { + const listItem = this.closest('[data-targets="dynamic-list.items"]') + + if (listItem) { + return listItem.querySelector(`#${this.dataset.targetId}`) + } else { + return document.getElementById(this.dataset.targetId) + } + } + } +} diff --git a/app/javascript/elements/submitter_autocomplete.js b/app/javascript/elements/submitter_autocomplete.js index 08db3808..5fc539bf 100644 --- a/app/javascript/elements/submitter_autocomplete.js +++ b/app/javascript/elements/submitter_autocomplete.js @@ -24,6 +24,7 @@ export default class extends HTMLElement { if (input && item[field]) { input.value = item[field] + input.dispatchEvent(new CustomEvent('linked-input.update', { bubbles: true })) } if (textarea && item[field]) { diff --git a/app/javascript/elements/toggle_attribute.js b/app/javascript/elements/toggle_attribute.js new file mode 100644 index 00000000..e9ee3075 --- /dev/null +++ b/app/javascript/elements/toggle_attribute.js @@ -0,0 +1,28 @@ +export default class extends HTMLElement { + connectedCallback () { + this.input.addEventListener('change', (event) => { + if (this.dataset.attribute) { + this.target[this.dataset.attribute] = event.target.checked + } + + if (this.dataset.className) { + this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value) + if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') { + this.target.disabled = event.target.value !== this.dataset.value + } + } + + if (this.dataset.attribute === 'disabled') { + this.target.value = '' + } + }) + } + + get input () { + return this.querySelector('input[type="checkbox"]') || this.querySelector('select') + } + + get target () { + return document.getElementById(this.dataset.targetId) + } +} diff --git a/app/javascript/elements/turbo_modal.js b/app/javascript/elements/turbo_modal.js index 5009f60b..69c20549 100644 --- a/app/javascript/elements/turbo_modal.js +++ b/app/javascript/elements/turbo_modal.js @@ -22,7 +22,7 @@ export default actionable(class extends HTMLElement { } onSubmit = (e) => { - if (e.detail.success) { + if (e.detail.success && e.detail?.formSubmission?.formElement?.dataset?.closeOnSubmit !== 'false') { this.close() } } 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/area.vue b/app/javascript/submission_form/area.vue index e7596862..1a67d904 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -226,7 +226,8 @@ export default { }, submitter: { type: Object, - required: true + required: false, + default: () => ({}) }, withSignatureId: { type: Boolean, diff --git a/app/javascript/submission_form/areas.vue b/app/javascript/submission_form/areas.vue index d9807ad5..65524fce 100644 --- a/app/javascript/submission_form/areas.vue +++ b/app/javascript/submission_form/areas.vue @@ -21,7 +21,7 @@ :values="values" :field="field" :area="area" - :submittable="true" + :submittable="submittable" :field-index="fieldIndex" :scroll-padding="scrollPadding" :submitter="submitter" @@ -58,6 +58,11 @@ export default { required: false, default: false }, + submittable: { + type: Boolean, + required: false, + default: true + }, submitter: { type: Object, required: true diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index c84c7eb1..946a3aca 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -13,6 +13,12 @@ :scroll-padding="scrollPadding" @focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]" /> +
+ [] + }, withSignatureId: { type: Boolean, required: false, @@ -733,6 +755,7 @@ export default { data () { return { isCompleted: false, + isInvite: false, isFormVisible: this.expand !== false, showFillAllRequiredFields: false, currentStep: 0, @@ -802,6 +825,9 @@ export default { currentField () { return this.currentStepFields[0] }, + readonlyConditionalFields () { + return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f)) + }, stepFields () { return this.fields.filter((f) => !f.readonly).reduce((acc, f) => { const prevStep = acc[acc.length - 1] @@ -933,12 +959,12 @@ export default { return acc && isEmpty(this.values[c.field_uuid]) } else if (['not_empty', 'checked'].includes(c.action)) { return acc && !isEmpty(this.values[c.field_uuid]) - } else if (['equal', 'contains'].includes(c.action)) { + } else if (['equal', 'contains'].includes(c.action) && field) { const option = field.options.find((o) => o.uuid === c.value) const values = [this.values[c.field_uuid]].flat() return acc && values.includes(this.optionValue(option, field.options.indexOf(option))) - } else if (['not_equal', 'does_not_contain'].includes(c.action)) { + } else if (['not_equal', 'does_not_contain'].includes(c.action) && field) { const option = field.options.find((o) => o.uuid === c.value) const values = [this.values[c.field_uuid]].flat() @@ -1110,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') } @@ -1149,18 +1175,10 @@ export default { if (emptyRequiredField === nextStep) { this.showFillAllRequiredFields = true } + } else if (this.inviteSubmitters.length) { + this.isInvite = true } else { - this.isCompleted = true - - const respData = await response.text() - - if (respData) { - this.onComplete(JSON.parse(respData)) - } - - if (this.completedRedirectUrl) { - window.location.href = this.completedRedirectUrl - } + this.performComplete(response) } }).catch(error => { console.error(error) @@ -1176,6 +1194,21 @@ export default { }).finally(() => { this.isSubmitting = false }) + }, + async performComplete (resp) { + this.isCompleted = true + + if (resp) { + const respData = await resp.text() + + if (respData) { + this.onComplete(JSON.parse(respData)) + } + } + + if (this.completedRedirectUrl) { + window.location.href = this.completedRedirectUrl + } } } } 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/javascript/submission_form/text_step.vue b/app/javascript/submission_form/text_step.vue index 3a8d509a..cd71d16d 100644 --- a/app/javascript/submission_form/text_step.vue +++ b/app/javascript/submission_form/text_step.vue @@ -147,7 +147,7 @@ export default { const textarea = this.$refs.textarea textarea.style.height = 'auto' - textarea.style.height = textarea.scrollHeight + 'px' + textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px' }, toggleTextArea () { this.isTextArea = true diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 812b233b..4334aa8e 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -134,6 +134,17 @@ Save and Preview +
  • + + + Preferences + +
  • @@ -404,7 +415,7 @@ import Contenteditable from './contenteditable' import DocumentPreview from './preview' import DocumentControls from './controls' import MobileFields from './mobile_fields' -import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle } from '@tabler/icons-vue' +import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue' import { v4 } from 'uuid' import { ref, computed } from 'vue' import { en as i18nEn } from './i18n' @@ -428,6 +439,7 @@ export default { Contenteditable, IconUsersPlus, IconChevronDown, + IconAdjustments, IconEye, IconDeviceFloppy }, @@ -770,6 +782,9 @@ export default { this.documentRefs = [] }, methods: { + closeDropdown () { + document.activeElement.blur() + }, t (key) { return this.i18n[key] || i18nEn[key] || key }, 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 e59e00e6..85c2c166 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -1,31 +1,38 @@ <%= 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 %> -