mirror of
https://github.com/docusealco/docuseal
synced 2026-11-09 23:42:32 -05:00
Compare commits
10
Commits
4741e64178
...
1.7.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be4a0505bc | ||
|
|
bd853c6265 | ||
|
|
5a1efd0e27 | ||
|
|
71b8cc7172 | ||
|
|
126a36dbf3 | ||
|
|
90db64cc03 | ||
|
|
227a067c77 | ||
|
|
dc24df0d4c | ||
|
|
2598962c5b | ||
|
|
b4884e94f6 |
No files matched your search
@@ -80,7 +80,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
render json: build_create_json(submissions)
|
render json: build_create_json(submissions)
|
||||||
rescue Submitters::NormalizeValues::BaseError => e
|
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ module Api
|
|||||||
with_urls: true,
|
with_urls: true,
|
||||||
with_events: false,
|
with_events: false,
|
||||||
params:)
|
params:)
|
||||||
rescue Submitters::NormalizeValues::BaseError => e
|
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ module Api
|
|||||||
:name,
|
:name,
|
||||||
:external_id,
|
:external_id,
|
||||||
{
|
{
|
||||||
submitters: [%i[name uuid]],
|
submitters: [%i[name uuid is_requester linked_to_uuid email]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description,
|
:title, :description,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
.submitters.new(uuid: @template.submitters.first['uuid'])
|
.submitters.new(uuid: filter_undefined_submitters(@template).first['uuid'])
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -22,15 +22,17 @@ class StartFormController < ApplicationController
|
|||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||||
else
|
else
|
||||||
if @template.submitters.to_a.size > 2 && @submitter.new_record?
|
if filter_undefined_submitters(@template).size > 2 && @submitter.new_record?
|
||||||
@error_message = 'Not found'
|
@error_message = 'Not found'
|
||||||
|
|
||||||
return render :show
|
return render :show
|
||||||
end
|
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 @submitter.save
|
||||||
if is_new_record
|
if is_new_record
|
||||||
@@ -66,7 +68,7 @@ class StartFormController < ApplicationController
|
|||||||
(Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
|
(Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
|
||||||
|
|
||||||
submitter.assign_attributes(
|
submitter.assign_attributes(
|
||||||
uuid: template.submitters.first['uuid'],
|
uuid: filter_undefined_submitters(template).first['uuid'],
|
||||||
ip: request.remote_ip,
|
ip: request.remote_ip,
|
||||||
ua: request.user_agent,
|
ua: request.user_agent,
|
||||||
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||||
@@ -83,25 +85,18 @@ class StartFormController < ApplicationController
|
|||||||
submitter.submission ||= Submission.new(template:,
|
submitter.submission ||= Submission.new(template:,
|
||||||
account_id: template.account_id,
|
account_id: template.account_id,
|
||||||
template_submitters: template.submitters,
|
template_submitters: template.submitters,
|
||||||
|
submitters: [submitter],
|
||||||
source: :link)
|
source: :link)
|
||||||
|
|
||||||
maybe_assign_default_second_submitter(submitter.submission)
|
|
||||||
|
|
||||||
submitter.account_id = submitter.submission.account_id
|
submitter.account_id = submitter.submission.account_id
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_assign_default_second_submitter(submission)
|
def filter_undefined_submitters(template)
|
||||||
return unless submission.new_record?
|
template.submitters.select do |item|
|
||||||
return if submission.template.submitters.to_a.size != 2
|
item['linked_to_uuid'].blank? && item['is_requester'].blank? && item['email'].blank?
|
||||||
|
end
|
||||||
submission.submitters_order = 'preserved'
|
|
||||||
submission.submitters.new(
|
|
||||||
account_id: submission.account_id,
|
|
||||||
uuid: submission.template.submitters.second['uuid'],
|
|
||||||
email: submission.template.author.email
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class TemplatesController < ApplicationController
|
|||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
:name,
|
:name,
|
||||||
{ schema: [%i[attachment_uuid name]],
|
{ schema: [%i[attachment_uuid name]],
|
||||||
submitters: [%i[name uuid]],
|
submitters: [%i[name uuid is_requester linked_to_uuid email]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description,
|
:title, :description,
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# 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 linked_to_uuid email option]]
|
||||||
|
).fetch(:submitters, {}).values.filter_map do |s|
|
||||||
|
next if s[:uuid].blank?
|
||||||
|
|
||||||
|
if s[:is_requester] == '1'
|
||||||
|
s[:is_requester] = true
|
||||||
|
else
|
||||||
|
s.delete(:is_requester)
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
when /\Alinked_to_(.*)\z/
|
||||||
|
s[:linked_to_uuid] = ::Regexp.last_match(-1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -52,7 +52,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
def create_file_params_from_url
|
def create_file_params_from_url
|
||||||
tempfile = Tempfile.new
|
tempfile = Tempfile.new
|
||||||
tempfile.binmode
|
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
|
tempfile.rewind
|
||||||
|
|
||||||
file = ActionDispatch::Http::UploadedFile.new(
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
@@ -65,10 +65,4 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
{ files: [file] }
|
{ files: [file] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def conn
|
|
||||||
Faraday.new do |faraday|
|
|
||||||
faraday.response :follow_redirects
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -26,6 +26,8 @@ import EmailsTextarea from './elements/emails_textarea'
|
|||||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
import PasswordInput from './elements/password_input'
|
import PasswordInput from './elements/password_input'
|
||||||
import SearchInput from './elements/search_input'
|
import SearchInput from './elements/search_input'
|
||||||
|
import ToggleAttribute from './elements/toggle_attribute'
|
||||||
|
import LinkedInput from './elements/linked_input'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -89,9 +91,13 @@ safeRegisterElement('toggle-cookies', ToggleCookies)
|
|||||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||||
safeRegisterElement('password-input', PasswordInput)
|
safeRegisterElement('password-input', PasswordInput)
|
||||||
safeRegisterElement('search-input', SearchInput)
|
safeRegisterElement('search-input', SearchInput)
|
||||||
|
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
||||||
|
safeRegisterElement('linked-input', LinkedInput)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
document.addEventListener('turbo:submit-end', this.onSubmit)
|
||||||
|
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
this.appElem.classList.add('md:h-screen')
|
this.appElem.classList.add('md:h-screen')
|
||||||
@@ -114,12 +120,22 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
acceptFileTypes: this.dataset.acceptFileTypes
|
acceptFileTypes: this.dataset.acceptFileTypes
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app.mount(this.appElem)
|
this.component = this.app.mount(this.appElem)
|
||||||
|
|
||||||
this.appendChild(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 () {
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('turbo:submit-end', this.onSubmit)
|
||||||
|
|
||||||
this.app?.unmount()
|
this.app?.unmount()
|
||||||
this.appElem?.remove()
|
this.appElem?.remove()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
if (input && item[field]) {
|
if (input && item[field]) {
|
||||||
input.value = item[field]
|
input.value = item[field]
|
||||||
|
input.dispatchEvent(new CustomEvent('linked-input.update', { bubbles: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textarea && item[field]) {
|
if (textarea && item[field]) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ export default actionable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = (e) => {
|
onSubmit = (e) => {
|
||||||
if (e.detail.success) {
|
if (e.detail.success && e.detail?.formSubmission?.formElement?.dataset?.closeOnSubmit !== 'false') {
|
||||||
this.close()
|
this.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,7 +226,8 @@ export default {
|
|||||||
},
|
},
|
||||||
submitter: {
|
submitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
},
|
},
|
||||||
withSignatureId: {
|
withSignatureId: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
:values="values"
|
:values="values"
|
||||||
:field="field"
|
:field="field"
|
||||||
:area="area"
|
:area="area"
|
||||||
:submittable="true"
|
:submittable="submittable"
|
||||||
:field-index="fieldIndex"
|
:field-index="fieldIndex"
|
||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
@@ -58,6 +58,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
submittable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
submitter: {
|
submitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -13,6 +13,12 @@
|
|||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||||
/>
|
/>
|
||||||
|
<FieldAreas
|
||||||
|
:steps="readonlyConditionalFields.map((e) => [e])"
|
||||||
|
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = f.default_value; return acc }, {})"
|
||||||
|
:submitter="submitter"
|
||||||
|
:submittable="false"
|
||||||
|
/>
|
||||||
<FormulaFieldAreas
|
<FormulaFieldAreas
|
||||||
v-if="formulaFields.length"
|
v-if="formulaFields.length"
|
||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
@@ -802,6 +808,9 @@ export default {
|
|||||||
currentField () {
|
currentField () {
|
||||||
return this.currentStepFields[0]
|
return this.currentStepFields[0]
|
||||||
},
|
},
|
||||||
|
readonlyConditionalFields () {
|
||||||
|
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f))
|
||||||
|
},
|
||||||
stepFields () {
|
stepFields () {
|
||||||
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
||||||
const prevStep = acc[acc.length - 1]
|
const prevStep = acc[acc.length - 1]
|
||||||
@@ -933,12 +942,12 @@ export default {
|
|||||||
return acc && isEmpty(this.values[c.field_uuid])
|
return acc && isEmpty(this.values[c.field_uuid])
|
||||||
} else if (['not_empty', 'checked'].includes(c.action)) {
|
} else if (['not_empty', 'checked'].includes(c.action)) {
|
||||||
return acc && !isEmpty(this.values[c.field_uuid])
|
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 option = field.options.find((o) => o.uuid === c.value)
|
||||||
const values = [this.values[c.field_uuid]].flat()
|
const values = [this.values[c.field_uuid]].flat()
|
||||||
|
|
||||||
return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
|
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 option = field.options.find((o) => o.uuid === c.value)
|
||||||
const values = [this.values[c.field_uuid]].flat()
|
const values = [this.values[c.field_uuid]].flat()
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,17 @@
|
|||||||
<span class="whitespace-nowrap">Save and Preview</span>
|
<span class="whitespace-nowrap">Save and Preview</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
:href="`/templates/${template.id}/preferences`"
|
||||||
|
data-turbo-frame="modal"
|
||||||
|
class="flex space-x-2"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<IconAdjustments class="w-6 h-6 flex-shrink-0" />
|
||||||
|
<span class="whitespace-nowrap">Preferences</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -404,7 +415,7 @@ import Contenteditable from './contenteditable'
|
|||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import MobileFields from './mobile_fields'
|
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 { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { en as i18nEn } from './i18n'
|
import { en as i18nEn } from './i18n'
|
||||||
@@ -428,6 +439,7 @@ export default {
|
|||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconUsersPlus,
|
IconUsersPlus,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
|
IconAdjustments,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconDeviceFloppy
|
IconDeviceFloppy
|
||||||
},
|
},
|
||||||
@@ -770,6 +782,9 @@ export default {
|
|||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
},
|
||||||
t (key) {
|
t (key) {
|
||||||
return this.i18n[key] || i18nEn[key] || key
|
return this.i18n[key] || i18nEn[key] || key
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,14 +18,20 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="name">
|
<submitters-autocomplete data-field="name">
|
||||||
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '', dir: 'auto' %>
|
<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: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: (params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : '', dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
|
||||||
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
|
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
|
||||||
<submitters-autocomplete data-field="email">
|
<submitters-autocomplete data-field="email">
|
||||||
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Email (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.email : '' %>">
|
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
|
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Email (optional)" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>">
|
||||||
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Phone (optional)">
|
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
|
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Phone (optional)" id="detailed_phone_<%= item['uuid'] %>">
|
||||||
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
</submitter-item>
|
</submitter-item>
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="email">
|
<submitters-autocomplete data-field="email">
|
||||||
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Email', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.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: 'input input-sm input-bordered w-full', placeholder: 'Email', required: index.zero?, value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "email_#{item['uuid']}" %>
|
||||||
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</submitter-item>
|
</submitter-item>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -19,18 +19,24 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Phone', required: index.zero? %>
|
<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('Use internatioanl format: +1xxx...') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Phone', required: index.zero?, id: "phone_phone_#{item['uuid']}" %>
|
||||||
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<% if template.submitters.size > 1 %>
|
<% if template.submitters.size > 1 %>
|
||||||
<submitters-autocomplete data-field="name">
|
<submitters-autocomplete data-field="name">
|
||||||
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>" dir="auto">
|
<linked-input data-target-id="<%= "phone_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
|
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= (params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : '' %>" dir="auto" id="phone_name_<%= item['uuid'] %>">
|
||||||
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% if template.submitters.size == 1 %>
|
<% if template.submitters.size == 1 %>
|
||||||
<div class="form-control flex">
|
<div class="form-control flex">
|
||||||
<submitters-autocomplete data-field="name">
|
<submitters-autocomplete data-field="name">
|
||||||
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>" dir="auto">
|
<linked-input data-target-id="<%= "phone_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
|
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= (params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : '' %>" dir="auto" id="phone_name_<%= item['uuid'] %>">
|
||||||
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
<span>
|
<span>
|
||||||
Reason:
|
Reason:
|
||||||
<%= simple_format(submitter.submission_events.find_by(event_type: :decline_form).data['reason']) %>
|
<%= simple_format(h(submitter.submission_events.find_by(event_type: :decline_form).data['reason'])) %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<% next if field['redacted'] && field['submitter_uuid'] != @submitter.uuid %>
|
<% next if field['redacted'] && field['submitter_uuid'] != @submitter.uuid %>
|
||||||
<% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %>
|
<% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %>
|
||||||
|
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
|
||||||
|
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
|
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
|
<%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<p><%= t('hi_there') %>,</p>
|
<p><%= t('hi_there') %>,</p>
|
||||||
<p><%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.template.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %></p>
|
<p><%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.template.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %></p>
|
||||||
<%= simple_format(@submitter.submission_events.find_by(event_type: :decline_form).data['reason']) %>
|
<%= simple_format(h(@submitter.submission_events.find_by(event_type: :decline_form).data['reason'])) %>
|
||||||
<p><%= link_to t('view'), submission_url(@submitter.submission) %></p>
|
<p><%= link_to t('view'), submission_url(@submitter.submission) %></p>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<%= render 'shared/turbo_modal_large', title: 'Preferences', close_after_submit: false do %>
|
<%= render 'shared/turbo_modal_large', title: 'Preferences' do %>
|
||||||
<% show_api = Docuseal.multitenant? || current_account.testing? || !current_account.linked_account_account %>
|
<% show_api = Docuseal.multitenant? || current_account.testing? || !current_account.linked_account_account %>
|
||||||
|
<% show_recipients = @template.submitters.to_a.length > 1 %>
|
||||||
<% options = [%w[General general]] %>
|
<% options = [%w[General general]] %>
|
||||||
|
<% options << %w[Recipients recipients] if show_recipients %>
|
||||||
<% options << ['API and Embedding', 'api'] if show_api %>
|
<% options << ['API and Embedding', 'api'] if show_api %>
|
||||||
<% if options.size > 1 %>
|
<% if options.size > 1 %>
|
||||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-3 block">
|
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-3 block">
|
||||||
@@ -8,7 +10,7 @@
|
|||||||
<% options.each_with_index do |(label, value), index| %>
|
<% options.each_with_index do |(label, value), index| %>
|
||||||
<span>
|
<span>
|
||||||
<%= radio_button_tag 'option', value, value == 'general', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
<%= radio_button_tag 'option', value, value == 'general', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
||||||
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item md:w-48 peer-checked:btn-active normal-case <%= 'px-8 md:px-0' if value == 'general' %>">
|
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item peer-checked:btn-active normal-case <%= 'px-8 md:px-0' if value.in?(%w[general recipients]) %> <%= options.size > 2 ? 'md:w-44' : 'md:w-48' %>">
|
||||||
<%= label %>
|
<%= label %>
|
||||||
</label>
|
</label>
|
||||||
</span>
|
</span>
|
||||||
@@ -17,7 +19,7 @@
|
|||||||
</toggle-visible>
|
</toggle-visible>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div id="general" class="px-5 mb-4">
|
<div id="general" class="px-5 mb-4">
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' } do |f| %>
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
|
||||||
<toggle-on-submit data-element-id="bcc_saved_alert"></toggle-on-submit>
|
<toggle-on-submit data-element-id="bcc_saved_alert"></toggle-on-submit>
|
||||||
<%= f.fields_for :preferences, Struct.new(:bcc_completed).new(@template.preferences['bcc_completed']) do |ff| %>
|
<%= f.fields_for :preferences, Struct.new(:bcc_completed).new(@template.preferences['bcc_completed']) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -46,7 +48,7 @@
|
|||||||
Signature request email
|
Signature request email
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
||||||
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
|
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
|
||||||
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
|
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
Documents copy email
|
Documents copy email
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
||||||
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
|
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
|
||||||
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
|
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
|
||||||
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit).new(@template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false) do |ff| %>
|
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit).new(@template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false) do |ff| %>
|
||||||
@@ -127,7 +129,7 @@
|
|||||||
Completed notification email
|
Completed notification email
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
||||||
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
|
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
|
||||||
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
|
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
|
||||||
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body, :completed_notification_email_enabled, :completed_notification_email_attach_audit, :completed_notification_email_attach_documents).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body'], @template.preferences['completed_notification_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['completed_notification_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['completed_notification_email_attach_documents'] != false) do |ff| %>
|
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body, :completed_notification_email_enabled, :completed_notification_email_attach_audit, :completed_notification_email_attach_documents).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body'], @template.preferences['completed_notification_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['completed_notification_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['completed_notification_email_attach_documents'] != false) do |ff| %>
|
||||||
@@ -176,6 +178,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% if show_recipients %>
|
||||||
|
<div id="recipients" class="hidden mt-2 mb-4 px-5">
|
||||||
|
<%= form_for @template, url: template_recipients_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form } do |f| %>
|
||||||
|
<div class="space-y-3 divide-y">
|
||||||
|
<% @template.submitters.each_with_index do |submitter, index| %>
|
||||||
|
<div class="pt-3">
|
||||||
|
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'linked_to_uuid')), index: do |ff| %>
|
||||||
|
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : '')) %>
|
||||||
|
<%= ff.hidden_field :uuid %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
|
||||||
|
<% if @template.submitters.size == 2 %>
|
||||||
|
<%= ff.email_field :email, class: 'base-input', autocomplete: 'off', placeholder: 'Default Email', disabled: ff.object.is_requester, id: field_uuid = SecureRandom.uuid %>
|
||||||
|
<% else %>
|
||||||
|
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
|
||||||
|
<%= ff.select :option, [['Not specified', 'not_set'], ['Submission requester', 'is_requester'], ['Specified email', 'email'], *(@template.submitters - [submitter]).map { |e| ["Same as #{e['name']}", "linked_to_#{e['uuid']}"] }], {}, class: 'base-select mb-3' %>
|
||||||
|
</toggle-attribute>
|
||||||
|
<%= ff.email_field :email, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: 'Default Email', id: email_field_uuid %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% if @template.submitters.size == 2 %>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
|
||||||
|
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
|
||||||
|
</toggle-attribute>
|
||||||
|
<span class="select-none">
|
||||||
|
Submission requester
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control mt-6 pb-2">
|
||||||
|
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% if show_api %>
|
<% if show_api %>
|
||||||
<div id="api" class="hidden mt-2 mb-4 px-5">
|
<div id="api" class="hidden mt-2 mb-4 px-5">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ Rails.application.routes.draw do
|
|||||||
resource :form, only: %i[show], controller: 'templates_form_preview'
|
resource :form, only: %i[show], controller: 'templates_form_preview'
|
||||||
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
||||||
resource :preferences, only: %i[show create], controller: 'templates_preferences'
|
resource :preferences, only: %i[show create], controller: 'templates_preferences'
|
||||||
|
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
||||||
resources :submissions_export, only: %i[index new]
|
resources :submissions_export, only: %i[index new]
|
||||||
end
|
end
|
||||||
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'
|
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DownloadUtils
|
||||||
|
LOCALHOSTS = %w[0.0.0.0 127.0.0.1 localhost].freeze
|
||||||
|
|
||||||
|
UnableToDownload = Class.new(StandardError)
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(url)
|
||||||
|
uri = Addressable::URI.parse(url)
|
||||||
|
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
raise UnableToDownload, "Error loading: #{uri.display_uri}. Only HTTPS is allowed." if uri.scheme != 'https'
|
||||||
|
|
||||||
|
if uri.host.in?(LOCALHOSTS)
|
||||||
|
raise UnableToDownload, "Error loading: #{uri.display_uri}. Can't download from localhost."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resp = conn.get(uri.display_uri.to_s)
|
||||||
|
|
||||||
|
raise UnableToDownload, "Error loading: #{uri.display_uri}" if resp.status >= 400
|
||||||
|
|
||||||
|
resp
|
||||||
|
end
|
||||||
|
|
||||||
|
def conn
|
||||||
|
Faraday.new do |faraday|
|
||||||
|
faraday.response :follow_redirects
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submissions
|
||||||
|
module AssignDefinedSubmitters
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(submission)
|
||||||
|
submission.submitters_order = 'preserved'
|
||||||
|
|
||||||
|
assign_defined_submitters(submission)
|
||||||
|
assign_linked_submitters(submission)
|
||||||
|
|
||||||
|
if submission.submitters.size == 1 && submission.template.submitters.size == 2
|
||||||
|
submission.submitters.new(
|
||||||
|
account_id: submission.account_id,
|
||||||
|
uuid: submission.template.submitters.second['uuid'],
|
||||||
|
email: submission.template.author.email
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
submission
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_defined_submitters(submission)
|
||||||
|
submission.template.submitters.to_a.select do |item|
|
||||||
|
next if item['email'].blank? && item['is_requester'].blank?
|
||||||
|
|
||||||
|
submission.submitters.new(
|
||||||
|
account_id: submission.account_id,
|
||||||
|
uuid: item['uuid'],
|
||||||
|
email: item['is_requester'] ? submission.template.author.email : item['email']
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_linked_submitters(submission)
|
||||||
|
submission.template.submitters.to_a.select do |item|
|
||||||
|
next if item['linked_to_uuid'].blank?
|
||||||
|
|
||||||
|
email = submission.submitters.find { |s| s.uuid == item['linked_to_uuid'] }&.email
|
||||||
|
|
||||||
|
next unless email
|
||||||
|
|
||||||
|
submission.submitters.new(
|
||||||
|
account_id: submission.account_id,
|
||||||
|
uuid: item['uuid'],
|
||||||
|
email:
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -532,6 +532,8 @@ module Submissions
|
|||||||
begin
|
begin
|
||||||
pdf.acro_form.create_appearances(force: true) if pdf.acro_form && pdf.acro_form[:NeedAppearances]
|
pdf.acro_form.create_appearances(force: true) if pdf.acro_form && pdf.acro_form[:NeedAppearances]
|
||||||
pdf.acro_form&.flatten
|
pdf.acro_form&.flatten
|
||||||
|
rescue HexaPDF::MissingGlyphError
|
||||||
|
nil
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,13 +48,15 @@ module Submissions
|
|||||||
|
|
||||||
submitters.each do |submitter|
|
submitters.each do |submitter|
|
||||||
submitter.values.each_value do |value|
|
submitter.values.each_value do |value|
|
||||||
attachment = attachments_index[value]
|
Array.wrap(value).each do |v|
|
||||||
|
attachment = attachments_index[v]
|
||||||
|
|
||||||
next unless attachment
|
next unless attachment
|
||||||
|
|
||||||
attachment.record = submitter
|
attachment.record = submitter
|
||||||
|
|
||||||
attachment.save!
|
attachment.save!
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ module Submitters
|
|||||||
UnknownFieldName = Class.new(BaseError)
|
UnknownFieldName = Class.new(BaseError)
|
||||||
InvalidDefaultValue = Class.new(BaseError)
|
InvalidDefaultValue = Class.new(BaseError)
|
||||||
UnknownSubmitterName = Class.new(BaseError)
|
UnknownSubmitterName = Class.new(BaseError)
|
||||||
UnableToDownload = Class.new(BaseError)
|
|
||||||
|
|
||||||
TRUE_VALUES = ['1', 'true', true, 'TRUE', 'True', 'yes', 'YES', 'Yes'].freeze
|
TRUE_VALUES = ['1', 'true', true, 'TRUE', 'True', 'yes', 'YES', 'Yes'].freeze
|
||||||
FALSE_VALUES = ['0', 'false', false, 'FALSE', 'False', 'no', 'NO', 'No'].freeze
|
FALSE_VALUES = ['0', 'false', false, 'FALSE', 'False', 'no', 'NO', 'No'].freeze
|
||||||
@@ -137,7 +136,7 @@ module Submitters
|
|||||||
elsif type.in?(%w[signature initials]) && value.length < 60
|
elsif type.in?(%w[signature initials]) && value.length < 60
|
||||||
find_or_create_blob_from_text(account, value, type)
|
find_or_create_blob_from_text(account, value, type)
|
||||||
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
|
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
|
||||||
Marcel::MimeType.for(data).include?('image')
|
Marcel::MimeType.for(data).exclude?('octet-stream')
|
||||||
find_or_create_blob_from_base64(account, data, type)
|
find_or_create_blob_from_base64(account, data, type)
|
||||||
else
|
else
|
||||||
raise InvalidDefaultValue, "Invalid value, url, base64 or text < 60 chars is expected: #{value.first(200)}..."
|
raise InvalidDefaultValue, "Invalid value, url, base64 or text < 60 chars is expected: #{value.first(200)}..."
|
||||||
@@ -185,12 +184,7 @@ module Submitters
|
|||||||
|
|
||||||
return blob if blob
|
return blob if blob
|
||||||
|
|
||||||
uri = Addressable::URI.parse(url)
|
data = DownloadUtils.call(url).body
|
||||||
resp = conn.get(uri.display_uri.to_s)
|
|
||||||
|
|
||||||
raise UnableToDownload, "Error loading: #{uri.display_uri}" if resp.status >= 400
|
|
||||||
|
|
||||||
data = resp.body
|
|
||||||
|
|
||||||
checksum = Digest::MD5.base64digest(data)
|
checksum = Digest::MD5.base64digest(data)
|
||||||
|
|
||||||
@@ -215,11 +209,5 @@ module Submitters
|
|||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def conn
|
|
||||||
Faraday.new do |faraday|
|
|
||||||
faraday.response :follow_redirects
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -15,7 +15,7 @@ module Templates
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(attachment, data, extract_fields: false)
|
def call(attachment, data, extract_fields: false, image_quality: nil)
|
||||||
if attachment.content_type == PDF_CONTENT_TYPE
|
if attachment.content_type == PDF_CONTENT_TYPE
|
||||||
if extract_fields && data.size < MAX_FLATTEN_FILE_SIZE
|
if extract_fields && data.size < MAX_FLATTEN_FILE_SIZE
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
@@ -23,23 +23,23 @@ module Templates
|
|||||||
fields = Templates::FindAcroFields.call(pdf, attachment)
|
fields = Templates::FindAcroFields.call(pdf, attachment)
|
||||||
end
|
end
|
||||||
|
|
||||||
generate_pdf_preview_images(attachment, data, pdf)
|
generate_pdf_preview_images(attachment, data, pdf, image_quality:)
|
||||||
|
|
||||||
attachment.metadata['pdf']['fields'] = fields if fields
|
attachment.metadata['pdf']['fields'] = fields if fields
|
||||||
elsif attachment.image?
|
elsif attachment.image?
|
||||||
generate_preview_image(attachment, data)
|
generate_preview_image(attachment, data, image_quality:)
|
||||||
end
|
end
|
||||||
|
|
||||||
attachment
|
attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_preview_image(attachment, data)
|
def generate_preview_image(attachment, data, image_quality: nil)
|
||||||
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
||||||
|
|
||||||
image = Vips::Image.new_from_buffer(data, '')
|
image = Vips::Image.new_from_buffer(data, '')
|
||||||
image = image.autorot.resize(MAX_WIDTH / image.width.to_f)
|
image = image.autorot.resize(MAX_WIDTH / image.width.to_f)
|
||||||
|
|
||||||
io = StringIO.new(image.write_to_buffer(FORMAT, Q: Q, interlace: true))
|
io = StringIO.new(image.write_to_buffer(FORMAT, Q: image_quality || Q, interlace: true))
|
||||||
|
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
blob: ActiveStorage::Blob.create_and_upload!(
|
blob: ActiveStorage::Blob.create_and_upload!(
|
||||||
@@ -51,7 +51,7 @@ module Templates
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_pdf_preview_images(attachment, data, pdf = nil)
|
def generate_pdf_preview_images(attachment, data, pdf = nil, image_quality: nil)
|
||||||
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
||||||
|
|
||||||
pdf ||= HexaPDF::Document.new(io: StringIO.new(data))
|
pdf ||= HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
@@ -70,7 +70,7 @@ module Templates
|
|||||||
page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number)
|
page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number)
|
||||||
page = page.resize(MAX_WIDTH / page.width.to_f)
|
page = page.resize(MAX_WIDTH / page.width.to_f)
|
||||||
|
|
||||||
io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true))
|
io = StringIO.new(page.write_to_buffer(FORMAT, Q: image_quality || Q, interlace: true))
|
||||||
|
|
||||||
ApplicationRecord.no_touching do
|
ApplicationRecord.no_touching do
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
@@ -97,11 +97,9 @@ module Templates
|
|||||||
pdf.write(io, incremental: false, validate: false)
|
pdf.write(io, incremental: false, validate: false)
|
||||||
|
|
||||||
io.string
|
io.string
|
||||||
rescue StandardError => e
|
rescue StandardError
|
||||||
raise if Rails.env.development?
|
raise if Rails.env.development?
|
||||||
|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
|
||||||
|
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -117,11 +115,11 @@ module Templates
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_pdf_preview_from_file(attachment, file_path, page_number)
|
def generate_pdf_preview_from_file(attachment, file_path, page_number, image_quality: nil)
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
command = [
|
command = [
|
||||||
'pdftocairo', '-jpeg', '-jpegopt', "progressive=y,quality=#{Q},optimize=y",
|
'pdftocairo', '-jpeg', '-jpegopt', "progressive=y,quality=#{image_quality || Q},optimize=y",
|
||||||
'-scale-to-x', MAX_WIDTH, '-scale-to-y', '-1',
|
'-scale-to-x', MAX_WIDTH, '-scale-to-y', '-1',
|
||||||
'-r', DPI, '-f', page_number + 1, '-l', page_number + 1,
|
'-r', DPI, '-f', page_number + 1, '-l', page_number + 1,
|
||||||
'-singlefile', Shellwords.escape(file_path), '-'
|
'-singlefile', Shellwords.escape(file_path), '-'
|
||||||
|
|||||||
Reference in new issue
Block a user