Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b81cc83027 | ||
|
|
766d653349 | ||
|
|
98c04eac15 | ||
|
|
c35dfea154 | ||
|
|
37a9a86a1d | ||
|
|
e9a759055b | ||
|
|
f6c061a57b | ||
|
|
91f3d8ed7e | ||
|
|
3b18502d3d | ||
|
|
df8e79683a | ||
|
|
7080e8ea33 | ||
|
|
c56f8cb38f | ||
|
|
1ceac968b0 | ||
|
|
938ee99cfc | ||
|
|
2367040e45 | ||
|
|
df19aabfba | ||
|
|
f7b5bcd52f | ||
|
|
a249b0a678 | ||
|
|
99bc46b40f | ||
|
|
c1e6f0970d | ||
|
|
e8c2316bb2 | ||
|
|
9866e5468c | ||
|
|
fc60df7974 | ||
|
|
08784d28c7 | ||
|
|
0440ea652f | ||
|
|
e466562391 | ||
|
|
b2c8667c53 | ||
|
|
d6893079f7 | ||
|
|
ac1d973605 | ||
|
|
1405fefc85 | ||
|
|
2a75055fb6 | ||
|
|
f84fdd51de | ||
|
|
b38073126e | ||
|
|
c6e681195d | ||
|
|
53d924be0b | ||
|
|
d6cf027d5d | ||
|
|
ec332bab88 | ||
|
|
8b3b90a7e2 | ||
|
|
9047e3bed3 | ||
|
|
72b7391245 | ||
|
|
2df05f0327 | ||
|
|
95f3eb0e5d | ||
|
|
ce8d964ce6 | ||
|
|
f0d5fc4e2d | ||
|
|
b4b17d847e | ||
|
|
fd537e7ac0 | ||
|
|
8c271104e1 | ||
|
|
51edd60d96 | ||
|
|
a265ca1716 | ||
|
|
a773f63a65 | ||
|
|
576c8a2e24 | ||
|
|
5829de9ee4 | ||
|
|
b202941b79 | ||
|
|
6f6fb74988 | ||
|
|
0137bdbe4c | ||
|
|
55d9511c01 | ||
|
|
929d29576d | ||
|
|
6f7128a51c | ||
|
|
fc5c4ff2fc | ||
|
|
a81595db2b | ||
|
|
a5615d500d | ||
|
|
da9266befc | ||
|
|
5b3cffeb73 | ||
|
|
8f7b8a5fdf | ||
|
|
6d93116993 | ||
|
|
a0f4916d3b | ||
|
|
ace897eaf9 | ||
|
|
b8667c5408 | ||
|
|
d87d66632e | ||
|
|
3015c2d156 | ||
|
|
52b9e9ee8e | ||
|
|
5bfad0271c | ||
|
|
58797891c7 | ||
|
|
897750ba1e | ||
|
|
99341e773f | ||
|
|
71319c1548 | ||
|
|
c0a42f6bfc | ||
|
|
d5abb46112 | ||
|
|
2508b5fc8a | ||
|
|
4bd1ef0da3 | ||
|
|
4efd748571 | ||
|
|
859892d1fd | ||
|
|
7adeaf93cd | ||
|
|
5dbe155ecb | ||
|
|
667d065bee | ||
|
|
bb613059de | ||
|
|
1b2dec4c2a | ||
|
|
2c2ef97c30 | ||
|
|
6df1d68237 | ||
|
|
20c9faedff | ||
|
|
d931a40ea9 | ||
|
|
1097b98b85 | ||
|
|
2e597fcf43 | ||
|
|
bb2fa6a261 | ||
|
|
1dc8369487 | ||
|
|
e6d60f1985 | ||
|
|
82e3cb8a4d | ||
|
|
aeec0cbf8e | ||
|
|
7a2d1fc6c5 | ||
|
|
68033ab7d2 | ||
|
|
7595c60ea8 | ||
|
|
d9318c04f8 | ||
|
|
2d76198290 | ||
|
|
02a7b6ea55 | ||
|
|
ab57076b8f | ||
|
|
82fba47c98 | ||
|
|
5f32e2a5af | ||
|
|
70012fbd7c | ||
|
|
668f6ca975 | ||
|
|
e78dd301ef | ||
|
|
f8ec3daf10 | ||
|
|
75c929639d | ||
|
|
e9c1242b8d | ||
|
|
e7eb1d4f12 | ||
|
|
abbd3e809a | ||
|
|
f430b4f5a6 | ||
|
|
e65234b4c8 | ||
|
|
fe04d22c94 | ||
|
|
44a7a5ebdc | ||
|
|
119ef0d0c7 | ||
|
|
cdb8eb7931 | ||
|
|
aef176c0cc | ||
|
|
0c21b55865 | ||
|
|
964b1444a5 | ||
|
|
48de343948 | ||
|
|
2640934c15 | ||
|
|
442dd4d6db | ||
|
|
3168feb3d6 | ||
|
|
fbae4933f6 | ||
|
|
78bb66c344 | ||
|
|
5af65ef74f | ||
|
|
4d354ae9ff | ||
|
|
1e57c1ace7 | ||
|
|
fefd138dfd | ||
|
|
16a77d3536 | ||
|
|
e03857bdb7 | ||
|
|
fc679bcd72 | ||
|
|
652b0da021 | ||
|
|
2d54b0db2f | ||
|
|
7a29894926 | ||
|
|
f9ae79dd5b | ||
|
|
cadb78ca35 | ||
|
|
36a6b7ff1d | ||
|
|
efee46f113 | ||
|
|
b6639738c3 | ||
|
|
aab07c5f49 | ||
|
|
33c0784bc6 | ||
|
|
6f5ebfc8a6 | ||
|
|
811091ff14 | ||
|
|
6666f39928 | ||
|
|
f67db9fe5f | ||
|
|
45f002803e | ||
|
|
8c19f90e88 | ||
|
|
a8f5d5cea7 | ||
|
|
6788071eec | ||
|
|
0c690b3033 | ||
|
|
dcb963b623 | ||
|
|
04e30dbdd6 |
No files matched your search
@@ -11,7 +11,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.2.2
|
ruby-version: 3.3.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.2.2
|
ruby-version: 3.3.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.2.2
|
ruby-version: 3.3.1
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ AllCops:
|
|||||||
- node_modules/**/*
|
- node_modules/**/*
|
||||||
- bin/*
|
- bin/*
|
||||||
- vendor/**/*
|
- vendor/**/*
|
||||||
TargetRubyVersion: '3.2'
|
TargetRubyVersion: '3.3'
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
FROM ruby:3.2.2-alpine3.18 as fonts
|
FROM ruby:3.3.1-alpine3.18 as fonts
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/v2031/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine3.18 as webpack
|
FROM ruby:3.3.1-alpine3.18 as webpack
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -23,27 +23,41 @@ RUN yarn install --network-timeout 1000000
|
|||||||
COPY ./bin/shakapacker ./bin/shakapacker
|
COPY ./bin/shakapacker ./bin/shakapacker
|
||||||
COPY ./config/webpack ./config/webpack
|
COPY ./config/webpack ./config/webpack
|
||||||
COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
||||||
COPY ./postcss.config.js ./postcss.config.js ./
|
COPY ./postcss.config.js ./postcss.config.js
|
||||||
COPY ./tailwind.config.js ./tailwind.config.js ./
|
COPY ./tailwind.config.js ./tailwind.config.js
|
||||||
COPY ./tailwind.form.config.js ./tailwind.form.config.js ./
|
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
||||||
COPY ./tailwind.application.config.js ./tailwind.application.config.js ./
|
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
||||||
COPY ./app/javascript ./app/javascript
|
COPY ./app/javascript ./app/javascript
|
||||||
COPY ./app/views ./app/views
|
COPY ./app/views ./app/views
|
||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine3.18 as app
|
FROM ruby:3.3.1-alpine3.18 as app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
|
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||||
|
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
|
\n\
|
||||||
|
[provider_sect]\n\
|
||||||
|
default = default_sect\n\
|
||||||
|
legacy = legacy_sect\n\
|
||||||
|
\n\
|
||||||
|
[default_sect]\n\
|
||||||
|
activate = 1\n\
|
||||||
|
\n\
|
||||||
|
[legacy_sect]\n\
|
||||||
|
activate = 1' >> /app/openssl_legacy.cnf
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN apk add --no-cache build-base && bundle update --bundler && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
RUN apk add --no-cache build-base && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY ./bin ./bin
|
||||||
COPY ./app ./app
|
COPY ./app ./app
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.2.2'
|
ruby '3.3.1'
|
||||||
|
|
||||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'azure-storage-blob', require: false
|
gem 'azure-storage-blob', require: false
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
gem 'cancancan'
|
gem 'cancancan'
|
||||||
|
gem 'csv'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'dotenv', require: false
|
gem 'dotenv', require: false
|
||||||
@@ -23,7 +24,6 @@ gem 'lograge'
|
|||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pdf-reader'
|
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'pretender'
|
gem 'pretender'
|
||||||
@@ -38,7 +38,7 @@ gem 'ruby-vips'
|
|||||||
gem 'rubyXL'
|
gem 'rubyXL'
|
||||||
gem 'shakapacker'
|
gem 'shakapacker'
|
||||||
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false, force_ruby_platform: true
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
gem 'twitter_cldr', require: false
|
gem 'twitter_cldr', require: false
|
||||||
|
|||||||
@@ -1,36 +1,35 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
Ascii85 (1.1.0)
|
actioncable (7.1.3.2)
|
||||||
actioncable (7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (7.1.2)
|
actionmailbox (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
activejob (= 7.1.2)
|
activejob (= 7.1.3.2)
|
||||||
activerecord (= 7.1.2)
|
activerecord (= 7.1.3.2)
|
||||||
activestorage (= 7.1.2)
|
activestorage (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.1.2)
|
actionmailer (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
actionview (= 7.1.2)
|
actionview (= 7.1.3.2)
|
||||||
activejob (= 7.1.2)
|
activejob (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.1.2)
|
actionpack (7.1.3.2)
|
||||||
actionview (= 7.1.2)
|
actionview (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
racc
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
@@ -38,35 +37,35 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.1.2)
|
actiontext (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
activerecord (= 7.1.2)
|
activerecord (= 7.1.3.2)
|
||||||
activestorage (= 7.1.2)
|
activestorage (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.1.2)
|
actionview (7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (7.1.2)
|
activejob (7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.1.2)
|
activemodel (7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
activerecord (7.1.2)
|
activerecord (7.1.3.2)
|
||||||
activemodel (= 7.1.2)
|
activemodel (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (7.1.2)
|
activestorage (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
activejob (= 7.1.2)
|
activejob (= 7.1.3.2)
|
||||||
activerecord (= 7.1.2)
|
activerecord (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (7.1.2)
|
activesupport (7.1.3.2)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
@@ -78,24 +77,23 @@ GEM
|
|||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.6)
|
addressable (2.8.6)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
afm (0.2.2)
|
|
||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
arabic-letter-connector (0.1.1)
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.874.0)
|
aws-partitions (1.913.0)
|
||||||
aws-sdk-core (3.190.1)
|
aws-sdk-core (3.191.6)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.75.0)
|
aws-sdk-kms (1.79.0)
|
||||||
aws-sdk-core (~> 3, >= 3.188.0)
|
aws-sdk-core (~> 3, >= 3.191.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.142.0)
|
aws-sdk-s3 (1.146.1)
|
||||||
aws-sdk-core (~> 3, >= 3.189.0)
|
aws-sdk-core (~> 3, >= 3.191.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
aws-sigv4 (1.8.0)
|
aws-sigv4 (1.8.0)
|
||||||
@@ -110,52 +108,55 @@ GEM
|
|||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
better_html (2.0.2)
|
better_html (2.1.1)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
ast (~> 2.0)
|
ast (~> 2.0)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
bigdecimal (3.1.5)
|
bigdecimal (3.1.8)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.17.0)
|
bootsnap (1.18.3)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (7.1.4)
|
bullet (7.1.6)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
camertron-eprun (1.1.1)
|
camertron-eprun (1.1.1)
|
||||||
cancancan (3.5.0)
|
cancancan (3.5.0)
|
||||||
capybara (3.39.2)
|
capybara (3.40.0)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.11)
|
||||||
rack (>= 1.6.0)
|
rack (>= 1.6.0)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
|
childprocess (5.0.0)
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
cldr-plurals-runtime-rb (1.1.0)
|
cldr-plurals-runtime-rb (1.1.0)
|
||||||
cmdparse (3.0.7)
|
cmdparse (3.0.7)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.2.3)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
crack (0.4.5)
|
crack (1.0.0)
|
||||||
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.16.0)
|
css_parser (1.17.1)
|
||||||
addressable
|
addressable
|
||||||
|
csv (3.3.0)
|
||||||
cuprite (0.15)
|
cuprite (0.15)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.14.0)
|
ferrum (~> 0.14.0)
|
||||||
date (3.3.4)
|
date (3.3.4)
|
||||||
debug (1.9.1)
|
debug (1.9.2)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
devise (4.9.3)
|
devise (4.9.4)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@@ -166,13 +167,12 @@ GEM
|
|||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties (~> 7.0)
|
railties (~> 7.0)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.1)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.6.5)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
dotenv (2.8.1)
|
dotenv (3.1.0)
|
||||||
drb (2.2.0)
|
drb (2.2.1)
|
||||||
ruby2_keywords
|
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb_lint (0.5.0)
|
erb_lint (0.5.0)
|
||||||
activesupport
|
activesupport
|
||||||
@@ -182,12 +182,12 @@ GEM
|
|||||||
rubocop
|
rubocop
|
||||||
smart_properties
|
smart_properties
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
factory_bot (6.4.4)
|
factory_bot (6.4.6)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.4.2)
|
factory_bot_rails (6.4.3)
|
||||||
factory_bot (~> 6.4)
|
factory_bot (~> 6.4)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.2)
|
faker (3.3.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
@@ -225,34 +225,34 @@ GEM
|
|||||||
geom2d (0.4.1)
|
geom2d (0.4.1)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-apis-core (0.11.2)
|
google-apis-core (0.14.1)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (~> 1.9)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
webrick
|
google-apis-iamcredentials_v1 (0.20.0)
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
google-apis-core (>= 0.14.0, < 2.a)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-storage_v1 (0.37.0)
|
||||||
google-apis-storage_v1 (0.29.0)
|
google-apis-core (>= 0.14.0, < 2.a)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-cloud-core (1.7.0)
|
||||||
google-cloud-core (1.6.1)
|
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (2.1.0)
|
google-cloud-env (2.1.1)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.3.1)
|
google-cloud-errors (1.4.0)
|
||||||
google-cloud-storage (1.45.0)
|
google-cloud-storage (1.49.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-core (~> 0.13)
|
||||||
google-apis-storage_v1 (~> 0.29.0)
|
google-apis-iamcredentials_v1 (~> 0.18)
|
||||||
|
google-apis-storage_v1 (~> 0.33)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (~> 1.9)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.9.1)
|
googleauth (1.11.0)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-env (~> 2.1)
|
google-cloud-env (~> 2.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
@@ -260,30 +260,31 @@ GEM
|
|||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.1.0)
|
hashdiff (1.1.0)
|
||||||
hashery (2.1.2)
|
hexapdf (0.42.0)
|
||||||
hexapdf (0.37.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)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.14.1)
|
i18n (1.14.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
io-console (0.7.1)
|
io-console (0.7.2)
|
||||||
irb (1.11.0)
|
irb (1.13.1)
|
||||||
rdoc
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.7.1)
|
json (2.7.2)
|
||||||
jwt (2.7.1)
|
jwt (2.8.1)
|
||||||
|
base64
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
launchy (2.5.2)
|
launchy (3.0.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.8.1)
|
childprocess (~> 5.0)
|
||||||
launchy (>= 2.2, < 3)
|
letter_opener (1.10.0)
|
||||||
|
launchy (>= 2.2, < 4)
|
||||||
letter_opener_web (2.0.0)
|
letter_opener_web (2.0.0)
|
||||||
actionmailer (>= 5.2)
|
actionmailer (>= 5.2)
|
||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
@@ -302,21 +303,21 @@ GEM
|
|||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.2)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
method_source (1.0.0)
|
method_source (1.1.0)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.5)
|
mini_portile2 (2.8.6)
|
||||||
minitest (5.20.0)
|
minitest (5.23.0)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.4.0)
|
||||||
mutex_m (0.2.0)
|
mutex_m (0.2.0)
|
||||||
mysql2 (0.5.5)
|
mysql2 (0.5.6)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.4.9)
|
net-imap (0.4.10)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -325,28 +326,27 @@ GEM
|
|||||||
timeout
|
timeout
|
||||||
net-smtp (0.4.0)
|
net-smtp (0.4.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.0)
|
nio4r (2.7.1)
|
||||||
nokogiri (1.15.5)
|
nokogiri (1.16.5)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.16.5-aarch64-linux)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.16.5-x86_64-linux)
|
||||||
|
racc (~> 1.4)
|
||||||
oj (3.16.3)
|
oj (3.16.3)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.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)
|
||||||
pagy (6.2.0)
|
package_json (0.1.0)
|
||||||
|
pagy (8.1.2)
|
||||||
parallel (1.24.0)
|
parallel (1.24.0)
|
||||||
parser (3.2.2.4)
|
parser (3.3.1.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pdf-reader (2.12.0)
|
pg (1.5.6)
|
||||||
Ascii85 (~> 1.0)
|
premailer (1.23.0)
|
||||||
afm (~> 0.2.1)
|
|
||||||
hashery (~> 2.0)
|
|
||||||
ruby-rc4
|
|
||||||
ttfunk
|
|
||||||
pg (1.5.4)
|
|
||||||
premailer (1.21.0)
|
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.12.0)
|
css_parser (>= 1.12.0)
|
||||||
htmlentities (>= 4.0.0)
|
htmlentities (>= 4.0.0)
|
||||||
@@ -363,34 +363,34 @@ GEM
|
|||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
psych (5.1.2)
|
psych (5.1.2)
|
||||||
stringio
|
stringio
|
||||||
public_suffix (5.0.4)
|
public_suffix (5.0.5)
|
||||||
puma (6.4.0)
|
puma (6.4.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.7.3)
|
racc (1.8.0)
|
||||||
rack (2.2.8)
|
rack (3.0.11)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (1.0.2)
|
rack-session (2.0.0)
|
||||||
rack (< 3)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (1.0.0)
|
rackup (2.1.0)
|
||||||
rack (< 3)
|
rack (>= 3)
|
||||||
webrick
|
webrick (~> 1.8)
|
||||||
rails (7.1.2)
|
rails (7.1.3.2)
|
||||||
actioncable (= 7.1.2)
|
actioncable (= 7.1.3.2)
|
||||||
actionmailbox (= 7.1.2)
|
actionmailbox (= 7.1.3.2)
|
||||||
actionmailer (= 7.1.2)
|
actionmailer (= 7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
actiontext (= 7.1.2)
|
actiontext (= 7.1.3.2)
|
||||||
actionview (= 7.1.2)
|
actionview (= 7.1.3.2)
|
||||||
activejob (= 7.1.2)
|
activejob (= 7.1.3.2)
|
||||||
activemodel (= 7.1.2)
|
activemodel (= 7.1.3.2)
|
||||||
activerecord (= 7.1.2)
|
activerecord (= 7.1.3.2)
|
||||||
activestorage (= 7.1.2)
|
activestorage (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.1.2)
|
railties (= 7.1.3.2)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
@@ -398,35 +398,35 @@ GEM
|
|||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (~> 1.14)
|
||||||
rails-i18n (7.0.8)
|
rails-i18n (7.0.9)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
rails_autolink (1.1.8)
|
rails_autolink (1.1.8)
|
||||||
actionview (> 3.1)
|
actionview (> 3.1)
|
||||||
activesupport (> 3.1)
|
activesupport (> 3.1)
|
||||||
railties (> 3.1)
|
railties (> 3.1)
|
||||||
railties (7.1.2)
|
railties (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
irb
|
irb
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.1.0)
|
rake (13.2.1)
|
||||||
rdoc (6.6.2)
|
rdoc (6.6.3.1)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.19.1)
|
redis-client (0.22.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.8.3)
|
regexp_parser (2.9.0)
|
||||||
reline (0.4.1)
|
reline (0.5.7)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
request_store (1.5.1)
|
request_store (1.6.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
@@ -438,73 +438,76 @@ GEM
|
|||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 1.0)
|
rqrcode_core (~> 1.0)
|
||||||
rqrcode_core (1.2.0)
|
rqrcode_core (1.2.0)
|
||||||
rspec-core (3.12.2)
|
rspec-core (3.13.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.12.3)
|
rspec-expectations (3.13.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.12.6)
|
rspec-mocks (3.13.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (6.1.0)
|
rspec-rails (6.1.2)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
rspec-core (~> 3.12)
|
rspec-core (~> 3.13)
|
||||||
rspec-expectations (~> 3.12)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.12)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.12)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.13.1)
|
||||||
rubocop (1.59.0)
|
rubocop (1.63.3)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.2.4)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.30.0)
|
rubocop-ast (1.31.2)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.3.0.4)
|
||||||
rubocop-capybara (2.19.0)
|
rubocop-capybara (2.20.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.24.0)
|
rubocop-factory_bot (2.25.1)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.41)
|
||||||
rubocop-performance (1.20.1)
|
rubocop-performance (1.21.0)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.23.1)
|
rubocop-rails (2.24.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rspec (2.25.0)
|
rubocop-rspec (2.29.1)
|
||||||
rubocop (~> 1.40)
|
rubocop (~> 1.40)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
|
rubocop-rspec_rails (~> 2.28)
|
||||||
|
rubocop-rspec_rails (2.28.3)
|
||||||
|
rubocop (~> 1.40)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-rc4 (0.1.5)
|
ruby-vips (2.2.1)
|
||||||
ruby-vips (2.2.0)
|
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyXL (3.4.25)
|
rubyXL (3.4.27)
|
||||||
nokogiri (>= 1.10.8)
|
nokogiri (>= 1.10.8)
|
||||||
rubyzip (>= 1.3.0)
|
rubyzip (>= 1.3.0)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
shakapacker (7.1.0)
|
shakapacker (8.0.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
|
package_json
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
sidekiq (7.2.0)
|
sidekiq (7.2.2)
|
||||||
concurrent-ruby (< 2)
|
concurrent-ruby (< 2)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 2.3.0)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
redis-client (>= 0.14.0)
|
redis-client (>= 0.19.0)
|
||||||
signet (0.18.0)
|
signet (0.19.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
@@ -516,17 +519,15 @@ 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)
|
||||||
sqlite3 (1.7.0)
|
sqlite3 (1.7.3)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
sqlite3 (1.7.0-arm64-darwin)
|
|
||||||
stringio (3.1.0)
|
stringio (3.1.0)
|
||||||
strip_attributes (1.13.0)
|
strip_attributes (1.13.0)
|
||||||
activemodel (>= 3.0, < 8.0)
|
activemodel (>= 3.0, < 8.0)
|
||||||
thor (1.3.0)
|
thor (1.3.1)
|
||||||
timeout (0.4.1)
|
timeout (0.4.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
ttfunk (1.7.0)
|
turbo-rails (2.0.5)
|
||||||
turbo-rails (1.5.0)
|
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activejob (>= 6.0.0)
|
activejob (>= 6.0.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
@@ -536,7 +537,7 @@ GEM
|
|||||||
tzinfo
|
tzinfo
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2023.4)
|
tzinfo-data (1.2024.1)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
@@ -548,7 +549,7 @@ GEM
|
|||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
webmock (3.19.1)
|
webmock (3.23.0)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
@@ -558,11 +559,12 @@ GEM
|
|||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.6.12)
|
zeitwerk (2.6.14)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-22
|
aarch64-linux
|
||||||
ruby
|
ruby
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
annotate
|
annotate
|
||||||
@@ -574,6 +576,7 @@ DEPENDENCIES
|
|||||||
bullet
|
bullet
|
||||||
cancancan
|
cancancan
|
||||||
capybara
|
capybara
|
||||||
|
csv
|
||||||
cuprite
|
cuprite
|
||||||
debug
|
debug
|
||||||
devise
|
devise
|
||||||
@@ -594,7 +597,6 @@ DEPENDENCIES
|
|||||||
mysql2
|
mysql2
|
||||||
oj
|
oj
|
||||||
pagy
|
pagy
|
||||||
pdf-reader
|
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
pretender
|
pretender
|
||||||
@@ -625,7 +627,7 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.2.2p53
|
ruby 3.3.1p55
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.3
|
2.5.3
|
||||||
@@ -3,3 +3,12 @@
|
|||||||
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||||
|
|
||||||
**We have a bug bounty program to reward security researchers.**
|
**We have a bug bounty program to reward security researchers.**
|
||||||
|
|
||||||
|
Out of scope vulnerabilities:
|
||||||
|
|
||||||
|
- CSRF
|
||||||
|
- DNSSEC, CAA, CSP headers
|
||||||
|
- DNS or email security related
|
||||||
|
- Rate Limiting
|
||||||
|
|
||||||
|
Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program.
|
||||||
@@ -9,7 +9,10 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::FORCE_MFA,
|
AccountConfig::FORCE_MFA,
|
||||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
AccountConfig::FORM_WITH_CONFETTI_KEY
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
|
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
|
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||||
|
AccountConfig::FLATTEN_RESULT_PDF_KEY
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
InvalidKey = Class.new(StandardError)
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
before_action :set_cors_headers
|
before_action :set_cors_headers
|
||||||
|
before_action :set_noindex_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
@@ -20,6 +21,8 @@ module Api
|
|||||||
|
|
||||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
|
authorization_check!(blob) if exp.blank?
|
||||||
|
|
||||||
if request.headers['Range'].present?
|
if request.headers['Range'].present?
|
||||||
send_blob_byte_range_data blob, request.headers['Range']
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
else
|
else
|
||||||
@@ -31,5 +34,22 @@ module Api
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authorization_check!(blob)
|
||||||
|
is_authorized =
|
||||||
|
blob.attachments.all? do |a|
|
||||||
|
a.name.in?(%w[logo preview_images]) ||
|
||||||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
|
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||||
|
end
|
||||||
|
|
||||||
|
return if is_authorized
|
||||||
|
|
||||||
|
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
||||||
|
|
||||||
|
raise CanCan::AccessDenied
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,6 +7,9 @@ module Api
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :set_cors_headers
|
||||||
|
before_action :set_noindex_headers
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def show
|
def show
|
||||||
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
||||||
|
|||||||
@@ -50,20 +50,26 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_user!
|
def authenticate_user!
|
||||||
@current_user ||=
|
|
||||||
if request.headers['X-Auth-Token'].present?
|
|
||||||
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
|
||||||
|
|
||||||
User.joins(:access_token).active.find_by(access_token: { sha256: })
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
super || @current_user ||=
|
||||||
|
if request.headers['X-Auth-Token'].present?
|
||||||
|
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
||||||
|
|
||||||
|
User.joins(:access_token).active.find_by(access_token: { sha256: })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_noindex_headers
|
||||||
|
headers['X-Robots-Tag'] = 'noindex'
|
||||||
|
end
|
||||||
|
|
||||||
def set_cors_headers
|
def set_cors_headers
|
||||||
headers['Access-Control-Allow-Origin'] = '*'
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ module Api
|
|||||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
|
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||||
|
template: :folder,
|
||||||
audit_trail_attachment: :blob))
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.as_json(serialize_params),
|
data: submissions.as_json(Submissions::SerializeForApi::SERIALIZE_PARAMS),
|
||||||
pagination: {
|
pagination: {
|
||||||
count: submissions.size,
|
count: submissions.size,
|
||||||
next: submissions.last&.id,
|
next: submissions.last&.id,
|
||||||
@@ -43,33 +44,11 @@ module Api
|
|||||||
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
|
render json: Submissions::SerializeForApi.call(@submission, submitters)
|
||||||
|
|
||||||
json = @submission.as_json(
|
|
||||||
serialize_params.deep_merge(
|
|
||||||
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if submitters.all?(&:completed_at?)
|
|
||||||
last_submitter = submitters.max_by(&:completed_at)
|
|
||||||
|
|
||||||
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
|
||||||
json[:status] = 'completed'
|
|
||||||
json[:completed_at] = last_submitter.completed_at
|
|
||||||
else
|
|
||||||
json[:documents] = []
|
|
||||||
json[:status] = 'pending'
|
|
||||||
json[:completed_at] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
json[:submitters] = serialized_submitters
|
|
||||||
|
|
||||||
render json:
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
Params::SubmissionCreateValidator.call(params, dry_run: !Docuseal.multitenant?)
|
Params::SubmissionCreateValidator.call(params)
|
||||||
|
|
||||||
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
||||||
|
|
||||||
@@ -84,6 +63,10 @@ module Api
|
|||||||
|
|
||||||
submissions = create_submissions(@template, params)
|
submissions = create_submissions(@template, params)
|
||||||
|
|
||||||
|
submissions.each do |submission|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_later(submission)
|
||||||
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
submissions.each do |submission|
|
submissions.each do |submission|
|
||||||
@@ -92,7 +75,13 @@ module Api
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: submissions.flat_map(&:submitters)
|
json = submissions.flat_map do |submission|
|
||||||
|
submission.submitters.map do |s|
|
||||||
|
Submitters::SerializeForApi.call(s, with_documents: false, with_urls: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json:
|
||||||
rescue Submitters::NormalizeValues::BaseError => e
|
rescue Submitters::NormalizeValues::BaseError => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -102,6 +91,8 @@ module Api
|
|||||||
def destroy
|
def destroy
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
|
||||||
|
|
||||||
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
|
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -144,32 +135,17 @@ module Api
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialize_params
|
|
||||||
{
|
|
||||||
only: %i[id source submitters_order created_at updated_at archived_at],
|
|
||||||
methods: %i[audit_log_url],
|
|
||||||
include: {
|
|
||||||
submitters: { only: %i[id slug uuid name email phone
|
|
||||||
completed_at opened_at sent_at
|
|
||||||
created_at updated_at external_id metadata],
|
|
||||||
methods: %i[status application_key] },
|
|
||||||
template: { only: %i[id name created_at updated_at] },
|
|
||||||
created_by_user: { only: %i[id email first_name last_name] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
permitted_attrs = [
|
permitted_attrs = [
|
||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||||
{
|
{
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :default_value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :validation_pattern, :invalid_message,
|
:readonly, :redacted, :validation_pattern, :invalid_message,
|
||||||
{ default_value: [] }] }]]
|
{ default_value: [], value: [] }] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ module Api
|
|||||||
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||||
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
|
if params[:template_id].present?
|
||||||
|
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
|
||||||
|
end
|
||||||
|
|
||||||
submitters = maybe_filder_by_completed_at(submitters, params)
|
submitters = maybe_filder_by_completed_at(submitters, params)
|
||||||
|
|
||||||
submitters = paginate(
|
submitters = paginate(
|
||||||
@@ -66,18 +71,21 @@ module Api
|
|||||||
Submitters.send_signature_requests([@submitter])
|
Submitters.send_signature_requests([@submitter])
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false)
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
|
||||||
|
with_urls: true,
|
||||||
|
with_events: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||||
|
|
||||||
submitter_params.permit(
|
submitter_params.permit(
|
||||||
:send_email, :send_sms, :uuid, :name, :email, :role,
|
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id,
|
:completed, :phone, :application_key, :external_id, :go_to_last,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [[:name, :default_value, :readonly, :validation_pattern, :invalid_message,
|
fields: [[:name, :uuid, :default_value, :value,
|
||||||
{ default_value: [] }]] }
|
:readonly, :redacted, :validation_pattern, :invalid_message,
|
||||||
|
{ default_value: [], value: [] }]] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -96,20 +104,41 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def assign_submitter_attrs(submitter, attrs)
|
def assign_submitter_attrs(submitter, attrs)
|
||||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
values = attrs[:values]&.to_unsafe_h || {}
|
||||||
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].present?
|
|
||||||
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
|
|
||||||
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
|
||||||
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
|
||||||
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
|
|
||||||
|
|
||||||
assign_submission_fields(submitter.submission)
|
assign_submission_fields(submitter.submission)
|
||||||
|
|
||||||
|
phone_field_uuid = submitter.submission.template_fields.find do |f|
|
||||||
|
values[f['uuid']].present? && f['type'] == 'phone'
|
||||||
|
end&.dig('uuid')
|
||||||
|
|
||||||
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
|
||||||
|
if attrs.key?(:phone)
|
||||||
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
||||||
|
elsif values[phone_field_uuid].present?
|
||||||
|
submitter.phone = values[phone_field_uuid].to_s.gsub(/[^0-9+]/, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
values = values.except(phone_field_uuid)
|
||||||
|
|
||||||
|
submitter.values = submitter.values.merge(values) if values.present?
|
||||||
|
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
|
||||||
|
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
|
||||||
|
|
||||||
|
assign_external_id(submitter, attrs)
|
||||||
assign_preferences(submitter, attrs)
|
assign_preferences(submitter, attrs)
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assign_external_id(submitter, attrs)
|
||||||
|
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
||||||
|
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
def assign_submission_fields(submission)
|
def assign_submission_fields(submission)
|
||||||
submission.template_fields ||= submission.template.fields
|
submission.template_fields ||= submission.template.fields
|
||||||
submission.template_schema ||= submission.template.schema
|
submission.template_schema ||= submission.template.schema
|
||||||
@@ -125,6 +154,14 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||||
|
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||||
|
if submitter_preferences.key?('go_to_last')
|
||||||
|
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||||
|
end
|
||||||
|
|
||||||
|
if submitter_preferences.key?('completed_redirect_url')
|
||||||
|
submitter.preferences['completed_redirect_url'] = submitter_preferences['completed_redirect_url']
|
||||||
|
end
|
||||||
|
|
||||||
return unless submitter_preferences.key?('email_message_uuid')
|
return unless submitter_preferences.key?('email_message_uuid')
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ module Api
|
|||||||
|
|
||||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||||
|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_later(cloned_template)
|
||||||
|
|
||||||
render json: Templates::SerializeForApi.call(cloned_template)
|
render json: Templates::SerializeForApi.call(cloned_template)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ module Api
|
|||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -91,10 +93,25 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
|
permitted_params = [
|
||||||
|
:name,
|
||||||
|
{
|
||||||
|
submitters: [%i[name uuid]],
|
||||||
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
|
{ preferences: {},
|
||||||
|
conditions: [%i[field_uuid value action]],
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
if params.key?(:template)
|
if params.key?(:template)
|
||||||
params.require(:template).permit(:name, submitters: [%i[name uuid]])
|
params.require(:template).permit(permitted_params)
|
||||||
else
|
else
|
||||||
params.permit(:name, submitters: [%i[name uuid]])
|
params.permit(permitted_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
|
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
|
||||||
|
|
||||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
||||||
|
|
||||||
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ class ErrorsController < ActionController::Base
|
|||||||
'/api/templates/docx'
|
'/api/templates/docx'
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
|
SAFE_ERROR_MESSAGE_CLASSES = [
|
||||||
|
ActionDispatch::Http::Parameters::ParseError,
|
||||||
|
JSON::ParserError
|
||||||
|
].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
|
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
|
||||||
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
|
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
|
||||||
@@ -24,7 +29,11 @@ class ErrorsController < ActionController::Base
|
|||||||
f.json do
|
f.json do
|
||||||
set_cors_headers
|
set_cors_headers
|
||||||
|
|
||||||
render json: { status: error_status_code }, status: error_status_code
|
exception = request.env['action_dispatch.exception']
|
||||||
|
|
||||||
|
error = exception.message if exception.class.in?(SAFE_ERROR_MESSAGE_CLASSES)
|
||||||
|
|
||||||
|
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
f.html { render error_status_code.to_s, status: error_status_code }
|
f.html { render error_status_code.to_s, status: error_status_code }
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ class EsignSettingsController < ApplicationController
|
|||||||
save_new_cert!(@encrypted_config, @cert_record)
|
save_new_cert!(@encrypted_config, @cert_record)
|
||||||
|
|
||||||
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||||
rescue OpenSSL::PKCS12::PKCS12Error
|
rescue OpenSSL::PKCS12::PKCS12Error => e
|
||||||
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
|
@cert_record.errors.add(:password, e.message)
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ class NotificationsSettingsController < ApplicationController
|
|||||||
|
|
||||||
def load_reminder_config
|
def load_reminder_config
|
||||||
@reminder_config =
|
@reminder_config =
|
||||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS)
|
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMINDERS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def email_config_params
|
def email_config_params
|
||||||
params.require(:account_config).permit!.tap do |attrs|
|
params.require(:account_config).permit!.tap do |attrs|
|
||||||
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS])
|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -18,6 +18,12 @@ class PersonalizationSettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
if @account_config.value.is_a?(Hash)
|
||||||
|
@account_config.value = @account_config.value.reject do |_, v|
|
||||||
|
v.blank? && v != false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if @account_config.value != false && @account_config.value.blank?
|
if @account_config.value != false && @account_config.value.blank?
|
||||||
@account_config.destroy!
|
@account_config.destroy!
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class SessionsController < Devise::SessionsController
|
|||||||
email = sign_in_params[:email].to_s.downcase
|
email = sign_in_params[:email].to_s.downcase
|
||||||
|
|
||||||
if Docuseal.multitenant? && !User.exists?(email:)
|
if Docuseal.multitenant? && !User.exists?(email:)
|
||||||
|
Rollbar.warning('Sign in new user') if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||||
notice: 'Create a new account'
|
notice: 'Create a new account'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
||||||
|
|
||||||
|
is_new_record = @submitter.new_record?
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_later(@submitter.submission) if is_new_record
|
||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class SubmissionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
|
|
||||||
|
save_template_message(@template, params) if params[:save_message] == '1'
|
||||||
|
|
||||||
if params[:is_custom_message] != '1'
|
if params[:is_custom_message] != '1'
|
||||||
params.delete(:subject)
|
params.delete(:subject)
|
||||||
params.delete(:body)
|
params.delete(:body)
|
||||||
@@ -42,6 +44,10 @@ class SubmissionsController < ApplicationController
|
|||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
submissions.each do |submission|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_later(submission)
|
||||||
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: 'New recipients have been added'
|
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||||
@@ -50,11 +56,20 @@ class SubmissionsController < ApplicationController
|
|||||||
def destroy
|
def destroy
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
|
||||||
|
|
||||||
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def save_template_message(template, params)
|
||||||
|
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
||||||
|
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
||||||
|
|
||||||
|
template.save!
|
||||||
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,13 +16,16 @@ class SubmissionsDebugController < ApplicationController
|
|||||||
render 'submit_form/show'
|
render 'submit_form/show'
|
||||||
end
|
end
|
||||||
f.pdf do
|
f.pdf do
|
||||||
if params[:audit]
|
result =
|
||||||
Submissions::GenerateAuditTrail.call(@submitter.submission)
|
if params[:audit]
|
||||||
else
|
Submissions::GenerateAuditTrail.call(@submitter.submission)
|
||||||
Submissions::GenerateResultAttachments.call(@submitter)
|
elsif params[:combined]
|
||||||
end
|
Submissions::GenerateCombinedAttachment.call(@submitter)
|
||||||
|
else
|
||||||
|
Submissions::GenerateResultAttachments.call(@submitter)
|
||||||
|
end
|
||||||
|
|
||||||
send_data ActiveStorage::Attachment.where(name: params[:audit] ? :audit_trail : :documents).last.download,
|
send_data Array.wrap(result).first.download,
|
||||||
filename: 'debug.pdf',
|
filename: 'debug.pdf',
|
||||||
disposition: 'inline',
|
disposition: 'inline',
|
||||||
type: 'application/pdf'
|
type: 'application/pdf'
|
||||||
|
|||||||
@@ -33,7 +33,17 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
return head :not_found
|
return head :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: build_urls(last_submitter)
|
if params[:combined]
|
||||||
|
url = build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
else
|
||||||
|
render json: build_urls(last_submitter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -44,7 +54,17 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
def build_urls(submitter)
|
def build_urls(submitter)
|
||||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.minutes.from_now.to_i)
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_combined_url(submitter)
|
||||||
|
return if submitter.submission.submitters.exists?(completed_at: nil)
|
||||||
|
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||||
|
|
||||||
|
attachment = submitter.submission.combined_document_attachment
|
||||||
|
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
||||||
|
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -55,7 +55,7 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormDrawSignatureController < ApplicationController
|
||||||
|
layout false
|
||||||
|
|
||||||
|
around_action :with_browser_locale, only: %i[show]
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def show
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
|
if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
||||||
|
return redirect_to submit_form_path(@submitter.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
render :show
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormValuesController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
|
return render json: {} if submitter.completed_at?
|
||||||
|
return render json: {} if submitter.submission.template.archived_at? || submitter.submission.archived_at?
|
||||||
|
|
||||||
|
value = submitter.values[params['field_uuid']]
|
||||||
|
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
value:,
|
||||||
|
attachment: attachment&.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
||||||
|
}, head: :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -25,7 +25,7 @@ class SubmittersAutocompleteController < ApplicationController
|
|||||||
|
|
||||||
term = "#{params[:q].downcase}%"
|
term = "#{params[:q].downcase}%"
|
||||||
|
|
||||||
submitters.where(column.matches(term, false, true))
|
submitters.where(column.matches(term))
|
||||||
else
|
else
|
||||||
Submitters.search(submitters, params[:q])
|
Submitters.search(submitters, params[:q])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class SubmittersSendEmailController < ApplicationController
|
|||||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||||
event_type: 'send_email',
|
event_type: 'send_email',
|
||||||
created_at: 24.hours.ago..Time.current)
|
created_at: 24.hours.ago..Time.current)
|
||||||
|
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
||||||
alert: 'Email has been sent already.')
|
alert: 'Email has been sent already.')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
if params[:blobs].blank? && params[:files].blank?
|
||||||
|
return render json: { error: 'File is missing' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
old_fields_hash = @template.fields.hash
|
old_fields_hash = @template.fields.hash
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ class TemplatesController < ApplicationController
|
|||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
maybe_redirect_to_template(@template)
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
||||||
@@ -70,6 +72,8 @@ class TemplatesController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesPreferencesController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
@template.preferences = @template.preferences.merge(template_params[:preferences])
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def template_params
|
||||||
|
params.require(:template).permit(
|
||||||
|
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||||
|
documents_copy_email_subject documents_copy_email_body]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -18,12 +18,16 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_later(@template)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
|
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
raise if Rails.env.local?
|
||||||
|
|
||||||
redirect_to root_path, alert: 'Unable to upload file'
|
redirect_to root_path, alert: 'Unable to upload file'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TimestampServerController < ApplicationController
|
class TimestampServerController < ApplicationController
|
||||||
|
HASH_ALGORITHM = 'SHA256'
|
||||||
|
|
||||||
before_action :build_encrypted_config
|
before_action :build_encrypted_config
|
||||||
authorize_resource :encrypted_config
|
authorize_resource :encrypted_config
|
||||||
|
|
||||||
|
TimestampError = Class.new(StandardError)
|
||||||
|
|
||||||
def create
|
def create
|
||||||
return head :not_found if Docuseal.multitenant?
|
return head :not_found if Docuseal.multitenant?
|
||||||
|
|
||||||
@@ -14,25 +18,29 @@ class TimestampServerController < ApplicationController
|
|||||||
else
|
else
|
||||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
end
|
end
|
||||||
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
|
rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError
|
||||||
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def test_timeserver_url(url)
|
def test_timeserver_url(url)
|
||||||
pdf = HexaPDF::Document.new
|
req = OpenSSL::Timestamp::Request.new
|
||||||
pdf.pages.add
|
req.algorithm = HASH_ALGORITHM
|
||||||
|
req.message_imprint = OpenSSL::Digest.digest(HASH_ALGORITHM, 'test')
|
||||||
|
|
||||||
pkcs = Accounts.load_signing_pkcs(current_account)
|
uri = Addressable::URI.parse(url)
|
||||||
|
|
||||||
pdf.sign(StringIO.new,
|
conn = Faraday.new(uri.origin) do |c|
|
||||||
reason: 'Test',
|
c.basic_auth(uri.user, uri.password) if uri.password.present?
|
||||||
certificate: pkcs.certificate,
|
end
|
||||||
key: pkcs.key,
|
|
||||||
signature_size: 10_000,
|
response = conn.post(uri.path, req.to_der,
|
||||||
certificate_chain: pkcs.ca_certs || [],
|
'content-type' => 'application/timestamp-query')
|
||||||
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
|
||||||
|
raise TimestampError if response.status != 200 || response.body.blank?
|
||||||
|
|
||||||
|
response
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_encrypted_config
|
def load_encrypted_config
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UserInitialsController < ApplicationController
|
||||||
|
before_action :load_user_config
|
||||||
|
authorize_resource :user_config
|
||||||
|
|
||||||
|
def edit; end
|
||||||
|
|
||||||
|
def update
|
||||||
|
file = params[:file]
|
||||||
|
|
||||||
|
return redirect_to settings_profile_index_path, notice: 'Unable to save initials' if file.blank?
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||||
|
filename: file.original_filename,
|
||||||
|
content_type: file.content_type)
|
||||||
|
|
||||||
|
attachment = ActiveStorage::Attachment.create!(
|
||||||
|
blob:,
|
||||||
|
name: 'initials',
|
||||||
|
record: current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
if @user_config.update(value: attachment.uuid)
|
||||||
|
redirect_to settings_profile_index_path, notice: 'Initials has been saved'
|
||||||
|
else
|
||||||
|
redirect_to settings_profile_index_path, notice: 'Unable to save initials'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@user_config.destroy
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path, notice: 'Initials has been removed'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_user_config
|
||||||
|
@user_config =
|
||||||
|
UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::INITIALS_KEY)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
load_and_authorize_resource :user, only: %i[index edit new update destroy]
|
load_and_authorize_resource :user, only: %i[index edit update destroy]
|
||||||
|
|
||||||
before_action :build_user, only: :create
|
before_action :build_user, only: %i[new create]
|
||||||
authorize_resource :user, only: :create
|
authorize_resource :user, only: %i[new create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@pagy, @users = pagy(@users.active.order(id: :desc))
|
@users =
|
||||||
|
if params[:status] == 'archived'
|
||||||
|
@users.archived
|
||||||
|
else
|
||||||
|
@users.active
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
@@ -15,10 +22,20 @@ class UsersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||||
|
|
||||||
|
if existing_user
|
||||||
|
existing_user.archived_at = nil
|
||||||
|
existing_user.assign_attributes(user_params)
|
||||||
|
existing_user.account = current_account
|
||||||
|
|
||||||
|
@user = existing_user
|
||||||
|
end
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
UserMailer.invitation_email(@user).deliver_later!
|
UserMailer.invitation_email(@user).deliver_later!
|
||||||
|
|
||||||
redirect_to settings_users_path, notice: 'User has been invited'
|
redirect_back fallback_location: settings_users_path, notice: 'User has been invited'
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -27,11 +44,11 @@ class UsersController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
||||||
|
|
||||||
attrs = user_params.compact_blank
|
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
||||||
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
||||||
|
|
||||||
if @user.update(attrs)
|
if @user.update(attrs)
|
||||||
redirect_to settings_users_path, notice: 'User has been updated'
|
redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -44,7 +61,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
@user.update!(archived_at: Time.current)
|
@user.update!(archived_at: Time.current)
|
||||||
|
|
||||||
redirect_to settings_users_path, notice: 'User has been removed'
|
redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -65,6 +82,11 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email, :first_name, :last_name, :password, :role)
|
if params.key?(:user)
|
||||||
|
params.require(:user).permit(:email, :first_name, :last_name, :password,
|
||||||
|
:role, :archived_at, :account_id)
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -27,7 +27,8 @@ class VerifyPdfSignatureController < ApplicationController
|
|||||||
trusted_certs = [default_pkcs.certificate,
|
trusted_certs = [default_pkcs.certificate,
|
||||||
*default_pkcs.ca_certs,
|
*default_pkcs.ca_certs,
|
||||||
*custom_certs.map(&:certificate),
|
*custom_certs.map(&:certificate),
|
||||||
*custom_certs.flat_map(&:ca_certs).compact]
|
*custom_certs.flat_map(&:ca_certs).compact,
|
||||||
|
*Docuseal.trusted_certs]
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WebhookPreferencesController < ApplicationController
|
||||||
|
EVENTS = %w[
|
||||||
|
form.viewed
|
||||||
|
form.started
|
||||||
|
form.completed
|
||||||
|
template.created
|
||||||
|
template.updated
|
||||||
|
submission.created
|
||||||
|
submission.archived
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
before_action :load_account_config
|
||||||
|
authorize_resource :account_config, parent: false
|
||||||
|
|
||||||
|
def create
|
||||||
|
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
|
||||||
|
|
||||||
|
@account_config.save!
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account_config
|
||||||
|
@account_config =
|
||||||
|
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
|
||||||
|
@account_config.value ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
params.permit(:event, :value)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -23,6 +23,7 @@ import SignatureForm from './elements/signature_form'
|
|||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
import PromptPassword from './elements/prompt_password'
|
import PromptPassword from './elements/prompt_password'
|
||||||
import EmailsTextarea from './elements/emails_textarea'
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -40,25 +41,6 @@ document.addEventListener('keyup', (e) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.customElements.define('toggle-visible', ToggleVisible)
|
|
||||||
window.customElements.define('disable-hidden', DisableHidden)
|
|
||||||
window.customElements.define('turbo-modal', TurboModal)
|
|
||||||
window.customElements.define('file-dropzone', FileDropzone)
|
|
||||||
window.customElements.define('menu-active', MenuActive)
|
|
||||||
window.customElements.define('clipboard-copy', ClipboardCopy)
|
|
||||||
window.customElements.define('dynamic-list', DynamicList)
|
|
||||||
window.customElements.define('download-button', DownloadButton)
|
|
||||||
window.customElements.define('set-origin-url', SetOriginUrl)
|
|
||||||
window.customElements.define('set-timezone', SetTimezone)
|
|
||||||
window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
|
||||||
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
|
||||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
|
||||||
window.customElements.define('signature-form', SignatureForm)
|
|
||||||
window.customElements.define('submit-form', SubmitForm)
|
|
||||||
window.customElements.define('prompt-password', PromptPassword)
|
|
||||||
window.customElements.define('emails-textarea', EmailsTextarea)
|
|
||||||
window.customElements.define('toggle-cookies', ToggleCookies)
|
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||||
@@ -82,7 +64,29 @@ document.addEventListener('turbo:submit-end', async (event) => {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
})
|
})
|
||||||
|
|
||||||
window.customElements.define('template-builder', class extends HTMLElement {
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
|
|
||||||
|
safeRegisterElement('toggle-visible', ToggleVisible)
|
||||||
|
safeRegisterElement('disable-hidden', DisableHidden)
|
||||||
|
safeRegisterElement('turbo-modal', TurboModal)
|
||||||
|
safeRegisterElement('file-dropzone', FileDropzone)
|
||||||
|
safeRegisterElement('menu-active', MenuActive)
|
||||||
|
safeRegisterElement('clipboard-copy', ClipboardCopy)
|
||||||
|
safeRegisterElement('dynamic-list', DynamicList)
|
||||||
|
safeRegisterElement('download-button', DownloadButton)
|
||||||
|
safeRegisterElement('set-origin-url', SetOriginUrl)
|
||||||
|
safeRegisterElement('set-timezone', SetTimezone)
|
||||||
|
safeRegisterElement('autoresize-textarea', AutoresizeTextarea)
|
||||||
|
safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete)
|
||||||
|
safeRegisterElement('folder-autocomplete', FolderAutocomplete)
|
||||||
|
safeRegisterElement('signature-form', SignatureForm)
|
||||||
|
safeRegisterElement('submit-form', SubmitForm)
|
||||||
|
safeRegisterElement('prompt-password', PromptPassword)
|
||||||
|
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||||
|
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||||
|
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||||
|
|
||||||
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
@@ -114,7 +118,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.customElements.define('import-list', class extends HTMLElement {
|
safeRegisterElement('import-list', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
|
||||||
|
|
||||||
|
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
|
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
||||||
|
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
||||||
|
|
||||||
|
this.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
|
this.pad = new SignaturePad(this.canvas)
|
||||||
|
|
||||||
|
this.pad.addEventListener('endStroke', () => {
|
||||||
|
this.updateSubmitButtonVisibility()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.clearButton.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.clearSignaturePad()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.submitButton.disabled = true
|
||||||
|
|
||||||
|
this.submitImage().then((data) => {
|
||||||
|
this.valueInput.value = data.uuid
|
||||||
|
|
||||||
|
return fetch(this.form.action, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: new FormData(this.form)
|
||||||
|
}).then((response) => {
|
||||||
|
this.form.classList.add('hidden')
|
||||||
|
this.success.classList.remove('hidden')
|
||||||
|
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
this.submitButton.disabled = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSignaturePad () {
|
||||||
|
this.pad.clear()
|
||||||
|
this.updateSubmitButtonVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSubmitButtonVisibility () {
|
||||||
|
if (this.pad.isEmpty()) {
|
||||||
|
this.submitButton.style.display = 'none'
|
||||||
|
this.placeholderButton.style.display = 'block'
|
||||||
|
} else {
|
||||||
|
this.submitButton.style.display = 'block'
|
||||||
|
this.placeholderButton.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitImage () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||||
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('submitter_slug', this.dataset.slug)
|
||||||
|
formData.append('name', 'attachments')
|
||||||
|
|
||||||
|
return fetch('/api/attachments', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then((resp) => resp.json()).then((attachment) => {
|
||||||
|
return resolve(attachment)
|
||||||
|
})
|
||||||
|
}).catch((error) => {
|
||||||
|
return reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitButton () {
|
||||||
|
return this.querySelector('button[type="submit"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get clearButton () {
|
||||||
|
return this.querySelector('button[aria-label="Clear"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get placeholderButton () {
|
||||||
|
return this.querySelector('button[disabled]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get canvas () {
|
||||||
|
return this.querySelector('canvas')
|
||||||
|
}
|
||||||
|
|
||||||
|
get valueInput () {
|
||||||
|
return this.querySelector('input[name^="values"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
|
||||||
|
get success () {
|
||||||
|
return this.querySelector('#success')
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -7,7 +7,7 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
resize () {
|
resize () {
|
||||||
if (this.textarea.clientHeight < this.textarea.scrollHeight) {
|
if (this.textarea.clientHeight < this.textarea.scrollHeight) {
|
||||||
this.textarea.style.height = `${this.textarea.scrollHeight}px`
|
this.textarea.style.height = `${this.textarea.scrollHeight + 5}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
this.toggleState()
|
this.toggleState()
|
||||||
|
|
||||||
fetch(this.dataset.src).then((response) => response.json()).then((urls) => {
|
fetch(this.dataset.src).then(async (response) => {
|
||||||
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
if (response.ok) {
|
||||||
|
const urls = await response.json()
|
||||||
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
|
|
||||||
if (isSafariIos && urls.length > 1) {
|
if (isSafariIos && urls.length > 1) {
|
||||||
this.downloadSafariIos(urls)
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.downloadUrls(urls)
|
alert('Failed to download files')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,11 +45,7 @@ export default class extends HTMLElement {
|
|||||||
this.currentFetch.then(async (resp) => {
|
this.currentFetch.then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
if (q.length < 3) {
|
resolve(items)
|
||||||
resolve(items.filter((e) => e[this.dataset.field].startsWith(q)))
|
|
||||||
} else {
|
|
||||||
resolve(items)
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
resolve([])
|
resolve([])
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
|
||||||
|
this.form.addEventListener('submit', this.onSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
|
||||||
|
this.form.removeEventListener('submit', this.onSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = () => {
|
||||||
|
this.element.classList.add('invisible')
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitEnd = (event) => {
|
||||||
|
if (event.target === this.form) {
|
||||||
|
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||||
|
|
||||||
|
if (resp?.status / 100 === 2) {
|
||||||
|
this.element.classList.remove('invisible')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get element () {
|
||||||
|
return document.getElementById(this.dataset.elementId)
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.button.disabled = false
|
||||||
|
|
||||||
|
this.form.addEventListener('submit', () => {
|
||||||
|
this.button.disabled = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.button.disabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
get button () {
|
||||||
|
return this.querySelector('[type="submit"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form') || this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,13 @@ import { createApp, reactive } from 'vue'
|
|||||||
|
|
||||||
import Form from './submission_form/form'
|
import Form from './submission_form/form'
|
||||||
import DownloadButton from './elements/download_button'
|
import DownloadButton from './elements/download_button'
|
||||||
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
|
|
||||||
window.customElements.define('download-button', DownloadButton)
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
window.customElements.define('submission-form', class extends HTMLElement {
|
|
||||||
|
safeRegisterElement('download-button', DownloadButton)
|
||||||
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
@@ -15,10 +19,12 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
values: reactive(JSON.parse(this.dataset.values)),
|
values: reactive(JSON.parse(this.dataset.values)),
|
||||||
completedButton: JSON.parse(this.dataset.completedButton || '{}'),
|
completedButton: JSON.parse(this.dataset.completedButton || '{}'),
|
||||||
|
withQrButton: true,
|
||||||
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
||||||
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import Rollbar from 'rollbar/dist/rollbar.umd'
|
||||||
|
|
||||||
|
const token = document.querySelector('meta[name="rollbar-token"]')?.getAttribute('content')
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
window.Rollbar ||= new Rollbar({
|
||||||
|
accessToken: token,
|
||||||
|
captureUncaught: true,
|
||||||
|
captureUnhandledRejections: true,
|
||||||
|
captureIp: false,
|
||||||
|
autoInstrument: false,
|
||||||
|
ignoredMessages: [
|
||||||
|
/Failed to fetch/i,
|
||||||
|
/NetworkError/i,
|
||||||
|
/Load failed/i,
|
||||||
|
/Clipboard write is not allowed/i
|
||||||
|
],
|
||||||
|
transform (payload) {
|
||||||
|
payload.body.telemetry = []
|
||||||
|
|
||||||
|
const string = JSON.stringify(payload)
|
||||||
|
|
||||||
|
return JSON.parse(string.replace(/(\/[des]\/)\w{6}/g, (_, m) => m))
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
client: {
|
||||||
|
javascript: {
|
||||||
|
source_map_enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
environment: 'production'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex absolute lg:text-base -outline-offset-1"
|
class="field-area flex absolute lg:text-base -outline-offset-1"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:style="computedStyle"
|
:style="computedStyle"
|
||||||
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 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"
|
||||||
@@ -165,6 +165,9 @@
|
|||||||
<span v-else-if="field.type === 'date'">
|
<span v-else-if="field.type === 'date'">
|
||||||
{{ formattedDate }}
|
{{ formattedDate }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="field.type === 'number'">
|
||||||
|
{{ formatNumber(modelValue, field.preferences?.format) }}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
@@ -373,6 +376,17 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
formatNumber (number, format) {
|
||||||
|
if (format === 'comma') {
|
||||||
|
return new Intl.NumberFormat('en-US').format(number)
|
||||||
|
} else if (format === 'dot') {
|
||||||
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
|
} else if (format === 'space') {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(number)
|
||||||
|
} else {
|
||||||
|
return number
|
||||||
|
}
|
||||||
|
},
|
||||||
formatDate (date, format) {
|
formatDate (date, format) {
|
||||||
const monthFormats = {
|
const monthFormats = {
|
||||||
M: 'numeric',
|
M: 'numeric',
|
||||||
@@ -388,6 +402,7 @@ export default {
|
|||||||
|
|
||||||
const yearFormats = {
|
const yearFormats = {
|
||||||
YYYY: 'numeric',
|
YYYY: 'numeric',
|
||||||
|
YYY: 'numeric',
|
||||||
YY: '2-digit'
|
YY: '2-digit'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,13 +197,18 @@ export default {
|
|||||||
download () {
|
download () {
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
|
||||||
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
if (response.ok) {
|
||||||
|
const urls = await response.json()
|
||||||
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
|
|
||||||
if (isSafariIos && urls.length > 1) {
|
if (isSafariIos && urls.length > 1) {
|
||||||
this.downloadSafariIos(urls)
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.downloadUrls(urls)
|
alert('Failed to download files')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-sm !normal-case font-normal"
|
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||||
@click.prevent="setCurrentDate"
|
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||||
>
|
>
|
||||||
<IconCalendarCheck :width="16" />
|
<IconCalendarCheck :width="16" />
|
||||||
{{ t('set_today') }}
|
{{ t('set_today') }}
|
||||||
|
|||||||
@@ -104,9 +104,13 @@ export default {
|
|||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
Array.from(files).map((file) => {
|
Array.from(files).map(async (file) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
|
if (file.type === 'image/bmp') {
|
||||||
|
file = await this.convertBmpToPng(file)
|
||||||
|
}
|
||||||
|
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
@@ -122,6 +126,32 @@ export default {
|
|||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
convertBmpToPng (bmpFile) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = function (event) {
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
img.onload = function () {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
canvas.width = img.width
|
||||||
|
canvas.height = img.height
|
||||||
|
ctx.drawImage(img, 0, 0)
|
||||||
|
canvas.toBlob(function (blob) {
|
||||||
|
const newFile = new File([blob], bmpFile.name.replace(/\.\w+$/, '.png'), { type: 'image/png' })
|
||||||
|
resolve(newFile)
|
||||||
|
}, 'image/png')
|
||||||
|
}
|
||||||
|
|
||||||
|
img.src = event.target.result
|
||||||
|
}
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(bmpFile)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,17 @@
|
|||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
id="expand_form_button"
|
id="expand_form_button"
|
||||||
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3"
|
||||||
|
style="width: 96%; margin-left: 2%"
|
||||||
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
||||||
>
|
>
|
||||||
<template v-if="['initials', 'signature'].includes(currentField.type)">
|
<template v-if="['initials', 'signature'].includes(currentField.type)">
|
||||||
<IconWritingSign stroke-width="1.5" />
|
<IconWritingSign stroke-width="1.5" />
|
||||||
{{ t('sign_now') }}
|
{{ t('sign_now') }}
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="alwaysMinimize">
|
||||||
|
{{ t('next') }}
|
||||||
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ t('submit_form') }}
|
{{ t('submit_form') }}
|
||||||
</template>
|
</template>
|
||||||
@@ -320,6 +324,9 @@
|
|||||||
:previous-value="previousSignatureValueFor(currentField)"
|
:previous-value="previousSignatureValueFor(currentField)"
|
||||||
:with-typed-signature="withTypedSignature"
|
:with-typed-signature="withTypedSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
|
:button-text="buttonText"
|
||||||
|
:with-disclosure="withDisclosure"
|
||||||
|
:with-qr-button="withQrButton"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@@ -356,7 +363,9 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:locale="language?.toLowerCase() || browserLanguage"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
|
:verified-value="phoneVerifiedValues[currentField.uuid]"
|
||||||
:default-value="submitter.phone"
|
:default-value="submitter.phone"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@@ -376,7 +385,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
|
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
|
||||||
class="mt-6 md:mt-8"
|
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id="submit_form_button"
|
id="submit_form_button"
|
||||||
@@ -390,11 +399,8 @@
|
|||||||
v-if="isSubmitting"
|
v-if="isSubmitting"
|
||||||
class="mr-1 animate-spin"
|
class="mr-1 animate-spin"
|
||||||
/>
|
/>
|
||||||
<span v-if="stepFields.length === currentStep + 1">
|
<span>
|
||||||
{{ t('submit') }}
|
{{ buttonText }}
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
{{ t('next') }}
|
|
||||||
</span><span
|
</span><span
|
||||||
v-if="isSubmitting"
|
v-if="isSubmitting"
|
||||||
class="w-6 flex justify-start mr-1"
|
class="w-6 flex justify-start mr-1"
|
||||||
@@ -466,10 +472,14 @@ import i18n from './i18n'
|
|||||||
const isEmpty = (obj) => {
|
const isEmpty = (obj) => {
|
||||||
if (obj == null) return true
|
if (obj == null) return true
|
||||||
|
|
||||||
if (Array.isArray(obj) || typeof obj === 'string') {
|
if (Array.isArray(obj)) {
|
||||||
return obj.length === 0
|
return obj.length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return obj.trim().length === 0
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof obj === 'object') {
|
if (typeof obj === 'object') {
|
||||||
return Object.keys(obj).length === 0
|
return Object.keys(obj).length === 0
|
||||||
}
|
}
|
||||||
@@ -539,16 +549,41 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
minimize: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withDisclosure: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
reuseSignature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
autoscrollFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
showFieldNames: {
|
showFieldNames: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withQrButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withTypedSignature: {
|
withTypedSignature: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -589,6 +624,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
dryRun: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
attribution: {
|
attribution: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -646,14 +686,29 @@ export default {
|
|||||||
isFormVisible: this.expand !== false,
|
isFormVisible: this.expand !== false,
|
||||||
showFillAllRequiredFields: false,
|
showFillAllRequiredFields: false,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
|
phoneVerifiedValues: {},
|
||||||
|
orientation: screen?.orientation?.type,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
submittedValues: {},
|
submittedValues: {},
|
||||||
recalculateButtonDisabledKey: ''
|
recalculateButtonDisabledKey: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isMobile () {
|
||||||
|
return /android|iphone|ipad/i.test(navigator.userAgent)
|
||||||
|
},
|
||||||
|
buttonText () {
|
||||||
|
if (this.alwaysMinimize || this.stepFields.length === this.currentStep + 1) {
|
||||||
|
return this.t('submit')
|
||||||
|
} else {
|
||||||
|
return this.t('next')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
alwaysMinimize () {
|
||||||
|
return this.minimize || (this.orientation?.includes('landscape') && this.isMobile && parseInt(window.innerHeight) < 550)
|
||||||
|
},
|
||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep] || []
|
||||||
},
|
},
|
||||||
browserLanguage () {
|
browserLanguage () {
|
||||||
return (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
return (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
||||||
@@ -672,9 +727,13 @@ export default {
|
|||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
previousInitialsValue () {
|
previousInitialsValue () {
|
||||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
if (this.reuseSignature !== false) {
|
||||||
|
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
|
||||||
return this.values[initialsField?.uuid]
|
return this.values[initialsField?.uuid]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isAnonymousChecboxes () {
|
isAnonymousChecboxes () {
|
||||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
|
||||||
@@ -724,11 +783,21 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
expand (value) {
|
expand (value) {
|
||||||
this.isFormVisible = value
|
this.isFormVisible = value
|
||||||
|
},
|
||||||
|
currentStepFields (value) {
|
||||||
|
if (isEmpty(value) && this.currentStep > 0) {
|
||||||
|
this.currentStep -= 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
screen?.orientation?.removeEventListener('change', this.onOrientationChange)
|
||||||
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||||
|
|
||||||
|
screen?.orientation.addEventListener('change', this.onOrientationChange)
|
||||||
|
|
||||||
this.fields.forEach((field) => {
|
this.fields.forEach((field) => {
|
||||||
if (field.default_value && !field.readonly) {
|
if (field.default_value && !field.readonly) {
|
||||||
this.values[field.uuid] ||= field.default_value
|
this.values[field.uuid] ||= field.default_value
|
||||||
@@ -762,6 +831,10 @@ export default {
|
|||||||
this.isFormVisible = false
|
this.isFormVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.alwaysMinimize) {
|
||||||
|
this.isFormVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
@@ -791,6 +864,9 @@ export default {
|
|||||||
t (key) {
|
t (key) {
|
||||||
return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
|
return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
|
||||||
},
|
},
|
||||||
|
onOrientationChange (event) {
|
||||||
|
this.orientation = event.target.type
|
||||||
|
},
|
||||||
checkFieldConditions (field) {
|
checkFieldConditions (field) {
|
||||||
if (field.conditions?.length) {
|
if (field.conditions?.length) {
|
||||||
return field.conditions.reduce((acc, c) => {
|
return field.conditions.reduce((acc, c) => {
|
||||||
@@ -885,11 +961,15 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
previousSignatureValueFor (field) {
|
previousSignatureValueFor (field) {
|
||||||
const signatureField = [...this.fields].reverse().find((f) =>
|
if (this.reuseSignature !== false) {
|
||||||
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
const signatureField = [...this.fields].reverse().find((f) =>
|
||||||
)
|
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
||||||
|
)
|
||||||
|
|
||||||
return this.values[signatureField?.uuid]
|
return this.values[signatureField?.uuid]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
@@ -898,19 +978,22 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalculateButtonDisabledKey = Math.random()
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
|
|
||||||
if (scrollToArea) {
|
if (!this.isCompleted) {
|
||||||
this.scrollIntoField(step[0])
|
if (scrollToArea) {
|
||||||
}
|
this.scrollIntoField(step[0])
|
||||||
|
|
||||||
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
|
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
||||||
this.$refs.form.querySelector('input[type="file"]')?.click()
|
this.$refs.form.querySelector('input[type="file"]')?.click()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveStep (formData) {
|
saveStep (formData) {
|
||||||
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
||||||
|
const currentFieldType = this.currentField.type
|
||||||
|
|
||||||
if (this.isCompleted) {
|
if (this.isCompleted) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
@@ -922,6 +1005,10 @@ export default {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
currentFieldUuids.forEach((fieldUuid) => {
|
currentFieldUuids.forEach((fieldUuid) => {
|
||||||
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
||||||
|
|
||||||
|
if (currentFieldType === 'phone') {
|
||||||
|
this.phoneVerifiedValues[fieldUuid] = this.values[fieldUuid]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -956,11 +1043,19 @@ export default {
|
|||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
const isLastStep = this.currentStep === this.stepFields.length - 1
|
const isLastStep = this.currentStep === this.stepFields.length - 1
|
||||||
|
|
||||||
if (isLastStep && !emptyRequiredField) {
|
if (isLastStep && !emptyRequiredField && !this.dryRun) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.saveStep(formData).then(async (response) => {
|
let saveStepRequest
|
||||||
|
|
||||||
|
if (!isLastStep && this.phoneVerifiedValues[this.currentField.uuid] && this.phoneVerifiedValues[this.currentField.uuid] === this.values[this.currentField.uuid]) {
|
||||||
|
saveStepRequest = Promise.resolve({})
|
||||||
|
} else {
|
||||||
|
saveStepRequest = this.saveStep(formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveStepRequest.then(async (response) => {
|
||||||
if (response.status === 422 || response.status === 500) {
|
if (response.status === 422 || response.status === 500) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
@@ -976,7 +1071,11 @@ export default {
|
|||||||
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
this.goToStep(nextStep, true)
|
if (this.alwaysMinimize) {
|
||||||
|
this.isFormVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.goToStep(nextStep, this.autoscrollFields)
|
||||||
|
|
||||||
if (emptyRequiredField === nextStep) {
|
if (emptyRequiredField === nextStep) {
|
||||||
this.showFillAllRequiredFields = true
|
this.showFillAllRequiredFields = true
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
const en = {
|
const en = {
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
|
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
|
||||||
|
electronic_signature_disclosure: 'Electronic Signature Disclosure',
|
||||||
|
esignature_disclosure: 'eSignature Disclosure',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initials',
|
initials: 'Initials',
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Drawn signature on a touchscreen device',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan the QR code with the camera app to open the form on mobile and draw your signature',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
number: 'Number',
|
number: 'Number',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
@@ -24,7 +29,7 @@ const en = {
|
|||||||
appears_on: 'Appears on',
|
appears_on: 'Appears on',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
select_your_option: 'Select your option',
|
select_your_option: 'Select your option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
|
complete_hightlighted_checkboxes_and_click: 'Complete highlighted checkboxes and click',
|
||||||
submit: 'submit',
|
submit: 'submit',
|
||||||
next: 'next',
|
next: 'next',
|
||||||
click_to_upload: 'Click to upload',
|
click_to_upload: 'Click to upload',
|
||||||
@@ -42,7 +47,7 @@ const en = {
|
|||||||
please_check_the_box_to_continue: 'Please check the box to continue',
|
please_check_the_box_to_continue: 'Please check the box to continue',
|
||||||
open_source_documents_software: 'open source documents software',
|
open_source_documents_software: 'open source documents software',
|
||||||
verified_phone_number: 'Verify Phone Number',
|
verified_phone_number: 'Verify Phone Number',
|
||||||
use_international_format: 'Use internatioanl format: +1xxx',
|
use_international_format: 'Use international format: +1xxx',
|
||||||
six_digits_code: '6-digit code',
|
six_digits_code: '6-digit code',
|
||||||
change_phone_number: 'Change phone number',
|
change_phone_number: 'Change phone number',
|
||||||
sending: 'Sending...',
|
sending: 'Sending...',
|
||||||
@@ -65,6 +70,11 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Firma dibujada en un dispositivo con pantalla táctil',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
||||||
|
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
||||||
|
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
||||||
|
esignature_disclosure: 'Divulgación de eFirma',
|
||||||
minimize: 'Minimizar',
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
@@ -130,6 +140,11 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Firma disegnata su un dispositivo con schermo tattile',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scansiona il codice QR con l\'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma',
|
||||||
|
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
||||||
|
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||||
|
esignature_disclosure: 'Divulgazione della eFirma',
|
||||||
minimize: 'Minimizza',
|
minimize: 'Minimizza',
|
||||||
text: 'Testo',
|
text: 'Testo',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
@@ -195,6 +210,11 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Gezeichnete Unterschrift auf einem Touchscreen-Gerät',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannen Sie den QR-Code mit der Kamera-App, um das Formular auf dem Handy zu öffnen und Ihre Unterschrift zu zeichnen',
|
||||||
|
by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie zu, dass Sie die',
|
||||||
|
electronic_signature_disclosure: 'Elektronische Unterschriftenoffenlegung',
|
||||||
|
esignature_disclosure: 'eSignatur Offenlegung',
|
||||||
minimize: 'Minimieren',
|
minimize: 'Minimieren',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Unterschrift',
|
signature: 'Unterschrift',
|
||||||
@@ -260,6 +280,11 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannez le code QR avec l\'application de l\'appareil photo pour ouvrir le formulaire sur mobile et dessiner votre signature',
|
||||||
|
by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la',
|
||||||
|
electronic_signature_disclosure: 'Divulgation de Signature Électronique',
|
||||||
|
esignature_disclosure: 'Divulgation de la eSignature',
|
||||||
minimize: 'Réduire',
|
minimize: 'Réduire',
|
||||||
text: 'Texte',
|
text: 'Texte',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
@@ -325,6 +350,11 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Podpis odręczny na urządzeniu z ekranem dotykowym',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
||||||
|
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
||||||
|
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
||||||
|
esignature_disclosure: 'Ujawnienie ePodpisu',
|
||||||
minimize: 'Zminimalizuj',
|
minimize: 'Zminimalizuj',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
@@ -390,6 +420,11 @@ const pl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Підпис на сенсорному пристрої',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
||||||
|
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
||||||
|
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
||||||
|
esignature_disclosure: 'Розголошення еПідпису',
|
||||||
minimize: 'Зменшити',
|
minimize: 'Зменшити',
|
||||||
text: 'Текст',
|
text: 'Текст',
|
||||||
signature: 'Підпис',
|
signature: 'Підпис',
|
||||||
@@ -455,6 +490,11 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Namalovaný podpis na dotykovém zařízení',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
||||||
|
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
||||||
|
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
||||||
|
esignature_disclosure: 'Zveřejnění ePodpisu',
|
||||||
minimize: 'Minimalizovat',
|
minimize: 'Minimalizovat',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
@@ -520,6 +560,11 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Assinatura desenhada em um dispositivo com tela sensível ao toque',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
||||||
|
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
||||||
|
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
||||||
|
esignature_disclosure: 'Divulgação da eAssinatura',
|
||||||
minimize: 'Minimizar',
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Assinatura',
|
signature: 'Assinatura',
|
||||||
@@ -585,6 +630,11 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'חתימה שנוצרה במכשיר עם מסך מגע',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
||||||
|
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
||||||
|
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||||
|
esignature_disclosure: 'חשיפת ה-eחתימה',
|
||||||
minimize: 'לקטן',
|
minimize: 'לקטן',
|
||||||
text: 'טקסט',
|
text: 'טקסט',
|
||||||
signature: 'חתימה',
|
signature: 'חתימה',
|
||||||
@@ -651,6 +701,11 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Getekende handtekening op een apparaat met een touchscreen',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan de QR-code met de camera-app om het formulier op mobiel te openen en uw handtekening te tekenen',
|
||||||
|
by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de',
|
||||||
|
electronic_signature_disclosure: 'Openbaarmaking van Elektronische Handtekening',
|
||||||
|
esignature_disclosure: 'Openbaarmaking van eHandtekening',
|
||||||
minimize: 'Minimaliseren',
|
minimize: 'Minimaliseren',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
signature: 'Handtekening',
|
signature: 'Handtekening',
|
||||||
@@ -717,6 +772,11 @@ const nl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
||||||
|
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
||||||
|
electronic_signature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||||
|
esignature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||||
text: 'نص',
|
text: 'نص',
|
||||||
signature: 'توقيع',
|
signature: 'توقيع',
|
||||||
initials: 'الاختصارات',
|
initials: 'الاختصارات',
|
||||||
@@ -783,6 +843,11 @@ const ar = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ko = {
|
const ko = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: '터치스크린 장치에서 그린 서명',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: '카메라 앱으로 QR 코드를 스캔하여 모바일에서 양식을 열고 서명을 그리세요',
|
||||||
|
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
||||||
|
electronic_signature_disclosure: '전자 서명 공개',
|
||||||
|
esignature_disclosure: '전자 서명 공개',
|
||||||
text: '텍스트',
|
text: '텍스트',
|
||||||
signature: '서명',
|
signature: '서명',
|
||||||
initials: '이니셜',
|
initials: '이니셜',
|
||||||
|
|||||||
@@ -189,12 +189,10 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale
|
||||||
|
|
||||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.textInput?.focus()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -210,7 +208,7 @@ export default {
|
|||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale
|
||||||
|
|
||||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
|
|||||||
@@ -108,10 +108,20 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
verifiedValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
locale: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'en'
|
||||||
|
},
|
||||||
showFieldNames: {
|
showFieldNames: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -158,12 +168,17 @@ export default {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
submitter_slug: this.submitterSlug,
|
||||||
|
locale: this.locale,
|
||||||
phone: this.$refs.phone.value
|
phone: this.$refs.phone.value
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
|
if (this.verifiedValue && this.verifiedValue === this.modelValue) {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.$refs.phone.value.toString().startsWith('+')) {
|
if (!this.$refs.phone.value.toString().startsWith('+')) {
|
||||||
alert(this.t('use_international_format'))
|
alert(this.t('use_international_format'))
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div dir="auto">
|
<div
|
||||||
|
dir="auto"
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="flex justify-between items-center w-full"
|
class="flex justify-between items-center w-full"
|
||||||
:class="{ 'mb-2': !field.description }"
|
:class="{ 'mb-2': !field.description }"
|
||||||
@@ -25,7 +28,7 @@
|
|||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm font-medium"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="[toggleTextInput(), hideQr()]"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature :width="16" />
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
@@ -42,7 +45,7 @@
|
|||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="[toggleTextInput(), hideQr()]"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize :width="16" />
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
@@ -84,7 +87,7 @@
|
|||||||
v-else
|
v-else
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm font-medium"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="clear"
|
@click.prevent="[clear(), hideQr()]"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
@@ -120,12 +123,56 @@
|
|||||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
||||||
>
|
>
|
||||||
<canvas
|
<div class="relative">
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
<div
|
||||||
ref="canvas"
|
v-if="withQrButton"
|
||||||
style="padding: 1px; 0"
|
class="absolute top-1.5 right-1.5 tooltip hidden md:inline"
|
||||||
class="bg-white border border-base-300 rounded-2xl w-full"
|
:data-tip="t('drawn_signature_on_a_touchscreen_device')"
|
||||||
/>
|
>
|
||||||
|
<a
|
||||||
|
v-if="!isShowQr && !isSignatureStarted && !isTextSignature && !modelValue"
|
||||||
|
href="#"
|
||||||
|
class="btn btn-sm btn-circle btn-ghost"
|
||||||
|
@click.prevent="showQr"
|
||||||
|
>
|
||||||
|
<IconQrcode />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<canvas
|
||||||
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
|
ref="canvas"
|
||||||
|
style="padding: 1px; 0"
|
||||||
|
class="bg-white border border-base-300 rounded-2xl w-full"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-show="isShowQr"
|
||||||
|
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute top-1.5 right-1.5 tooltip"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-sm btn-circle btn-normal btn-outline"
|
||||||
|
@click.prevent="hideQr"
|
||||||
|
>
|
||||||
|
<IconX />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center w-full h-full p-4">
|
||||||
|
<div
|
||||||
|
v-if="withQrButton"
|
||||||
|
ref="qr"
|
||||||
|
class="bg-white p-4 rounded-xl h-full"
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref="qrCanvas"
|
||||||
|
class="h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature"
|
||||||
id="signature_text_input"
|
id="signature_text_input"
|
||||||
@@ -136,11 +183,39 @@
|
|||||||
type="text"
|
type="text"
|
||||||
@input="updateWrittenSignature"
|
@input="updateWrittenSignature"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="isShowQr"
|
||||||
|
dir="auto"
|
||||||
|
class="text-base-content/60 text-xs text-center w-full mt-1"
|
||||||
|
>
|
||||||
|
{{ t('scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="withDisclosure"
|
||||||
|
dir="auto"
|
||||||
|
class="text-base-content/60 text-xs text-center w-full mt-1"
|
||||||
|
>
|
||||||
|
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
||||||
|
href="https://www.docuseal.co/esign-disclosure"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="inline md:hidden">
|
||||||
|
{{ t('esignature_disclosure') }}
|
||||||
|
</span>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
{{ t('electronic_signature_disclosure') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="mt-5 md:mt-7"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
@@ -156,7 +231,9 @@ export default {
|
|||||||
AppearsOn,
|
AppearsOn,
|
||||||
IconReload,
|
IconReload,
|
||||||
IconCamera,
|
IconCamera,
|
||||||
|
IconQrcode,
|
||||||
MarkdownContent,
|
MarkdownContent,
|
||||||
|
IconX,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
@@ -176,6 +253,21 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withDisclosure: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withQrButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'Submit'
|
||||||
|
},
|
||||||
withTypedSignature: {
|
withTypedSignature: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -201,6 +293,7 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: !!this.previousValue,
|
isSignatureStarted: !!this.previousValue,
|
||||||
|
isShowQr: false,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
isTextSignature: this.field.preferences?.format === 'typed',
|
isTextSignature: this.field.preferences?.format === 'typed',
|
||||||
uploadImageInputKey: Math.random().toString()
|
uploadImageInputKey: Math.random().toString()
|
||||||
@@ -223,6 +316,18 @@ export default {
|
|||||||
|
|
||||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.withQrButton) {
|
||||||
|
import('qr-creator').then(({ default: Qr }) => {
|
||||||
|
Qr.render({
|
||||||
|
text: `${document.location.origin}/p/${this.submitterSlug}?f=${this.field.uuid.split('-')[0]}`,
|
||||||
|
radius: 0.0,
|
||||||
|
ecLevel: 'H',
|
||||||
|
background: null,
|
||||||
|
size: 132
|
||||||
|
}, this.$refs.qrCanvas)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -252,6 +357,7 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeUnmount () {
|
beforeUnmount () {
|
||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
|
this.stopCheckSignature()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
@@ -273,6 +379,41 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showQr () {
|
||||||
|
this.isShowQr = true
|
||||||
|
|
||||||
|
this.startCheckSignature()
|
||||||
|
},
|
||||||
|
hideQr () {
|
||||||
|
this.isShowQr = false
|
||||||
|
|
||||||
|
this.stopCheckSignature()
|
||||||
|
},
|
||||||
|
startCheckSignature () {
|
||||||
|
const after = JSON.stringify(new Date())
|
||||||
|
|
||||||
|
this.checkSignatureInterval = setInterval(() => {
|
||||||
|
this.checkSignature({ after })
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
stopCheckSignature () {
|
||||||
|
if (this.checkSignatureInterval) {
|
||||||
|
clearInterval(this.checkSignatureInterval)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkSignature (params = {}) {
|
||||||
|
return fetch(this.baseUrl + '/s/' + this.submitterSlug + '/values?field_uuid=' + this.field.uuid + '&after=' + params.after, {
|
||||||
|
method: 'GET'
|
||||||
|
}).then(async (resp) => {
|
||||||
|
const { attachment } = await resp.json()
|
||||||
|
|
||||||
|
if (attachment?.uuid) {
|
||||||
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
this.hideQr()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
clear () {
|
clear () {
|
||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,9 @@
|
|||||||
@keydown.enter.prevent="onNameEnter"
|
@keydown.enter.prevent="onNameEnter"
|
||||||
@focus="onNameFocus"
|
@focus="onNameFocus"
|
||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
>{{ optionIndexText }} {{ field.name || defaultName }}</span>
|
>{{ optionIndexText }} {{ (defaultField ? (field.title || field.name) : field.name) || defaultName }}</span>
|
||||||
<div
|
<div
|
||||||
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
|
v-if="isSettingsFocus || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
||||||
class="flex items-center ml-1.5"
|
class="flex items-center ml-1.5"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -86,6 +86,47 @@
|
|||||||
@click.prevent="field.required = !field.required"
|
@click.prevent="field.required = !field.required"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>{{ t('required') }}</label>
|
>{{ t('required') }}</label>
|
||||||
|
<span
|
||||||
|
v-if="field.type !== 'payment'"
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
ref="settingsButton"
|
||||||
|
tabindex="0"
|
||||||
|
:title="t('settings')"
|
||||||
|
class="cursor-pointer flex items-center"
|
||||||
|
style="height: 25px"
|
||||||
|
@focus="isSettingsFocus = true"
|
||||||
|
@blur="maybeBlurSettings"
|
||||||
|
>
|
||||||
|
<IconDotsVertical class="w-5 h-5" />
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
v-if="renderDropdown"
|
||||||
|
ref="settingsDropdown"
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu menu-xs px-2 pb-2 pt-1 shadow rounded-box w-52 z-10 rounded-t-none"
|
||||||
|
:style="{ backgroundColor: 'white' }"
|
||||||
|
@dragstart.prevent.stop
|
||||||
|
@click="closeDropdown"
|
||||||
|
@focusout="maybeBlurSettings"
|
||||||
|
>
|
||||||
|
<FieldSettings
|
||||||
|
:field="field"
|
||||||
|
:default-field="defaultField"
|
||||||
|
:editable="editable"
|
||||||
|
:background-color="'white'"
|
||||||
|
:with-required="false"
|
||||||
|
:with-areas="false"
|
||||||
|
@click-formula="isShowFormulaModal = true"
|
||||||
|
@click-description="isShowDescriptionModal = true"
|
||||||
|
@click-condition="isShowConditionsModal = true"
|
||||||
|
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else-if="editable"
|
v-else-if="editable"
|
||||||
@@ -120,6 +161,10 @@
|
|||||||
class="aspect-square mx-auto"
|
class="aspect-square mx-auto"
|
||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
v-else-if="field.type === 'number'"
|
||||||
|
class="whitespace-pre-wrap"
|
||||||
|
>{{ formatNumber(field.default_value, field.preferences?.format) }}</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
@@ -145,6 +190,38 @@
|
|||||||
@mousedown.stop="startResize"
|
@mousedown.stop="startResize"
|
||||||
@touchstart="startTouchResize"
|
@touchstart="startTouchResize"
|
||||||
/>
|
/>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFormulaModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FormulaModal
|
||||||
|
:field="field"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="isShowFormulaModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowConditionsModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<ConditionsModal
|
||||||
|
:field="field"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="isShowConditionsModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowDescriptionModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<DescriptionModal
|
||||||
|
:field="field"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="isShowDescriptionModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -152,7 +229,11 @@
|
|||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import { IconX, IconCheck } from '@tabler/icons-vue'
|
import FieldSettings from './field_settings'
|
||||||
|
import FormulaModal from './formula_modal'
|
||||||
|
import ConditionsModal from './conditions_modal'
|
||||||
|
import DescriptionModal from './description_modal'
|
||||||
|
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -160,6 +241,11 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
FieldType,
|
FieldType,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
|
FieldSettings,
|
||||||
|
FormulaModal,
|
||||||
|
IconDotsVertical,
|
||||||
|
DescriptionModal,
|
||||||
|
ConditionsModal,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
@@ -195,11 +281,16 @@ export default {
|
|||||||
default: null
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove'],
|
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
isShowFormulaModal: false,
|
||||||
|
isShowConditionsModal: false,
|
||||||
|
isSettingsFocus: false,
|
||||||
|
isShowDescriptionModal: false,
|
||||||
isResize: false,
|
isResize: false,
|
||||||
isDragged: false,
|
isDragged: false,
|
||||||
|
renderDropdown: false,
|
||||||
isNameFocus: false,
|
isNameFocus: false,
|
||||||
textOverflowChars: 0,
|
textOverflowChars: 0,
|
||||||
dragFrom: { x: 0, y: 0 }
|
dragFrom: { x: 0, y: 0 }
|
||||||
@@ -208,6 +299,9 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
return this.buildDefaultName(this.field, this.template.fields)
|
return this.buildDefaultName(this.field, this.template.fields)
|
||||||
},
|
},
|
||||||
@@ -324,6 +418,25 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
buildDefaultName: Field.methods.buildDefaultName,
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
},
|
||||||
|
formatNumber (number, format) {
|
||||||
|
if (format === 'comma') {
|
||||||
|
return new Intl.NumberFormat('en-US').format(number)
|
||||||
|
} else if (format === 'dot') {
|
||||||
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
|
} else if (format === 'space') {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(number)
|
||||||
|
} else {
|
||||||
|
return number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeBlurSettings (e) {
|
||||||
|
if (!e.relatedTarget || !this.$refs.settingsDropdown.contains(e.relatedTarget)) {
|
||||||
|
this.isSettingsFocus = false
|
||||||
|
}
|
||||||
|
},
|
||||||
onNameFocus (e) {
|
onNameFocus (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
@@ -379,6 +492,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onNameBlur (e) {
|
onNameBlur (e) {
|
||||||
|
if (e.relatedTarget === this.$refs.settingsButton) {
|
||||||
|
this.isSettingsFocus = true
|
||||||
|
}
|
||||||
|
|
||||||
const text = this.$refs.name.innerText.trim()
|
const text = this.$refs.name.innerText.trim()
|
||||||
|
|
||||||
this.isNameFocus = false
|
this.isNameFocus = false
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||||
:target="template.submitters.length > 1 ? '' : '_blank'"
|
:target="template.submitters.length > 1 ? '' : '_blank'"
|
||||||
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
|
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
|
||||||
@click="maybeShowEmptyTemplateAlert"
|
@click="maybeShowErrorTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconWritingSign
|
<IconWritingSign
|
||||||
width="22"
|
width="22"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
:href="`/templates/${template.id}/submissions/new?with_link=true`"
|
:href="`/templates/${template.id}/submissions/new?with_link=true`"
|
||||||
data-turbo-frame="modal"
|
data-turbo-frame="modal"
|
||||||
class="white-button md:!px-6"
|
class="white-button md:!px-6"
|
||||||
@click="maybeShowEmptyTemplateAlert"
|
@click="maybeShowErrorTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconUsersPlus
|
<IconUsersPlus
|
||||||
width="20"
|
width="20"
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:document="document"
|
:document="document"
|
||||||
:is-drag="!!dragField"
|
:is-drag="!!dragField"
|
||||||
:default-fields="defaultFields"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
:allow-draw="!onlyDefinedFields"
|
:allow-draw="!onlyDefinedFields"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
@@ -242,7 +242,8 @@
|
|||||||
:with-help="withHelp"
|
:with-help="withHelp"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:draw-field-type="drawFieldType"
|
:draw-field-type="drawFieldType"
|
||||||
:default-fields="defaultFields"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
|
:default-required-fields="defaultRequiredFields"
|
||||||
:field-types="fieldTypes"
|
:field-types="fieldTypes"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
:only-defined-fields="onlyDefinedFields"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
@@ -273,7 +274,8 @@
|
|||||||
<MobileFields
|
<MobileFields
|
||||||
v-if="sortedDocuments.length && !drawField && editable"
|
v-if="sortedDocuments.length && !drawField && editable"
|
||||||
:fields="template.fields"
|
:fields="template.fields"
|
||||||
:default-fields="defaultFields"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
|
:default-required-fields="defaultRequiredFields"
|
||||||
:field-types="fieldTypes"
|
:field-types="fieldTypes"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
@select="startFieldDraw($event)"
|
@select="startFieldDraw($event)"
|
||||||
@@ -371,6 +373,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defaultRequiredFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
withSelectedFieldType: {
|
withSelectedFieldType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -425,6 +432,13 @@ export default {
|
|||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onChange: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -522,6 +536,11 @@ export default {
|
|||||||
|
|
||||||
return areas
|
return areas
|
||||||
},
|
},
|
||||||
|
isAllRequiredFieldsAdded () {
|
||||||
|
return !this.defaultRequiredFields?.some((f) => {
|
||||||
|
return !this.template.fields?.some((field) => field.name === f.name)
|
||||||
|
})
|
||||||
|
},
|
||||||
selectedField () {
|
selectedField () {
|
||||||
return this.template.fields.find((f) => f.areas?.includes(this.selectedAreaRef.value))
|
return this.template.fields.find((f) => f.areas?.includes(this.selectedAreaRef.value))
|
||||||
},
|
},
|
||||||
@@ -838,7 +857,7 @@ export default {
|
|||||||
type = this.fieldTypes[0]
|
type = this.fieldTypes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'checkbox' && !this.drawFieldType) {
|
if (type === 'checkbox' && !this.drawFieldType && (this.template.fields[this.template.fields.length - 1]?.type === 'checkbox' || area.w)) {
|
||||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
||||||
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
||||||
|
|
||||||
@@ -1093,7 +1112,19 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
maybeShowEmptyTemplateAlert (e) {
|
maybeShowErrorTemplateAlert (e) {
|
||||||
|
if (!this.isAllRequiredFieldsAdded) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const fields = this.defaultRequiredFields?.filter((f) => {
|
||||||
|
return !this.template.fields?.some((field) => field.name === f.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (fields?.length) {
|
||||||
|
return alert(this.t('add_all_required_fields_to_continue') + ': ' + fields.map((f) => f.name).join(', '))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.template.fields.length) {
|
if (!this.template.fields.length) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -1101,6 +1132,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSaveClick () {
|
onSaveClick () {
|
||||||
|
if (!this.isAllRequiredFieldsAdded) {
|
||||||
|
const fields = this.defaultRequiredFields?.filter((f) => {
|
||||||
|
return !this.template.fields?.some((field) => field.name === f.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (fields?.length) {
|
||||||
|
return alert(this.t('add_all_required_fields_to_continue') + ': ' + fields.map((f) => f.name).join(', '))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.template.fields.length) {
|
if (this.template.fields.length) {
|
||||||
this.isSaving = true
|
this.isSaving = true
|
||||||
|
|
||||||
@@ -1131,6 +1172,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
save ({ force } = { force: false }) {
|
save ({ force } = { force: false }) {
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.autosave && !force) {
|
if (!this.autosave && !force) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,7 @@
|
|||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
>
|
>
|
||||||
<template
|
{{ value }}
|
||||||
v-for="(item, index) in (value || '').split(/(_)/)"
|
|
||||||
:key="index"
|
|
||||||
>{{ item }}<wbr></template>
|
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="withRequired"
|
v-if="withRequired"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
v-model="description"
|
v-model="description"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="base-textarea !text-base w-full"
|
class="base-textarea !text-base w-full"
|
||||||
|
:readonly="!editable"
|
||||||
@input="resizeTextarea"
|
@input="resizeTextarea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
id="title_field"
|
id="title_field"
|
||||||
v-model="title"
|
v-model="title"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
:readonly="!editable"
|
||||||
class="base-input !text-base w-full"
|
class="base-input !text-base w-full"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,6 +75,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
buildDefaultName: {
|
buildDefaultName: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
:image="image"
|
:image="image"
|
||||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||||
@remove-area="$emit('remove-area', $event)"
|
@remove-area="$emit('remove-area', $event)"
|
||||||
|
@scroll-to="scrollToArea"
|
||||||
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
|
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,7 +126,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
scrollToArea (area) {
|
scrollToArea (area) {
|
||||||
this.pageRefs[area.page].areaRefs.find((e) => e.area === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
this.$nextTick(() => {
|
||||||
|
this.pageRefs[area.page].areaRefs.find((e) => e.area === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
})
|
||||||
},
|
},
|
||||||
setPageRefs (el) {
|
setPageRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|||||||
@@ -16,12 +16,14 @@
|
|||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:button-width="20"
|
:button-width="20"
|
||||||
|
:menu-classes="'mt-1.5'"
|
||||||
|
:menu-style="{ backgroundColor: dropdownBgColor }"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@click="scrollToFirstArea"
|
@click="scrollToFirstArea"
|
||||||
/>
|
/>
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
ref="name"
|
ref="name"
|
||||||
:model-value="field.name || defaultName"
|
:model-value="(defaultField ? (field.title || field.name) : field.name) || defaultName"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:icon-inline="true"
|
:icon-inline="true"
|
||||||
:icon-width="18"
|
:icon-width="18"
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
class="flex items-center space-x-1"
|
class="flex items-center space-x-1"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="field && !field.areas.length"
|
v-if="field && !field.areas?.length"
|
||||||
:title="t('draw')"
|
:title="t('draw')"
|
||||||
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
@click="$emit('set-draw', { field })"
|
@click="$emit('set-draw', { field })"
|
||||||
@@ -110,284 +112,23 @@
|
|||||||
<ul
|
<ul
|
||||||
v-if="renderDropdown"
|
v-if="renderDropdown"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart.prevent.stop
|
@dragstart.prevent.stop
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<div
|
<FieldSettings
|
||||||
v-if="['number'].includes(field.type)"
|
:field="field"
|
||||||
class="py-1.5 px-1 relative"
|
:default-field="defaultField"
|
||||||
@click.stop
|
:editable="editable"
|
||||||
>
|
:background-color="dropdownBgColor"
|
||||||
<select
|
@click-formula="isShowFormulaModal = true"
|
||||||
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal"
|
@click-description="isShowDescriptionModal = true"
|
||||||
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
|
@click-condition="isShowConditionsModal = true"
|
||||||
>
|
@set-draw="$emit('set-draw', $event)"
|
||||||
<option
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
v-for="value in ['left', 'right', 'center']"
|
/>
|
||||||
:key="value"
|
|
||||||
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
|
|
||||||
:value="value"
|
|
||||||
>
|
|
||||||
{{ t(value) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label
|
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
|
||||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
|
||||||
style="font-size: 8px"
|
|
||||||
>
|
|
||||||
{{ t('align') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
|
||||||
class="py-1.5 px-1 relative"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-model="field.default_value"
|
|
||||||
:placeholder="t('default_value')"
|
|
||||||
dir="auto"
|
|
||||||
:type="field.type"
|
|
||||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
|
||||||
@blur="save"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
v-if="field.default_value"
|
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
|
||||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
|
||||||
style="font-size: 8px"
|
|
||||||
>
|
|
||||||
{{ t('default_value') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="field.type === 'date'"
|
|
||||||
class="py-1.5 px-1 relative"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
v-model="field.preferences.format"
|
|
||||||
:placeholder="t('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"
|
|
||||||
>
|
|
||||||
{{ t('format') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="field.type === 'signature'"
|
|
||||||
class="py-1.5 px-1 relative"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
:placeholder="t('format')"
|
|
||||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
|
|
||||||
@change="[field.preferences.format = $event.target.value, save()]"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
value="any"
|
|
||||||
:selected="!field.preferences?.format || field.preferences.format === 'any'"
|
|
||||||
>
|
|
||||||
{{ t('any') }}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="drawn"
|
|
||||||
:selected="field.preferences?.format === 'drawn'"
|
|
||||||
>
|
|
||||||
{{ t('drawn') }}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="typed"
|
|
||||||
:selected="field.preferences?.format === 'typed'"
|
|
||||||
>
|
|
||||||
{{ t('typed') }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label
|
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
|
||||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
|
||||||
style="font-size: 8px"
|
|
||||||
>
|
|
||||||
{{ t('format') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<li
|
|
||||||
v-if="field.type != 'phone' && field.type != 'stamp'"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<label class="cursor-pointer py-1.5">
|
|
||||||
<input
|
|
||||||
v-model="field.required"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@update:model-value="save"
|
|
||||||
>
|
|
||||||
<span class="label-text">{{ t('required') }}</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-if="field.type == 'stamp'"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<label class="cursor-pointer py-1.5">
|
|
||||||
<input
|
|
||||||
:checked="field.preferences?.with_logo != false"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, save()]"
|
|
||||||
>
|
|
||||||
<span class="label-text">{{ t('with_logo') }}</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-if="field.type == 'checkbox'"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<label class="cursor-pointer py-1.5">
|
|
||||||
<input
|
|
||||||
v-model="field.default_value"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]"
|
|
||||||
>
|
|
||||||
<span class="label-text">{{ t('checked') }}</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-if="field.type == 'date'"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<label class="cursor-pointer py-1.5">
|
|
||||||
<input
|
|
||||||
v-model="field.readonly"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]"
|
|
||||||
>
|
|
||||||
<span class="label-text">{{ t('set_signing_date') }}</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<label class="cursor-pointer py-1.5">
|
|
||||||
<input
|
|
||||||
v-model="field.readonly"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@update:model-value="save"
|
|
||||||
>
|
|
||||||
<span class="label-text">{{ t('read_only') }}</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<hr
|
|
||||||
v-if="field.type != 'stamp'"
|
|
||||||
class="pb-0.5 mt-0.5"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-if="field.type != 'stamp'"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
|
||||||
@click="isShowDescriptionModal = !isShowDescriptionModal"
|
|
||||||
>
|
|
||||||
<IconInfoCircle
|
|
||||||
width="18"
|
|
||||||
/>
|
|
||||||
<span class="text-sm">
|
|
||||||
{{ t('description') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-if="field.type != 'stamp'"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
|
||||||
@click="isShowConditionsModal = !isShowConditionsModal"
|
|
||||||
>
|
|
||||||
<IconRouteAltLeft
|
|
||||||
width="18"
|
|
||||||
/>
|
|
||||||
<span class="text-sm">
|
|
||||||
{{ t('condition') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li v-if="field.type == 'number'">
|
|
||||||
<label
|
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
|
||||||
@click="isShowFormulaModal = true"
|
|
||||||
>
|
|
||||||
<IconMathFunction
|
|
||||||
width="18"
|
|
||||||
/>
|
|
||||||
<span class="text-sm">
|
|
||||||
{{ t('formula') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<hr class="pb-0.5 mt-0.5">
|
|
||||||
<li
|
|
||||||
v-for="(area, index) in sortedAreas"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="text-sm py-1 px-2"
|
|
||||||
@click.prevent="$emit('scroll-to', area)"
|
|
||||||
>
|
|
||||||
<IconShape
|
|
||||||
:width="20"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
{{ t('page') }}
|
|
||||||
<template v-if="template.schema.length > 1">{{ template.schema.findIndex((item) => item.attachment_uuid === area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="text-sm py-1 px-2"
|
|
||||||
@click.prevent="$emit('set-draw', { field })"
|
|
||||||
>
|
|
||||||
<IconNewSection
|
|
||||||
:width="20"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
{{ t('draw_new_area') }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="text-sm py-1 px-2"
|
|
||||||
@click.prevent="copyToAllPages(field)"
|
|
||||||
>
|
|
||||||
<IconCopy
|
|
||||||
:width="20"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
{{ t('copy_to_all_pages') }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -481,6 +222,7 @@
|
|||||||
>
|
>
|
||||||
<FormulaModal
|
<FormulaModal
|
||||||
:field="field"
|
:field="field"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -501,6 +243,7 @@
|
|||||||
>
|
>
|
||||||
<DescriptionModal
|
<DescriptionModal
|
||||||
:field="field"
|
:field="field"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -512,10 +255,11 @@
|
|||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import PaymentSettings from './payment_settings'
|
import PaymentSettings from './payment_settings'
|
||||||
|
import FieldSettings from './field_settings'
|
||||||
import FormulaModal from './formula_modal'
|
import FormulaModal from './formula_modal'
|
||||||
import ConditionsModal from './conditions_modal'
|
import ConditionsModal from './conditions_modal'
|
||||||
import DescriptionModal from './description_modal'
|
import DescriptionModal from './description_modal'
|
||||||
import { IconInfoCircle, IconRouteAltLeft, IconMathFunction, IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -523,17 +267,15 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconShape,
|
FieldSettings,
|
||||||
PaymentSettings,
|
PaymentSettings,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
IconInfoCircle,
|
|
||||||
FormulaModal,
|
FormulaModal,
|
||||||
DescriptionModal,
|
DescriptionModal,
|
||||||
ConditionsModal,
|
ConditionsModal,
|
||||||
IconRouteAltLeft,
|
IconRouteAltLeft,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
IconMathFunction,
|
IconMathFunction,
|
||||||
IconCopy,
|
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
|
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
|
||||||
@@ -566,6 +308,9 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
dropdownBgColor () {
|
||||||
|
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
|
||||||
|
},
|
||||||
schemaAttachmentsIndexes () {
|
schemaAttachmentsIndexes () {
|
||||||
return (this.template.schema || []).reduce((acc, item, index) => {
|
return (this.template.schema || []).reduce((acc, item, index) => {
|
||||||
acc[item.attachment_uuid] = index
|
acc[item.attachment_uuid] = index
|
||||||
@@ -581,29 +326,6 @@ export default {
|
|||||||
modalContainerEl () {
|
modalContainerEl () {
|
||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
},
|
},
|
||||||
dateFormats () {
|
|
||||||
const formats = [
|
|
||||||
'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'
|
|
||||||
]
|
|
||||||
|
|
||||||
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
|
|
||||||
formats.push('YYYY년 MM월 DD일')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.field.preferences?.format && !formats.includes(this.field.preferences.format)) {
|
|
||||||
formats.unshift(this.field.preferences.format)
|
|
||||||
}
|
|
||||||
|
|
||||||
return formats
|
|
||||||
},
|
|
||||||
defaultName () {
|
defaultName () {
|
||||||
return this.buildDefaultName(this.field, this.template.fields)
|
return this.buildDefaultName(this.field, this.template.fields)
|
||||||
},
|
},
|
||||||
@@ -638,54 +360,6 @@ export default {
|
|||||||
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
|
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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) {
|
|
||||||
const areaString = JSON.stringify(field.areas[0])
|
|
||||||
|
|
||||||
this.template.documents.forEach((attachment) => {
|
|
||||||
const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length
|
|
||||||
|
|
||||||
for (let page = 0; page <= numberOfPages - 1; page++) {
|
|
||||||
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === page)) {
|
|
||||||
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
|
|
||||||
})
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
onNameFocus (e) {
|
onNameFocus (e) {
|
||||||
this.isNameFocus = true
|
this.isNameFocus = true
|
||||||
|
|
||||||
@@ -761,11 +435,6 @@ export default {
|
|||||||
|
|
||||||
this.isNameFocus = false
|
this.isNameFocus = false
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
removeArea (area) {
|
|
||||||
this.field.areas.splice(this.field.areas.indexOf(area), 1)
|
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,462 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'number'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
:placeholder="t('format')"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in numberFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
|
||||||
|
>
|
||||||
|
{{ formatNumber(123456789.567, format) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('format') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['number'].includes(field.type)"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="value in ['left', 'right', 'center']"
|
||||||
|
:key="value"
|
||||||
|
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
|
||||||
|
:value="value"
|
||||||
|
>
|
||||||
|
{{ t(value) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('align') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="field.default_value"
|
||||||
|
:placeholder="t('default_value')"
|
||||||
|
dir="auto"
|
||||||
|
:type="field.type"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.default_value"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('default_value') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'date'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="field.preferences.format"
|
||||||
|
:placeholder="t('format')"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="save"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in dateFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
>
|
||||||
|
{{ formatDate(new Date(), format) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('format') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'signature'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
:placeholder="t('format')"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="[field.preferences.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="any"
|
||||||
|
:selected="!field.preferences?.format || field.preferences.format === 'any'"
|
||||||
|
>
|
||||||
|
{{ t('any') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="drawn"
|
||||||
|
:selected="field.preferences?.format === 'drawn'"
|
||||||
|
>
|
||||||
|
{{ t('drawn') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="typed"
|
||||||
|
:selected="field.preferences?.format === 'typed'"
|
||||||
|
>
|
||||||
|
{{ t('typed') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('format') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="withRequired && field.type != 'phone' && field.type != 'stamp'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.required"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="!editable || defaultField"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="save"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('required') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type == 'stamp'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="field.preferences?.with_logo != false"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('with_logo') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type == 'checkbox'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.default_value"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('checked') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type == 'date'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.readonly"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('set_signing_date') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.readonly"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="save"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('read_only') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<hr
|
||||||
|
v-if="field.type != 'stamp'"
|
||||||
|
class="pb-0.5 mt-0.5"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-if="field.type != 'stamp'"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
|
@click="$emit('click-description')"
|
||||||
|
>
|
||||||
|
<IconInfoCircle
|
||||||
|
width="18"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ t('description') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type != 'stamp'"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
|
@click="$emit('click-condition')"
|
||||||
|
>
|
||||||
|
<IconRouteAltLeft
|
||||||
|
width="18"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ t('condition') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li v-if="field.type == 'number'">
|
||||||
|
<label
|
||||||
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
|
@click="$emit('click-formula')"
|
||||||
|
>
|
||||||
|
<IconMathFunction
|
||||||
|
width="18"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ t('formula') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<hr class="pb-0.5 mt-0.5">
|
||||||
|
<template v-if="withAreas">
|
||||||
|
<li
|
||||||
|
v-for="(area, index) in sortedAreas"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
@click.prevent="$emit('scroll-to', area)"
|
||||||
|
>
|
||||||
|
<IconShape
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
{{ t('page') }}
|
||||||
|
<template v-if="template.schema.length > 1">{{ template.schema.findIndex((item) => item.attachment_uuid === area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
@click.prevent="$emit('set-draw', { field })"
|
||||||
|
>
|
||||||
|
<IconNewSection
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
{{ t('draw_new_area') }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
@click.prevent="copyToAllPages(field)"
|
||||||
|
>
|
||||||
|
<IconCopy
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
{{ t('copy_to_all_pages') }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconRouteAltLeft, IconShape, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FieldSettings',
|
||||||
|
components: {
|
||||||
|
IconShape,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconMathFunction,
|
||||||
|
IconRouteAltLeft,
|
||||||
|
IconCopy,
|
||||||
|
IconNewSection
|
||||||
|
},
|
||||||
|
inject: ['template', 'save', 't'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
withRequired: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withAreas: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
schemaAttachmentsIndexes () {
|
||||||
|
return (this.template.schema || []).reduce((acc, item, index) => {
|
||||||
|
acc[item.attachment_uuid] = index
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
numberFormats () {
|
||||||
|
return [
|
||||||
|
'none',
|
||||||
|
'comma',
|
||||||
|
'dot',
|
||||||
|
'space'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
dateFormats () {
|
||||||
|
const formats = [
|
||||||
|
'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'
|
||||||
|
]
|
||||||
|
|
||||||
|
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
|
||||||
|
formats.push('YYYY년 MM월 DD일')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.field.preferences?.format && !formats.includes(this.field.preferences.format)) {
|
||||||
|
formats.unshift(this.field.preferences.format)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formats
|
||||||
|
},
|
||||||
|
sortedAreas () {
|
||||||
|
return (this.field.areas || []).sort((a, b) => {
|
||||||
|
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copyToAllPages (field) {
|
||||||
|
const areaString = JSON.stringify(field.areas[0])
|
||||||
|
|
||||||
|
this.template.documents.forEach((attachment) => {
|
||||||
|
const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length
|
||||||
|
|
||||||
|
for (let page = 0; page <= numberOfPages - 1; page++) {
|
||||||
|
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === page)) {
|
||||||
|
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
formatNumber (number, format) {
|
||||||
|
if (format === 'comma') {
|
||||||
|
return new Intl.NumberFormat('en-US').format(number)
|
||||||
|
} else if (format === 'dot') {
|
||||||
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
|
} else if (format === 'space') {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(number)
|
||||||
|
} else {
|
||||||
|
return number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="rounded-md min-w-max mb-2"
|
class="rounded-md min-w-max mb-2"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
|
:style="menuStyle"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
@@ -136,6 +137,7 @@
|
|||||||
v-if="(editable || !compact) && renderDropdown"
|
v-if="(editable || !compact) && renderDropdown"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
|
:style="menuStyle"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
@@ -253,10 +255,15 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
menuStyle: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
menuClasses: {
|
menuClasses: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'dropdown-content menu p-2 shadow bg-base-100 rounded-box w-full z-10'
|
default: 'dropdown-content menu p-2 shadow rounded-box w-full z-10'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'],
|
emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'],
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
v-if="editable && renderDropdown"
|
v-if="editable && renderDropdown"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
||||||
|
:style="menuStyle"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
@@ -65,6 +66,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: 'mt-1.5 bg-base-100'
|
default: 'mt-1.5 bg-base-100'
|
||||||
},
|
},
|
||||||
|
menuStyle: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
buttonClasses: {
|
buttonClasses: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
:model-value="selectedSubmitter.uuid"
|
:model-value="selectedSubmitter.uuid"
|
||||||
class="roles-dropdown w-full rounded-lg"
|
class="roles-dropdown w-full rounded-lg"
|
||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:style="withStickySubmitters ? { backgroundColor } : {}"
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
|
:menu-style="{ backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
|
||||||
:editable="editable && !defaultSubmitters.length"
|
:editable="editable && !defaultSubmitters.length"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="submitterDefaultFields.length">
|
<div v-if="submitterDefaultFields.length && editable">
|
||||||
<hr class="mb-2">
|
<hr class="mb-2">
|
||||||
<template v-if="isShowFieldSearch">
|
<template v-if="isShowFieldSearch">
|
||||||
<input
|
<input
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
:key="field.name"
|
:key="field.name"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
|
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
|
||||||
@dragstart="onDragstart({ type: 'text', ...field })"
|
@dragstart="onDragstart({ type: 'text', ...field })"
|
||||||
@@ -86,9 +87,16 @@
|
|||||||
:button-width="20"
|
:button-width="20"
|
||||||
/>
|
/>
|
||||||
<span class="block pl-0.5">
|
<span class="block pl-0.5">
|
||||||
{{ field.name }}
|
{{ field.title || field.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="defaultRequiredFields.includes(field)"
|
||||||
|
:data-tip="t('required')"
|
||||||
|
class="text-red-400 text-3xl pr-1.5 tooltip tooltip-left h-8"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -106,7 +114,7 @@
|
|||||||
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
|
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
||||||
@dragstart="onDragstart({ type: type })"
|
@dragstart="onDragstart({ type: type })"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@@ -135,7 +143,7 @@
|
|||||||
href="https://www.docuseal.co/pricing"
|
href="https://www.docuseal.co/pricing"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="w-0 absolute left-0">
|
<div class="w-0 absolute left-0">
|
||||||
<IconLock
|
<IconLock
|
||||||
@@ -210,6 +218,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defaultRequiredFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
onlyDefinedFields: {
|
onlyDefinedFields: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
ref="textarea"
|
ref="textarea"
|
||||||
v-model="formula"
|
v-model="formula"
|
||||||
class="base-textarea !rounded-xl !text-base font-mono w-full !outline-0 !ring-0 !px-3"
|
class="base-textarea !rounded-xl !text-base font-mono w-full !outline-0 !ring-0 !px-3"
|
||||||
|
:readonly="!editable"
|
||||||
required="true"
|
required="true"
|
||||||
@input="resizeTextarea"
|
@input="resizeTextarea"
|
||||||
/>
|
/>
|
||||||
@@ -134,6 +135,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
buildDefaultName: {
|
buildDefaultName: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const en = {
|
|||||||
field_not_found: 'Field not found',
|
field_not_found: 'Field not found',
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
align: 'Align',
|
align: 'Align',
|
||||||
|
add_all_required_fields_to_continue: 'Add all required fields to continue',
|
||||||
left: 'Left',
|
left: 'Left',
|
||||||
right: 'Right',
|
right: 'Right',
|
||||||
center: 'Center',
|
center: 'Center',
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:mobile-view="true"
|
:mobile-view="true"
|
||||||
|
:menu-style="{ backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@name-change="save"
|
@name-change="save"
|
||||||
@@ -43,7 +44,7 @@ export default {
|
|||||||
Field,
|
Field,
|
||||||
FieldSubmitter
|
FieldSubmitter
|
||||||
},
|
},
|
||||||
inject: ['save', 't'],
|
inject: ['save', 't', 'backgroundColor'],
|
||||||
props: {
|
props: {
|
||||||
drawField: {
|
drawField: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -38,7 +38,14 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
:width="20"
|
:width="20"
|
||||||
/>
|
/>
|
||||||
{{ field.name }}
|
{{ field.title || field.name }}
|
||||||
|
<span
|
||||||
|
v-if="defaultRequiredFields.includes(field)"
|
||||||
|
:data-tip="t('required')"
|
||||||
|
class="text-red-400 text-2xl tooltip tooltip-left h-6"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -99,6 +106,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defaultRequiredFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
defaultFields: {
|
defaultFields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
@start-drag="isMove = true"
|
@start-drag="isMove = true"
|
||||||
@stop-drag="isMove = false"
|
@stop-drag="isMove = false"
|
||||||
@remove="$emit('remove-area', item.area)"
|
@remove="$emit('remove-area', item.area)"
|
||||||
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
/>
|
/>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
v-if="newArea"
|
v-if="newArea"
|
||||||
@@ -116,7 +117,7 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field', 'remove-area'],
|
emits: ['draw', 'drop-field', 'remove-area', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
areaRefs: [],
|
areaRefs: [],
|
||||||
@@ -219,7 +220,7 @@ export default {
|
|||||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight
|
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.drawField?.type === 'cells') {
|
if ((this.drawField?.type || this.drawFieldType) === 'cells') {
|
||||||
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
enqueue_completed_emails(submitter)
|
enqueue_completed_emails(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
return if Accounts.load_webhook_configs(submitter.account).blank?
|
return if Accounts.load_webhook_url(submitter.account).blank?
|
||||||
|
|
||||||
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
||||||
end
|
end
|
||||||
@@ -34,6 +34,7 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
bcc = submission.preferences['bcc_completed'].presence ||
|
bcc = submission.preferences['bcc_completed'].presence ||
|
||||||
|
submission.template.preferences['bcc_completed'].presence ||
|
||||||
submission.account.account_configs
|
submission.account.account_configs
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
||||||
|
queue_as :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(submitter, params = {})
|
def perform(submitter, params = {})
|
||||||
attempt = params[:attempt].to_i
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
url = Accounts.load_webhook_url(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if url.blank?
|
||||||
|
|
||||||
|
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
||||||
|
|
||||||
|
return if preferences['form.completed'] == false
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(config.value,
|
Faraday.post(url,
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SendFormStartedWebhookRequestJob < ApplicationJob
|
class SendFormStartedWebhookRequestJob < ApplicationJob
|
||||||
|
queue_as :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(submitter, params = {})
|
def perform(submitter, params = {})
|
||||||
attempt = params[:attempt].to_i
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
url = Accounts.load_webhook_url(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if url.blank?
|
||||||
|
|
||||||
|
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
||||||
|
|
||||||
|
return if preferences['form.started'] == false
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(config.value,
|
Faraday.post(url,
|
||||||
{
|
{
|
||||||
event_type: 'form.started',
|
event_type: 'form.started',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SendFormViewedWebhookRequestJob < ApplicationJob
|
class SendFormViewedWebhookRequestJob < ApplicationJob
|
||||||
|
queue_as :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(submitter, params = {})
|
def perform(submitter, params = {})
|
||||||
attempt = params[:attempt].to_i
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
url = Accounts.load_webhook_url(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if url.blank?
|
||||||
|
|
||||||
|
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
||||||
|
|
||||||
|
return if preferences['form.viewed'] == false
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(config.value,
|
Faraday.post(url,
|
||||||
{
|
{
|
||||||
event_type: 'form.viewed',
|
event_type: 'form.viewed',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendSubmissionArchivedWebhookRequestJob < ApplicationJob
|
||||||
|
queue_as :webhooks
|
||||||
|
|
||||||
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(submission, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
|
url = Accounts.load_webhook_url(submission.account)
|
||||||
|
|
||||||
|
return if url.blank?
|
||||||
|
|
||||||
|
preferences = Accounts.load_webhook_preferences(submission.account)
|
||||||
|
|
||||||
|
return if preferences['submission.archived'].blank?
|
||||||
|
|
||||||
|
resp = begin
|
||||||
|
Faraday.post(url,
|
||||||
|
{
|
||||||
|
event_type: 'submission.archived',
|
||||||
|
timestamp: Time.current,
|
||||||
|
data: submission.as_json(only: %i[id archived_at])
|
||||||
|
}.to_json,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
rescue Faraday::Error
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||||
|
SendSubmissionArchivedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submission, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp&.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendSubmissionCreatedWebhookRequestJob < ApplicationJob
|
||||||
|
queue_as :webhooks
|
||||||
|
|
||||||
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(submission, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
|
url = Accounts.load_webhook_url(submission.account)
|
||||||
|
|
||||||
|
return if url.blank?
|
||||||
|
|
||||||
|
preferences = Accounts.load_webhook_preferences(submission.account)
|
||||||
|
|
||||||
|
return if preferences['submission.created'].blank?
|
||||||
|
|
||||||
|
resp = begin
|
||||||
|
Faraday.post(url,
|
||||||
|
{
|
||||||
|
event_type: 'submission.created',
|
||||||
|
timestamp: Time.current,
|
||||||
|
data: Submissions::SerializeForApi.call(submission)
|
||||||
|
}.to_json,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
rescue Faraday::Error
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submission, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp&.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,7 +4,7 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
|
|||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account)
|
if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
|
||||||
Rollbar.error("Skip email: #{submitter.id}") if defined?(Rollbar)
|
Rollbar.error("Skip email: #{submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendTemplateCreatedWebhookRequestJob < ApplicationJob
|
||||||
|
queue_as :webhooks
|
||||||
|
|
||||||
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(template, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
|
url = Accounts.load_webhook_url(template.account)
|
||||||
|
|
||||||
|
return if url.blank?
|
||||||
|
|
||||||
|
preferences = Accounts.load_webhook_preferences(template.account)
|
||||||
|
|
||||||
|
return if preferences['template.created'].blank?
|
||||||
|
|
||||||
|
resp = begin
|
||||||
|
Faraday.post(url,
|
||||||
|
{
|
||||||
|
event_type: 'template.created',
|
||||||
|
timestamp: Time.current,
|
||||||
|
data: Templates::SerializeForApi.call(template)
|
||||||
|
}.to_json,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
rescue Faraday::Error
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||||
|
SendTemplateCreatedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(template, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp&.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendTemplateUpdatedWebhookRequestJob < ApplicationJob
|
||||||
|
queue_as :webhooks
|
||||||
|
|
||||||
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(template, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
|
url = Accounts.load_webhook_url(template.account)
|
||||||
|
|
||||||
|
return if url.blank?
|
||||||
|
|
||||||
|
preferences = Accounts.load_webhook_preferences(template.account)
|
||||||
|
|
||||||
|
return if preferences['template.updated'].blank?
|
||||||
|
|
||||||
|
resp = begin
|
||||||
|
Faraday.post(url,
|
||||||
|
{
|
||||||
|
event_type: 'template.updated',
|
||||||
|
timestamp: Time.current,
|
||||||
|
data: Templates::SerializeForApi.call(template)
|
||||||
|
}.to_json,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
rescue Faraday::Error
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(template, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp&.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
default from: 'DocuSeal <hi@docuseal.co>'
|
default from: 'DocuSeal <info@docuseal.co>'
|
||||||
layout 'mailer'
|
layout 'mailer'
|
||||||
|
|
||||||
register_interceptor ActionMailerConfigsInterceptor
|
register_interceptor ActionMailerConfigsInterceptor
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
|
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
|
||||||
end
|
end
|
||||||
|
|
||||||
@body = @email_message&.body.presence
|
@body = @email_message&.body.presence || @submitter.template.preferences['request_email_body'].presence
|
||||||
@subject = @email_message&.subject.presence
|
@subject = @email_message&.subject.presence || @submitter.template.preferences['request_email_subject'].presence
|
||||||
|
|
||||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||||
|
|
||||||
@@ -76,9 +76,15 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
|
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@subject = @submitter.template.preferences['documents_copy_email_subject'].presence
|
||||||
|
@subject ||= @email_config.value['subject'] if @email_config
|
||||||
|
|
||||||
|
@body = @submitter.template.preferences['documents_copy_email_body'].presence
|
||||||
|
@body ||= @email_config.value['body'] if @email_config
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config
|
if @subject.present?
|
||||||
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(@subject, submitter:)
|
||||||
else
|
else
|
||||||
'Your document copy'
|
'Your document copy'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
#
|
#
|
||||||
# Table name: accounts
|
# Table name: accounts
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# locale :string not null
|
# archived_at :datetime
|
||||||
# name :string not null
|
# locale :string not null
|
||||||
# timezone :string not null
|
# name :string not null
|
||||||
# uuid :string not null
|
# timezone :string not null
|
||||||
# created_at :datetime not null
|
# uuid :string not null
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
@@ -49,6 +50,8 @@ class Account < ApplicationRecord
|
|||||||
attribute :timezone, :string, default: 'UTC'
|
attribute :timezone, :string, default: 'UTC'
|
||||||
attribute :locale, :string, default: 'en-US'
|
attribute :locale, :string, default: 'en-US'
|
||||||
|
|
||||||
|
scope :active, -> { where(archived_at: nil) }
|
||||||
|
|
||||||
def testing?
|
def testing?
|
||||||
linked_account_account&.testing?
|
linked_account_account&.testing?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,11 +28,15 @@ class AccountConfig < ApplicationRecord
|
|||||||
FORCE_MFA = 'force_mfa'
|
FORCE_MFA = 'force_mfa'
|
||||||
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
||||||
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
||||||
SUBMITTER_REMAILERS = 'submitter_reminders'
|
SUBMITTER_REMINDERS = 'submitter_reminders'
|
||||||
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||||
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
|
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
|
||||||
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
|
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
|
||||||
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
||||||
|
WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
|
||||||
|
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
|
||||||
|
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
|
||||||
|
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
||||||
|
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY => {
|
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class Submission < ApplicationRecord
|
|||||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|
||||||
has_one_attached :audit_trail
|
has_one_attached :audit_trail
|
||||||
|
has_one_attached :combined_document
|
||||||
|
|
||||||
has_many :template_schema_documents,
|
has_many :template_schema_documents,
|
||||||
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
||||||
@@ -63,6 +64,7 @@ class Submission < ApplicationRecord
|
|||||||
|
|
||||||
enum :source, {
|
enum :source, {
|
||||||
invite: 'invite',
|
invite: 'invite',
|
||||||
|
bulk: 'bulk',
|
||||||
api: 'api',
|
api: 'api',
|
||||||
embed: 'embed',
|
embed: 'embed',
|
||||||
link: 'link'
|
link: 'link'
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class Submitter < ApplicationRecord
|
|||||||
has_many :document_generation_events, dependent: :destroy
|
has_many :document_generation_events, dependent: :destroy
|
||||||
has_many :submission_events, dependent: :destroy
|
has_many :submission_events, dependent: :destroy
|
||||||
|
|
||||||
|
scope :completed, -> { where.not(completed_at: nil) }
|
||||||
|
|
||||||
def status
|
def status
|
||||||
if completed_at?
|
if completed_at?
|
||||||
'completed'
|
'completed'
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
# archived_at :datetime
|
# archived_at :datetime
|
||||||
# fields :text not null
|
# fields :text not null
|
||||||
# name :string not null
|
# name :string not null
|
||||||
|
# preferences :text not null
|
||||||
# schema :text not null
|
# schema :text not null
|
||||||
# slug :string not null
|
# slug :string not null
|
||||||
# source :text not null
|
# source :text not null
|
||||||
@@ -41,12 +42,14 @@ class Template < ApplicationRecord
|
|||||||
|
|
||||||
before_validation :maybe_set_default_folder, on: :create
|
before_validation :maybe_set_default_folder, on: :create
|
||||||
|
|
||||||
|
attribute :preferences, :string, default: -> { {} }
|
||||||
attribute :fields, :string, default: -> { [] }
|
attribute :fields, :string, default: -> { [] }
|
||||||
attribute :schema, :string, default: -> { [] }
|
attribute :schema, :string, default: -> { [] }
|
||||||
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
|
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
|
||||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
attribute :source, :string, default: 'native'
|
attribute :source, :string, default: 'native'
|
||||||
|
|
||||||
|
serialize :preferences, coder: JSON
|
||||||
serialize :fields, coder: JSON
|
serialize :fields, coder: JSON
|
||||||
serialize :schema, coder: JSON
|
serialize :schema, coder: JSON
|
||||||
serialize :submitters, coder: JSON
|
serialize :submitters, coder: JSON
|
||||||
@@ -62,6 +65,8 @@ class Template < ApplicationRecord
|
|||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
scope :archived, -> { where.not(archived_at: nil) }
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
|
|
||||||
|
delegate :name, to: :folder, prefix: true
|
||||||
|
|
||||||
def application_key
|
def application_key
|
||||||
external_id
|
external_id
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class User < ApplicationRecord
|
|||||||
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
|
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
|
||||||
|
|
||||||
has_one_attached :signature
|
has_one_attached :signature
|
||||||
|
has_one_attached :initials
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_one :access_token, dependent: :destroy
|
has_one :access_token, dependent: :destroy
|
||||||
@@ -65,6 +66,7 @@ class User < ApplicationRecord
|
|||||||
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
scope :admins, -> { where(role: ADMIN_ROLE) }
|
scope :admins, -> { where(role: ADMIN_ROLE) }
|
||||||
|
|
||||||
def access_token
|
def access_token
|
||||||
@@ -72,7 +74,7 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def active_for_authentication?
|
def active_for_authentication?
|
||||||
super && !archived_at?
|
super && !archived_at? && !account.archived_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
def remember_me
|
def remember_me
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#
|
#
|
||||||
class UserConfig < ApplicationRecord
|
class UserConfig < ApplicationRecord
|
||||||
SIGNATURE_KEY = 'signature'
|
SIGNATURE_KEY = 'signature'
|
||||||
|
INITIALS_KEY = 'initials'
|
||||||
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
|
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="flex-grow max-w-xl mx-auto">
|
<div class="flex-grow max-w-xl mx-auto">
|
||||||
<h1 class="text-4xl font-bold mb-4">Account</h1>
|
<h1 class="text-4xl font-bold mb-4">Account</h1>
|
||||||
<%= form_for '', url: settings_account_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
<%= form_for '', url: settings_account_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||||
<%= f.fields_for current_account do |ff| %>
|
<%= fields_for current_account do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :name, 'Company Name', class: 'label' %>
|
<%= ff.label :name, 'Company Name', class: 'label' %>
|
||||||
<%= ff.text_field :name, required: true, class: 'base-input', dir: 'auto' %>
|
<%= ff.text_field :name, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
|
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
|
||||||
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? %>
|
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? %>
|
||||||
<%= f.fields_for encrypted_config do |ff| %>
|
<%= fields_for encrypted_config do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :value, 'App URL', class: 'label' %>
|
<%= ff.label :value, 'App URL', class: 'label' %>
|
||||||
<%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %>
|
<%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %>
|
||||||
@@ -75,6 +75,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
Require authentication for file download links
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
|
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>">
|
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>">
|
||||||
🌎 Global
|
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
||||||
|
Global
|
||||||
</a>
|
</a>
|
||||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
||||||
🇪🇺 Europe
|
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||||
|
Europe
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'multiple') %>
|
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'single') %>
|
||||||
<% if can?(:manage, account_config) %>
|
<% if can?(:manage, account_config) %>
|
||||||
<div class="px-1 mt-8 max-w-xl">
|
<div class="px-1 mt-8 max-w-xl">
|
||||||
<div class="flex justify-between items-end mb-4 mt-8">
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
@@ -140,6 +140,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FLATTEN_RESULT_PDF_KEY) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
Remove PDF form fillable fields from the signed PDF (flatten form)
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, { class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' } %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M14 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M4 6l8 0" />
|
||||||
|
<path d="M16 6l4 0" />
|
||||||
|
<path d="M8 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M4 12l2 0" />
|
||||||
|
<path d="M10 12l10 0" />
|
||||||
|
<path d="M17 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M4 18l11 0" />
|
||||||
|
<path d="M19 18l1 0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 602 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M6 9l6 6l6 -6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 312 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0 -18" />
|
||||||
|
<path d="M16 12l-4 -4" />
|
||||||
|
<path d="M16 12h-8" />
|
||||||
|
<path d="M12 16l4 -4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 415 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" aria-hidden="true" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 338 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6.06004 6.00016C6.21678 5.55461 6.52614 5.1789 6.93334 4.93958C7.34055 4.70027 7.8193 4.61279 8.28483 4.69264C8.75035 4.77249 9.17259 5.01451 9.47676 5.37585C9.78093 5.73718 9.94741 6.19451 9.94671 6.66683C9.94671 8.00016 7.94671 8.66683 7.94671 8.66683M8.00004 11.3335H8.00671M14.6667 8.00016C14.6667 11.6821 11.6819 14.6668 8.00004 14.6668C4.31814 14.6668 1.33337 11.6821 1.33337 8.00016C1.33337 4.31826 4.31814 1.3335 8.00004 1.3335C11.6819 1.3335 14.6667 4.31826 14.6667 8.00016Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 730 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M6 6v6a3 3 0 0 0 3 3h10l-4 -4m0 8l4 -4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 337 B |
@@ -0,0 +1,125 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" version="1.1" viewBox="0 0 1000 1000">
|
||||||
|
<defs />
|
||||||
|
<path fill="#039" d="M8,195.25 C178.83,110.03 349.03,140.83 521.26,167.28 C676.47,191.12 833.42,211.85 992,132.75 L992,804.75 C821.17,889.97 650.97,859.17 478.74,832.72 C323.53,808.88 166.58,788.15 8,867.25 L8,195.25" />
|
||||||
|
<path fill="#fc0" d="M500,238.67 L488.74,272.43 L506.07,280.88 L500,238.67" />
|
||||||
|
<path fill="#fc0" d="M500,238.67 L511.26,275.92 L493.93,279 L500,238.67" />
|
||||||
|
<path fill="#fc0" d="M534.66,269.79 L498.22,264.19 L498.22,282.85 L534.66,269.79" />
|
||||||
|
<path fill="#fc0" d="M534.66,269.79 L505.18,287.21 L494.47,270.45 L534.66,269.79" />
|
||||||
|
<path fill="#fc0" d="M521.42,309.51 L510.16,272.27 L492.83,275.36 L521.42,309.51" />
|
||||||
|
<path fill="#fc0" d="M521.42,309.51 L491.94,283.01 L502.65,269.57 L521.42,309.51" />
|
||||||
|
<path fill="#fc0" d="M465.34,259.13 L501.78,264.74 L501.78,283.41 L465.34,259.13" />
|
||||||
|
<path fill="#fc0" d="M465.34,259.13 L494.82,285.61 L505.53,272.16 L465.34,259.13" />
|
||||||
|
<path fill="#fc0" d="M478.58,302.9 L489.84,269.13 L507.17,277.58 L478.58,302.9" />
|
||||||
|
<path fill="#fc0" d="M478.58,302.9 L508.06,285.51 L497.35,268.75 L478.58,302.9" />
|
||||||
|
<path fill="#fc0" d="M500,686.67 L488.74,720.43 L506.07,728.88 L500,686.67" />
|
||||||
|
<path fill="#fc0" d="M500,686.67 L511.26,723.92 L493.93,727 L500,686.67" />
|
||||||
|
<path fill="#fc0" d="M534.66,717.79 L498.22,712.19 L498.22,730.85 L534.66,717.79" />
|
||||||
|
<path fill="#fc0" d="M534.66,717.79 L505.18,735.21 L494.47,718.45 L534.66,717.79" />
|
||||||
|
<path fill="#fc0" d="M521.42,757.51 L510.16,720.27 L492.83,723.36 L521.42,757.51" />
|
||||||
|
<path fill="#fc0" d="M521.42,757.51 L491.94,731.01 L502.65,717.57 L521.42,757.51" />
|
||||||
|
<path fill="#fc0" d="M465.34,707.13 L501.78,712.74 L501.78,731.41 L465.34,707.13" />
|
||||||
|
<path fill="#fc0" d="M465.34,707.13 L494.82,733.61 L505.53,720.16 L465.34,707.13" />
|
||||||
|
<path fill="#fc0" d="M478.58,750.9 L489.84,717.13 L507.17,725.58 L478.58,750.9" />
|
||||||
|
<path fill="#fc0" d="M478.58,750.9 L508.06,733.51 L497.35,716.75 L478.58,750.9" />
|
||||||
|
<path fill="#fc0" d="M281.33,438.24 L270.07,473.54 L287.4,479.69 L281.33,438.24" />
|
||||||
|
<path fill="#fc0" d="M281.33,438.24 L292.6,474.11 L275.27,479.39 L281.33,438.24" />
|
||||||
|
<path fill="#fc0" d="M315.99,465.59 C303.97,464.84 291.94,464.29 279.55,464 L279.55,482.66 L315.99,465.59" />
|
||||||
|
<path fill="#fc0" d="M315.99,465.59 L286.51,486.13 L275.8,470.76 C289.06,468.76 302.33,467.06 315.99,465.59" />
|
||||||
|
<path fill="#fc0" d="M302.76,506.59 L291.49,470.59 L274.16,475.89 L302.76,506.59" />
|
||||||
|
<path fill="#fc0" d="M302.76,506.59 L273.27,483.67 L283.98,468.81 L302.76,506.59" />
|
||||||
|
<path fill="#fc0" d="M246.67,463.91 C258.7,463.69 270.72,463.75 283.12,464.09 L283.12,482.75 C271.09,476.27 259.06,470.03 246.67,463.91" />
|
||||||
|
<path fill="#fc0" d="M246.67,463.91 L276.16,485.87 L286.87,471.04 C273.6,468.37 260.34,465.98 246.67,463.91" />
|
||||||
|
<path fill="#fc0" d="M259.91,505.52 L271.17,470.08 L288.5,476.25 L259.91,505.52" />
|
||||||
|
<path fill="#fc0" d="M259.91,505.52 L289.4,484.08 L278.69,468.67 L259.91,505.52" />
|
||||||
|
<path fill="#fc0" d="M369.24,317.99 L398.73,299.53 L388.02,283.1 L369.24,317.99" />
|
||||||
|
<path fill="#fc0" d="M369.24,317.99 L380.51,283.75 L397.84,291.62 L369.24,317.99" />
|
||||||
|
<path fill="#fc0" d="M356.01,274.87 L385.49,300.04 L396.2,286.25 L356.01,274.87" />
|
||||||
|
<path fill="#fc0" d="M356.01,274.87 L392.45,278.94 L392.45,297.61 L356.01,274.87" />
|
||||||
|
<path fill="#fc0" d="M390.67,252.93 L379.4,287.09 L396.73,294.96 L390.67,252.93" />
|
||||||
|
<path fill="#fc0" d="M390.67,252.93 L401.93,289.85 L384.6,293.47 L390.67,252.93" />
|
||||||
|
<path fill="#fc0" d="M412.09,323.22 L382.6,297.55 L393.31,283.75 L412.09,323.22" />
|
||||||
|
<path fill="#fc0" d="M412.09,323.22 L400.83,286.23 L383.5,289.87 L412.09,323.22" />
|
||||||
|
<path fill="#fc0" d="M425.33,283.29 L395.84,301.31 L385.13,284.9 L425.33,283.29" />
|
||||||
|
<path fill="#fc0" d="M425.33,283.29 L388.88,278.51 L388.88,297.17 L425.33,283.29" />
|
||||||
|
<path fill="#fc0" d="M332.05,396.47 L320.79,360.14 L303.46,364.88 L332.05,396.47" />
|
||||||
|
<path fill="#fc0" d="M332.05,396.47 L302.57,372.63 L313.28,358.12 L332.05,396.47" />
|
||||||
|
<path fill="#fc0" d="M289.21,394.02 L318.69,373.56 L307.98,357.82 L289.21,394.02" />
|
||||||
|
<path fill="#fc0" d="M289.21,394.02 L300.47,358.97 L317.8,365.71 L289.21,394.02" />
|
||||||
|
<path fill="#fc0" d="M275.97,351.92 L305.45,374.93 L316.16,360.45 C302.9,357.36 289.63,354.51 275.97,351.92" />
|
||||||
|
<path fill="#fc0" d="M275.97,351.92 C288,352.16 300.02,352.64 312.41,353.37 L312.41,372.04 L275.97,351.92" />
|
||||||
|
<path fill="#fc0" d="M345.29,355.85 L315.81,375.53 L305.1,359.81 C318.36,358.25 331.62,356.95 345.29,355.85" />
|
||||||
|
<path fill="#fc0" d="M345.29,355.85 L308.85,353.17 L308.85,371.83 L345.29,355.85" />
|
||||||
|
<path fill="#fc0" d="M310.63,327.47 L321.89,363.69 L304.56,368.42 L310.63,327.47" />
|
||||||
|
<path fill="#fc0" d="M310.63,327.47 L299.37,362.4 L316.7,369.12 L310.63,327.47" />
|
||||||
|
<path fill="#fc0" d="M345.29,579.85 L308.85,577.17 L308.85,595.83 L345.29,579.85" />
|
||||||
|
<path fill="#fc0" d="M345.29,579.85 L315.81,599.53 L305.1,583.81 C318.36,582.25 331.62,580.95 345.29,579.85" />
|
||||||
|
<path fill="#fc0" d="M332.05,620.47 L320.79,584.14 L303.46,588.88 L332.05,620.47" />
|
||||||
|
<path fill="#fc0" d="M332.05,620.47 L302.57,596.63 L313.28,582.12 L332.05,620.47" />
|
||||||
|
<path fill="#fc0" d="M289.21,618.02 L318.69,597.56 L307.98,581.82 L289.21,618.02" />
|
||||||
|
<path fill="#fc0" d="M289.21,618.02 L300.47,582.97 L317.8,589.71 L289.21,618.02" />
|
||||||
|
<path fill="#fc0" d="M310.63,551.47 L321.89,587.69 L304.56,592.42 L310.63,551.47" />
|
||||||
|
<path fill="#fc0" d="M310.63,551.47 L299.37,586.4 L316.7,593.12 L310.63,551.47" />
|
||||||
|
<path fill="#fc0" d="M275.97,575.92 C288,576.16 300.02,576.64 312.41,577.37 L312.41,596.04 L275.97,575.92" />
|
||||||
|
<path fill="#fc0" d="M275.97,575.92 L305.45,598.93 L316.16,584.45 C302.9,581.36 289.63,578.51 275.97,575.92" />
|
||||||
|
<path fill="#fc0" d="M425.33,671.27 L388.88,666.49 L388.88,685.15 L425.33,671.27" />
|
||||||
|
<path fill="#fc0" d="M425.33,671.27 L395.84,689.29 L385.13,672.88 L425.33,671.27" />
|
||||||
|
<path fill="#fc0" d="M412.09,711.2 L400.83,674.21 L383.5,677.84 L412.09,711.2" />
|
||||||
|
<path fill="#fc0" d="M412.09,711.2 L382.6,685.53 L393.31,671.73 L412.09,711.2" />
|
||||||
|
<path fill="#fc0" d="M369.24,705.97 L398.73,687.51 L388.02,671.08 L369.24,705.97" />
|
||||||
|
<path fill="#fc0" d="M369.24,705.97 L380.51,671.73 L397.84,679.6 L369.24,705.97" />
|
||||||
|
<path fill="#fc0" d="M390.67,640.91 L401.93,677.83 L384.6,681.45 L390.67,640.91" />
|
||||||
|
<path fill="#fc0" d="M390.67,640.91 L379.4,675.07 L396.73,682.94 L390.67,640.91" />
|
||||||
|
<path fill="#fc0" d="M356.01,662.85 L392.45,666.92 L392.45,685.59 L356.01,662.85" />
|
||||||
|
<path fill="#fc0" d="M356.01,662.85 L385.49,688.02 L396.2,674.23 L356.01,662.85" />
|
||||||
|
<path fill="#fc0" d="M718.67,487.09 L729.93,522.81 L712.6,528.19 L718.67,487.09" />
|
||||||
|
<path fill="#fc0" d="M718.67,487.09 L707.4,522.24 L724.73,528.5 L718.67,487.09" />
|
||||||
|
<path fill="#fc0" d="M684.01,511.33 C696.03,512.09 708.06,512.63 720.45,512.93 L720.45,531.6 L684.01,511.33" />
|
||||||
|
<path fill="#fc0" d="M684.01,511.33 L713.49,534.68 L724.2,519.85 C710.94,517.34 697.67,514.52 684.01,511.33" />
|
||||||
|
<path fill="#fc0" d="M697.24,553.82 L708.51,518.8 L725.84,525.04 L697.24,553.82" />
|
||||||
|
<path fill="#fc0" d="M697.24,553.82 L726.73,532.85 L716.02,517.51 L697.24,553.82" />
|
||||||
|
<path fill="#fc0" d="M753.33,513.02 C741.3,513.24 729.28,513.17 716.88,512.84 L716.88,531.51 C728.91,525.67 740.93,519.59 753.33,513.02" />
|
||||||
|
<path fill="#fc0" d="M753.33,513.02 L723.84,534.95 L713.13,519.57 C726.4,517.73 739.66,515.59 753.33,513.02" />
|
||||||
|
<path fill="#fc0" d="M740.09,554.89 L728.83,519.32 L711.5,524.68 L740.09,554.89" />
|
||||||
|
<path fill="#fc0" d="M740.09,554.89 L710.6,532.44 L721.31,517.64 L740.09,554.89" />
|
||||||
|
<path fill="#fc0" d="M630.76,354.44 L601.27,329.01 L611.98,315.24 L630.76,354.44" />
|
||||||
|
<path fill="#fc0" d="M630.76,354.44 L619.49,317.67 L602.16,321.33 L630.76,354.44" />
|
||||||
|
<path fill="#fc0" d="M643.99,314.07 L614.51,332.79 L603.8,316.38 L643.99,314.07" />
|
||||||
|
<path fill="#fc0" d="M643.99,314.07 L607.55,310 L607.55,328.67 L643.99,314.07" />
|
||||||
|
<path fill="#fc0" d="M609.33,284.42 L620.6,321.27 L603.27,324.95 L609.33,284.42" />
|
||||||
|
<path fill="#fc0" d="M609.33,284.42 L598.07,318.52 L615.4,326.43 L609.33,284.42" />
|
||||||
|
<path fill="#fc0" d="M587.91,349.21 L617.4,330.99 L606.69,314.59 L587.91,349.21" />
|
||||||
|
<path fill="#fc0" d="M587.91,349.21 L599.17,315.18 L616.5,323.09 L587.91,349.21" />
|
||||||
|
<path fill="#fc0" d="M574.67,305.66 L604.16,331.52 L614.87,317.73 L574.67,305.66" />
|
||||||
|
<path fill="#fc0" d="M574.67,305.66 L611.12,310.44 L611.12,329.11 L574.67,305.66" />
|
||||||
|
<path fill="#fc0" d="M667.95,439.93 L679.21,405.25 L696.54,412.05 L667.95,439.93" />
|
||||||
|
<path fill="#fc0" d="M667.95,439.93 L697.43,419.88 L686.72,404.19 L667.95,439.93" />
|
||||||
|
<path fill="#fc0" d="M710.79,442.39 L681.31,418.95 L692.02,404.5 L710.79,442.39" />
|
||||||
|
<path fill="#fc0" d="M710.79,442.39 L699.53,406.42 L682.2,411.22 L710.79,442.39" />
|
||||||
|
<path fill="#fc0" d="M724.03,401.01 L694.55,421.89 L683.84,406.16 C697.1,404.74 710.36,403.06 724.03,401.01" />
|
||||||
|
<path fill="#fc0" d="M724.03,401.01 C712,400.77 699.98,400.28 687.59,399.55 L687.59,418.22 L724.03,401.01" />
|
||||||
|
<path fill="#fc0" d="M654.71,397.08 L684.19,421.29 L694.9,406.8 C681.64,403.85 668.38,400.63 654.71,397.08" />
|
||||||
|
<path fill="#fc0" d="M654.71,397.08 L691.15,399.76 L691.15,418.42 L654.71,397.08" />
|
||||||
|
<path fill="#fc0" d="M689.37,373.86 L678.11,408.65 L695.44,415.47 L689.37,373.86" />
|
||||||
|
<path fill="#fc0" d="M689.37,373.86 L700.63,409.95 L683.3,414.76 L689.37,373.86" />
|
||||||
|
<path fill="#fc0" d="M654.71,621.08 L691.15,623.76 L691.15,642.42 L654.71,621.08" />
|
||||||
|
<path fill="#fc0" d="M654.71,621.08 L684.19,645.29 L694.9,630.8 C681.64,627.85 668.38,624.63 654.71,621.08" />
|
||||||
|
<path fill="#fc0" d="M667.95,663.93 L679.21,629.25 L696.54,636.05 L667.95,663.93" />
|
||||||
|
<path fill="#fc0" d="M667.95,663.93 L697.43,643.88 L686.72,628.19 L667.95,663.93" />
|
||||||
|
<path fill="#fc0" d="M710.79,666.39 L681.31,642.95 L692.02,628.5 L710.79,666.39" />
|
||||||
|
<path fill="#fc0" d="M710.79,666.39 L699.53,630.42 L682.2,635.22 L710.79,666.39" />
|
||||||
|
<path fill="#fc0" d="M689.37,597.86 L678.11,632.65 L695.44,639.47 L689.37,597.86" />
|
||||||
|
<path fill="#fc0" d="M689.37,597.86 L700.63,633.95 L683.3,638.76 L689.37,597.86" />
|
||||||
|
<path fill="#fc0" d="M724.03,625.01 C712,624.77 699.98,624.28 687.59,623.55 L687.59,642.22 L724.03,625.01" />
|
||||||
|
<path fill="#fc0" d="M724.03,625.01 L694.55,645.89 L683.84,630.16 C697.1,628.74 710.36,627.06 724.03,625.01" />
|
||||||
|
<path fill="#fc0" d="M574.67,693.64 L611.12,698.42 L611.12,717.09 L574.67,693.64" />
|
||||||
|
<path fill="#fc0" d="M574.67,693.64 L604.16,719.5 L614.87,705.71 L574.67,693.64" />
|
||||||
|
<path fill="#fc0" d="M587.91,737.19 L599.17,703.16 L616.5,711.07 L587.91,737.19" />
|
||||||
|
<path fill="#fc0" d="M587.91,737.19 L617.4,718.97 L606.69,702.57 L587.91,737.19" />
|
||||||
|
<path fill="#fc0" d="M630.76,742.42 L601.27,716.99 L611.98,703.22 L630.76,742.42" />
|
||||||
|
<path fill="#fc0" d="M630.76,742.42 L619.49,705.65 L602.16,709.31 L630.76,742.42" />
|
||||||
|
<path fill="#fc0" d="M609.33,672.41 L598.07,706.49 L615.4,714.41 L609.33,672.41" />
|
||||||
|
<path fill="#fc0" d="M609.33,672.41 L620.6,709.25 L603.27,712.93 L609.33,672.41" />
|
||||||
|
<path fill="#fc0" d="M643.99,702.05 L607.55,697.98 L607.55,716.65 L643.99,702.05" />
|
||||||
|
<path fill="#fc0" d="M643.99,702.05 L614.51,720.77 L603.8,704.36 L643.99,702.05" />
|
||||||
|
<path fill="#1A1A1A" opacity="0.2" d="M8,195.25 C178.83,110.03 349.03,140.83 521.26,167.28 C676.47,191.12 833.42,211.85 992,132.75 L992,804.75 C821.17,889.97 650.97,859.17 478.74,832.72 C323.53,808.88 166.58,788.15 8,867.25 L8,195.25 M39.25,214.64 L39.25,819.14 C345.81,690.88 650.43,915.18 960.75,785.36 L960.75,180.86 C654.19,309.12 349.57,84.82 39.25,214.64" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
|
||||||
|
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 872 B |