mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 23:32:07 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14c84eff93 | |||
| 01d982b093 | |||
| 35852ef6ab | |||
| 47b235dc5f | |||
| 0e11d2a581 | |||
| e57e11f206 | |||
| 51f639065c | |||
| 6c1d4b466f | |||
| b940c597dc | |||
| 1ce67bcb39 | |||
| 5050a67d08 | |||
| 76321cef3c | |||
| 8f3a203994 | |||
| ea24562861 | |||
| 4a888d048e | |||
| 36331bb993 | |||
| 6f0e7c0ca8 | |||
| 2491b7b7e3 | |||
| 7965c17f46 | |||
| 5519459519 | |||
| d8bf2cc101 | |||
| 2aece98d67 | |||
| f602497255 | |||
| 11db68cdc7 | |||
| bed98f4344 |
@@ -112,7 +112,9 @@ module Api
|
||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||
|
||||
if params[:template_folder].present?
|
||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||
folder = TemplateFolder.accessible_by(current_ability).find_by(name: params[:template_folder])
|
||||
|
||||
submissions = folder ? submissions.joins(:template).where(template: { folder_id: folder.id }) : submissions.none
|
||||
end
|
||||
|
||||
if params.key?(:archived)
|
||||
|
||||
@@ -93,7 +93,12 @@ module Api
|
||||
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
||||
|
||||
if params[:folder].present?
|
||||
folder = TemplateFolder.accessible_by(current_ability).find_by(name: params[:folder])
|
||||
|
||||
templates = folder ? templates.where(folder:) : templates.none
|
||||
end
|
||||
|
||||
templates
|
||||
end
|
||||
|
||||
@@ -5,8 +5,12 @@ class ErrorsController < ActionController::Base
|
||||
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||
|
||||
ENTERPRISE_PATHS = [
|
||||
'/submissions/html',
|
||||
'/api/submissions/html',
|
||||
'/templates/html',
|
||||
'/api/templates/html',
|
||||
'/submissions/pdf',
|
||||
'/api/submissions/pdf',
|
||||
'/templates/pdf',
|
||||
'/api/templates/pdf',
|
||||
'/templates/doc',
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SearchEntriesReindexController < ApplicationController
|
||||
def create
|
||||
authorize!(:manage, EncryptedConfig)
|
||||
|
||||
ReindexAllSearchEntriesJob.perform_async
|
||||
|
||||
AccountConfig.find_or_initialize_by(account_id: Account.minimum(:id), key: :fulltext_search)
|
||||
.update!(value: true)
|
||||
|
||||
Docuseal.instance_variable_set(:@fulltext_search, nil)
|
||||
|
||||
redirect_back(fallback_location: settings_account_path,
|
||||
notice: "Started building search index. Visit #{root_url}jobs/busy to check progress.")
|
||||
end
|
||||
end
|
||||
@@ -11,11 +11,16 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
def create
|
||||
if params[:template_slug]
|
||||
@submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||
template: { slug: params[:template_slug] })
|
||||
template = Template.find_by!(slug: params[:template_slug])
|
||||
|
||||
@submitter =
|
||||
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
|
||||
elsif params[:submission_slug]
|
||||
@submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase,
|
||||
submission: { slug: params[:submission_slug] })
|
||||
submission = Submission.find_by(slug: params[:submission_slug])
|
||||
|
||||
if submission
|
||||
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
|
||||
end
|
||||
|
||||
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||
else
|
||||
@@ -24,14 +29,18 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter,
|
||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||
end
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter)
|
||||
|
||||
respond_to do |f|
|
||||
f.html { render :success }
|
||||
f.json { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def already_sent?(submitter)
|
||||
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,6 +34,7 @@ class SetupController < ApplicationController
|
||||
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
|
||||
]
|
||||
@account.encrypted_configs.create!(encrypted_configs)
|
||||
@account.account_configs.create!(key: :fulltext_search, value: true) if SearchEntry.table_exists?
|
||||
|
||||
Docuseal.refresh_default_url_options!
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class SubmittersAutocompleteController < ApplicationController
|
||||
|
||||
def search_submitters(submitters)
|
||||
if SELECT_COLUMNS.include?(params[:field])
|
||||
if Docuseal.fulltext_search?(current_user)
|
||||
if Docuseal.fulltext_search?
|
||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
||||
else
|
||||
column = Submitter.arel_table[params[:field].to_sym]
|
||||
|
||||
@@ -6,7 +6,9 @@ class TemplateFoldersAutocompleteController < ApplicationController
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||
templates_query = Template.accessible_by(current_ability).where(archived_at: nil)
|
||||
|
||||
template_folders = @template_folders.where(id: templates_query.select(:folder_id))
|
||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||
|
||||
render json: template_folders.as_json(only: %i[name archived_at])
|
||||
|
||||
@@ -36,6 +36,8 @@ class TemplatesDashboardController < ApplicationController
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||
|
||||
load_related_submissions if params[:q].present? && @templates.blank?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -100,4 +102,19 @@ class TemplatesDashboardController < ApplicationController
|
||||
template_folders.order(id: :desc)
|
||||
end
|
||||
end
|
||||
|
||||
def load_related_submissions
|
||||
@related_submissions = Submission.accessible_by(current_ability)
|
||||
.left_joins(:template)
|
||||
.where(archived_at: nil)
|
||||
.where(templates: { archived_at: nil })
|
||||
.preload(:template_accesses, :created_by_user,
|
||||
template: :author,
|
||||
submitters: :start_form_submission_events)
|
||||
|
||||
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||
.order(id: :desc)
|
||||
|
||||
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class TemplatesPreferencesController < ApplicationController
|
||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||
completed_notification_email_attach_documents
|
||||
completed_redirect_url validate_unique_submitters
|
||||
submitters_order require_phone_2fa
|
||||
require_all_submitters submitters_order require_phone_2fa
|
||||
default_expire_at_duration
|
||||
default_expire_at
|
||||
completed_notification_email_subject completed_notification_email_body
|
||||
|
||||
@@ -1052,6 +1052,12 @@ export default {
|
||||
field.readonly = true
|
||||
}
|
||||
|
||||
if (type === 'datenow') {
|
||||
field.type = 'date'
|
||||
field.readonly = true
|
||||
field.default_value = '{{date}}'
|
||||
}
|
||||
|
||||
if (type === 'date') {
|
||||
field.preferences = {
|
||||
format: this.defaultDateFormat
|
||||
@@ -1450,6 +1456,12 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'datenow') {
|
||||
field.type = 'date'
|
||||
field.readonly = true
|
||||
field.default_value = '{{date}}'
|
||||
}
|
||||
|
||||
if (['stamp', 'heading'].includes(field.type)) {
|
||||
field.readonly = true
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FiledTypeDropdown',
|
||||
@@ -101,6 +101,7 @@ export default {
|
||||
signature: this.t('signature'),
|
||||
initials: this.t('initials'),
|
||||
date: this.t('date'),
|
||||
datenow: this.t('date_signed'),
|
||||
number: this.t('number'),
|
||||
image: this.t('image'),
|
||||
file: this.t('file'),
|
||||
@@ -142,6 +143,7 @@ export default {
|
||||
signature: IconWritingSign,
|
||||
initials: IconLetterCaseUpper,
|
||||
date: IconCalendarEvent,
|
||||
datenow: IconCalendarCheck,
|
||||
number: IconSquareNumber1,
|
||||
image: IconPhoto,
|
||||
checkbox: IconCheckbox,
|
||||
@@ -164,7 +166,7 @@ export default {
|
||||
return acc
|
||||
}, {})
|
||||
} else {
|
||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading'))
|
||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -347,7 +347,7 @@ export default {
|
||||
return acc
|
||||
}, {})
|
||||
} else {
|
||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading'))
|
||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||
}
|
||||
},
|
||||
submitterFields () {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const en = {
|
||||
font: 'Font',
|
||||
party: 'Party',
|
||||
date_signed: 'Date Signed',
|
||||
method: 'Method',
|
||||
reorder_fields: 'Reorder fields',
|
||||
verify_id: 'Verify ID',
|
||||
@@ -167,6 +168,7 @@ const en = {
|
||||
}
|
||||
|
||||
const es = {
|
||||
date_signed: 'Fecha actual',
|
||||
fuente: 'Fuente',
|
||||
party: 'Parte',
|
||||
method: 'Método',
|
||||
@@ -335,6 +337,7 @@ const es = {
|
||||
}
|
||||
|
||||
const it = {
|
||||
date_signed: 'Data attuale',
|
||||
font: 'Carattere',
|
||||
party: 'Parte',
|
||||
method: 'Metodo',
|
||||
@@ -503,6 +506,7 @@ const it = {
|
||||
}
|
||||
|
||||
const pt = {
|
||||
date_signed: 'Data atual',
|
||||
fonte: 'Fonte',
|
||||
party: 'Parte',
|
||||
method: 'Método',
|
||||
@@ -671,6 +675,7 @@ const pt = {
|
||||
}
|
||||
|
||||
const fr = {
|
||||
date_signed: 'Date actuelle',
|
||||
font: 'Police',
|
||||
party: 'Partie',
|
||||
method: 'Méthode',
|
||||
@@ -839,6 +844,7 @@ const fr = {
|
||||
}
|
||||
|
||||
const de = {
|
||||
date_now: 'Akt. Datum',
|
||||
font: 'Schriftart',
|
||||
party: 'Partei',
|
||||
method: 'Verfahren',
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ReindexAllSearchEntriesJob
|
||||
include Sidekiq::Job
|
||||
|
||||
def perform
|
||||
SearchEntries.reindex_all
|
||||
end
|
||||
end
|
||||
@@ -20,6 +20,7 @@
|
||||
#
|
||||
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
|
||||
# index_email_events_on_email (email)
|
||||
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
|
||||
# index_email_events_on_emailable (emailable_type,emailable_id)
|
||||
# index_email_events_on_message_id (message_id)
|
||||
#
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_submissions_on_account_id_and_id (account_id,id)
|
||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||
# index_submissions_on_slug (slug) UNIQUE
|
||||
# index_submissions_on_template_id (template_id)
|
||||
# index_submissions_on_account_id_and_id (account_id,id)
|
||||
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
|
||||
# index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
|
||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||
# index_submissions_on_slug (slug) UNIQUE
|
||||
# index_submissions_on_template_id (template_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
|
||||
@@ -23,11 +23,13 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_templates_on_account_id (account_id)
|
||||
# index_templates_on_author_id (author_id)
|
||||
# index_templates_on_external_id (external_id)
|
||||
# index_templates_on_folder_id (folder_id)
|
||||
# index_templates_on_slug (slug) UNIQUE
|
||||
# index_templates_on_account_id (account_id)
|
||||
# index_templates_on_account_id_and_folder_id_and_id (account_id,folder_id,id) WHERE (archived_at IS NULL)
|
||||
# index_templates_on_account_id_and_id_archived (account_id,id) WHERE (archived_at IS NOT NULL)
|
||||
# index_templates_on_author_id (author_id)
|
||||
# index_templates_on_external_id (external_id)
|
||||
# index_templates_on_folder_id (folder_id)
|
||||
# index_templates_on_slug (slug) UNIQUE
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
|
||||
@@ -165,6 +165,15 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render 'extra_preferences' %>
|
||||
<% if !Docuseal.multitenant? && SearchEntry.table_exists? && (!Docuseal.fulltext_search? || params[:reindex] == 'true') && can?(:manage, EncryptedConfig) %>
|
||||
<div class="flex items-center justify-between py-2.5">
|
||||
<span>
|
||||
Efficient search with search index
|
||||
</span>
|
||||
<%= button_to params[:reindex] == 'true' ? 'Reindex' : 'Build Search Index', settings_search_entries_reindex_index_path, method: :post, class: 'btn btn-sm btn-neutral text-white px-4' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render 'compliances' %>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="us_server_selector" class="flex justify-center hidden">
|
||||
<div id="server_selector" class="flex justify-center <%= 'hidden' unless eu_server %>">
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
|
||||
<% if eu_server %>
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
if (!window.customElements.get('server-selector')) {
|
||||
customElements.define('server-selector', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const usServerSelector = this.querySelector('#us_server_selector');
|
||||
const serverSelector = this.querySelector('#server_selector');
|
||||
const globalServerSelector = this.querySelector('#global_server_selector');
|
||||
const euServerAlert = this.querySelector('#eu_server_alert');
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
|
||||
|
||||
if (!serverSelector.classList.contains('hidden')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (usTimezones.test(timezone)) {
|
||||
usServerSelector.classList.remove('hidden');
|
||||
serverSelector.classList.remove('hidden');
|
||||
} else if (timezone.includes('Europe')) {
|
||||
globalServerSelector.classList.remove('hidden');
|
||||
euServerAlert?.classList?.remove('hidden');
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<% link = pagy_anchor(@pagy) %>
|
||||
<% if @pagy.pages > 1 %>
|
||||
<% link = pagy_anchor(pagy) %>
|
||||
<% if pagy.pages > 1 %>
|
||||
<div class="flex my-6 justify-center md:justify-between">
|
||||
<div class="hidden md:block text-sm">
|
||||
<% if @pagy.count.nil? %>
|
||||
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to)) %>
|
||||
<% if pagy.count.nil? %>
|
||||
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to)) %>
|
||||
<% else %>
|
||||
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to), count: local_assigns.fetch(:count, @pagy.count)) %>
|
||||
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to), count: local_assigns.fetch(:count, pagy.count)) %>
|
||||
<% end %>
|
||||
<%= local_assigns[:left_additional_html] %>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<%= local_assigns[:right_additional_html] %>
|
||||
<div class="join">
|
||||
<% if @pagy.prev %>
|
||||
<%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
|
||||
<% if pagy.prev %>
|
||||
<%== link.call(pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
|
||||
<% else %>
|
||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
||||
<% end %>
|
||||
<span class="join-item btn font-medium uppercase min-h-full h-10">
|
||||
<%= t('page_number', number: @pagy.page) %>
|
||||
<%= t('page_number', number: pagy.page) %>
|
||||
</span>
|
||||
<% if @pagy.next %>
|
||||
<%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
|
||||
<% if local_assigns[:next_page_path].present? %>
|
||||
<%= link_to '»', local_assigns[:next_page_path], class: 'join-item btn min-h-full h-10' %>
|
||||
<% elsif pagy.next %>
|
||||
<%== link.call(pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
|
||||
<% else %>
|
||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
||||
<% end %>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||
<submitters-autocomplete data-field="name">
|
||||
<linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero?, value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
|
||||
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||
<submitters-autocomplete data-field="email">
|
||||
<linked-input data-target-id="<%= "email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('email'), required: index.zero?, value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "email_#{item['uuid']}" %>
|
||||
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('email'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "email_#{item['uuid']}" %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
</submitter-item>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero?, id: "phone_phone_#{item['uuid']}" %>
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero? || template.preferences['require_all_submitters'], id: "phone_phone_#{item['uuid']}" %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<% if submitters.size > 1 %>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<% if @templates.present? %>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<%= render partial: 'templates/template', collection: @templates %>
|
||||
<% if show_dropzone && (current_user.created_at > 2.weeks.ago || params[:tour] == 'true') && current_user == true_user %>
|
||||
<% if (show_dropzone && current_user.created_at > 2.weeks.ago && current_user == true_user) || params[:tour] == 'true' %>
|
||||
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
|
||||
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
|
||||
<div class="hidden md:block">
|
||||
@@ -98,5 +98,14 @@
|
||||
<%= t('templates_not_found') %>
|
||||
</div>
|
||||
</div>
|
||||
<% if @related_submissions.present? %>
|
||||
<h1 class="text-2xl md:text-3xl sm:text-4xl font-bold mt-8 md:mt-4">
|
||||
<%= t('submissions') %>
|
||||
</h1>
|
||||
<div class="space-y-4 mt-4">
|
||||
<%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %>
|
||||
</div>
|
||||
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_path(q: params[:q]) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render 'shared/review_form' %>
|
||||
|
||||
@@ -358,6 +358,16 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
|
||||
<div class="flex items-center mt-4 justify-between w-full">
|
||||
<span>
|
||||
<%= t('require_all_recipients') %>
|
||||
</span>
|
||||
<%= f.fields_for :preferences, Struct.new(:require_all_submitters).new(@template.preferences['require_all_submitters']) do |ff| %>
|
||||
<%= ff.check_box :require_all_submitters, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', '' %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div class="form-control mt-5 pb-2">
|
||||
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
|
||||
|
||||
@@ -22,7 +22,9 @@ en: &en
|
||||
hi_there: Hi there
|
||||
thanks: Thanks
|
||||
private: Private
|
||||
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
|
||||
stripe_integration: Stripe Integration
|
||||
require_all_recipients: Require all recipients
|
||||
stripe_account_has_been_connected: Stripe account has been connected.
|
||||
re_connect_stripe: Re-connect Stripe
|
||||
bcc_recipients: BCC recipients
|
||||
@@ -749,6 +751,7 @@ en: &en
|
||||
share_link: Share link
|
||||
enable_shared_link: Enable shared link
|
||||
share_link_is_currently_disabled: Share link is currently disabled
|
||||
select_data_residency: Select data residency
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Bulk Send
|
||||
@@ -851,6 +854,8 @@ en: &en
|
||||
range_without_total: "%{from}-%{to} items"
|
||||
|
||||
es: &es
|
||||
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
|
||||
require_all_recipients: Requerir a todos los destinatarios
|
||||
stripe_integration: Integración con Stripe
|
||||
stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada.
|
||||
re_connect_stripe: Volver a conectar Stripe
|
||||
@@ -1581,6 +1586,7 @@ es: &es
|
||||
share_link: Enlace para compartir
|
||||
enable_shared_link: Habilitar enlace compartido
|
||||
share_link_is_currently_disabled: El enlace compartido está deshabilitado actualmente
|
||||
select_data_residency: Seleccionar ubicación de datos
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envío masivo
|
||||
@@ -1683,6 +1689,8 @@ es: &es
|
||||
range_without_total: "%{from}-%{to} elementos"
|
||||
|
||||
it: &it
|
||||
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
|
||||
require_all_recipients: Richiedi tutti i destinatari
|
||||
stripe_integration: Integrazione Stripe
|
||||
stripe_account_has_been_connected: L'account Stripe è stato collegato.
|
||||
re_connect_stripe: Ricollega Stripe
|
||||
@@ -2411,6 +2419,7 @@ it: &it
|
||||
share_link: Link di condivisione
|
||||
enable_shared_link: Abilita link condiviso
|
||||
share_link_is_currently_disabled: Il link condiviso è attualmente disabilitato
|
||||
select_data_residency: Seleziona la residenza dei dati
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Invio massivo
|
||||
@@ -2513,6 +2522,8 @@ it: &it
|
||||
range_without_total: "%{from}-%{to} elementi"
|
||||
|
||||
fr: &fr
|
||||
authenticate_embedded_form_preview_with_token: Authentifier l’aperçu du formulaire intégré avec un jeton
|
||||
require_all_recipients: Exiger tous les destinataires
|
||||
stripe_integration: Intégration Stripe
|
||||
stripe_account_has_been_connected: Le compte Stripe a été connecté.
|
||||
re_connect_stripe: Reconnecter Stripe
|
||||
@@ -3244,6 +3255,7 @@ fr: &fr
|
||||
share_link: Lien de partage
|
||||
enable_shared_link: Activer le lien de partage
|
||||
share_link_is_currently_disabled: Le lien de partage est actuellement désactivé
|
||||
select_data_residency: Sélectionner la résidence des données
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envoi en masse
|
||||
@@ -3346,6 +3358,8 @@ fr: &fr
|
||||
range_without_total: "%{from} à %{to} éléments"
|
||||
|
||||
pt: &pt
|
||||
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
|
||||
require_all_recipients: Exigir todos os destinatários
|
||||
stripe_integration: Integração com Stripe
|
||||
stripe_account_has_been_connected: Conta Stripe foi conectada.
|
||||
re_connect_stripe: Reconectar Stripe
|
||||
@@ -4076,6 +4090,7 @@ pt: &pt
|
||||
share_link: Link de compartilhamento
|
||||
enable_shared_link: Ativar link compartilhado
|
||||
share_link_is_currently_disabled: O link compartilhado está desativado no momento
|
||||
select_data_residency: Selecionar local dos dados
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envio em massa
|
||||
@@ -4179,6 +4194,8 @@ pt: &pt
|
||||
range_without_total: "%{from}-%{to} itens"
|
||||
|
||||
de: &de
|
||||
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
|
||||
require_all_recipients: Alle Empfänger erforderlich
|
||||
stripe_integration: Stripe-Integration
|
||||
stripe_account_has_been_connected: Stripe-Konto wurde verbunden.
|
||||
re_connect_stripe: Stripe erneut verbinden
|
||||
@@ -4909,6 +4926,7 @@ de: &de
|
||||
share_link: Freigabelink
|
||||
enable_shared_link: 'Freigabelink aktivieren'
|
||||
share_link_is_currently_disabled: 'Freigabelink ist derzeit deaktiviert'
|
||||
select_data_residency: Datenstandort auswählen
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Massenversand
|
||||
@@ -5080,6 +5098,9 @@ pl:
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Wprowadź adres e-mail powiązany z ukończonym zgłoszeniem.
|
||||
privacy_policy: Polityka Prywatności
|
||||
esignature_disclosure: Użycie e-podpisu
|
||||
select_data_residency: Wybierz lokalizację danych
|
||||
company_name: Nazwa firmy
|
||||
optional: opcjonalne
|
||||
|
||||
uk:
|
||||
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
|
||||
@@ -5151,6 +5172,9 @@ uk:
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: "Введіть адресу електронної пошти, пов'язану із завершеним поданням."
|
||||
privacy_policy: Політика конфіденційності
|
||||
esignature_disclosure: Використання e-підпису
|
||||
select_data_residency: Виберіть місце зберігання даних
|
||||
company_name: Назва компанії
|
||||
optional: необов’язково
|
||||
|
||||
cs:
|
||||
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
|
||||
@@ -5222,6 +5246,9 @@ cs:
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Zadejte e-mailovou adresu spojenou s dokončeným odesláním.
|
||||
privacy_policy: Zásady Ochrany Osobních Údajů
|
||||
esignature_disclosure: Použití e-podpisu
|
||||
select_data_residency: Vyberte umístění dat
|
||||
company_name: Název společnosti
|
||||
optional: volitelné
|
||||
|
||||
he:
|
||||
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
|
||||
@@ -5293,6 +5320,9 @@ he:
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.'
|
||||
privacy_policy: 'מדיניות פרטיות'
|
||||
esignature_disclosure: 'גילוי חתימה אלקטרונית'
|
||||
select_data_residency: בחר מיקום נתונים
|
||||
company_name: שם החברה
|
||||
optional: אופציונלי
|
||||
|
||||
nl:
|
||||
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
|
||||
@@ -5364,6 +5394,9 @@ nl:
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Voer het e-mailadres in dat is gekoppeld aan de voltooide indiening.
|
||||
privacy_policy: Privacybeleid
|
||||
esignature_disclosure: Gebruik van e-handtekening
|
||||
select_data_residency: Selecteer gegevenslocatie
|
||||
company_name: Bedrijfsnaam
|
||||
optional: optioneel
|
||||
|
||||
ar:
|
||||
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
|
||||
@@ -5435,6 +5468,9 @@ ar:
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.'
|
||||
privacy_policy: 'سياسة الخصوصية'
|
||||
esignature_disclosure: 'إفصاح التوقيع الإلكتروني'
|
||||
select_data_residency: اختر موقع تخزين البيانات
|
||||
company_name: اسم الشركة
|
||||
optional: اختياري
|
||||
|
||||
ko:
|
||||
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
|
||||
@@ -5506,6 +5542,9 @@ ko:
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.'
|
||||
privacy_policy: 개인정보 처리방침
|
||||
esignature_disclosure: 전자서명 공개
|
||||
select_data_residency: 데이터 저장 위치 선택
|
||||
company_name: 회사 이름
|
||||
optional: 선택 사항
|
||||
|
||||
ja:
|
||||
require_phone_2fa_to_open: 電話による2段階認証が必要です
|
||||
@@ -5577,6 +5616,9 @@ ja:
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。'
|
||||
privacy_policy: プライバシーポリシー
|
||||
esignature_disclosure: 電子署名に関する開示
|
||||
select_data_residency: データ保存場所を選択
|
||||
company_name: 会社名
|
||||
optional: 任意
|
||||
|
||||
en-US:
|
||||
<<: *en
|
||||
|
||||
@@ -163,6 +163,7 @@ Rails.application.routes.draw do
|
||||
scope '/settings', as: :settings do
|
||||
unless Docuseal.multitenant?
|
||||
resources :storage, only: %i[index create], controller: 'storage_settings'
|
||||
resources :search_entries_reindex, only: %i[create]
|
||||
resources :sms, only: %i[index], controller: 'sms_settings'
|
||||
end
|
||||
resources :email, only: %i[index create], controller: 'email_smtp_settings'
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddEmailEventsEventTypeIndex < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_index :email_events, :email, where: "event_type IN ('bounce', 'soft_bounce', 'complaint', 'soft_complaint')",
|
||||
name: 'index_email_events_on_email_event_types'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddTemplatesFolderIndex < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_index :templates, %i[account_id folder_id id], where: 'archived_at IS NULL'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddSubmissionsTemplateIndex < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_index :submissions, %i[account_id template_id id], where: 'archived_at IS NULL'
|
||||
add_index :submissions, %i[account_id template_id id],
|
||||
where: 'archived_at IS NOT NULL',
|
||||
name: 'index_submissions_on_account_id_and_template_id_and_id_archived'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddArchivedTemplatesIndex < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_index :templates, %i[account_id id], where: 'archived_at IS NOT NULL',
|
||||
name: 'index_templates_on_account_id_and_id_archived'
|
||||
end
|
||||
end
|
||||
+6
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_06_15_091654) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_06_18_085322) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "btree_gin"
|
||||
enable_extension "plpgsql"
|
||||
@@ -177,6 +177,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_15_091654) do
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["account_id", "event_datetime"], name: "index_email_events_on_account_id_and_event_datetime"
|
||||
t.index ["email"], name: "index_email_events_on_email"
|
||||
t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))"
|
||||
t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable"
|
||||
t.index ["message_id"], name: "index_email_events_on_message_id"
|
||||
end
|
||||
@@ -304,6 +305,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_15_091654) do
|
||||
t.datetime "expire_at"
|
||||
t.text "name"
|
||||
t.index ["account_id", "id"], name: "index_submissions_on_account_id_and_id"
|
||||
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id", where: "(archived_at IS NULL)"
|
||||
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id_archived", where: "(archived_at IS NOT NULL)"
|
||||
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
|
||||
t.index ["slug"], name: "index_submissions_on_slug", unique: true
|
||||
t.index ["template_id"], name: "index_submissions_on_template_id"
|
||||
@@ -383,6 +386,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_15_091654) do
|
||||
t.string "external_id"
|
||||
t.text "preferences", null: false
|
||||
t.boolean "shared_link", default: false, null: false
|
||||
t.index ["account_id", "folder_id", "id"], name: "index_templates_on_account_id_and_folder_id_and_id", where: "(archived_at IS NULL)"
|
||||
t.index ["account_id", "id"], name: "index_templates_on_account_id_and_id_archived", where: "(archived_at IS NOT NULL)"
|
||||
t.index ["account_id"], name: "index_templates_on_account_id"
|
||||
t.index ["author_id"], name: "index_templates_on_author_id"
|
||||
t.index ["external_id"], name: "index_templates_on_external_id"
|
||||
|
||||
+8
-5
@@ -73,12 +73,15 @@ module Docuseal
|
||||
@default_pkcs ||= GenerateCertificate.load_pkcs(Docuseal::CERTS)
|
||||
end
|
||||
|
||||
def fulltext_search?(_user)
|
||||
return false unless SearchEntry.table_exists?
|
||||
return true if Docuseal.multitenant?
|
||||
return true if Rails.env.local?
|
||||
def fulltext_search?
|
||||
return @fulltext_search unless @fulltext_search.nil?
|
||||
|
||||
false
|
||||
@fulltext_search =
|
||||
if SearchEntry.table_exists?
|
||||
Docuseal.multitenant? ? true : AccountConfig.exists?(key: :fulltext_search, value: true)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def enable_pwa?
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
module MarkdownToHtml
|
||||
LINK_REGEXP = %r{\[([^\]]+)\]\((https?://[^)]+)\)}
|
||||
LINK_REPLACE = '<a href="\2">\1</a>'
|
||||
|
||||
module_function
|
||||
|
||||
def call(text)
|
||||
text.gsub(LINK_REGEXP, LINK_REPLACE)
|
||||
text.gsub(LINK_REGEXP) do
|
||||
ApplicationController.helpers.link_to(Regexp.last_match(1), Regexp.last_match(2))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -332,12 +332,8 @@ class Pdfium
|
||||
|
||||
Pdfium.FPDF_RenderPageBitmap(bitmap_ptr, page_ptr, 0, 0, render_width, render_height, 0, flags)
|
||||
|
||||
Pdfium.check_last_error('Failed to render page to bitmap')
|
||||
|
||||
unless form_handle.null?
|
||||
Pdfium.FPDF_FFLDraw(form_handle, bitmap_ptr, page_ptr, 0, 0, render_width, render_height, 0, flags)
|
||||
|
||||
Pdfium.check_last_error('Call to FPDF_FFLDraw completed (check for rendering issues if any)')
|
||||
end
|
||||
|
||||
buffer_ptr = Pdfium.FPDFBitmap_GetBuffer(bitmap_ptr)
|
||||
|
||||
@@ -56,7 +56,7 @@ module SearchEntries
|
||||
end
|
||||
|
||||
[sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword]
|
||||
elsif keyword.match?(/[^\p{L}\d&@._\-+]/) || keyword.match?(/\A['"].*['"]\z/)
|
||||
elsif keyword.match?(/[^\p{L}\d&@._\-]/) || keyword.match?(/\A['"].*['"]\z/)
|
||||
['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)]
|
||||
else
|
||||
keyword = TextUtils.transliterate(keyword.downcase).squish
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ module Submissions
|
||||
module_function
|
||||
|
||||
def search(current_user, submissions, keyword, search_values: false, search_template: false)
|
||||
if Docuseal.fulltext_search?(current_user)
|
||||
if Docuseal.fulltext_search?
|
||||
fulltext_search(current_user, submissions, keyword, search_template:)
|
||||
else
|
||||
plain_search(submissions, keyword, search_values:, search_template:)
|
||||
|
||||
@@ -195,6 +195,8 @@ module Submissions
|
||||
merged_submitter['uuid'] = new_uuid
|
||||
merged_submitter['name'] = name
|
||||
merged_submitter.delete('linked_to_uuid')
|
||||
|
||||
next merged_submitter
|
||||
end
|
||||
|
||||
submitter
|
||||
|
||||
@@ -256,7 +256,15 @@ module Submissions
|
||||
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
|
||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||
|
||||
image = load_vips_image(attachment, attachments_data_cache).autorot
|
||||
image =
|
||||
begin
|
||||
load_vips_image(attachment, attachments_data_cache).autorot
|
||||
rescue Vips::Error
|
||||
next unless attachment.content_type.starts_with?('image/')
|
||||
next if attachment.byte_size.zero?
|
||||
|
||||
raise
|
||||
end
|
||||
|
||||
reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence
|
||||
|
||||
|
||||
@@ -36,18 +36,18 @@ module Submissions
|
||||
last_submitter = submitters.max_by(&:completed_at)
|
||||
|
||||
if with_documents
|
||||
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
||||
json['documents'] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
||||
end
|
||||
|
||||
json[:status] = 'completed'
|
||||
json[:completed_at] = last_submitter.completed_at
|
||||
json['status'] = 'completed'
|
||||
json['completed_at'] = last_submitter.completed_at
|
||||
else
|
||||
json[:documents] = [] if with_documents
|
||||
json[:status] = build_status(submission, submitters)
|
||||
json[:completed_at] = nil
|
||||
json['documents'] = [] if with_documents
|
||||
json['status'] = build_status(submission, submitters)
|
||||
json['completed_at'] = nil
|
||||
end
|
||||
|
||||
json[:submitters] = serialized_submitters
|
||||
json['submitters'] = serialized_submitters
|
||||
|
||||
json
|
||||
end
|
||||
|
||||
+11
-10
@@ -14,7 +14,7 @@ module Submitters
|
||||
module_function
|
||||
|
||||
def search(current_user, submitters, keyword)
|
||||
if Docuseal.fulltext_search?(current_user)
|
||||
if Docuseal.fulltext_search?
|
||||
fulltext_search(current_user, submitters, keyword)
|
||||
else
|
||||
plain_search(submitters, keyword)
|
||||
@@ -35,11 +35,11 @@ module Submitters
|
||||
def fulltext_search_field(current_user, submitters, keyword, field_name)
|
||||
keyword = keyword.delete("\0")
|
||||
|
||||
return submitters if keyword.blank?
|
||||
return submitters.none if keyword.blank?
|
||||
|
||||
weight = FIELD_NAME_WEIGHTS[field_name]
|
||||
|
||||
return submitters if weight.blank?
|
||||
return submitters.none if weight.blank?
|
||||
|
||||
query =
|
||||
if keyword.match?(/\d/) && !keyword.match?(/\p{L}/)
|
||||
@@ -53,7 +53,7 @@ module Submitters
|
||||
end
|
||||
|
||||
[sql, number, weight, number.length > 1 ? number.delete_prefix('0') : number, weight]
|
||||
elsif keyword.match?(/[^\p{L}\d&@._\-+]/)
|
||||
elsif keyword.match?(/[^\p{L}\d&@._\-]/)
|
||||
terms = TextUtils.transliterate(keyword.downcase).split(/\b/).map(&:squish).compact_blank.uniq
|
||||
|
||||
if terms.size > 1
|
||||
@@ -65,12 +65,13 @@ module Submitters
|
||||
SearchEntries.build_weights_wildcard_tsquery(keyword, weight)
|
||||
end
|
||||
|
||||
submitters.where(
|
||||
id: SearchEntry.where(record_type: 'Submitter')
|
||||
.where(account_id: current_user.account_id)
|
||||
.where(*query)
|
||||
.select(:record_id)
|
||||
)
|
||||
submitter_ids = SearchEntry.where(record_type: 'Submitter')
|
||||
.where(account_id: current_user.account_id)
|
||||
.where(*query)
|
||||
.limit(500)
|
||||
.pluck(:record_id)
|
||||
|
||||
submitters.where(id: submitter_ids.first(100))
|
||||
end
|
||||
|
||||
def plain_search(submitters, keyword)
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ module Templates
|
||||
end
|
||||
|
||||
def search(current_user, templates, keyword)
|
||||
if Docuseal.fulltext_search?(current_user)
|
||||
if Docuseal.fulltext_search?
|
||||
fulltext_search(current_user, templates, keyword)
|
||||
else
|
||||
plain_search(templates, keyword)
|
||||
|
||||
@@ -53,6 +53,8 @@ module TimeUtils
|
||||
def format_date_string(string, format, locale)
|
||||
date = Date.parse(string.to_s)
|
||||
|
||||
format = format.upcase if format
|
||||
|
||||
format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT
|
||||
|
||||
i18n_format = format.sub(/D+/, DAY_FORMATS[format[/D+/]])
|
||||
|
||||
@@ -49,5 +49,18 @@ RSpec.describe 'Dashboard Page' do
|
||||
expect(page).to have_current_path(edit_template_path(Template.last), ignore_query: true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'searches be submitter email' do
|
||||
submission = create(:submission, :with_submitters, template: templates[0])
|
||||
submitter = submission.submitters.first
|
||||
|
||||
SearchEntries.reindex_all
|
||||
|
||||
visit root_path(q: submitter.email)
|
||||
|
||||
expect(page).to have_content('Templates not Found')
|
||||
expect(page).to have_content('Submissions')
|
||||
expect(page).to have_content(submitter.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user