mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 14:42:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ddbcae153 | ||
|
|
10cd3aa72a | ||
|
|
6684a27e2f | ||
|
|
b6f7a7dd15 | ||
|
|
309e43e18e | ||
|
|
d5b8640a0e | ||
|
|
49b9064d9f | ||
|
|
698031cfc0 | ||
|
|
6dc21e66e1 | ||
|
|
60b32fec94 | ||
|
|
c621166b45 | ||
|
|
8e2da83f9e | ||
|
|
a39ba5b275 | ||
|
|
edf466a6de | ||
|
|
3b8bb8c5bb | ||
|
|
79f1061353 | ||
|
|
745f2160c5 | ||
|
|
2f0c79444b | ||
|
|
69a3782903 | ||
|
|
3dd7c47558 | ||
|
|
761bb07ee6 | ||
|
|
92d7bc65f5 | ||
|
|
79e158be43 | ||
|
|
41ee2dba6a | ||
|
|
8e1354175f | ||
|
|
2474adffc2 | ||
|
|
b8fd5a77d4 | ||
|
|
18bb57aa99 | ||
|
|
17ff193e34 | ||
|
|
20ac63e524 | ||
|
|
518bbe3d1c | ||
|
|
fa5e3e7163 | ||
|
|
2b8280168e | ||
|
|
82854b8ce8 | ||
|
|
276b4f4a5f | ||
|
|
b73a730390 | ||
|
|
2e98885f41 | ||
|
|
bfece574b9 | ||
|
|
0dd5bbe25a | ||
|
|
3975c963c5 | ||
|
|
1f41807412 | ||
|
|
872fbbc875 | ||
|
|
b98a874e20 | ||
|
|
50e123c221 | ||
|
|
7f97bfb3bd | ||
|
|
784665b549 | ||
|
|
c651709e45 | ||
|
|
881a2acbfc | ||
|
|
c49cb4b0c8 | ||
|
|
17b8354c40 |
No files matched your search
@@ -81,9 +81,9 @@ HOST=your-domain-name.com docker-compose up
|
|||||||
|
|
||||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||||
|
|
||||||
[](https://cal.com/docuseal)
|
[Book a Meeting](https://calendly.com/kriti-docuseal/30min)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||||
Unless otherwise noted, all files © 2023 Oleksandr Turchyn.
|
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
module Api
|
module Api
|
||||||
class ApiBaseController < ActionController::API
|
class ApiBaseController < ActionController::API
|
||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
|
include Pagy::Backend
|
||||||
|
|
||||||
|
DEFAULT_LIMIT = 10
|
||||||
|
MAX_LIMIT = 100
|
||||||
|
|
||||||
|
wrap_parameters false
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
check_authorization
|
check_authorization
|
||||||
@@ -17,6 +23,16 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def paginate(relation)
|
||||||
|
result = relation.order(id: :desc)
|
||||||
|
.limit([params[:limit] || DEFAULT_LIMIT, MAX_LIMIT].min)
|
||||||
|
|
||||||
|
result = result.where('id < ?', params[:after]) if params[:after].present?
|
||||||
|
result = result.where('id > ?', params[:before]) if params[:before].present?
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,17 +2,57 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class SubmissionsController < ApiBaseController
|
class SubmissionsController < ApiBaseController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template, only: :create
|
||||||
|
load_and_authorize_resource :submission, only: %i[show index]
|
||||||
|
|
||||||
before_action do
|
before_action only: :create do
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
submissions = Submissions.search(@submissions, params[:q])
|
||||||
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
|
|
||||||
|
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters))
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
data: submissions.as_json(serialize_params),
|
||||||
|
pagination: {
|
||||||
|
count: submissions.size,
|
||||||
|
next: submissions.last&.id,
|
||||||
|
prev: submissions.first&.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
serialized_subbmitters =
|
||||||
|
@submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob).map do |submitter|
|
||||||
|
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
|
||||||
|
|
||||||
|
Submitters::SerializeForApi.call(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
json = @submission.as_json(
|
||||||
|
serialize_params.deep_merge(
|
||||||
|
include: {
|
||||||
|
submission_events: {
|
||||||
|
only: %i[id submitter_id event_type event_timestamp]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
json[:submitters] = serialized_subbmitters
|
||||||
|
|
||||||
|
render json:
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
is_send_email = !params[:send_email].in?(['false', false])
|
is_send_email = !params[:send_email].in?(['false', false])
|
||||||
|
|
||||||
submissions =
|
submissions =
|
||||||
if (emails = (params[:emails] || params[:email]).presence)
|
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
|
||||||
Submissions.create_from_emails(template: @template,
|
Submissions.create_from_emails(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :api,
|
source: :api,
|
||||||
@@ -42,11 +82,33 @@ module Api
|
|||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@submission.update!(deleted_at: Time.current)
|
||||||
|
|
||||||
|
render json: @submission.as_json(only: %i[id deleted_at])
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def serialize_params
|
||||||
|
{
|
||||||
|
only: %i[id source submitters_order created_at updated_at],
|
||||||
|
include: {
|
||||||
|
submitters: { only: %i[id slug uuid name email phone
|
||||||
|
completed_at opened_at sent_at
|
||||||
|
created_at updated_at],
|
||||||
|
methods: %i[status] },
|
||||||
|
template: { only: %i[id name created_at updated_at] },
|
||||||
|
created_by_user: { only: %i[id email first_name last_name] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: [{
|
params.permit(submission: [{
|
||||||
submitters: [[:uuid, :name, :email, :role, :completed, :phone, { values: {} }]]
|
submitters: [[:uuid, :name, :email, :role, :completed, :phone,
|
||||||
|
{ values: {}, readonly_fields: [],
|
||||||
|
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
|
||||||
}])
|
}])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -55,11 +117,15 @@ module Api
|
|||||||
|
|
||||||
Array.wrap(submissions_params).each do |submission|
|
Array.wrap(submissions_params).each do |submission|
|
||||||
submission[:submitters].each_with_index do |submitter, index|
|
submission[:submitters].each_with_index do |submitter, index|
|
||||||
next if submitter[:values].blank?
|
default_values = submitter[:values] || {}
|
||||||
|
|
||||||
|
submitter[:fields]&.each { |f| default_values[f[:name]] = f[:default_value] if f[:default_value].present? }
|
||||||
|
|
||||||
|
next if default_values.blank?
|
||||||
|
|
||||||
values, new_attachments =
|
values, new_attachments =
|
||||||
Submitters::NormalizeValues.call(template,
|
Submitters::NormalizeValues.call(template,
|
||||||
submitter[:values],
|
default_values,
|
||||||
submitter[:role] || template.submitters[index]['name'])
|
submitter[:role] || template.submitters[index]['name'])
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
attachments.push(*new_attachments)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class SubmittersController < ApiBaseController
|
||||||
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
|
def show
|
||||||
|
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
||||||
|
|
||||||
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,22 +5,51 @@ module Api
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: @templates
|
templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
|
templates = params[:archived] ? templates.archived : templates.active
|
||||||
|
|
||||||
|
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
data: templates.as_json(serialize_params),
|
||||||
|
pagination: {
|
||||||
|
count: templates.size,
|
||||||
|
next: templates.last&.id,
|
||||||
|
prev: templates.first&.id
|
||||||
|
}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @template.as_json(include: { author: { only: %i[id email first_name last_name] },
|
render json: @template.as_json(serialize_params)
|
||||||
documents: { only: %i[id uuid], methods: %i[url filename] } })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
if (folder_name = params.dig(:template, :folder_name))
|
||||||
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
||||||
|
end
|
||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
render :ok
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@template.update!(deleted_at: Time.current)
|
||||||
|
|
||||||
|
render json: @template.as_json(only: %i[id deleted_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def serialize_params
|
||||||
|
{
|
||||||
|
include: { author: { only: %i[id email first_name last_name] },
|
||||||
|
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name,
|
params.require(:template).permit(:name,
|
||||||
schema: [%i[attachment_uuid name]],
|
schema: [%i[attachment_uuid name]],
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ApplicationController < ActionController::Base
|
|||||||
private
|
private
|
||||||
|
|
||||||
def sign_in_for_demo
|
def sign_in_for_demo
|
||||||
sign_in(User.order('random()').take) unless signed_in?
|
sign_in(User.active.order('random()').take) unless signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
@@ -22,7 +22,7 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
||||||
else
|
else
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
|
|
||||||
@error_message = 'Code is invalid'
|
@error_message = 'Code is invalid'
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class RegistrationsController < Devise::RegistrationsController
|
|||||||
|
|
||||||
self.resource = account.users.new(user_params)
|
self.resource = account.users.new(user_params)
|
||||||
|
|
||||||
account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create'
|
account.name ||= resource.full_name if params[:action] == 'create'
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ class SubmitFormController < ApplicationController
|
|||||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||||
|
|
||||||
cookies[:submitter_sid] = @submitter.signed_id
|
cookies[:submitter_sid] = @submitter.signed_id
|
||||||
|
|
||||||
|
render @submitter.submission.template.deleted_at? ? :archived : :show
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ class UsersController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
||||||
|
|
||||||
if @user.update(user_params.compact_blank.except(current_user == @user ? :role : nil))
|
attrs = user_params.compact_blank
|
||||||
|
attrs.delete(:role) if User::ROLES.exclude?(attrs[:role])
|
||||||
|
|
||||||
|
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
||||||
redirect_to settings_users_path, notice: 'User has been updated'
|
redirect_to settings_users_path, notice: 'User has been updated'
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
||||||
|
|||||||
@@ -76,12 +76,12 @@ document.addEventListener('turbo:submit-end', async (event) => {
|
|||||||
window.customElements.define('template-builder', class extends HTMLElement {
|
window.customElements.define('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
this.appElem.classList.add('max-h-screen')
|
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -118,3 +118,10 @@ button[disabled] .enabled {
|
|||||||
@apply bg-base-300;
|
@apply bg-base-300;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-outlined {
|
||||||
|
outline-style: solid;
|
||||||
|
outline-width: 1px;
|
||||||
|
outline-offset: 3px;
|
||||||
|
outline-color: hsl(var(--bc) / 0.2);
|
||||||
|
}
|
||||||
@@ -91,11 +91,13 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
this.append(input)
|
this.append(input)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.dataset.submitOnUpload) {
|
if (this.dataset.submitOnUpload === 'true') {
|
||||||
this.closest('form').querySelector('button[type="submit"]').click()
|
this.closest('form').querySelector('button[type="submit"]').click()
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
if (this.dataset.submitOnUpload !== 'true') {
|
||||||
this.toggleLoading()
|
this.toggleLoading()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (this.dataset.submitOnUpload) {
|
if (this.dataset.submitOnUpload) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
withConfetti: true,
|
withConfetti: true,
|
||||||
values: reactive(JSON.parse(this.dataset.values)),
|
values: reactive(JSON.parse(this.dataset.values)),
|
||||||
|
completedButton: JSON.parse(this.dataset.completedButton),
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
fields: JSON.parse(this.dataset.fields)
|
fields: JSON.parse(this.dataset.fields)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ select:required:invalid {
|
|||||||
@apply border-base-content/20;
|
@apply border-base-content/20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.base-textarea {
|
||||||
|
@apply textarea textarea-bordered bg-white rounded-3xl;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply no-animation;
|
@apply no-animation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,9 +110,10 @@
|
|||||||
<span v-else-if="field.type === 'date'">
|
<span v-else-if="field.type === 'date'">
|
||||||
{{ formattedDate }}
|
{{ formattedDate }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span
|
||||||
{{ modelValue }}
|
v-else
|
||||||
</span>
|
class="whitespace-pre-wrap"
|
||||||
|
>{{ modelValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,6 +11,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
|
<a
|
||||||
|
v-if="completedButton.url"
|
||||||
|
:href="completedButton.url"
|
||||||
|
class="white-button flex items-center w-full"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ completedButton.title || 'Back to Website' }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
<button
|
<button
|
||||||
v-if="canSendEmail && !isDemo"
|
v-if="canSendEmail && !isDemo"
|
||||||
class="white-button !h-auto flex items-center space-x-1 w-full"
|
class="white-button !h-auto flex items-center space-x-1 w-full"
|
||||||
@@ -115,6 +124,11 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
completedButton: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
|||||||
@@ -58,42 +58,29 @@
|
|||||||
>
|
>
|
||||||
<div class="md:mt-4">
|
<div class="md:mt-4">
|
||||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||||
<label
|
<TextStep
|
||||||
v-if="currentField.name"
|
:key="currentField.uuid"
|
||||||
:for="currentField.uuid"
|
|
||||||
class="label text-2xl mb-2"
|
|
||||||
>{{ currentField.name }}
|
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="py-1"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
:id="currentField.uuid"
|
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
class="base-input !text-2xl w-full"
|
:field="currentField"
|
||||||
:required="currentField.required"
|
|
||||||
:placeholder="`${t('type_here')}...${currentField.required ? '' : ` (${t('optional')})`}`"
|
|
||||||
type="text"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="currentField.type === 'date'">
|
<div v-else-if="currentField.type === 'date'">
|
||||||
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
<label
|
<label
|
||||||
v-if="currentField.name"
|
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name || t('date') }}
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<button
|
||||||
v-else
|
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||||
class="py-1"
|
@click.prevent="setCurrentDate"
|
||||||
/>
|
>
|
||||||
|
<IconCalendarCheck :width="16" />
|
||||||
|
{{ t('set_today') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<input
|
<input
|
||||||
:id="currentField.uuid"
|
:id="currentField.uuid"
|
||||||
@@ -179,6 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<MultiSelectStep
|
<MultiSelectStep
|
||||||
v-else-if="currentField.type === 'multiple'"
|
v-else-if="currentField.type === 'multiple'"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
/>
|
/>
|
||||||
@@ -224,6 +212,9 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="base-checkbox !h-7 !w-7"
|
class="base-checkbox !h-7 !w-7"
|
||||||
|
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
|
||||||
|
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
||||||
|
:required="field.required"
|
||||||
:checked="!!values[field.uuid]"
|
:checked="!!values[field.uuid]"
|
||||||
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
>
|
>
|
||||||
@@ -237,6 +228,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
v-else-if="currentField.type === 'image'"
|
v-else-if="currentField.type === 'image'"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -247,6 +239,7 @@
|
|||||||
<SignatureStep
|
<SignatureStep
|
||||||
v-else-if="currentField.type === 'signature'"
|
v-else-if="currentField.type === 'signature'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -259,6 +252,7 @@
|
|||||||
<InitialsStep
|
<InitialsStep
|
||||||
v-else-if="currentField.type === 'initials'"
|
v-else-if="currentField.type === 'initials'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -271,6 +265,7 @@
|
|||||||
/>
|
/>
|
||||||
<AttachmentStep
|
<AttachmentStep
|
||||||
v-else-if="currentField.type === 'file'"
|
v-else-if="currentField.type === 'file'"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
@@ -281,6 +276,7 @@
|
|||||||
<PhoneStep
|
<PhoneStep
|
||||||
v-else-if="currentField.type === 'phone'"
|
v-else-if="currentField.type === 'phone'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:default-value="submitter.phone"
|
:default-value="submitter.phone"
|
||||||
@@ -291,6 +287,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-6 md:mt-8">
|
<div class="mt-6 md:mt-8">
|
||||||
<button
|
<button
|
||||||
|
ref="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="base-button w-full flex justify-center"
|
class="base-button w-full flex justify-center"
|
||||||
:disabled="isButtonDisabled"
|
:disabled="isButtonDisabled"
|
||||||
@@ -311,12 +308,19 @@
|
|||||||
><span>...</span></span>
|
><span>...</span></span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="showFillAllRequiredFields"
|
||||||
|
class="text-center mt-1"
|
||||||
|
>
|
||||||
|
{{ t('please_fill_all_required_fields') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<FormCompleted
|
<FormCompleted
|
||||||
v-else
|
v-else
|
||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
:attribution="attribution"
|
:attribution="attribution"
|
||||||
|
:completed-button="completedButton"
|
||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail && !!submitter.email"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -328,7 +332,7 @@
|
|||||||
:key="step[0].uuid"
|
:key="step[0].uuid"
|
||||||
href="#"
|
href="#"
|
||||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
|
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
|
||||||
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
|
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -345,8 +349,9 @@ import InitialsStep from './initials_step'
|
|||||||
import AttachmentStep from './attachment_step'
|
import AttachmentStep from './attachment_step'
|
||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
|
import TextStep from './text_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2, IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
import { t } from './i18n'
|
import { t } from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -360,7 +365,9 @@ export default {
|
|||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
|
TextStep,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
|
IconCalendarCheck,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
@@ -415,6 +422,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
allowToSkip: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
goToLast: {
|
goToLast: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -434,14 +446,22 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
completedButton: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
isFormVisible: true,
|
isFormVisible: true,
|
||||||
|
showFillAllRequiredFields: false,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
|
submittedValues: {},
|
||||||
|
isSecondWalkthrough: false,
|
||||||
recalculateButtonDisabledKey: ''
|
recalculateButtonDisabledKey: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -469,7 +489,7 @@ export default {
|
|||||||
return this.currentStepFields[0]
|
return this.currentStepFields[0]
|
||||||
},
|
},
|
||||||
stepFields () {
|
stepFields () {
|
||||||
return this.fields.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]
|
||||||
|
|
||||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
|
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
|
||||||
@@ -493,11 +513,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||||
|
|
||||||
if (this.goToLast) {
|
if (this.goToLast) {
|
||||||
this.currentStep = Math.min(
|
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.values[f.uuid])))
|
||||||
this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1,
|
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1
|
||||||
this.stepFields.length - 1
|
|
||||||
)
|
const indexesList = [this.stepFields.length - 1]
|
||||||
|
|
||||||
|
if (requiredEmptyStepIndex !== -1) {
|
||||||
|
indexesList.push(requiredEmptyStepIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastFilledStepIndex !== -1) {
|
||||||
|
indexesList.push(lastFilledStepIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentStep = Math.min(...indexesList)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||||
@@ -517,7 +549,10 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalculateButtonDisabledKey = Math.random()
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
|
|
||||||
this.maybeTrackEmailClick().finally(() => {
|
Promise.all([
|
||||||
|
this.maybeTrackEmailClick(),
|
||||||
|
this.maybeTrackSmsClick()
|
||||||
|
]).finally(() => {
|
||||||
this.trackViewForm()
|
this.trackViewForm()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -548,6 +583,30 @@ export default {
|
|||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
maybeTrackSmsClick () {
|
||||||
|
const queryParams = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
|
if (queryParams.has('c')) {
|
||||||
|
const c = queryParams.get('c')
|
||||||
|
|
||||||
|
queryParams.delete('c')
|
||||||
|
const newUrl = [window.location.pathname, queryParams.toString()].filter(Boolean).join('?')
|
||||||
|
window.history.replaceState({}, document.title, newUrl)
|
||||||
|
|
||||||
|
return fetch(this.baseUrl + '/api/submitter_sms_clicks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
c,
|
||||||
|
submitter_slug: this.submitterSlug
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
},
|
||||||
trackViewForm () {
|
trackViewForm () {
|
||||||
fetch(this.baseUrl + '/api/submitter_form_views', {
|
fetch(this.baseUrl + '/api/submitter_form_views', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -561,6 +620,7 @@ export default {
|
|||||||
},
|
},
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
|
this.showFillAllRequiredFields = false
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalculateButtonDisabledKey = Math.random()
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
@@ -576,6 +636,13 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
setCurrentDate () {
|
||||||
|
const inputEl = document.getElementById(this.currentField.uuid)
|
||||||
|
|
||||||
|
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||||
|
|
||||||
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
},
|
||||||
saveStep (formData) {
|
saveStep (formData) {
|
||||||
if (this.isCompleted) {
|
if (this.isCompleted) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
@@ -594,9 +661,14 @@ export default {
|
|||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
stepPromise().then(async () => {
|
stepPromise().then(async () => {
|
||||||
const formData = new FormData(this.$refs.form)
|
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||||
|
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip || !this.isSecondWalkthrough) && !this.submittedValues[fields[0].uuid]
|
||||||
|
})
|
||||||
|
|
||||||
if (this.currentStep === this.stepFields.length - 1) {
|
const formData = new FormData(this.$refs.form)
|
||||||
|
const isLastStep = this.currentStep === this.stepFields.length - 1
|
||||||
|
|
||||||
|
if (isLastStep && !emptyRequiredField) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,10 +681,20 @@ export default {
|
|||||||
return Promise.reject(new Error(data.error))
|
return Promise.reject(new Error(data.error))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = this.stepFields[this.currentStep + 1]
|
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
|
||||||
|
|
||||||
|
if (isLastStep) {
|
||||||
|
this.isSecondWalkthrough = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
this.goToStep(this.stepFields[this.currentStep + 1], true)
|
this.goToStep(nextStep, true)
|
||||||
|
|
||||||
|
if (emptyRequiredField === nextStep) {
|
||||||
|
this.showFillAllRequiredFields = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.isCompleted = true
|
this.isCompleted = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const en = {
|
|||||||
form_has_been_completed: 'Form has been completed!',
|
form_has_been_completed: 'Form has been completed!',
|
||||||
create_a_free_account: 'Create a Free Account',
|
create_a_free_account: 'Create a Free Account',
|
||||||
signed_with: 'Signed with',
|
signed_with: 'Signed with',
|
||||||
|
please_check_the_box_to_continue: 'Please check the box to continue',
|
||||||
open_source_documents_software: 'open source documents software',
|
open_source_documents_software: 'open source documents software',
|
||||||
verified_phone_number: 'Verify Phone Number',
|
verified_phone_number: 'Verify Phone Number',
|
||||||
use_international_format: 'Use internatioanl format: +1xxx',
|
use_international_format: 'Use internatioanl format: +1xxx',
|
||||||
@@ -28,6 +29,10 @@ const en = {
|
|||||||
sending: 'Sending...',
|
sending: 'Sending...',
|
||||||
resend_code: 'Re-send code',
|
resend_code: 'Re-send code',
|
||||||
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
||||||
|
please_fill_all_required_fields: 'Please fill all required fields',
|
||||||
|
set_today: 'Set Today',
|
||||||
|
toggle_multiline_text: 'Toggle Multiline Text',
|
||||||
|
date: 'Date',
|
||||||
email_has_been_sent: 'Email has been sent'
|
email_has_been_sent: 'Email has been sent'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +58,7 @@ const es = {
|
|||||||
form_has_been_completed: '¡El formulario ha sido completado!',
|
form_has_been_completed: '¡El formulario ha sido completado!',
|
||||||
create_a_free_account: 'Crear una Cuenta Gratuita',
|
create_a_free_account: 'Crear una Cuenta Gratuita',
|
||||||
signed_with: 'Firmado con',
|
signed_with: 'Firmado con',
|
||||||
|
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar',
|
||||||
open_source_documents_software: 'software de documentos de código abierto',
|
open_source_documents_software: 'software de documentos de código abierto',
|
||||||
verified_phone_number: 'Verificar número de teléfono',
|
verified_phone_number: 'Verificar número de teléfono',
|
||||||
use_international_format: 'Usar formato internacional: +1xxx',
|
use_international_format: 'Usar formato internacional: +1xxx',
|
||||||
@@ -61,6 +67,10 @@ const es = {
|
|||||||
sending: 'Enviando...',
|
sending: 'Enviando...',
|
||||||
resend_code: 'Reenviar código',
|
resend_code: 'Reenviar código',
|
||||||
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
||||||
|
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
|
||||||
|
set_today: 'Establecer Hoy',
|
||||||
|
date: 'Fecha',
|
||||||
|
toggle_multiline_text: 'Alternar Texto Multilínea',
|
||||||
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +96,7 @@ const it = {
|
|||||||
form_has_been_completed: 'Il modulo è stato completato!',
|
form_has_been_completed: 'Il modulo è stato completato!',
|
||||||
create_a_free_account: 'Crea un Account Gratuito',
|
create_a_free_account: 'Crea un Account Gratuito',
|
||||||
signed_with: 'Firmato con',
|
signed_with: 'Firmato con',
|
||||||
|
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare',
|
||||||
open_source_documents_software: 'software di documenti open source',
|
open_source_documents_software: 'software di documenti open source',
|
||||||
verified_phone_number: 'Verifica numero di telefono',
|
verified_phone_number: 'Verifica numero di telefono',
|
||||||
use_international_format: 'Usa formato internazionale: +1xxx',
|
use_international_format: 'Usa formato internazionale: +1xxx',
|
||||||
@@ -94,6 +105,10 @@ const it = {
|
|||||||
sending: 'Invio in corso...',
|
sending: 'Invio in corso...',
|
||||||
resend_code: 'Rinvia codice',
|
resend_code: 'Rinvia codice',
|
||||||
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
||||||
|
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||||
|
set_today: 'Imposta Oggi',
|
||||||
|
date: 'Data',
|
||||||
|
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||||
email_has_been_sent: "L'email è stata inviata"
|
email_has_been_sent: "L'email è stata inviata"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +134,7 @@ const de = {
|
|||||||
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
||||||
create_a_free_account: 'Kostenloses Konto erstellen',
|
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||||
signed_with: 'Unterschrieben mit',
|
signed_with: 'Unterschrieben mit',
|
||||||
|
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren',
|
||||||
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||||
verified_phone_number: 'Telefonnummer überprüfen',
|
verified_phone_number: 'Telefonnummer überprüfen',
|
||||||
use_international_format: 'Internationales Format verwenden: +1xxx',
|
use_international_format: 'Internationales Format verwenden: +1xxx',
|
||||||
@@ -127,6 +143,10 @@ const de = {
|
|||||||
sending: 'Senden...',
|
sending: 'Senden...',
|
||||||
resend_code: 'Code erneut senden',
|
resend_code: 'Code erneut senden',
|
||||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
||||||
|
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
||||||
|
set_today: 'Heute einstellen',
|
||||||
|
date: 'Datum',
|
||||||
|
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||||
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +172,7 @@ const fr = {
|
|||||||
form_has_been_completed: 'Le formulaire a été complété !',
|
form_has_been_completed: 'Le formulaire a été complété !',
|
||||||
create_a_free_account: 'Créer un Compte Gratuit',
|
create_a_free_account: 'Créer un Compte Gratuit',
|
||||||
signed_with: 'Signé avec',
|
signed_with: 'Signé avec',
|
||||||
|
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer',
|
||||||
open_source_documents_software: 'logiciel de documents open source',
|
open_source_documents_software: 'logiciel de documents open source',
|
||||||
verified_phone_number: 'Vérifier le numéro de téléphone',
|
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||||
use_international_format: 'Utiliser le format international : +1xxx',
|
use_international_format: 'Utiliser le format international : +1xxx',
|
||||||
@@ -160,6 +181,10 @@ const fr = {
|
|||||||
sending: 'Envoi en cours...',
|
sending: 'Envoi en cours...',
|
||||||
resend_code: 'Renvoyer le code',
|
resend_code: 'Renvoyer le code',
|
||||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
||||||
|
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||||
|
set_today: "Définir Aujourd'hui",
|
||||||
|
date: 'Date',
|
||||||
|
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||||
email_has_been_sent: "L'email a été envoyé"
|
email_has_been_sent: "L'email a été envoyé"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +210,7 @@ const pl = {
|
|||||||
form_has_been_completed: 'Formularz został wypełniony!',
|
form_has_been_completed: 'Formularz został wypełniony!',
|
||||||
create_a_free_account: 'Utwórz darmowe konto',
|
create_a_free_account: 'Utwórz darmowe konto',
|
||||||
signed_with: 'Podpisane za pomocą',
|
signed_with: 'Podpisane za pomocą',
|
||||||
|
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować',
|
||||||
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
||||||
verified_phone_number: 'Zweryfikuj numer telefonu',
|
verified_phone_number: 'Zweryfikuj numer telefonu',
|
||||||
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
||||||
@@ -193,6 +219,10 @@ const pl = {
|
|||||||
sending: 'Wysyłanie...',
|
sending: 'Wysyłanie...',
|
||||||
resend_code: 'Ponownie wyślij kod',
|
resend_code: 'Ponownie wyślij kod',
|
||||||
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
||||||
|
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
||||||
|
set_today: 'Ustaw Dziś',
|
||||||
|
date: 'Data',
|
||||||
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
email_has_been_sent: 'E-mail został wysłany'
|
email_has_been_sent: 'E-mail został wysłany'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +248,7 @@ const uk = {
|
|||||||
form_has_been_completed: 'Форму заповнено!',
|
form_has_been_completed: 'Форму заповнено!',
|
||||||
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||||
signed_with: 'Підписано за допомогою',
|
signed_with: 'Підписано за допомогою',
|
||||||
|
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити',
|
||||||
open_source_documents_software: 'відкритий програмний засіб для документів',
|
open_source_documents_software: 'відкритий програмний засіб для документів',
|
||||||
verified_phone_number: 'Підтвердіть номер телефону',
|
verified_phone_number: 'Підтвердіть номер телефону',
|
||||||
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
||||||
@@ -226,6 +257,10 @@ const uk = {
|
|||||||
sending: 'Надсилаю...',
|
sending: 'Надсилаю...',
|
||||||
resend_code: 'Повторно відправити код',
|
resend_code: 'Повторно відправити код',
|
||||||
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
||||||
|
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||||
|
set_today: 'Задати Сьогодні',
|
||||||
|
date: 'Дата',
|
||||||
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений'
|
email_has_been_sent: 'Електронний лист був відправлений'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +286,7 @@ const cs = {
|
|||||||
form_has_been_completed: 'Formulář byl dokončen!',
|
form_has_been_completed: 'Formulář byl dokončen!',
|
||||||
create_a_free_account: 'Vytvořit bezplatný účet',
|
create_a_free_account: 'Vytvořit bezplatný účet',
|
||||||
signed_with: 'Podepsáno pomocí',
|
signed_with: 'Podepsáno pomocí',
|
||||||
|
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování',
|
||||||
open_source_documents_software: 'open source software pro dokumenty',
|
open_source_documents_software: 'open source software pro dokumenty',
|
||||||
verified_phone_number: 'Ověřte telefonní číslo',
|
verified_phone_number: 'Ověřte telefonní číslo',
|
||||||
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
||||||
@@ -259,6 +295,10 @@ const cs = {
|
|||||||
sending: 'Odesílání...',
|
sending: 'Odesílání...',
|
||||||
resend_code: 'Znovu odeslat kód',
|
resend_code: 'Znovu odeslat kód',
|
||||||
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
||||||
|
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||||
|
set_today: 'Nastavit Dnes',
|
||||||
|
date: 'Datum',
|
||||||
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
email_has_been_sent: 'E-mail byl odeslán'
|
email_has_been_sent: 'E-mail byl odeslán'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +324,7 @@ const pt = {
|
|||||||
form_has_been_completed: 'O formulário foi concluído!',
|
form_has_been_completed: 'O formulário foi concluído!',
|
||||||
create_a_free_account: 'Criar uma Conta Gratuita',
|
create_a_free_account: 'Criar uma Conta Gratuita',
|
||||||
signed_with: 'Assinado com',
|
signed_with: 'Assinado com',
|
||||||
|
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar',
|
||||||
open_source_documents_software: 'software de documentos de código aberto',
|
open_source_documents_software: 'software de documentos de código aberto',
|
||||||
verified_phone_number: 'Verificar Número de Telefone',
|
verified_phone_number: 'Verificar Número de Telefone',
|
||||||
use_international_format: 'Use formato internacional: +1xxx',
|
use_international_format: 'Use formato internacional: +1xxx',
|
||||||
@@ -292,6 +333,10 @@ const pt = {
|
|||||||
sending: 'Enviando...',
|
sending: 'Enviando...',
|
||||||
resend_code: 'Reenviar código',
|
resend_code: 'Reenviar código',
|
||||||
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
||||||
|
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
||||||
|
set_today: 'Definir Hoje',
|
||||||
|
date: 'Data',
|
||||||
|
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||||
email_has_been_sent: 'Email enviado'
|
email_has_been_sent: 'Email enviado'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,33 +9,37 @@
|
|||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<button
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
class="btn btn-sm btn-circle"
|
class="btn btn-sm btn-circle"
|
||||||
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature :width="16" />
|
||||||
</button>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<a
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
v-else
|
v-else
|
||||||
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
title="Minimize"
|
title="Minimize"
|
||||||
|
href="#"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
>
|
>
|
||||||
@@ -43,7 +47,7 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -66,7 +70,7 @@
|
|||||||
id="initials_text_input"
|
id="initials_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6 text-center"
|
class="base-input !text-2xl w-full mt-6 text-center"
|
||||||
:required="field.required && !isInitialsStarted"
|
:required="field.required && !isInitialsStarted && !modelValue"
|
||||||
:placeholder="`${t('type_initial_here')}...`"
|
:placeholder="`${t('type_initial_here')}...`"
|
||||||
type="text"
|
type="text"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
:name="`values[${field.uuid}][]`"
|
:name="`values[${field.uuid}][]`"
|
||||||
:value="option"
|
:value="option"
|
||||||
class="base-checkbox !h-7 !w-7"
|
class="base-checkbox !h-7 !w-7"
|
||||||
:checked="modelValue.includes(option)"
|
:checked="(modelValue || []).includes(option)"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="field.name"
|
||||||
|
:for="field.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ field.name }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="py-1"
|
||||||
|
/>
|
||||||
|
<div class="items-center flex">
|
||||||
|
<input
|
||||||
|
v-if="!isTextArea"
|
||||||
|
:id="field.uuid"
|
||||||
|
v-model="text"
|
||||||
|
class="base-input !text-2xl w-full !pr-11 -mr-10"
|
||||||
|
:required="field.required"
|
||||||
|
:pattern="field.validation?.pattern"
|
||||||
|
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
|
||||||
|
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
|
||||||
|
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
|
type="text"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
v-if="isTextArea"
|
||||||
|
:id="field.uuid"
|
||||||
|
ref="textarea"
|
||||||
|
v-model="text"
|
||||||
|
class="base-textarea !text-2xl w-full"
|
||||||
|
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
|
:required="field.required"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@input="resizeTextarea"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="!isTextArea"
|
||||||
|
class="tooltip"
|
||||||
|
:data-tip="t('toggle_multiline_text')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-ghost btn-circle btn-sm"
|
||||||
|
@click.prevent="toggleTextArea"
|
||||||
|
>
|
||||||
|
<IconAlignBoxLeftTop />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TextStep',
|
||||||
|
components: {
|
||||||
|
IconAlignBoxLeftTop
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isTextArea: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
text: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.isTextArea = this.modelValue?.includes('\n')
|
||||||
|
|
||||||
|
if (this.isTextArea) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.resizeTextarea()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resizeTextarea () {
|
||||||
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
|
textarea.style.height = 'auto'
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px'
|
||||||
|
},
|
||||||
|
toggleTextArea () {
|
||||||
|
this.isTextArea = true
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.textarea.focus()
|
||||||
|
this.$refs.textarea.setSelectionRange(this.$refs.textarea.value.length, this.$refs.textarea.value.length)
|
||||||
|
|
||||||
|
this.resizeTextarea()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
:style="positionStyle"
|
:style="positionStyle"
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@mousedown.stop="startDrag"
|
@mousedown.stop="startDrag"
|
||||||
|
@touchstart="startTouchDrag"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isSelected || isDraw"
|
v-if="isSelected || isDraw"
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="field?.type"
|
v-if="field?.type"
|
||||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap group-hover:flex group-hover:z-10"
|
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap group-hover:flex group-hover:z-10"
|
||||||
:class="{ 'flex z-10': isNameFocus || isSelected, hidden: !isNameFocus && !isSelected }"
|
:class="{ 'flex z-10': isNameFocus || isSelected, invisible: !isNameFocus && !isSelected }"
|
||||||
style="top: -25px; height: 25px"
|
style="top: -25px; height: 25px"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@@ -108,12 +109,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
ref="touchTarget"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field?.type"
|
v-if="field?.type"
|
||||||
class="h-2.5 w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
||||||
@mousedown.stop="startResize"
|
@mousedown.stop="startResize"
|
||||||
|
@touchstart="startTouchResize"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -186,7 +189,12 @@ export default {
|
|||||||
'border-sky-500',
|
'border-sky-500',
|
||||||
'border-emerald-500',
|
'border-emerald-500',
|
||||||
'border-yellow-300',
|
'border-yellow-300',
|
||||||
'border-purple-600'
|
'border-purple-600',
|
||||||
|
'border-pink-500',
|
||||||
|
'border-cyan-500',
|
||||||
|
'border-orange-500',
|
||||||
|
'border-lime-500',
|
||||||
|
'border-indigo-500'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
bgColors () {
|
bgColors () {
|
||||||
@@ -195,7 +203,12 @@ export default {
|
|||||||
'bg-sky-100',
|
'bg-sky-100',
|
||||||
'bg-emerald-100',
|
'bg-emerald-100',
|
||||||
'bg-yellow-100',
|
'bg-yellow-100',
|
||||||
'bg-purple-100'
|
'bg-purple-100',
|
||||||
|
'bg-pink-100',
|
||||||
|
'bg-cyan-100',
|
||||||
|
'bg-orange-100',
|
||||||
|
'bg-lime-100',
|
||||||
|
'bg-indigo-100'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
@@ -309,6 +322,47 @@ export default {
|
|||||||
|
|
||||||
this.$emit('start-drag')
|
this.$emit('start-drag')
|
||||||
},
|
},
|
||||||
|
startTouchDrag (e) {
|
||||||
|
if (e.target !== this.$refs.touchTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs?.name?.blur()
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.isDragged = true
|
||||||
|
|
||||||
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
|
||||||
|
|
||||||
|
this.$el.getRootNode().addEventListener('touchmove', this.touchDrag)
|
||||||
|
this.$el.getRootNode().addEventListener('touchend', this.stopTouchDrag)
|
||||||
|
|
||||||
|
this.$emit('start-drag')
|
||||||
|
},
|
||||||
|
touchDrag (e) {
|
||||||
|
const page = this.$parent.$refs.mask.previousSibling
|
||||||
|
const rect = page.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.area.x = (this.dragFrom.x + e.touches[0].clientX - rect.left) / rect.width
|
||||||
|
this.area.y = (this.dragFrom.y + e.touches[0].clientY - rect.top) / rect.height
|
||||||
|
},
|
||||||
|
stopTouchDrag () {
|
||||||
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
|
||||||
|
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag)
|
||||||
|
|
||||||
|
if (this.isDragged) {
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDragged = false
|
||||||
|
|
||||||
|
this.$emit('stop-drag')
|
||||||
|
},
|
||||||
stopDrag () {
|
stopDrag () {
|
||||||
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
||||||
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
||||||
@@ -335,6 +389,33 @@ export default {
|
|||||||
|
|
||||||
this.$emit('stop-resize')
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
startTouchResize (e) {
|
||||||
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
this.$refs?.name?.blur()
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.$el.getRootNode().addEventListener('touchmove', this.touchResize)
|
||||||
|
this.$el.getRootNode().addEventListener('touchend', this.stopTouchResize)
|
||||||
|
|
||||||
|
this.$emit('start-resize', 'nwse')
|
||||||
|
},
|
||||||
|
touchResize (e) {
|
||||||
|
const page = this.$parent.$refs.mask.previousSibling
|
||||||
|
const rect = page.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
|
||||||
|
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
|
||||||
|
},
|
||||||
|
stopTouchResize () {
|
||||||
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
|
||||||
|
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchResize)
|
||||||
|
|
||||||
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
style="max-width: 1600px"
|
style="max-width: 1600px"
|
||||||
class="mx-auto pl-4 h-full"
|
class="mx-auto pl-3 md:pl-4 h-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4">
|
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<a
|
<a
|
||||||
v-if="withLogoLink"
|
v-if="withLogo"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
<Logo />
|
<Logo />
|
||||||
</a>
|
</a>
|
||||||
<Logo v-else />
|
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
:model-value="template.name"
|
:model-value="template.name"
|
||||||
class="text-3xl font-semibold focus:text-clip"
|
class="text-3xl font-semibold focus:text-clip"
|
||||||
@@ -60,10 +62,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex md:max-h-[calc(100vh-60px)]">
|
||||||
class="flex"
|
|
||||||
style="max-height: calc(100% - 60px)"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
ref="previews"
|
ref="previews"
|
||||||
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||||
@@ -98,7 +97,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
<div class="w-full overflow-y-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
||||||
<div
|
<div
|
||||||
ref="documents"
|
ref="documents"
|
||||||
class="pr-3.5 pl-0.5"
|
class="pr-3.5 pl-0.5"
|
||||||
@@ -153,17 +152,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<span
|
||||||
v-if="sortedDocuments.length"
|
v-if="drawField"
|
||||||
class="sticky md:hidden"
|
class="fixed text-center w-full left-1/2 bottom-0 transform -translate-x-1/2"
|
||||||
style="bottom: 100px"
|
|
||||||
>
|
>
|
||||||
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between ml-4 mr-6">
|
<span
|
||||||
<span class="w-full text-center text-lg">
|
class="rounded bg-base-200 px-4 py-2 rounded-full inline-flex space-x-2 mx-auto items-center mb-4 z-20 md:hidden"
|
||||||
You need a larger screen to use builder tools.
|
>
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[drawField.type]"
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
class="inline"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Draw {{ fieldNames[drawField.type] }} Field
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<a
|
||||||
</div>
|
href="#"
|
||||||
|
class="link block text-center"
|
||||||
|
@click.prevent="drawField = null"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<FieldType
|
||||||
|
v-if="sortedDocuments.length && !drawField"
|
||||||
|
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
|
||||||
|
:model-value="''"
|
||||||
|
@update:model-value="startFieldDraw($event)"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="btn btn-neutral text-white btn-circle btn-lg group"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<IconPlus
|
||||||
|
class="group-focus:hidden"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
/>
|
||||||
|
<IconX
|
||||||
|
class="hidden group-focus:inline"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</FieldType>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
||||||
@@ -215,7 +251,8 @@ import Logo from './logo'
|
|||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
|
import FieldType from './field_type'
|
||||||
|
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
@@ -225,6 +262,9 @@ export default {
|
|||||||
Upload,
|
Upload,
|
||||||
Document,
|
Document,
|
||||||
Fields,
|
Fields,
|
||||||
|
IconPlus,
|
||||||
|
FieldType,
|
||||||
|
IconX,
|
||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
@@ -269,7 +309,7 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
withLogoLink: {
|
withLogo: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
@@ -301,6 +341,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
selectedAreaRef: () => ref(),
|
selectedAreaRef: () => ref(),
|
||||||
fieldAreasIndex () {
|
fieldAreasIndex () {
|
||||||
const areas = {}
|
const areas = {}
|
||||||
@@ -330,16 +372,21 @@ export default {
|
|||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.undoStack = [JSON.stringify(this.template)]
|
||||||
|
this.redoStack = []
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.onWindowResize()
|
this.onWindowResize()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('keyup', this.onKeyUp)
|
document.addEventListener('keyup', this.onKeyUp)
|
||||||
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
document.removeEventListener('keyup', this.onKeyUp)
|
document.removeEventListener('keyup', this.onKeyUp)
|
||||||
|
window.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.removeEventListener('resize', this.onWindowResize)
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
},
|
},
|
||||||
@@ -347,6 +394,52 @@ export default {
|
|||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
startFieldDraw (type) {
|
||||||
|
const field = {
|
||||||
|
name: '',
|
||||||
|
uuid: v4(),
|
||||||
|
required: type !== 'checkbox',
|
||||||
|
areas: [],
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
|
field.options = ['']
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawField = field
|
||||||
|
},
|
||||||
|
undo () {
|
||||||
|
if (this.undoStack.length > 1) {
|
||||||
|
this.undoStack.pop()
|
||||||
|
const stringData = this.undoStack[this.undoStack.length - 1]
|
||||||
|
const currentStringData = JSON.stringify(this.template)
|
||||||
|
|
||||||
|
if (stringData && stringData !== currentStringData) {
|
||||||
|
this.redoStack.push(currentStringData)
|
||||||
|
|
||||||
|
Object.assign(this.template, JSON.parse(stringData))
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
redo () {
|
||||||
|
const stringData = this.redoStack.pop()
|
||||||
|
this.lastRedoData = stringData
|
||||||
|
const currentStringData = JSON.stringify(this.template)
|
||||||
|
|
||||||
|
if (stringData && stringData !== currentStringData) {
|
||||||
|
if (this.undoStack[this.undoStack.length - 1] !== currentStringData) {
|
||||||
|
this.undoStack.push(currentStringData)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this.template, JSON.parse(stringData))
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
},
|
||||||
onWindowResize (e) {
|
onWindowResize (e) {
|
||||||
const breakpointLg = 1024
|
const breakpointLg = 1024
|
||||||
|
|
||||||
@@ -374,6 +467,19 @@ export default {
|
|||||||
this.selectedAreaRef.value = null
|
this.selectedAreaRef.value = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onKeyDown (event) {
|
||||||
|
if ((event.metaKey && event.shiftKey && event.key === 'z') || (event.ctrlKey && event.key === 'Z')) {
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
this.redo()
|
||||||
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
this.undo()
|
||||||
|
}
|
||||||
|
},
|
||||||
removeArea (area) {
|
removeArea (area) {
|
||||||
const field = this.template.fields.find((f) => f.areas?.includes(area))
|
const field = this.template.fields.find((f) => f.areas?.includes(area))
|
||||||
|
|
||||||
@@ -385,11 +491,26 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
|
pushUndo () {
|
||||||
|
const stringData = JSON.stringify(this.template)
|
||||||
|
|
||||||
|
if (this.undoStack[this.undoStack.length - 1] !== stringData) {
|
||||||
|
this.undoStack.push(stringData)
|
||||||
|
|
||||||
|
if (this.lastRedoData !== stringData) {
|
||||||
|
this.redoStack = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onDraw (area) {
|
onDraw (area) {
|
||||||
if (this.drawField) {
|
if (this.drawField) {
|
||||||
this.drawField.areas ||= []
|
this.drawField.areas ||= []
|
||||||
this.drawField.areas.push(area)
|
this.drawField.areas.push(area)
|
||||||
|
|
||||||
|
if (this.template.fields.indexOf(this.drawField) === -1) {
|
||||||
|
this.template.fields.push(this.drawField)
|
||||||
|
}
|
||||||
|
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
@@ -597,6 +718,8 @@ export default {
|
|||||||
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pushUndo()
|
||||||
|
|
||||||
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
v-if="isNameFocus"
|
v-if="isNameFocus"
|
||||||
class="flex items-center relative"
|
class="flex items-center relative"
|
||||||
>
|
>
|
||||||
<template v-if="field.type !== 'checkbox'">
|
|
||||||
<input
|
<input
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
v-model="field.required"
|
v-model="field.required"
|
||||||
@@ -45,7 +44,6 @@
|
|||||||
@click.prevent="field.required = !field.required"
|
@click.prevent="field.required = !field.required"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>Required</label>
|
>Required</label>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="submitters.length < 5">
|
<li v-if="submitters.length < 10">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2"
|
class="flex px-2"
|
||||||
@@ -130,7 +130,12 @@ export default {
|
|||||||
'bg-sky-500',
|
'bg-sky-500',
|
||||||
'bg-emerald-500',
|
'bg-emerald-500',
|
||||||
'bg-yellow-300',
|
'bg-yellow-300',
|
||||||
'bg-purple-600'
|
'bg-purple-600',
|
||||||
|
'bg-pink-500',
|
||||||
|
'bg-cyan-500',
|
||||||
|
'bg-orange-500',
|
||||||
|
'bg-lime-500',
|
||||||
|
'bg-indigo-500'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
names () {
|
names () {
|
||||||
@@ -139,7 +144,12 @@ export default {
|
|||||||
'Second Submitter',
|
'Second Submitter',
|
||||||
'Third Submitter',
|
'Third Submitter',
|
||||||
'Fourth Submitter',
|
'Fourth Submitter',
|
||||||
'Fifth Submitter'
|
'Fifth Submitter',
|
||||||
|
'Sixth Submitter',
|
||||||
|
'Seventh Submitter',
|
||||||
|
'Eighth Submitter',
|
||||||
|
'Ninth Submitter',
|
||||||
|
'Tenth Submitter'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
selectedSubmitter () {
|
selectedSubmitter () {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="dropdown">
|
<span class="dropdown">
|
||||||
|
<slot>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
:title="fieldNames[modelValue]"
|
:title="fieldNames[modelValue]"
|
||||||
@@ -12,9 +13,10 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
</slot>
|
||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
|
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -107,9 +107,6 @@
|
|||||||
<li>
|
<li>
|
||||||
Draw a text field on the page with a mouse
|
Draw a text field on the page with a mouse
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
Single click on the page to add a checkbox
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
Drag & drop any other field type on the page
|
Drag & drop any other field type on the page
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative cursor-crosshair select-none">
|
<div
|
||||||
|
class="relative cursor-crosshair select-none"
|
||||||
|
:style="drawField ? 'touch-action: none' : ''"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
ref="image"
|
ref="image"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
@@ -32,12 +35,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="resizeDirection || isMove || isDrag || showMask"
|
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
|
||||||
id="mask"
|
id="mask"
|
||||||
ref="mask"
|
ref="mask"
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute z-10"
|
class="top-0 bottom-0 left-0 right-0 absolute z-10"
|
||||||
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||||
@pointermove="onPointermove"
|
@pointermove="onPointermove"
|
||||||
|
@pointerdown="onStartDraw"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@pointerup="onPointerup"
|
@pointerup="onPointerup"
|
||||||
@@ -93,6 +97,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isMobile () {
|
||||||
|
return /android|iphone|ipad/i.test(navigator.userAgent)
|
||||||
|
},
|
||||||
resizeDirectionClasses () {
|
resizeDirectionClasses () {
|
||||||
return {
|
return {
|
||||||
nwse: 'cursor-nwse-resize',
|
nwse: 'cursor-nwse-resize',
|
||||||
@@ -125,6 +132,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onStartDraw (e) {
|
onStartDraw (e) {
|
||||||
|
if (this.isMobile && !this.drawField) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.showMask = true
|
this.showMask = true
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
|
|||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter)
|
||||||
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
|
|||||||
Faraday.post(config.value,
|
Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
timestamp: Time.current.iso8601,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
|
|||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter)
|
||||||
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
|
|||||||
Faraday.post(config.value,
|
Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.started',
|
event_type: 'form.started',
|
||||||
timestamp: Time.current.iso8601,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
|
|||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter)
|
||||||
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
|
|||||||
Faraday.post(config.value,
|
Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.viewed',
|
event_type: 'form.viewed',
|
||||||
timestamp: Time.current.iso8601,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
if @email_config || subject.present?
|
if @email_config || subject.present?
|
||||||
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
|
||||||
else
|
else
|
||||||
'You have been invited to submit a form'
|
'You are invited to submit a form'
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(to: @submitter.friendly_name,
|
mail(to: @submitter.friendly_name,
|
||||||
@@ -62,7 +62,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
if @email_config
|
if @email_config
|
||||||
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||||
else
|
else
|
||||||
'Your copy of documents'
|
'Your document copy'
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UserMailer < ApplicationMailer
|
class UserMailer < ApplicationMailer
|
||||||
def invitation_email(user)
|
def invitation_email(user, invited_by: nil)
|
||||||
@current_account = user.account
|
@current_account = invited_by&.account || user.account
|
||||||
@user = user
|
@user = user
|
||||||
@token = @user.send(:set_reset_password_token)
|
@token = @user.send(:set_reset_password_token)
|
||||||
|
|
||||||
mail(to: @user.friendly_name,
|
mail(to: @user.friendly_name,
|
||||||
subject: 'You have been invited to Docuseal')
|
subject: 'You are invited to DocuSeal')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -26,12 +26,13 @@ class AccountConfig < ApplicationRecord
|
|||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||||
BCC_EMAILS = 'bcc_emails'
|
BCC_EMAILS = 'bcc_emails'
|
||||||
SUBMITTER_REMAILERS = 'submitter_reminders'
|
SUBMITTER_REMAILERS = 'submitter_reminders'
|
||||||
|
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||||
|
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY => {
|
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||||
'subject' => 'You have been invited to submit a form',
|
'subject' => 'You are invited to submit a form',
|
||||||
'body' => "Hi there,\n\n" \
|
'body' => "Hi there,\n\n" \
|
||||||
"You have been invited to submit the \"{{template.name}}\" form:\n\n" \
|
"You have been invited to submit the \"{{template.name}}\" form.\n\n" \
|
||||||
"{{submitter.link}}\n\n" \
|
"{{submitter.link}}\n\n" \
|
||||||
"Please contact us by replying to this email if you didn't request this.\n\n" \
|
"Please contact us by replying to this email if you didn't request this.\n\n" \
|
||||||
"Thanks,\n" \
|
"Thanks,\n" \
|
||||||
@@ -44,10 +45,10 @@ class AccountConfig < ApplicationRecord
|
|||||||
'{{submission.link}}'
|
'{{submission.link}}'
|
||||||
},
|
},
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
|
||||||
'subject' => 'Your copy of documents',
|
'subject' => 'Your document copy',
|
||||||
'body' => "Hi there,\n\n" \
|
'body' => "Hi there,\n\n" \
|
||||||
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
||||||
"Alternatively, you can download the copy using:\n\n" \
|
"Alternatively, you can download your copy using:\n\n" \
|
||||||
"{{documents.links}}\n\n" \
|
"{{documents.links}}\n\n" \
|
||||||
"Thanks,\n" \
|
"Thanks,\n" \
|
||||||
'{{account.name}}'
|
'{{account.name}}'
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
open_email: 'open_email',
|
open_email: 'open_email',
|
||||||
click_email: 'click_email',
|
click_email: 'click_email',
|
||||||
click_sms: 'click_sms',
|
click_sms: 'click_sms',
|
||||||
|
phone_verified: 'phone_verified',
|
||||||
start_form: 'start_form',
|
start_form: 'start_form',
|
||||||
view_form: 'view_form',
|
view_form: 'view_form',
|
||||||
complete_form: 'complete_form'
|
complete_form: 'complete_form'
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class Template < ApplicationRecord
|
|||||||
has_many :submissions, dependent: :destroy
|
has_many :submissions, dependent: :destroy
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
|
scope :archived, -> { where.not(deleted_at: nil) }
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% unless Docuseal.multitenant? %>
|
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
|
||||||
<%= f.fields_for @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) do |ff| %>
|
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %>
|
||||||
|
<%= f.fields_for encrypted_config do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :value, 'App URL', class: 'label' %>
|
<%= ff.label :value, 'App URL', class: 'label' %>
|
||||||
<%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %>
|
<%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %>
|
||||||
|
|||||||
@@ -12,32 +12,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4 mt-4">
|
<div class="space-y-4 mt-4">
|
||||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
|
||||||
<input type="checkbox">
|
|
||||||
<div class="collapse-title text-xl font-medium">
|
|
||||||
<div>
|
|
||||||
Request signature, single submitter
|
|
||||||
</div>
|
|
||||||
<div class="mt-1">
|
|
||||||
<div class="badge badge-warning badge-lg">POST</div>
|
|
||||||
<div class="badge badge-primary badge-lg"><%= api_submissions_path %></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="collapse-content" style="display: inherit">
|
|
||||||
<div class="mockup-code overflow-hidden">
|
|
||||||
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
|
|
||||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
|
|
||||||
--data-raw '{
|
|
||||||
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
|
||||||
"emails": "<%= current_user.email.sub('@', '+test@') %>, <%= current_user.email.sub('@', '+test2@') %>"
|
|
||||||
}'<% end.to_str %>
|
|
||||||
<span class="top-0 right-0 absolute">
|
|
||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
|
||||||
</span>
|
|
||||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
@@ -66,7 +40,7 @@
|
|||||||
"Form Text Field Name": "Default Value"
|
"Form Text Field Name": "Default Value"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
{ "role": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -78,6 +52,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||||
|
<input type="checkbox">
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
<div>
|
||||||
|
Request signature, single submitter
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<div class="badge badge-warning badge-lg">POST</div>
|
||||||
|
<div class="badge badge-primary badge-lg"><%= api_submissions_emails_path %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content" style="display: inherit">
|
||||||
|
<div class="mockup-code overflow-hidden">
|
||||||
|
<% text = capture do %>curl --location '<%= api_submissions_emails_url %>' \
|
||||||
|
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
|
||||||
|
--data-raw '{
|
||||||
|
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||||
|
"emails": "<%= current_user.email.sub('@', '+test@') %>, <%= current_user.email.sub('@', '+test2@') %>"
|
||||||
|
}'<% end.to_str %>
|
||||||
|
<span class="top-0 right-0 absolute">
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||||
|
</span>
|
||||||
|
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
@@ -101,5 +101,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<%= link_to 'Open Full API Reference', "#{Docuseal::PRODUCT_URL}/docs/api", class: 'btn btn-warning text-base mt-4 px-8', target: '_blank', rel: 'noopener' %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,20 +26,44 @@
|
|||||||
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
|
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
|
||||||
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
|
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
|
||||||
</div>
|
</div>
|
||||||
<% if @templates.blank? %>
|
|
||||||
<% if @pagy.pages > 1 %>
|
|
||||||
<%= render 'shared/pagination', pagy: @pagy, left_additional_html: view_archived_html %>
|
|
||||||
<% elsif params[:q].blank? %>
|
|
||||||
<div class="mt-2">
|
|
||||||
<%= view_archived_html %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @templates.present? %>
|
<% if @templates.present? %>
|
||||||
<div class="grid gap-4 md:grid-cols-3">
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
<%= render partial: 'templates/template', collection: @templates %>
|
<%= render partial: 'templates/template', collection: @templates %>
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
|
||||||
|
<%= form_for '', url: templates_upload_path, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
|
||||||
|
<button type="submit" class="hidden"></button>
|
||||||
|
<file-dropzone data-submit-on-upload="true" class="w-full">
|
||||||
|
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
|
||||||
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
|
||||||
|
<span>
|
||||||
|
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
||||||
|
</span>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
Upload New Document
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="font-medium">Click to upload</span> or drag and drop
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
|
||||||
|
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
Uploading...
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</file-dropzone>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if @templates.present? || params[:q].blank? %>
|
||||||
<% if @pagy.pages > 1 %>
|
<% if @pagy.pages > 1 %>
|
||||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
|
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
|
||||||
<% else %>
|
<% else %>
|
||||||
@@ -60,7 +84,7 @@
|
|||||||
<div class="flex items-center h-full">
|
<div class="flex items-center h-full">
|
||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<div class="max-w-xl mx-auto">
|
<div class="max-w-xl mx-auto">
|
||||||
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to DocuSeal</h1>
|
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to <%= Docuseal.product_name %></h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-lg mx-auto">
|
<div class="max-w-lg mx-auto">
|
||||||
<p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms</p>
|
<p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms</p>
|
||||||
|
|||||||
@@ -21,6 +21,5 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Change my password', disabled_with: 'Changing password'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Change my password', disabled_with: 'Changing password'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'devise/shared/links' %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,5 +12,4 @@
|
|||||||
<%= f.button button_title(title: 'Reset password', disabled_with: 'Resetting password'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Reset password', disabled_with: 'Resetting password'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="max-w-lg mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||||
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1>
|
<h1 class="text-4xl font-bold text-center mt-8">Sign In</h1>
|
||||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||||
<% if params[:redir].present? %>
|
<% if params[:redir].present? %>
|
||||||
<%= hidden_field_tag :redir, params[:redir] %>
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
@@ -16,11 +16,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Sign In', disabled_with: 'Signing In'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if devise_mapping.omniauthable? %>
|
<% if devise_mapping.omniauthable? %>
|
||||||
<%= button_to button_title(title: 'Log in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
<%= button_to button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="max-w-lg mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1>
|
<h1 class="text-4xl font-bold text-center mt-8">Sign In</h1>
|
||||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||||
<%= f.hidden_field :email %>
|
<%= f.hidden_field :email %>
|
||||||
<%= f.hidden_field :password %>
|
<%= f.hidden_field :password %>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Sign In', disabled_with: 'Signing In'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="flex justify-between mt-4">
|
<div class="flex justify-between mt-4">
|
||||||
<%- if controller_name != 'sessions' %>
|
<%- if controller_name != 'sessions' %>
|
||||||
<%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %>
|
<%= link_to 'Already have an account?', new_session_path(resource_name), class: 'link link-hover mx-auto' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
||||||
<%= link_to 'Create free account', registration_path({ redir: params[:redir] }.compact_blank), class: 'link link-hover' %>
|
<%= link_to 'Create free account', registration_path({ redir: params[:redir] }.compact_blank), class: 'link link-hover' %>
|
||||||
@@ -8,10 +8,4 @@
|
|||||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||||
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
|
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
|
||||||
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name), class: 'link link-hover' %>
|
|
||||||
<% end %>
|
|
||||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
|
||||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -18,11 +18,11 @@
|
|||||||
<div class="grid md:grid-cols-2 gap-4">
|
<div class="grid md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :username, class: 'label' %>
|
<%= ff.label :username, class: 'label' %>
|
||||||
<%= ff.text_field :username, value: value['username'], required: true, class: 'base-input' %>
|
<%= ff.text_field :username, value: value['username'], class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :password, class: 'label' %>
|
<%= ff.label :password, class: 'label' %>
|
||||||
<%= ff.password_field :password, value: value['password'], required: true, class: 'base-input' %>
|
<%= ff.password_field :password, value: value['password'], class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid md:grid-cols-2 gap-4">
|
<div class="grid md:grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M3 21l18 0"></path>
|
||||||
|
<path d="M9 8l1 0"></path>
|
||||||
|
<path d="M9 12l1 0"></path>
|
||||||
|
<path d="M9 16l1 0"></path>
|
||||||
|
<path d="M14 8l1 0"></path>
|
||||||
|
<path d="M14 12l1 0"></path>
|
||||||
|
<path d="M14 16l1 0"></path>
|
||||||
|
<path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 565 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 3a3 3 0 0 0 -3 3v12a3 3 0 0 0 3 3"></path>
|
||||||
|
<path d="M6 3a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3"></path>
|
||||||
|
<path d="M13 7h7a1 1 0 0 1 1 1v8a1 1 0 0 1 -1 1h-7"></path>
|
||||||
|
<path d="M5 7h-1a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1h1"></path>
|
||||||
|
<path d="M17 12h.01"></path>
|
||||||
|
<path d="M13 12h.01"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 588 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path>
|
||||||
|
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path>
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||||
|
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 504 B |
@@ -1,5 +1,5 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-xl mx-auto px-2">
|
||||||
<h1 class="text-4xl font-bold text-center my-8">👋 Welcome to Docuseal</h1>
|
<h1 class="text-4xl font-bold text-center my-8">👋 Welcome to <%= Docuseal.product_name %></h1>
|
||||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
|
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= render 'devise/shared/error_messages', resource: %>
|
<%= render 'devise/shared/error_messages', resource: %>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<title>
|
||||||
|
DocuSeal | Open Source Document Filling and Signing
|
||||||
|
</title>
|
||||||
|
<%= render 'shared/meta' %>
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html data-theme="docuseal" lang="en">
|
<html data-theme="docuseal" lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>
|
<%= render 'layouts/head_tags' %>
|
||||||
DocuSeal | Open Source Document Filling and Signing
|
|
||||||
</title>
|
|
||||||
<%= render 'shared/meta' %>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
<%= javascript_pack_tag 'application', defer: true %>
|
<%= javascript_pack_tag 'application', defer: true %>
|
||||||
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
||||||
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
||||||
|
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<turbo-frame id="modal"></turbo-frame>
|
<turbo-frame id="modal"></turbo-frame>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html data-theme="docuseal" lang="en">
|
<html data-theme="docuseal" lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>
|
<%= render 'layouts/head_tags' %>
|
||||||
DocuSeal | Open Source Document Filling and Signing
|
|
||||||
</title>
|
|
||||||
<%= render 'shared/meta' %>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html data-theme="docuseal" lang="en">
|
<html data-theme="docuseal" lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>
|
<%= render 'layouts/head_tags' %>
|
||||||
DocuSeal | Open Source Document Filling and Signing
|
|
||||||
</title>
|
|
||||||
<%= render 'shared/meta' %>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
|
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
|
||||||
<p class="text-base text-gray-500">
|
<p class="text-base text-gray-500">
|
||||||
Run on your own host using <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a> container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
|
Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,4 +70,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<%= render 'shared/attribution' %>
|
<%= render 'shared/attribution', with_counter: true %>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
|
<input type="checkbox">
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
<div>
|
||||||
|
Completed Form Redirect Button
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_BUTTON_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.label :title, 'Button title', class: 'label' %>
|
||||||
|
<%= ff.text_field :title, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.label :url, 'Button URL', class: 'label' %>
|
||||||
|
<%= ff.url_field :url, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
||||||
<%= render 'logo_form' %>
|
<%= render 'logo_form' %>
|
||||||
|
<p class="text-4xl font-bold mb-4 mt-8">Submitter Form</p>
|
||||||
|
<%= render 'form_completed_button_form' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1 +1 @@
|
|||||||
<%= render 'shared/powered_by' %>
|
<%= render 'shared/powered_by', with_counter: local_assigns[:with_counter] %>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<% 'stats stat stat-figure stat-title stat-value text-accent' %>
|
||||||
@@ -3,8 +3,7 @@
|
|||||||
<div class="w-full text-center">
|
<div class="w-full text-center">
|
||||||
<span class="font-bold">Demo Environment</span>
|
<span class="font-bold">Demo Environment</span>
|
||||||
<br>
|
<br>
|
||||||
Feel free to
|
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form or
|
||||||
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">create a new template</a> document form or
|
|
||||||
<a href="<%= start_form_url(slug: ::Template.first&.slug) %>" target="_blank" class="inline underline font-medium">submit the existing one</a> 😊
|
<a href="<%= start_form_url(slug: ::Template.first&.slug) %>" target="_blank" class="inline underline font-medium">submit the existing one</a> 😊
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
---
|
---
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
|
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal.product_name %></a> free document signing.
|
||||||
</p>
|
</p>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
|
||||||
|
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
|
||||||
|
</a>
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
<div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
|
<div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
|
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
|
||||||
<%= render 'shared/logo' %>
|
<%= render 'shared/title' %>
|
||||||
<span>DocuSeal</span>
|
|
||||||
</a>
|
</a>
|
||||||
<% unless Docuseal.demo? %>
|
<%= render 'shared/github' %>
|
||||||
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
|
|
||||||
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
|
|
||||||
</a>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<% if signed_in? %>
|
<% if signed_in? %>
|
||||||
<div class="space-x-4 flex items-center">
|
<div class="space-x-4 flex items-center">
|
||||||
<% if Docuseal.demo? %>
|
<% if Docuseal.demo? %>
|
||||||
|
<a href="https://docuseal.co/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
|
||||||
|
Sign Up
|
||||||
|
</a>
|
||||||
|
<span class="hidden sm:inline">
|
||||||
<%= render 'shared/github_button' %>
|
<%= render 'shared/github_button' %>
|
||||||
|
</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex items-center justify-center space-x-4 mr-1">
|
<div class="flex items-center justify-center space-x-4 mr-1">
|
||||||
|
<% if can?(:manage, EncryptedConfig) && !can?(:manage, :tenants) %>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'hidden md:inline-flex items-center font-medium text-lg', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'hidden md:inline-flex items-center font-medium text-lg', data: { prefetch: false } do %>
|
||||||
Console
|
Console
|
||||||
<span class="badge badge-warning ml-1">New</span>
|
<span class="badge badge-warning ml-1">New</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
||||||
|
<% end %>
|
||||||
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
|
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -35,7 +37,7 @@
|
|||||||
<span class="mr-1">Profile</span>
|
<span class="mr-1">Profile</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% unless Docuseal.demo? %>
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, data: { prefetch: false }, class: 'flex items-center' do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, data: { prefetch: false }, class: 'flex items-center' do %>
|
||||||
<%= svg_icon('terminal', class: 'w-5 h-5 stroke-2') %>
|
<%= svg_icon('terminal', class: 'w-5 h-5 stroke-2') %>
|
||||||
@@ -43,12 +45,14 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to settings_esign_path, class: 'flex items-center' do %>
|
<%= link_to settings_esign_path, class: 'flex items-center' do %>
|
||||||
<%= svg_icon('zoom_check', class: 'w-5 h-5 stroke-2') %>
|
<%= svg_icon('zoom_check', class: 'w-5 h-5 stroke-2') %>
|
||||||
<span class="mr-1">Verify PDF</span>
|
<span class="mr-1">Verify PDF</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
<% end %>
|
||||||
<li>
|
<li>
|
||||||
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %>
|
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %>
|
||||||
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 mr-2 inline') %>
|
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 mr-2 inline') %>
|
||||||
@@ -59,18 +63,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex space-x-6">
|
<div class="flex space-x-2">
|
||||||
|
<% if request.path != new_user_session_path %>
|
||||||
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
||||||
<span class="flex items-center justify-center space-x-1">
|
<span class="flex items-center justify-center space-x-1">
|
||||||
<%= svg_icon('login', class: 'w-6 h-6') %>
|
<%= svg_icon('login', class: 'w-6 h-6') %>
|
||||||
<span>Sign in</span>
|
<span>Sign in</span>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if Docuseal.multitenant? %>
|
<% end %>
|
||||||
<%= link_to registration_path, class: 'font-medium text-lg hidden md:inline' do %>
|
<% if Docuseal.multitenant? && !request.path.in?([registration_path, new_registration_path]) %>
|
||||||
|
<%= link_to registration_path, class: 'btn btn-neutral btn-sm btn-outline' do %>
|
||||||
<span class="flex items-center justify-center space-x-1">
|
<span class="flex items-center justify-center space-x-1">
|
||||||
<%= svg_icon('user_plus', class: 'w-6 h-6') %>
|
<span>Create free Account</span>
|
||||||
<span>Try for Free</span>
|
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script defer data-domain="<%= ENV.fetch('PLAUSIBLE_DOMAIN', nil) %>" src="https://plausible.io/js/script.manual.js"></script>
|
||||||
|
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("turbo:load", function (e) {
|
||||||
|
if (e.detail.url.match(/sign_in|sign_up|password/)) {
|
||||||
|
plausible('pageview')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
<div class="text-center px-2">
|
<div class="text-center px-2">
|
||||||
|
<% if local_assigns[:with_counter] %>
|
||||||
|
<% count = Submitter.where.not(completed_at: nil).distinct.count(:submission_id) %>
|
||||||
|
<% if count > 1 %>
|
||||||
|
<b><%= count %></b> documents signed with
|
||||||
|
<% else %>
|
||||||
Powered by
|
Powered by
|
||||||
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal::PRODUCT_NAME %></a> - open source documents software
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
Powered by
|
||||||
|
<% end %>
|
||||||
|
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal.product_name %></a> - open source documents software
|
||||||
</div>
|
</div>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<form action="<%= request.path %>" method="get" class="items-center hidden md:flex">
|
<form action="<%= request.path %>" method="get" class="items-center hidden md:flex">
|
||||||
<div class="relative">
|
|
||||||
<% if params[:q].present? %>
|
<% if params[:q].present? %>
|
||||||
|
<div class="relative">
|
||||||
<a href="<%= request.path %>" title="Clear" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
<a href="<%= request.path %>" title="Clear" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
||||||
×
|
×
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12 <%= 'pl-8 input-bordered' if params[:q].present? %>">
|
<% end %>
|
||||||
|
<input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12 <%= 'pl-8 input-outlined' if params[:q].present? %>">
|
||||||
<button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]">
|
<button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]">
|
||||||
<span class="enabled">
|
<span class="enabled">
|
||||||
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
|
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
|
||||||
|
|||||||
@@ -41,6 +41,13 @@
|
|||||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||||
|
<li>
|
||||||
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
|
Plans
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||||
<% if can?(:read, AccessToken) %>
|
<% if can?(:read, AccessToken) %>
|
||||||
<li>
|
<li>
|
||||||
@@ -58,7 +65,8 @@
|
|||||||
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% unless Docuseal.demo? %>
|
<%= render 'shared/settings_nav_extra' %>
|
||||||
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
Console
|
Console
|
||||||
@@ -68,27 +76,31 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</menu-active>
|
</menu-active>
|
||||||
|
<% if !can?(:manage, :tenants) %>
|
||||||
<div class="mx-4 border-t border-base-300 hidden md:block">
|
<div class="mx-4 border-t border-base-300 hidden md:block">
|
||||||
<div class="text-sm mt-3">
|
<div class="text-sm mt-3">
|
||||||
Need help? Ask a question:
|
Need help? Ask a question:
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-3 space-x-3">
|
<div class="flex mt-3 space-x-3">
|
||||||
|
<div class="tooltip" data-tip="GitHub">
|
||||||
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
|
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
|
||||||
<%= svg_icon('brand_github', class: 'w-8 h-8') %>
|
<%= svg_icon('brand_github', class: 'w-8 h-8') %>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip" data-tip="Discord Community">
|
||||||
<a href="<%= Docuseal::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
|
<a href="<%= Docuseal::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
|
||||||
<%= svg_icon('brand_discord', class: 'w-8 h-8') %>
|
<%= svg_icon('brand_discord', class: 'w-8 h-8') %>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip" data-tip="Twitter">
|
||||||
<a href="<%= Docuseal::TWITTER_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
|
<a href="<%= Docuseal::TWITTER_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
|
||||||
<%= svg_icon('brand_twitter', class: 'w-8 h-8') %>
|
<%= svg_icon('brand_twitter', class: 'w-8 h-8') %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="https://twitter.com/intent/tweet?<%= { text: "Open source DocuSign alternative\n#{Docuseal::GITHUB_URL} #{Docuseal::TWITTER_HANDLE}" }.to_query %>" target="_blank" class="btn btn-neutral btn-outline w-full mt-5">
|
</div>
|
||||||
<%= svg_icon('send', class: 'h-5 w-5') %>
|
|
||||||
Tell about us
|
|
||||||
</a>
|
|
||||||
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
|
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
|
||||||
<%= Docuseal::SUPPORT_EMAIL %>
|
<%= Docuseal::SUPPORT_EMAIL %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
Whitespace-only changes.
@@ -0,0 +1,2 @@
|
|||||||
|
<%= render 'shared/logo' %>
|
||||||
|
<span>DocuSeal</span>
|
||||||
@@ -3,7 +3,9 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="text-center w-full space-y-6">
|
<div class="text-center w-full space-y-6">
|
||||||
<%= render 'banner' %>
|
<%= render 'banner' %>
|
||||||
|
<% unless @template.deleted_at? %>
|
||||||
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -12,11 +14,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
|
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
|
||||||
|
<% if @template.deleted_at? %>
|
||||||
|
<p class="text-sm">Form has been deleted by <span class="font-semibold"><%= @template.account.name %></span>.</p>
|
||||||
|
<% else %>
|
||||||
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p>
|
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% unless @template.deleted_at? %>
|
||||||
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
|
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
|
||||||
<div class="form-control !mt-0">
|
<div class="form-control !mt-0">
|
||||||
<%= f.label :email, class: 'label' %>
|
<%= f.label :email, class: 'label' %>
|
||||||
@@ -26,6 +33,7 @@
|
|||||||
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution' %>
|
<%= render 'shared/attribution' %>
|
||||||
@@ -28,8 +28,6 @@
|
|||||||
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
|
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex items-center px-0.5">
|
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
|
||||||
<%= Array.wrap(value).join(', ') %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
|
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
|
||||||
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600] %>
|
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %>
|
||||||
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
|
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
|
||||||
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
|
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
|
||||||
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
|
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
||||||
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
||||||
<span><%= Docuseal::PRODUCT_NAME %></span>
|
<span><%= Docuseal.product_name %></span>
|
||||||
</a>
|
</a>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
||||||
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form>
|
<% completed_button_params = submitter.submission.template.account.account_configs.find_by(key: AccountConfig::FORM_COMPLETED_BUTTON_KEY)&.value || {} %>
|
||||||
|
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= completed_button_params.to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
|
<div class="space-y-6 mx-auto">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<%= render 'start_form/banner' %>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-3">
|
||||||
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
|
||||||
|
<p class="text-sm">Form has been deleted by <span class="font-semibold"><%= @submitter.submission.template.account.name %></span>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= render 'shared/attribution' %>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
|
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
|
||||||
<% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||||
<div style="max-height: -webkit-fill-available;">
|
<div style="max-height: -webkit-fill-available;">
|
||||||
<div id="scrollbox">
|
<div id="scrollbox">
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||||
<% value = values[field['uuid']] %>
|
<% value = values[field['uuid']] %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
|
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
||||||
<p>Alternatively, you can download the copy using:</p>
|
<p>Alternatively, you can download your copy using:</p>
|
||||||
<% @documents.each do |document| %>
|
<% @documents.each do |document| %>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<% if @email_config || @body.present? %>
|
<% if @email_config || @body.present? %>
|
||||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@body.presence || @email_config.value['body'], submitter: @submitter)))) %>
|
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@body.presence || @email_config.value['body'], submitter: @submitter)))) %>
|
||||||
|
<% if !(@body.presence || @email_config.value['body']).include?(ReplaceEmailVariables::SUBMITTER_LINK) %>
|
||||||
|
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
||||||
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>You have been invited to submit the "<%= @submitter.submission.template.name %>" form</p>
|
<p>You have been invited to submit the "<%= @submitter.submission.template.name %>" form.</p>
|
||||||
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
||||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
||||||
<% if !submission.deleted_at? && can?(:destroy, submission) %>
|
<% if !submission.deleted_at? && can?(:destroy, submission) %>
|
||||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
||||||
<% unless submission.deleted_at? %>
|
<% unless submission.deleted_at? %>
|
||||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p>Hello <%= @user.first_name %>,</p>
|
<p>Hello <%= @user.first_name %>,</p>
|
||||||
<p>You have been invited to <%= @user.account.name %> DocuSeal. Please sign up using the link below:</p>
|
<p>You have been invited to <%= @user.account.name %> <%= Docuseal.product_name %>. Please sign up using the link below:</p>
|
||||||
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
|
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
|
||||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rollbar' if ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||||
|
|
||||||
if defined?(Rollbar)
|
if defined?(Rollbar)
|
||||||
Rollbar.configure do |config|
|
Rollbar.configure do |config|
|
||||||
config.access_token = ENV.fetch('ROLLBAR_ACCESS_TOKEN', nil)
|
config.access_token = ENV.fetch('ROLLBAR_ACCESS_TOKEN', nil)
|
||||||
|
|||||||
+8
-3
@@ -35,9 +35,14 @@ Rails.application.routes.draw do
|
|||||||
resources :template_folders_autocomplete, only: %i[index]
|
resources :template_folders_autocomplete, only: %i[index]
|
||||||
resources :submitter_email_clicks, only: %i[create]
|
resources :submitter_email_clicks, only: %i[create]
|
||||||
resources :submitter_form_views, only: %i[create]
|
resources :submitter_form_views, only: %i[create]
|
||||||
resources :submissions, only: %i[create]
|
resources :submitters, only: %i[show]
|
||||||
resources :templates, only: %i[update show index] do
|
resources :submissions, only: %i[index show create destroy] do
|
||||||
resources :submissions, only: %i[create]
|
collection do
|
||||||
|
resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails
|
||||||
|
end
|
||||||
|
end
|
||||||
|
resources :templates, only: %i[update show index destroy] do
|
||||||
|
resources :submissions, only: %i[index create]
|
||||||
resources :documents, only: %i[create], controller: 'templates_documents'
|
resources :documents, only: %i[create], controller: 'templates_documents'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class AddUuidToUsers < ActiveRecord::Migration[7.0]
|
|||||||
end
|
end
|
||||||
|
|
||||||
def up
|
def up
|
||||||
add_column :users, :uuid, :text
|
add_column :users, :uuid, :string
|
||||||
add_index :users, :uuid, unique: true
|
add_index :users, :uuid, unique: true
|
||||||
|
|
||||||
MigrationUser.all.each do |user|
|
MigrationUser.all.each do |user|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class AddSourceToSubmissions < ActiveRecord::Migration[7.0]
|
|||||||
end
|
end
|
||||||
|
|
||||||
def up
|
def up
|
||||||
add_column :submissions, :source, :text
|
add_column :submissions, :source, :string
|
||||||
|
|
||||||
MigrationSubmission.where(source: nil).update_all(source: :invite)
|
MigrationSubmission.where(source: nil).update_all(source: :invite)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class CreateAccessTokens < ActiveRecord::Migration[7.0]
|
|||||||
create_table :access_tokens do |t|
|
create_table :access_tokens do |t|
|
||||||
t.references :user, null: false, foreign_key: true, index: true
|
t.references :user, null: false, foreign_key: true, index: true
|
||||||
t.text :token, null: false
|
t.text :token, null: false
|
||||||
t.text :sha256, null: false, index: { unique: true }
|
t.string :sha256, null: false, index: { unique: true }
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UpdateCheckboxesRequired < ActiveRecord::Migration[7.0]
|
||||||
|
class MigrationTemplate < ApplicationRecord
|
||||||
|
self.table_name = 'templates'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
MigrationTemplate.find_each do |template|
|
||||||
|
fields = JSON.parse(template.fields)
|
||||||
|
|
||||||
|
fields.each do |field|
|
||||||
|
field['required'] = false if field['type'] == 'checkbox'
|
||||||
|
end
|
||||||
|
|
||||||
|
template.update_columns(fields: fields.to_json) if JSON.parse(template.fields) != fields
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_09_22_072041) do
|
ActiveRecord::Schema[7.0].define(version: 2023_10_07_052818) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -8,6 +8,7 @@ module Accounts
|
|||||||
|
|
||||||
new_user = account.users.first.dup
|
new_user = account.users.first.dup
|
||||||
|
|
||||||
|
new_user.uuid = SecureRandom.uuid
|
||||||
new_user.account = new_account
|
new_user.account = new_account
|
||||||
new_user.encrypted_password = SecureRandom.hex
|
new_user.encrypted_password = SecureRandom.hex
|
||||||
new_user.email = "#{SecureRandom.hex}@docuseal.co"
|
new_user.email = "#{SecureRandom.hex}@docuseal.co"
|
||||||
@@ -18,12 +19,14 @@ module Accounts
|
|||||||
new_template.account = new_account
|
new_template.account = new_account
|
||||||
new_template.slug = SecureRandom.base58(14)
|
new_template.slug = SecureRandom.base58(14)
|
||||||
|
|
||||||
|
new_template.deleted_at = nil
|
||||||
new_template.save!
|
new_template.save!
|
||||||
|
|
||||||
Templates::CloneAttachments.call(template: new_template, original_template: template)
|
Templates::CloneAttachments.call(template: new_template, original_template: template)
|
||||||
end
|
end
|
||||||
|
|
||||||
new_user.save!(validate: false)
|
new_user.save!(validate: false)
|
||||||
|
new_account.templates.update_all(folder_id: new_account.default_template_folder.id)
|
||||||
|
|
||||||
new_account
|
new_account
|
||||||
end
|
end
|
||||||
@@ -43,12 +46,19 @@ module Accounts
|
|||||||
new_template
|
new_template
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_webhook_configs(account)
|
||||||
|
account = Account.order(:id).first unless Docuseal.multitenant?
|
||||||
|
|
||||||
|
account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
end
|
||||||
|
|
||||||
def load_signing_pkcs(account)
|
def load_signing_pkcs(account)
|
||||||
cert_data =
|
cert_data =
|
||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
Docuseal::CERTS
|
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || Docuseal::CERTS
|
||||||
else
|
else
|
||||||
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY).value
|
EncryptedConfig.find_by(account: Account.order(:id).first,
|
||||||
|
key: EncryptedConfig::ESIGN_CERTS_KEY).value
|
||||||
end
|
end
|
||||||
|
|
||||||
if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' })
|
if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' })
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ module ActionMailerConfigsInterceptor
|
|||||||
address: value['host'],
|
address: value['host'],
|
||||||
port: value['port'],
|
port: value['port'],
|
||||||
domain: value['domain'],
|
domain: value['domain'],
|
||||||
|
openssl_verify_mode: OpenSSL::SSL::VERIFY_NONE,
|
||||||
authentication: value.fetch('authentication', 'plain'),
|
authentication: value.fetch('authentication', 'plain'),
|
||||||
enable_starttls_auto: true,
|
enable_starttls_auto: true,
|
||||||
ssl: value['security'] == 'ssl',
|
ssl: value['security'] == 'ssl',
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ module Docuseal
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def product_name
|
||||||
|
PRODUCT_NAME
|
||||||
|
end
|
||||||
|
|
||||||
def refresh_default_url_options!
|
def refresh_default_url_options!
|
||||||
@default_url_options = nil
|
@default_url_options = nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module GenerateCertificate
|
module GenerateCertificate
|
||||||
NAME = Docuseal::PRODUCT_NAME
|
|
||||||
SIZE = 2**11
|
SIZE = 2**11
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(name = NAME)
|
def call(name = Docuseal.product_name)
|
||||||
root_cert, root_key = generate_root_ca(name)
|
root_cert, root_key = generate_root_ca(name)
|
||||||
|
|
||||||
sub_cert, sub_key = generate_sub_ca(name, root_cert, root_key)
|
sub_cert, sub_key = generate_sub_ca(name, root_cert, root_key)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ module ReplaceEmailVariables
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(text, submitter:)
|
def call(text, submitter:, tracking_event_type: 'click_email')
|
||||||
submitter_link = build_submitter_link(submitter)
|
submitter_link = build_submitter_link(submitter, tracking_event_type)
|
||||||
|
|
||||||
submission_link = build_submission_link(submitter.submission) if submitter.submission
|
submission_link = build_submission_link(submitter.submission) if submitter.submission
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ module ReplaceEmailVariables
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_documents_links_text(submitter)
|
def build_documents_links_text(submitter)
|
||||||
submitter.documents.map do |document|
|
Submitters.select_attachments_for_download(submitter).map do |document|
|
||||||
link =
|
link =
|
||||||
Rails.application.routes.url_helpers.rails_blob_url(
|
Rails.application.routes.url_helpers.rails_blob_url(
|
||||||
document, **Docuseal.default_url_options
|
document, **Docuseal.default_url_options
|
||||||
@@ -43,12 +43,20 @@ module ReplaceEmailVariables
|
|||||||
end.join
|
end.join
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_submitter_link(submitter)
|
def build_submitter_link(submitter, tracking_event_type)
|
||||||
|
if tracking_event_type == 'click_email'
|
||||||
Rails.application.routes.url_helpers.submit_form_url(
|
Rails.application.routes.url_helpers.submit_form_url(
|
||||||
slug: submitter.slug,
|
slug: submitter.slug,
|
||||||
t: SubmissionEvents.build_tracking_param(submitter, 'click_email'),
|
t: SubmissionEvents.build_tracking_param(submitter, 'click_email'),
|
||||||
**Docuseal.default_url_options
|
**Docuseal.default_url_options
|
||||||
)
|
)
|
||||||
|
else
|
||||||
|
Rails.application.routes.url_helpers.submit_form_url(
|
||||||
|
slug: submitter.slug,
|
||||||
|
c: SubmissionEvents.build_tracking_param(submitter, 'click_sms'),
|
||||||
|
**Docuseal.default_url_options
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_submission_link(submission)
|
def build_submission_link(submission)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ module SubmissionEvents
|
|||||||
open_email: 'Email opened',
|
open_email: 'Email opened',
|
||||||
click_email: 'Email link clicked',
|
click_email: 'Email link clicked',
|
||||||
click_sms: 'SMS link clicked',
|
click_sms: 'SMS link clicked',
|
||||||
|
phone_verified: 'Phone verified',
|
||||||
start_form: 'Submission started',
|
start_form: 'Submission started',
|
||||||
view_form: 'Form viewed',
|
view_form: 'Form viewed',
|
||||||
complete_form: 'Submission completed'
|
complete_form: 'Submission completed'
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ module Submissions
|
|||||||
submission = template.submissions.new(created_by_user: user, source:,
|
submission = template.submissions.new(created_by_user: user, source:,
|
||||||
template_submitters: template.submitters, submitters_order:)
|
template_submitters: template.submitters, submitters_order:)
|
||||||
|
|
||||||
|
maybe_set_template_fields(submission, attrs[:submitters])
|
||||||
|
|
||||||
attrs[:submitters].each_with_index do |submitter_attrs, index|
|
attrs[:submitters].each_with_index do |submitter_attrs, index|
|
||||||
uuid =
|
uuid = find_submitter_uuid(template, submitter_attrs, index)
|
||||||
submitter_attrs[:uuid].presence ||
|
|
||||||
template.submitters.find { |e| e['name'] == submitter_attrs[:role] }&.dig('uuid') ||
|
|
||||||
template.submitters[index]&.dig('uuid')
|
|
||||||
|
|
||||||
next if uuid.blank?
|
next if uuid.blank?
|
||||||
|
|
||||||
@@ -26,6 +25,66 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_set_template_fields(submission, submitters_attrs)
|
||||||
|
template_fields = submission.template.fields.deep_dup
|
||||||
|
|
||||||
|
submitters_attrs.each_with_index do |submitter_attrs, index|
|
||||||
|
submitter_uuid = find_submitter_uuid(submission.template, submitter_attrs, index)
|
||||||
|
|
||||||
|
process_readonly_fields_param(submitter_attrs[:readonly_fields], template_fields, submitter_uuid)
|
||||||
|
|
||||||
|
process_fields_param(submitter_attrs[:fields], template_fields, submitter_uuid)
|
||||||
|
end
|
||||||
|
|
||||||
|
if template_fields != submission.template.fields
|
||||||
|
submission.template_fields = template_fields
|
||||||
|
submission.template_schema = submission.template.schema
|
||||||
|
end
|
||||||
|
|
||||||
|
submission
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_readonly_fields_param(readonly_fields, template_fields, submitter_uuid)
|
||||||
|
return if readonly_fields.blank?
|
||||||
|
|
||||||
|
template_fields.each do |f|
|
||||||
|
next if f['submitter_uuid'] != submitter_uuid ||
|
||||||
|
(!f['name'].in?(readonly_fields) &&
|
||||||
|
!f['name'].to_s.parameterize.underscore.in?(readonly_fields))
|
||||||
|
|
||||||
|
f['readonly'] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_fields_param(fields, template_fields, submitter_uuid)
|
||||||
|
return if fields.blank?
|
||||||
|
|
||||||
|
template_fields.each do |f|
|
||||||
|
next if f['submitter_uuid'] != submitter_uuid
|
||||||
|
|
||||||
|
field_configs = fields.find do |e|
|
||||||
|
e['name'] == f['name'] || e['name'] == f['name'].to_s.parameterize.underscore
|
||||||
|
end
|
||||||
|
|
||||||
|
next if field_configs.blank?
|
||||||
|
|
||||||
|
f['readonly'] = field_configs['readonly'] if field_configs['readonly'].present?
|
||||||
|
|
||||||
|
next if field_configs['validation_pattern'].blank?
|
||||||
|
|
||||||
|
f['validation'] = {
|
||||||
|
'pattern' => field_configs['validation_pattern'],
|
||||||
|
'message' => field_configs['invalid_message']
|
||||||
|
}.compact_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_submitter_uuid(template, attrs, index)
|
||||||
|
attrs[:uuid].presence ||
|
||||||
|
template.submitters.find { |e| e['name'] == attrs[:role] }&.dig('uuid') ||
|
||||||
|
template.submitters[index]&.dig('uuid')
|
||||||
|
end
|
||||||
|
|
||||||
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:)
|
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:)
|
||||||
email = Submissions.normalize_email(attrs[:email])
|
email = Submissions.normalize_email(attrs[:email])
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ module Submissions
|
|||||||
'Helvetica'
|
'Helvetica'
|
||||||
end
|
end
|
||||||
|
|
||||||
INFO_CREATOR = "#{Docuseal::PRODUCT_NAME} (#{Docuseal::PRODUCT_URL})".freeze
|
INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze
|
||||||
SIGN_REASON = 'Signed with DocuSeal.co'
|
SIGN_REASON = 'Signed with DocuSeal.co'
|
||||||
VERIFIED_TEXT = if Docuseal.multitenant?
|
VERIFIED_TEXT = if Docuseal.multitenant?
|
||||||
'Verified by DocuSeal'
|
'Verified by DocuSeal'
|
||||||
@@ -62,15 +62,7 @@ module Submissions
|
|||||||
composer.new_page
|
composer.new_page
|
||||||
|
|
||||||
composer.column(columns: 1) do |column|
|
composer.column(columns: 1) do |column|
|
||||||
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
add_logo(column)
|
||||||
|
|
||||||
column.formatted_text([{ text: 'DocuSeal',
|
|
||||||
link: Docuseal::PRODUCT_URL }],
|
|
||||||
font_size: 20,
|
|
||||||
font: [FONT_BOLD_NAME, { variant: :bold }],
|
|
||||||
width: 100,
|
|
||||||
padding: [12, 0, 0, 8],
|
|
||||||
position: :float, position_hint: :left)
|
|
||||||
|
|
||||||
column.text('Audit Log',
|
column.text('Audit Log',
|
||||||
font_size: 16,
|
font_size: 16,
|
||||||
@@ -92,7 +84,7 @@ module Submissions
|
|||||||
documents_data = Submitters.select_attachments_for_download(last_submitter).map do |document|
|
documents_data = Submitters.select_attachments_for_download(last_submitter).map do |document|
|
||||||
original_documents = submission.template.documents.select { |e| e.uuid == document.uuid }.presence
|
original_documents = submission.template.documents.select { |e| e.uuid == document.uuid }.presence
|
||||||
original_documents ||= submission.template.documents.select do |e|
|
original_documents ||= submission.template.documents.select do |e|
|
||||||
e.image? && submission.schema.any? do |item|
|
e.image? && submission.template_schema.any? do |item|
|
||||||
item['attachment_uuid'] == e.uuid
|
item['attachment_uuid'] == e.uuid
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -119,9 +111,11 @@ module Submissions
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if documents_data.present?
|
||||||
composer.table(documents_data, cell_style: { padding: [0, 0, 25, 0], border: { width: 0 } })
|
composer.table(documents_data, cell_style: { padding: [0, 0, 25, 0], border: { width: 0 } })
|
||||||
|
|
||||||
composer.draw_box(divider)
|
composer.draw_box(divider)
|
||||||
|
end
|
||||||
|
|
||||||
submission.template_submitters.filter_map do |item|
|
submission.template_submitters.filter_map do |item|
|
||||||
submitter = submission.submitters.find { |e| e.uuid == item['uuid'] }
|
submitter = submission.submitters.find { |e| e.uuid == item['uuid'] }
|
||||||
@@ -239,10 +233,10 @@ module Submissions
|
|||||||
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
|
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
|
||||||
font: [FONT_BOLD_NAME, { variant: :bold }] },
|
font: [FONT_BOLD_NAME, { variant: :bold }] },
|
||||||
event.event_type.include?('send_') ? ' to ' : ' by ',
|
event.event_type.include?('send_') ? ' to ' : ' by ',
|
||||||
if event.event_type.include?('sms')
|
if event.event_type.include?('sms') || event.event_type.include?('phone')
|
||||||
submitter.phone
|
submitter.phone
|
||||||
else
|
else
|
||||||
(submitter.email || submitter.name || submitter.phone)
|
(submitter.name || submitter.email || submitter.phone)
|
||||||
end
|
end
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -268,6 +262,18 @@ module Submissions
|
|||||||
record: submission
|
record: submission
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_logo(column)
|
||||||
|
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
||||||
|
|
||||||
|
column.formatted_text([{ text: 'DocuSeal',
|
||||||
|
link: Docuseal::PRODUCT_URL }],
|
||||||
|
font_size: 20,
|
||||||
|
font: [FONT_BOLD_NAME, { variant: :bold }],
|
||||||
|
width: 100,
|
||||||
|
padding: [12, 0, 0, 8],
|
||||||
|
position: :float, position_hint: :left)
|
||||||
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -10,7 +10,7 @@ module Submissions
|
|||||||
'Helvetica'
|
'Helvetica'
|
||||||
end
|
end
|
||||||
|
|
||||||
INFO_CREATOR = "#{Docuseal::PRODUCT_NAME} (#{Docuseal::PRODUCT_URL})".freeze
|
INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze
|
||||||
SIGN_REASON = 'Signed by %<email>s with DocuSeal.co'
|
SIGN_REASON = 'Signed by %<email>s with DocuSeal.co'
|
||||||
|
|
||||||
TEXT_LEFT_MARGIN = 1
|
TEXT_LEFT_MARGIN = 1
|
||||||
@@ -23,7 +23,6 @@ module Submissions
|
|||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def call(submitter)
|
def call(submitter)
|
||||||
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center)
|
|
||||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, align: :center)
|
cell_layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, align: :center)
|
||||||
|
|
||||||
template = submitter.submission.template
|
template = submitter.submission.template
|
||||||
@@ -38,7 +37,6 @@ module Submissions
|
|||||||
|
|
||||||
field.fetch('areas', []).each do |area|
|
field.fetch('areas', []).each do |area|
|
||||||
pdf = pdfs_index[area['attachment_uuid']]
|
pdf = pdfs_index[area['attachment_uuid']]
|
||||||
pdf.fonts.add(FONT_NAME)
|
|
||||||
|
|
||||||
page = pdf.pages[area['page']]
|
page = pdf.pages[area['page']]
|
||||||
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
||||||
@@ -50,6 +48,8 @@ module Submissions
|
|||||||
height = page.box.height
|
height = page.box.height
|
||||||
font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
||||||
|
|
||||||
|
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, font: pdf.fonts.add(FONT_NAME), font_size:)
|
||||||
|
|
||||||
value = submitter.values[field['uuid']]
|
value = submitter.values[field['uuid']]
|
||||||
|
|
||||||
next if Array.wrap(value).compact_blank.blank?
|
next if Array.wrap(value).compact_blank.blank?
|
||||||
@@ -203,9 +203,9 @@ module Submissions
|
|||||||
def save_signed_pdf(pdf:, submitter:, pkcs:, uuid:, name:)
|
def save_signed_pdf(pdf:, submitter:, pkcs:, uuid:, name:)
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
pdf.trailer.info[:Creator] = INFO_CREATOR
|
pdf.trailer.info[:Creator] = info_creator
|
||||||
|
|
||||||
pdf.sign(io, reason: format(SIGN_REASON, email: submitter.email),
|
pdf.sign(io, reason: sign_reason(submitter.email),
|
||||||
certificate: pkcs.certificate,
|
certificate: pkcs.certificate,
|
||||||
key: pkcs.key,
|
key: pkcs.key,
|
||||||
certificate_chain: pkcs.ca_certs || [])
|
certificate_chain: pkcs.ca_certs || [])
|
||||||
@@ -269,6 +269,14 @@ module Submissions
|
|||||||
pdf
|
pdf
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sign_reason(email)
|
||||||
|
format(SIGN_REASON, email:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def info_creator
|
||||||
|
INFO_CREATOR
|
||||||
|
end
|
||||||
|
|
||||||
def h
|
def h
|
||||||
Rails.application.routes.url_helpers
|
Rails.application.routes.url_helpers
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_fields_index(fields)
|
def build_fields_index(fields)
|
||||||
fields.index_by { |e| e['name'] }.merge(fields.index_by { |e| e['name'].parameterize.underscore })
|
fields.index_by { |e| e['name'] }.merge(fields.index_by { |e| e['name'].to_s.parameterize.underscore })
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_attachment_value(value, account)
|
def normalize_attachment_value(value, account)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module SerializeForApi
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(submitter, with_template: false, with_events: false)
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [submitter],
|
||||||
|
associations: [documents_attachments: :blob, attachments_attachments: :blob]
|
||||||
|
).call
|
||||||
|
|
||||||
|
values = SerializeForWebhook.build_values_array(submitter)
|
||||||
|
documents = SerializeForWebhook.build_documents_array(submitter)
|
||||||
|
|
||||||
|
submitter_name = (submitter.submission.template_submitters ||
|
||||||
|
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
|
||||||
|
|
||||||
|
serialize_params = {
|
||||||
|
include: {},
|
||||||
|
only: %i[id slug uuid name email phone completed_at
|
||||||
|
opened_at sent_at created_at updated_at]
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize_params[:include][:template] = { only: %i[id name created_at updated_at] } if with_template
|
||||||
|
|
||||||
|
if with_events
|
||||||
|
serialize_params[:include][:submission_events] =
|
||||||
|
{ as: :events, only: %i[id submitter_id event_type event_timestamp] }
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.as_json(serialize_params)
|
||||||
|
.merge('values' => values,
|
||||||
|
'documents' => documents,
|
||||||
|
'role' => submitter_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,6 +5,11 @@ module Submitters
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(submitter)
|
def call(submitter)
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [submitter],
|
||||||
|
associations: [documents_attachments: :blob, attachments_attachments: :blob]
|
||||||
|
).call
|
||||||
|
|
||||||
values = build_values_array(submitter)
|
values = build_values_array(submitter)
|
||||||
documents = build_documents_array(submitter)
|
documents = build_documents_array(submitter)
|
||||||
|
|
||||||
@@ -21,7 +26,7 @@ module Submitters
|
|||||||
def build_values_array(submitter)
|
def build_values_array(submitter)
|
||||||
fields_index = (submitter.submission.template_fields ||
|
fields_index = (submitter.submission.template_fields ||
|
||||||
submitter.submission.template.fields).index_by { |e| e['uuid'] }
|
submitter.submission.template.fields).index_by { |e| e['uuid'] }
|
||||||
attachments_index = submitter.attachments.preload(:blob).index_by(&:uuid)
|
attachments_index = submitter.attachments.index_by(&:uuid)
|
||||||
submitter_field_counters = Hash.new { 0 }
|
submitter_field_counters = Hash.new { 0 }
|
||||||
|
|
||||||
submitter.values.map do |uuid, value|
|
submitter.values.map do |uuid, value|
|
||||||
@@ -38,7 +43,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_documents_array(submitter)
|
def build_documents_array(submitter)
|
||||||
submitter.documents.preload(:blob).map do |attachment|
|
submitter.documents.map do |attachment|
|
||||||
{ name: attachment.filename.base, url: rails_storage_proxy_url(attachment) }
|
{ name: attachment.filename.base, url: rails_storage_proxy_url(attachment) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ module Submitters
|
|||||||
def update_submitter!(submitter, params, request)
|
def update_submitter!(submitter, params, request)
|
||||||
values = normalized_values(params)
|
values = normalized_values(params)
|
||||||
|
|
||||||
validate_values!(values, submitter, params)
|
|
||||||
|
|
||||||
submitter.values.merge!(values)
|
submitter.values.merge!(values)
|
||||||
submitter.opened_at ||= Time.current
|
submitter.opened_at ||= Time.current
|
||||||
|
|
||||||
@@ -38,11 +36,15 @@ module Submitters
|
|||||||
submitter.completed_at = Time.current
|
submitter.completed_at = Time.current
|
||||||
submitter.ip = request.remote_ip
|
submitter.ip = request.remote_ip
|
||||||
submitter.ua = request.user_agent
|
submitter.ua = request.user_agent
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
validate_values!(values, submitter, params, request)
|
||||||
|
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) if params[:completed] == 'true'
|
||||||
|
|
||||||
submitter.save!
|
submitter.save!
|
||||||
|
end
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
@@ -59,15 +61,15 @@ module Submitters
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_values!(values, submitter, params)
|
def validate_values!(values, submitter, params, request)
|
||||||
values.each do |key, value|
|
values.each do |key, value|
|
||||||
field = submitter.submission.template_fields.find { |e| e['uuid'] == key }
|
field = submitter.submission.template_fields.find { |e| e['uuid'] == key }
|
||||||
|
|
||||||
validate_value!(value, field, params)
|
validate_value!(value, field, params, submitter, request)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_value!(_value, _field, _params)
|
def validate_value!(_value, _field, _params, _submitter, _request)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user