mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c8fae78d1 | ||
|
|
441c1d7044 | ||
|
|
203f4633f7 | ||
|
|
e558f4d9fb | ||
|
|
990e4020e9 | ||
|
|
1ad3e45519 | ||
|
|
442ced8381 | ||
|
|
ba46202a4d | ||
|
|
1c77a1ee90 | ||
|
|
ea74b59535 | ||
|
|
0e9343d9e2 | ||
|
|
af4cf7257f | ||
|
|
c8d4ee58f8 | ||
|
|
65e57c22e5 | ||
|
|
7351ae0a77 | ||
|
|
f1e0d5d137 | ||
|
|
8a1b71688c |
No files matched your search
+1
-1
@@ -24,7 +24,7 @@ COPY ./app/views ./app/views
|
|||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine as app
|
FROM ruby:3.2.2-alpine3.18 as app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ gem 'jwt'
|
|||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'omniauth-github'
|
|
||||||
gem 'omniauth-google-oauth2'
|
|
||||||
gem 'omniauth-microsoft-office365'
|
|
||||||
gem 'omniauth-rails_csrf_protection'
|
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pdf-reader'
|
gem 'pdf-reader'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
|
|||||||
@@ -245,7 +245,6 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
hashery (2.1.2)
|
hashery (2.1.2)
|
||||||
hashie (5.0.0)
|
|
||||||
hexapdf (0.34.1)
|
hexapdf (0.34.1)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
@@ -296,7 +295,6 @@ GEM
|
|||||||
minitest (5.20.0)
|
minitest (5.20.0)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
mysql2 (0.5.5)
|
mysql2 (0.5.5)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
@@ -316,35 +314,7 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-arm64-darwin)
|
nokogiri (1.15.4-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth2 (2.0.9)
|
|
||||||
faraday (>= 0.17.3, < 3.0)
|
|
||||||
jwt (>= 1.0, < 3.0)
|
|
||||||
multi_xml (~> 0.5)
|
|
||||||
rack (>= 1.2, < 4)
|
|
||||||
snaky_hash (~> 2.0)
|
|
||||||
version_gem (~> 1.1)
|
|
||||||
oj (3.16.0)
|
oj (3.16.0)
|
||||||
omniauth (2.1.1)
|
|
||||||
hashie (>= 3.4.6)
|
|
||||||
rack (>= 2.2.3)
|
|
||||||
rack-protection
|
|
||||||
omniauth-github (2.0.1)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-oauth2 (~> 1.8)
|
|
||||||
omniauth-google-oauth2 (1.1.1)
|
|
||||||
jwt (>= 2.0)
|
|
||||||
oauth2 (~> 2.0.6)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-oauth2 (~> 1.8.0)
|
|
||||||
omniauth-microsoft-office365 (0.0.8)
|
|
||||||
omniauth
|
|
||||||
omniauth-oauth2
|
|
||||||
omniauth-oauth2 (1.8.0)
|
|
||||||
oauth2 (>= 1.4, < 3)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-rails_csrf_protection (1.0.1)
|
|
||||||
actionpack (>= 4.2)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
openssl (3.2.0)
|
openssl (3.2.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
@@ -378,8 +348,6 @@ GEM
|
|||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.7.1)
|
racc (1.7.1)
|
||||||
rack (2.2.8)
|
rack (2.2.8)
|
||||||
rack-protection (3.1.0)
|
|
||||||
rack (~> 2.2, >= 2.2.4)
|
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
@@ -520,9 +488,6 @@ GEM
|
|||||||
simplecov-html (0.12.3)
|
simplecov-html (0.12.3)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
snaky_hash (2.0.1)
|
|
||||||
hashie
|
|
||||||
version_gem (~> 1.1, >= 1.1.1)
|
|
||||||
sqlite3 (1.6.3)
|
sqlite3 (1.6.3)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
sqlite3 (1.6.3-arm64-darwin)
|
sqlite3 (1.6.3-arm64-darwin)
|
||||||
@@ -543,7 +508,6 @@ GEM
|
|||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
version_gem (1.1.3)
|
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.0)
|
web-console (4.2.0)
|
||||||
@@ -595,10 +559,6 @@ DEPENDENCIES
|
|||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
oj
|
oj
|
||||||
omniauth-github
|
|
||||||
omniauth-google-oauth2
|
|
||||||
omniauth-microsoft-office365
|
|
||||||
omniauth-rails_csrf_protection
|
|
||||||
pagy
|
pagy
|
||||||
pdf-reader
|
pdf-reader
|
||||||
pg
|
pg
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ module Api
|
|||||||
emails:,
|
emails:,
|
||||||
params:)
|
params:)
|
||||||
else
|
else
|
||||||
submissions_attrs, attachments = normalize_submissions_params!(submissions_params, template)
|
submissions_attrs, attachments =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
|
||||||
|
|
||||||
submissions = Submissions.create_from_submitters(
|
submissions = Submissions.create_from_submitters(
|
||||||
template:,
|
template:,
|
||||||
@@ -97,7 +98,8 @@ module Api
|
|||||||
params:
|
params:
|
||||||
)
|
)
|
||||||
|
|
||||||
save_default_value_attachments!(attachments, submissions.flat_map(&:submitters))
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments,
|
||||||
|
submissions.flat_map(&:submitters))
|
||||||
|
|
||||||
submissions
|
submissions
|
||||||
end
|
end
|
||||||
@@ -133,49 +135,5 @@ module Api
|
|||||||
]
|
]
|
||||||
).fetch(key, [])
|
).fetch(key, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_submissions_params!(submissions_params, template)
|
|
||||||
attachments = []
|
|
||||||
|
|
||||||
Array.wrap(submissions_params).each do |submission|
|
|
||||||
submission[:submitters].each_with_index do |submitter, index|
|
|
||||||
default_values = submitter[:values] || {}
|
|
||||||
|
|
||||||
submitter[:fields]&.each { |f| default_values[f[:name]] = f[:default_value] if f[:default_value].present? }
|
|
||||||
|
|
||||||
next if default_values.blank?
|
|
||||||
|
|
||||||
values, new_attachments =
|
|
||||||
Submitters::NormalizeValues.call(template,
|
|
||||||
default_values,
|
|
||||||
submitter[:role] || template.submitters[index]['name'],
|
|
||||||
throw_errors: true)
|
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
|
||||||
|
|
||||||
submitter[:values] = values
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
[submissions_params, attachments]
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_default_value_attachments!(attachments, submitters)
|
|
||||||
return if attachments.blank?
|
|
||||||
|
|
||||||
attachments_index = attachments.index_by(&:uuid)
|
|
||||||
|
|
||||||
submitters.each do |submitter|
|
|
||||||
submitter.values.to_a.each do |_, value|
|
|
||||||
attachment = attachments_index[value]
|
|
||||||
|
|
||||||
next unless attachment
|
|
||||||
|
|
||||||
attachment.record = submitter
|
|
||||||
|
|
||||||
attachment.save!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -30,5 +30,79 @@ module Api
|
|||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @submitter.completed_at?
|
||||||
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||||
|
|
||||||
|
normalized_params, new_attachments =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template,
|
||||||
|
for_submitter: @submitter)
|
||||||
|
|
||||||
|
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
|
||||||
|
[normalized_params],
|
||||||
|
submitter_uuid: @submitter.uuid)
|
||||||
|
|
||||||
|
assign_submitter_attrs(@submitter, normalized_params)
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter])
|
||||||
|
|
||||||
|
@submitter.save!
|
||||||
|
|
||||||
|
@submitter.submission.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
if @submitter.completed_at?
|
||||||
|
ProcessSubmitterCompletionJob.perform_later(@submitter)
|
||||||
|
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||||
|
Submitters.send_signature_requests([@submitter])
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def submitter_params
|
||||||
|
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||||
|
|
||||||
|
submitter_params.permit(
|
||||||
|
:send_email, :send_sms, :uuid, :name, :email, :role, :completed, :phone, :application_key,
|
||||||
|
{ values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
|
fields: [%i[name default_value readonly validation_pattern invalid_message]] }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def assign_submitter_attrs(submitter, attrs)
|
||||||
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
||||||
|
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values]
|
||||||
|
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
|
||||||
|
submitter.application_key = attrs[:application_key] if attrs.key?(:application_key)
|
||||||
|
|
||||||
|
assign_preferences(submitter, attrs)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_preferences(submitter, attrs)
|
||||||
|
submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, attrs)
|
||||||
|
|
||||||
|
if submitter_preferences.key?('send_email')
|
||||||
|
submitter.preferences['send_email'] = submitter_preferences['send_email']
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||||
|
|
||||||
|
return unless submitter_preferences.key?('email_message_uuid')
|
||||||
|
|
||||||
|
submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -58,7 +58,9 @@ module Api
|
|||||||
schema: [%i[attachment_uuid name]],
|
schema: [%i[attachment_uuid name]],
|
||||||
submitters: [%i[name uuid]],
|
submitters: [%i[name uuid]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
||||||
{ options: [%i[value uuid]], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
{ preferences: {},
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
||||||
def google_oauth2
|
|
||||||
@user = Users.from_omniauth(request.env['omniauth.auth'])
|
|
||||||
|
|
||||||
if @user.persisted?
|
|
||||||
flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google')
|
|
||||||
|
|
||||||
sign_in_and_redirect @user, event: :authentication
|
|
||||||
else
|
|
||||||
redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)),
|
|
||||||
notice: 'Please complete registration with Google auth'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -42,6 +42,12 @@ class SubmitFormController < ApplicationController
|
|||||||
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
|
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if submitter.template.deleted_at? || submitter.submission.deleted_at?
|
||||||
|
Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
|
withPayment: this.dataset.withPayment !== 'false',
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
|
|
||||||
this.app = createApp(Form, {
|
this.app = createApp(Form, {
|
||||||
submitter: JSON.parse(this.dataset.submitter),
|
submitter: JSON.parse(this.dataset.submitter),
|
||||||
authenticityToken: this.dataset.authenticityToken,
|
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||||
goToLast: this.dataset.goToLast === 'true',
|
goToLast: this.dataset.goToLast === 'true',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex absolute lg:text-base"
|
class="flex absolute lg:text-base"
|
||||||
:style="computedStyle"
|
:style="computedStyle"
|
||||||
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
|
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
:src="initials.url"
|
:src="initials.url"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'file'"
|
v-else-if="field.type === 'file' || field.type === 'payment'"
|
||||||
class="px-0.5 flex flex-col justify-center"
|
class="px-0.5 flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -231,7 +231,8 @@ export default {
|
|||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
multiple: 'Multiple Select',
|
multiple: 'Multiple Select',
|
||||||
phone: 'Phone'
|
phone: 'Phone',
|
||||||
|
payment: 'Payment'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -247,7 +248,8 @@ export default {
|
|||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck,
|
||||||
|
payment: IconCreditCard
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -271,9 +273,15 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
locale () {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions()?.locale
|
||||||
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue))
|
return this.formatDate(
|
||||||
|
new Date(this.modelValue),
|
||||||
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -281,6 +289,8 @@ export default {
|
|||||||
attachments () {
|
attachments () {
|
||||||
if (this.field.type === 'file') {
|
if (this.field.type === 'file') {
|
||||||
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
||||||
|
} else if (this.field.type === 'payment') {
|
||||||
|
return [this.attachmentsIndex[this.modelValue]].filter(Boolean)
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -311,6 +321,36 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatDate (date, format) {
|
||||||
|
const monthFormats = {
|
||||||
|
M: 'numeric',
|
||||||
|
MM: '2-digit',
|
||||||
|
MMM: 'short',
|
||||||
|
MMMM: 'long'
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayFormats = {
|
||||||
|
D: 'numeric',
|
||||||
|
DD: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearFormats = {
|
||||||
|
YYYY: 'numeric',
|
||||||
|
YY: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], {
|
||||||
|
day: dayFormats[format.match(/D+/)],
|
||||||
|
month: monthFormats[format.match(/M+/)],
|
||||||
|
year: yearFormats[format.match(/Y+/)],
|
||||||
|
timeZone: 'UTC'
|
||||||
|
}).formatToParts(date)
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||||
|
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||||
|
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||||
|
},
|
||||||
updateMultipleSelectValue (value) {
|
updateMultipleSelectValue (value) {
|
||||||
if (this.modelValue?.includes(value)) {
|
if (this.modelValue?.includes(value)) {
|
||||||
const newValue = [...this.modelValue]
|
const newValue = [...this.modelValue]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
:key="areaIndex"
|
:key="areaIndex"
|
||||||
>
|
>
|
||||||
<Teleport
|
<Teleport
|
||||||
|
v-if="findPageElementForArea(area)"
|
||||||
:to="findPageElementForArea(area)"
|
:to="findPageElementForArea(area)"
|
||||||
>
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-if="canSendEmail && !isDemo"
|
v-if="canSendEmail && !isDemo && withSendCopyButton"
|
||||||
class="white-button !h-auto flex items-center space-x-1 w-full"
|
class="white-button !h-auto flex items-center space-x-1 w-full"
|
||||||
:disabled="isSendingCopy"
|
:disabled="isSendingCopy"
|
||||||
@click.prevent="sendCopyToEmail"
|
@click.prevent="sendCopyToEmail"
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!isWebView"
|
v-if="!isWebView && withDownloadButton"
|
||||||
class="base-button flex items-center space-x-1 w-full"
|
class="base-button flex items-center space-x-1 w-full"
|
||||||
:disabled="isDownloading"
|
:disabled="isDownloading"
|
||||||
@click.prevent="download"
|
@click.prevent="download"
|
||||||
@@ -115,6 +115,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withDownloadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withSendCopyButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -266,8 +266,22 @@
|
|||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
@submit="submitStep"
|
@submit="submitStep"
|
||||||
/>
|
/>
|
||||||
|
<PaymentStep
|
||||||
|
v-else-if="currentField.type === 'payment'"
|
||||||
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:field="currentField"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
@attached="attachments.push($event)"
|
||||||
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
|
@submit="submitStep"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 md:mt-8">
|
<div
|
||||||
|
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
|
||||||
|
class="mt-6 md:mt-8"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
ref="submitButton"
|
ref="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -303,6 +317,8 @@
|
|||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
:attribution="attribution"
|
:attribution="attribution"
|
||||||
:completed-button="completedButton"
|
:completed-button="completedButton"
|
||||||
|
:with-send-copy-button="withSendCopyButton"
|
||||||
|
:with-download-button="withDownloadButton"
|
||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail && !!submitter.email"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -331,6 +347,7 @@ import InitialsStep from './initials_step'
|
|||||||
import AttachmentStep from './attachment_step'
|
import AttachmentStep from './attachment_step'
|
||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
|
import PaymentStep from './payment_step'
|
||||||
import TextStep from './text_step'
|
import TextStep from './text_step'
|
||||||
import DateStep from './date_step'
|
import DateStep from './date_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
@@ -351,6 +368,7 @@ export default {
|
|||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
TextStep,
|
TextStep,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
|
PaymentStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
@@ -402,11 +420,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
authenticityToken: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -442,6 +455,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
withSendCopyButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withDownloadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
completedButton: {
|
completedButton: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -463,6 +486,12 @@ export default {
|
|||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep]
|
||||||
},
|
},
|
||||||
|
queryParams () {
|
||||||
|
return new URLSearchParams(window.location.search)
|
||||||
|
},
|
||||||
|
authenticityToken () {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
|
},
|
||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
@@ -525,7 +554,13 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.goToLast) {
|
if (this.queryParams.get('field_uuid')) {
|
||||||
|
const stepIndex = this.stepFields.findIndex((fields) => {
|
||||||
|
return fields.some((f) => f.uuid === this.queryParams.get('field_uuid'))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.currentStep = Math.max(stepIndex, 0)
|
||||||
|
} else if (this.goToLast) {
|
||||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
||||||
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||||
|
|
||||||
@@ -570,7 +605,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
t,
|
t,
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
const { queryParams } = this
|
||||||
|
|
||||||
if (queryParams.has('t')) {
|
if (queryParams.has('t')) {
|
||||||
const t = queryParams.get('t')
|
const t = queryParams.get('t')
|
||||||
@@ -594,7 +629,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeTrackSmsClick () {
|
maybeTrackSmsClick () {
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
const { queryParams } = this
|
||||||
|
|
||||||
if (queryParams.has('c')) {
|
if (queryParams.has('c')) {
|
||||||
const c = queryParams.get('c')
|
const c = queryParams.get('c')
|
||||||
@@ -667,7 +702,7 @@ export default {
|
|||||||
async submitStep () {
|
async submitStep () {
|
||||||
this.isSubmitting = true
|
this.isSubmitting = true
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials'].includes(this.currentField.type)
|
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
@@ -684,7 +719,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.saveStep(formData).then(async (response) => {
|
await this.saveStep(formData).then(async (response) => {
|
||||||
if (response.status === 422) {
|
if (response.status === 422 || response.status === 500) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
alert(data.error || 'Value is invalid')
|
alert(data.error || 'Value is invalid')
|
||||||
@@ -714,7 +749,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error submitting form:', error)
|
console.error(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ const en = {
|
|||||||
type: 'Type',
|
type: 'Type',
|
||||||
type_text: 'Type text',
|
type_text: 'Type text',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
email_has_been_sent: 'Email has been sent'
|
email_has_been_sent: 'Email has been sent',
|
||||||
|
processing: 'Processing',
|
||||||
|
pay_with_strip: 'Pay with Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
@@ -81,7 +83,9 @@ const es = {
|
|||||||
draw: 'Dibujar',
|
draw: 'Dibujar',
|
||||||
type: 'Escribir',
|
type: 'Escribir',
|
||||||
type_text: 'Escribir texto',
|
type_text: 'Escribir texto',
|
||||||
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
email_has_been_sent: 'El correo electrónico ha sido enviado',
|
||||||
|
processing: 'Procesando',
|
||||||
|
pay_with_strip: 'Pagar con Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
@@ -124,7 +128,9 @@ const it = {
|
|||||||
type: 'Inserisci',
|
type: 'Inserisci',
|
||||||
type_text: 'Inserisci testo',
|
type_text: 'Inserisci testo',
|
||||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||||
email_has_been_sent: "L'email è stata inviata"
|
email_has_been_sent: "L'email è stata inviata",
|
||||||
|
processing: 'Elaborazione',
|
||||||
|
pay_with_strip: 'Paga con Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
@@ -167,7 +173,9 @@ const de = {
|
|||||||
type: 'Eingeben',
|
type: 'Eingeben',
|
||||||
type_text: 'Text eingeben',
|
type_text: 'Text eingeben',
|
||||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||||
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
email_has_been_sent: 'Die E-Mail wurde gesendet',
|
||||||
|
processing: 'Verarbeitung',
|
||||||
|
pay_with_strip: 'Mit Stripe bezahlen'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
@@ -210,7 +218,9 @@ const fr = {
|
|||||||
type: 'Saisir',
|
type: 'Saisir',
|
||||||
type_text: 'Saisir du texte',
|
type_text: 'Saisir du texte',
|
||||||
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||||
email_has_been_sent: "L'email a été envoyé"
|
email_has_been_sent: "L'email a été envoyé",
|
||||||
|
processing: 'Traitement',
|
||||||
|
pay_with_strip: 'Paiement avec Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
@@ -253,7 +263,9 @@ const pl = {
|
|||||||
type: 'Wprowadź',
|
type: 'Wprowadź',
|
||||||
type_text: 'Wprowadź tekst',
|
type_text: 'Wprowadź tekst',
|
||||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
email_has_been_sent: 'E-mail został wysłany'
|
email_has_been_sent: 'E-mail został wysłany',
|
||||||
|
processing: 'Przetwarzanie',
|
||||||
|
pay_with_strip: 'Płatność za pomocą Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
@@ -296,7 +308,9 @@ const uk = {
|
|||||||
type: 'Текст',
|
type: 'Текст',
|
||||||
type_text: 'Введіть текст',
|
type_text: 'Введіть текст',
|
||||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений'
|
email_has_been_sent: 'Електронний лист був відправлений',
|
||||||
|
processing: 'Обробка',
|
||||||
|
pay_with_strip: 'Сплатити за допомогою Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
@@ -339,7 +353,9 @@ const cs = {
|
|||||||
type: 'Zadat',
|
type: 'Zadat',
|
||||||
type_text: 'Zadat text',
|
type_text: 'Zadat text',
|
||||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
email_has_been_sent: 'E-mail byl odeslán'
|
email_has_been_sent: 'E-mail byl odeslán',
|
||||||
|
processing: 'Zpracování',
|
||||||
|
pay_with_strip: 'Zaplacení přes Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
@@ -382,7 +398,9 @@ const pt = {
|
|||||||
type: 'Inserir',
|
type: 'Inserir',
|
||||||
type_text: 'Inserir texto',
|
type_text: 'Inserir texto',
|
||||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||||
email_has_been_sent: 'Email enviado'
|
email_has_been_sent: 'Email enviado',
|
||||||
|
processing: 'Processamento',
|
||||||
|
pay_with_strip: 'Pagar com Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt }
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt }
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="!modelValue && !sessionId"
|
||||||
|
:for="field.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ field.name || defaultName }}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="modelValue"
|
||||||
|
hidden
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
class="hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="modelValue && !sessionId"
|
||||||
|
class=" text-2xl mb-2"
|
||||||
|
>
|
||||||
|
Already paid
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button
|
||||||
|
v-if="sessionId"
|
||||||
|
disabled
|
||||||
|
class="base-button w-full"
|
||||||
|
>
|
||||||
|
<IconLoader
|
||||||
|
width="22"
|
||||||
|
class="animate-spin"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ t('processing') }}...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
:id="field.uuid"
|
||||||
|
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
|
||||||
|
:class="{ disabled: isCreatingCheckout }"
|
||||||
|
:disabled="isCreatingCheckout"
|
||||||
|
@click.prevent="startCheckout"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isCreatingCheckout"
|
||||||
|
width="22"
|
||||||
|
class="animate-spin"
|
||||||
|
/>
|
||||||
|
<IconBrandStripe
|
||||||
|
v-else
|
||||||
|
width="22"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ t('pay_with_strip') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconBrandStripe, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PaymentStep',
|
||||||
|
components: {
|
||||||
|
IconBrandStripe,
|
||||||
|
IconInnerShadowTop,
|
||||||
|
IconLoader
|
||||||
|
},
|
||||||
|
inject: ['baseUrl', 't'],
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitterSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['focus', 'submit', 'update:model-value', 'attached'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isCreatingCheckout: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
queryParams () {
|
||||||
|
return new URLSearchParams(window.location.search)
|
||||||
|
},
|
||||||
|
sessionId () {
|
||||||
|
return this.queryParams.get('stripe_session_id')
|
||||||
|
},
|
||||||
|
defaultName () {
|
||||||
|
const { price, currency } = this.field.preferences || {}
|
||||||
|
|
||||||
|
const formattedPrice = new Intl.NumberFormat([], {
|
||||||
|
style: 'currency',
|
||||||
|
currency
|
||||||
|
}).format(price)
|
||||||
|
|
||||||
|
return `Pay ${formattedPrice}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.sessionId) {
|
||||||
|
this.$emit('submit')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async submit () {
|
||||||
|
if (this.sessionId) {
|
||||||
|
return fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments/' + this.sessionId), {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
submitter_slug: this.submitterSlug
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
alert(data.error || 'Unexpected error')
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await resp.json()
|
||||||
|
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
this.$emit('attached', attachment)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startCheckout () {
|
||||||
|
this.isCreatingCheckout = true
|
||||||
|
|
||||||
|
fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments'), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
submitter_slug: this.submitterSlug,
|
||||||
|
field_uuid: this.field.uuid
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
alert(data.message || 'Unexpected error')
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.message))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await resp.json()
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = url
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.isCreatingCheckout = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -218,30 +218,30 @@ export default {
|
|||||||
},
|
},
|
||||||
borderColors () {
|
borderColors () {
|
||||||
return [
|
return [
|
||||||
'border-red-500/50',
|
'border-red-500/80',
|
||||||
'border-sky-500/50',
|
'border-sky-500/80',
|
||||||
'border-emerald-500/50',
|
'border-emerald-500/80',
|
||||||
'border-yellow-300/50',
|
'border-yellow-300/80',
|
||||||
'border-purple-600/50',
|
'border-purple-600/80',
|
||||||
'border-pink-500/50',
|
'border-pink-500/80',
|
||||||
'border-cyan-500/50',
|
'border-cyan-500/80',
|
||||||
'border-orange-500/50',
|
'border-orange-500/80',
|
||||||
'border-lime-500/50',
|
'border-lime-500/80',
|
||||||
'border-indigo-500/50'
|
'border-indigo-500/80'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
bgColors () {
|
bgColors () {
|
||||||
return [
|
return [
|
||||||
'bg-red-100/50',
|
'bg-red-100/80',
|
||||||
'bg-sky-100/50',
|
'bg-sky-100/80',
|
||||||
'bg-emerald-100/50',
|
'bg-emerald-100/80',
|
||||||
'bg-yellow-100/50',
|
'bg-yellow-100/80',
|
||||||
'bg-purple-100/50',
|
'bg-purple-100/80',
|
||||||
'bg-pink-100/50',
|
'bg-pink-100/80',
|
||||||
'bg-cyan-100/50',
|
'bg-cyan-100/80',
|
||||||
'bg-orange-100/50',
|
'bg-orange-100/80',
|
||||||
'bg-lime-100/50',
|
'bg-lime-100/80',
|
||||||
'bg-indigo-100/50'
|
'bg-indigo-100/80'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
|
|||||||
@@ -244,6 +244,7 @@
|
|||||||
:fields="template.fields"
|
:fields="template.fields"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
|
:default-submitters="defaultSubmitters"
|
||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
@@ -301,6 +302,7 @@ export default {
|
|||||||
baseFetch: this.baseFetch,
|
baseFetch: this.baseFetch,
|
||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
withPhone: this.withPhone,
|
withPhone: this.withPhone,
|
||||||
|
withPayment: this.withPayment,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -349,6 +351,13 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
onUpload: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -369,6 +378,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withPayment: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -436,6 +450,12 @@ export default {
|
|||||||
window.addEventListener('keydown', this.onKeyDown)
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (document.location.search?.includes('stripe_connect_success')) {
|
||||||
|
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
document.removeEventListener('keyup', this.onKeyUp)
|
document.removeEventListener('keyup', this.onKeyUp)
|
||||||
@@ -461,6 +481,12 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.drawField = field
|
this.drawField = field
|
||||||
this.drawOption = null
|
this.drawOption = null
|
||||||
},
|
},
|
||||||
@@ -637,6 +663,12 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fieldArea = {
|
const fieldArea = {
|
||||||
x: (area.x - 6) / area.maskW,
|
x: (area.x - 6) / area.maskW,
|
||||||
y: area.y / area.maskH,
|
y: area.y / area.maskH,
|
||||||
@@ -707,6 +739,10 @@ export default {
|
|||||||
this.scrollIntoDocument(schema[0])
|
this.scrollIntoDocument(schema[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.onUpload) {
|
||||||
|
this.onUpload(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
updateName (value) {
|
updateName (value) {
|
||||||
@@ -740,6 +776,10 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.onUpload) {
|
||||||
|
this.onUpload(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
moveDocument (item, direction) {
|
moveDocument (item, direction) {
|
||||||
|
|||||||
@@ -65,8 +65,12 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<PaymentSettings
|
||||||
|
v-if="field.type === 'payment'"
|
||||||
|
:field="field"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="!defaultField"
|
v-else-if="!defaultField"
|
||||||
class="dropdown dropdown-end"
|
class="dropdown dropdown-end"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
@@ -107,6 +111,33 @@
|
|||||||
Default value
|
Default value
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'date'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="field.preferences.format"
|
||||||
|
placeholder="Format"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
|
||||||
|
@change="save"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in dateFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
>
|
||||||
|
{{ formatDate(new Date(), format) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
Format
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'phone'"
|
v-if="field.type != 'phone'"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -261,6 +292,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
|
import PaymentSettings from './payment_settings'
|
||||||
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
@@ -270,6 +302,7 @@ export default {
|
|||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconShape,
|
IconShape,
|
||||||
|
PaymentSettings,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
@@ -295,23 +328,85 @@ export default {
|
|||||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isNameFocus: false
|
isNameFocus: false,
|
||||||
|
showPaymentModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
dateFormats () {
|
||||||
|
return [
|
||||||
|
'MM/DD/YYYY',
|
||||||
|
'DD/MM/YYYY',
|
||||||
|
'YYYY-MM-DD',
|
||||||
|
'DD-MM-YYYY',
|
||||||
|
'DD.MM.YYYY',
|
||||||
|
'MMM D, YYYY',
|
||||||
|
'MMMM D, YYYY',
|
||||||
|
'D MMM YYYY',
|
||||||
|
'D MMMM YYYY'
|
||||||
|
]
|
||||||
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
if (this.field.type === 'payment' && this.field.preferences?.price) {
|
||||||
|
const { price, currency } = this.field.preferences || {}
|
||||||
|
|
||||||
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field'
|
const formattedPrice = new Intl.NumberFormat([], {
|
||||||
|
style: 'currency',
|
||||||
|
currency
|
||||||
|
}).format(price)
|
||||||
|
|
||||||
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
return `${this.fieldNames[this.field.type]} ${formattedPrice}`
|
||||||
|
} else {
|
||||||
|
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
||||||
|
|
||||||
|
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field'
|
||||||
|
|
||||||
|
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
areas () {
|
areas () {
|
||||||
return this.field.areas || []
|
return this.field.areas || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
this.field.preferences ||= {}
|
||||||
|
|
||||||
|
if (this.field.type === 'date') {
|
||||||
|
this.field.preferences.format ||=
|
||||||
|
(Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatDate (date, format) {
|
||||||
|
const monthFormats = {
|
||||||
|
M: 'numeric',
|
||||||
|
MM: '2-digit',
|
||||||
|
MMM: 'short',
|
||||||
|
MMMM: 'long'
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayFormats = {
|
||||||
|
D: 'numeric',
|
||||||
|
DD: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearFormats = {
|
||||||
|
YYYY: 'numeric',
|
||||||
|
YY: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], {
|
||||||
|
day: dayFormats[format.match(/D+/)],
|
||||||
|
month: monthFormats[format.match(/M+/)],
|
||||||
|
year: yearFormats[format.match(/Y+/)]
|
||||||
|
}).formatToParts(date)
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||||
|
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||||
|
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||||
|
},
|
||||||
copyToAllPages (field) {
|
copyToAllPages (field) {
|
||||||
const areaString = JSON.stringify(field.areas[0])
|
const areaString = JSON.stringify(field.areas[0])
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="withPhone || type !== 'phone'">
|
<li v-if="withPhone || withPayment || !['phone', 'payment'].includes(type)">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -46,11 +46,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
inject: ['withPhone'],
|
inject: ['withPhone', 'withPayment'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -92,7 +92,8 @@ export default {
|
|||||||
multiple: 'Multiple',
|
multiple: 'Multiple',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cells',
|
cells: 'Cells',
|
||||||
phone: 'Phone'
|
phone: 'Phone',
|
||||||
|
payment: 'Payment'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -108,7 +109,8 @@ export default {
|
|||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck,
|
||||||
|
payment: IconCreditCard
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
class="w-full rounded-lg"
|
class="w-full rounded-lg"
|
||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
:editable="editable"
|
:editable="editable && !defaultSubmitters.length"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@name-change="save"
|
@name-change="save"
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="withPhone || type != 'phone'"
|
v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="type == 'phone'"
|
||||||
class="tooltip tooltip-bottom-end flex"
|
class="tooltip tooltip-bottom-end flex"
|
||||||
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||||
>
|
>
|
||||||
@@ -152,7 +152,7 @@ export default {
|
|||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -168,6 +168,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defaultSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -256,6 +261,12 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.fields.push(field)
|
this.fields.push(field)
|
||||||
|
|
||||||
if (['signature', 'initials', 'cells'].includes(type)) {
|
if (['signature', 'initials', 'cells'].includes(type)) {
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
:class="{ 'dropdown-open': (!field.preferences?.price || !isConnected) && !isLoading }"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
title="Settings"
|
||||||
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
>
|
||||||
|
<IconSettings
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.prevent.stop
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="field.preferences.currency"
|
||||||
|
placeholder="Price"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
|
||||||
|
@change="save"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="currency in currencies"
|
||||||
|
:key="currency"
|
||||||
|
:value="currency"
|
||||||
|
>
|
||||||
|
{{ currency }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
Currency
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="field.preferences.price"
|
||||||
|
type="number"
|
||||||
|
placeholder="Price"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.preferences.price"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
Price
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!isConnected || isOauthSuccess"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isConnected && isOauthSuccess"
|
||||||
|
class="text-sm text-center"
|
||||||
|
>
|
||||||
|
<IconCircleCheck
|
||||||
|
class="inline text-green-600 w-4 h-4"
|
||||||
|
/>
|
||||||
|
Stripe Connected
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
v-if="!isConnected"
|
||||||
|
data-turbo="false"
|
||||||
|
action="/auth/stripe_connect"
|
||||||
|
accept-charset="UTF-8"
|
||||||
|
target="_blank"
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="state"
|
||||||
|
:value="oauthState"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="redirect_uri"
|
||||||
|
:value="redirectUri"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="scope"
|
||||||
|
value="read_write"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="authenticity_token"
|
||||||
|
:value="authenticityToken"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="btn bg-[#7B73FF] hover:bg-[#0A2540] btn-sm text-white w-full"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="isLoading"
|
||||||
|
class="flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
class="w-4 h-4 animate-spin inline"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Connect Stripe
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<IconBrandStripe
|
||||||
|
class="w-4 h-4 inline"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Connect Stripe
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconSettings, IconCircleCheck, IconBrandStripe, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const isConnected = ref(false)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PaymentSettings',
|
||||||
|
components: {
|
||||||
|
IconSettings,
|
||||||
|
IconCircleCheck,
|
||||||
|
IconInnerShadowTop,
|
||||||
|
IconBrandStripe
|
||||||
|
},
|
||||||
|
inject: ['backgroundColor', 'save'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isConnected: () => isConnected.value,
|
||||||
|
isOauthSuccess () {
|
||||||
|
return document.location.search?.includes('stripe_connect_success')
|
||||||
|
},
|
||||||
|
redirectUri () {
|
||||||
|
return document.location.origin + '/auth/stripe_connect/callback'
|
||||||
|
},
|
||||||
|
currencies () {
|
||||||
|
return ['USD', 'EUR', 'GBP']
|
||||||
|
},
|
||||||
|
authenticityToken () {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
|
},
|
||||||
|
oauthState () {
|
||||||
|
const params = new URLSearchParams('')
|
||||||
|
|
||||||
|
params.set('redir', document.location.href)
|
||||||
|
|
||||||
|
return params.toString()
|
||||||
|
},
|
||||||
|
defaultCurrency () {
|
||||||
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
if (userTimezone.startsWith('Europe')) {
|
||||||
|
return 'EUR'
|
||||||
|
} else if (userTimezone.includes('London') || userTimezone.includes('Belfast')) {
|
||||||
|
return 'GBP'
|
||||||
|
} else {
|
||||||
|
return 'USD'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.field.preferences ||= {}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.field.preferences.currency ||= this.defaultCurrency
|
||||||
|
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.checkStatus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkStatus () {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
fetch('/api/stripe_connect').then(async (resp) => {
|
||||||
|
const { status } = await resp.json()
|
||||||
|
|
||||||
|
if (status === 'connected') {
|
||||||
|
isConnected.value = true
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -25,10 +25,12 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
DEFAULT_INVITATION_SUBJECT
|
DEFAULT_INVITATION_SUBJECT
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(to: @submitter.friendly_name,
|
mail(
|
||||||
from: from_address_for_submitter(submitter),
|
to: @submitter.friendly_name,
|
||||||
subject:,
|
from: from_address_for_submitter(submitter),
|
||||||
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name)
|
subject:,
|
||||||
|
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user, bcc: nil)
|
def completed_email(submitter, user, bcc: nil)
|
||||||
@@ -53,7 +55,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
to: user.friendly_name,
|
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name,
|
||||||
bcc:,
|
bcc:,
|
||||||
subject:)
|
subject:)
|
||||||
end
|
end
|
||||||
@@ -98,7 +100,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
||||||
|
|
||||||
file_fields = submitter.submission.template_fields.select { |e| e['type'] == 'file' }
|
file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) }
|
||||||
|
|
||||||
if file_fields.pluck('submitter_uuid').uniq.size == 1
|
if file_fields.pluck('submitter_uuid').uniq.size == 1
|
||||||
storage_attachments =
|
storage_attachments =
|
||||||
@@ -125,6 +127,11 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def from_address_for_submitter(submitter)
|
def from_address_for_submitter(submitter)
|
||||||
submitter.submission.created_by_user&.friendly_name || submitter.submission.template.author.friendly_name
|
if submitter.submission.source.in?(%w[api embed]) &&
|
||||||
|
(from_email = AccountConfig.find_by(account: submitter.account, key: 'integration_from_email')&.value.presence)
|
||||||
|
from_email
|
||||||
|
else
|
||||||
|
(submitter.submission.created_by_user || submitter.submission.template.author).friendly_name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -59,10 +59,6 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||||
|
|
||||||
if Docuseal.multitenant?
|
|
||||||
devise :registerable, :omniauthable, omniauth_providers: %i[google_oauth2 microsoft_office365 github]
|
|
||||||
end
|
|
||||||
|
|
||||||
attribute :role, :string, default: ADMIN_ROLE
|
attribute :role, :string, default: ADMIN_ROLE
|
||||||
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
|
|||||||
Whitespace-only changes.
@@ -33,5 +33,6 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= render 'extra_links' %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<%= render 'email_stats' %>
|
<%= render 'email_stats' %>
|
||||||
<%= render 'bcc_form', config: @bcc_config %>
|
<%= render 'bcc_form', config: @bcc_config %>
|
||||||
<div class="flex justify-between items-end mb-4 mt-8">
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
<h2 class="text-3xl font-bold">Signature Email Reminders</h2>
|
<h2 class="text-3xl font-bold">Sign Request Email Reminders</h2>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'reminder_banner' %>
|
<%= render 'reminder_banner' %>
|
||||||
<%= render 'reminder_form', config: @reminder_config %>
|
<%= render 'reminder_form', config: @reminder_config %>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
||||||
<% if field['type'].in?(['signature', 'image', 'initials']) %>
|
<% if field['type'].in?(['signature', 'image', 'initials']) %>
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' %>
|
<% elsif field['type'].in?(['file', 'payment']) %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="px-0.5 flex flex-col justify-center">
|
<div class="px-0.5 flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="flex items-center px-0.5">
|
<div class="flex items-center px-0.5">
|
||||||
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
|
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'image' %>
|
<% elsif field['type'] == 'image' %>
|
||||||
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' %>
|
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
<a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>">
|
<a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>">
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
<% elsif field['type'] == 'checkbox' %>
|
<% elsif field['type'] == 'checkbox' %>
|
||||||
<%= svg_icon('check', class: 'w-6 h-6') %>
|
<%= svg_icon('check', class: 'w-6 h-6') %>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<%= l(Date.parse(value), locale: @submission.template.account.locale, format: :long) %>
|
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.template.account.locale) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= Array.wrap(value).join(', ') %>
|
<%= Array.wrap(value).join(', ') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
||||||
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
||||||
<% configs = Submitters::FormConfigs.call(submitter) %>
|
<% configs = Submitters::FormConfigs.call(submitter) %>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
|
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<% if @email_config || @body.present? %>
|
<% if @email_config || @body.present? %>
|
||||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@body.presence || @email_config.value['body'], submitter: @submitter)))) %>
|
<% body = (@body.presence || @email_config.value['body']).to_s %>
|
||||||
<% if !(@body.presence || @email_config.value['body']).include?(ReplaceEmailVariables::SUBMITTER_LINK) %>
|
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(body, submitter: @submitter)))) %>
|
||||||
|
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) %>
|
||||||
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="pb-6 pt-1 space-y-1">
|
<div class="pb-6 pt-1 space-y-1">
|
||||||
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
||||||
<%= svg_icon('user', class: 'w-4 h-4') %>
|
<%= svg_icon('user', class: 'w-4 h-4') %>
|
||||||
<span><%= template.author.full_name.presence || template.author.email %></span>
|
<span><%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %></span>
|
||||||
</p>
|
</p>
|
||||||
<p class="flex text-xs text-base-content/60">
|
<p class="flex text-xs text-base-content/60">
|
||||||
<span class="flex items-center space-x-1 w-1/2">
|
<span class="flex items-center space-x-1 w-1/2">
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Devise.otp_allowed_drift = 60.seconds
|
|||||||
#
|
#
|
||||||
# Use this hook to configure devise mailer, warden hooks and so forth.
|
# Use this hook to configure devise mailer, warden hooks and so forth.
|
||||||
# Many of these configuration options can be set straight in your model.
|
# Many of these configuration options can be set straight in your model.
|
||||||
# rubocop:disable Metrics/BlockLength
|
|
||||||
Devise.setup do |config|
|
Devise.setup do |config|
|
||||||
config.warden do |manager|
|
config.warden do |manager|
|
||||||
manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable)
|
manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable)
|
||||||
@@ -274,17 +273,6 @@ Devise.setup do |config|
|
|||||||
# The default HTTP method used to sign out a resource. Default is :delete.
|
# The default HTTP method used to sign out a resource. Default is :delete.
|
||||||
config.sign_out_via = :delete
|
config.sign_out_via = :delete
|
||||||
|
|
||||||
# ==> OmniAuth
|
|
||||||
# Add a new OmniAuth provider. Check the wiki for more information on setting
|
|
||||||
# up on your models and hooks.
|
|
||||||
config.omniauth :google_oauth2, ENV.fetch('GOOGLE_CLIENT_ID', nil), ENV.fetch('GOOGLE_CLIENT_SECRET', nil), {}
|
|
||||||
config.omniauth :microsoft_office365, ENV.fetch('OFFICE365_CLIENT_ID', nil),
|
|
||||||
ENV.fetch('OFFICE365_CLIENT_SECRET', nil), {}
|
|
||||||
|
|
||||||
if ENV['GITHUB_CLIENT_ID']
|
|
||||||
config.omniauth :github, ENV.fetch('GITHUB_CLIENT_ID', nil), ENV.fetch('GITHUB_CLIENT_SECRET', nil), {}
|
|
||||||
end
|
|
||||||
|
|
||||||
# ==> Warden configuration
|
# ==> Warden configuration
|
||||||
# If you want to use other strategies, that are not supported by Devise, or
|
# If you want to use other strategies, that are not supported by Devise, or
|
||||||
# change the failure app, you can configure them inside the config.warden block.
|
# change the failure app, you can configure them inside the config.warden block.
|
||||||
@@ -322,5 +310,6 @@ Devise.setup do |config|
|
|||||||
# When set to false, does not sign a user in automatically after their password is
|
# When set to false, does not sign a user in automatically after their password is
|
||||||
# changed. Defaults to true, so a user is signed in automatically after changing a password.
|
# changed. Defaults to true, so a user is signed in automatically after changing a password.
|
||||||
# config.sign_in_after_change_password = true
|
# config.sign_in_after_change_password = true
|
||||||
|
|
||||||
|
ActiveSupport.run_load_hooks(:devise_config, config)
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/BlockLength
|
|
||||||
+2
-2
@@ -10,7 +10,7 @@ Rails.application.routes.draw do
|
|||||||
path: '/', only: %i[sessions passwords omniauth_callbacks],
|
path: '/', only: %i[sessions passwords omniauth_callbacks],
|
||||||
controllers: begin
|
controllers: begin
|
||||||
options = { sessions: 'sessions', passwords: 'passwords' }
|
options = { sessions: 'sessions', passwords: 'passwords' }
|
||||||
options[:omniauth_callbacks] = 'omniauth_callbacks' if Docuseal.multitenant?
|
options[:omniauth_callbacks] = 'omniauth_callbacks' if User.devise_modules.include?(:omniauthable)
|
||||||
options
|
options
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ Rails.application.routes.draw do
|
|||||||
resources :template_folders_autocomplete, only: %i[index]
|
resources :template_folders_autocomplete, only: %i[index]
|
||||||
resources :submitter_email_clicks, only: %i[create]
|
resources :submitter_email_clicks, only: %i[create]
|
||||||
resources :submitter_form_views, only: %i[create]
|
resources :submitter_form_views, only: %i[create]
|
||||||
resources :submitters, only: %i[index show]
|
resources :submitters, only: %i[index show update]
|
||||||
resources :submissions, only: %i[index show create destroy] do
|
resources :submissions, only: %i[index show create destroy] do
|
||||||
collection do
|
collection do
|
||||||
resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails
|
resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PdfProcessor < HexaPDF::Content::Processor
|
||||||
|
attr_reader :result, :contents
|
||||||
|
attr_accessor :handler
|
||||||
|
|
||||||
|
def initialize(page)
|
||||||
|
super
|
||||||
|
|
||||||
|
@contents = ''.b
|
||||||
|
@result = []
|
||||||
|
|
||||||
|
@serializer = HexaPDF::Serializer.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def process(operator, operands = [])
|
||||||
|
super
|
||||||
|
|
||||||
|
contents << @operators[operator].serialize(
|
||||||
|
@serializer,
|
||||||
|
*handler.call(self, operator, operands)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.call(data, pdf_handler, result_handler, acc = {})
|
||||||
|
doc = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
|
doc.pages.each do |page|
|
||||||
|
processor = PdfProcessor.new(page)
|
||||||
|
processor.handler = pdf_handler
|
||||||
|
|
||||||
|
page.process_contents(processor)
|
||||||
|
|
||||||
|
page.contents = processor.contents
|
||||||
|
|
||||||
|
processor.result.each do |item|
|
||||||
|
result_handler.call(item, page, acc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
new_io = StringIO.new
|
||||||
|
|
||||||
|
doc.write(new_io)
|
||||||
|
|
||||||
|
[new_io.tap(&:rewind).read, acc]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,24 +1,31 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ReplaceEmailVariables
|
module ReplaceEmailVariables
|
||||||
TEMAPLTE_NAME = '{{template.name}}'
|
TEMPLATE_NAME = '{{template.name}}'
|
||||||
|
TEMPLATE_ID = '{{template.id}}'
|
||||||
SUBMITTER_LINK = '{{submitter.link}}'
|
SUBMITTER_LINK = '{{submitter.link}}'
|
||||||
ACCOUNT_NAME = '{{account.name}}'
|
ACCOUNT_NAME = '{{account.name}}'
|
||||||
SUBMITTER_EMAIL = '{{submitter.email}}'
|
SUBMITTER_EMAIL = '{{submitter.email}}'
|
||||||
SUBMITTER_NAME = '{{submitter.name}}'
|
SUBMITTER_NAME = '{{submitter.name}}'
|
||||||
|
SUBMITTER_ID = '{{submitter.id}}'
|
||||||
SUBMISSION_LINK = '{{submission.link}}'
|
SUBMISSION_LINK = '{{submission.link}}'
|
||||||
|
SUBMISSION_ID = '{{submission.id}}'
|
||||||
SUBMISSION_SUBMITTERS = '{{submission.submitters}}'
|
SUBMISSION_SUBMITTERS = '{{submission.submitters}}'
|
||||||
DOCUMENTS_LINKS = '{{documents.links}}'
|
DOCUMENTS_LINKS = '{{documents.links}}'
|
||||||
DOCUMENTS_LINK = '{{documents.link}}'
|
DOCUMENTS_LINK = '{{documents.link}}'
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
def call(text, submitter:, tracking_event_type: 'click_email')
|
def call(text, submitter:, tracking_event_type: 'click_email')
|
||||||
submitter_link = build_submitter_link(submitter, tracking_event_type)
|
submitter_link = build_submitter_link(submitter, tracking_event_type)
|
||||||
|
|
||||||
submission_link = build_submission_link(submitter.submission) if submitter.submission
|
submission_link = build_submission_link(submitter.submission) if submitter.submission
|
||||||
|
|
||||||
text = text.gsub(TEMAPLTE_NAME, submitter.template.name) if submitter.template
|
text = text.gsub(TEMPLATE_NAME, submitter.template.name) if submitter.template
|
||||||
|
text = text.gsub(TEMPLATE_ID, submitter.template.id.to_s) if submitter.template
|
||||||
|
text = text.gsub(SUBMITTER_ID, submitter.id.to_s)
|
||||||
|
text = text.gsub(SUBMISSION_ID, submitter.submission.id.to_s) if submitter.submission
|
||||||
text = text.gsub(SUBMITTER_EMAIL, submitter.email) if submitter.email
|
text = text.gsub(SUBMITTER_EMAIL, submitter.email) if submitter.email
|
||||||
text = text.gsub(SUBMITTER_NAME, submitter.name || submitter.email || submitter.phone)
|
text = text.gsub(SUBMITTER_NAME, submitter.name || submitter.email || submitter.phone)
|
||||||
text = text.gsub(SUBMITTER_LINK, submitter_link)
|
text = text.gsub(SUBMITTER_LINK, submitter_link)
|
||||||
@@ -33,6 +40,7 @@ module ReplaceEmailVariables
|
|||||||
|
|
||||||
text
|
text
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
def build_documents_links_text(submitter)
|
def build_documents_links_text(submitter)
|
||||||
Rails.application.routes.url_helpers.submissions_preview_url(
|
Rails.application.routes.url_helpers.submissions_preview_url(
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_set_template_fields(submission, submitters_attrs)
|
def maybe_set_template_fields(submission, submitters_attrs, submitter_uuid: nil)
|
||||||
template_fields = submission.template.fields.deep_dup
|
template_fields = (submission.template_fields || submission.template.fields).deep_dup
|
||||||
|
|
||||||
submitters_attrs.each_with_index do |submitter_attrs, index|
|
submitters_attrs.each_with_index do |submitter_attrs, index|
|
||||||
submitter_uuid = find_submitter_uuid(submission.template, submitter_attrs, index)
|
submitter_uuid ||= find_submitter_uuid(submission.template, submitter_attrs, index)
|
||||||
|
|
||||||
process_readonly_fields_param(submitter_attrs[:readonly_fields], template_fields, submitter_uuid)
|
process_readonly_fields_param(submitter_attrs[:readonly_fields], template_fields, submitter_uuid)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ module Submissions
|
|||||||
VERIFIED_TEXT = 'Verified'
|
VERIFIED_TEXT = 'Verified'
|
||||||
UNVERIFIED_TEXT = 'Unverified'
|
UNVERIFIED_TEXT = 'Unverified'
|
||||||
|
|
||||||
|
CURRENCY_SYMBOLS = {
|
||||||
|
'USD' => '$',
|
||||||
|
'EUR' => '€',
|
||||||
|
'GBP' => '£'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
@@ -70,9 +76,11 @@ module Submissions
|
|||||||
|
|
||||||
composer.column(columns: 1) do |column|
|
composer.column(columns: 1) do |column|
|
||||||
column.text("Envelope ID: #{submission.id}", font_size: 12, padding: [20, 0, 10, 0], position: :float)
|
column.text("Envelope ID: #{submission.id}", font_size: 12, padding: [20, 0, 10, 0], position: :float)
|
||||||
column.formatted_text([
|
|
||||||
{ link: verify_url, text: 'Verify', style: :link }
|
unless submission.source_embed?
|
||||||
], font_size: 9, padding: [22, 0, 10, 0], position: :float, align: :right)
|
column.formatted_text([{ link: verify_url, text: 'Verify', style: :link }],
|
||||||
|
font_size: 9, padding: [22, 0, 10, 0], position: :float, align: :right)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
composer.draw_box(divider)
|
composer.draw_box(divider)
|
||||||
@@ -198,7 +206,15 @@ module Submissions
|
|||||||
|
|
||||||
composer.image(io, width:, height:, margin: [0, 0, 10, 0])
|
composer.image(io, width:, height:, margin: [0, 0, 10, 0])
|
||||||
composer.formatted_text_box([{ text: '' }])
|
composer.formatted_text_box([{ text: '' }])
|
||||||
elsif field['type'] == 'file'
|
elsif field['type'].in?(%w[file payment])
|
||||||
|
if field['type'] == 'payment'
|
||||||
|
unit = CURRENCY_SYMBOLS[field['preferences']['currency']]
|
||||||
|
|
||||||
|
price = ApplicationController.helpers.number_to_currency(field['preferences']['price'], unit:)
|
||||||
|
|
||||||
|
composer.formatted_text_box([{ text: "Paid #{price}\n" }], padding: [0, 0, 10, 0])
|
||||||
|
end
|
||||||
|
|
||||||
composer.formatted_text_box(
|
composer.formatted_text_box(
|
||||||
Array.wrap(value).map do |uuid|
|
Array.wrap(value).map do |uuid|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
@@ -212,7 +228,10 @@ module Submissions
|
|||||||
elsif field['type'] == 'checkbox'
|
elsif field['type'] == 'checkbox'
|
||||||
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
|
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
|
||||||
else
|
else
|
||||||
value = I18n.l(Date.parse(value), format: :long, locale: account.locale) if field['type'] == 'date'
|
if field['type'] == 'date'
|
||||||
|
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
|
||||||
|
end
|
||||||
|
|
||||||
value = value.join(', ') if value.is_a?(Array)
|
value = value.join(', ') if value.is_a?(Array)
|
||||||
|
|
||||||
composer.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], padding: [0, 0, 10, 0])
|
composer.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], padding: [0, 0, 10, 0])
|
||||||
|
|||||||
@@ -39,7 +39,12 @@ module Submissions
|
|||||||
field.fetch('areas', []).each do |area|
|
field.fetch('areas', []).each do |area|
|
||||||
pdf = pdfs_index[area['attachment_uuid']]
|
pdf = pdfs_index[area['attachment_uuid']]
|
||||||
|
|
||||||
|
next if pdf.nil?
|
||||||
|
|
||||||
page = pdf.pages[area['page']]
|
page = pdf.pages[area['page']]
|
||||||
|
|
||||||
|
next if page.nil?
|
||||||
|
|
||||||
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
||||||
|
|
||||||
page[:Annots] ||= []
|
page[:Annots] ||= []
|
||||||
@@ -80,7 +85,7 @@ module Submissions
|
|||||||
width: image.width * scale,
|
width: image.width * scale,
|
||||||
height: image.height * scale
|
height: image.height * scale
|
||||||
)
|
)
|
||||||
when 'file'
|
when 'file', 'payment'
|
||||||
items = Array.wrap(value).each_with_object([]) do |uuid, acc|
|
items = Array.wrap(value).each_with_object([]) do |uuid, acc|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
|
|
||||||
@@ -159,7 +164,9 @@ module Submissions
|
|||||||
height - (area['y'] * height))
|
height - (area['y'] * height))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
value = I18n.l(Date.parse(value), format: :default, locale: account.locale) if field['type'] == 'date'
|
if field['type'] == 'date'
|
||||||
|
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
|
||||||
|
end
|
||||||
|
|
||||||
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME),
|
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME),
|
||||||
font_size:)
|
font_size:)
|
||||||
@@ -263,7 +270,7 @@ module Submissions
|
|||||||
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
||||||
|
|
||||||
documents = latest_submitter&.documents&.preload(:blob).to_a.presence
|
documents = latest_submitter&.documents&.preload(:blob).to_a.presence
|
||||||
documents ||= submitter.submission.template.documents.preload(:blob)
|
documents ||= submitter.submission.template_schema_documents.preload(:blob)
|
||||||
|
|
||||||
documents.to_h do |attachment|
|
documents.to_h do |attachment|
|
||||||
pdf =
|
pdf =
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submissions
|
||||||
|
module NormalizeParamUtils
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def normalize_submissions_params!(submissions_params, template)
|
||||||
|
attachments = []
|
||||||
|
|
||||||
|
Array.wrap(submissions_params).each do |submission|
|
||||||
|
submission[:submitters].each_with_index do |submitter, index|
|
||||||
|
_, new_attachments = normalize_submitter_params!(submitter, template, index)
|
||||||
|
|
||||||
|
attachments.push(*new_attachments)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[submissions_params, attachments]
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_submitter_params!(submitter_params, template, index = nil, for_submitter: nil)
|
||||||
|
default_values = submitter_params[:values] || {}
|
||||||
|
|
||||||
|
submitter_params[:fields]&.each do |f|
|
||||||
|
default_values[f[:name]] = f[:default_value] if f[:default_value].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
return submitter_params if default_values.blank?
|
||||||
|
|
||||||
|
values, new_attachments =
|
||||||
|
Submitters::NormalizeValues.call(template,
|
||||||
|
default_values,
|
||||||
|
submitter_name: submitter_params[:role] ||
|
||||||
|
template.submitters.dig(index, 'name'),
|
||||||
|
for_submitter:,
|
||||||
|
throw_errors: true)
|
||||||
|
|
||||||
|
submitter_params[:values] = values
|
||||||
|
|
||||||
|
[submitter_params, new_attachments]
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_default_value_attachments!(attachments, submitters)
|
||||||
|
return if attachments.blank?
|
||||||
|
|
||||||
|
attachments_index = attachments.index_by(&:uuid)
|
||||||
|
|
||||||
|
submitters.each do |submitter|
|
||||||
|
submitter.values.to_a.each do |_, value|
|
||||||
|
attachment = attachments_index[value]
|
||||||
|
|
||||||
|
next unless attachment
|
||||||
|
|
||||||
|
attachment.record = submitter
|
||||||
|
|
||||||
|
attachment.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,12 +9,8 @@ module Submitters
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(template, values, submitter_name, throw_errors: false)
|
def call(template, values, submitter_name: nil, for_submitter: nil, throw_errors: false)
|
||||||
submitter =
|
fields = fetch_fields(template, submitter_name:, for_submitter:)
|
||||||
template.submitters.find { |e| e['name'] == submitter_name } ||
|
|
||||||
raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}")
|
|
||||||
|
|
||||||
fields = template.fields.select { |e| e['submitter_uuid'] == submitter['uuid'] }
|
|
||||||
|
|
||||||
fields_uuid_index = fields.index_by { |e| e['uuid'] }
|
fields_uuid_index = fields.index_by { |e| e['uuid'] }
|
||||||
fields_name_index = build_fields_index(fields)
|
fields_name_index = build_fields_index(fields)
|
||||||
@@ -31,7 +27,7 @@ module Submitters
|
|||||||
next if key.blank?
|
next if key.blank?
|
||||||
|
|
||||||
if fields_uuid_index[key]['type'].in?(%w[initials signature image file])
|
if fields_uuid_index[key]['type'].in?(%w[initials signature image file])
|
||||||
new_value, new_attachments = normalize_attachment_value(value, template.account)
|
new_value, new_attachments = normalize_attachment_value(value, template.account, for_submitter)
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
attachments.push(*new_attachments)
|
||||||
|
|
||||||
@@ -44,27 +40,45 @@ module Submitters
|
|||||||
[normalized_values, attachments]
|
[normalized_values, attachments]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_fields(template, submitter_name: nil, for_submitter: nil)
|
||||||
|
if submitter_name
|
||||||
|
submitter =
|
||||||
|
template.submitters.find { |e| e['name'] == submitter_name } ||
|
||||||
|
raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}")
|
||||||
|
end
|
||||||
|
|
||||||
|
fields = for_submitter&.submission&.template_fields || template.fields
|
||||||
|
|
||||||
|
fields.select { |e| e['submitter_uuid'] == (for_submitter&.uuid || submitter['uuid']) }
|
||||||
|
end
|
||||||
|
|
||||||
def build_fields_index(fields)
|
def build_fields_index(fields)
|
||||||
fields.index_by { |e| e['name'] }.merge(fields.index_by { |e| e['name'].to_s.parameterize.underscore })
|
fields.index_by { |e| e['name'] }.merge(fields.index_by { |e| e['name'].to_s.parameterize.underscore })
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_attachment_value(value, account)
|
def normalize_attachment_value(value, account, for_submitter = nil)
|
||||||
if value.is_a?(Array)
|
if value.is_a?(Array)
|
||||||
new_attachments = value.map { |v| build_attachment(v, account) }
|
new_attachments = value.map { |v| find_or_build_attachment(v, account, for_submitter) }
|
||||||
|
|
||||||
[new_attachments.map(&:uuid), new_attachments]
|
[new_attachments.map(&:uuid), new_attachments]
|
||||||
else
|
else
|
||||||
new_attachment = build_attachment(value, account)
|
new_attachment = find_or_build_attachment(value, account, for_submitter)
|
||||||
|
|
||||||
[new_attachment.uuid, new_attachment]
|
[new_attachment.uuid, new_attachment]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_attachment(value, account)
|
def find_or_build_attachment(value, account, for_submitter = nil)
|
||||||
ActiveStorage::Attachment.new(
|
blob = find_or_create_blobs(account, value)
|
||||||
blob: find_or_create_blobs(account, value),
|
|
||||||
|
attachment = for_submitter.attachments.find_by(blob_id: blob.id) if for_submitter
|
||||||
|
|
||||||
|
attachment ||= ActiveStorage::Attachment.new(
|
||||||
|
blob:,
|
||||||
name: 'attachments'
|
name: 'attachments'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_blobs(account, url)
|
def find_or_create_blobs(account, url)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ module Submitters
|
|||||||
def replace_default_variables(value, attrs, template, with_time: false)
|
def replace_default_variables(value, attrs, template, with_time: false)
|
||||||
return if value.blank?
|
return if value.blank?
|
||||||
|
|
||||||
value.gsub(VARIABLE_REGEXP) do |e|
|
value.to_s.gsub(VARIABLE_REGEXP) do |e|
|
||||||
case key = ::Regexp.last_match(1)
|
case key = ::Regexp.last_match(1)
|
||||||
when 'time'
|
when 'time'
|
||||||
if with_time
|
if with_time
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module TimeUtils
|
module TimeUtils
|
||||||
|
MONTH_FORMATS = {
|
||||||
|
'M' => '%-m',
|
||||||
|
'MM' => '%m',
|
||||||
|
'MMM' => '%b',
|
||||||
|
'MMMM' => '%B'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
DAY_FORMATS = {
|
||||||
|
'D' => '%-d',
|
||||||
|
'DD' => '%d'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
YEAR_FORMATS = {
|
||||||
|
'YYYY' => '%Y',
|
||||||
|
'YY' => '%y'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY'
|
||||||
|
DEFAULT_DATE_FORMAT = 'DD/MM/YYYY'
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def timezone_abbr(timezone, time = Time.current)
|
def timezone_abbr(timezone, time = Time.current)
|
||||||
@@ -10,4 +30,20 @@ module TimeUtils
|
|||||||
|
|
||||||
tz_info.abbreviation(time)
|
tz_info.abbreviation(time)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def format_date_string(string, format, locale)
|
||||||
|
date = Date.parse(string)
|
||||||
|
|
||||||
|
format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT
|
||||||
|
|
||||||
|
i18n_format = format.sub(/D+/, DAY_FORMATS[format[/D+/]])
|
||||||
|
.sub(/M+/, MONTH_FORMATS[format[/M+/]])
|
||||||
|
.sub(/Y+/, YEAR_FORMATS[format[/Y+/]])
|
||||||
|
|
||||||
|
I18n.l(date, format: i18n_format, locale:)
|
||||||
|
rescue Date::Error => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
string
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Reference in new issue
Block a user