diff --git a/.erb-lint.yml b/.erb_lint.yml similarity index 100% rename from .erb-lint.yml rename to .erb_lint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcaec28d..1a28aa8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,13 +7,13 @@ jobs: name: Rubocop runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.4.1 - name: Cache gems - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: vendor/bundle key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} @@ -31,13 +31,13 @@ jobs: name: Erblint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.4.1 - name: Cache gems - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: vendor/bundle key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} @@ -49,13 +49,13 @@ jobs: bundle config path vendor/bundle bundle install --jobs 4 --retry 4 - name: Run Erblint - run: bundle exec erblint ./app + run: bundle exec erb_lint ./app eslint: name: ESLint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Node.js uses: actions/setup-node@v1 with: @@ -63,7 +63,7 @@ jobs: - name: Cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v1 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -96,7 +96,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Ruby uses: ruby/setup-ruby@v1 with: @@ -110,12 +110,12 @@ jobs: with: chrome-version: 125 - name: Cache node_modules - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} - name: Cache gems - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: vendor/bundle key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9c2f57bd..2315d2b1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,22 +25,22 @@ jobs: tags: | type=semver,pattern={{version}} - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Create .version file run: echo ${{ github.ref_name }} > .version - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: . push: true diff --git a/Gemfile.lock b/Gemfile.lock index 7f37cb0c..de6e71b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -223,9 +223,12 @@ GEM concurrent-ruby (~> 1.1) webrick (~> 1.7) websocket-driver (~> 0.7) - ffi (1.17.0) - ffi (1.17.0-aarch64-linux-gnu) - ffi (1.17.0-x86_64-linux-gnu) + ffi (1.17.1) + ffi (1.17.1-aarch64-linux-gnu) + ffi (1.17.1-aarch64-linux-musl) + ffi (1.17.1-arm64-darwin) + ffi (1.17.1-x86_64-linux-gnu) + ffi (1.17.1-x86_64-linux-musl) geom2d (0.4.1) globalid (1.2.1) activesupport (>= 6.1) @@ -324,7 +327,7 @@ GEM mysql2 (0.5.6) net-http-persistent (4.0.5) connection_pool (~> 2.2) - net-imap (0.5.3) + net-imap (0.5.6) date net-protocol net-pop (0.1.2) @@ -334,12 +337,18 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.17.2) + nokogiri (1.18.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.17.2-aarch64-linux) + nokogiri (1.18.2-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.17.2-x86_64-linux) + nokogiri (1.18.2-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.2-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.2-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.2-x86_64-linux-musl) racc (~> 1.4) oj (3.16.8) bigdecimal (>= 3.0) @@ -377,7 +386,7 @@ GEM puma (6.5.0) nio4r (~> 2.0) racc (1.8.1) - rack (3.1.8) + rack (3.1.10) rack-proxy (0.7.7) rack rack-session (2.0.0) @@ -520,10 +529,13 @@ GEM simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) - sqlite3 (2.4.1) + sqlite3 (2.5.0) mini_portile2 (~> 2.8.0) - sqlite3 (2.4.1-aarch64-linux-gnu) - sqlite3 (2.4.1-x86_64-linux-gnu) + sqlite3 (2.5.0-aarch64-linux-gnu) + sqlite3 (2.5.0-aarch64-linux-musl) + sqlite3 (2.5.0-arm64-darwin) + sqlite3 (2.5.0-x86_64-linux-gnu) + sqlite3 (2.5.0-x86_64-linux-musl) stringio (3.1.2) strip_attributes (1.14.1) activemodel (>= 3.0, < 9.0) @@ -569,8 +581,11 @@ GEM PLATFORMS aarch64-linux + aarch64-linux-musl + arm64-darwin ruby x86_64-linux + x86_64-linux-musl DEPENDENCIES annotate diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 454ac39b..daeb44cc 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -50,7 +50,7 @@ class AccountsController < ApplicationController # rubocop:disable Layout/LineLength render turbo_stream: turbo_stream.replace( :account_delete_button, - html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account')) + html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) ) # rubocop:enable Layout/LineLength end diff --git a/app/controllers/api/submission_documents_controller.rb b/app/controllers/api/submission_documents_controller.rb index aa3021c5..283b55bf 100644 --- a/app/controllers/api/submission_documents_controller.rb +++ b/app/controllers/api/submission_documents_controller.rb @@ -10,7 +10,7 @@ module Api last_submitter = @submission.submitters.max_by(&:completed_at) if last_submitter.documents_attachments.blank? - last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter) + last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter) end last_submitter.documents_attachments diff --git a/app/controllers/api/tools_controller.rb b/app/controllers/api/tools_controller.rb index 7b2d5cb4..63a50f89 100644 --- a/app/controllers/api/tools_controller.rb +++ b/app/controllers/api/tools_controller.rb @@ -34,6 +34,8 @@ module Api } end } + rescue HexaPDF::MalformedPDFError + render json: { error: 'Malformed PDF' }, status: :unprocessable_entity end end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a48146ee..a3cb0242 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests end - if Rails.env.production? + if Rails.env.production? || Rails.env.test? rescue_from CanCan::AccessDenied do |e| Rollbar.warning(e) if defined?(Rollbar) diff --git a/app/controllers/pwa_controller.rb b/app/controllers/pwa_controller.rb new file mode 100644 index 00000000..d1d4e4f3 --- /dev/null +++ b/app/controllers/pwa_controller.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class PwaController < ActionController::Base +end diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb index 10a3d8d7..06481b1a 100644 --- a/app/controllers/send_submission_email_controller.rb +++ b/app/controllers/send_submission_email_controller.rb @@ -9,8 +9,6 @@ class SendSubmissionEmailController < ApplicationController SEND_DURATION = 30.minutes - def success; end - def create @submitter = if params[:template_slug] @@ -31,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController end respond_to do |f| - f.html { redirect_to success_send_submission_email_index_path } + f.html { render :success } f.json { head :ok } end end diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 209c81b4..ee05ff28 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -34,6 +34,8 @@ class StartFormController < ApplicationController assign_submission_attributes(@submitter, @template) Submissions::AssignDefinedSubmitters.call(@submitter.submission) + else + @submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent) end if @submitter.save @@ -64,8 +66,10 @@ class StartFormController < ApplicationController .or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) .order(id: :desc) .where(declined_at: nil) + .where(external_id: nil) + .where(ip: [nil, request.remote_ip]) .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } - .find_or_initialize_by(**submitter_params.compact_blank) + .find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank) end def assign_submission_attributes(submitter, template) diff --git a/app/controllers/submission_events_controller.rb b/app/controllers/submission_events_controller.rb new file mode 100644 index 00000000..9dbd57ed --- /dev/null +++ b/app/controllers/submission_events_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class SubmissionEventsController < ApplicationController + SUBMISSION_EVENT_ICONS = { + 'view_form' => 'eye', + 'start_form' => 'player_play', + 'complete_form' => 'check', + 'send_email' => 'mail_forward', + 'click_email' => 'hand_click', + 'api_complete_form' => 'check', + 'send_reminder_email' => 'mail_forward', + 'send_2fa_sms' => '2fa', + 'send_sms' => 'send', + 'phone_verified' => 'phone_check', + 'click_sms' => 'hand_click', + 'decline_form' => 'x', + 'start_verification' => 'player_play', + 'complete_verification' => 'check', + 'invite_party' => 'user_plus' + }.freeze + + load_and_authorize_resource :submission + + def index; end +end diff --git a/app/controllers/submissions_preview_controller.rb b/app/controllers/submissions_preview_controller.rb index 0d61cb00..1df14e21 100644 --- a/app/controllers/submissions_preview_controller.rb +++ b/app/controllers/submissions_preview_controller.rb @@ -20,7 +20,10 @@ class SubmissionsPreviewController < ApplicationController @submission ||= Submission.find_by!(slug: params[:slug]) - if @submission.account.archived_at? || (!@submission.submitters.all?(&:completed_at?) && current_user.blank?) + raise ActionController::RoutingError if @submission.account.archived_at? + + if !@submission.submitters.all?(&:completed_at?) && !signature_valid && + (!current_user || !current_ability.can?(:read, @submission)) raise ActionController::RoutingError, I18n.t('not_found') end @@ -37,6 +40,7 @@ class SubmissionsPreviewController < ApplicationController def completed @submission = Submission.find_by!(slug: params[:submissions_preview_slug]) + @template = @submission.template render :completed, layout: 'form' end diff --git a/app/controllers/submitters_resubmit_controller.rb b/app/controllers/submitters_resubmit_controller.rb new file mode 100644 index 00000000..fb893d98 --- /dev/null +++ b/app/controllers/submitters_resubmit_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class SubmittersResubmitController < ApplicationController + load_and_authorize_resource :submitter, parent: false + + def update + return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email + + submission = @submitter.template.submissions.new(created_by_user: current_user, + submitters_order: :preserved, + **@submitter.submission.slice(:template_fields, + :account_id, + :template_schema, + :template_submitters, + :preferences)) + + @submitter.submission.submitters.each do |submitter| + new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name, + :preferences, :metadata, :account_id)) + + next unless submitter.uuid == @submitter.uuid + + assign_submitter_values(new_submitter, submitter) + + @new_submitter ||= new_submitter + end + + submission.save! + + redirect_to submit_form_path(slug: @new_submitter.slug) + end + + private + + def assign_submitter_values(new_submitter, submitter) + attachments_index = submitter.attachments.index_by(&:uuid) + + submitter.submission.template_fields.each do |field| + next if field['submitter_uuid'] != submitter.uuid + next if field['default_value'] == '{{date}}' + next if field['type'] == 'stamp' + next if field['type'] == 'signature' + next if field.dig('preferences', 'formula').present? + + value = submitter.values[field['uuid']] + + next if value.blank? + + if field['type'].in?(%w[image file initials]) + Array.wrap(value).each do |attachment_uuid| + new_submitter.attachments << attachments_index[attachment_uuid].dup + end + end + + new_submitter.values[field['uuid']] = value + end + end +end diff --git a/app/controllers/templates_preferences_controller.rb b/app/controllers/templates_preferences_controller.rb index 7e358537..a9221f7b 100644 --- a/app/controllers/templates_preferences_controller.rb +++ b/app/controllers/templates_preferences_controller.rb @@ -28,8 +28,10 @@ class TemplatesPreferencesController < ApplicationController submitters_order completed_notification_email_subject completed_notification_email_body completed_notification_email_enabled completed_notification_email_attach_audit] + - [completed_message: %i[title body]] + [completed_message: %i[title body], + submitters: [%i[uuid request_email_subject request_email_body]]] ).tap do |attrs| + attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1' attrs[:preferences] = attrs[:preferences].transform_values do |value| if %w[true false].include?(value) value == 'true' diff --git a/app/javascript/elements/autoresize_textarea.js b/app/javascript/elements/autoresize_textarea.js index 8fd25801..a5a8b5ed 100644 --- a/app/javascript/elements/autoresize_textarea.js +++ b/app/javascript/elements/autoresize_textarea.js @@ -3,6 +3,8 @@ export default class extends HTMLElement { this.resize() this.textarea.addEventListener('input', () => this.resize()) + + this.observeVisibility() } resize () { @@ -11,6 +13,28 @@ export default class extends HTMLElement { } } + observeVisibility () { + this.observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + this.resize() + this.observer.unobserve(this.textarea) + } + }) + }, + { + threshold: 0.1 + } + ) + + this.observer.observe(this.textarea) + } + + disconnectedCallback () { + this.observer.unobserve(this.textarea) + } + get textarea () { return this.querySelector('textarea') } diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 1c705228..0a453397 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -3,7 +3,7 @@ class="field-area flex absolute lg:text-base -outline-offset-1" dir="auto" :style="computedStyle" - :class="{ 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" + :class="{ 'font-mono': field.preferences?.font === 'Courier', 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" >
{{ field.name }} - + {{ modelValue.join(', ') }} - + {{ formattedDate }} @@ -109,6 +109,14 @@ export default { areaRefs: [] } }, + computed: { + isMobileContainer () { + const root = this.$root.$el.parentNode.getRootNode() + const container = root.body || root.querySelector('div') + + return container.style.overflow === 'hidden' + } + }, beforeUpdate () { this.areaRefs = [] }, @@ -121,14 +129,16 @@ export default { this.scrollIntoArea(field.areas[0]) } }, + maybeScrollOnClick (field, area) { + if (['text', 'number', 'cells'].includes(field.type) && this.isMobileContainer) { + this.scrollIntoArea(area) + } + }, scrollIntoArea (area) { const areaRef = this.areaRefs.find((a) => a.area === area) if (areaRef) { - const root = this.$root.$el.parentNode.getRootNode() - const container = root.body || root.querySelector('div') - - if (container.style.overflow === 'hidden') { + if (this.isMobileContainer) { this.scrollInContainer(areaRef.$el) } else { const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect() @@ -157,13 +167,15 @@ export default { const formContainer = root.getElementById('form_container') const container = root.body || root.querySelector('div') - const padding = 64 + const isAndroid = /android/i.test(navigator.userAgent) + const padding = isAndroid ? 128 : 64 + const scrollboxTop = isAndroid ? scrollbox.getBoundingClientRect().top : 0 const boxRect = scrollbox.children[0].getBoundingClientRect() const targetRect = target.getBoundingClientRect() const targetTopRelativeToBox = targetRect.top - boxRect.top - scrollbox.scrollTo({ top: targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' }) + scrollbox.scrollTo({ top: targetTopRelativeToBox + scrollboxTop - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' }) }, setAreaRef (el) { if (el) { diff --git a/app/javascript/submission_form/attachment_step.vue b/app/javascript/submission_form/attachment_step.vue index f64dbb23..9f9f5f8b 100644 --- a/app/javascript/submission_form/attachment_step.vue +++ b/app/javascript/submission_form/attachment_step.vue @@ -49,7 +49,7 @@