mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
48
Commits
44dcc7f312
...
3.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c65b0e4ba | ||
|
|
ced5ab67ab | ||
|
|
28c4177128 | ||
|
|
5d86a08b5f | ||
|
|
a69fb81429 | ||
|
|
e6ac3380d4 | ||
|
|
3a535e7112 | ||
|
|
7464f9cfad | ||
|
|
58e24ef758 | ||
|
|
c1d809f989 | ||
|
|
80b24f0e2a | ||
|
|
2e253904a3 | ||
|
|
a7f381caf0 | ||
|
|
c6a7555f54 | ||
|
|
0772880bbc | ||
|
|
73339d8cd9 | ||
|
|
25504e8cb1 | ||
|
|
63a874528a | ||
|
|
c1c99621ff | ||
|
|
5c168f4a29 | ||
|
|
bef825c4e0 | ||
|
|
a990d22831 | ||
|
|
1687a372ae | ||
|
|
9b267d4a07 | ||
|
|
2189c2e9fe | ||
|
|
1e43fe8969 | ||
|
|
f77a716658 | ||
|
|
a70ac571e3 | ||
|
|
4f5e72f983 | ||
|
|
8f47ec6d4e | ||
|
|
9e1bbf2944 | ||
|
|
aa7d6e9aaa | ||
|
|
57c8be33ce | ||
|
|
ed7109bfc6 | ||
|
|
9e2590489b | ||
|
|
d793f00b37 | ||
|
|
5896c9a2aa | ||
|
|
78b0ce9828 | ||
|
|
e187644716 | ||
|
|
a847e7aa9c | ||
|
|
0905e7d215 | ||
|
|
91147d2f48 | ||
|
|
acce6195b0 | ||
|
|
ce9ab691ec | ||
|
|
4190544edc | ||
|
|
db8e5efbd5 | ||
|
|
a5c6f4e40c | ||
|
|
7537d05d1f |
No files matched your search
+6
-6
@@ -497,7 +497,7 @@ GEM
|
|||||||
ruby-vips (2.3.0)
|
ruby-vips (2.3.0)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
rubyzip (3.2.2)
|
rubyzip (3.7.0)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
semantic_range (3.1.1)
|
semantic_range (3.1.1)
|
||||||
shakapacker (9.7.0)
|
shakapacker (9.7.0)
|
||||||
@@ -524,11 +524,11 @@ GEM
|
|||||||
simplecov-html (0.13.2)
|
simplecov-html (0.13.2)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
sqlite3 (2.9.5-aarch64-linux-gnu)
|
sqlite3 (2.9.6-aarch64-linux-gnu)
|
||||||
sqlite3 (2.9.5-aarch64-linux-musl)
|
sqlite3 (2.9.6-aarch64-linux-musl)
|
||||||
sqlite3 (2.9.5-arm64-darwin)
|
sqlite3 (2.9.6-arm64-darwin)
|
||||||
sqlite3 (2.9.5-x86_64-linux-gnu)
|
sqlite3 (2.9.6-x86_64-linux-gnu)
|
||||||
sqlite3 (2.9.5-x86_64-linux-musl)
|
sqlite3 (2.9.6-x86_64-linux-musl)
|
||||||
strip_attributes (2.0.1)
|
strip_attributes (2.0.1)
|
||||||
activemodel (>= 3.0, < 9.0)
|
activemodel (>= 3.0, < 9.0)
|
||||||
strscan (3.1.8)
|
strscan (3.1.8)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||||
- Automatic PDF eSignature
|
- Automatic PDF eSignature
|
||||||
- PDF signature verification
|
- PDF signature verification
|
||||||
- Users management
|
- User management
|
||||||
- Mobile-optimized
|
- Mobile-optimized
|
||||||
- 7 UI languages with signing available in 14 languages
|
- 7 UI languages with signing available in 14 languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ module Api
|
|||||||
|
|
||||||
render json: build_create_json(submissions)
|
render json: build_create_json(submissions)
|
||||||
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
||||||
DownloadUtils::UnableToDownload => e
|
Submitters::SubmitValues::ValidationError, DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ module Api
|
|||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
|
||||||
with_events: false, params:)
|
with_events: false, params:)
|
||||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
rescue Submitters::NormalizeValues::BaseError, Submitters::SubmitValues::ValidationError,
|
||||||
|
DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
|
|||||||
@@ -118,7 +118,12 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sign_in_for_demo
|
def sign_in_for_demo
|
||||||
sign_in(User.active.order('random()').take) unless signed_in?
|
return if signed_in?
|
||||||
|
|
||||||
|
account = Account.create!(name: "Demo #{SecureRandom.hex(3)}")
|
||||||
|
|
||||||
|
sign_in(account.users.create!(first_name: 'Demo', last_name: 'User',
|
||||||
|
email: "#{SecureRandom.hex}@docuseal.com", password: SecureRandom.hex))
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ class SubmissionsController < ApplicationController
|
|||||||
create_submissions(@template, submissions_params, params)
|
create_submissions(@template, submissions_params, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
raise Submissions::CreateFromSubmitters::BaseError, 'Invalid email address' if submissions.blank?
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ class SubmitFormEmail2fasController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
RateLimit.call("verify-2fa-code-1h-#{@submitter.id}", limit: 10, ttl: 1.hour, enabled: true)
|
|
||||||
RateLimit.call("verify-2fa-code-1d-#{@submitter.id}", limit: 20, ttl: 1.day, enabled: true)
|
|
||||||
|
|
||||||
value = [@submitter.email.downcase.strip, @submitter.slug].join(':')
|
value = [@submitter.email.downcase.squish, @submitter.slug].join(':')
|
||||||
|
|
||||||
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
|
if OtpChallenges.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value, purpose: 'form_email_2fa')
|
||||||
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
||||||
|
|
||||||
cookies.encrypted[:email_2fa_slug] =
|
cookies.encrypted[:email_2fa_slug] =
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ class SubmitFormMetadataController < ApplicationController
|
|||||||
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: %i[blob record]).call
|
ActiveRecord::Associations::Preloader.new(records: documents, associations: :blob).call
|
||||||
|
|
||||||
text_runs = documents.to_h do |document|
|
text_runs = documents.to_h do |document|
|
||||||
[
|
[
|
||||||
document.uuid,
|
document.uuid,
|
||||||
DocumentMetadatas.find_or_create_for_document(document, account_id: document.record.account_id).text_runs
|
DocumentMetadatas.find_or_create_for_document(document, account_id: submission.account_id).text_runs
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ class SubmitFormPrefillAttachmentsController < ActionController::API
|
|||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
unless can_upload?(submitter)
|
unless can_upload?(@submitter)
|
||||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
uuid, slug = ApplicationRecord.signed_id_verifier.verify(params[:prefill_token], purpose: :prefill_attachment)
|
uuid, slug = ApplicationRecord.signed_id_verifier.verify(params[:prefill_token], purpose: :prefill_attachment)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if slug != submitter.slug
|
raise ActiveRecord::RecordNotFound if slug != @submitter.slug
|
||||||
|
|
||||||
source_attachment = ActiveStorage::Attachment.find_by!(uuid:)
|
source_attachment = ActiveStorage::Attachment.find_by!(uuid:)
|
||||||
|
|
||||||
attachment = submitter.attachments_attachments.find_or_create_by!(blob_id: source_attachment.blob_id)
|
attachment = @submitter.attachments_attachments.find_or_create_by!(blob_id: source_attachment.blob_id)
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -81,9 +81,26 @@ class SubmittersController < ApplicationController
|
|||||||
|
|
||||||
submitter.name = attrs[:name] if attrs.key?(:name)
|
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||||
|
|
||||||
|
if submitter_changed?(submitter)
|
||||||
|
submitter.submitter_versions.new(slug: submitter.slug, email: submitter.email_was,
|
||||||
|
name: submitter.name_was, phone: submitter.phone_was)
|
||||||
|
|
||||||
|
submitter.slug = SecureRandom.base58(14)
|
||||||
|
submitter.values = Submitters.fetch_values_for_delegate(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def submitter_changed?(submitter)
|
||||||
|
if submitter.email_was.blank? && submitter.phone_was.blank?
|
||||||
|
submitter.name_was.present? && submitter.name_changed?
|
||||||
|
else
|
||||||
|
(submitter.email_was.present? && submitter.email_changed?) ||
|
||||||
|
(submitter.phone_was.present? && submitter.phone_changed?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
|
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
documents = @template.schema.map do |item|
|
||||||
|
if item['dynamic']
|
||||||
|
dynamic_document = @template.schema_dynamic_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||||
|
|
||||||
|
DynamicDocuments::EnsureVersionGenerated.call(dynamic_document).document_attachment
|
||||||
|
else
|
||||||
|
@template.schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ import PromptPassword from './elements/prompt_password'
|
|||||||
import EmailsTextarea from './elements/emails_textarea'
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
import ToggleSubmit from './elements/toggle_submit'
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
|
import ToggleOnChecked from './elements/toggle_on_checked'
|
||||||
|
import ToggleSendTitle from './elements/toggle_send_title'
|
||||||
|
import AppendTemplate from './elements/append_template'
|
||||||
import CheckOnClick from './elements/check_on_click'
|
import CheckOnClick from './elements/check_on_click'
|
||||||
import PasswordInput from './elements/password_input'
|
import PasswordInput from './elements/password_input'
|
||||||
import SearchInput from './elements/search_input'
|
import SearchInput from './elements/search_input'
|
||||||
@@ -159,6 +162,9 @@ safeRegisterElement('emails-textarea', EmailsTextarea)
|
|||||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||||
|
safeRegisterElement('toggle-on-checked', ToggleOnChecked)
|
||||||
|
safeRegisterElement('toggle-send-title', ToggleSendTitle)
|
||||||
|
safeRegisterElement('append-template', AppendTemplate)
|
||||||
safeRegisterElement('password-input', PasswordInput)
|
safeRegisterElement('password-input', PasswordInput)
|
||||||
safeRegisterElement('search-input', SearchInput)
|
safeRegisterElement('search-input', SearchInput)
|
||||||
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.addEventListener('click', this.onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.removeEventListener('click', this.onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const target = document.getElementById(this.dataset.targetId)
|
||||||
|
const item = this.querySelector('template').content.firstElementChild.cloneNode(true)
|
||||||
|
|
||||||
|
item.querySelector('a').addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
item.remove()
|
||||||
|
|
||||||
|
target.classList.toggle('hidden', !target.childElementCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
target.append(item)
|
||||||
|
target.classList.remove('hidden')
|
||||||
|
|
||||||
|
item.querySelector('input').focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { actionable } from '@github/catalyst/lib/actionable'
|
|||||||
import { targets, targetable } from '@github/catalyst/lib/targetable'
|
import { targets, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
|
||||||
export default actionable(targetable(class extends HTMLElement {
|
export default actionable(targetable(class extends HTMLElement {
|
||||||
static [targets.static] = ['items']
|
static [targets.static] = ['items', 'groups']
|
||||||
|
|
||||||
addItem (e) {
|
addItem (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -24,11 +24,28 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden'))
|
duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden'))
|
||||||
|
|
||||||
originalItem.parentNode.append(duplicateItem)
|
originalItem.parentNode.append(duplicateItem)
|
||||||
|
|
||||||
|
this.toggleMultipleClasses()
|
||||||
|
|
||||||
|
duplicateItem.querySelector("input:not([type='hidden'])")?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
removeItem (e) {
|
removeItem (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
this.items.find((item) => item.contains(e.target))?.remove()
|
this.items.find((item) => item.contains(e.target))?.remove()
|
||||||
|
|
||||||
|
this.toggleMultipleClasses()
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMultipleClasses () {
|
||||||
|
const isMultiple = this.items.length > 1
|
||||||
|
const elements = [...this.items, ...this.groups]
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
const classes = element.dataset.multipleClasses || this.dataset.multipleClasses
|
||||||
|
|
||||||
|
classes?.split(' ').forEach((cls) => element.classList.toggle(cls, isMultiple))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.form.addEventListener('change', this.toggle)
|
||||||
|
|
||||||
|
this.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.form.removeEventListener('change', this.toggle)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
const selector = this.dataset.names.split(' ').map((name) => `[name="${name}"]:checked:enabled`).join(', ')
|
||||||
|
const isChecked = !!this.form.querySelector(selector)
|
||||||
|
|
||||||
|
this.firstElementChild.classList.toggle('hidden', !isChecked)
|
||||||
|
this.lastElementChild.classList.toggle('hidden', isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.events.forEach((event) => this.form.addEventListener(event, this.toggle))
|
||||||
|
|
||||||
|
this.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.events.forEach((event) => this.form.removeEventListener(event, this.toggle))
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle = (event) => {
|
||||||
|
if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return
|
||||||
|
|
||||||
|
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field)
|
||||||
|
|
||||||
|
const isSend = (this.isChecked('send_email') && !isNameOnly('email')) ||
|
||||||
|
(this.isChecked('send_sms') && !isNameOnly('phone'))
|
||||||
|
|
||||||
|
this.firstElementChild.classList.toggle('hidden', !isSend)
|
||||||
|
this.lastElementChild.classList.toggle('hidden', isSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
isChecked (name) {
|
||||||
|
return !!this.form.querySelector(`[name="${name}"]:checked:enabled`)
|
||||||
|
}
|
||||||
|
|
||||||
|
isFilled (field) {
|
||||||
|
return [...this.form.querySelectorAll(this.fieldSelector(field))].some((input) => input.value.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldSelector (field) {
|
||||||
|
return `[name$="[][${field}]"]`
|
||||||
|
}
|
||||||
|
|
||||||
|
get events () {
|
||||||
|
return ['change', 'input', 'linked-input.update']
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@ const ICONS = {
|
|||||||
user: 'person',
|
user: 'person',
|
||||||
users: 'person.2',
|
users: 'person.2',
|
||||||
users_plus: 'person.badge.plus',
|
users_plus: 'person.badge.plus',
|
||||||
|
user_plus: 'person.badge.plus',
|
||||||
|
eye_plus: 'eye',
|
||||||
|
mail_plus: 'envelope',
|
||||||
photo: 'photo',
|
photo: 'photo',
|
||||||
settings: 'gearshape',
|
settings: 'gearshape',
|
||||||
logout: 'rectangle.portrait.and.arrow.right',
|
logout: 'rectangle.portrait.and.arrow.right',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const en = {
|
|||||||
must_be_valid_email: 'Must be a valid email address',
|
must_be_valid_email: 'Must be a valid email address',
|
||||||
must_be_valid_url: 'Must be a valid URL (starting with http:// or https://)',
|
must_be_valid_url: 'Must be a valid URL (starting with http:// or https://)',
|
||||||
must_be_valid_zip: 'Must be a valid ZIP code (XXXXX or XXXXX-XXXX)',
|
must_be_valid_zip: 'Must be a valid ZIP code (XXXXX or XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Must be a valid IBAN',
|
||||||
must_contain_numbers_only: 'Must contain numbers only',
|
must_contain_numbers_only: 'Must contain numbers only',
|
||||||
must_contain_letters_only: 'Must contain letters only',
|
must_contain_letters_only: 'Must contain letters only',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||||
@@ -132,6 +133,7 @@ const es = {
|
|||||||
must_be_valid_email: 'Debe ser una dirección de correo electrónico válida',
|
must_be_valid_email: 'Debe ser una dirección de correo electrónico válida',
|
||||||
must_be_valid_url: 'Debe ser una URL válida (que comience con http:// o https://)',
|
must_be_valid_url: 'Debe ser una URL válida (que comience con http:// o https://)',
|
||||||
must_be_valid_zip: 'Debe ser un ZIP válido (XXXXX o XXXXX-XXXX)',
|
must_be_valid_zip: 'Debe ser un ZIP válido (XXXXX o XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Debe ser un IBAN válido',
|
||||||
must_contain_numbers_only: 'Debe contener solo números',
|
must_contain_numbers_only: 'Debe contener solo números',
|
||||||
must_contain_letters_only: 'Debe contener solo letras',
|
must_contain_letters_only: 'Debe contener solo letras',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||||
@@ -251,6 +253,7 @@ const it = {
|
|||||||
must_be_valid_email: 'Deve essere un indirizzo email valido',
|
must_be_valid_email: 'Deve essere un indirizzo email valido',
|
||||||
must_be_valid_url: 'Deve essere un URL valido (che inizia con http:// o https://)',
|
must_be_valid_url: 'Deve essere un URL valido (che inizia con http:// o https://)',
|
||||||
must_be_valid_zip: 'Deve essere uno ZIP valido (XXXXX o XXXXX-XXXX)',
|
must_be_valid_zip: 'Deve essere uno ZIP valido (XXXXX o XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Deve essere un IBAN valido',
|
||||||
must_contain_numbers_only: 'Deve contenere solo numeri',
|
must_contain_numbers_only: 'Deve contenere solo numeri',
|
||||||
must_contain_letters_only: 'Deve contenere solo lettere',
|
must_contain_letters_only: 'Deve contenere solo lettere',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||||
@@ -370,6 +373,7 @@ const de = {
|
|||||||
must_be_valid_email: 'Muss eine gültige E-Mail-Adresse sein',
|
must_be_valid_email: 'Muss eine gültige E-Mail-Adresse sein',
|
||||||
must_be_valid_url: 'Muss eine gültige URL sein (beginnend mit http:// oder https://)',
|
must_be_valid_url: 'Muss eine gültige URL sein (beginnend mit http:// oder https://)',
|
||||||
must_be_valid_zip: 'Muss eine gültige ZIP sein (XXXXX oder XXXXX-XXXX)',
|
must_be_valid_zip: 'Muss eine gültige ZIP sein (XXXXX oder XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Muss eine gültige IBAN sein',
|
||||||
must_contain_numbers_only: 'Darf nur Zahlen enthalten',
|
must_contain_numbers_only: 'Darf nur Zahlen enthalten',
|
||||||
must_contain_letters_only: 'Darf nur Buchstaben enthalten',
|
must_contain_letters_only: 'Darf nur Buchstaben enthalten',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
||||||
@@ -489,6 +493,7 @@ const fr = {
|
|||||||
must_be_valid_email: 'Doit être une adresse e-mail valide',
|
must_be_valid_email: 'Doit être une adresse e-mail valide',
|
||||||
must_be_valid_url: 'Doit être une URL valide (commençant par http:// ou https://)',
|
must_be_valid_url: 'Doit être une URL valide (commençant par http:// ou https://)',
|
||||||
must_be_valid_zip: 'Doit être un ZIP valide (XXXXX ou XXXXX-XXXX)',
|
must_be_valid_zip: 'Doit être un ZIP valide (XXXXX ou XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Doit être un IBAN valide',
|
||||||
must_contain_numbers_only: 'Doit contenir uniquement des chiffres',
|
must_contain_numbers_only: 'Doit contenir uniquement des chiffres',
|
||||||
must_contain_letters_only: 'Doit contenir uniquement des lettres',
|
must_contain_letters_only: 'Doit contenir uniquement des lettres',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
||||||
@@ -608,6 +613,7 @@ const pl = {
|
|||||||
must_be_valid_email: 'Musi być prawidłowym adresem e-mail',
|
must_be_valid_email: 'Musi być prawidłowym adresem e-mail',
|
||||||
must_be_valid_url: 'Musi być prawidłowym adresem URL (zaczynającym się od http:// lub https://)',
|
must_be_valid_url: 'Musi być prawidłowym adresem URL (zaczynającym się od http:// lub https://)',
|
||||||
must_be_valid_zip: 'Musi być prawidłowym ZIP (XXXXX lub XXXXX-XXXX)',
|
must_be_valid_zip: 'Musi być prawidłowym ZIP (XXXXX lub XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Musi być prawidłowym numerem IBAN',
|
||||||
must_contain_numbers_only: 'Może zawierać tylko cyfry',
|
must_contain_numbers_only: 'Może zawierać tylko cyfry',
|
||||||
must_contain_letters_only: 'Może zawierać tylko litery',
|
must_contain_letters_only: 'Może zawierać tylko litery',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||||
@@ -727,6 +733,7 @@ const uk = {
|
|||||||
must_be_valid_email: 'Має бути дійсною адресою електронної пошти',
|
must_be_valid_email: 'Має бути дійсною адресою електронної пошти',
|
||||||
must_be_valid_url: 'Має бути дійсним URL (починаючи з http:// або https://)',
|
must_be_valid_url: 'Має бути дійсним URL (починаючи з http:// або https://)',
|
||||||
must_be_valid_zip: 'Має бути дійсним ZIP (XXXXX або XXXXX-XXXX)',
|
must_be_valid_zip: 'Має бути дійсним ZIP (XXXXX або XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Має бути дійсним IBAN',
|
||||||
must_contain_numbers_only: 'Має містити лише цифри',
|
must_contain_numbers_only: 'Має містити лише цифри',
|
||||||
must_contain_letters_only: 'Має містити лише літери',
|
must_contain_letters_only: 'Має містити лише літери',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||||
@@ -846,6 +853,7 @@ const cs = {
|
|||||||
must_be_valid_email: 'Musí být platná e-mailová adresa',
|
must_be_valid_email: 'Musí být platná e-mailová adresa',
|
||||||
must_be_valid_url: 'Musí být platná URL (začínající http:// nebo https://)',
|
must_be_valid_url: 'Musí být platná URL (začínající http:// nebo https://)',
|
||||||
must_be_valid_zip: 'Musí být platný ZIP (XXXXX nebo XXXXX-XXXX)',
|
must_be_valid_zip: 'Musí být platný ZIP (XXXXX nebo XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Musí být platný IBAN',
|
||||||
must_contain_numbers_only: 'Musí obsahovat pouze čísla',
|
must_contain_numbers_only: 'Musí obsahovat pouze čísla',
|
||||||
must_contain_letters_only: 'Musí obsahovat pouze písmena',
|
must_contain_letters_only: 'Musí obsahovat pouze písmena',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||||
@@ -965,6 +973,7 @@ const pt = {
|
|||||||
must_be_valid_email: 'Deve ser um endereço de e-mail válido',
|
must_be_valid_email: 'Deve ser um endereço de e-mail válido',
|
||||||
must_be_valid_url: 'Deve ser uma URL válida (começando com http:// ou https://)',
|
must_be_valid_url: 'Deve ser uma URL válida (começando com http:// ou https://)',
|
||||||
must_be_valid_zip: 'Deve ser um ZIP válido (XXXXX ou XXXXX-XXXX)',
|
must_be_valid_zip: 'Deve ser um ZIP válido (XXXXX ou XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Deve ser um IBAN válido',
|
||||||
must_contain_numbers_only: 'Deve conter apenas números',
|
must_contain_numbers_only: 'Deve conter apenas números',
|
||||||
must_contain_letters_only: 'Deve conter apenas letras',
|
must_contain_letters_only: 'Deve conter apenas letras',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||||
@@ -1084,6 +1093,7 @@ const he = {
|
|||||||
must_be_valid_email: 'חייבת להיות כתובת אימייל חוקית',
|
must_be_valid_email: 'חייבת להיות כתובת אימייל חוקית',
|
||||||
must_be_valid_url: 'חייבת להיות כתובת URL חוקית (המתחילה ב-http:// או https://)',
|
must_be_valid_url: 'חייבת להיות כתובת URL חוקית (המתחילה ב-http:// או https://)',
|
||||||
must_be_valid_zip: 'חייב להיות ZIP חוקי (XXXXX או XXXXX-XXXX)',
|
must_be_valid_zip: 'חייב להיות ZIP חוקי (XXXXX או XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'חייב להיות IBAN חוקי',
|
||||||
must_contain_numbers_only: 'חייב להכיל מספרים בלבד',
|
must_contain_numbers_only: 'חייב להכיל מספרים בלבד',
|
||||||
must_contain_letters_only: 'חייב להכיל אותיות בלבד',
|
must_contain_letters_only: 'חייב להכיל אותיות בלבד',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||||
@@ -1203,6 +1213,7 @@ const nl = {
|
|||||||
must_be_valid_email: 'Moet een geldig e-mailadres zijn',
|
must_be_valid_email: 'Moet een geldig e-mailadres zijn',
|
||||||
must_be_valid_url: 'Moet een geldige URL zijn (beginnend met http:// of https://)',
|
must_be_valid_url: 'Moet een geldige URL zijn (beginnend met http:// of https://)',
|
||||||
must_be_valid_zip: 'Moet een geldige ZIP zijn (XXXXX of XXXXX-XXXX)',
|
must_be_valid_zip: 'Moet een geldige ZIP zijn (XXXXX of XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Moet een geldig IBAN zijn',
|
||||||
must_contain_numbers_only: 'Mag alleen cijfers bevatten',
|
must_contain_numbers_only: 'Mag alleen cijfers bevatten',
|
||||||
must_contain_letters_only: 'Mag alleen letters bevatten',
|
must_contain_letters_only: 'Mag alleen letters bevatten',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||||
@@ -1322,6 +1333,7 @@ const ar = {
|
|||||||
must_be_valid_email: 'يجب أن يكون عنوان بريد إلكتروني صالحًا',
|
must_be_valid_email: 'يجب أن يكون عنوان بريد إلكتروني صالحًا',
|
||||||
must_be_valid_url: 'يجب أن يكون عنوان URL صالحًا (يبدأ بـ http:// أو https://)',
|
must_be_valid_url: 'يجب أن يكون عنوان URL صالحًا (يبدأ بـ http:// أو https://)',
|
||||||
must_be_valid_zip: 'يجب أن يكون ZIP صالحًا (XXXXX أو XXXXX-XXXX)',
|
must_be_valid_zip: 'يجب أن يكون ZIP صالحًا (XXXXX أو XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'يجب أن يكون IBAN صالحًا',
|
||||||
must_contain_numbers_only: 'يجب أن يحتوي على أرقام فقط',
|
must_contain_numbers_only: 'يجب أن يحتوي على أرقام فقط',
|
||||||
must_contain_letters_only: 'يجب أن يحتوي على أحرف فقط',
|
must_contain_letters_only: 'يجب أن يحتوي على أحرف فقط',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||||
@@ -1441,6 +1453,7 @@ const ko = {
|
|||||||
must_be_valid_email: '유효한 이메일 주소여야 합니다',
|
must_be_valid_email: '유효한 이메일 주소여야 합니다',
|
||||||
must_be_valid_url: '유효한 URL이어야 합니다 (http:// 또는 https://로 시작)',
|
must_be_valid_url: '유효한 URL이어야 합니다 (http:// 또는 https://로 시작)',
|
||||||
must_be_valid_zip: '유효한 ZIP이어야 합니다 (XXXXX 또는 XXXXX-XXXX)',
|
must_be_valid_zip: '유효한 ZIP이어야 합니다 (XXXXX 또는 XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: '유효한 IBAN이어야 합니다',
|
||||||
must_contain_numbers_only: '숫자만 포함해야 합니다',
|
must_contain_numbers_only: '숫자만 포함해야 합니다',
|
||||||
must_contain_letters_only: '문자만 포함해야 합니다',
|
must_contain_letters_only: '문자만 포함해야 합니다',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||||
@@ -1560,6 +1573,7 @@ const ja = {
|
|||||||
must_be_valid_email: '有効なメールアドレスである必要があります',
|
must_be_valid_email: '有効なメールアドレスである必要があります',
|
||||||
must_be_valid_url: '有効なURLである必要があります (http://またはhttps://で始まる)',
|
must_be_valid_url: '有効なURLである必要があります (http://またはhttps://で始まる)',
|
||||||
must_be_valid_zip: '有効なZIPである必要があります (XXXXX または XXXXX-XXXX)',
|
must_be_valid_zip: '有効なZIPである必要があります (XXXXX または XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: '有効なIBANである必要があります',
|
||||||
must_contain_numbers_only: '数字のみを含む必要があります',
|
must_contain_numbers_only: '数字のみを含む必要があります',
|
||||||
must_contain_letters_only: '文字のみを含む必要があります',
|
must_contain_letters_only: '文字のみを含む必要があります',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ export default {
|
|||||||
this.clearCountdown()
|
this.clearCountdown()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = { ...this.form, submitter_slug: this.submitterSlug }
|
const payload = { ...this.form }
|
||||||
|
|
||||||
if (payload.dob) {
|
if (payload.dob) {
|
||||||
payload.dob = payload.dob.replace(/-/g, '')
|
payload.dob = payload.dob.replace(/-/g, '')
|
||||||
@@ -497,7 +497,7 @@ export default {
|
|||||||
payload.phone = payload.phone.replace(/^\+1/, '')
|
payload.phone = payload.phone.replace(/^\+1/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await fetch(this.baseUrl + '/api/kba', {
|
const resp = await fetch(this.baseUrl + `/s/${this.submitterSlug}/kba`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
...this.fetchOptions,
|
...this.fetchOptions,
|
||||||
@@ -548,13 +548,12 @@ export default {
|
|||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(this.baseUrl + `/api/kba/${this.field.uuid}`, {
|
const resp = await fetch(this.baseUrl + `/s/${this.submitterSlug}/kba/${this.field.uuid}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
token: this.token,
|
token: this.token,
|
||||||
answers: formattedAnswers,
|
answers: formattedAnswers,
|
||||||
reference: this.reference,
|
reference: this.reference
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
}),
|
}),
|
||||||
...this.fetchOptions,
|
...this.fetchOptions,
|
||||||
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export default {
|
|||||||
return this.provider === 'paypal'
|
return this.provider === 'paypal'
|
||||||
},
|
},
|
||||||
paymentEndpoint () {
|
paymentEndpoint () {
|
||||||
return this.isPaypal ? '/api/paypal_payments' : '/api/stripe_payments'
|
return `/s/${this.submitterSlug}/${this.isPaypal ? 'paypal_payments' : 'stripe_payments'}`
|
||||||
},
|
},
|
||||||
sessionId () {
|
sessionId () {
|
||||||
return this.queryParams.get('stripe_session_id') || this.queryParams.get('paypal_order_id') || (this.isPaypal && this.queryParams.get('token'))
|
return this.queryParams.get('stripe_session_id') || this.queryParams.get('paypal_order_id') || (this.isPaypal && this.queryParams.get('token'))
|
||||||
@@ -255,9 +255,6 @@ export default {
|
|||||||
if (this.sessionId) {
|
if (this.sessionId) {
|
||||||
return fetch(this.baseUrl + this.paymentEndpoint + '/' + this.sessionId, {
|
return fetch(this.baseUrl + this.paymentEndpoint + '/' + this.sessionId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
}),
|
|
||||||
...this.fetchOptions,
|
...this.fetchOptions,
|
||||||
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
@@ -294,7 +291,6 @@ export default {
|
|||||||
fetch(this.baseUrl + this.paymentEndpoint, {
|
fetch(this.baseUrl + this.paymentEndpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
|
||||||
field_uuid: this.field.uuid,
|
field_uuid: this.field.uuid,
|
||||||
check_status: checkStatus,
|
check_status: checkStatus,
|
||||||
referer: this.normalizedPaymentUrl()
|
referer: this.normalizedPaymentUrl()
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export default {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
sendVerificationCode () {
|
sendVerificationCode () {
|
||||||
return fetch(this.baseUrl + '/api/send_phone_verification_code', {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
submitter_slug: this.submitterSlug,
|
||||||
@@ -293,6 +293,14 @@ export default {
|
|||||||
phone: this.fullInternationalPhoneValue
|
phone: this.fullInternationalPhoneValue
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(this.baseUrl + `/s/${this.submitterSlug}/send_phone_verification_code`, options).then((resp) => {
|
||||||
|
if (resp.status === 404) {
|
||||||
|
return fetch(this.baseUrl + '/api/send_phone_verification_code', options)
|
||||||
|
} else {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
if ([422, 429].includes(resp.status)) {
|
if ([422, 429].includes(resp.status)) {
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<input
|
<input
|
||||||
v-if="!isTextArea"
|
v-if="!isTextArea"
|
||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
|
ref="input"
|
||||||
v-model="text"
|
v-model="text"
|
||||||
:maxlength="cellsMaxLegth"
|
:maxlength="cellsMaxLegth"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
|
@input="validateValue($event.target)"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -137,6 +139,9 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isIbanValidation () {
|
||||||
|
return this.patternMessageKeys[this.field.validation?.pattern] === 'must_be_valid_iban'
|
||||||
|
},
|
||||||
patternMessageKeys () {
|
patternMessageKeys () {
|
||||||
return {
|
return {
|
||||||
'^[0-9]{3}-[0-9]{2}-[0-9]{4}$': 'must_be_valid_ssn',
|
'^[0-9]{3}-[0-9]{2}-[0-9]{4}$': 'must_be_valid_ssn',
|
||||||
@@ -144,6 +149,7 @@ export default {
|
|||||||
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'must_be_valid_email',
|
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'must_be_valid_email',
|
||||||
'^https?://.*': 'must_be_valid_url',
|
'^https?://.*': 'must_be_valid_url',
|
||||||
'^[0-9]{5}(?:-[0-9]{4})?$': 'must_be_valid_zip',
|
'^[0-9]{5}(?:-[0-9]{4})?$': 'must_be_valid_zip',
|
||||||
|
'^[A-Z]{2}[0-9]{2}(?: ?[A-Z0-9]){11,30}$': 'must_be_valid_iban',
|
||||||
'^[0-9]+$': 'must_contain_numbers_only',
|
'^[0-9]+$': 'must_contain_numbers_only',
|
||||||
'^[a-zA-Z]+$': 'must_contain_letters_only'
|
'^[a-zA-Z]+$': 'must_contain_letters_only'
|
||||||
}
|
}
|
||||||
@@ -186,8 +192,21 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.validateValue(this.$refs.input)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
validateValue (input) {
|
||||||
|
if (this.isIbanValidation) {
|
||||||
|
input.setCustomValidity(this.isValidIban(input.value) ? '' : this.t('must_be_valid_iban'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isValidIban (value) {
|
||||||
|
const iban = value.replace(/\s/g, '')
|
||||||
|
const digits = (iban.slice(4) + iban.slice(0, 4)).replace(/[A-Z]/g, (char) => char.charCodeAt(0) - 55)
|
||||||
|
|
||||||
|
return !iban || digits.split('').reduce((remainder, digit) => (remainder * 10 + parseInt(digit)) % 97, 0) === 1
|
||||||
|
},
|
||||||
resizeTextarea () {
|
resizeTextarea () {
|
||||||
const textarea = this.$refs.textarea
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
|
|||||||
@@ -162,10 +162,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
start () {
|
start () {
|
||||||
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
|
return fetch(this.baseUrl + `/s/${this.submitterSlug}/identity_verification/${this.field.uuid}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
|
||||||
redirect_url: document.location.href
|
redirect_url: document.location.href
|
||||||
}),
|
}),
|
||||||
...this.fetchOptions,
|
...this.fetchOptions,
|
||||||
@@ -206,11 +205,8 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
const resp = await fetch(this.baseUrl + '/api/identity_verification', {
|
const resp = await fetch(this.baseUrl + `/s/${this.submitterSlug}/identity_verification`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
}),
|
|
||||||
...this.fetchOptions,
|
...this.fetchOptions,
|
||||||
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title', 'multi-select'],
|
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isContenteditable: false,
|
isContenteditable: false,
|
||||||
@@ -798,8 +798,6 @@ export default {
|
|||||||
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
|
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('multi-select', e)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export default {
|
|||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['t', 'isMobile', 'nativePlatform'],
|
inject: ['t', 'nativePlatform'],
|
||||||
props: {
|
props: {
|
||||||
template: {
|
template: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
ref="dragContainer"
|
ref="dragContainer"
|
||||||
style="max-width: 1600px"
|
style="max-width: 1600px"
|
||||||
:class="nativeView ? (isSheetView ? 'bg-base-100' : 'px-4') : 'mx-auto pl-3 md:pl-4 h-full'"
|
:class="nativeView ? (isSheetView ? 'bg-base-100' : 'px-4') : 'mx-auto pl-3 md:pl-4 h-full'"
|
||||||
|
@dragstart="isInternalDrag = true"
|
||||||
|
@dragend="isInternalDrag = false"
|
||||||
@dragover="onDragover"
|
@dragover="onDragover"
|
||||||
@drop="isDragFile = false"
|
@drop="[isDragFile = false, isInternalDrag = false]"
|
||||||
>
|
>
|
||||||
<HoverDropzone
|
<HoverDropzone
|
||||||
v-if="sortedDocuments.length && withUploadButton && editable && !isSheetView"
|
v-if="sortedDocuments.length && withUploadButton && editable && !isSheetView"
|
||||||
@@ -906,7 +908,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="join-item bg-base-content text-white h-9 pl-2 pr-3 inline-flex items-center justify-center cursor-pointer hover:opacity-90 border-l border-white/20"
|
class="join-item bg-base-content text-white h-9 pl-2 pr-3 inline-flex items-center justify-center cursor-pointer hover:opacity-90 border-l border-white/20"
|
||||||
@click="zoomLevel = 1"
|
@click="resetZoomLevel"
|
||||||
>
|
>
|
||||||
<IconX class="w-4 h-4 stroke-2" />
|
<IconX class="w-4 h-4 stroke-2" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1007,6 +1009,9 @@ export default {
|
|||||||
return {
|
return {
|
||||||
template: this.template,
|
template: this.template,
|
||||||
save: this.save,
|
save: this.save,
|
||||||
|
pushUndo: this.pushUndo,
|
||||||
|
undo: this.undo,
|
||||||
|
redo: this.redo,
|
||||||
t: this.t,
|
t: this.t,
|
||||||
assignDropAreaSize: this.assignDropAreaSize,
|
assignDropAreaSize: this.assignDropAreaSize,
|
||||||
currencies: this.currencies,
|
currencies: this.currencies,
|
||||||
@@ -1387,6 +1392,7 @@ export default {
|
|||||||
drawOption: null,
|
drawOption: null,
|
||||||
dragField: null,
|
dragField: null,
|
||||||
isDragFile: false,
|
isDragFile: false,
|
||||||
|
isInternalDrag: false,
|
||||||
isMathLoaded: false,
|
isMathLoaded: false,
|
||||||
isRevisionsModalOpen: false,
|
isRevisionsModalOpen: false,
|
||||||
isDocumentsDrawerOpen: false,
|
isDocumentsDrawerOpen: false,
|
||||||
@@ -1640,18 +1646,20 @@ export default {
|
|||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.undoStack = [JSON.stringify(this.template)]
|
this.undoStack = []
|
||||||
this.redoStack = []
|
this.redoStack = []
|
||||||
|
|
||||||
|
this.undoStack.push(this.buildUndoData())
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.onWindowResize()
|
this.resizeObserver = new ResizeObserver(this.onResize)
|
||||||
|
this.resizeObserver.observe(this.$el.getRootNode().querySelector('div[data-v-app]'))
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('keyup', this.onKeyUp)
|
document.addEventListener('keyup', this.onKeyUp)
|
||||||
document.addEventListener('template-builder:apply-revision', this.onApplyRevisionEvent)
|
document.addEventListener('template-builder:apply-revision', this.onApplyRevisionEvent)
|
||||||
window.addEventListener('keydown', this.onKeyDown)
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
|
||||||
window.addEventListener('dragleave', this.onWindowDragLeave)
|
window.addEventListener('dragleave', this.onWindowDragLeave)
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -1675,7 +1683,7 @@ export default {
|
|||||||
document.removeEventListener('template-builder:apply-revision', this.onApplyRevisionEvent)
|
document.removeEventListener('template-builder:apply-revision', this.onApplyRevisionEvent)
|
||||||
window.removeEventListener('keydown', this.onKeyDown)
|
window.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.removeEventListener('resize', this.onWindowResize)
|
this.resizeObserver.disconnect()
|
||||||
window.removeEventListener('dragleave', this.onWindowDragLeave)
|
window.removeEventListener('dragleave', this.onWindowDragLeave)
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
@@ -2034,8 +2042,18 @@ export default {
|
|||||||
|
|
||||||
ref.x = e.clientX - ref.offsetX
|
ref.x = e.clientX - ref.offsetX
|
||||||
ref.y = e.clientY - ref.offsetY
|
ref.y = e.clientY - ref.offsetY
|
||||||
} else if (e.dataTransfer?.types?.includes('Files') && !this.editModalDocumentUuid) {
|
} else if (e.dataTransfer?.types?.includes('Files') && !this.editModalDocumentUuid && !this.isInternalDrag) {
|
||||||
this.isDragFile = true
|
const items = Array.from(e.dataTransfer.items)
|
||||||
|
|
||||||
|
if (this.dynamicDocuments.length && items.length && items.every((item) => item.type.startsWith('image/'))) {
|
||||||
|
if (!e.defaultPrevented) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
e.dataTransfer.dropEffect = 'none'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isDragFile = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onWindowDragLeave (event) {
|
onWindowDragLeave (event) {
|
||||||
@@ -2287,6 +2305,8 @@ export default {
|
|||||||
|
|
||||||
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
|
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
|
||||||
|
|
||||||
|
this.pushUndo()
|
||||||
|
|
||||||
dynamicDocumentRefs.forEach((ref) => ref.update())
|
dynamicDocumentRefs.forEach((ref) => ref.update())
|
||||||
|
|
||||||
this.rebuildVariablesSchema({ disable: false })
|
this.rebuildVariablesSchema({ disable: false })
|
||||||
@@ -2414,42 +2434,85 @@ export default {
|
|||||||
this.drawOption = null
|
this.drawOption = null
|
||||||
},
|
},
|
||||||
undo () {
|
undo () {
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
|
||||||
if (this.undoStack.length > 1) {
|
if (this.undoStack.length > 1) {
|
||||||
this.undoStack.pop()
|
this.undoStack.pop()
|
||||||
const stringData = this.undoStack[this.undoStack.length - 1]
|
const undoData = this.undoStack[this.undoStack.length - 1]
|
||||||
const currentStringData = JSON.stringify(this.template)
|
const currentUndoData = this.buildUndoData()
|
||||||
|
|
||||||
if (stringData && stringData !== currentStringData) {
|
if (!this.isSameUndoData(undoData, currentUndoData)) {
|
||||||
this.redoStack.push(currentStringData)
|
this.redoStack.push(currentUndoData)
|
||||||
|
|
||||||
Object.assign(this.template, JSON.parse(stringData))
|
this.applyUndoData(undoData, currentUndoData)
|
||||||
|
|
||||||
this.save()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
redo () {
|
redo () {
|
||||||
const stringData = this.redoStack.pop()
|
this.selectedAreasRef.value = []
|
||||||
this.lastRedoData = stringData
|
|
||||||
const currentStringData = JSON.stringify(this.template)
|
|
||||||
|
|
||||||
if (stringData && stringData !== currentStringData) {
|
const undoData = this.redoStack.pop()
|
||||||
if (this.undoStack[this.undoStack.length - 1] !== currentStringData) {
|
const currentUndoData = this.buildUndoData()
|
||||||
this.undoStack.push(currentStringData)
|
|
||||||
|
if (undoData && !this.isSameUndoData(undoData, currentUndoData)) {
|
||||||
|
if (!this.isSameUndoData(this.undoStack[this.undoStack.length - 1], currentUndoData)) {
|
||||||
|
this.undoStack.push(currentUndoData)
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(this.template, JSON.parse(stringData))
|
this.undoStack.push(undoData)
|
||||||
|
|
||||||
this.save()
|
this.applyUndoData(undoData, currentUndoData)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onWindowResize (e) {
|
buildUndoData () {
|
||||||
|
const template = JSON.stringify(toRaw(this.template))
|
||||||
|
const lastUndoData = this.undoStack[this.undoStack.length - 1]
|
||||||
|
|
||||||
|
return { template: template === lastUndoData?.template ? lastUndoData.template : template, sectionDocs: this.buildSectionDocs() }
|
||||||
|
},
|
||||||
|
buildSectionDocs () {
|
||||||
|
return new Map(this.documentRefs.flatMap((ref) => ref.isDynamic ? ref.getSectionDocs() : []))
|
||||||
|
},
|
||||||
|
isSameUndoData (undoData, currentUndoData) {
|
||||||
|
return undoData.template === currentUndoData.template && this.isSameSectionDocs(undoData, currentUndoData)
|
||||||
|
},
|
||||||
|
isSameSectionDocs (undoData, currentUndoData) {
|
||||||
|
return [...currentUndoData.sectionDocs].every(([sectionKey, doc]) => undoData.sectionDocs.get(sectionKey)?.eq(doc))
|
||||||
|
},
|
||||||
|
applyUndoData (undoData, currentUndoData) {
|
||||||
|
const isTemplateChanged = undoData.template !== currentUndoData.template
|
||||||
|
|
||||||
|
if (isTemplateChanged) {
|
||||||
|
Object.assign(this.template, JSON.parse(undoData.template))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.documentRefs.forEach((ref) => {
|
||||||
|
if (ref.isDynamic) ref.restoreSectionDocs(undoData.sectionDocs)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.buildSectionDocs().forEach((doc, sectionKey) => undoData.sectionDocs.set(sectionKey, doc))
|
||||||
|
|
||||||
|
if (isTemplateChanged) {
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$nextTick(this.syncUndoTemplate)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
syncUndoTemplate () {
|
||||||
|
const lastUndoData = this.undoStack[this.undoStack.length - 1]
|
||||||
|
const template = JSON.stringify(toRaw(this.template))
|
||||||
|
|
||||||
|
if (template !== lastUndoData.template) lastUndoData.template = template
|
||||||
|
},
|
||||||
|
onResize () {
|
||||||
const breakpointLg = 1024
|
const breakpointLg = 1024
|
||||||
const breakpointMd = 768
|
const breakpointMd = 768
|
||||||
const width = this.$el.getRootNode().querySelector('div[data-v-app]').offsetWidth
|
const width = this.$el.getRootNode().querySelector('div[data-v-app]').offsetWidth
|
||||||
|
|
||||||
this.isBreakpointLg = width < breakpointLg
|
if (width) {
|
||||||
this.isBreakpointMd = width < breakpointMd
|
this.isBreakpointLg = width < breakpointLg
|
||||||
|
this.isBreakpointMd = width < breakpointMd
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onPagesWheel (event) {
|
onPagesWheel (event) {
|
||||||
const isFastScroll = Math.abs(event.deltaY) > 1
|
const isFastScroll = Math.abs(event.deltaY) > 1
|
||||||
@@ -2462,23 +2525,33 @@ export default {
|
|||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
const oldZoom = this.zoomLevel
|
this.setZoomLevel(this.zoomLevel - event.deltaY * 0.006, event.clientX, event.clientY)
|
||||||
const nextZoom = Math.max(1, Math.min(3, oldZoom - event.deltaY * 0.006))
|
},
|
||||||
|
resetZoomLevel () {
|
||||||
if (nextZoom === oldZoom) return
|
|
||||||
|
|
||||||
const rect = this.$refs.pagesContainer.getBoundingClientRect()
|
const rect = this.$refs.pagesContainer.getBoundingClientRect()
|
||||||
const cursorX = event.clientX - rect.left
|
|
||||||
const cursorY = event.clientY - rect.top
|
this.setZoomLevel(1, rect.left + rect.width / 2, (Math.max(rect.top, 0) + Math.min(rect.bottom, window.innerHeight)) / 2)
|
||||||
const ratio = nextZoom / oldZoom
|
},
|
||||||
const nextScrollLeft = (this.$refs.pagesContainer.scrollLeft + cursorX) * ratio - cursorX
|
setZoomLevel (zoomLevel, clientX, clientY) {
|
||||||
const nextScrollTop = (this.$refs.pagesContainer.scrollTop + cursorY) * ratio - cursorY
|
const nextZoom = Math.max(1, Math.min(3, zoomLevel))
|
||||||
|
|
||||||
|
if (nextZoom === this.zoomLevel) return
|
||||||
|
|
||||||
|
const container = this.$refs.pagesContainer
|
||||||
|
const rect = this.$refs.documents.getBoundingClientRect()
|
||||||
|
|
||||||
this.zoomLevel = nextZoom
|
this.zoomLevel = nextZoom
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.pagesContainer.scrollLeft = nextScrollLeft
|
this.documentRefs.filter((ref) => ref.isDynamic).forEach((ref) => ref.updateContainerWidth())
|
||||||
this.$refs.pagesContainer.scrollTop = nextScrollTop
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const nextRect = this.$refs.documents.getBoundingClientRect()
|
||||||
|
const scrollContainer = container.scrollHeight > container.clientHeight ? container : document.scrollingElement
|
||||||
|
|
||||||
|
container.scrollLeft += nextRect.left - clientX + (clientX - rect.left) * nextRect.width / rect.width
|
||||||
|
scrollContainer.scrollTop += nextRect.top - clientY + (clientY - rect.top) * nextRect.height / rect.height
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setDocumentRefs (el) {
|
setDocumentRefs (el) {
|
||||||
@@ -2597,15 +2670,11 @@ export default {
|
|||||||
event.stopImmediatePropagation()
|
event.stopImmediatePropagation()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
this.selectedAreasRef.value = []
|
|
||||||
|
|
||||||
this.redo()
|
this.redo()
|
||||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
|
||||||
event.stopImmediatePropagation()
|
event.stopImmediatePropagation()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
this.selectedAreasRef.value = []
|
|
||||||
|
|
||||||
this.undo()
|
this.undo()
|
||||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'c' && document.activeElement === document.body) {
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'c' && document.activeElement === document.body) {
|
||||||
if (this.selectedAreasRef.value.length > 1) {
|
if (this.selectedAreasRef.value.length > 1) {
|
||||||
@@ -2980,16 +3049,29 @@ export default {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pushUndo () {
|
pushUndo ({ amend = false } = {}) {
|
||||||
const stringData = JSON.stringify(this.template)
|
const undoData = this.buildUndoData()
|
||||||
|
const lastUndoData = this.undoStack[this.undoStack.length - 1]
|
||||||
|
const isTemplateChanged = undoData.template !== lastUndoData.template
|
||||||
|
const isSectionDocsChanged = !this.isSameSectionDocs(lastUndoData, undoData)
|
||||||
|
|
||||||
if (this.undoStack[this.undoStack.length - 1] !== stringData) {
|
if (this.isUndoBatch || (amend && (!isSectionDocsChanged || (!this.redoStack.length && !isTemplateChanged)))) {
|
||||||
this.undoStack.push(stringData)
|
this.undoStack[this.undoStack.length - 1] = undoData
|
||||||
|
} else if (isTemplateChanged || isSectionDocsChanged) {
|
||||||
|
this.undoStack.push(undoData)
|
||||||
|
|
||||||
if (this.lastRedoData !== stringData) {
|
this.redoStack = []
|
||||||
this.redoStack = []
|
} else {
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isUndoBatch = true
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isUndoBatch = false
|
||||||
|
|
||||||
|
this.syncUndoTemplate()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
setDefaultAreaSize (area, type) {
|
setDefaultAreaSize (area, type) {
|
||||||
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||||
@@ -3959,7 +4041,7 @@ export default {
|
|||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$el.closest('template-builder')) {
|
if (this.$el.closest('template-builder')) {
|
||||||
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
this.$el.closest('template-builder').dataset.template = JSON.stringify(toRaw(this.template))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3992,10 +4074,12 @@ export default {
|
|||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$el.closest('template-builder')) {
|
if (this.$el.closest('template-builder')) {
|
||||||
this.$el.closest('template-builder').dataset.dynamicDocuments = JSON.stringify(this.dynamicDocuments)
|
this.$el.closest('template-builder').dataset.dynamicDocuments = JSON.stringify(toRaw(this.dynamicDocuments))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.pushUndo({ amend: true })
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
rebuildVariablesSchema ({ disable = true } = {}) {
|
rebuildVariablesSchema ({ disable = true } = {}) {
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="absolute z-[5] pointer-events-none select-none"
|
||||||
|
:style="{ top: coords.top + 'px', left: coords.left + 'px', width: coords.width + 'px', height: coords.height + 'px' }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute left-0 bottom-0"
|
||||||
|
:style="{ top: Math.max(coords.top - toolbarHeight - 2, 0) - coords.top + 'px' }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="toolbar"
|
||||||
|
class="sticky top-1.5 flex flex-col sm:flex-row gap-0.5 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg pointer-events-auto"
|
||||||
|
@mousedown.prevent
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(row, rowIndex) in actionRows"
|
||||||
|
:key="rowIndex"
|
||||||
|
class="flex items-center gap-0.5"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(group, groupIndex) in row"
|
||||||
|
:key="groupIndex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="rowIndex > 0 || groupIndex > 0"
|
||||||
|
class="w-px h-5 bg-neutral-200 mx-1"
|
||||||
|
:class="{ 'hidden sm:block': rowIndex > 0 && groupIndex === 0 }"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-for="action in group"
|
||||||
|
:key="action.title"
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md"
|
||||||
|
:class="{ 'text-red-600': action.danger, 'bg-neutral-200': action.active, 'hover:bg-neutral-100': !action.active && !isDisabled(action), 'opacity-50 cursor-not-allowed': isDisabled(action) }"
|
||||||
|
:title="action.structural && !coords.isRegular ? t('irregular_table_structure') : t(action.title)"
|
||||||
|
:disabled="isDisabled(action)"
|
||||||
|
@click="$emit('command', action.command, action.arg)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="action.icon"
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="size"
|
||||||
|
class="absolute top-0 left-0 border border-dashed border-blue-500"
|
||||||
|
:style="{ width: size.width + 'px', height: size.height + 'px' }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="coords.type === 'table'"
|
||||||
|
class="absolute w-3 h-3 bg-white border-2 border-blue-500 rounded-sm pointer-events-auto cursor-nwse-resize touch-none before:absolute before:-inset-3"
|
||||||
|
:style="{ left: (size?.width ?? coords.width) - 6 + 'px', top: (size?.height ?? coords.height) - 6 + 'px' }"
|
||||||
|
:title="t('resize')"
|
||||||
|
@pointerdown.prevent.stop="startResize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconRowInsertTop, IconRowInsertBottom, IconColumnInsertLeft, IconColumnInsertRight, IconRowRemove, IconColumnRemove, IconTrash } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
const IconMergeCells = (props) => h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', ...props }, [
|
||||||
|
h('path', { d: 'M5 4h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2z' }),
|
||||||
|
h('path', { d: 'M6 12h4m-2 -2l2 2l-2 2' }),
|
||||||
|
h('path', { d: 'M18 12h-4m2 -2l-2 2l2 2' })
|
||||||
|
])
|
||||||
|
|
||||||
|
const IconSplitCell = (props) => h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', ...props }, [
|
||||||
|
h('path', { d: 'M5 4h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2z' }),
|
||||||
|
h('path', { d: 'M12 4v16' }),
|
||||||
|
h('path', { d: 'M10 12h-4m2 -2l-2 2l2 2' }),
|
||||||
|
h('path', { d: 'M14 12h4m-2 -2l2 2l-2 2' })
|
||||||
|
])
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicBlockMenu',
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
coords: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
getResizeLimits: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['command', 'resize'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
size: null,
|
||||||
|
toolbarHeight: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
actionRows () {
|
||||||
|
const alignActions = [
|
||||||
|
{ command: 'setBlockAlign', arg: 'left', icon: IconAlignLeft, title: 'align_left', active: this.coords.align === 'left' },
|
||||||
|
{ command: 'setBlockAlign', arg: 'center', icon: IconAlignCenter, title: 'align_center', active: this.coords.align === 'center' },
|
||||||
|
{ command: 'setBlockAlign', arg: 'right', icon: IconAlignRight, title: 'align_right', active: this.coords.align === 'right' }
|
||||||
|
]
|
||||||
|
|
||||||
|
if (this.coords.type !== 'table') {
|
||||||
|
return [[alignActions, [{ command: 'deleteSelection', icon: IconTrash, title: 'delete_image', danger: true }]]]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
alignActions,
|
||||||
|
[
|
||||||
|
{ command: 'addTableRow', arg: false, icon: IconRowInsertTop, title: 'insert_row_above', structural: true },
|
||||||
|
{ command: 'addTableRow', arg: true, icon: IconRowInsertBottom, title: 'insert_row_below', structural: true },
|
||||||
|
{ command: 'addTableColumn', arg: false, icon: IconColumnInsertLeft, title: 'insert_column_left', structural: true },
|
||||||
|
{ command: 'addTableColumn', arg: true, icon: IconColumnInsertRight, title: 'insert_column_right', structural: true }
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ command: 'mergeTableCells', icon: IconMergeCells, title: 'merge_cells', structural: true, disabled: !this.coords.canMerge },
|
||||||
|
{ command: 'splitTableCell', icon: IconSplitCell, title: 'split_cell', structural: true, disabled: !this.coords.canSplit }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ command: 'deleteTableRow', icon: IconRowRemove, title: 'delete_row', danger: true, structural: true },
|
||||||
|
{ command: 'deleteTableColumn', icon: IconColumnRemove, title: 'delete_column', danger: true, structural: true },
|
||||||
|
{ command: 'deleteTable', icon: IconTrash, title: 'delete_table', danger: true }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.toolbarHeight = this.$refs.toolbar.offsetHeight
|
||||||
|
},
|
||||||
|
updated () {
|
||||||
|
this.toolbarHeight = this.$refs.toolbar.offsetHeight
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
window.removeEventListener('pointermove', this.onResizeMove)
|
||||||
|
window.removeEventListener('pointerup', this.onResizeEnd)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isDisabled (action) {
|
||||||
|
return (action.structural && !this.coords.isRegular) || action.disabled
|
||||||
|
},
|
||||||
|
startResize (event) {
|
||||||
|
this.resizeStart = { x: event.clientX, y: event.clientY, width: this.coords.width, height: this.coords.height, ...this.getResizeLimits() }
|
||||||
|
this.size = { width: this.coords.width, height: this.coords.height }
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', this.onResizeMove)
|
||||||
|
window.addEventListener('pointerup', this.onResizeEnd)
|
||||||
|
},
|
||||||
|
onResizeMove (event) {
|
||||||
|
const { x, y, width, height, minHeight, maxWidth } = this.resizeStart
|
||||||
|
this.size = {
|
||||||
|
width: Math.min(Math.max(width + event.clientX - x, 20), maxWidth),
|
||||||
|
height: Math.max(height + event.clientY - y, minHeight)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResizeEnd () {
|
||||||
|
window.removeEventListener('pointermove', this.onResizeMove)
|
||||||
|
window.removeEventListener('pointerup', this.onResizeEnd)
|
||||||
|
|
||||||
|
if (this.size.width !== this.coords.width || this.size.height !== this.coords.height) {
|
||||||
|
this.$emit('resize', this.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.size = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -22,6 +22,12 @@
|
|||||||
v-if="shadow"
|
v-if="shadow"
|
||||||
:to="shadow"
|
:to="shadow"
|
||||||
>
|
>
|
||||||
|
<component
|
||||||
|
:is="'style'"
|
||||||
|
v-if="listsStyle"
|
||||||
|
>
|
||||||
|
{{ listsStyle }}
|
||||||
|
</component>
|
||||||
<DynamicSection
|
<DynamicSection
|
||||||
v-for="section in sections"
|
v-for="section in sections"
|
||||||
:ref="setSectionRefs"
|
:ref="setSectionRefs"
|
||||||
@@ -44,15 +50,17 @@
|
|||||||
@draw="$emit('draw', $event)"
|
@draw="$emit('draw', $event)"
|
||||||
@add-custom-field="$emit('add-custom-field', $event)"
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
|
@add-attachment="document.attachments.push($event)"
|
||||||
|
@add-list="ensureListsStyle"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import DynamicSection from './dynamic_section.vue'
|
import DynamicSection from './dynamic_section.vue'
|
||||||
import { dynamicStylesheet, tiptapStylesheet } from './dynamic_editor.js'
|
import { dynamicStylesheet, tiptapStylesheet, listsCss } from './dynamic_editor.js'
|
||||||
import { buildVariablesSchema, mergeSchemaProperties } from './dynamic_variables_schema.js'
|
import { buildVariablesSchema, mergeSchemaProperties } from './dynamic_variables_schema.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -61,6 +69,11 @@ export default {
|
|||||||
DynamicSection
|
DynamicSection
|
||||||
},
|
},
|
||||||
inject: ['baseFetch', 'template'],
|
inject: ['baseFetch', 'template'],
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
documentFonts: computed(() => this.documentFonts)
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
document: {
|
document: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -107,6 +120,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
containerWidth: 1040,
|
containerWidth: 1040,
|
||||||
isMounted: false,
|
isMounted: false,
|
||||||
|
listsStyle: null,
|
||||||
sectionRefs: []
|
sectionRefs: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -134,6 +148,40 @@ export default {
|
|||||||
styles () {
|
styles () {
|
||||||
return this.headDom.querySelectorAll('style')
|
return this.headDom.querySelectorAll('style')
|
||||||
},
|
},
|
||||||
|
documentFonts () {
|
||||||
|
const fonts = []
|
||||||
|
const firstFamily = (value) => value.split(',')[0].trim().replace(/^["']|["']$/g, '')
|
||||||
|
const add = (label, value) => {
|
||||||
|
if (label && !fonts.some((font) => font.label === label)) fonts.push({ label, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.styles.forEach((style) => {
|
||||||
|
const css = style.textContent
|
||||||
|
|
||||||
|
for (const type of ['minor', 'major']) {
|
||||||
|
for (const [, name, value] of css.matchAll(new RegExp(`(--[\\w-]+-${type}HAnsi-font):\\s*([^;}]+)`, 'g'))) {
|
||||||
|
add(firstFamily(value), `var(${name})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [, value] of css.matchAll(/@font-face\s*\{[^}]*?font-family:\s*([^;}]+)/g)) {
|
||||||
|
add(firstFamily(value), value.trim())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const skipFamilies = ['symbol', 'wingdings', 'wingdings 2', 'wingdings 3', 'webdings', 'serif', 'sans-serif', 'monospace', 'inherit', 'initial']
|
||||||
|
const addUsed = (value) => {
|
||||||
|
if (value && !value.startsWith('var(') && !skipFamilies.includes(firstFamily(value).toLowerCase())) add(firstFamily(value), value.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.styles.forEach((style) => {
|
||||||
|
for (const [, value] of style.textContent.replace(/@font-face\s*\{[^}]*\}/g, '').matchAll(/font-family:\s*([^;}]+)/g)) addUsed(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.bodyDom.querySelectorAll('[style*="font-family"]').forEach((el) => addUsed(el.style.fontFamily))
|
||||||
|
|
||||||
|
return fonts
|
||||||
|
},
|
||||||
shadow () {
|
shadow () {
|
||||||
if (this.isMounted) {
|
if (this.isMounted) {
|
||||||
return this.$refs.shadow.attachShadow({ mode: 'open' })
|
return this.$refs.shadow.attachShadow({ mode: 'open' })
|
||||||
@@ -142,16 +190,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
this.updatedSectionEditors = new Map()
|
||||||
|
|
||||||
|
this.loadListsStyle()
|
||||||
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.isMounted = true
|
this.isMounted = true
|
||||||
|
|
||||||
this.shadow.adoptedStyleSheets.push(dynamicStylesheet, tiptapStylesheet)
|
this.shadow.adoptedStyleSheets.push(dynamicStylesheet, tiptapStylesheet)
|
||||||
|
|
||||||
this.containerWidth = this.$refs.container.clientWidth
|
this.updateContainerWidth()
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
if (this.$refs.container) {
|
if (this.$refs.container) {
|
||||||
this.containerWidth = this.$refs.container.clientWidth
|
this.updateContainerWidth()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -169,6 +222,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
mergeSchemaProperties,
|
mergeSchemaProperties,
|
||||||
|
updateContainerWidth () {
|
||||||
|
this.containerWidth = this.$refs.container.clientWidth
|
||||||
|
},
|
||||||
removeArea (area) {
|
removeArea (area) {
|
||||||
this.sectionRefs.forEach((sectionRef) => {
|
this.sectionRefs.forEach((sectionRef) => {
|
||||||
const pos = sectionRef.findAreaNodePos(area.uuid)
|
const pos = sectionRef.findAreaNodePos(area.uuid)
|
||||||
@@ -184,8 +240,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
reloadContent () {
|
reloadContent () {
|
||||||
|
this.loadListsStyle()
|
||||||
|
|
||||||
this.sectionRefs.forEach((ref) => ref.reloadContent())
|
this.sectionRefs.forEach((ref) => ref.reloadContent())
|
||||||
},
|
},
|
||||||
|
getSectionDocs () {
|
||||||
|
return this.sectionRefs.map((ref) => [ref.sectionKey, ref.editor.state.doc])
|
||||||
|
},
|
||||||
|
restoreSectionDocs (sectionDocs) {
|
||||||
|
this.sectionRefs.forEach((ref) => {
|
||||||
|
if (sectionDocs.has(ref.sectionKey)) ref.restoreDoc(sectionDocs.get(ref.sectionKey))
|
||||||
|
})
|
||||||
|
},
|
||||||
onBeforeUnload (event) {
|
onBeforeUnload (event) {
|
||||||
if (this.saveTimer) {
|
if (this.saveTimer) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -211,21 +277,38 @@ export default {
|
|||||||
onSectionUpdate (section, { editor }) {
|
onSectionUpdate (section, { editor }) {
|
||||||
clearTimeout(this.saveTimer)
|
clearTimeout(this.saveTimer)
|
||||||
|
|
||||||
this.saveTimer = setTimeout(async () => {
|
this.updatedSectionEditors.set(section.id, editor)
|
||||||
await this.updateSectionAndSave(section, editor)
|
|
||||||
|
|
||||||
delete this.saveTimer
|
this.saveTimer = setTimeout(() => this.updateSectionsAndSave(), 1000)
|
||||||
}, 1000)
|
},
|
||||||
|
loadListsStyle () {
|
||||||
|
this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null
|
||||||
|
},
|
||||||
|
ensureListsStyle () {
|
||||||
|
if (this.listsStyle) return
|
||||||
|
|
||||||
|
const style = this.bodyDom.createElement('style')
|
||||||
|
|
||||||
|
style.dataset.lists = ''
|
||||||
|
style.textContent = listsCss
|
||||||
|
|
||||||
|
this.bodyDom.body.append(style)
|
||||||
|
|
||||||
|
this.listsStyle = listsCss
|
||||||
},
|
},
|
||||||
updateVariablesSchema () {
|
updateVariablesSchema () {
|
||||||
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
|
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
|
||||||
},
|
},
|
||||||
updateSectionAndSave (section, editor) {
|
updateSectionsAndSave () {
|
||||||
const target = this.bodyDom.getElementById(section.id)
|
this.updatedSectionEditors.forEach((editor, sectionId) => {
|
||||||
|
const target = this.bodyDom.getElementById(sectionId)
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
target.innerHTML = this.getHtmlForSave(editor)
|
target.innerHTML = this.getHtmlForSave(editor)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.updatedSectionEditors.clear()
|
||||||
|
|
||||||
this.document.body = this.bodyDom.body.innerHTML
|
this.document.body = this.bodyDom.body.innerHTML
|
||||||
|
|
||||||
@@ -245,6 +328,8 @@ export default {
|
|||||||
|
|
||||||
delete this.saveTimer
|
delete this.saveTimer
|
||||||
|
|
||||||
|
this.updatedSectionEditors.clear()
|
||||||
|
|
||||||
this.sectionRefs.forEach(({ section, editor }) => {
|
this.sectionRefs.forEach(({ section, editor }) => {
|
||||||
const target = this.bodyDom.getElementById(section.id)
|
const target = this.bodyDom.getElementById(section.id)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<div class="absolute top-7 left-0 min-w-36 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg">
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
@pointerenter="$event.pointerType === 'mouse' ? submenu = 'field' : null"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 w-full px-2 py-1 rounded-md"
|
||||||
|
:class="{ 'bg-neutral-100': submenu === 'field' }"
|
||||||
|
@click="submenu = submenu === 'field' ? null : 'field'"
|
||||||
|
>
|
||||||
|
<IconForms
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
{{ t('field') }}
|
||||||
|
<IconChevronRight
|
||||||
|
:width="14"
|
||||||
|
:height="14"
|
||||||
|
class="ml-auto"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="submenu === 'field'"
|
||||||
|
class="absolute top-0 left-full min-w-40 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(icon, type) in fieldTypesList"
|
||||||
|
:key="type"
|
||||||
|
class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-neutral-100"
|
||||||
|
@click="[$emit('insert-field', type), $emit('close')]"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
{{ fieldNames[type] }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
@pointerenter="$event.pointerType === 'mouse' ? submenu = 'table' : null"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 w-full px-2 py-1 rounded-md"
|
||||||
|
:class="{ 'bg-neutral-100': submenu === 'table' }"
|
||||||
|
@click="submenu = submenu === 'table' ? null : 'table'"
|
||||||
|
>
|
||||||
|
<IconTable
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
{{ t('table') }}
|
||||||
|
<IconChevronRight
|
||||||
|
:width="14"
|
||||||
|
:height="14"
|
||||||
|
class="ml-auto"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="submenu === 'table'"
|
||||||
|
class="absolute top-0 left-full w-max p-2 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!isMobile"
|
||||||
|
class="grid grid-cols-8 gap-0.5 mb-2"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="row in 8"
|
||||||
|
:key="row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="col in 8"
|
||||||
|
:key="col"
|
||||||
|
class="w-4 h-4 border rounded-sm"
|
||||||
|
:class="row <= tableSize.rows && col <= tableSize.cols ? 'bg-neutral-200 border-neutral-400' : 'bg-white border-neutral-200'"
|
||||||
|
:aria-label="`${row} × ${col}`"
|
||||||
|
@pointerenter="$event.pointerType === 'mouse' ? tableSize = { rows: row, cols: col } : null"
|
||||||
|
@click="[$emit('insert-table', { rows: row, cols: col }), $emit('close')]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center gap-1 text-xs">
|
||||||
|
<template
|
||||||
|
v-for="key in ['rows', 'cols']"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
<span v-if="key === 'cols'">×</span>
|
||||||
|
<div class="flex items-center border border-neutral-200 rounded-md">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-6 h-6 rounded-md hover:bg-neutral-100"
|
||||||
|
:aria-label="t(`decrease_${key}`)"
|
||||||
|
@click="tableSize[key] = Math.max(tableSize[key] - 1, 1)"
|
||||||
|
>
|
||||||
|
<IconMinus
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span class="w-5 text-center">{{ tableSize[key] }}</span>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-6 h-6 rounded-md hover:bg-neutral-100"
|
||||||
|
:aria-label="t(`increase_${key}`)"
|
||||||
|
@click="tableSize[key] = Math.min(tableSize[key] + 1, 20)"
|
||||||
|
>
|
||||||
|
<IconPlus
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="isMobile || tableSize.rows > 8 || tableSize.cols > 8"
|
||||||
|
class="w-full mt-2 px-2 py-1 rounded-md bg-neutral-100 hover:bg-neutral-200"
|
||||||
|
@click="[$emit('insert-table', { ...tableSize }), $emit('close')]"
|
||||||
|
>
|
||||||
|
{{ t('insert') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-neutral-100"
|
||||||
|
@mouseenter="submenu = null"
|
||||||
|
@click="[$emit('insert-variable'), $emit('close')]"
|
||||||
|
>
|
||||||
|
<IconBracketsContain
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
{{ t('variable') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-neutral-100"
|
||||||
|
@mouseenter="submenu = null"
|
||||||
|
@click="[$emit('insert-image'), $emit('close')]"
|
||||||
|
>
|
||||||
|
<IconPhoto
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
{{ t('image') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconPlus, IconMinus, IconForms, IconTable, IconPhoto, IconChevronRight, IconBracketsContain } from '@tabler/icons-vue'
|
||||||
|
import FieldType from './field_type.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicInsertMenu',
|
||||||
|
components: {
|
||||||
|
IconPlus,
|
||||||
|
IconMinus,
|
||||||
|
IconForms,
|
||||||
|
IconTable,
|
||||||
|
IconPhoto,
|
||||||
|
IconChevronRight,
|
||||||
|
IconBracketsContain
|
||||||
|
},
|
||||||
|
inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 't', 'fieldTypes', 'isMobile'],
|
||||||
|
emits: ['insert-field', 'insert-table', 'insert-variable', 'insert-image', 'close'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
submenu: null,
|
||||||
|
tableSize: { rows: 3, cols: 3 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
skipTypes: FieldType.computed.skipTypes,
|
||||||
|
fieldIconsSorted: FieldType.computed.fieldIconsSorted,
|
||||||
|
fieldTypesList () {
|
||||||
|
return Object.fromEntries(Object.entries(this.fieldIconsSorted).filter(([type]) => {
|
||||||
|
return this.fieldTypes.includes(type) || ((this.withPhone || type !== 'phone') && (this.withPayment || type !== 'payment') && (this.withVerification || type !== 'verification') && (this.withKba || type !== 'kba'))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.addEventListener('mousedown', this.onDocumentMouseDown)
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
document.removeEventListener('mousedown', this.onDocumentMouseDown)
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDocumentMouseDown (event) {
|
||||||
|
if (!event.composedPath().includes(this.$el.parentElement)) {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,107 +1,360 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="visible"
|
v-if="visible"
|
||||||
class="absolute z-10 flex items-center gap-0.5 px-1.5 py-1 bg-white border border-base-300 rounded-lg shadow select-none"
|
ref="menu"
|
||||||
:style="{ top: (coords.top - 42) + 'px', left: coords.left + 'px', transform: 'translateX(-50%)' }"
|
class="absolute z-10 flex flex-col gap-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg select-none text-sm"
|
||||||
|
:style="{ top: coords.top + 'px', left: menuLeft + 'px' }"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>
|
>
|
||||||
<button
|
<div class="flex items-center gap-0.5">
|
||||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
<div class="relative">
|
||||||
:class="isBold ? 'bg-base-200' : 'bg-transparent'"
|
<button
|
||||||
title="Bold"
|
class="flex items-center justify-between gap-1 w-24 sm:w-36 h-7 px-2 border border-neutral-200 rounded-md hover:bg-neutral-100"
|
||||||
@click="toggleBold"
|
:title="t('font')"
|
||||||
>
|
@click="toggleDropdown('font')"
|
||||||
<IconBold
|
>
|
||||||
:width="16"
|
<span
|
||||||
:height="16"
|
class="truncate"
|
||||||
/>
|
:style="{ fontFamily }"
|
||||||
</button>
|
>{{ fontFamily }}</span>
|
||||||
<button
|
<IconChevronDown
|
||||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
:width="14"
|
||||||
:class="isItalic ? 'bg-base-200' : 'bg-transparent'"
|
:height="14"
|
||||||
title="Italic"
|
/>
|
||||||
@click="toggleItalic"
|
</button>
|
||||||
>
|
<div
|
||||||
<IconItalic
|
v-if="dropdown === 'font'"
|
||||||
:width="16"
|
ref="dropdown"
|
||||||
:height="16"
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
/>
|
class="absolute z-10 top-full mt-1 w-48 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg max-h-64 overflow-y-auto"
|
||||||
</button>
|
>
|
||||||
<button
|
<button
|
||||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
v-for="font in fonts"
|
||||||
:class="isUnderline ? 'bg-base-200' : 'bg-transparent'"
|
:key="font.label"
|
||||||
title="Underline"
|
class="block w-full text-left px-2 py-1 rounded-md truncate"
|
||||||
@click="toggleUnderline"
|
:class="font.label === fontFamily ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
>
|
:style="{ fontFamily: font.value }"
|
||||||
<IconUnderline
|
@click="setSpanStyle('font-family', font.value)"
|
||||||
:width="16"
|
>
|
||||||
:height="16"
|
{{ font.label }}
|
||||||
/>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<button
|
</div>
|
||||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
<div class="relative">
|
||||||
:class="isStrike ? 'bg-base-200' : 'bg-transparent'"
|
<button
|
||||||
title="Strikethrough"
|
class="flex items-center justify-between gap-1 w-12 sm:w-16 h-7 px-2 border border-neutral-200 rounded-md hover:bg-neutral-100"
|
||||||
@click="toggleStrike"
|
:title="t('font_size')"
|
||||||
>
|
@click="toggleDropdown('size')"
|
||||||
<IconStrikethrough
|
>
|
||||||
:width="16"
|
<span>{{ fontSize }}</span>
|
||||||
:height="16"
|
<IconChevronDown
|
||||||
/>
|
:width="14"
|
||||||
</button>
|
:height="14"
|
||||||
<div class="w-px h-5 bg-base-300 mx-1" />
|
/>
|
||||||
<button
|
</button>
|
||||||
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
<div
|
||||||
title="Wrap in variable"
|
v-if="dropdown === 'size'"
|
||||||
@click="wrapVariable"
|
ref="dropdown"
|
||||||
>
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
<IconBracketsContain
|
class="absolute z-10 top-full mt-1 w-16 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg max-h-64 overflow-y-auto"
|
||||||
:width="16"
|
>
|
||||||
:height="16"
|
<button
|
||||||
:stroke-width="1.6"
|
v-for="size in fontSizes"
|
||||||
/>
|
:key="size"
|
||||||
<span class="px-0.5">
|
class="block w-full text-left px-2 py-1 rounded-md"
|
||||||
Variable
|
:class="size === fontSize ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
</span>
|
@click="setFontSize(size)"
|
||||||
</button>
|
>
|
||||||
<div class="w-px h-5 bg-base-300 mx-1" />
|
{{ size }}
|
||||||
<button
|
</button>
|
||||||
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
</div>
|
||||||
title="Wrap in condition"
|
</div>
|
||||||
@click="wrapCondition"
|
<button
|
||||||
>
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
<svg
|
:title="t('increase_font_size')"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
@click="setFontSize(fontSizes.find((size) => size > fontSize) || fontSize)"
|
||||||
width="16"
|
>
|
||||||
height="16"
|
<IconTextIncrease
|
||||||
viewBox="0 0 24 24"
|
:width="16"
|
||||||
fill="none"
|
:height="16"
|
||||||
stroke="currentColor"
|
/>
|
||||||
stroke-width="1.6"
|
</button>
|
||||||
stroke-linecap="round"
|
<button
|
||||||
stroke-linejoin="round"
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
class="tabler-icon tabler-icon-brackets-contain"
|
:title="t('decrease_font_size')"
|
||||||
><path d="M7 4h-4v16h4" /><path d="M17 4h4v16h-4" />
|
@click="setFontSize([...fontSizes].reverse().find((size) => size < fontSize) || fontSize)"
|
||||||
<text
|
>
|
||||||
x="12"
|
<IconTextDecrease
|
||||||
y="16.5"
|
:width="16"
|
||||||
text-anchor="middle"
|
:height="16"
|
||||||
fill="currentColor"
|
/>
|
||||||
stroke="none"
|
</button>
|
||||||
font-size="14"
|
<div class="w-px h-5 bg-neutral-200 mx-1" />
|
||||||
font-weight="600"
|
<div class="relative">
|
||||||
font-family="ui-sans-serif, system-ui, sans-serif"
|
<button
|
||||||
>if</text>
|
class="inline-flex items-center justify-center h-7 px-1 rounded-md hover:bg-neutral-100"
|
||||||
</svg>
|
:title="t('line_spacing')"
|
||||||
<span class="px-0.5">
|
@click="toggleDropdown('lineHeight')"
|
||||||
Condition
|
>
|
||||||
</span>
|
<IconLineHeight
|
||||||
</button>
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
<IconChevronDown
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="dropdown === 'lineHeight'"
|
||||||
|
ref="dropdown"
|
||||||
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
|
class="absolute z-10 top-full mt-1 w-20 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="value in lineHeights"
|
||||||
|
:key="value"
|
||||||
|
class="block w-full text-left px-2 py-1 rounded-md"
|
||||||
|
:class="value === lineHeight ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
|
@click="setBlockStyle('line-height', `calc(${value} * 1.15)`)"
|
||||||
|
>
|
||||||
|
{{ value }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center h-7 px-1 rounded-md hover:bg-neutral-100"
|
||||||
|
:title="t('align')"
|
||||||
|
@click="toggleDropdown('align')"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="alignIcons[textAlign]"
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
<IconChevronDown
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="dropdown === 'align'"
|
||||||
|
ref="dropdown"
|
||||||
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
|
class="absolute z-10 top-full mt-1 w-max flex gap-0.5 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(icon, align) in alignIcons"
|
||||||
|
:key="align"
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md"
|
||||||
|
:class="align === textAlign ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
|
:title="t(align)"
|
||||||
|
@click="setBlockStyle('text-align', align)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center h-7 px-1 rounded-md"
|
||||||
|
:class="listStyle ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
|
:title="t('bullet_list')"
|
||||||
|
@click="toggleDropdown('list')"
|
||||||
|
>
|
||||||
|
<IconList
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
<IconChevronDown
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="dropdown === 'list'"
|
||||||
|
ref="dropdown"
|
||||||
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
|
class="absolute z-10 top-full mt-1 w-28 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="({ label, marker }, style) in listStyles"
|
||||||
|
:key="style"
|
||||||
|
class="flex items-center gap-2 w-full px-2 py-1 rounded-md"
|
||||||
|
:class="style === listStyle ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
|
@click="toggleList(style)"
|
||||||
|
>
|
||||||
|
<span class="w-4 text-center">{{ marker }}</span>
|
||||||
|
{{ label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-0.5">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md"
|
||||||
|
:class="isBold ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
|
:title="t('bold')"
|
||||||
|
@click="toggleBold"
|
||||||
|
>
|
||||||
|
<IconBold
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md"
|
||||||
|
:class="isItalic ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
|
:title="t('italic')"
|
||||||
|
@click="toggleItalic"
|
||||||
|
>
|
||||||
|
<IconItalic
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md"
|
||||||
|
:class="isUnderline ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
|
:title="t('underline')"
|
||||||
|
@click="toggleUnderline"
|
||||||
|
>
|
||||||
|
<IconUnderline
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md"
|
||||||
|
:class="isStrike ? 'bg-neutral-200' : 'hover:bg-neutral-100'"
|
||||||
|
:title="t('strikethrough')"
|
||||||
|
@click="toggleStrike"
|
||||||
|
>
|
||||||
|
<IconStrikethrough
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-5 bg-neutral-200 mx-1" />
|
||||||
|
<div
|
||||||
|
v-for="colorProperty in ['color', 'background-color']"
|
||||||
|
:key="colorProperty"
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center h-7 px-1 rounded-md hover:bg-neutral-100"
|
||||||
|
:title="colorProperty === 'color' ? t('text_color') : t('background_color')"
|
||||||
|
@click="toggleDropdown(colorProperty)"
|
||||||
|
>
|
||||||
|
<span class="flex flex-col items-center">
|
||||||
|
<component
|
||||||
|
:is="colorProperty === 'color' ? 'IconLetterA' : 'IconHighlight'"
|
||||||
|
:width="16"
|
||||||
|
:height="14"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="w-4 h-1 rounded-sm border border-neutral-200"
|
||||||
|
:style="{ backgroundColor: colorProperty === 'color' ? color : backgroundColor }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<IconChevronDown
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="dropdown === colorProperty"
|
||||||
|
ref="dropdown"
|
||||||
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
|
class="absolute z-10 top-full mt-1 w-max p-2 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-1 w-full mb-2 px-2 py-1 rounded-md hover:bg-neutral-100"
|
||||||
|
@click="setSpanStyle(colorProperty, null)"
|
||||||
|
>
|
||||||
|
<IconX
|
||||||
|
:width="14"
|
||||||
|
:height="14"
|
||||||
|
/>
|
||||||
|
{{ colorProperty === 'color' ? t('default') : t('none') }}
|
||||||
|
</button>
|
||||||
|
<div class="grid grid-cols-10 gap-1">
|
||||||
|
<button
|
||||||
|
v-for="value in colors"
|
||||||
|
:key="value"
|
||||||
|
class="w-4 h-4 rounded-sm border border-neutral-200 hover:scale-125 transition-transform"
|
||||||
|
:style="{ backgroundColor: value }"
|
||||||
|
:title="value"
|
||||||
|
@click="setSpanStyle(colorProperty, value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-5 bg-neutral-200 mx-1 sm:mx-0.5" />
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center text-xs h-7 px-1 rounded-md"
|
||||||
|
:class="isCellSelection ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-100'"
|
||||||
|
:title="t('create_variable')"
|
||||||
|
:disabled="isCellSelection"
|
||||||
|
@click="wrapVariable"
|
||||||
|
>
|
||||||
|
<IconBracketsContain
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span class="hidden sm:inline px-0.5 font-mono">
|
||||||
|
{{ t('variable') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-5 bg-neutral-200 mx-1 sm:mx-0.5" />
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center text-xs h-7 px-1 rounded-md"
|
||||||
|
:class="isCellSelection ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-100'"
|
||||||
|
:title="t('create_condition')"
|
||||||
|
:disabled="isCellSelection"
|
||||||
|
@click="wrapCondition"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="tabler-icon tabler-icon-brackets-contain"
|
||||||
|
><path d="M7 4h-4v16h4" /><path d="M17 4h4v16h-4" />
|
||||||
|
<text
|
||||||
|
x="12"
|
||||||
|
y="16.5"
|
||||||
|
text-anchor="middle"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="none"
|
||||||
|
font-size="14"
|
||||||
|
font-weight="600"
|
||||||
|
font-family="ui-sans-serif, system-ui, sans-serif"
|
||||||
|
>if</text>
|
||||||
|
</svg>
|
||||||
|
<span class="hidden sm:inline px-0.5 font-mono">
|
||||||
|
{{ t('condition') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconBold, IconItalic, IconUnderline, IconStrikethrough, IconBracketsContain } from '@tabler/icons-vue'
|
import { IconBold, IconItalic, IconUnderline, IconStrikethrough, IconBracketsContain, IconChevronDown, IconTextIncrease, IconTextDecrease, IconLineHeight, IconAlignLeft, IconAlignCenter, IconAlignRight, IconAlignJustified, IconLetterA, IconHighlight, IconList, IconX } from '@tabler/icons-vue'
|
||||||
|
import { CellSelection } from '@tiptap/pm/tables'
|
||||||
|
import { findNumbering, numberingListStyle } from './dynamic_editor.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DynamicMenu',
|
name: 'DynamicMenu',
|
||||||
@@ -110,8 +363,17 @@ export default {
|
|||||||
IconItalic,
|
IconItalic,
|
||||||
IconUnderline,
|
IconUnderline,
|
||||||
IconStrikethrough,
|
IconStrikethrough,
|
||||||
IconBracketsContain
|
IconBracketsContain,
|
||||||
|
IconChevronDown,
|
||||||
|
IconTextIncrease,
|
||||||
|
IconTextDecrease,
|
||||||
|
IconLineHeight,
|
||||||
|
IconLetterA,
|
||||||
|
IconHighlight,
|
||||||
|
IconList,
|
||||||
|
IconX
|
||||||
},
|
},
|
||||||
|
inject: ['t', 'documentFonts'],
|
||||||
props: {
|
props: {
|
||||||
editor: {
|
editor: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -123,23 +385,90 @@ export default {
|
|||||||
default: null
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['add-variable', 'add-condition'],
|
emits: ['add-variable', 'add-condition', 'add-list'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isMouseDown: false,
|
isMouseDown: false,
|
||||||
isBold: this.editor.isActive('bold'),
|
dropdown: null,
|
||||||
isItalic: this.editor.isActive('italic'),
|
isDropdownAlignedRight: false,
|
||||||
isUnderline: this.editor.isActive('underline'),
|
menuWidth: 0,
|
||||||
isStrike: this.editor.isActive('strike')
|
isBold: false,
|
||||||
|
isItalic: false,
|
||||||
|
isUnderline: false,
|
||||||
|
isStrike: false,
|
||||||
|
fontFamily: '',
|
||||||
|
fontSize: 12,
|
||||||
|
color: '',
|
||||||
|
backgroundColor: '',
|
||||||
|
lineHeight: '',
|
||||||
|
textAlign: 'left',
|
||||||
|
listStyle: null,
|
||||||
|
isCellSelection: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
visible () {
|
visible () {
|
||||||
return !!this.coords && !this.isMouseDown
|
return !!this.coords && !this.isMouseDown
|
||||||
|
},
|
||||||
|
menuLeft () {
|
||||||
|
return Math.max(4, Math.min(this.coords.left - this.menuWidth / 2, this.coords.width - this.menuWidth - 4))
|
||||||
|
},
|
||||||
|
standardFonts () {
|
||||||
|
return [
|
||||||
|
{ label: 'Arial', value: 'Arial' },
|
||||||
|
{ label: 'Times New Roman', value: "'Times New Roman'" },
|
||||||
|
{ label: 'Courier New', value: "'Courier New'" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
fonts () {
|
||||||
|
return [
|
||||||
|
...this.documentFonts.filter((font) => !this.standardFonts.some((standardFont) => standardFont.label === font.label)),
|
||||||
|
...this.standardFonts
|
||||||
|
]
|
||||||
|
},
|
||||||
|
fontSizes () {
|
||||||
|
return [8, 9, 10, 10.5, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72]
|
||||||
|
},
|
||||||
|
lineHeights () {
|
||||||
|
return ['1', '1.15', '1.5', '2', '2.5', '3']
|
||||||
|
},
|
||||||
|
alignIcons () {
|
||||||
|
return {
|
||||||
|
left: IconAlignLeft,
|
||||||
|
center: IconAlignCenter,
|
||||||
|
right: IconAlignRight,
|
||||||
|
justify: IconAlignJustified
|
||||||
|
}
|
||||||
|
},
|
||||||
|
listStyles () {
|
||||||
|
return {
|
||||||
|
disc: { label: this.t('dots'), marker: '•' },
|
||||||
|
dash: { label: this.t('dash'), marker: '–' },
|
||||||
|
decimal: { label: this.t('numbers'), marker: '1.' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors () {
|
||||||
|
return [
|
||||||
|
'#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#d9d9d9', '#efefef', '#f3f3f3', '#ffffff',
|
||||||
|
'#980000', '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#0000ff', '#9900ff', '#ff00ff',
|
||||||
|
'#e6b8af', '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#cfe2f3', '#d9d2e9', '#ead1dc',
|
||||||
|
'#cc4125', '#e06666', '#f6b26b', '#ffd966', '#93c47d', '#76a5af', '#6d9eeb', '#6fa8dc', '#8e7cc3', '#c27ba0',
|
||||||
|
'#85200c', '#990000', '#b45f06', '#bf9000', '#38761d', '#134f5c', '#1155cc', '#0b5394', '#351c75', '#741b47'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible (value) {
|
||||||
|
this.dropdown = null
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
this.readState()
|
||||||
|
this.$nextTick(this.measure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.editor.view.dom.addEventListener('mousedown', this.onMouseDown)
|
this.editor.view.dom.parentElement.addEventListener('mousedown', this.onMouseDown)
|
||||||
|
|
||||||
document.addEventListener('mouseup', this.onMouseUp)
|
document.addEventListener('mouseup', this.onMouseUp)
|
||||||
|
|
||||||
@@ -147,18 +476,79 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeUnmount () {
|
beforeUnmount () {
|
||||||
if (!this.editor.isDestroyed) {
|
if (!this.editor.isDestroyed) {
|
||||||
this.editor.view.dom.removeEventListener('mousedown', this.onMouseDown)
|
this.editor.view.dom.parentElement.removeEventListener('mousedown', this.onMouseDown)
|
||||||
this.editor.off('transaction', this.onTransaction)
|
this.editor.off('transaction', this.onTransaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.removeEventListener('mouseup', this.onMouseUp)
|
document.removeEventListener('mouseup', this.onMouseUp)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
measure () {
|
||||||
|
if (this.$refs.menu) {
|
||||||
|
this.menuWidth = this.$refs.menu.offsetWidth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleDropdown (name) {
|
||||||
|
this.dropdown = this.dropdown === name ? null : name
|
||||||
|
this.isDropdownAlignedRight = false
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const dropdownEl = [this.$refs.dropdown].flat()[0]
|
||||||
|
|
||||||
|
if (!dropdownEl) return
|
||||||
|
|
||||||
|
const dropdownRect = dropdownEl.getBoundingClientRect()
|
||||||
|
const containerRect = this.$el.parentElement.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.isDropdownAlignedRight = dropdownRect.right > containerRect.right
|
||||||
|
})
|
||||||
|
},
|
||||||
toggleBold () {
|
toggleBold () {
|
||||||
this.editor.chain().focus().toggleBold().run()
|
this.toggleStyledMark('bold', 'font-weight', this.isBold, (style) => parseInt(style.fontWeight) >= 600)
|
||||||
},
|
},
|
||||||
toggleItalic () {
|
toggleItalic () {
|
||||||
this.editor.chain().focus().toggleItalic().run()
|
this.toggleStyledMark('italic', 'font-style', this.isItalic, (style) => style.fontStyle === 'italic')
|
||||||
|
},
|
||||||
|
toggleStyledMark (markName, property, isActive, isStyled) {
|
||||||
|
const isSegmentStyled = ({ from }) => isStyled(getComputedStyle(this.textElementAt(from)))
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
this.editor.chain().focus().unsetMark(markName).setListMarkerStyle(property, null).run()
|
||||||
|
|
||||||
|
if (this.selectedTextSegments().some(isSegmentStyled)) {
|
||||||
|
this.editor.chain().setSpanStyle(property, 'normal').run()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.editor.chain().focus().setSpanStyle(property, null).run()
|
||||||
|
|
||||||
|
const { state, view } = this.editor
|
||||||
|
const tr = state.tr
|
||||||
|
|
||||||
|
this.selectedTextSegments().forEach((segment) => {
|
||||||
|
if (!isSegmentStyled(segment)) tr.addMark(segment.from, segment.to, state.schema.marks[markName].create())
|
||||||
|
})
|
||||||
|
|
||||||
|
if (tr.docChanged) view.dispatch(tr)
|
||||||
|
|
||||||
|
this.editor.commands.setListMarkerStyle(property, markName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedTextSegments () {
|
||||||
|
const { state } = this.editor
|
||||||
|
const segments = []
|
||||||
|
|
||||||
|
state.selection.ranges.forEach(({ $from, $to }) => state.doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
|
||||||
|
if (node.isText) segments.push({ from: Math.max(pos, $from.pos), to: Math.min(pos + node.nodeSize, $to.pos) })
|
||||||
|
}))
|
||||||
|
|
||||||
|
return segments
|
||||||
|
},
|
||||||
|
textElementAt (pos) {
|
||||||
|
const { node, offset } = this.editor.view.domAtPos(pos, 1)
|
||||||
|
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) return node.parentElement
|
||||||
|
|
||||||
|
return node.childNodes[offset]?.nodeType === Node.ELEMENT_NODE ? node.childNodes[offset] : node
|
||||||
},
|
},
|
||||||
toggleUnderline () {
|
toggleUnderline () {
|
||||||
this.editor.chain().focus().toggleUnderline().run()
|
this.editor.chain().focus().toggleUnderline().run()
|
||||||
@@ -166,6 +556,30 @@ export default {
|
|||||||
toggleStrike () {
|
toggleStrike () {
|
||||||
this.editor.chain().focus().toggleStrike().run()
|
this.editor.chain().focus().toggleStrike().run()
|
||||||
},
|
},
|
||||||
|
setSpanStyle (property, value) {
|
||||||
|
this.dropdown = null
|
||||||
|
|
||||||
|
const chain = this.editor.chain().focus().setSpanStyle(property, value)
|
||||||
|
|
||||||
|
if (property !== 'background-color') chain.setListMarkerStyle(property, value)
|
||||||
|
|
||||||
|
chain.run()
|
||||||
|
},
|
||||||
|
setFontSize (size) {
|
||||||
|
this.setSpanStyle('font-size', `${size}pt`)
|
||||||
|
},
|
||||||
|
setBlockStyle (property, value) {
|
||||||
|
this.dropdown = null
|
||||||
|
|
||||||
|
this.editor.chain().focus().setBlockStyle(property, value).run()
|
||||||
|
},
|
||||||
|
toggleList (style) {
|
||||||
|
this.dropdown = null
|
||||||
|
|
||||||
|
if (style !== this.listStyle) this.$emit('add-list')
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleList(style).run()
|
||||||
|
},
|
||||||
wrapVariable () {
|
wrapVariable () {
|
||||||
const { from, to } = this.editor.state.selection
|
const { from, to } = this.editor.state.selection
|
||||||
const replacement = '[[variable]]'
|
const replacement = '[[variable]]'
|
||||||
@@ -201,10 +615,62 @@ export default {
|
|||||||
}, 1)
|
}, 1)
|
||||||
},
|
},
|
||||||
onTransaction () {
|
onTransaction () {
|
||||||
this.isBold = this.editor.isActive('bold')
|
if (this.visible) {
|
||||||
this.isItalic = this.editor.isActive('italic')
|
this.readState()
|
||||||
this.isUnderline = this.editor.isActive('underline')
|
}
|
||||||
this.isStrike = this.editor.isActive('strike')
|
},
|
||||||
|
readState () {
|
||||||
|
const { view, state } = this.editor
|
||||||
|
const { ranges } = state.selection
|
||||||
|
const selectionFrom = Math.min(...ranges.map((range) => range.$from.pos))
|
||||||
|
const selectionTo = Math.max(...ranges.map((range) => range.$to.pos))
|
||||||
|
let from = null
|
||||||
|
|
||||||
|
state.doc.nodesBetween(selectionFrom, selectionTo, (node, pos) => {
|
||||||
|
if (from === null && node.isText) from = Math.max(pos, selectionFrom)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (from === null) from = selectionFrom
|
||||||
|
|
||||||
|
const $from = state.doc.resolve(from)
|
||||||
|
const el = this.textElementAt(from)
|
||||||
|
const style = getComputedStyle(el)
|
||||||
|
const blockDom = $from.parent.isTextblock ? view.nodeDOM($from.before()) : view.dom
|
||||||
|
const decorations = []
|
||||||
|
|
||||||
|
this.isCellSelection = state.selection instanceof CellSelection
|
||||||
|
this.isBold = parseInt(style.fontWeight) >= 600
|
||||||
|
this.isItalic = style.fontStyle === 'italic'
|
||||||
|
|
||||||
|
this.fontFamily = style.fontFamily.split(',')[0].replace(/["']/g, '').trim()
|
||||||
|
this.fontSize = Math.round(parseFloat(style.fontSize) * 0.75 * 2) / 2
|
||||||
|
this.color = style.color
|
||||||
|
this.backgroundColor = 'transparent'
|
||||||
|
|
||||||
|
for (let inlineEl = el; inlineEl && view.dom.contains(inlineEl); inlineEl = inlineEl.parentElement) {
|
||||||
|
const { textDecorationLine, backgroundColor } = getComputedStyle(inlineEl)
|
||||||
|
|
||||||
|
decorations.push(textDecorationLine)
|
||||||
|
|
||||||
|
if (inlineEl === blockDom) break
|
||||||
|
|
||||||
|
if (this.backgroundColor === 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'transparent') {
|
||||||
|
this.backgroundColor = backgroundColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUnderline = this.editor.isActive('underline') || decorations.some((line) => line.includes('underline'))
|
||||||
|
this.isStrike = this.editor.isActive('strike') || decorations.some((line) => line.includes('line-through'))
|
||||||
|
|
||||||
|
if ($from.parent.isTextblock) {
|
||||||
|
const blockStyle = getComputedStyle(blockDom)
|
||||||
|
const align = blockStyle.textAlign.replace('start', 'left').replace('end', 'right')
|
||||||
|
|
||||||
|
this.textAlign = this.alignIcons[align] ? align : 'left'
|
||||||
|
this.lineHeight = String(Math.round(parseFloat(blockStyle.lineHeight) / parseFloat(blockStyle.fontSize) / 1.15 * 100) / 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listStyle = findNumbering($from.parent) ? numberingListStyle(view, $from.before()) : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
<div
|
<div
|
||||||
class="relative bg-white select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
class="relative bg-white select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
||||||
:class="{ 'cursor-crosshair': isDrawMode && editable }"
|
:class="{ 'cursor-crosshair': isDrawMode && editable }"
|
||||||
|
@mousedown="onMarginMouseDown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isDrawMode && editable && cursorHighlightCoords"
|
v-if="isDrawMode && editable && cursorHighlightCoords"
|
||||||
class="absolute pointer-events-none z-10 bg-black"
|
class="absolute pointer-events-none z-10 bg-black"
|
||||||
:style="{ width: '1px', height: cursorHighlightCoords.height + 'px', left: cursorHighlightCoords.x + 'px', top: cursorHighlightCoords.y + 'px' }"
|
:style="{ width: '1px', height: cursorHighlightCoords.height + 'px', left: cursorHighlightCoords.x + 'px', top: cursorHighlightCoords.y + 'px' }"
|
||||||
/>
|
/>
|
||||||
<div :style="{ zoom: containerWidth / sectionWidthPx }">
|
<div
|
||||||
|
:style="{ zoom, '--zoom': zoom }"
|
||||||
|
:data-zoomed-out="zoom < 0.75 || null"
|
||||||
|
>
|
||||||
<section
|
<section
|
||||||
:id="section.id"
|
:id="section.id"
|
||||||
ref="editorElement"
|
ref="editorElement"
|
||||||
@@ -17,6 +21,13 @@
|
|||||||
:style="section.style.cssText"
|
:style="section.style.cssText"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
ref="imageInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@change="onImageSelected"
|
||||||
|
>
|
||||||
<Teleport
|
<Teleport
|
||||||
v-if="editor"
|
v-if="editor"
|
||||||
:to="container"
|
:to="container"
|
||||||
@@ -30,6 +41,7 @@
|
|||||||
:area="selectedArea"
|
:area="selectedArea"
|
||||||
:field="selectedField"
|
:field="selectedField"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:is-mobile="isMobile"
|
||||||
:template="template"
|
:template="template"
|
||||||
:selected-areas-ref="selectedAreasRef"
|
:selected-areas-ref="selectedAreasRef"
|
||||||
:get-field-type-index="getFieldTypeIndex"
|
:get-field-type-index="getFieldTypeIndex"
|
||||||
@@ -41,9 +53,50 @@
|
|||||||
v-if="editable"
|
v-if="editable"
|
||||||
v-show="!selectedAreasRef.value.length"
|
v-show="!selectedAreasRef.value.length"
|
||||||
:editor="editor"
|
:editor="editor"
|
||||||
:coords="dynamicMenuCoords"
|
:coords="isInsertMenuOpen ? null : dynamicMenuCoords"
|
||||||
@add-variable="dynamicMenuCoords = null"
|
@add-variable="dynamicMenuCoords = null"
|
||||||
@add-condition="dynamicMenuCoords = null"
|
@add-condition="dynamicMenuCoords = null"
|
||||||
|
@add-list="$emit('add-list')"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="editable && insertMenuCoords && !isDrawMode"
|
||||||
|
class="absolute z-10 select-none text-sm"
|
||||||
|
:style="{ top: insertMenuCoords.top - 12 + 'px', left: insertMenuCoords.left - 12 + 'px' }"
|
||||||
|
@mousedown.prevent
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-center w-6 h-6 rounded-md border border-neutral-200"
|
||||||
|
:class="isInsertMenuOpen ? 'bg-neutral-100 text-base-content' : 'bg-white text-base-content/40 hover:bg-neutral-100 hover:text-base-content'"
|
||||||
|
:title="t('insert')"
|
||||||
|
@click="isInsertMenuOpen = !isInsertMenuOpen"
|
||||||
|
>
|
||||||
|
<IconX
|
||||||
|
v-if="isInsertMenuOpen"
|
||||||
|
:width="14"
|
||||||
|
:height="14"
|
||||||
|
/>
|
||||||
|
<IconPlus
|
||||||
|
v-else
|
||||||
|
:width="14"
|
||||||
|
:height="14"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<DynamicInsertMenu
|
||||||
|
v-if="isInsertMenuOpen"
|
||||||
|
:key="insertMenuCoords.top"
|
||||||
|
@close="isInsertMenuOpen = false"
|
||||||
|
@insert-field="onInsertField"
|
||||||
|
@insert-table="onInsertTable"
|
||||||
|
@insert-variable="editor.chain().focus().insertContent('[[variable]]').run()"
|
||||||
|
@insert-image="$refs.imageInput.click()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DynamicBlockMenu
|
||||||
|
v-if="editable && blockMenuCoords && !isDrawMode"
|
||||||
|
:coords="blockMenuCoords"
|
||||||
|
:get-resize-limits="getBlockResizeLimits"
|
||||||
|
@command="onBlockCommand"
|
||||||
|
@resize="onBlockResize"
|
||||||
/>
|
/>
|
||||||
<FieldContextMenu
|
<FieldContextMenu
|
||||||
v-if="contextMenu && contextMenuField"
|
v-if="contextMenu && contextMenuField"
|
||||||
@@ -64,20 +117,29 @@
|
|||||||
<script>
|
<script>
|
||||||
import { shallowRef } from 'vue'
|
import { shallowRef } from 'vue'
|
||||||
import { DOMSerializer, Fragment } from '@tiptap/pm/model'
|
import { DOMSerializer, Fragment } from '@tiptap/pm/model'
|
||||||
|
import { Selection, TextSelection } from '@tiptap/pm/state'
|
||||||
|
import { findTable, CellSelection } from '@tiptap/pm/tables'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
import { IconPlus, IconX } from '@tabler/icons-vue'
|
||||||
import FieldContextMenu from './field_context_menu.vue'
|
import FieldContextMenu from './field_context_menu.vue'
|
||||||
import AreaTitle from './area_title.vue'
|
import AreaTitle from './area_title.vue'
|
||||||
import DynamicMenu from './dynamic_menu.vue'
|
import DynamicMenu from './dynamic_menu.vue'
|
||||||
import { buildEditor } from './dynamic_editor.js'
|
import DynamicInsertMenu from './dynamic_insert_menu.vue'
|
||||||
|
import DynamicBlockMenu from './dynamic_block_menu.vue'
|
||||||
|
import { buildEditor, isRegularTable, resizeTable, tableRowMinHeights } from './dynamic_editor.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DynamicSection',
|
name: 'DynamicSection',
|
||||||
components: {
|
components: {
|
||||||
DynamicMenu,
|
DynamicMenu,
|
||||||
|
DynamicInsertMenu,
|
||||||
|
DynamicBlockMenu,
|
||||||
FieldContextMenu,
|
FieldContextMenu,
|
||||||
AreaTitle
|
AreaTitle,
|
||||||
|
IconPlus,
|
||||||
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['template', 'save', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'selectedAreasRef', 'getFieldTypeIndex', 'fieldTypes', 'withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor'],
|
inject: ['template', 'save', 'pushUndo', 'undo', 'redo', 'baseFetch', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'selectedAreasRef', 'getFieldTypeIndex', 'fieldTypes', 'withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor', 'isMobile'],
|
||||||
props: {
|
props: {
|
||||||
section: {
|
section: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -147,12 +209,15 @@ export default {
|
|||||||
default: null
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update', 'draw', 'set-draw', 'add-custom-field'],
|
emits: ['update', 'draw', 'set-draw', 'add-custom-field', 'add-attachment', 'add-list'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isAreaDrag: false,
|
isAreaDrag: false,
|
||||||
areaToolbarCoords: null,
|
areaToolbarCoords: null,
|
||||||
dynamicMenuCoords: null,
|
dynamicMenuCoords: null,
|
||||||
|
insertMenuCoords: null,
|
||||||
|
isInsertMenuOpen: false,
|
||||||
|
blockMenuCoords: null,
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
cursorHighlightCoords: null
|
cursorHighlightCoords: null
|
||||||
}
|
}
|
||||||
@@ -196,6 +261,9 @@ export default {
|
|||||||
editor () {
|
editor () {
|
||||||
return this.editorRef.value
|
return this.editorRef.value
|
||||||
},
|
},
|
||||||
|
sectionKey () {
|
||||||
|
return `${this.attachmentUuid}:${this.section.id}`
|
||||||
|
},
|
||||||
sectionWidthPx () {
|
sectionWidthPx () {
|
||||||
const pt = parseFloat(this.section.style.width)
|
const pt = parseFloat(this.section.style.width)
|
||||||
|
|
||||||
@@ -235,9 +303,19 @@ export default {
|
|||||||
if (this.dynamicMenuCoords && this.editor && !this.editor.state.selection.empty) {
|
if (this.dynamicMenuCoords && this.editor && !this.editor.state.selection.empty) {
|
||||||
this.$nextTick(() => this.setDynamicMenuCoords(this.editor))
|
this.$nextTick(() => this.setDynamicMenuCoords(this.editor))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.insertMenuCoords && this.editor) {
|
||||||
|
this.$nextTick(() => this.setInsertMenuCoords(this.editor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.blockMenuCoords && this.editor) {
|
||||||
|
this.$nextTick(() => this.setBlockMenuCoords(this.editor))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.lastUpdateTime = 0
|
||||||
|
|
||||||
this.editorRef.value = buildEditor({
|
this.editorRef.value = buildEditor({
|
||||||
dynamicAreaProps: {
|
dynamicAreaProps: {
|
||||||
template: this.template,
|
template: this.template,
|
||||||
@@ -250,17 +328,36 @@ export default {
|
|||||||
onAreaResize: this.onAreaResize,
|
onAreaResize: this.onAreaResize,
|
||||||
onAreaDragStart: this.onAreaDragStart
|
onAreaDragStart: this.onAreaDragStart
|
||||||
},
|
},
|
||||||
attachmentsIndex: this.attachmentsIndex,
|
getAttachmentsIndex: () => this.attachmentsIndex,
|
||||||
onFieldDrop: this.onFieldDrop,
|
onFieldDrop: this.onFieldDrop,
|
||||||
onFieldDestroy: this.onFieldDestroy,
|
onFieldDestroy: this.onFieldDestroy,
|
||||||
|
onUndo: this.undo,
|
||||||
|
onRedo: this.redo,
|
||||||
renderHtmlForSaveRef: this.renderHtmlForSaveRef,
|
renderHtmlForSaveRef: this.renderHtmlForSaveRef,
|
||||||
editorOptions: {
|
editorOptions: {
|
||||||
element: this.$refs.editorElement,
|
element: this.$refs.editorElement,
|
||||||
editable: this.editable,
|
editable: this.editable,
|
||||||
content: this.section.innerHTML,
|
content: this.section.innerHTML,
|
||||||
onUpdate: (event) => this.$emit('update', event),
|
onUpdate: (event) => {
|
||||||
|
this.$emit('update', event)
|
||||||
|
|
||||||
|
if (event.transaction.getMeta('addToHistory') !== false) {
|
||||||
|
this.pushUndo({ amend: event.transaction.time - this.lastUpdateTime < 500 })
|
||||||
|
|
||||||
|
this.lastUpdateTime = event.transaction.time
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.blockMenuCoords) {
|
||||||
|
this.$nextTick(() => this.setBlockMenuCoords(this.editor))
|
||||||
|
}
|
||||||
|
},
|
||||||
onSelectionUpdate: this.onSelectionUpdate,
|
onSelectionUpdate: this.onSelectionUpdate,
|
||||||
onBlur: () => { this.dynamicMenuCoords = null }
|
onFocus: this.onSelectionUpdate,
|
||||||
|
onBlur: () => {
|
||||||
|
this.dynamicMenuCoords = null
|
||||||
|
this.insertMenuCoords = null
|
||||||
|
this.blockMenuCoords = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -269,6 +366,8 @@ export default {
|
|||||||
this.editor.view.dom.addEventListener('mousemove', this.onEditorMouseMove)
|
this.editor.view.dom.addEventListener('mousemove', this.onEditorMouseMove)
|
||||||
this.editor.view.dom.addEventListener('mouseleave', this.onEditorMouseLeave)
|
this.editor.view.dom.addEventListener('mouseleave', this.onEditorMouseLeave)
|
||||||
this.editor.view.dom.addEventListener('keydown', this.onEditorKeyDown)
|
this.editor.view.dom.addEventListener('keydown', this.onEditorKeyDown)
|
||||||
|
|
||||||
|
this.$nextTick(() => this.pushUndo({ amend: true }))
|
||||||
},
|
},
|
||||||
beforeUnmount () {
|
beforeUnmount () {
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
@@ -284,6 +383,19 @@ export default {
|
|||||||
reloadContent () {
|
reloadContent () {
|
||||||
this.editor.commands.setContent(this.section.innerHTML, { emitUpdate: false, parseOptions: this.editor.options.parseOptions })
|
this.editor.commands.setContent(this.section.innerHTML, { emitUpdate: false, parseOptions: this.editor.options.parseOptions })
|
||||||
},
|
},
|
||||||
|
restoreDoc (doc) {
|
||||||
|
const { state, view } = this.editor
|
||||||
|
const { content } = doc.type.schema === state.schema ? doc : state.schema.nodeFromJSON(doc.toJSON())
|
||||||
|
const diffEnd = content.findDiffEnd(state.doc.content)
|
||||||
|
|
||||||
|
if (diffEnd) {
|
||||||
|
const tr = state.tr.replaceWith(0, state.doc.content.size, content).setMeta('addToHistory', false)
|
||||||
|
|
||||||
|
view.dispatch(tr.setSelection(Selection.near(tr.doc.resolve(diffEnd.a))).scrollIntoView())
|
||||||
|
|
||||||
|
this.lastUpdateTime = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
findAreaNodePos (areaUuid) {
|
findAreaNodePos (areaUuid) {
|
||||||
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
|
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
|
||||||
|
|
||||||
@@ -402,16 +514,163 @@ export default {
|
|||||||
this.selectedAreasRef.value = [area]
|
this.selectedAreasRef.value = [area]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.blockMenuCoords = null
|
||||||
} else {
|
} else {
|
||||||
this.areaToolbarCoords = null
|
this.areaToolbarCoords = null
|
||||||
this.selectedAreasRef.value = []
|
this.selectedAreasRef.value = []
|
||||||
|
|
||||||
if (editor.state.selection.empty) {
|
this.setBlockMenuCoords(editor)
|
||||||
|
|
||||||
|
if (selection.node || selection.empty || !editor.isFocused) {
|
||||||
this.dynamicMenuCoords = null
|
this.dynamicMenuCoords = null
|
||||||
} else {
|
} else {
|
||||||
this.setDynamicMenuCoords(editor)
|
this.setDynamicMenuCoords(editor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selection instanceof CellSelection) {
|
||||||
|
this.insertMenuCoords = null
|
||||||
|
} else if (editor.isFocused) {
|
||||||
|
this.setInsertMenuCoords(editor)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setInsertMenuCoords (editor) {
|
||||||
|
const coords = editor.view.coordsAtPos(editor.state.selection.head)
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
const sectionEl = this.$refs.editorElement
|
||||||
|
const sectionRect = sectionEl.getBoundingClientRect()
|
||||||
|
const paddingLeft = parseFloat(getComputedStyle(sectionEl).paddingLeft) * this.zoom
|
||||||
|
|
||||||
|
this.insertMenuCoords = {
|
||||||
|
top: (coords.top + coords.bottom) / 2 - containerRect.top,
|
||||||
|
left: sectionRect.left - containerRect.left + Math.max(paddingLeft / 2, 14)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setBlockMenuCoords (editor) {
|
||||||
|
const { selection } = editor.state
|
||||||
|
const isImage = selection.node?.type.name === 'image'
|
||||||
|
const table = isImage ? null : findTable(selection.$from)
|
||||||
|
|
||||||
|
if (!isImage && !table) {
|
||||||
|
this.blockMenuCoords = null
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = isImage ? selection.from : table.pos
|
||||||
|
const dom = editor.view.nodeDOM(pos)
|
||||||
|
const rect = dom.getBoundingClientRect()
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
const coords = {
|
||||||
|
type: isImage ? 'image' : 'table',
|
||||||
|
pos,
|
||||||
|
align: 'left',
|
||||||
|
top: rect.top - containerRect.top,
|
||||||
|
left: rect.left - containerRect.left,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
coords.align = getComputedStyle(dom.parentElement).textAlign.replace('start', 'left').replace('end', 'right')
|
||||||
|
} else {
|
||||||
|
const { marginLeft, marginRight, float } = dom.style
|
||||||
|
|
||||||
|
if (float === 'left' || float === 'right') {
|
||||||
|
coords.align = float
|
||||||
|
} else if (marginLeft === 'auto') {
|
||||||
|
coords.align = marginRight === 'auto' ? 'center' : 'right'
|
||||||
|
} else if (dom.getAttribute('align')) {
|
||||||
|
coords.align = dom.getAttribute('align')
|
||||||
|
}
|
||||||
|
|
||||||
|
coords.isRegular = isRegularTable(table.node)
|
||||||
|
coords.canMerge = editor.can().mergeTableCells()
|
||||||
|
coords.canSplit = editor.can().splitTableCell()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blockMenuCoords = coords
|
||||||
|
},
|
||||||
|
getBlockResizeLimits () {
|
||||||
|
const dom = this.editor.view.nodeDOM(this.blockMenuCoords.pos)
|
||||||
|
const rect = dom.getBoundingClientRect()
|
||||||
|
|
||||||
|
return {
|
||||||
|
minHeight: tableRowMinHeights(dom, this.zoom, false).reduce((acc, value) => acc + value, 0),
|
||||||
|
maxWidth: Math.max(this.$refs.editorElement.getBoundingClientRect().right - rect.left, rect.width)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBlockResize ({ width, height }) {
|
||||||
|
resizeTable(this.editor.view, this.blockMenuCoords.pos, { width: width / this.zoom, height: height / this.zoom }, this.zoom)
|
||||||
|
|
||||||
|
this.editor.commands.focus()
|
||||||
|
|
||||||
|
this.$nextTick(() => this.setBlockMenuCoords(this.editor))
|
||||||
|
},
|
||||||
|
onBlockCommand (command, arg) {
|
||||||
|
this.editor.chain().focus()[command](arg).run()
|
||||||
|
|
||||||
|
this.$nextTick(() => this.setBlockMenuCoords(this.editor))
|
||||||
|
},
|
||||||
|
onInsertField (type) {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
|
||||||
|
this.insertFieldAtRange({ sourceField: { type }, from, to })
|
||||||
|
},
|
||||||
|
onInsertTable ({ rows, cols }) {
|
||||||
|
const $pos = this.editor.state.tr.deleteSelection().selection.$head
|
||||||
|
const cell = '<td style="border: 1px solid #000000; padding: 4pt; vertical-align: top"><p style="margin: 0"></p></td>'
|
||||||
|
const html = `<table style="width: 100%; border-collapse: collapse"><tbody>${`<tr>${cell.repeat(cols)}</tr>`.repeat(rows)}</tbody></table>`
|
||||||
|
const isEmpty = $pos.parent.isTextblock && !$pos.parent.content.size
|
||||||
|
const from = !$pos.parent.isTextblock ? $pos.pos : isEmpty ? $pos.before() : $pos.after()
|
||||||
|
const to = isEmpty ? $pos.after() : from
|
||||||
|
|
||||||
|
this.editor.chain().focus()
|
||||||
|
.deleteSelection()
|
||||||
|
.insertContentAt({ from, to }, html)
|
||||||
|
.setTextSelection(from + 4)
|
||||||
|
.run()
|
||||||
|
},
|
||||||
|
async onImageSelected (event) {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
|
||||||
|
event.target.value = ''
|
||||||
|
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const image = await this.uploadImage(file)
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
this.editor.chain().focus().insertContent(image).run()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async insertImageFiles (files, pos) {
|
||||||
|
const images = (await Promise.all(files.map(this.uploadImage))).filter(Boolean)
|
||||||
|
|
||||||
|
if (images.length) {
|
||||||
|
this.editor.chain().focus().insertContentAt(pos, images).run()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async uploadImage (file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
const response = await this.baseFetch(`/templates/${this.template.id}/dynamic_documents/${this.attachmentUuid}/attachments`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) return alert(data.error)
|
||||||
|
|
||||||
|
this.$emit('add-attachment', data)
|
||||||
|
|
||||||
|
await this.$nextTick()
|
||||||
|
|
||||||
|
return { type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } }
|
||||||
},
|
},
|
||||||
setDynamicMenuCoords (editor) {
|
setDynamicMenuCoords (editor) {
|
||||||
const { from, to } = editor.state.selection
|
const { from, to } = editor.state.selection
|
||||||
@@ -419,11 +678,11 @@ export default {
|
|||||||
const start = view.coordsAtPos(from)
|
const start = view.coordsAtPos(from)
|
||||||
const end = view.coordsAtPos(to)
|
const end = view.coordsAtPos(to)
|
||||||
const containerRect = this.container.getBoundingClientRect()
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
const left = (start.left + end.right) / 2 - containerRect.left
|
|
||||||
|
|
||||||
this.dynamicMenuCoords = {
|
this.dynamicMenuCoords = {
|
||||||
top: Math.min(start.top, end.top) - containerRect.top,
|
top: Math.max(start.bottom, end.bottom) - containerRect.top + 8,
|
||||||
left: Math.max(80, Math.min(left, containerRect.width - 80))
|
left: (start.left + end.right) / 2 - containerRect.left,
|
||||||
|
width: containerRect.width
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFieldDestroy (node) {
|
onFieldDestroy (node) {
|
||||||
@@ -493,6 +752,8 @@ export default {
|
|||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
this.editor.chain().setNodeSelection(0).blur().run()
|
this.editor.chain().setNodeSelection(0).blur().run()
|
||||||
this.deselectArea()
|
this.deselectArea()
|
||||||
|
} else if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
|
||||||
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEditorMouseMove (event) {
|
onEditorMouseMove (event) {
|
||||||
@@ -608,25 +869,23 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newField = {
|
const newField = payload.parentField || {
|
||||||
...field,
|
...field,
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
areas: [area]
|
areas: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newField.areas.push(area)
|
||||||
|
|
||||||
if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
|
if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
|
||||||
const oldOptionUuid = area.option_uuid
|
const oldOptionIndex = field.options.findIndex((opt) => opt.uuid === area.option_uuid)
|
||||||
const optionsMap = {}
|
|
||||||
|
|
||||||
newField.options = field.options.map((opt) => {
|
if (!payload.parentField) {
|
||||||
const newUuid = v4()
|
newField.options = field.options.map((opt) => ({ ...opt, uuid: v4() }))
|
||||||
optionsMap[opt.uuid] = newUuid
|
}
|
||||||
|
|
||||||
return { ...opt, uuid: newUuid }
|
area.option_uuid = (newField.options[oldOptionIndex] || newField.options[0]).uuid
|
||||||
})
|
|
||||||
|
|
||||||
area.option_uuid = optionsMap[oldOptionUuid] || newField.options[0]?.uuid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { field: newField, area }
|
return { field: newField, area }
|
||||||
@@ -642,6 +901,8 @@ export default {
|
|||||||
const text = clipboardData.getData('text/plain')
|
const text = clipboardData.getData('text/plain')
|
||||||
const clipboardHtml = html || (text.includes('<dynamic-field') ? text : '')
|
const clipboardHtml = html || (text.includes('<dynamic-field') ? text : '')
|
||||||
|
|
||||||
|
if (/class="[^"]*doc-list-/.test(html)) this.$emit('add-list')
|
||||||
|
|
||||||
if (!clipboardHtml || !clipboardHtml.includes('data-field=')) {
|
if (!clipboardHtml || !clipboardHtml.includes('data-field=')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -662,10 +923,13 @@ export default {
|
|||||||
|
|
||||||
let lastArea = null
|
let lastArea = null
|
||||||
|
|
||||||
|
const parentFieldsIndex = {}
|
||||||
|
|
||||||
fieldNodes.forEach((fieldNode) => {
|
fieldNodes.forEach((fieldNode) => {
|
||||||
const fieldValue = fieldNode.dataset.field
|
const fieldValue = fieldNode.dataset.field
|
||||||
const areaValue = fieldNode.dataset.area
|
const areaValue = fieldNode.dataset.area
|
||||||
const templateId = fieldNode.dataset.templateId
|
const templateId = fieldNode.dataset.templateId
|
||||||
|
const sourceFieldUuid = fieldNode.getAttribute('uuid')
|
||||||
|
|
||||||
if (!fieldValue || !areaValue) {
|
if (!fieldValue || !areaValue) {
|
||||||
return
|
return
|
||||||
@@ -674,11 +938,16 @@ export default {
|
|||||||
const { field, area } = this.buildCopiedField({
|
const { field, area } = this.buildCopiedField({
|
||||||
field: JSON.parse(fieldValue),
|
field: JSON.parse(fieldValue),
|
||||||
area: JSON.parse(areaValue),
|
area: JSON.parse(areaValue),
|
||||||
templateId: Number(templateId)
|
templateId: Number(templateId),
|
||||||
|
parentField: parentFieldsIndex[sourceFieldUuid]
|
||||||
})
|
})
|
||||||
|
|
||||||
this.insertFieldInTemplate(field, insertIndex)
|
if (!parentFieldsIndex[sourceFieldUuid]) {
|
||||||
insertIndex += 1
|
parentFieldsIndex[sourceFieldUuid] = field
|
||||||
|
|
||||||
|
this.insertFieldInTemplate(field, insertIndex)
|
||||||
|
insertIndex += 1
|
||||||
|
}
|
||||||
|
|
||||||
fieldNode.setAttribute('uuid', field.uuid)
|
fieldNode.setAttribute('uuid', field.uuid)
|
||||||
fieldNode.setAttribute('area-uuid', area.uuid)
|
fieldNode.setAttribute('area-uuid', area.uuid)
|
||||||
@@ -718,7 +987,7 @@ export default {
|
|||||||
onSelectedAreaChange () {
|
onSelectedAreaChange () {
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
onFieldDrop (view, event, _slice, moved) {
|
onFieldDrop (view, event, slice, moved) {
|
||||||
this.isAreaDrag = false
|
this.isAreaDrag = false
|
||||||
|
|
||||||
if (moved) {
|
if (moved) {
|
||||||
@@ -726,8 +995,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const draggedField = this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField
|
const draggedField = this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField
|
||||||
|
const imageFiles = slice.size ? [] : Array.from(event.dataTransfer.files).filter((file) => file.type.startsWith('image/'))
|
||||||
|
|
||||||
if (!draggedField) return false
|
if (!draggedField && !imageFiles.length) return false
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
@@ -735,17 +1005,63 @@ export default {
|
|||||||
|
|
||||||
if (!pos) return false
|
if (!pos) return false
|
||||||
|
|
||||||
this.insertFieldAtRange({
|
if (draggedField) {
|
||||||
sourceField: draggedField,
|
this.insertFieldAtRange({
|
||||||
existingField: this.fieldsDragFieldRef?.value,
|
sourceField: draggedField,
|
||||||
from: pos.pos
|
existingField: this.fieldsDragFieldRef?.value,
|
||||||
})
|
from: pos.pos
|
||||||
|
})
|
||||||
|
|
||||||
this.fieldsDragFieldRef.value = null
|
this.fieldsDragFieldRef.value = null
|
||||||
this.customDragFieldRef.value = null
|
this.customDragFieldRef.value = null
|
||||||
|
} else {
|
||||||
|
this.insertImageFiles(imageFiles, pos.pos)
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
closestPosAtCoords ({ clientX, clientY }) {
|
||||||
|
const view = this.editor.view
|
||||||
|
const rect = view.dom.getBoundingClientRect()
|
||||||
|
const left = Math.min(Math.max(clientX, rect.left + 1), rect.right - 1)
|
||||||
|
const top = Math.min(Math.max(clientY, rect.top + 1), rect.bottom - 1)
|
||||||
|
const caret = document.caretPositionFromPoint?.(left, top, { shadowRoots: [view.root] })
|
||||||
|
|
||||||
|
if (caret && view.dom.contains(caret.offsetNode)) {
|
||||||
|
return view.posAtDOM(caret.offsetNode, caret.offset)
|
||||||
|
} else {
|
||||||
|
return view.posAtCoords({ left, top }).pos
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMarginMouseDown (event) {
|
||||||
|
const view = this.editor.view
|
||||||
|
|
||||||
|
if (event.button !== 0 || !this.editable || this.isDrawMode || view.dom.contains(event.target)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const anchor = this.closestPosAtCoords(event)
|
||||||
|
|
||||||
|
const selectTo = (e) => {
|
||||||
|
const { doc, tr } = view.state
|
||||||
|
|
||||||
|
view.dispatch(tr.setSelection(TextSelection.between(doc.resolve(anchor), doc.resolve(this.closestPosAtCoords(e)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
window.removeEventListener('mousemove', selectTo)
|
||||||
|
window.removeEventListener('mouseup', onMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectTo(event)
|
||||||
|
|
||||||
|
view.focus()
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', selectTo)
|
||||||
|
window.addEventListener('mouseup', onMouseUp)
|
||||||
|
},
|
||||||
onEditorPointerDown (event) {
|
onEditorPointerDown (event) {
|
||||||
if (!this.isDrawMode || !this.editable || this.isDraggingField) {
|
if (!this.isDrawMode || !this.editable || this.isDraggingField) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -19,4 +19,5 @@
|
|||||||
|
|
||||||
:host {
|
:host {
|
||||||
all: initial;
|
all: initial;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
}
|
}
|
||||||
@@ -745,6 +745,7 @@ export default {
|
|||||||
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'email',
|
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'email',
|
||||||
'^https?://.*': 'url',
|
'^https?://.*': 'url',
|
||||||
'^[0-9]{5}(?:-[0-9]{4})?$': 'zip',
|
'^[0-9]{5}(?:-[0-9]{4})?$': 'zip',
|
||||||
|
'^[A-Z]{2}[0-9]{2}(?: ?[A-Z0-9]){11,30}$': 'iban',
|
||||||
'^[0-9]+$': 'numbers_only',
|
'^[0-9]+$': 'numbers_only',
|
||||||
'^[a-zA-Z]+$': 'letters_only'
|
'^[a-zA-Z]+$': 'letters_only'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ const en = {
|
|||||||
email: 'Email',
|
email: 'Email',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
zip: 'ZIP',
|
zip: 'ZIP',
|
||||||
|
iban: 'IBAN',
|
||||||
custom: 'Custom',
|
custom: 'Custom',
|
||||||
numbers_only: 'Numbers only',
|
numbers_only: 'Numbers only',
|
||||||
letters_only: 'Letters only',
|
letters_only: 'Letters only',
|
||||||
@@ -248,7 +249,44 @@ const en = {
|
|||||||
no_revisions_yet: 'No revisions yet',
|
no_revisions_yet: 'No revisions yet',
|
||||||
viewing_revision_from: 'Viewing revision from {date}',
|
viewing_revision_from: 'Viewing revision from {date}',
|
||||||
connect_google_drive: 'Connect Google Drive',
|
connect_google_drive: 'Connect Google Drive',
|
||||||
submitting: 'Submitting'
|
submitting: 'Submitting',
|
||||||
|
font_size: 'Font size',
|
||||||
|
increase_font_size: 'Increase font size',
|
||||||
|
decrease_font_size: 'Decrease font size',
|
||||||
|
line_spacing: 'Line spacing',
|
||||||
|
justify: 'Justify',
|
||||||
|
bullet_list: 'List',
|
||||||
|
dots: 'Dots',
|
||||||
|
dash: 'Dash',
|
||||||
|
numbers: 'Numbers',
|
||||||
|
bold: 'Bold',
|
||||||
|
italic: 'Italic',
|
||||||
|
underline: 'Underline',
|
||||||
|
strikethrough: 'Strikethrough',
|
||||||
|
text_color: 'Text color',
|
||||||
|
background_color: 'Background color',
|
||||||
|
variable: 'Variable',
|
||||||
|
create_variable: 'Wrap in variable',
|
||||||
|
create_condition: 'Wrap in condition',
|
||||||
|
insert: 'Insert',
|
||||||
|
field: 'Field',
|
||||||
|
table: 'Table',
|
||||||
|
decrease_rows: 'Fewer rows',
|
||||||
|
increase_rows: 'More rows',
|
||||||
|
decrease_cols: 'Fewer columns',
|
||||||
|
increase_cols: 'More columns',
|
||||||
|
align_center: 'Align Center',
|
||||||
|
insert_row_above: 'Insert row above',
|
||||||
|
insert_row_below: 'Insert row below',
|
||||||
|
insert_column_left: 'Insert column left',
|
||||||
|
insert_column_right: 'Insert column right',
|
||||||
|
merge_cells: 'Merge cells',
|
||||||
|
split_cell: 'Split cell',
|
||||||
|
delete_row: 'Delete row',
|
||||||
|
delete_column: 'Delete column',
|
||||||
|
delete_table: 'Delete table',
|
||||||
|
delete_image: 'Delete image',
|
||||||
|
irregular_table_structure: 'Rows and columns can\'t be edited: irregular table layout'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
@@ -463,6 +501,7 @@ const es = {
|
|||||||
email: 'Correo electrónico',
|
email: 'Correo electrónico',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
zip: 'ZIP',
|
zip: 'ZIP',
|
||||||
|
iban: 'IBAN',
|
||||||
custom: 'Personalizado',
|
custom: 'Personalizado',
|
||||||
numbers_only: 'Solo números',
|
numbers_only: 'Solo números',
|
||||||
letters_only: 'Solo letras',
|
letters_only: 'Solo letras',
|
||||||
@@ -501,7 +540,44 @@ const es = {
|
|||||||
no_revisions_yet: 'Aún no hay revisiones',
|
no_revisions_yet: 'Aún no hay revisiones',
|
||||||
viewing_revision_from: 'Viendo revisión del {date}',
|
viewing_revision_from: 'Viendo revisión del {date}',
|
||||||
connect_google_drive: 'Conectar Google Drive',
|
connect_google_drive: 'Conectar Google Drive',
|
||||||
submitting: 'Enviando'
|
submitting: 'Enviando',
|
||||||
|
font_size: 'Tamaño',
|
||||||
|
increase_font_size: 'Aumentar tamaño',
|
||||||
|
decrease_font_size: 'Reducir tamaño',
|
||||||
|
line_spacing: 'Interlineado',
|
||||||
|
justify: 'Justificar',
|
||||||
|
bullet_list: 'Lista',
|
||||||
|
dots: 'Puntos',
|
||||||
|
dash: 'Guiones',
|
||||||
|
numbers: 'Números',
|
||||||
|
bold: 'Negrita',
|
||||||
|
italic: 'Cursiva',
|
||||||
|
underline: 'Subrayado',
|
||||||
|
strikethrough: 'Tachado',
|
||||||
|
text_color: 'Color de texto',
|
||||||
|
background_color: 'Color de fondo',
|
||||||
|
variable: 'Variable',
|
||||||
|
create_variable: 'Crear variable',
|
||||||
|
create_condition: 'Crear condición',
|
||||||
|
insert: 'Insertar',
|
||||||
|
field: 'Campo',
|
||||||
|
table: 'Tabla',
|
||||||
|
decrease_rows: 'Menos filas',
|
||||||
|
increase_rows: 'Más filas',
|
||||||
|
decrease_cols: 'Menos columnas',
|
||||||
|
increase_cols: 'Más columnas',
|
||||||
|
align_center: 'Centrar',
|
||||||
|
insert_row_above: 'Fila arriba',
|
||||||
|
insert_row_below: 'Fila abajo',
|
||||||
|
insert_column_left: 'Columna izquierda',
|
||||||
|
insert_column_right: 'Columna derecha',
|
||||||
|
merge_cells: 'Combinar celdas',
|
||||||
|
split_cell: 'Dividir celda',
|
||||||
|
delete_row: 'Eliminar fila',
|
||||||
|
delete_column: 'Eliminar columna',
|
||||||
|
delete_table: 'Eliminar tabla',
|
||||||
|
delete_image: 'Eliminar imagen',
|
||||||
|
irregular_table_structure: 'No se pueden editar filas ni columnas: tabla irregular'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
@@ -716,6 +792,7 @@ const it = {
|
|||||||
email: 'Email',
|
email: 'Email',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
zip: 'CAP',
|
zip: 'CAP',
|
||||||
|
iban: 'IBAN',
|
||||||
custom: 'Personalizzato',
|
custom: 'Personalizzato',
|
||||||
numbers_only: 'Solo numeri',
|
numbers_only: 'Solo numeri',
|
||||||
letters_only: 'Solo lettere',
|
letters_only: 'Solo lettere',
|
||||||
@@ -754,7 +831,44 @@ const it = {
|
|||||||
no_revisions_yet: 'Nessuna revisione ancora',
|
no_revisions_yet: 'Nessuna revisione ancora',
|
||||||
viewing_revision_from: 'Visualizzazione revisione del {date}',
|
viewing_revision_from: 'Visualizzazione revisione del {date}',
|
||||||
connect_google_drive: 'Connetti Google Drive',
|
connect_google_drive: 'Connetti Google Drive',
|
||||||
submitting: 'Invio'
|
submitting: 'Invio',
|
||||||
|
font_size: 'Dimensione',
|
||||||
|
increase_font_size: 'Aumenta dimensione',
|
||||||
|
decrease_font_size: 'Riduci dimensione',
|
||||||
|
line_spacing: 'Interlinea',
|
||||||
|
justify: 'Giustifica',
|
||||||
|
bullet_list: 'Elenco',
|
||||||
|
dots: 'Punti',
|
||||||
|
dash: 'Trattini',
|
||||||
|
numbers: 'Numeri',
|
||||||
|
bold: 'Grassetto',
|
||||||
|
italic: 'Corsivo',
|
||||||
|
underline: 'Sottolineato',
|
||||||
|
strikethrough: 'Barrato',
|
||||||
|
text_color: 'Colore testo',
|
||||||
|
background_color: 'Colore sfondo',
|
||||||
|
variable: 'Variabile',
|
||||||
|
create_variable: 'Crea variabile',
|
||||||
|
create_condition: 'Crea condizione',
|
||||||
|
insert: 'Inserisci',
|
||||||
|
field: 'Campo',
|
||||||
|
table: 'Tabella',
|
||||||
|
decrease_rows: 'Meno righe',
|
||||||
|
increase_rows: 'Più righe',
|
||||||
|
decrease_cols: 'Meno colonne',
|
||||||
|
increase_cols: 'Più colonne',
|
||||||
|
align_center: 'Centra',
|
||||||
|
insert_row_above: 'Riga sopra',
|
||||||
|
insert_row_below: 'Riga sotto',
|
||||||
|
insert_column_left: 'Colonna a sinistra',
|
||||||
|
insert_column_right: 'Colonna a destra',
|
||||||
|
merge_cells: 'Unisci celle',
|
||||||
|
split_cell: 'Dividi cella',
|
||||||
|
delete_row: 'Elimina riga',
|
||||||
|
delete_column: 'Elimina colonna',
|
||||||
|
delete_table: 'Elimina tabella',
|
||||||
|
delete_image: 'Elimina immagine',
|
||||||
|
irregular_table_structure: 'Righe e colonne non modificabili: tabella irregolare'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
@@ -969,6 +1083,7 @@ const pt = {
|
|||||||
email: 'Email',
|
email: 'Email',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
zip: 'ZIP',
|
zip: 'ZIP',
|
||||||
|
iban: 'IBAN',
|
||||||
custom: 'Personalizado',
|
custom: 'Personalizado',
|
||||||
numbers_only: 'Somente números',
|
numbers_only: 'Somente números',
|
||||||
letters_only: 'Somente letras',
|
letters_only: 'Somente letras',
|
||||||
@@ -1007,7 +1122,44 @@ const pt = {
|
|||||||
no_revisions_yet: 'Nenhuma revisão ainda',
|
no_revisions_yet: 'Nenhuma revisão ainda',
|
||||||
viewing_revision_from: 'Visualizando revisão de {date}',
|
viewing_revision_from: 'Visualizando revisão de {date}',
|
||||||
connect_google_drive: 'Conectar Google Drive',
|
connect_google_drive: 'Conectar Google Drive',
|
||||||
submitting: 'Enviando'
|
submitting: 'Enviando',
|
||||||
|
font_size: 'Tamanho',
|
||||||
|
increase_font_size: 'Aumentar tamanho',
|
||||||
|
decrease_font_size: 'Diminuir tamanho',
|
||||||
|
line_spacing: 'Espaçamento',
|
||||||
|
justify: 'Justificar',
|
||||||
|
bullet_list: 'Lista',
|
||||||
|
dots: 'Pontos',
|
||||||
|
dash: 'Traços',
|
||||||
|
numbers: 'Números',
|
||||||
|
bold: 'Negrito',
|
||||||
|
italic: 'Itálico',
|
||||||
|
underline: 'Sublinhado',
|
||||||
|
strikethrough: 'Tachado',
|
||||||
|
text_color: 'Cor do texto',
|
||||||
|
background_color: 'Cor de fundo',
|
||||||
|
variable: 'Variável',
|
||||||
|
create_variable: 'Criar variável',
|
||||||
|
create_condition: 'Criar condição',
|
||||||
|
insert: 'Inserir',
|
||||||
|
field: 'Campo',
|
||||||
|
table: 'Tabela',
|
||||||
|
decrease_rows: 'Menos linhas',
|
||||||
|
increase_rows: 'Mais linhas',
|
||||||
|
decrease_cols: 'Menos colunas',
|
||||||
|
increase_cols: 'Mais colunas',
|
||||||
|
align_center: 'Centralizar',
|
||||||
|
insert_row_above: 'Linha acima',
|
||||||
|
insert_row_below: 'Linha abaixo',
|
||||||
|
insert_column_left: 'Coluna à esquerda',
|
||||||
|
insert_column_right: 'Coluna à direita',
|
||||||
|
merge_cells: 'Mesclar células',
|
||||||
|
split_cell: 'Dividir célula',
|
||||||
|
delete_row: 'Excluir linha',
|
||||||
|
delete_column: 'Excluir coluna',
|
||||||
|
delete_table: 'Excluir tabela',
|
||||||
|
delete_image: 'Excluir imagem',
|
||||||
|
irregular_table_structure: 'Linhas e colunas não editáveis: tabela irregular'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
@@ -1222,6 +1374,7 @@ const fr = {
|
|||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
zip: 'ZIP',
|
zip: 'ZIP',
|
||||||
|
iban: 'IBAN',
|
||||||
custom: 'Personnalisé',
|
custom: 'Personnalisé',
|
||||||
numbers_only: 'Chiffres uniquement',
|
numbers_only: 'Chiffres uniquement',
|
||||||
letters_only: 'Lettres uniquement',
|
letters_only: 'Lettres uniquement',
|
||||||
@@ -1260,7 +1413,44 @@ const fr = {
|
|||||||
no_revisions_yet: 'Aucune révision pour le moment',
|
no_revisions_yet: 'Aucune révision pour le moment',
|
||||||
viewing_revision_from: 'Affichage de la révision du {date}',
|
viewing_revision_from: 'Affichage de la révision du {date}',
|
||||||
connect_google_drive: 'Connecter Google Drive',
|
connect_google_drive: 'Connecter Google Drive',
|
||||||
submitting: 'Soumission'
|
submitting: 'Soumission',
|
||||||
|
font_size: 'Taille',
|
||||||
|
increase_font_size: 'Augmenter la taille',
|
||||||
|
decrease_font_size: 'Réduire la taille',
|
||||||
|
line_spacing: 'Interligne',
|
||||||
|
justify: 'Justifier',
|
||||||
|
bullet_list: 'Liste',
|
||||||
|
dots: 'Points',
|
||||||
|
dash: 'Tirets',
|
||||||
|
numbers: 'Numéros',
|
||||||
|
bold: 'Gras',
|
||||||
|
italic: 'Italique',
|
||||||
|
underline: 'Souligné',
|
||||||
|
strikethrough: 'Barré',
|
||||||
|
text_color: 'Couleur du texte',
|
||||||
|
background_color: 'Couleur de fond',
|
||||||
|
variable: 'Variable',
|
||||||
|
create_variable: 'Créer une variable',
|
||||||
|
create_condition: 'Créer une condition',
|
||||||
|
insert: 'Insérer',
|
||||||
|
field: 'Champ',
|
||||||
|
table: 'Tableau',
|
||||||
|
decrease_rows: 'Moins de lignes',
|
||||||
|
increase_rows: 'Plus de lignes',
|
||||||
|
decrease_cols: 'Moins de colonnes',
|
||||||
|
increase_cols: 'Plus de colonnes',
|
||||||
|
align_center: 'Centrer',
|
||||||
|
insert_row_above: 'Ligne au-dessus',
|
||||||
|
insert_row_below: 'Ligne en dessous',
|
||||||
|
insert_column_left: 'Colonne à gauche',
|
||||||
|
insert_column_right: 'Colonne à droite',
|
||||||
|
merge_cells: 'Fusionner',
|
||||||
|
split_cell: 'Scinder',
|
||||||
|
delete_row: 'Supprimer la ligne',
|
||||||
|
delete_column: 'Supprimer la colonne',
|
||||||
|
delete_table: 'Supprimer le tableau',
|
||||||
|
delete_image: 'Supprimer l\'image',
|
||||||
|
irregular_table_structure: 'Lignes et colonnes non modifiables : tableau irrégulier'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
@@ -1475,6 +1665,7 @@ const de = {
|
|||||||
email: 'E-Mail',
|
email: 'E-Mail',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
zip: 'ZIP',
|
zip: 'ZIP',
|
||||||
|
iban: 'IBAN',
|
||||||
custom: 'Benutzerdefiniert',
|
custom: 'Benutzerdefiniert',
|
||||||
numbers_only: 'Nur Zahlen',
|
numbers_only: 'Nur Zahlen',
|
||||||
letters_only: 'Nur Buchstaben',
|
letters_only: 'Nur Buchstaben',
|
||||||
@@ -1513,7 +1704,44 @@ const de = {
|
|||||||
no_revisions_yet: 'Noch keine Revisionen',
|
no_revisions_yet: 'Noch keine Revisionen',
|
||||||
viewing_revision_from: 'Ansicht der Revision vom {date}',
|
viewing_revision_from: 'Ansicht der Revision vom {date}',
|
||||||
connect_google_drive: 'Google Drive verbinden',
|
connect_google_drive: 'Google Drive verbinden',
|
||||||
submitting: 'Übermittlung'
|
submitting: 'Übermittlung',
|
||||||
|
font_size: 'Schriftgröße',
|
||||||
|
increase_font_size: 'Schrift vergrößern',
|
||||||
|
decrease_font_size: 'Schrift verkleinern',
|
||||||
|
line_spacing: 'Zeilenabstand',
|
||||||
|
justify: 'Blocksatz',
|
||||||
|
bullet_list: 'Liste',
|
||||||
|
dots: 'Punkte',
|
||||||
|
dash: 'Striche',
|
||||||
|
numbers: 'Zahlen',
|
||||||
|
bold: 'Fett',
|
||||||
|
italic: 'Kursiv',
|
||||||
|
underline: 'Unterstrichen',
|
||||||
|
strikethrough: 'Durchgestrichen',
|
||||||
|
text_color: 'Textfarbe',
|
||||||
|
background_color: 'Hintergrundfarbe',
|
||||||
|
variable: 'Variable',
|
||||||
|
create_variable: 'Variable erstellen',
|
||||||
|
create_condition: 'Bedingung erstellen',
|
||||||
|
insert: 'Einfügen',
|
||||||
|
field: 'Feld',
|
||||||
|
table: 'Tabelle',
|
||||||
|
decrease_rows: 'Weniger Zeilen',
|
||||||
|
increase_rows: 'Mehr Zeilen',
|
||||||
|
decrease_cols: 'Weniger Spalten',
|
||||||
|
increase_cols: 'Mehr Spalten',
|
||||||
|
align_center: 'Zentrieren',
|
||||||
|
insert_row_above: 'Zeile darüber',
|
||||||
|
insert_row_below: 'Zeile darunter',
|
||||||
|
insert_column_left: 'Spalte links',
|
||||||
|
insert_column_right: 'Spalte rechts',
|
||||||
|
merge_cells: 'Zellen verbinden',
|
||||||
|
split_cell: 'Zelle teilen',
|
||||||
|
delete_row: 'Zeile löschen',
|
||||||
|
delete_column: 'Spalte löschen',
|
||||||
|
delete_table: 'Tabelle löschen',
|
||||||
|
delete_image: 'Bild löschen',
|
||||||
|
irregular_table_structure: 'Zeilen und Spalten nicht bearbeitbar: unregelmäßige Tabelle'
|
||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
@@ -1728,6 +1956,7 @@ const nl = {
|
|||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
zip: 'Postcode',
|
zip: 'Postcode',
|
||||||
|
iban: 'IBAN',
|
||||||
custom: 'Aangepast',
|
custom: 'Aangepast',
|
||||||
numbers_only: 'Alleen cijfers',
|
numbers_only: 'Alleen cijfers',
|
||||||
letters_only: 'Alleen letters',
|
letters_only: 'Alleen letters',
|
||||||
@@ -1766,7 +1995,44 @@ const nl = {
|
|||||||
no_revisions_yet: 'Nog geen revisies',
|
no_revisions_yet: 'Nog geen revisies',
|
||||||
viewing_revision_from: 'Revisie van {date} bekijken',
|
viewing_revision_from: 'Revisie van {date} bekijken',
|
||||||
connect_google_drive: 'Verbind Google Drive',
|
connect_google_drive: 'Verbind Google Drive',
|
||||||
submitting: 'Indienen'
|
submitting: 'Indienen',
|
||||||
|
font_size: 'Tekstgrootte',
|
||||||
|
increase_font_size: 'Tekst vergroten',
|
||||||
|
decrease_font_size: 'Tekst verkleinen',
|
||||||
|
line_spacing: 'Regelafstand',
|
||||||
|
justify: 'Uitvullen',
|
||||||
|
bullet_list: 'Lijst',
|
||||||
|
dots: 'Punten',
|
||||||
|
dash: 'Streepjes',
|
||||||
|
numbers: 'Nummers',
|
||||||
|
bold: 'Vet',
|
||||||
|
italic: 'Cursief',
|
||||||
|
underline: 'Onderstrepen',
|
||||||
|
strikethrough: 'Doorhalen',
|
||||||
|
text_color: 'Tekstkleur',
|
||||||
|
background_color: 'Achtergrondkleur',
|
||||||
|
variable: 'Variabele',
|
||||||
|
create_variable: 'Maak variabele',
|
||||||
|
create_condition: 'Maak voorwaarde',
|
||||||
|
insert: 'Invoegen',
|
||||||
|
field: 'Veld',
|
||||||
|
table: 'Tabel',
|
||||||
|
decrease_rows: 'Minder rijen',
|
||||||
|
increase_rows: 'Meer rijen',
|
||||||
|
decrease_cols: 'Minder kolommen',
|
||||||
|
increase_cols: 'Meer kolommen',
|
||||||
|
align_center: 'Centreren',
|
||||||
|
insert_row_above: 'Rij erboven',
|
||||||
|
insert_row_below: 'Rij eronder',
|
||||||
|
insert_column_left: 'Kolom links',
|
||||||
|
insert_column_right: 'Kolom rechts',
|
||||||
|
merge_cells: 'Cellen samenvoegen',
|
||||||
|
split_cell: 'Cel splitsen',
|
||||||
|
delete_row: 'Rij verwijderen',
|
||||||
|
delete_column: 'Kolom verwijderen',
|
||||||
|
delete_table: 'Tabel verwijderen',
|
||||||
|
delete_image: 'Afbeelding verwijderen',
|
||||||
|
irregular_table_structure: 'Rijen en kolommen niet bewerkbaar: onregelmatige tabel'
|
||||||
}
|
}
|
||||||
|
|
||||||
export { en, es, it, pt, fr, de, nl }
|
export { en, es, it, pt, fr, de, nl }
|
||||||
@@ -57,7 +57,6 @@
|
|||||||
@scroll-to="$emit('scroll-to', $event)"
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
@add-custom-field="$emit('add-custom-field', $event)"
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
||||||
@multi-select="openMultiSelectContextMenu"
|
|
||||||
@click-title="closeContextMenu"
|
@click-title="closeContextMenu"
|
||||||
/>
|
/>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
@@ -450,13 +449,6 @@ export default {
|
|||||||
areas: this.selectedAreasRef.value
|
areas: this.selectedAreasRef.value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openMultiSelectContextMenu (event) {
|
|
||||||
if (this.selectedAreasRef.value.length >= 2) {
|
|
||||||
this.openSelectionContextMenu(event)
|
|
||||||
} else {
|
|
||||||
this.closeContextMenu()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSelectionCopy () {
|
handleSelectionCopy () {
|
||||||
this.$emit('copy-selected-areas')
|
this.$emit('copy-selected-areas')
|
||||||
|
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ export default {
|
|||||||
checkStripeStatus () {
|
checkStripeStatus () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
fetch('/api/stripe_connect').then(async (resp) => {
|
fetch('/stripe_connect').then(async (resp) => {
|
||||||
const { status } = await resp.json()
|
const { status } = await resp.json()
|
||||||
|
|
||||||
if (status === 'connected') {
|
if (status === 'connected') {
|
||||||
@@ -427,7 +427,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
checkPaypalStatus () {
|
checkPaypalStatus () {
|
||||||
fetch('/api/paypal_connect').then(async (resp) => {
|
fetch('/paypal_connect').then(async (resp) => {
|
||||||
const { status } = await resp.json()
|
const { status } = await resp.json()
|
||||||
|
|
||||||
if (status === 'connected') {
|
if (status === 'connected') {
|
||||||
|
|||||||
@@ -181,12 +181,21 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
def otp_verification_email(submitter, locale: nil)
|
def otp_verification_email(submitter, locale: nil)
|
||||||
@current_account = submitter.account
|
@current_account = submitter.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@otp_code = EmailVerificationCodes.generate([submitter.email.downcase.strip, submitter.slug].join(':'))
|
@otp_code = OtpChallenges.generate([submitter.email.downcase.squish, submitter.slug].join(':'),
|
||||||
|
purpose: 'form_email_2fa')
|
||||||
|
|
||||||
assign_message_metadata('otp_verification_email', submitter)
|
assign_message_metadata('otp_verification_email', submitter)
|
||||||
|
|
||||||
|
from =
|
||||||
|
if Docuseal.multitenant? &&
|
||||||
|
AccountConfig.exists?(account_id: submitter.account_id, key: 'custom_otp_email', value: true)
|
||||||
|
from_address_for_submitter(submitter)
|
||||||
|
else
|
||||||
|
default_params[:from]
|
||||||
|
end
|
||||||
|
|
||||||
I18n.with_locale(locale || submitter.account.locale) do
|
I18n.with_locale(locale || submitter.account.locale) do
|
||||||
mail(to: submitter.email, subject: I18n.t('email_verification'))
|
mail(to: submitter.email, from:, subject: I18n.t('email_verification'))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,20 @@ class TemplateMailer < ApplicationMailer
|
|||||||
@current_account = template.account
|
@current_account = template.account
|
||||||
@template = template
|
@template = template
|
||||||
|
|
||||||
@otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':'))
|
@otp_code = OtpChallenges.generate([email.downcase.squish, template.slug].join(':'), purpose: 'form_email_2fa')
|
||||||
|
|
||||||
assign_message_metadata('otp_verification_email', template)
|
assign_message_metadata('otp_verification_email', template)
|
||||||
|
|
||||||
mail(to: email, subject: I18n.t('email_verification'))
|
from =
|
||||||
|
if Docuseal.multitenant? &&
|
||||||
|
AccountConfig.exists?(account_id: template.account_id, key: 'custom_otp_email', value: true)
|
||||||
|
put_metadata('from_user_id' => template.author_id)
|
||||||
|
|
||||||
|
template.author.friendly_name
|
||||||
|
else
|
||||||
|
default_params[:from]
|
||||||
|
end
|
||||||
|
|
||||||
|
mail(to: email, from:, subject: I18n.t('email_verification'))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: otp_challenges
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# attempts :integer default(0), not null
|
||||||
|
# expire_at :datetime not null
|
||||||
|
# key :string not null
|
||||||
|
# nonce :string not null
|
||||||
|
# verified_at :datetime
|
||||||
|
# version :integer not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_otp_challenges_on_key_and_version (key,version) UNIQUE
|
||||||
|
#
|
||||||
|
class OtpChallenge < ApplicationRecord
|
||||||
|
end
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %>
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %>
|
||||||
<% if can?(:manage, account_config) %>
|
<% if !Docuseal.demo? && can?(:manage, account_config) %>
|
||||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
<%= f.hidden_field :key %>
|
<%= f.hidden_field :key %>
|
||||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||||
|
|||||||
@@ -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 d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||||
|
<path d="M12 18c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||||
|
<path d="M16 19h6" />
|
||||||
|
<path d="M19 16v6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 460 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 d="M12 19h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v5.5" />
|
||||||
|
<path d="M16 19h6" />
|
||||||
|
<path d="M19 16v6" />
|
||||||
|
<path d="M3 7l9 6l9 -6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 441 B |
@@ -1,8 +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">
|
<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 stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M5 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
|
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
|
||||||
<path d="M3 21v-2a4 4 0 0 1 4 -4h4c.96 0 1.84 .338 2.53 .901" />
|
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
||||||
<path d="M16 19h6" />
|
<path d="M16 19h6" />
|
||||||
<path d="M19 16v6" />
|
<path d="M19 16v6" />
|
||||||
|
<path d="M6 21v-2a4 4 0 0 1 4 -4h4" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 421 B |
@@ -3,7 +3,7 @@
|
|||||||
<div class="w-full text-center">
|
<div class="w-full text-center">
|
||||||
<span class="font-bold"><%= t('demo_environment') %></span>
|
<span class="font-bold"><%= t('demo_environment') %></span>
|
||||||
<br>
|
<br>
|
||||||
<%= t('create_a_new_template_document_form_or_submit_the_existing_one_html', new_template_link: new_template_path, start_form_link: start_form_url(slug: ::Template.first&.slug)) %> 😊
|
<%= t('create_a_new_template_document_form_html', new_template_link: new_template_path) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template&.name %></p>
|
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template&.name %></p>
|
||||||
<p dir="auto" class="text-sm">
|
<p dir="auto" class="text-sm">
|
||||||
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %>
|
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<% fields_id = "add_new_fields_#{SecureRandom.hex(3)}" %>
|
||||||
|
<div id="<%= fields_id %>" class="space-y-3 mt-4 hidden" data-targets="dynamic-list.groups" data-multiple-classes="px-3 pt-3 pb-4 rounded-xl border border-dashed border-base-300"></div>
|
||||||
|
<native-menu class="<%= turbo_native_app? ? 'block' : 'dropdown block max-sm:hidden' %> mt-4 -mb-2">
|
||||||
|
<label tabindex="0" class="btn btn-ghost btn-sm normal-case font-normal w-full flex items-center justify-center border-dashed border-base-300 hover:border-base-content/20 !bg-transparent">
|
||||||
|
<%= svg_icon('users_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
|
<span><%= t('add_new') %></span>
|
||||||
|
</label>
|
||||||
|
<ul tabindex="0" class="<%= turbo_native_app? ? 'hidden' : 'z-10 dropdown-content p-2 mt-1 shadow menu bg-base-100 rounded-box' %>" style="left: 50%; --tw-translate-x: -50%">
|
||||||
|
<li>
|
||||||
|
<native-menu-action data-icon="user_plus" data-label="<%= t('recipients') %>" class="grid-cols-1 p-0">
|
||||||
|
<a href="#" class="flex items-center gap-2 px-4 py-2" data-action="click:dynamic-list#addItem">
|
||||||
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
|
<span class="whitespace-nowrap"><%= t('recipients') %></span>
|
||||||
|
</a>
|
||||||
|
</native-menu-action>
|
||||||
|
</li>
|
||||||
|
<append-template class="contents" data-target-id="<%= fields_id %>">
|
||||||
|
<li>
|
||||||
|
<native-menu-action data-icon="eye_plus" data-label="<%= t('cc_view_only') %>" class="grid-cols-1 p-0">
|
||||||
|
<a href="#" class="flex items-center gap-2 px-4 py-2">
|
||||||
|
<%= svg_icon('eye_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
|
<span class="whitespace-nowrap"><%= t('cc_view_only') %></span>
|
||||||
|
</a>
|
||||||
|
</native-menu-action>
|
||||||
|
</li>
|
||||||
|
<template>
|
||||||
|
<submitter-item class="form-control">
|
||||||
|
<label class="label pt-0 pb-1">
|
||||||
|
<span class="label-text"><%= t('cc_view_only') %></span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<submitters-autocomplete data-field="email">
|
||||||
|
<input type="email" name="cc_recipients[]" autocomplete="off" class="base-input !h-10 w-full pr-10" placeholder="<%= t('email') %>">
|
||||||
|
</submitters-autocomplete>
|
||||||
|
<a href="#" class="absolute right-2 top-2 block w-6 h-6 text-neutral-700 p-1 hover:text-base-content">
|
||||||
|
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</submitter-item>
|
||||||
|
</template>
|
||||||
|
</append-template>
|
||||||
|
<append-template class="contents" data-target-id="<%= fields_id %>">
|
||||||
|
<li>
|
||||||
|
<native-menu-action data-icon="mail_plus" data-label="<%= t('bcc_completed') %>" class="grid-cols-1 p-0">
|
||||||
|
<a href="#" class="flex items-center gap-2 px-4 py-2">
|
||||||
|
<%= svg_icon('mail_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
|
<span class="whitespace-nowrap"><%= t('bcc_completed') %></span>
|
||||||
|
</a>
|
||||||
|
</native-menu-action>
|
||||||
|
</li>
|
||||||
|
<template>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pt-0 pb-1">
|
||||||
|
<span class="label-text"><%= t('bcc_completed') %></span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="email" name="bcc_completed[]" autocomplete="off" class="base-input !h-10 w-full pr-10" placeholder="<%= t('email') %>">
|
||||||
|
<a href="#" class="absolute right-2 top-2 block w-6 h-6 text-neutral-700 p-1 hover:text-base-content">
|
||||||
|
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% default_bcc_completed = template.preferences['bcc_completed'].presence || current_account.account_configs.find_by(key: AccountConfig::BCC_EMAILS)&.value %>
|
||||||
|
<% if default_bcc_completed.present? %>
|
||||||
|
<input type="hidden" name="bcc_completed[]" value="<%= default_bcc_completed %>">
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</append-template>
|
||||||
|
</ul>
|
||||||
|
</native-menu>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<% has_phone_field = false %>
|
<% has_phone_field = false %>
|
||||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
||||||
<dynamic-list class="space-y-4">
|
<dynamic-list class="block" data-multiple-classes="px-3 <%= submitters.size > 1 ? 'pt-3 pb-4' : 'py-4' %> rounded-xl border border-dashed border-base-300">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40 max-sm:p-0 max-sm:bg-transparent' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items">
|
<div class="relative" data-targets="dynamic-list.items">
|
||||||
<div>
|
<div>
|
||||||
<div class="absolute right-4 top-5">
|
<div class="absolute top-5" style="right: 12px">
|
||||||
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 text-neutral-700 p-1 hidden hover:text-base-content">
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,14 +86,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? && local_assigns[:variables_form].blank? %>
|
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? && local_assigns[:variables_form].blank? %>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center max-sm:hidden" data-action="click:dynamic-list#addItem">
|
<%= render 'add_new', template: %>
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
|
||||||
<span><%= t('add_new') %></span>
|
|
||||||
</a>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<%= local_assigns[:variables_form] %>
|
<%= local_assigns[:variables_form] %>
|
||||||
<div>
|
<div class="mt-4">
|
||||||
<%= render('submitters_order', f:, template:) if can_send_emails %>
|
<%= render('submitters_order', f:, template:) if can_send_emails %>
|
||||||
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
|
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
|
||||||
<% if has_phone_field %>
|
<% if has_phone_field %>
|
||||||
@@ -101,7 +98,12 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'extra_fields', f: %>
|
<%= render 'extra_fields', f: %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control mt-4">
|
||||||
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %>
|
<%= f.button class: 'base-button' do %>
|
||||||
|
<toggle-send-title>
|
||||||
|
<span><%= button_title(title: t('send'), disabled_with: t('sending')) %></span>
|
||||||
|
<span class="hidden"><%= button_title(title: t('add_recipients')) %></span>
|
||||||
|
</toggle-send-title>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
||||||
<% if submitters.size == 1 && local_assigns[:variables_form] %>
|
<% if submitters.size == 1 && local_assigns[:variables_form] %>
|
||||||
<% item = submitters.first %>
|
<% item = submitters.first %>
|
||||||
@@ -26,18 +26,18 @@
|
|||||||
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>">
|
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>">
|
||||||
<submitters-autocomplete data-field="email" class="block relative">
|
<submitters-autocomplete data-field="email" class="block relative">
|
||||||
<autoresize-textarea>
|
<autoresize-textarea>
|
||||||
<%= f.text_area :emails, required: true, class: 'base-textarea w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %>
|
<%= f.text_area :emails, required: true, class: 'base-textarea block w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %>
|
||||||
</autoresize-textarea>
|
</autoresize-textarea>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</emails-textarea>
|
</emails-textarea>
|
||||||
</submitter-item>
|
</submitter-item>
|
||||||
<% else %>
|
<% else %>
|
||||||
<dynamic-list class="space-y-4">
|
<dynamic-list class="block" data-multiple-classes="px-3 pt-3 pb-4 rounded-xl border border-dashed border-base-300">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40 max-sm:p-0 max-sm:bg-transparent' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items">
|
<div class="relative" data-targets="dynamic-list.items">
|
||||||
<div>
|
<div>
|
||||||
<div class="absolute right-4 top-5">
|
<div class="absolute top-5" style="right: 12px">
|
||||||
<a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
<a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 text-neutral-700 p-1 hidden hover:text-base-content">
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,22 +60,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
|
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center max-sm:hidden" data-action="click:dynamic-list#addItem">
|
<%= render 'add_new', template: %>
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
|
||||||
<span><%= t('add_new') %></span>
|
|
||||||
</a>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= local_assigns[:variables_form] %>
|
<%= local_assigns[:variables_form] %>
|
||||||
<div>
|
<div class="mt-4">
|
||||||
<%= render('submitters_order', f:, template:) if can_send_emails %>
|
<%= render('submitters_order', f:, template:) if can_send_emails %>
|
||||||
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
|
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
|
||||||
<%= render 'extra_fields', f: %>
|
<%= render 'extra_fields', f: %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control mt-4">
|
||||||
<%= content_for(:submit_button) || capture do %>
|
<%= content_for(:submit_button) || capture do %>
|
||||||
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %>
|
<%= f.button class: 'base-button' do %>
|
||||||
|
<toggle-on-checked data-names="send_email send_sms">
|
||||||
|
<span><%= button_title(title: t('send'), disabled_with: t('sending')) %></span>
|
||||||
|
<span class="hidden"><%= button_title(title: t('add_recipients')) %></span>
|
||||||
|
</toggle-on-checked>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
||||||
<dynamic-list class="space-y-4">
|
<dynamic-list class="block" data-multiple-classes="px-3 <%= submitters.size > 1 ? 'pt-3 pb-4' : 'py-4' %> rounded-xl border border-dashed border-base-300">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40 max-sm:p-0 max-sm:bg-transparent' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items">
|
<div class="relative" data-targets="dynamic-list.items">
|
||||||
<div>
|
<div>
|
||||||
<div class="absolute right-4 top-5">
|
<div class="absolute top-5" style="right: 12px">
|
||||||
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 text-neutral-700 p-1 hidden hover:text-base-content">
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,19 +48,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
|
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center max-sm:hidden" data-action="click:dynamic-list#addItem">
|
<%= render 'add_new', template: %>
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
|
||||||
<span><%= t('add_new') %></span>
|
|
||||||
</a>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<%= local_assigns[:variables_form] %>
|
<%= local_assigns[:variables_form] %>
|
||||||
<div>
|
<div class="mt-4">
|
||||||
<%= render 'submitters_order', f:, template: %>
|
<%= render 'submitters_order', f:, template: %>
|
||||||
<%= render 'send_sms', f:, checked: true %>
|
<%= render 'send_sms', f:, checked: true %>
|
||||||
<%= render 'extra_phone_fields', f: %>
|
<%= render 'extra_phone_fields', f: %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control mt-4">
|
||||||
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %>
|
<%= f.button class: 'base-button' do %>
|
||||||
|
<toggle-on-checked data-names="send_email send_sms">
|
||||||
|
<span><%= button_title(title: t('send'), disabled_with: t('sending')) %></span>
|
||||||
|
<span class="hidden"><%= button_title(title: t('add_recipients')) %></span>
|
||||||
|
</toggle-on-checked>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
|
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
|
||||||
<label>
|
<label>
|
||||||
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
|
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
|
||||||
<span class="link peer-checked:hidden"><%= t('edit_message') %></span>
|
<span class="link link-hover peer-checked:hidden"><%= t('edit_message') %></span>
|
||||||
</label>
|
</label>
|
||||||
</toggle-visible>
|
</toggle-visible>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<% unless turbo_native_ios? %>
|
<% unless turbo_native_ios? %>
|
||||||
<div class="form-control">
|
<toggle-on-checked data-names="is_custom_message" class="block">
|
||||||
<%= render 'sms_settings/placeholder' %>
|
<div class="form-control">
|
||||||
</div>
|
<%= render 'sms_settings/placeholder' %>
|
||||||
|
</div>
|
||||||
|
</toggle-on-checked>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</toggle-visible>
|
</toggle-visible>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="px-5 mb-5 mt-4">
|
<div class="px-5 mb-5 mt-5">
|
||||||
<% unless only_detailed %>
|
<% unless only_detailed %>
|
||||||
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
|
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
|
||||||
<%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
|
<%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
|
||||||
|
|||||||
@@ -54,7 +54,8 @@
|
|||||||
<% if @submission.submitters.to_a.any?(&:completed_at?) %>
|
<% if @submission.submitters.to_a.any?(&:completed_at?) %>
|
||||||
<% show_combined_download = @submission.completed_at? && !is_combined_enabled %>
|
<% show_combined_download = @submission.completed_at? && !is_combined_enabled %>
|
||||||
<% show_unarchive = signed_in? && can?(:destroy, @submission) && @submission.archived_at? && @submission.completed_at? %>
|
<% show_unarchive = signed_in? && can?(:destroy, @submission) && @submission.archived_at? && @submission.completed_at? %>
|
||||||
<% show_download_dropdown = show_combined_download || show_unarchive %>
|
<% show_event_log_download = signed_in? && can?(:manage, :event_log_download) && can?(:read, @submission) %>
|
||||||
|
<% show_download_dropdown = show_combined_download || show_event_log_download || show_unarchive %>
|
||||||
<% if @submission.completed_at? || !is_combined_enabled %>
|
<% if @submission.completed_at? || !is_combined_enabled %>
|
||||||
<div class="join relative">
|
<div class="join relative">
|
||||||
<native-action data-label="<%= t('download') %>" data-icon="download">
|
<native-action data-label="<%= t('download') %>" data-icon="download">
|
||||||
@@ -95,6 +96,9 @@
|
|||||||
</li>
|
</li>
|
||||||
</native-action>
|
</native-action>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if show_event_log_download %>
|
||||||
|
<%= render 'submissions/download_event_log' %>
|
||||||
|
<% end %>
|
||||||
<% if show_unarchive %>
|
<% if show_unarchive %>
|
||||||
<native-action data-label="<%= t('unarchive') %>" data-icon="rotate" data-section="archive">
|
<native-action data-label="<%= t('unarchive') %>" data-icon="rotate" data-section="archive">
|
||||||
<li>
|
<li>
|
||||||
@@ -163,6 +167,7 @@
|
|||||||
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||||
<% value ||= field['default_value'] if field['type'] == 'heading' %>
|
<% value ||= field['default_value'] if field['type'] == 'heading' %>
|
||||||
<% submitter = submitters_index[field['submitter_uuid']] %>
|
<% submitter = submitters_index[field['submitter_uuid']] %>
|
||||||
|
<% value = nil if field['type'] == 'signature' && submitter && !submitter.completed_at? %>
|
||||||
<% if value.present? %>
|
<% if value.present? %>
|
||||||
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
|
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
|
||||||
<span class="group">
|
<span class="group">
|
||||||
@@ -320,6 +325,7 @@
|
|||||||
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
<% next if field['type'] == 'heading' || field['type'] == 'strikethrough' %>
|
<% next if field['type'] == 'heading' || field['type'] == 'strikethrough' %>
|
||||||
|
<% next if field['type'] == 'signature' && submitter && !submitter.completed_at? %>
|
||||||
<div class="pt-2.5 border-b border-base-300">
|
<div class="pt-2.5 border-b border-base-300">
|
||||||
<div class="text-xs font-medium uppercase mb-0.5" dir="auto">
|
<div class="text-xs font-medium uppercase mb-0.5" dir="auto">
|
||||||
<%= field['name'].presence || "#{t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" %>
|
<%= field['name'].presence || "#{t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" %>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<% last_submitter = @submission.submitters.completed.order(:completed_at).last %>
|
<% last_submitter = @submission.submitters.completed.order(:completed_at).last %>
|
||||||
<% if last_submitter %>
|
<% if last_submitter %>
|
||||||
<p dir="auto" class="text-sm">
|
<p dir="auto" class="text-sm">
|
||||||
<%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.to_date, format: :long)) %>
|
<%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.in_time_zone(@submission.account.timezone).to_date, format: :long)) %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
|
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
|
||||||
<p dir="auto" class="text-sm">
|
<p dir="auto" class="text-sm">
|
||||||
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(completed_at.to_date, format: :long)) %>
|
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
|
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
|
||||||
<p class="text-sm"><%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at, format: :long)) %></p>
|
<p class="text-sm"><%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
|
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
|
||||||
<p class="text-sm"><%= t('document_has_been_delegated_on_html', time: l(@submitter.submission_events.order(event_timestamp: :desc).find_by!(event_type: :delegate_form).event_timestamp, format: :long)) %></p>
|
<p class="text-sm"><%= t('document_has_been_delegated_on_html', time: l(@submitter.submission_events.order(event_timestamp: :desc).find_by!(event_type: :delegate_form).event_timestamp.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<% last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last %>
|
<% last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last %>
|
||||||
<% if last_submitter %>
|
<% if last_submitter %>
|
||||||
<p dir="auto" class="text-sm">
|
<p dir="auto" class="text-sm">
|
||||||
<%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.to_date, format: :long)) %>
|
<%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
|
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
|
||||||
<p class="text-sm"><%= t('form_expired_at_html', time: l(@submitter.submission.expire_at, format: :long)) %></p>
|
<p class="text-sm"><%= t('form_expired_at_html', time: l(@submitter.submission.expire_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% templates_order_select_html = capture do %>
|
<% templates_order_select_html = capture do %>
|
||||||
<% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %>
|
<% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %>
|
||||||
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000, selected_order: %>
|
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 1_000, selected_order: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/pagination', pagy: @pagy, items_name: @templates.present? ? 'templates' : 'template_folders', right_additional_html: templates_order_select_html %>
|
<%= render 'shared/pagination', pagy: @pagy, items_name: @templates.present? ? 'templates' : 'template_folders', right_additional_html: templates_order_select_html %>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% templates_order_select_html = capture do %>
|
<% templates_order_select_html = capture do %>
|
||||||
<% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %>
|
<% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %>
|
||||||
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000, selected_order: %>
|
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 1_000, selected_order: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @template_folders.present? || @show_default_folder || @show_shared_folder %>
|
<% if @template_folders.present? || @show_default_folder || @show_shared_folder %>
|
||||||
|
|||||||
@@ -35,10 +35,12 @@
|
|||||||
<%= svg_icon('user', class: 'w-5 h-5 inline') %>
|
<%= svg_icon('user', class: 'w-5 h-5 inline') %>
|
||||||
<span><%= signature.reason %></span>
|
<span><%= signature.reason %></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-1">
|
<% if signature.signing_time %>
|
||||||
<%= svg_icon('calendar', class: 'w-5 h-5 inline') %>
|
<div class="flex items-center space-x-1">
|
||||||
<span><%= l(signature.signing_time.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %></span>
|
<%= svg_icon('calendar', class: 'w-5 h-5 inline') %>
|
||||||
</div>
|
<span><%= l(signature.signing_time.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %></span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<%= svg_icon('certificate', class: 'w-5 h-5 inline') %>
|
<%= svg_icon('certificate', class: 'w-5 h-5 inline') %>
|
||||||
<span><%= signature.common_name %></span>
|
<span><%= signature.common_name %></span>
|
||||||
|
|||||||
+47
-19
@@ -624,6 +624,7 @@ en: &en
|
|||||||
form_has_been_declined: Form has been declined.
|
form_has_been_declined: Form has been declined.
|
||||||
form_is_view_only: Form is view only.
|
form_is_view_only: Form is view only.
|
||||||
file_is_missing: File is missing
|
file_is_missing: File is missing
|
||||||
|
file_is_not_an_image: File is not an image
|
||||||
folder_name_has_been_updated: Folder name has been updated.
|
folder_name_has_been_updated: Folder name has been updated.
|
||||||
unable_to_rename_folder: Unable to rename folder.
|
unable_to_rename_folder: Unable to rename folder.
|
||||||
template_has_been_removed: Template has been removed.
|
template_has_been_removed: Template has been removed.
|
||||||
@@ -683,7 +684,7 @@ en: &en
|
|||||||
thirty_days: 30 days
|
thirty_days: 30 days
|
||||||
free: Free
|
free: Free
|
||||||
unlimited_documents_storage: Unlimited documents storage
|
unlimited_documents_storage: Unlimited documents storage
|
||||||
users_management: Users management
|
user_management: User management
|
||||||
ten_field_types: 10 field types
|
ten_field_types: 10 field types
|
||||||
multiple_signers: Multiple signers
|
multiple_signers: Multiple signers
|
||||||
automatic_esignature: Automatic eSignature
|
automatic_esignature: Automatic eSignature
|
||||||
@@ -703,6 +704,7 @@ en: &en
|
|||||||
developer_sandbox: Developer Sandbox.
|
developer_sandbox: Developer Sandbox.
|
||||||
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> to start using in Production.'
|
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> to start using in Production.'
|
||||||
download_combined_pdf: Download combined PDF
|
download_combined_pdf: Download combined PDF
|
||||||
|
download_event_log: Download Event Log
|
||||||
audit_log: Audit Log
|
audit_log: Audit Log
|
||||||
signers: Signers
|
signers: Signers
|
||||||
not_invited_yet: Not invited yet
|
not_invited_yet: Not invited yet
|
||||||
@@ -714,7 +716,7 @@ en: &en
|
|||||||
declined_on_time: 'Declined on %{time}'
|
declined_on_time: 'Declined on %{time}'
|
||||||
expire_on_time: 'Expire on %{time}'
|
expire_on_time: 'Expire on %{time}'
|
||||||
sign_in_person: Sign In-person
|
sign_in_person: Sign In-person
|
||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form or <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submit the existing one</a>
|
create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Send email copy with completed documents to a specified BCC address.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Send email copy with completed documents to a specified BCC address.
|
||||||
re_send_email: Re-send Email
|
re_send_email: Re-send Email
|
||||||
re_send_emails: Re-send Emails
|
re_send_emails: Re-send Emails
|
||||||
@@ -1026,6 +1028,8 @@ en: &en
|
|||||||
failed_to_scan_the_document: Failed to scan the document
|
failed_to_scan_the_document: Failed to scan the document
|
||||||
document_scanning_is_not_available_on_this_device: Document scanning is not available on this device
|
document_scanning_is_not_available_on_this_device: Document scanning is not available on this device
|
||||||
click_to_upload: Click to upload
|
click_to_upload: Click to upload
|
||||||
|
cc_view_only: CC View-only
|
||||||
|
bcc_completed: BCC Completed
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Your email address has been successfully confirmed.
|
confirmed: Your email address has been successfully confirmed.
|
||||||
@@ -1781,6 +1785,7 @@ es: &es
|
|||||||
form_has_been_declined: El formulario ha sido rechazado.
|
form_has_been_declined: El formulario ha sido rechazado.
|
||||||
form_is_view_only: El formulario es de solo lectura.
|
form_is_view_only: El formulario es de solo lectura.
|
||||||
file_is_missing: Falta el archivo
|
file_is_missing: Falta el archivo
|
||||||
|
file_is_not_an_image: El archivo no es una imagen
|
||||||
folder_name_has_been_updated: El nombre de la carpeta ha sido actualizado.
|
folder_name_has_been_updated: El nombre de la carpeta ha sido actualizado.
|
||||||
unable_to_rename_folder: No se pudo renombrar la carpeta.
|
unable_to_rename_folder: No se pudo renombrar la carpeta.
|
||||||
template_has_been_removed: La plantilla ha sido eliminada.
|
template_has_been_removed: La plantilla ha sido eliminada.
|
||||||
@@ -1840,7 +1845,7 @@ es: &es
|
|||||||
thirty_days: 30 días
|
thirty_days: 30 días
|
||||||
free: Gratis
|
free: Gratis
|
||||||
unlimited_documents_storage: Almacenamiento ilimitado
|
unlimited_documents_storage: Almacenamiento ilimitado
|
||||||
users_management: Gestión de usuarios
|
user_management: Gestión de usuarios
|
||||||
ten_field_types: 10 tipos de campos
|
ten_field_types: 10 tipos de campos
|
||||||
multiple_signers: Múltiples firmantes
|
multiple_signers: Múltiples firmantes
|
||||||
automatic_esignature: Firma electrónica automática
|
automatic_esignature: Firma electrónica automática
|
||||||
@@ -1860,6 +1865,7 @@ es: &es
|
|||||||
developer_sandbox: Sandbox para desarrolladores.
|
developer_sandbox: Sandbox para desarrolladores.
|
||||||
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Actualiza</a> para empezar a usar en Producción.'
|
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Actualiza</a> para empezar a usar en Producción.'
|
||||||
download_combined_pdf: Descargar PDF combinado
|
download_combined_pdf: Descargar PDF combinado
|
||||||
|
download_event_log: Descargar registro de eventos
|
||||||
audit_log: Registro de Auditoría
|
audit_log: Registro de Auditoría
|
||||||
signers: Firmantes
|
signers: Firmantes
|
||||||
not_invited_yet: Aún no invitado
|
not_invited_yet: Aún no invitado
|
||||||
@@ -1871,7 +1877,7 @@ es: &es
|
|||||||
declined_on_time: 'Rechazado el %{time}'
|
declined_on_time: 'Rechazado el %{time}'
|
||||||
expire_on_time: 'Expira el %{time}'
|
expire_on_time: 'Expira el %{time}'
|
||||||
sign_in_person: Firma en persona
|
sign_in_person: Firma en persona
|
||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crear una nueva plantilla</a> de documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">enviar el existente</a>
|
create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crear una nueva plantilla</a> de documento
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Enviar una copia del correo electrónico con los documentos completados a una dirección BCC especificada.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Enviar una copia del correo electrónico con los documentos completados a una dirección BCC especificada.
|
||||||
re_send_email: Reenviar email
|
re_send_email: Reenviar email
|
||||||
re_send_emails: Reenviar emails
|
re_send_emails: Reenviar emails
|
||||||
@@ -2172,6 +2178,8 @@ es: &es
|
|||||||
failed_to_save_the_scan: No se pudo guardar el escaneo
|
failed_to_save_the_scan: No se pudo guardar el escaneo
|
||||||
failed_to_scan_the_document: No se pudo escanear el documento
|
failed_to_scan_the_document: No se pudo escanear el documento
|
||||||
document_scanning_is_not_available_on_this_device: El escaneo de documentos no está disponible en este dispositivo
|
document_scanning_is_not_available_on_this_device: El escaneo de documentos no está disponible en este dispositivo
|
||||||
|
cc_view_only: CC Solo lectura
|
||||||
|
bcc_completed: CCO Completado
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
|
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
|
||||||
@@ -2941,6 +2949,7 @@ it: &it
|
|||||||
form_has_been_declined: Il modulo è stato rifiutato.
|
form_has_been_declined: Il modulo è stato rifiutato.
|
||||||
form_is_view_only: Il modulo è di sola lettura.
|
form_is_view_only: Il modulo è di sola lettura.
|
||||||
file_is_missing: File mancante
|
file_is_missing: File mancante
|
||||||
|
file_is_not_an_image: Il file non è un'immagine
|
||||||
folder_name_has_been_updated: Il nome della cartella è stato aggiornato.
|
folder_name_has_been_updated: Il nome della cartella è stato aggiornato.
|
||||||
unable_to_rename_folder: Impossibile rinominare la cartella.
|
unable_to_rename_folder: Impossibile rinominare la cartella.
|
||||||
template_has_been_removed: Il modello è stato rimosso.
|
template_has_been_removed: Il modello è stato rimosso.
|
||||||
@@ -3000,7 +3009,7 @@ it: &it
|
|||||||
thirty_days: 30 giorni
|
thirty_days: 30 giorni
|
||||||
free: Gratuito
|
free: Gratuito
|
||||||
unlimited_documents_storage: Archiviazione illimitata di documenti
|
unlimited_documents_storage: Archiviazione illimitata di documenti
|
||||||
users_management: Gestione utenti
|
user_management: Gestione utenti
|
||||||
ten_field_types: 10 tipi di campi
|
ten_field_types: 10 tipi di campi
|
||||||
multiple_signers: Firmatari multipli
|
multiple_signers: Firmatari multipli
|
||||||
automatic_esignature: Firma elettronica automatica
|
automatic_esignature: Firma elettronica automatica
|
||||||
@@ -3020,6 +3029,7 @@ it: &it
|
|||||||
developer_sandbox: Sandbox per sviluppatori
|
developer_sandbox: Sandbox per sviluppatori
|
||||||
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Aggiorna</a> per iniziare a usare in produzione.'
|
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Aggiorna</a> per iniziare a usare in produzione.'
|
||||||
download_combined_pdf: Scarica PDF combinato
|
download_combined_pdf: Scarica PDF combinato
|
||||||
|
download_event_log: Scarica registro degli eventi
|
||||||
audit_log: Registro di audit
|
audit_log: Registro di audit
|
||||||
signers: Firmatari
|
signers: Firmatari
|
||||||
not_invited_yet: Non ancora invitato
|
not_invited_yet: Non ancora invitato
|
||||||
@@ -3031,7 +3041,7 @@ it: &it
|
|||||||
declined_on_time: 'Rifiutato il %{time}'
|
declined_on_time: 'Rifiutato il %{time}'
|
||||||
expire_on_time: 'Scade il %{time}'
|
expire_on_time: 'Scade il %{time}'
|
||||||
sign_in_person: Firma di persona
|
sign_in_person: Firma di persona
|
||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crea un nuovo modello</a> di documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">invia quello esistente</a>
|
create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crea un nuovo modello</a> di documento
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Invia una copia dell'email con i documenti completati a un indirizzo BCC specificato.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Invia una copia dell'email con i documenti completati a un indirizzo BCC specificato.
|
||||||
re_send_email: Reinvia email
|
re_send_email: Reinvia email
|
||||||
re_send_emails: Reinvia le email
|
re_send_emails: Reinvia le email
|
||||||
@@ -3332,6 +3342,8 @@ it: &it
|
|||||||
failed_to_save_the_scan: Impossibile salvare la scansione
|
failed_to_save_the_scan: Impossibile salvare la scansione
|
||||||
failed_to_scan_the_document: Impossibile scansionare il documento
|
failed_to_scan_the_document: Impossibile scansionare il documento
|
||||||
document_scanning_is_not_available_on_this_device: La scansione dei documenti non è disponibile su questo dispositivo
|
document_scanning_is_not_available_on_this_device: La scansione dei documenti non è disponibile su questo dispositivo
|
||||||
|
cc_view_only: CC Sola lettura
|
||||||
|
bcc_completed: BCC Completato
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Il tuo indirizzo email è stato confermato con successo.
|
confirmed: Il tuo indirizzo email è stato confermato con successo.
|
||||||
@@ -4087,6 +4099,7 @@ fr: &fr
|
|||||||
form_has_been_declined: Le formulaire a été refusé.
|
form_has_been_declined: Le formulaire a été refusé.
|
||||||
form_is_view_only: Le formulaire est en lecture seule.
|
form_is_view_only: Le formulaire est en lecture seule.
|
||||||
file_is_missing: Fichier manquant
|
file_is_missing: Fichier manquant
|
||||||
|
file_is_not_an_image: Le fichier n'est pas une image
|
||||||
folder_name_has_been_updated: Le nom du dossier a été mis à jour.
|
folder_name_has_been_updated: Le nom du dossier a été mis à jour.
|
||||||
unable_to_rename_folder: Impossible de renommer le dossier.
|
unable_to_rename_folder: Impossible de renommer le dossier.
|
||||||
template_has_been_removed: Le modèle a été supprimé.
|
template_has_been_removed: Le modèle a été supprimé.
|
||||||
@@ -4146,7 +4159,7 @@ fr: &fr
|
|||||||
thirty_days: 30 jours
|
thirty_days: 30 jours
|
||||||
free: Gratuit
|
free: Gratuit
|
||||||
unlimited_documents_storage: Stockage de documents illimité
|
unlimited_documents_storage: Stockage de documents illimité
|
||||||
users_management: Gestion des utilisateurs
|
user_management: Gestion des utilisateurs
|
||||||
ten_field_types: 10 types de champs
|
ten_field_types: 10 types de champs
|
||||||
multiple_signers: Plusieurs signataires
|
multiple_signers: Plusieurs signataires
|
||||||
automatic_esignature: Signature électronique automatique
|
automatic_esignature: Signature électronique automatique
|
||||||
@@ -4166,6 +4179,7 @@ fr: &fr
|
|||||||
developer_sandbox: Bac à sable développeur.
|
developer_sandbox: Bac à sable développeur.
|
||||||
upgrade_to_start_using_in_production_html: <a href="%{url}" target="_blank" class="link font-semibold">Mettez à niveau</a> pour commencer à utiliser en production.
|
upgrade_to_start_using_in_production_html: <a href="%{url}" target="_blank" class="link font-semibold">Mettez à niveau</a> pour commencer à utiliser en production.
|
||||||
download_combined_pdf: Télécharger le PDF combiné
|
download_combined_pdf: Télécharger le PDF combiné
|
||||||
|
download_event_log: Télécharger le journal des événements
|
||||||
audit_log: Journal d’audit
|
audit_log: Journal d’audit
|
||||||
signers: Signataires
|
signers: Signataires
|
||||||
not_invited_yet: Pas encore invité
|
not_invited_yet: Pas encore invité
|
||||||
@@ -4177,7 +4191,7 @@ fr: &fr
|
|||||||
declined_on_time: Refusé le %{time}
|
declined_on_time: Refusé le %{time}
|
||||||
expire_on_time: Expire le %{time}
|
expire_on_time: Expire le %{time}
|
||||||
sign_in_person: Signer en personne
|
sign_in_person: Signer en personne
|
||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Créez un nouveau modèle</a> de document ou <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">soumettez l’existant</a>
|
create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Créez un nouveau modèle</a> de document
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envoyer une copie e‑mail avec les documents complétés à une adresse Cci spécifiée.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envoyer une copie e‑mail avec les documents complétés à une adresse Cci spécifiée.
|
||||||
re_send_email: Renvoyer l’e‑mail
|
re_send_email: Renvoyer l’e‑mail
|
||||||
re_send_emails: Renvoyer e‑mails
|
re_send_emails: Renvoyer e‑mails
|
||||||
@@ -4475,6 +4489,8 @@ fr: &fr
|
|||||||
failed_to_save_the_scan: Impossible d’enregistrer la numérisation
|
failed_to_save_the_scan: Impossible d’enregistrer la numérisation
|
||||||
failed_to_scan_the_document: Impossible de numériser le document
|
failed_to_scan_the_document: Impossible de numériser le document
|
||||||
document_scanning_is_not_available_on_this_device: La numérisation de documents n’est pas disponible sur cet appareil
|
document_scanning_is_not_available_on_this_device: La numérisation de documents n’est pas disponible sur cet appareil
|
||||||
|
cc_view_only: Cc Lecture seule
|
||||||
|
bcc_completed: Cci Terminé
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Votre adresse e-mail a été confirmée avec succès.
|
confirmed: Votre adresse e-mail a été confirmée avec succès.
|
||||||
@@ -5243,6 +5259,7 @@ pt: &pt
|
|||||||
form_has_been_declined: O formulário foi recusado.
|
form_has_been_declined: O formulário foi recusado.
|
||||||
form_is_view_only: O formulário é somente leitura.
|
form_is_view_only: O formulário é somente leitura.
|
||||||
file_is_missing: O arquivo está ausente
|
file_is_missing: O arquivo está ausente
|
||||||
|
file_is_not_an_image: O arquivo não é uma imagem
|
||||||
folder_name_has_been_updated: O nome da pasta foi atualizado.
|
folder_name_has_been_updated: O nome da pasta foi atualizado.
|
||||||
unable_to_rename_folder: Não foi possível renomear a pasta.
|
unable_to_rename_folder: Não foi possível renomear a pasta.
|
||||||
template_has_been_removed: O modelo foi removido.
|
template_has_been_removed: O modelo foi removido.
|
||||||
@@ -5302,7 +5319,7 @@ pt: &pt
|
|||||||
thirty_days: 30 dias
|
thirty_days: 30 dias
|
||||||
free: Gratuito
|
free: Gratuito
|
||||||
unlimited_documents_storage: Armazenamento ilimitado
|
unlimited_documents_storage: Armazenamento ilimitado
|
||||||
users_management: Gerenciamento de usuários
|
user_management: Gerenciamento de usuários
|
||||||
ten_field_types: 10 tipos de campo
|
ten_field_types: 10 tipos de campo
|
||||||
multiple_signers: Múltiplos signatários
|
multiple_signers: Múltiplos signatários
|
||||||
automatic_esignature: Assinatura eletrônica automática
|
automatic_esignature: Assinatura eletrônica automática
|
||||||
@@ -5322,6 +5339,7 @@ pt: &pt
|
|||||||
developer_sandbox: Sandbox para desenvolvedores
|
developer_sandbox: Sandbox para desenvolvedores
|
||||||
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Atualizar</a> para começar a usar em produção.'
|
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Atualizar</a> para começar a usar em produção.'
|
||||||
download_combined_pdf: Baixar PDF combinado
|
download_combined_pdf: Baixar PDF combinado
|
||||||
|
download_event_log: Baixar log de eventos
|
||||||
audit_log: Log de auditoria
|
audit_log: Log de auditoria
|
||||||
signers: Signatários
|
signers: Signatários
|
||||||
not_invited_yet: Ainda não convidado
|
not_invited_yet: Ainda não convidado
|
||||||
@@ -5333,7 +5351,7 @@ pt: &pt
|
|||||||
declined_on_time: 'Recusado em %{time}'
|
declined_on_time: 'Recusado em %{time}'
|
||||||
expire_on_time: 'Expira em %{time}'
|
expire_on_time: 'Expira em %{time}'
|
||||||
sign_in_person: Assinar pessoalmente
|
sign_in_person: Assinar pessoalmente
|
||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: '<a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Criar um novo modelo</a> de documento ou <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submeter o existente</a>'
|
create_a_new_template_document_form_html: '<a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Criar um novo modelo</a> de documento'
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envie uma cópia do e-mail com documentos concluídos para um endereço BCC especificado.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envie uma cópia do e-mail com documentos concluídos para um endereço BCC especificado.
|
||||||
re_send_email: Reenviar e-mail
|
re_send_email: Reenviar e-mail
|
||||||
re_send_emails: Reenviar e-mails
|
re_send_emails: Reenviar e-mails
|
||||||
@@ -5634,6 +5652,8 @@ pt: &pt
|
|||||||
failed_to_save_the_scan: Não foi possível salvar a digitalização
|
failed_to_save_the_scan: Não foi possível salvar a digitalização
|
||||||
failed_to_scan_the_document: Não foi possível digitalizar o documento
|
failed_to_scan_the_document: Não foi possível digitalizar o documento
|
||||||
document_scanning_is_not_available_on_this_device: A digitalização de documentos não está disponível neste dispositivo
|
document_scanning_is_not_available_on_this_device: A digitalização de documentos não está disponível neste dispositivo
|
||||||
|
cc_view_only: CC Somente leitura
|
||||||
|
bcc_completed: BCC Concluído
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Seu endereço de e-mail foi confirmado com sucesso.
|
confirmed: Seu endereço de e-mail foi confirmado com sucesso.
|
||||||
@@ -6402,6 +6422,7 @@ de: &de
|
|||||||
form_has_been_declined: Das Formular wurde abgelehnt.
|
form_has_been_declined: Das Formular wurde abgelehnt.
|
||||||
form_is_view_only: Das Formular ist schreibgeschützt.
|
form_is_view_only: Das Formular ist schreibgeschützt.
|
||||||
file_is_missing: Datei fehlt
|
file_is_missing: Datei fehlt
|
||||||
|
file_is_not_an_image: Datei ist kein Bild
|
||||||
folder_name_has_been_updated: Der Ordnername wurde aktualisiert.
|
folder_name_has_been_updated: Der Ordnername wurde aktualisiert.
|
||||||
unable_to_rename_folder: Der Ordner konnte nicht umbenannt werden.
|
unable_to_rename_folder: Der Ordner konnte nicht umbenannt werden.
|
||||||
template_has_been_removed: Die Vorlage wurde entfernt.
|
template_has_been_removed: Die Vorlage wurde entfernt.
|
||||||
@@ -6461,7 +6482,7 @@ de: &de
|
|||||||
thirty_days: 30 Tage
|
thirty_days: 30 Tage
|
||||||
free: Kostenlos
|
free: Kostenlos
|
||||||
unlimited_documents_storage: Unbegrenzter Dokumentenspeicher
|
unlimited_documents_storage: Unbegrenzter Dokumentenspeicher
|
||||||
users_management: Benutzerverwaltung
|
user_management: Benutzerverwaltung
|
||||||
ten_field_types: 10 Feldtypen
|
ten_field_types: 10 Feldtypen
|
||||||
multiple_signers: Mehrere Unterzeichner
|
multiple_signers: Mehrere Unterzeichner
|
||||||
automatic_esignature: Automatische E‑Signatur
|
automatic_esignature: Automatische E‑Signatur
|
||||||
@@ -6481,6 +6502,7 @@ de: &de
|
|||||||
developer_sandbox: Entwickler-Sandbox.
|
developer_sandbox: Entwickler-Sandbox.
|
||||||
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> durchführen, um die Produktionsnutzung zu starten.'
|
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> durchführen, um die Produktionsnutzung zu starten.'
|
||||||
download_combined_pdf: Kombiniertes PDF herunterladen
|
download_combined_pdf: Kombiniertes PDF herunterladen
|
||||||
|
download_event_log: Ereignisprotokoll herunterladen
|
||||||
audit_log: Prüfprotokoll
|
audit_log: Prüfprotokoll
|
||||||
signers: Unterzeichner
|
signers: Unterzeichner
|
||||||
not_invited_yet: Noch nicht eingeladen
|
not_invited_yet: Noch nicht eingeladen
|
||||||
@@ -6492,7 +6514,7 @@ de: &de
|
|||||||
declined_on_time: 'Abgelehnt am %{time}'
|
declined_on_time: 'Abgelehnt am %{time}'
|
||||||
expire_on_time: 'Läuft ab am %{time}'
|
expire_on_time: 'Läuft ab am %{time}'
|
||||||
sign_in_person: Vor Ort unterschreiben
|
sign_in_person: Vor Ort unterschreiben
|
||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Neue Vorlage erstellen</a> oder <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">bestehende einreichen</a>
|
create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Neue Vorlage erstellen</a>
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Senden Sie eine E-Mail-Kopie mit abgeschlossenen Dokumenten an eine angegebene BCC-Adresse.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Senden Sie eine E-Mail-Kopie mit abgeschlossenen Dokumenten an eine angegebene BCC-Adresse.
|
||||||
re_send_email: E-Mail erneut senden
|
re_send_email: E-Mail erneut senden
|
||||||
re_send_emails: E-Mails senden
|
re_send_emails: E-Mails senden
|
||||||
@@ -6793,6 +6815,8 @@ de: &de
|
|||||||
failed_to_save_the_scan: Scan konnte nicht gespeichert werden
|
failed_to_save_the_scan: Scan konnte nicht gespeichert werden
|
||||||
failed_to_scan_the_document: Dokument konnte nicht gescannt werden
|
failed_to_scan_the_document: Dokument konnte nicht gescannt werden
|
||||||
document_scanning_is_not_available_on_this_device: Das Scannen von Dokumenten ist auf diesem Gerät nicht verfügbar
|
document_scanning_is_not_available_on_this_device: Das Scannen von Dokumenten ist auf diesem Gerät nicht verfügbar
|
||||||
|
cc_view_only: CC Nur Ansicht
|
||||||
|
bcc_completed: BCC Abgeschlossen
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
|
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
|
||||||
@@ -6975,14 +6999,14 @@ pl:
|
|||||||
decline_reason: Powód odrzucenia
|
decline_reason: Powód odrzucenia
|
||||||
provide_a_reason: Podaj powód
|
provide_a_reason: Podaj powód
|
||||||
notify_the_sender_with_the_reason_you_declined: Powiadom nadawcę o powodzie, dla którego odrzuciłeś
|
notify_the_sender_with_the_reason_you_declined: Powiadom nadawcę o powodzie, dla którego odrzuciłeś
|
||||||
form_has_been_declined_on_html: 'Formularz został odrzucony o <span class="font-semibold">%{time}</span>'
|
form_has_been_declined_on_html: 'Formularz został odrzucony <span class="font-semibold">%{time}</span>'
|
||||||
document_has_been_delegated_on_html: 'Dokument został oddelegowany o <span class="font-semibold">%{time}</span>'
|
document_has_been_delegated_on_html: 'Dokument został oddelegowany <span class="font-semibold">%{time}</span>'
|
||||||
name_declined_by_submitter: '"%{name}" odrzucono przez %{submitter}'
|
name_declined_by_submitter: '"%{name}" odrzucono przez %{submitter}'
|
||||||
name_declined_by_submitter_with_the_following_reason: '"%{name}" został odrzucony przez %{submitter} z następującym powodem:'
|
name_declined_by_submitter_with_the_following_reason: '"%{name}" został odrzucony przez %{submitter} z następującym powodem:'
|
||||||
email: E-mail
|
email: E-mail
|
||||||
verification_code_code: 'Kod weryfikacyjny: %{code}'
|
verification_code_code: 'Kod weryfikacyjny: %{code}'
|
||||||
digitally_signed_by: Podpis cyfrowy przez
|
digitally_signed_by: Podpis cyfrowy przez
|
||||||
form_expired_at_html: 'Formularz wygasł o <span class="font-semibold">%{time}</span>'
|
form_expired_at_html: 'Formularz wygasł <span class="font-semibold">%{time}</span>'
|
||||||
role: Rola
|
role: Rola
|
||||||
provide_your_email_to_start: Podaj swój e-mail, aby rozpocząć
|
provide_your_email_to_start: Podaj swój e-mail, aby rozpocząć
|
||||||
provide_your_email: Podaj swój e-mail
|
provide_your_email: Podaj swój e-mail
|
||||||
@@ -7117,8 +7141,8 @@ uk:
|
|||||||
decline_reason: Причина відхилення
|
decline_reason: Причина відхилення
|
||||||
provide_a_reason: Надайте причину
|
provide_a_reason: Надайте причину
|
||||||
notify_the_sender_with_the_reason_you_declined: Повідомте відправника про причину свого відхилення
|
notify_the_sender_with_the_reason_you_declined: Повідомте відправника про причину свого відхилення
|
||||||
form_has_been_declined_on_html: 'Форму було відхилено о <span class="font-semibold">%{time}</span>'
|
form_has_been_declined_on_html: 'Форму було відхилено <span class="font-semibold">%{time}</span>'
|
||||||
document_has_been_delegated_on_html: 'Документ було делеговано о <span class="font-semibold">%{time}</span>'
|
document_has_been_delegated_on_html: 'Документ було делеговано <span class="font-semibold">%{time}</span>'
|
||||||
name_declined_by_submitter: '"%{name}" відхилено %{submitter}'
|
name_declined_by_submitter: '"%{name}" відхилено %{submitter}'
|
||||||
name_declined_by_submitter_with_the_following_reason: '"%{name}" було відхилено %{submitter} з такою причиною:'
|
name_declined_by_submitter_with_the_following_reason: '"%{name}" було відхилено %{submitter} з такою причиною:'
|
||||||
email: E-mail
|
email: E-mail
|
||||||
@@ -8089,6 +8113,7 @@ nl: &nl
|
|||||||
form_has_been_declined: Formulier is geweigerd.
|
form_has_been_declined: Formulier is geweigerd.
|
||||||
form_is_view_only: Formulier is alleen-lezen.
|
form_is_view_only: Formulier is alleen-lezen.
|
||||||
file_is_missing: Bestand ontbreekt
|
file_is_missing: Bestand ontbreekt
|
||||||
|
file_is_not_an_image: Bestand is geen afbeelding
|
||||||
folder_name_has_been_updated: Mapnaam is bijgewerkt.
|
folder_name_has_been_updated: Mapnaam is bijgewerkt.
|
||||||
unable_to_rename_folder: Kan map niet hernoemen.
|
unable_to_rename_folder: Kan map niet hernoemen.
|
||||||
template_has_been_removed: Sjabloon is verwijderd.
|
template_has_been_removed: Sjabloon is verwijderd.
|
||||||
@@ -8148,7 +8173,7 @@ nl: &nl
|
|||||||
thirty_days: 30 dagen
|
thirty_days: 30 dagen
|
||||||
free: Gratis
|
free: Gratis
|
||||||
unlimited_documents_storage: Onbeperkte documentopslag
|
unlimited_documents_storage: Onbeperkte documentopslag
|
||||||
users_management: Gebruikersbeheer
|
user_management: Gebruikersbeheer
|
||||||
ten_field_types: 10 veldtypen
|
ten_field_types: 10 veldtypen
|
||||||
multiple_signers: Meerdere ondertekenaars
|
multiple_signers: Meerdere ondertekenaars
|
||||||
automatic_esignature: Automatische e-handtekening
|
automatic_esignature: Automatische e-handtekening
|
||||||
@@ -8168,6 +8193,7 @@ nl: &nl
|
|||||||
developer_sandbox: Ontwikkelaars-sandbox.
|
developer_sandbox: Ontwikkelaars-sandbox.
|
||||||
upgrade_to_start_using_in_production_html: <a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> om in productie te gaan gebruiken.
|
upgrade_to_start_using_in_production_html: <a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> om in productie te gaan gebruiken.
|
||||||
download_combined_pdf: Gecombineerde PDF downloaden
|
download_combined_pdf: Gecombineerde PDF downloaden
|
||||||
|
download_event_log: Gebeurtenislogboek downloaden
|
||||||
audit_log: Auditlogboek
|
audit_log: Auditlogboek
|
||||||
signers: Ondertekenaars
|
signers: Ondertekenaars
|
||||||
not_invited_yet: Nog niet uitgenodigd
|
not_invited_yet: Nog niet uitgenodigd
|
||||||
@@ -8179,7 +8205,7 @@ nl: &nl
|
|||||||
declined_on_time: Geweigerd op %{time}
|
declined_on_time: Geweigerd op %{time}
|
||||||
expire_on_time: Verloopt op %{time}
|
expire_on_time: Verloopt op %{time}
|
||||||
sign_in_person: In persoon ondertekenen
|
sign_in_person: In persoon ondertekenen
|
||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Maak een nieuw sjabloon</a> voor een documentformulier of <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">dien het bestaande in</a>
|
create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Maak een nieuw sjabloon</a> voor een documentformulier
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Stuur een e-mailkopie met voltooide documenten naar een opgegeven BCC-adres.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Stuur een e-mailkopie met voltooide documenten naar een opgegeven BCC-adres.
|
||||||
re_send_email: E-mail opnieuw verzenden
|
re_send_email: E-mail opnieuw verzenden
|
||||||
re_send_emails: E-mails verzenden
|
re_send_emails: E-mails verzenden
|
||||||
@@ -8476,6 +8502,8 @@ nl: &nl
|
|||||||
failed_to_save_the_scan: Scan kon niet worden opgeslagen
|
failed_to_save_the_scan: Scan kon niet worden opgeslagen
|
||||||
failed_to_scan_the_document: Document kon niet worden gescand
|
failed_to_scan_the_document: Document kon niet worden gescand
|
||||||
document_scanning_is_not_available_on_this_device: Documenten scannen is niet beschikbaar op dit apparaat
|
document_scanning_is_not_available_on_this_device: Documenten scannen is niet beschikbaar op dit apparaat
|
||||||
|
cc_view_only: CC Alleen bekijken
|
||||||
|
bcc_completed: BCC Voltooid
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Uw e-mailadres is succesvol bevestigd.
|
confirmed: Uw e-mailadres is succesvol bevestigd.
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateOtpChallenges < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
create_table :otp_challenges do |t|
|
||||||
|
t.string :key, null: false
|
||||||
|
t.integer :version, null: false
|
||||||
|
t.string :nonce, null: false
|
||||||
|
t.integer :attempts, null: false, default: 0
|
||||||
|
t.datetime :expire_at, null: false
|
||||||
|
t.datetime :verified_at
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
|
||||||
|
t.index %i[key version], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+12
-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[8.1].define(version: 2026_08_19_091500) do
|
ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) 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 "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
@@ -322,6 +322,17 @@ ActiveRecord::Schema[8.1].define(version: 2026_08_19_091500) do
|
|||||||
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
|
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "otp_challenges", force: :cascade do |t|
|
||||||
|
t.integer "attempts", default: 0, null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "expire_at", null: false
|
||||||
|
t.string "key", null: false
|
||||||
|
t.string "nonce", null: false
|
||||||
|
t.datetime "verified_at"
|
||||||
|
t.integer "version", null: false
|
||||||
|
t.index ["key", "version"], name: "index_otp_challenges_on_key_and_version", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "search_entries", force: :cascade do |t|
|
create_table "search_entries", force: :cascade do |t|
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
|||||||
@@ -5,34 +5,6 @@ module Accounts
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def create_duplicate(account)
|
|
||||||
new_account = account.dup
|
|
||||||
|
|
||||||
new_user = account.users.first.dup
|
|
||||||
|
|
||||||
new_user.uuid = SecureRandom.uuid
|
|
||||||
new_user.account = new_account
|
|
||||||
new_user.encrypted_password = SecureRandom.hex
|
|
||||||
new_user.email = "#{SecureRandom.hex}@docuseal.com"
|
|
||||||
|
|
||||||
account.templates.each do |template|
|
|
||||||
new_template = template.dup
|
|
||||||
|
|
||||||
new_template.account = new_account
|
|
||||||
new_template.slug = SecureRandom.base58(14)
|
|
||||||
|
|
||||||
new_template.archived_at = nil
|
|
||||||
new_template.save!
|
|
||||||
|
|
||||||
Templates::CloneAttachments.call(template: new_template, original_template: template)
|
|
||||||
end
|
|
||||||
|
|
||||||
new_user.save!(validate: false)
|
|
||||||
new_account.templates.update_all(folder_id: new_account.default_template_folder.id)
|
|
||||||
|
|
||||||
new_account
|
|
||||||
end
|
|
||||||
|
|
||||||
def users_count(account)
|
def users_count(account)
|
||||||
rel = User.where(account_id: account.id).or(
|
rel = User.where(account_id: account.id).or(
|
||||||
User.where(account_id: account.account_linked_accounts
|
User.where(account_id: account.account_linked_accounts
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module EmailVerificationCodes
|
|
||||||
DRIFT_BEHIND = 5.minutes
|
|
||||||
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def generate(value)
|
|
||||||
totp = ROTP::TOTP.new(build_totp_secret(value))
|
|
||||||
|
|
||||||
totp.at(Time.current)
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify(code, value)
|
|
||||||
totp = ROTP::TOTP.new(build_totp_secret(value))
|
|
||||||
|
|
||||||
totp.verify(code, drift_behind: DRIFT_BEHIND)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_totp_secret(value)
|
|
||||||
ROTP::Base32.encode(
|
|
||||||
Digest::SHA1.digest(
|
|
||||||
[Rails.application.secret_key_base, 'form_email_2fa', value].join(':')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module OtpChallenges
|
||||||
|
CODE_TTL = 10.minutes
|
||||||
|
REUSE_MIN_TTL = 5.minutes
|
||||||
|
MAX_ATTEMPTS = 20
|
||||||
|
MAX_TOTAL_ATTEMPTS = 100
|
||||||
|
ATTEMPTS_WINDOW = 23.hours
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def generate(value, purpose:)
|
||||||
|
key = build_key(value, purpose)
|
||||||
|
latest_challenge = OtpChallenge.where(key:).order(version: :desc).first
|
||||||
|
|
||||||
|
otp_challenge =
|
||||||
|
if latest_challenge && !latest_challenge.verified_at? && latest_challenge.expire_at > REUSE_MIN_TTL.from_now
|
||||||
|
latest_challenge
|
||||||
|
else
|
||||||
|
version = latest_challenge ? latest_challenge.version + 1 : 1
|
||||||
|
|
||||||
|
OtpChallenge.create_with(nonce: SecureRandom.base58(16), expire_at: CODE_TTL.from_now)
|
||||||
|
.create_or_find_by!(key:, version:)
|
||||||
|
end
|
||||||
|
|
||||||
|
build_hotp(value, purpose, otp_challenge.nonce).at(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify(code, value, purpose:)
|
||||||
|
challenges = OtpChallenge.where(key: build_key(value, purpose))
|
||||||
|
|
||||||
|
recent_challenges = challenges.where(expire_at: ATTEMPTS_WINDOW.ago..).order(version: :desc).to_a
|
||||||
|
|
||||||
|
latest_challenge = recent_challenges.first
|
||||||
|
|
||||||
|
return false if latest_challenge.nil? || latest_challenge.verified_at? || latest_challenge.expire_at.past?
|
||||||
|
|
||||||
|
raise RateLimit::LimitApproached if recent_challenges.sum(&:attempts) >= MAX_ATTEMPTS
|
||||||
|
raise RateLimit::LimitApproached if challenges.sum(:attempts) >= MAX_TOTAL_ATTEMPTS
|
||||||
|
|
||||||
|
if build_hotp(value, purpose, latest_challenge.nonce).verify(code.to_s, 0)
|
||||||
|
OtpChallenge.where(id: latest_challenge.id, verified_at: nil).update_all(verified_at: Time.current) == 1
|
||||||
|
else
|
||||||
|
latest_challenge.increment!(:attempts)
|
||||||
|
|
||||||
|
OtpChallenge.current_transaction.after_rollback { OtpChallenge.update_counters(latest_challenge.id, attempts: 1) }
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_key(value, purpose)
|
||||||
|
Digest::SHA256.hexdigest([purpose, value].join(':'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_hotp(value, purpose, nonce)
|
||||||
|
ROTP::HOTP.new(
|
||||||
|
ROTP::Base32.encode(
|
||||||
|
Digest::SHA1.digest([Rails.application.secret_key_base, purpose, value, nonce].join(':'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -90,6 +90,10 @@ module Submissions
|
|||||||
|
|
||||||
maybe_add_invite_submitters(submission, template, attrs[:submitters])
|
maybe_add_invite_submitters(submission, template, attrs[:submitters])
|
||||||
|
|
||||||
|
maybe_add_cc_submitters(submission, params[:cc_recipients],
|
||||||
|
submitters_order:, user:, params:,
|
||||||
|
preferences: preferences.merge(submission_preferences))
|
||||||
|
|
||||||
assign_submitters_is_viewer(submission)
|
assign_submitters_is_viewer(submission)
|
||||||
|
|
||||||
submission.template = nil unless with_template
|
submission.template = nil unless with_template
|
||||||
@@ -211,6 +215,25 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_add_cc_submitters(submission, recipients, submitters_order:, user:, params:, preferences:)
|
||||||
|
order = submission.template_submitters.filter_map { |s| s['order'] }.max
|
||||||
|
|
||||||
|
Array.wrap(recipients).each do |recipient|
|
||||||
|
attrs = recipient.is_a?(String) ? { email: recipient } : recipient.slice(:name, :email, :phone)
|
||||||
|
|
||||||
|
next if attrs.compact_blank.blank?
|
||||||
|
|
||||||
|
uuid = SecureRandom.uuid
|
||||||
|
|
||||||
|
submission.template_submitters <<
|
||||||
|
{ 'name' => 'CC', 'uuid' => uuid, 'is_viewer' => true, 'order' => order }.compact
|
||||||
|
|
||||||
|
build_submitter(submission:, attrs:, uuid:, user:, params:,
|
||||||
|
is_order_sent: submitters_order == 'random',
|
||||||
|
preferences: preferences.except('email_message_uuid'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def submitter_message_preferences(uuid, params)
|
def submitter_message_preferences(uuid, params)
|
||||||
return {} if params[:request_email_per_submitter] != '1'
|
return {} if params[:request_email_per_submitter] != '1'
|
||||||
return {} if params[:is_custom_message] != '1'
|
return {} if params[:is_custom_message] != '1'
|
||||||
|
|||||||
+12
-4
@@ -152,7 +152,7 @@ module Submitters
|
|||||||
preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms')
|
preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms')
|
||||||
preferences['require_phone_2fa'] = params['require_phone_2fa'].in?(TRUE_VALUES) if params.key?('require_phone_2fa')
|
preferences['require_phone_2fa'] = params['require_phone_2fa'].in?(TRUE_VALUES) if params.key?('require_phone_2fa')
|
||||||
preferences['require_email_2fa'] = params['require_email_2fa'].in?(TRUE_VALUES) if params.key?('require_email_2fa')
|
preferences['require_email_2fa'] = params['require_email_2fa'].in?(TRUE_VALUES) if params.key?('require_email_2fa')
|
||||||
preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed')
|
preferences['bcc_completed'] = normalize_bcc_completed(params['bcc_completed']) if params.key?('bcc_completed')
|
||||||
preferences['reply_to'] = params['reply_to'] if params.key?('reply_to')
|
preferences['reply_to'] = params['reply_to'] if params.key?('reply_to')
|
||||||
preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last')
|
preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last')
|
||||||
preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url')
|
preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url')
|
||||||
@@ -160,6 +160,14 @@ module Submitters
|
|||||||
preferences
|
preferences
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalize_bcc_completed(value)
|
||||||
|
if value.is_a?(Array)
|
||||||
|
value.compact_blank.uniq.join(', ')
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_values_for_delegate(submitter)
|
def fetch_values_for_delegate(submitter)
|
||||||
fields = submitter.submission.template_fields || submitter.template.fields
|
fields = submitter.submission.template_fields || submitter.template.fields
|
||||||
default_values = submitter.preferences['default_values'] || {}
|
default_values = submitter.preferences['default_values'] || {}
|
||||||
@@ -280,12 +288,12 @@ module Submitters
|
|||||||
email_digest = Digest::MD5.base64digest(submitter.email.downcase.squish)
|
email_digest = Digest::MD5.base64digest(submitter.email.downcase.squish)
|
||||||
|
|
||||||
RateLimit.call("verify-2fa-code-#{email_digest}", limit: 2, ttl: 45.seconds, enabled: true)
|
RateLimit.call("verify-2fa-code-#{email_digest}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
RateLimit.call("verify-2fa-code-1h-#{email_digest}", limit: 10, ttl: 1.hour, enabled: true)
|
|
||||||
RateLimit.call("verify-2fa-code-1d-#{email_digest}", limit: 20, ttl: 1.day, enabled: true)
|
|
||||||
|
|
||||||
link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':')
|
link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':')
|
||||||
|
|
||||||
raise InvalidOtp, I18n.t(:invalid_code) unless EmailVerificationCodes.verify(otp, link_2fa_key)
|
unless OtpChallenges.verify(otp.to_s.gsub(/\D/, ''), link_2fa_key, purpose: 'form_email_2fa')
|
||||||
|
raise InvalidOtp, I18n.t(:invalid_code)
|
||||||
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ module Templates
|
|||||||
['.png', 'image/png', {}]
|
['.png', 'image/png', {}]
|
||||||
end
|
end
|
||||||
|
|
||||||
data = image.write_to_buffer(extension, **format_args)
|
data = image.write_to_buffer(extension, strip: true, **format_args)
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.create_and_upload!(
|
blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
io: StringIO.new(data),
|
io: StringIO.new(data),
|
||||||
|
|||||||
@@ -90,23 +90,28 @@ module Templates
|
|||||||
|
|
||||||
pool = Concurrent::FixedThreadPool.new(concurrency)
|
pool = Concurrent::FixedThreadPool.new(concurrency)
|
||||||
|
|
||||||
promises =
|
blobs =
|
||||||
range.map do |page_number|
|
range.each_slice(concurrency).flat_map do |page_numbers|
|
||||||
doc_page = doc.get_page(page_number)
|
promises =
|
||||||
|
page_numbers.map do |page_number|
|
||||||
|
doc_page = doc.get_page(page_number)
|
||||||
|
|
||||||
hide_placeholder_widgets(doc_page, hide_empty: !flatten_pages)
|
hide_placeholder_widgets(doc_page, hide_empty: !flatten_pages)
|
||||||
doc_page.flatten if flatten_pages
|
doc_page.flatten if flatten_pages
|
||||||
|
|
||||||
bytes, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH)
|
bytes, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH)
|
||||||
|
|
||||||
image = Vips::Image.new_from_memory_copy(bytes, width, height, 4, :uchar)
|
image = Vips::Image.new_from_memory_copy(bytes, width, height, 4, :uchar)
|
||||||
|
|
||||||
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(image, page_number) }
|
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(image, page_number) }
|
||||||
ensure
|
ensure
|
||||||
doc_page&.close
|
doc_page&.close
|
||||||
|
end
|
||||||
|
|
||||||
|
Concurrent::Promise.zip(*promises).value!
|
||||||
end
|
end
|
||||||
|
|
||||||
Concurrent::Promise.zip(*promises).value!.each do |blob|
|
blobs.each do |blob|
|
||||||
next unless blob
|
next unless blob
|
||||||
|
|
||||||
ApplicationRecord.no_touching do
|
ApplicationRecord.no_touching do
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"@tiptap/extension-dropcursor": "^3.19.0",
|
"@tiptap/extension-dropcursor": "^3.19.0",
|
||||||
"@tiptap/extension-gapcursor": "^3.19.0",
|
"@tiptap/extension-gapcursor": "^3.19.0",
|
||||||
"@tiptap/extension-hard-break": "^3.19.0",
|
"@tiptap/extension-hard-break": "^3.19.0",
|
||||||
"@tiptap/extension-history": "^3.19.0",
|
|
||||||
"@tiptap/extension-italic": "^3.19.0",
|
"@tiptap/extension-italic": "^3.19.0",
|
||||||
"@tiptap/extension-link": "^3.19.0",
|
"@tiptap/extension-link": "^3.19.0",
|
||||||
"@tiptap/extension-paragraph": "^3.19.0",
|
"@tiptap/extension-paragraph": "^3.19.0",
|
||||||
|
|||||||
@@ -1796,11 +1796,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz#7120524cec9ed4b957963693cb4c57cbecbaecf8"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz#7120524cec9ed4b957963693cb4c57cbecbaecf8"
|
||||||
integrity sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew==
|
integrity sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew==
|
||||||
|
|
||||||
"@tiptap/extension-history@^3.19.0":
|
|
||||||
version "3.19.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-3.19.0.tgz#dc3a52f4eb16b7ac077c28284684b558c03adf00"
|
|
||||||
integrity sha512-hhN5nL7Pqcd9iomzeUUMKnmSQieKNlJ2FUgf2dHUqqvn4pWvcHA6P6UwdDNhuKFivSJNNMqtajkOvO4bYq1KPw==
|
|
||||||
|
|
||||||
"@tiptap/extension-italic@^3.19.0":
|
"@tiptap/extension-italic@^3.19.0":
|
||||||
version "3.19.0"
|
version "3.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz#af2a9c095ec846e379041f3e17e1dd101a5a4bf8"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz#af2a9c095ec846e379041f3e17e1dd101a5a4bf8"
|
||||||
|
|||||||
Reference in new issue
Block a user