From 3f8d2831e2ac08d461d11d7fd6e40d87c54d2b03 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 12 May 2025 13:22:11 +0300 Subject: [PATCH 01/20] allow double dash email --- lib/params/base_validator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/params/base_validator.rb b/lib/params/base_validator.rb index 8a5abcf1..988cbfcb 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -73,7 +73,7 @@ module Params return if params[key].to_s.include?('<') if params[key].to_s.strip.split(/\s*[;,]\s*/).compact_blank - .all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) } + .all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) || email.include?('--') } return end From 751be1dae5b28236fd10d6d36de12a2d5af8c19d Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 12 May 2025 15:38:13 +0300 Subject: [PATCH 02/20] fix template filter --- app/controllers/api/submitters_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index c2e6a07f..4d53d3cb 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -162,7 +162,7 @@ module Api submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present? if params[:template_id].present? - submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] }) + submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] }) end maybe_filder_by_completed_at(submitters, params) From 93ba17f42e1f25bf1b787b2ee0659a862b2c01d8 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 12 May 2025 18:23:17 +0300 Subject: [PATCH 03/20] expire at webhook --- app/controllers/start_form_controller.rb | 15 ++++++++++++--- lib/submissions.rb | 8 +++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 080b9807..c79535ec 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -43,9 +43,11 @@ class StartFormController < ApplicationController if @submitter.save if is_new_record - WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url| - SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id, - 'webhook_url_id' => webhook_url.id) + enqueue_submission_create_webhooks(@submitter) + + if @submitter.submission.expire_at? + ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at, + 'submission_id' => @submitter.submission_id) end end @@ -64,6 +66,13 @@ class StartFormController < ApplicationController private + def enqueue_submission_create_webhooks(submitter) + WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url| + SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id, + 'webhook_url_id' => webhook_url.id) + end + end + def find_or_initialize_submitter(template, submitter_params) Submitter.where(submission: template.submissions.where(expire_at: Time.current..) .or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) diff --git a/lib/submissions.rb b/lib/submissions.rb index ada1d320..cf6ef851 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -74,7 +74,13 @@ module Submissions preferences:, sent_at: mark_as_sent ? Time.current : nil) - submission.tap(&:save!) + submission.save! + + if submission.expire_at? + ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id) + end + + submission end end From 3166defa1a13e44f229d8b6761d65d4dd48edcea Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 13 May 2025 12:27:27 +0300 Subject: [PATCH 04/20] add submitter value var --- lib/replace_email_variables.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/replace_email_variables.rb b/lib/replace_email_variables.rb index 8ad9111b..6b3efacd 100644 --- a/lib/replace_email_variables.rb +++ b/lib/replace_email_variables.rb @@ -13,6 +13,7 @@ module ReplaceEmailVariables SUBMITTER_FIRST_NAME = /\{+submitter\.first_name\}+/i SUBMITTER_ID = /\{+submitter\.id\}+/i SUBMITTER_SLUG = /\{+submitter\.slug\}+/i + SUBMITTER_FIELD_VALUE = /\{+submitter\.(?[^}]+)\}+/i SUBMISSION_LINK = /\{+submission\.link\}+/i SUBMISSION_ID = /\{+submission\.id\}+/i SUBMISSION_EXPIRE_AT = /\{+submission\.expire_at\}+/i @@ -72,6 +73,13 @@ module ReplaceEmailVariables build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :values, match[:field_name].to_s.strip) end + text = replace(text, SUBMITTER_FIELD_VALUE, html_escape:) do |match| + submitters = submitter.submission.template_submitters || submitter.submission.template.submitters + index = submitters.find_index { |e| e['uuid'] == submitter.uuid } + + build_submitters_n_field(submitter.submission, index, :values, match[:field_name].to_s.strip) + end + replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') } end # rubocop:enable Metrics From 87265e2f22942e7841b82d2809059f41600e7ac3 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 14 May 2025 13:04:35 +0300 Subject: [PATCH 05/20] fix prefill same name field --- lib/submitters/normalize_values.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/submitters/normalize_values.rb b/lib/submitters/normalize_values.rb index 08941916..4cdf5e4b 100644 --- a/lib/submitters/normalize_values.rb +++ b/lib/submitters/normalize_values.rb @@ -52,10 +52,10 @@ module Submitters attachments.push(*new_attachments) - value = new_value + acc[field['uuid']] = normalize_value(field, new_value) + else + acc[field['uuid']] = normalize_value(field, value) end - - acc[field['uuid']] = normalize_value(field, value) end end From 2dca7d8a8ee2d4f5fbb37ebf037a1f84097ae082 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Tue, 13 May 2025 21:27:40 +0300 Subject: [PATCH 06/20] add builder 'replace' button specs --- spec/rails_helper.rb | 2 ++ spec/request_helper.rb | 37 ++++++++++++++++++++++++++++ spec/system/template_builder_spec.rb | 34 +++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 spec/request_helper.rb create mode 100644 spec/system/template_builder_spec.rb diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index db91da56..dab44cb6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -11,6 +11,7 @@ require 'capybara/rspec' require 'webmock/rspec' require 'sidekiq/testing' require 'signing_form_helper' +require 'request_helper' Sidekiq::Testing.fake! @@ -54,6 +55,7 @@ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods config.include Devise::Test::IntegrationHelpers config.include SigningFormHelper + config.include RequestHelper config.before(:each, type: :system) do if ENV['HEADLESS'] == 'false' diff --git a/spec/request_helper.rb b/spec/request_helper.rb new file mode 100644 index 00000000..7240c076 --- /dev/null +++ b/spec/request_helper.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module RequestHelper + module_function + + def wait_for_fetch + page.execute_script( + <<~JS + if (!window.fetchInitialized) { + window.pendingFetchCount = 0; + + const originalFetch = window.fetch; + + window.fetch = function(...args) { + window.pendingFetchCount++; + + return originalFetch.apply(this, args).finally(() => { + window.pendingFetchCount--; + }); + }; + + window.fetchInitialized = true; + } + JS + ) + + yield + + Timeout.timeout(Capybara.default_max_wait_time) do + loop do + break if page.evaluate_script('window.pendingFetchCount') == 0 + + sleep 0.1 + end + end + end +end diff --git a/spec/system/template_builder_spec.rb b/spec/system/template_builder_spec.rb new file mode 100644 index 00000000..f150bea9 --- /dev/null +++ b/spec/system/template_builder_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Template Builder' do + let(:account) { create(:account) } + let(:author) { create(:user, account:) } + let(:template) { create(:template, account:, author:, attachment_count: 3, except_field_types: %w[phone payment]) } + + before do + sign_in(author) + end + + context 'when manage template documents' do + before do + visit edit_template_path(template) + end + + it 'replaces the document' do + doc = find("div[id='documents_container'] div[data-document-uuid='#{template.schema[1]['attachment_uuid']}'") + doc.click + + expect do + wait_for_fetch do + doc.find('.replace-document-button').click + doc.find('.replace-document-button input[type="file"]', visible: false) + .attach_file(Rails.root.join('spec/fixtures/sample-image.png')) + end + end.to change { template.documents.count }.by(1) + + expect(template.reload['schema'][1]['name']).to eq('sample-image') + end + end +end From 0122a237e350a8efeb5291f49a7876bc927b9321 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 14 May 2025 13:11:12 +0300 Subject: [PATCH 07/20] remove request helper --- spec/rails_helper.rb | 2 -- spec/request_helper.rb | 37 ---------------------------- spec/system/template_builder_spec.rb | 12 ++++----- 3 files changed, 6 insertions(+), 45 deletions(-) delete mode 100644 spec/request_helper.rb diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index dab44cb6..db91da56 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -11,7 +11,6 @@ require 'capybara/rspec' require 'webmock/rspec' require 'sidekiq/testing' require 'signing_form_helper' -require 'request_helper' Sidekiq::Testing.fake! @@ -55,7 +54,6 @@ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods config.include Devise::Test::IntegrationHelpers config.include SigningFormHelper - config.include RequestHelper config.before(:each, type: :system) do if ENV['HEADLESS'] == 'false' diff --git a/spec/request_helper.rb b/spec/request_helper.rb deleted file mode 100644 index 7240c076..00000000 --- a/spec/request_helper.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module RequestHelper - module_function - - def wait_for_fetch - page.execute_script( - <<~JS - if (!window.fetchInitialized) { - window.pendingFetchCount = 0; - - const originalFetch = window.fetch; - - window.fetch = function(...args) { - window.pendingFetchCount++; - - return originalFetch.apply(this, args).finally(() => { - window.pendingFetchCount--; - }); - }; - - window.fetchInitialized = true; - } - JS - ) - - yield - - Timeout.timeout(Capybara.default_max_wait_time) do - loop do - break if page.evaluate_script('window.pendingFetchCount') == 0 - - sleep 0.1 - end - end - end -end diff --git a/spec/system/template_builder_spec.rb b/spec/system/template_builder_spec.rb index f150bea9..d14ce3b9 100644 --- a/spec/system/template_builder_spec.rb +++ b/spec/system/template_builder_spec.rb @@ -21,14 +21,14 @@ RSpec.describe 'Template Builder' do doc.click expect do - wait_for_fetch do - doc.find('.replace-document-button').click - doc.find('.replace-document-button input[type="file"]', visible: false) - .attach_file(Rails.root.join('spec/fixtures/sample-image.png')) - end + doc.find('.replace-document-button').click + doc.find('.replace-document-button input[type="file"]', visible: false) + .attach_file(Rails.root.join('spec/fixtures/sample-image.png')) + + page.driver.wait_for_network_idle end.to change { template.documents.count }.by(1) - expect(template.reload['schema'][1]['name']).to eq('sample-image') + expect(page).to have_content('sample-image') end end end From 15b886d4fffd06faf94f138a6b1f84ba9f3b47e9 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 14 May 2025 20:22:34 +0300 Subject: [PATCH 08/20] update reminder durations --- lib/account_configs.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/account_configs.rb b/lib/account_configs.rb index 5f884657..111320f8 100644 --- a/lib/account_configs.rb +++ b/lib/account_configs.rb @@ -9,7 +9,11 @@ module AccountConfigs 'twelve_hours' => '12 hours', 'twenty_four_hours' => '24 hours', 'two_days' => '2 days', + 'three_days' => '3 days', 'four_days' => '4 days', + 'five_days' => '5 days', + 'six_days' => '6 days', + 'seven_days' => '7 days', 'eight_days' => '8 days', 'fifteen_days' => '15 days' }.freeze From 91b182b04afbd184359d2ffa8be7563208473216 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 14 May 2025 20:36:24 +0300 Subject: [PATCH 09/20] adjust email validate --- lib/params/base_validator.rb | 2 +- lib/submissions.rb | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/params/base_validator.rb b/lib/params/base_validator.rb index 988cbfcb..cd0005f6 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -72,7 +72,7 @@ module Params return if params[key].blank? return if params[key].to_s.include?('<') - if params[key].to_s.strip.split(/\s*[;,]\s*/).compact_blank + if params[key].to_s.strip.split(%r{\s*[;,/]\s*}).compact_blank .all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) || email.include?('--') } return end diff --git a/lib/submissions.rb b/lib/submissions.rb index cf6ef851..3f52cbf5 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -118,15 +118,17 @@ module Submissions return if email.blank? return if email.is_a?(Numeric) - return email.downcase if email.to_s.include?(',') || - email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) || - email.to_s.exclude?('.') + email = email.to_s.tr('/', ',') + + return email.downcase if email.include?(',') || + email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) || + email.exclude?('.') fixed_email = EmailTypo.call(email.delete_prefix('<')) return fixed_email if fixed_email == email - domain = email.to_s.split('@').last.to_s.downcase + domain = email.split('@').last.to_s.downcase fixed_domain = fixed_email.to_s.split('@').last return email.downcase if domain == fixed_domain From 318b65af182c87999b88b2ccd06183479b8c5a19 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 14 May 2025 21:28:52 +0300 Subject: [PATCH 10/20] fix i18n --- config/locales/i18n.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 893fe39c..6bf79399 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -845,7 +845,7 @@ es: &es select_user: Seleccionar usuario team_member_permissions: Permisos de miembros del equipo entire_team: Todo el equipo - admin_only: Solo administrador + admin_only: Solo admin accessiable_by: Accesible por team_access: Acceso del equipo remove_filter: Eliminar filtro @@ -1649,7 +1649,7 @@ it: &it select_user: Seleziona utente team_member_permissions: Permessi membri del team entire_team: Intero team - admin_only: Solo amministratore + admin_only: Solo admin accessiable_by: Accessibile da team_access: Accesso al team remove_filter: Rimuovi filtro @@ -2454,7 +2454,7 @@ fr: &fr select_user: Sélectionner un utilisateur team_member_permissions: Permissions des membres de l'équipe entire_team: Équipe entière - admin_only: Administrateur uniquement + admin_only: Admin seul accessiable_by: Accessible par team_access: Accès à l'équipe remove_filter: Supprimer le filtre @@ -3260,7 +3260,7 @@ pt: &pt select_user: Selecionar usuário team_member_permissions: Permissões de membro da equipe entire_team: Toda a equipe - admin_only: Somente administrador + admin_only: Somente admin accessiable_by: Acessível por team_access: Acesso à equipe remove_filter: Remover filtro @@ -4066,7 +4066,7 @@ de: &de select_user: Benutzer auswählen team_member_permissions: Berechtigungen für Teammitglieder entire_team: Gesamtes Team - admin_only: Nur Administratoren + admin_only: Nur Admins accessiable_by: Zugänglich für team_access: Teamzugang remove_filter: Filter entfernen From 5f44806287997456977895d40996ac9bb1b17839 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Wed, 14 May 2025 00:54:48 +0300 Subject: [PATCH 11/20] adjust dropoze border style --- app/javascript/elements/dashboard_dropzone.js | 16 ++++++++++++ app/javascript/elements/file_dropzone.js | 26 ++++++++++++++++--- app/javascript/template_builder/builder.vue | 1 - app/javascript/template_builder/dropzone.vue | 7 +---- .../template_builder/hover_dropzone.vue | 5 +--- app/javascript/template_builder/i18n.js | 12 ++++----- app/views/shared/_classes.html.erb | 2 +- app/views/templates/_dropzone.html.erb | 2 +- 8 files changed, 48 insertions(+), 23 deletions(-) diff --git a/app/javascript/elements/dashboard_dropzone.js b/app/javascript/elements/dashboard_dropzone.js index b99c00ff..3ac0a4c7 100644 --- a/app/javascript/elements/dashboard_dropzone.js +++ b/app/javascript/elements/dashboard_dropzone.js @@ -165,11 +165,27 @@ export default targetable(class extends HTMLElement { onDragover (e) { if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') { this.style.backgroundColor = '#F7F3F0' + + if (this.classList.contains('before:border-base-300')) { + this.classList.remove('before:border-base-300') + this.classList.add('before:border-base-content/50') + } else if (this.classList.contains('border-base-300')) { + this.classList.remove('border-base-300') + this.classList.add('border-base-content/50') + } } } onDragleave () { this.style.backgroundColor = null + + if (this.classList.contains('before:border-base-content/50')) { + this.classList.remove('before:border-base-content/50') + this.classList.add('before:border-base-300') + } else if (this.classList.contains('border-base-content/50')) { + this.classList.remove('border-base-content/50') + this.classList.add('border-base-300') + } } onWindowDragleave = (e) => { diff --git a/app/javascript/elements/file_dropzone.js b/app/javascript/elements/file_dropzone.js index 76411a50..02fc1083 100644 --- a/app/javascript/elements/file_dropzone.js +++ b/app/javascript/elements/file_dropzone.js @@ -5,19 +5,37 @@ export default actionable(targetable(class extends HTMLElement { static [target.static] = [ 'loading', 'icon', - 'input' + 'input', + 'area' ] connectedCallback () { - this.addEventListener('drop', this.onDrop) - this.addEventListener('dragover', (e) => e.preventDefault()) - + this.addEventListener('drop', this.onDrop) document.addEventListener('turbo:submit-end', this.toggleLoading) + this.area?.addEventListener('dragover', this.onDragover) + this.area?.addEventListener('dragleave', this.onDragleave) } disconnectedCallback () { + this.removeEventListener('drop', this.onDrop) document.removeEventListener('turbo:submit-end', this.toggleLoading) + this.area?.removeEventListener('dragover', this.onDragover) + this.area?.removeEventListener('dragleave', this.onDragleave) + } + + onDragover (e) { + if (e.dataTransfer?.types?.includes('Files')) { + this.style.backgroundColor = '#F7F3F0' + this.classList.remove('border-base-300', 'hover:bg-base-200/30') + this.classList.add('border-base-content/50') + } + } + + onDragleave () { + this.style.backgroundColor = null + this.classList.remove('border-base-content/50') + this.classList.add('border-base-300', 'hover:bg-base-200/30') } onDrop (e) { diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 77e1bb96..514740eb 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -13,7 +13,6 @@ :template-id="template.id" :accept-file-types="acceptFileTypes" :with-replace-and-clone="withReplaceAndCloneUpload" - hover-class="bg-base-200/50" @add="[updateFromUpload($event), isDragFile = false]" @replace="[onDocumentsReplace($event), isDragFile = false]" @replace-and-clone="onDocumentsReplaceAndTemplateClone($event)" diff --git a/app/javascript/template_builder/dropzone.vue b/app/javascript/template_builder/dropzone.vue index ce97bcc1..e6f0030d 100644 --- a/app/javascript/template_builder/dropzone.vue +++ b/app/javascript/template_builder/dropzone.vue @@ -10,7 +10,7 @@ id="document_dropzone" class="w-full relative rounded-md border-2 border-base-content/10 border-dashed" :for="inputId" - :class="[{ 'opacity-50': isLoading, 'hover:bg-base-200/30': !hoverClass }, isDragEntering && hoverClass ? hoverClass : '']" + :class="{ 'opacity-50': isLoading, 'hover:bg-base-200/50': !isDragEntering, 'bg-base-200/50 border-base-content/50': isDragEntering }" >
@@ -84,11 +84,6 @@ export default { required: false, default: 'IconCloudUpload' }, - hoverClass: { - type: String, - required: false, - default: null - }, cloneTemplateOnUpload: { type: Boolean, required: false, diff --git a/app/javascript/template_builder/hover_dropzone.vue b/app/javascript/template_builder/hover_dropzone.vue index 5c9ccf28..54a495c1 100644 --- a/app/javascript/template_builder/hover_dropzone.vue +++ b/app/javascript/template_builder/hover_dropzone.vue @@ -6,12 +6,11 @@
+<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open border-base-content/50 before:border-base-content/50' %> diff --git a/app/views/templates/_dropzone.html.erb b/app/views/templates/_dropzone.html.erb index 95128c1a..766fb0dc 100644 --- a/app/views/templates/_dropzone.html.erb +++ b/app/views/templates/_dropzone.html.erb @@ -2,7 +2,7 @@ -