Compare commits

...

24 Commits

Author SHA1 Message Date
Pete Matsyburka 202943f58d fix pidfile issue 2024-03-26 01:17:55 +02:00
Pete Matsyburka 6dd15b2091 use letter page size for us 2024-03-25 20:20:38 +02:00
Pete Matsyburka 1c55e4f18b always display default value 2024-03-25 19:16:12 +02:00
Pete Matsyburka 6cc36b1728 allow to update template roles 2024-03-25 17:37:46 +02:00
Pete Matsyburka 37751cd5c1 update template submitters 2024-03-25 16:34:45 +02:00
Pete Matsyburka 80d882cd20 add reply to param 2024-03-24 14:09:28 +02:00
Pete Matsyburka 8bef75e570 move autocomplete controllers 2024-03-23 23:52:50 +02:00
Pete Matsyburka 077eab7005 refactor templates controller 2024-03-23 23:52:49 +02:00
Pete Matsyburka 715c70d422 rename to users 2024-03-23 21:23:59 +02:00
Pete Matsyburka a3c23f9d53 update readme 2024-03-22 17:35:52 +02:00
Pete Matsyburka c1e67a8af0 add submission id var 2024-03-22 13:11:33 +02:00
Pete Matsyburka 2fdc294293 fix annot 2024-03-21 16:25:49 +02:00
Pete Matsyburka 1f49ba069e disable puma pidfile 2024-03-21 15:03:50 +02:00
Pete Matsyburka 43ef0fc0d0 use FORCE_SSL=false 2024-03-21 14:12:11 +02:00
Pete Matsyburka a1a920e536 allow to remove default value 2024-03-21 00:04:53 +02:00
Pete Matsyburka dec33b4331 add extra small field font size 2024-03-20 16:52:53 +02:00
Pete Matsyburka 432f290633 assign template fields on completed via API 2024-03-20 00:13:16 +02:00
Pete Matsyburka cf98616a27 remove log 2024-03-19 13:57:21 +02:00
Pete Matsyburka 1a720cf1ad allow to clone template from testing 2024-03-19 01:44:16 +02:00
Pete Matsyburka 557c6c34f2 search default fields 2024-03-19 01:21:44 +02:00
Pete Matsyburka 943c76505d remove api error log 2024-03-18 23:18:35 +02:00
Pete Matsyburka 66eb83e6a7 allow to search on value 2024-03-18 20:16:26 +02:00
Pete Matsyburka 0844427357 add rollbar queue 2024-03-18 19:35:19 +02:00
Pete Matsyburka 72e9d9ea2d update readme 2024-03-17 23:16:27 +02:00
42 changed files with 284 additions and 175 deletions
+1 -2
View File
@@ -36,7 +36,6 @@ FROM ruby:3.2.2-alpine3.18 as app
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
ENV PIDFILE=/dev/null
WORKDIR /app
@@ -67,4 +66,4 @@ WORKDIR /data/docuseal
ENV WORKDIR=/data/docuseal
EXPOSE 3000
CMD ["/app/bin/rails", "server"]
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
+9 -5
View File
@@ -51,11 +51,12 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- User roles
- Automated reminders
- Invitation and identify verification via SMS
- Conditional fields and formulas
- SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- [Learn more](https://www.docuseal.co/pricing)
## Deploy
@@ -65,9 +66,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|**DigitalOcean**|**Render**|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|**Koyeb**|**Elestio**|
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | [<img alt="Deploy on Elestio" src="https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png">](https://dash.elest.io/deploy?soft=DocuSeal&id=339) |
#### Docker
@@ -100,3 +98,9 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
Unless otherwise noted, all files © 2023 DocuSeal LLC.
## Tools
- [Signature Maker](https://www.docuseal.co/online-signature)
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
@@ -27,8 +27,6 @@ module Api
if Rails.env.production?
rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :forbidden
end
@@ -161,11 +161,11 @@ module Api
def submissions_params
permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to,
{
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id,
:completed, :phone, :application_key, :external_id, :reply_to,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
@@ -1,35 +0,0 @@
# frozen_string_literal: true
module Api
class SubmittersAutocompleteController < ApiBaseController
load_and_authorize_resource :submitter, parent: false
SELECT_COLUMNS = %w[email phone name].freeze
LIMIT = 100
def index
submitters = search_submitters(@submitters)
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs
end
private
def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field])
column = Submitter.arel_table[params[:field].to_sym]
term = "#{params[:q].downcase}%"
submitters.where(column.matches(term, false, true))
else
Submitters.search(submitters, params[:q])
end
end
end
end
@@ -1,16 +0,0 @@
# frozen_string_literal: true
module Api
class TemplateFoldersAutocompleteController < ApiBaseController
load_and_authorize_resource :template_folder, parent: false
LIMIT = 100
def index
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
render json: template_folders.as_json(only: %i[name archived_at])
end
end
end
+10 -16
View File
@@ -49,6 +49,14 @@ module Api
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
end
Array.wrap(params[:roles].presence || params.dig(:template, :roles).presence).each_with_index do |role, index|
if (item = @template.submitters[index])
item['name'] = role
else
@template.submitters << { 'name' => role, 'uuid' => SecureRandom.uuid }
end
end
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
if archived.in?([true, false])
@@ -83,24 +91,10 @@ module Api
end
def template_params
permit_params = [
:name,
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
]
if params.key?(:template)
params.require(:template).permit(*permit_params)
params.require(:template).permit(:name, submitters: [%i[name uuid]])
else
params.permit(*permit_params)
params.permit(:name, submitters: [%i[name uuid]])
end
end
end
@@ -1,29 +0,0 @@
# frozen_string_literal: true
module Api
class TemplatesDocumentsController < ApiBaseController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
documents = Templates::CreateAttachments.call(@template, params)
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
end
render json: {
schema:,
documents: documents.as_json(
methods: %i[metadata signed_uuid],
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end
end
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class SubmittersAutocompleteController < ApplicationController
load_and_authorize_resource :submitter, parent: false
SELECT_COLUMNS = %w[email phone name].freeze
LIMIT = 100
def index
submitters = search_submitters(@submitters)
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs
end
private
def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field])
column = Submitter.arel_table[params[:field].to_sym]
term = "#{params[:q].downcase}%"
submitters.where(column.matches(term, false, true))
else
Submitters.search(submitters, params[:q])
end
end
end
@@ -0,0 +1,27 @@
# frozen_string_literal: true
class TemplateDocumentsController < ApplicationController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
documents = Templates::CreateAttachments.call(@template, params)
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
end
render json: {
schema:,
documents: documents.as_json(
methods: %i[metadata signed_uuid],
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
class TemplateFoldersAutocompleteController < ApplicationController
load_and_authorize_resource :template_folder, parent: false
LIMIT = 100
def index
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
render json: template_folders.as_json(only: %i[name archived_at])
end
end
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index
@submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q])
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
rescue ActiveRecord::RecordNotFound
+23 -5
View File
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
def show
submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q])
submissions = Submissions.search(submissions, params[:q], search_values: true)
@base_submissions = submissions
@@ -67,6 +67,12 @@ class TemplatesController < ApplicationController
end
end
def update
@template.update!(template_params)
head :ok
end
def destroy
notice =
if params[:permanently].present?
@@ -86,6 +92,22 @@ class TemplatesController < ApplicationController
private
def template_params
params.require(:template).permit(
:name,
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
)
end
def authorized_clone_account_id?(account_id)
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
end
@@ -98,10 +120,6 @@ class TemplatesController < ApplicationController
end
end
def template_params
params.require(:template).permit(:name)
end
def load_base_template
return if params[:base_template_id].blank?
+1
View File
@@ -91,6 +91,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withPayment: this.dataset.withPayment === 'true',
withFormula: this.dataset.withFormula === 'true',
withConditions: this.dataset.withConditions === 'true',
@@ -20,7 +20,7 @@ export default class extends HTMLElement {
fetch = (text, resolve) => {
const queryParams = new URLSearchParams({ q: text })
fetch('/api/template_folders_autocomplete?' + queryParams).then(async (resp) => {
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
const items = await resp.json()
resolve(items)
@@ -40,7 +40,7 @@ export default class extends HTMLElement {
if (q) {
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
this.currentFetch ||= fetch('/api/submitters_autocomplete?' + queryParams)
this.currentFetch ||= fetch('/submitters_autocomplete?' + queryParams)
this.currentFetch.then(async (resp) => {
const items = await resp.json()
+1
View File
@@ -17,6 +17,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
attribution: this.dataset.attribution !== 'false',
withConfetti: this.dataset.withConfetti !== 'false',
withTypedSignature: this.dataset.withTypedSignature !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
values: reactive(JSON.parse(this.dataset.values)),
completedButton: JSON.parse(this.dataset.completedButton),
completedRedirectUrl: this.dataset.completedRedirectUrl,
+5 -3
View File
@@ -611,6 +611,11 @@ export default {
required: false,
default: () => ({})
},
authenticityToken: {
type: String,
required: false,
default: ''
},
i18n: {
type: Object,
required: false,
@@ -658,9 +663,6 @@ export default {
queryParams () {
return new URLSearchParams(window.location.search)
},
authenticityToken () {
return document.querySelector('meta[name="csrf-token"]')?.content
},
submitterSlug () {
return this.submitter.slug
},
+1 -1
View File
@@ -42,7 +42,7 @@
v-model="field.submitter_uuid"
class="border-r"
:compact="true"
:editable="editable && !defaultField"
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
:allow-add-new="!defaultSubmitters.length"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
:submitters="template.submitters"
+11 -2
View File
@@ -444,6 +444,11 @@ export default {
required: false,
default: true
},
authenticityToken: {
type: String,
required: false,
default: ''
},
withDocumentsList: {
type: Boolean,
required: false,
@@ -1023,7 +1028,11 @@ export default {
baseFetch (path, options = {}) {
return fetch(this.baseUrl + path, {
...options,
headers: { ...this.fetchOptions.headers, ...options.headers }
headers: {
'X-CSRF-Token': this.authenticityToken,
...this.fetchOptions.headers,
...options.headers
}
})
},
save ({ force } = { force: false }) {
@@ -1039,7 +1048,7 @@ export default {
this.pushUndo()
return this.baseFetch(`/api/templates/${this.template.id}`, {
return this.baseFetch(`/templates/${this.template.id}`, {
method: 'PUT',
body: JSON.stringify({
template: {
+67 -21
View File
@@ -36,32 +36,63 @@
</div>
<div v-if="submitterDefaultFields.length">
<hr class="mb-2">
<template
v-for="field in submitterDefaultFields"
:key="field.name"
<template v-if="isShowFieldSearch">
<input
v-model="defaultFieldsSearch"
:placeholder="t('search_field')"
class="input input-ghost input-xs px-0 text-base mb-2 !outline-0 !rounded bg-transparent w-full"
>
<hr class="mb-2">
</template>
<div
class="overflow-auto relative"
:style="{
maxHeight: isShowFieldSearch ? '210px' : '',
minHeight: isShowFieldSearch ? '210px' : ''
}"
>
<div
:style="{ backgroundColor: backgroundColor }"
draggable="true"
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
@dragstart="onDragstart({ type: 'text', ...field })"
@dragend="$emit('drag-end')"
v-if="!filteredSubmitterDefaultFields.length && defaultFieldsSearch"
class="top-0 bottom-0 text-center absolute flex items-center justify-center w-full flex-col"
>
<div class="flex items-center justify-between relative cursor-grab">
<div class="flex items-center p-1 space-x-1">
<IconDrag />
<FieldType
:model-value="field.type || 'text'"
:editable="false"
:button-width="20"
/>
<span class="block pl-0.5">
{{ field.name }}
</span>
<div>
{{ t('field_not_found') }}
</div>
<a
href="#"
class="link"
@click.prevent="defaultFieldsSearch = ''"
>
{{ t('clear') }}
</a>
</div>
<template
v-for="field in filteredSubmitterDefaultFields"
:key="field.name"
>
<div
:style="{ backgroundColor: backgroundColor }"
draggable="true"
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
@dragstart="onDragstart({ type: 'text', ...field })"
@dragend="$emit('drag-end')"
>
<div class="flex items-center justify-between relative cursor-grab">
<div class="flex items-center p-1 space-x-1">
<IconDrag />
<FieldType
:model-value="field.type || 'text'"
:editable="false"
:button-width="20"
/>
<span class="block pl-0.5">
{{ field.name }}
</span>
</div>
</div>
</div>
</div>
</template>
</template>
</div>
</div>
<div
v-if="editable && !onlyDefinedFields"
@@ -205,9 +236,17 @@ export default {
}
},
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
data () {
return {
defaultFieldsSearch: ''
}
},
computed: {
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons,
isShowFieldSearch () {
return this.submitterDefaultFields.length > 15
},
fieldIconsSorted () {
if (this.fieldTypes.length) {
return this.fieldTypes.reduce((acc, type) => {
@@ -226,6 +265,13 @@ export default {
return this.defaultFields.filter((f) => {
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
})
},
filteredSubmitterDefaultFields () {
if (this.defaultFieldsSearch) {
return this.submitterDefaultFields.filter((f) => f.name.toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
} else {
return this.submitterDefaultFields
}
}
},
methods: {
+3
View File
@@ -1,4 +1,7 @@
const en = {
search_field: 'Search field',
field_not_found: 'Field not found',
clear: 'Clear',
align: 'Align',
left: 'Left',
right: 'Right',
+4 -4
View File
@@ -119,7 +119,7 @@ export default {
this.isProcessing = true
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
this.baseFetch(`/templates/${this.templateId}/documents`, {
method: 'POST',
body: JSON.stringify({ blobs }),
headers: { 'Content-Type': 'application/json' }
@@ -132,7 +132,7 @@ export default {
} else if (resp.status === 422) {
resp.json().then((data) => {
if (data.error === 'PDF encrypted') {
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
this.baseFetch(`/templates/${this.templateId}/documents`, {
method: 'POST',
body: JSON.stringify({ blobs, password: prompt('Enter PDF password') }),
headers: { 'Content-Type': 'application/json' }
@@ -151,7 +151,7 @@ export default {
this.isProcessing = false
})
} else {
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
this.baseFetch(`/templates/${this.templateId}/documents`, {
method: 'POST',
body: new FormData(this.$refs.form)
}).then((resp) => {
@@ -167,7 +167,7 @@ export default {
formData.append('password', prompt('Enter PDF password'))
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
this.baseFetch(`/templates/${this.templateId}/documents`, {
method: 'POST',
body: formData
}).then(async (resp) => {
+5 -1
View File
@@ -30,7 +30,8 @@ class SubmitterMailer < ApplicationMailer
to: @submitter.friendly_name,
from: from_address_for_submitter(submitter),
subject:,
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
reply_to: submitter.preferences['reply_to'].presence ||
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
)
end
@@ -84,6 +85,9 @@ class SubmitterMailer < ApplicationMailer
mail(from: from_address_for_submitter(submitter),
to: to || @submitter.friendly_name,
reply_to: @submitter.preferences['reply_to'].presence ||
(@submitter.submission.created_by_user ||
@submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@'),
subject:)
end
@@ -5,6 +5,10 @@
if (this.field.scrollHeight > this.field.clientHeight) {
this.field.classList.remove('text-[1.5vw]', 'lg:text-base');
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
if (this.field.scrollHeight > this.field.clientHeight) {
this.field.classList.add('text-[0.8vw]', 'lg:text-[0.50rem]');
}
}
}
get field() {
+1 -1
View File
@@ -39,7 +39,7 @@
<% end %>
<% if can?(:read, User) %>
<li>
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
<%= link_to 'Users', settings_users_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% unless Docuseal.multitenant? %>
+1 -1
View File
@@ -21,7 +21,7 @@
<%= render 'submissions/annotation', annot: %>
<% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']].presence || (field['readonly'] ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %>
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
<% next if field.dig('preferences', 'formula') && field['submitter_uuid'] == @submitter.uuid %>
+5 -5
View File
@@ -3,6 +3,11 @@
<% if @base_template %>
<%= hidden_field_tag :base_template_id, @base_template.id %>
<% end %>
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.account_linked_accounts.exists? %>
<div class="form-control -mb-2 mt-2">
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
</div>
<% end %>
<div class="form-control mt-6">
<%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input', dir: 'auto' %>
</div>
@@ -17,11 +22,6 @@
</a>
</folder-autocomplete>
</div>
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.account_linked_accounts.exists?(account_type: :linked) %>
<div class="form-control mb-4">
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts.where.not(id: true_user.account.testing_accounts)].map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
</div>
<% end %>
<div class="form-control">
<%= f.button button_title(title: @base_template ? 'Submit' : 'Create', disabled_with: 'Creating'), class: 'base-button' %>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %>
<div class="md:flex-grow">
<div class="flex justify-between mb-4">
<h1 class="text-4xl font-bold">Team</h1>
<h1 class="text-4xl font-bold">Users</h1>
<div class="flex items-center space-x-4">
<% if can?(:create, User.new(account: current_account)) %>
<%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2', data: { turbo_frame: 'modal' } do %>
+1 -1
View File
@@ -60,7 +60,7 @@ Rails.application.configure do
# config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = ENV['FORCE_SSL'].present?
config.force_ssl = ENV['FORCE_SSL'].present? && ENV['FORCE_SSL'] != 'false'
# Include generic and useful information about system operation, but avoid logging too much
# information to avoid inadvertent exposure of personally identifiable information (PII).
+1 -1
View File
@@ -24,7 +24,7 @@ port ENV.fetch('PORT', 3000)
environment ENV.fetch('RAILS_ENV', 'development')
# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid")
@options[:pidfile] = false
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
+4 -4
View File
@@ -24,8 +24,6 @@ Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
resources :attachments, only: %i[create]
resources :submitters_autocomplete, only: %i[index]
resources :template_folders_autocomplete, only: %i[index]
resources :submitter_email_clicks, only: %i[create]
resources :submitter_form_views, only: %i[create]
resources :submitters, only: %i[index show update]
@@ -37,7 +35,6 @@ Rails.application.routes.draw do
resources :templates, only: %i[update show index destroy] do
resources :clone, only: %i[create], controller: 'templates_clone'
resources :submissions, only: %i[index create]
resources :documents, only: %i[create], controller: 'templates_documents'
end
end
@@ -58,6 +55,8 @@ Rails.application.routes.draw do
resources :upgrade, only: %i[index], controller: 'console_redirect'
resource :testing_account, only: %i[show destroy]
resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index]
resources :template_folders_autocomplete, only: %i[index]
resource :templates_upload, only: %i[create]
authenticated do
resource :templates_upload, only: %i[show], path: 'new'
@@ -65,7 +64,8 @@ Rails.application.routes.draw do
resources :templates_archived, only: %i[index], path: 'archived'
resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
resources :template_sharings_testing, only: %i[create]
resources :templates, only: %i[new create edit show destroy] do
resources :templates, only: %i[new create edit update show destroy] do
resources :documents, only: %i[create], controller: 'template_documents'
resources :restore, only: %i[create], controller: 'templates_restore'
resources :archived, only: %i[index], controller: 'templates_archived_submissions'
resources :submissions, only: %i[new create]
+1
View File
@@ -2,6 +2,7 @@ queues:
- [default, 1]
- [mailers, 1]
- [recurrent, 1]
- [rollbar, 1]
production:
:concurrency: 15
@@ -42,6 +42,7 @@ module Params
type(params, :order, String)
type(params, :completed_redirect_url, String)
type(params, :bcc_completed, String)
type(params, :reply_to, String)
type(params, :message, Hash)
type(params, :submitters, Array)
@@ -69,6 +70,7 @@ module Params
required(submitter_params, %i[email phone name])
type(submitter_params, :name, String)
type(submitter_params, :reply_to, String)
type(submitter_params, :email, String)
format(submitter_params, :email, /@/, message: 'email is invalid')
type(submitter_params, :phone, String)
+3 -1
View File
@@ -7,7 +7,7 @@ module Submissions
module_function
def search(submissions, keyword)
def search(submissions, keyword, search_values: false)
return submissions if keyword.blank?
term = "%#{keyword.downcase}%"
@@ -18,6 +18,8 @@ module Submissions
.or(arel_table[:phone].matches(term))
.or(arel_table[:name].lower.matches(term))
arel = arel.or(Arel::Table.new(:submitters)[:values].matches(term)) if search_values
submissions.joins(:submitters).where(arel).distinct
end
+16 -11
View File
@@ -52,7 +52,8 @@ module Submissions
process_fields_param(submitter_attrs[:fields], template_fields, submitter_uuid)
end
if template_fields != submission.template.fields
if template_fields != (submission.template_fields || submission.template.fields) ||
submitters_attrs.any? { |e| e[:completed].present? }
submission.template_fields = template_fields
submission.template_schema = submission.template.schema
end
@@ -80,11 +81,15 @@ module Submissions
next if f['type'].in?(%w[signature image initials file])
next if f['submitter_uuid'] != submitter_uuid
next unless values.key?(f['uuid'])
value = values[f['uuid']]
next if value.blank?
f['default_value'] = value
if value.present?
f['default_value'] = value
else
f.delete('default_value')
end
end
end
@@ -110,8 +115,12 @@ module Submissions
field['readonly'] = attrs['readonly'] if attrs.key?('readonly')
field['required'] = attrs['required'] if attrs.key?('required')
if attrs['default_value'].present? && !field['type'].in?(%w[signature image initials file])
field['default_value'] = Submitters::NormalizeValues.normalize_value(field, attrs['default_value'])
if attrs.key?('default_value') && !field['type'].in?(%w[signature image initials file])
if attrs['default_value'].present?
field['default_value'] = Submitters::NormalizeValues.normalize_value(field, attrs['default_value'])
else
field.delete('default_value')
end
end
return field if attrs['validation_pattern'].blank?
@@ -128,10 +137,6 @@ module Submissions
uuid = attrs[:uuid].presence
uuid ||= template.submitters.find { |e| e['name'].to_s.casecmp(attrs[:role].to_s).zero? }&.dig('uuid')
if attrs[:role].present? && uuid.blank? && defined?(Rollbar)
Rollbar.error("Role doesn't existng: #{attrs[:role]}, #{template.id}")
end
uuid || template.submitters[index]&.dig('uuid')
end
@@ -144,7 +149,7 @@ module Submissions
phone: attrs[:phone].to_s.gsub(/[^0-9+]/, ''),
name: attrs[:name],
external_id: attrs[:external_id].presence || attrs[:application_key],
completed_at: attrs[:completed] ? Time.current : nil,
completed_at: attrs[:completed].present? ? Time.current : nil,
sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil,
values: attrs[:values] || {},
metadata: attrs[:metadata] || {},
+9 -1
View File
@@ -30,6 +30,8 @@ module Submissions
RTL_REGEXP = TextUtils::RTL_REGEXP
MAX_IMAGE_HEIGHT = 100
US_TIMEZONES = %w[EST CST MST PST HST AKDT].freeze
module_function
# rubocop:disable Metrics
@@ -38,6 +40,12 @@ module Submissions
pkcs = Accounts.load_signing_pkcs(account)
tsa_url = Accounts.load_timeserver_url(account)
verify_url = Rails.application.routes.url_helpers.settings_esign_url(**Docuseal.default_url_options)
page_size =
if TimeUtils.timezone_abbr(account.timezone, Time.current.beginning_of_year).in?(US_TIMEZONES)
:Letter
else
:A4
end
composer = HexaPDF::Composer.new(skip_page_creation: true)
composer.document.fonts.add(FONT_BOLD_NAME, variant: :bold)
@@ -53,7 +61,7 @@ module Submissions
height: 1
)
composer.page_style(:default, page_size: :A4) do |canvas, style|
composer.page_style(:default, page_size:) do |canvas, style|
box = canvas.context.box(:media)
canvas.save_graphics_state do
canvas.fill_color('FAF7F5')
@@ -209,6 +209,16 @@ module Submissions
box_height = lines.sum(&:height)
end
if box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value,
font: pdf.fonts.add(FONT_NAME),
font_size: (font_size / 1.9).to_i)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
box_height = lines.sum(&:height)
end
height_diff = [0, box_height - (area['h'] * height)].max
right_align_x_adjustment =
+1 -1
View File
@@ -22,7 +22,7 @@ module Submissions
default_values = submitter_params[:values] || {}
submitter_params[:fields]&.each do |f|
default_values[f[:name]] = f[:default_value] if f[:default_value].present?
default_values[f[:name]] = f[:default_value] if f.key?(:default_value)
end
return submitter_params if default_values.blank?
+1
View File
@@ -61,6 +61,7 @@ module Submitters
preferences['send_email'] = params['send_email'].in?(TRUE_VALUES) if params.key?('send_email')
preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms')
preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed')
preferences['reply_to'] = params['reply_to'] if params.key?('reply_to')
preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url')
preferences
+2
View File
@@ -131,6 +131,8 @@ module Submitters
value.to_s.gsub(VARIABLE_REGEXP) do |e|
case key = ::Regexp.last_match(1)
when 'id'
attrs['submission_id']
when 'time'
if with_time
I18n.l(Time.current.in_time_zone(template.account.timezone), format: :long, locale: template.account.locale)
+1
View File
@@ -9,6 +9,7 @@ module Templates
pdf.pages.flat_map.with_index do |page, index|
(page[:Annots] || []).filter_map do |annot|
next if annot.blank?
next if annot[:A].blank? || annot[:A][:URI].blank?
next unless annot[:Subtype] == :Link
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')